diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index 1108c89..cb5ffa3 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -1,5 +1,5 @@ import { useConfirmation } from '../components/ConfirmationDialog'; -import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { @@ -1144,7 +1144,7 @@ function UsageTrend({ const [hoveredRatio, setHoveredRatio] = useState(null); const [hiddenModels, setHiddenModels] = useState([]); const plotRef = useRef(null); - const [plotWidth, setPlotWidth] = useState(800); + const [plotWidth, setPlotWidth] = useState(0); const series = useMemo( () => buildUsageTrendSeries(points, range), @@ -1164,12 +1164,29 @@ function UsageTrend({ const count = series.points.length; - useEffect(() => { + useLayoutEffect(() => { const plot = plotRef.current; if (!plot) return; - const observer = new ResizeObserver(([entry]) => setPlotWidth(entry.contentRect.width)); - observer.observe(plot); - return () => observer.disconnect(); + let frame = 0; + const updateWidth = (width: number) => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => { + const next = Math.max(0, Math.round(width)); + setPlotWidth((current) => current === next ? current : next); + }); + }; + const measure = () => updateWidth(plot.getBoundingClientRect().width); + measure(); + const observer = typeof ResizeObserver === 'undefined' + ? null + : new ResizeObserver(([entry]) => updateWidth(entry.contentRect.width)); + observer?.observe(plot); + window.addEventListener('resize', measure); + return () => { + cancelAnimationFrame(frame); + observer?.disconnect(); + window.removeEventListener('resize', measure); + }; }, [count > 0]); useEffect(() => { diff --git a/src/services/usageTrend.ts b/src/services/usageTrend.ts index 7845787..7323441 100644 --- a/src/services/usageTrend.ts +++ b/src/services/usageTrend.ts @@ -265,7 +265,36 @@ export function trendTimeAxisTicks(start: Date, end: Date, width: number, labelW if (span <= 0) return [start]; // Choose a readable time interval from the selected duration and available space. const minStep = span * labelWidth / Math.max(labelWidth, width); - const steps = [1 / 60, 5 / 60, 15 / 60, 0.5, 1, 2, 3, 6, 12, 24, 48, 72, 168, 336, 720, 2160, 4380, 8760]; + const steps = [ + 1 / 60, + 2 / 60, + 5 / 60, + 10 / 60, + 15 / 60, + 20 / 60, + 0.5, + 0.75, + 1, + 2, + 3, + 4, + 6, + 8, + 12, + 24, + 48, + 72, + 96, + 168, + 336, + 720, + 1440, + 2160, + 2880, + 4380, + 8760, + 17520, + ]; const step = steps.map((hours) => hours * HOUR_MS).find((value) => value >= minStep) ?? niceCeiling(minStep / (8760 * HOUR_MS)) * 8760 * HOUR_MS; const ticks = [start]; diff --git a/tests/usage-layout-ui.cjs b/tests/usage-layout-ui.cjs index b507077..1f92d7a 100644 --- a/tests/usage-layout-ui.cjs +++ b/tests/usage-layout-ui.cjs @@ -68,7 +68,24 @@ const base = 'http://127.0.0.1:1421'; return axis.getBoundingClientRect().bottom <= layout.getBoundingClientRect().bottom + 1; }), 'The complete X axis is reachable by scrolling the overview'); - console.log('PASS: 1213px single row stat cards and constrained wrapped overview scrolling passed.'); + const narrowAxis = await page.evaluate(() => ({ + width: document.querySelector('.usage-trend-plot')?.getBoundingClientRect().width ?? 0, + ticks: document.querySelectorAll('.usage-trend-x-axis span').length, + })); + await page.setViewportSize({ width: 1500, height: 600 }); + await page.waitForFunction((previous) => { + const plot = document.querySelector('.usage-trend-plot'); + const ticks = document.querySelectorAll('.usage-trend-x-axis span').length; + return !!plot && plot.getBoundingClientRect().width > previous.width && ticks > previous.ticks; + }, narrowAxis); + const wideAxis = await page.evaluate(() => ({ + width: document.querySelector('.usage-trend-plot')?.getBoundingClientRect().width ?? 0, + ticks: document.querySelectorAll('.usage-trend-x-axis span').length, + })); + assert.ok(wideAxis.width > narrowAxis.width, 'The trend plot follows the wider window'); + assert.ok(wideAxis.ticks > narrowAxis.ticks, 'The X axis adds readable ticks when more width is available'); + + console.log('PASS: usage overview layout and responsive trend axis passed.'); } finally { await browser.close(); } diff --git a/tests/usageTrend.test.ts b/tests/usageTrend.test.ts index 49c0061..f4d291a 100644 --- a/tests/usageTrend.test.ts +++ b/tests/usageTrend.test.ts @@ -254,6 +254,17 @@ describe('usage trend helpers', () => { expect(months.every((date) => date.getDate() === 1)).toBe(true); }); + test('increases time tick density progressively as the chart stretches', () => { + const start = new Date(2026, 8, 14, 0, 0); + const end = new Date(2026, 8, 15, 0, 0); + const widths = [320, 480, 640, 800, 1000, 1400]; + const counts = widths.map((width) => trendTimeAxisTicks(start, end, width, 112).length); + + expect(counts.every((count, index) => index === 0 || count >= counts[index - 1])).toBe(true); + expect(new Set(counts).size).toBeGreaterThanOrEqual(4); + expect(counts.at(-1)).toBeGreaterThan(counts[0]); + }); + test('stacks model tokens and groups overflow models', () => { const start = new Date(2026, 8, 14, 10, 0, 0); const end = new Date(2026, 8, 14, 12, 0, 0);