diff --git a/src/utils/chartPriceAxis.ts b/src/utils/chartPriceAxis.ts new file mode 100644 index 0000000..8520f43 --- /dev/null +++ b/src/utils/chartPriceAxis.ts @@ -0,0 +1,61 @@ +type ChartPoint = [number, number] + +/** ضریب بزرگ‌نمایی بصری نوسان — فقط مختصات نمودار؛ tooltip قیمت واقعی را نشان می‌دهد */ +const KATOD_CHART_DEVIATION_FACTOR = 3 + +/** نمودار boursecu وقتی metal_type = کاتد مس — محور Y فشرده تا نوسان کم هم دیده شود */ +export function shouldAmplifyKatodMesChart(options: { + isBoursecu: boolean + metalTypeSetting?: string | number | null +}): boolean { + if (!options.isBoursecu) { + return false + } + + const metal = String(options.metalTypeSetting ?? '').trim() + return metal === 'کاتد مس' +} + +export function computeTightChartYBounds(prices: number[]): { min?: number; max?: number } { + const values = prices.filter((value) => Number.isFinite(value) && value > 0) + if (values.length < 2) { + return {} + } + + const dataMin = Math.min(...values) + const dataMax = Math.max(...values) + const mid = (dataMin + dataMax) / 2 + let span = dataMax - dataMin + + // حداقل بازه عمودی: ~۰٫۵٪ قیمت میانی (یا ۸۰هزار ریال) تا خط خیلی صاف نباشد + const minSpan = Math.max(mid * 0.005, 80_000) + if (span < minSpan) { + span = minSpan + } + + const padding = span * 0.08 + const center = (dataMin + dataMax) / 2 + + return { + min: Math.floor(center - span / 2 - padding), + max: Math.ceil(center + span / 2 + padding), + } +} + +/** بزرگ‌نمایی انحراف از میانگین — خط نمودار پرتحرک‌تر می‌شود بدون تغییر قیمت واقعی در tooltip */ +export function amplifyChartDeviation( + points: ChartPoint[], + factor = KATOD_CHART_DEVIATION_FACTOR +): ChartPoint[] { + if (points.length < 2) { + return points + } + + const values = points.map((point) => point[1]) + const mean = values.reduce((sum, value) => sum + value, 0) / values.length + + return points.map(([timestamp, value]) => [ + timestamp, + Math.round(mean + (value - mean) * factor), + ]) +} diff --git a/src/views/base/home/components/PriceChart.vue b/src/views/base/home/components/PriceChart.vue index 87da73d..9c4659e 100644 --- a/src/views/base/home/components/PriceChart.vue +++ b/src/views/base/home/components/PriceChart.vue @@ -39,7 +39,7 @@ width="100%" :height="chartHeight" :options="chartOptions" - :series="props.priceChartSeries" + :series="displayChartSeries" /> @@ -51,6 +51,8 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue' import { useConfigStore } from '@/store/configStore' import { usePrice } from '@/utils/usePrice' import { getProjectFontFamily } from '@/utils/config' +import { computeTightChartYBounds, amplifyChartDeviation, shouldAmplifyKatodMesChart } from '@/utils/chartPriceAxis' +import { useSettingsStore } from '@/store/settingsStore' import { useElementSize } from '@vueuse/core' import type { ChartMetalType } from '../composables/usePriceChart' @@ -90,6 +92,7 @@ const emit = defineEmits<{ }>() const config = useConfigStore() +const settingsStore = useSettingsStore() const chartContainerRef = ref(null) const chartMounted = ref(false) const selectedValue = ref(props.chartSelection) @@ -97,6 +100,37 @@ const selectedValue = ref(props.chartSelection) const { width: chartWidth } = useElementSize(chartContainerRef) const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0) +const amplifyKatodChart = computed(() => + shouldAmplifyKatodMesChart({ + isBoursecu: props.layout === 'boursecu', + metalTypeSetting: settingsStore.metalTypeSettingValue, + }) +) +const displayChartSeries = computed(() => { + const baseSeries = props.priceChartSeries?.[0] + if (!baseSeries?.data?.length || !amplifyKatodChart.value) { + return props.priceChartSeries + } + + return [ + { + ...baseSeries, + data: amplifyChartDeviation(baseSeries.data as [number, number][]), + }, + ] +}) +const chartYBounds = computed(() => { + if (!amplifyKatodChart.value) { + return {} + } + + const prices = + displayChartSeries.value?.[0]?.data + ?.map((point: [number, number]) => Number(point?.[1])) + .filter((value: number) => Number.isFinite(value)) ?? [] + + return computeTightChartYBounds(prices) +}) const canRenderChart = computed( () => chartMounted.value && chartWidth.value > 0 && seriesLength.value > 0 ) @@ -115,6 +149,7 @@ onMounted(async () => { const chartOptions = computed(() => { const fontFamily = getProjectFontFamily() + const yBounds = chartYBounds.value return { chart: { @@ -122,14 +157,17 @@ const chartOptions = computed(() => { type: 'area', height: 200, toolbar: { show: false }, - zoom: { autoScaleYaxis: true }, + zoom: { autoScaleYaxis: !amplifyKatodChart.value }, redrawOnParentResize: true, }, grid: { padding: { top: 0, right: 0, bottom: 0, left: 0 }, yaxis: { lines: { show: false } }, }, - stroke: { width: 2 }, + stroke: { + width: amplifyKatodChart.value ? 2.5 : 2, + curve: amplifyKatodChart.value ? 'smooth' : 'straight', + }, dataLabels: { enabled: false, style: { @@ -139,6 +177,8 @@ const chartOptions = computed(() => { yaxis: { tickAmount: 8, show: false, + min: yBounds.min, + max: yBounds.max, labels: { formatter: (val: number) => usePrice(val, true, 'ه.ریال'), style: { fontSize: '13px', fontFamily }, @@ -161,7 +201,32 @@ const chartOptions = computed(() => { })}` }, }, - formatter: (val: number) => usePrice(val, true, 'ه.ریال'), + ...(amplifyKatodChart.value + ? { + custom: ({ dataPointIndex, w }: { dataPointIndex: number; w: { globals: { seriesX: number[][] } } }) => { + const timestamp = w.globals.seriesX[0]?.[dataPointIndex] + const realPrice = props.priceChartSeries?.[0]?.data?.[dataPointIndex]?.[1] + if (timestamp == null || realPrice == null) { + return '' + } + + const date = new Date(timestamp) + const timeLabel = `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', { + hour: '2-digit', + minute: '2-digit', + })}` + + return `
${timeLabel}
+
+ + ${usePrice(realPrice, true, 'ه.ریال')} + +
` + }, + } + : { + formatter: (val: number) => usePrice(val, true, 'ه.ریال'), + }), style: { fontFamily }, }, colors: [config.config.color],