From 2cb36c5bbf1716c172d59c95879782324d262b0a Mon Sep 17 00:00:00 2001 From: Orion Date: Fri, 10 Jul 2026 15:35:03 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E5=B9=B3=E6=BB=91=E8=B6=8B?= =?UTF-8?q?=E5=8A=BF=E5=9B=BE=E7=BC=A9=E6=94=BE=E4=B8=8E=E5=88=BB=E5=BA=A6?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/SmoothTrendCanvas.vue | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/components/common/SmoothTrendCanvas.vue b/src/components/common/SmoothTrendCanvas.vue index 3b4275e..8c4c96a 100644 --- a/src/components/common/SmoothTrendCanvas.vue +++ b/src/components/common/SmoothTrendCanvas.vue @@ -69,6 +69,11 @@ type ChartPoint = { row: AnalyticsDailyRow } +type ScaleVisual = { + max: number + ticks: number[] +} + const canvasRef = ref(null) const offset = ref(0) const windowSize = ref(30) @@ -81,6 +86,8 @@ const inertiaRafId = ref(null) const lastInertiaTime = ref(0) const lastHoverTime = ref(0) const tooltip = ref(null) +const scaleVisual = ref(null) +const lastScaleTime = ref(0) const INERTIA_REFERENCE_FRAME_MS = 1000 / 120 const INERTIA_MAX_FRAME_MS = 1000 / 30 @@ -88,6 +95,8 @@ const INERTIA_DECAY_PER_120HZ_FRAME = 0.96 const INERTIA_STOP_VELOCITY = 0.01 const HOVER_EASE_PER_120HZ_FRAME = 0.34 const HOVER_SNAP_DISTANCE = 0.2 +const SCALE_EASE_PER_120HZ_FRAME = 0.2 +const SCALE_SNAP_DISTANCE = 0.01 const getYAxisTicks = (rawMax: number) => { const roundedMax = Math.max(1, Math.ceil(rawMax)) if (roundedMax <= 4) { @@ -99,6 +108,11 @@ const getYAxisTicks = (rawMax: number) => { return Array.from({ length: scaleMax / step + 1 }, (_, index) => scaleMax - index * step) } +const formatAxisTick = (value: number) => { + const rounded = Math.round(value) + return Math.abs(value - rounded) < 0.01 ? String(rounded) : value.toFixed(1) +} + const panelClass = computed(() => props.isDarkMode ? 'border-[#40545c] bg-[linear-gradient(135deg,rgba(34,49,56,0.96),rgba(24,35,42,0.92))] shadow-md shadow-black/25' @@ -177,8 +191,25 @@ const draw = (timestamp = performance.now()) => { 0, ...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)]) ) - const yTicks = getYAxisTicks(rawMax) - const max = yTicks[0] + const targetTicks = getYAxisTicks(rawMax) + const targetMax = targetTicks[0] + const elapsedScale = lastScaleTime.value + ? Math.min(timestamp - lastScaleTime.value, INERTIA_MAX_FRAME_MS) + : INERTIA_REFERENCE_FRAME_MS + lastScaleTime.value = timestamp + if (!scaleVisual.value) { + scaleVisual.value = { max: targetMax, ticks: targetTicks } + } else { + const ease = 1 - Math.pow(1 - SCALE_EASE_PER_120HZ_FRAME, elapsedScale / INERTIA_REFERENCE_FRAME_MS) + const nextMax = scaleVisual.value.max + (targetMax - scaleVisual.value.max) * ease + scaleVisual.value = { + max: Math.abs(nextMax - targetMax) < SCALE_SNAP_DISTANCE ? targetMax : nextMax, + ticks: targetTicks + } + } + const max = scaleVisual.value.max + const yTicks = targetTicks.map((tick) => (tick * max) / targetMax) + const isScaleSettling = Math.abs(max - targetMax) >= SCALE_SNAP_DISTANCE const muted = props.isDarkMode ? '#9ca3af' : '#6b7280' const border = props.isDarkMode ? '#374151' : '#e5e7eb' const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3' @@ -195,8 +226,9 @@ const draw = (timestamp = performance.now()) => { ctx.moveTo(padLeft, y) ctx.lineTo(padLeft + width, y) ctx.stroke() - ctx.fillText(String(tick), 4, y + 4) + ctx.fillText(formatAxisTick(tick), 4, y + 4) }) + if (isScaleSettling) scheduleDraw() const denominator = Math.max(1, windowSize.value - 1) const plotLeft = padLeft