From 5d3f63e875905c17e172279ddff495a2eaeb7167 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 20 Jun 2026 23:24:57 -0700 Subject: [PATCH] FN-6818: keep Activity line markers circular Use uniform SVG scaling so Command Center Activity line charts preserve proportional geometry.\n\n- Switch the shared hand-rolled LineChart from non-uniform stretching to centered uniform scaling.\n- Cover marker distortion invariants in chart and Activity area tests.\n- Document the Activity chart rendering invariant for desktop and mobile layouts.\n\nFiles changed:\n docs/dashboard-guide.md | 1 +\n .../command-center/__tests__/charts.test.tsx | 22 ++++++++++++++++++++++\n .../command-center/areas/__tests__/areas.test.tsx | 12 ++++++++++++\n .../components/command-center/charts/LineChart.tsx | 5 ++++-\n .../components/command-center/charts/charts.css | 3 +++\n 5 files changed, 42 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-6818 Fusion-Task-Lineage: 304dc64b-688d-42e4-ba7f-c7db16c49de9 --- docs/dashboard-guide.md | 1 + .../command-center/__tests__/charts.test.tsx | 22 +++++++++++++++++++ .../areas/__tests__/areas.test.tsx | 12 ++++++++++ .../command-center/charts/LineChart.tsx | 5 ++++- .../command-center/charts/charts.css | 3 +++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 82948b9c8c..438e9535d8 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -698,6 +698,7 @@ Features: Rendering invariants: - On mobile (`max-width: 768px`), `.cc-tabpanel` remains the sole vertical scroll owner for every chart-bearing tab. Shared chart primitives (`Bar`, `StackedBar`, `Sparkline`, `LineChart`, `RadialGauge`, `Funnel`, `TokenSeriesChart`, and the Command Center recharts wrappers) must shrink within the tabpanel, keep non-zero usable height, avoid stretch/clipping artifacts, and never introduce a competing vertical overflow container. +- The hand-rolled Activity `LineChart` uses uniform SVG scaling so point markers remain true circles and line geometry remains proportional even when the CSS chart box is wide/short on desktop or auto-aspect on mobile. - Mobile chart text must not rely on min-content luck: bar labels, values, token-series axis labels, funnel headers, radial labels, legends, and chart tracks need explicit `min-inline-size: 0`, wrapping, or ellipsis rules so long model/agent/repo labels cannot crush the track or create hidden horizontal overflow in a real browser. - On tablet (`min-width: 769px` and `max-width: 1024px`), `.project-content`, `.command-center`, and `.cc-tabpanel` keep the same definite flex/min-height scroll-owner chain, while the live strip and chart grids collapse before they can create document-level horizontal overflow. - Command Center stat cards, overview chart cards, live strips, table wrappers, Team chart panels, token-series plots, system control cards, and gauge/chart cards share the same tokenized rhythm: `--space-md` gaps/padding for card-like surfaces, `1px solid var(--border-subtle)` borders, `--radius-md` radii, and `--surface-1` backgrounds. Area-specific accents may use `color-mix(...)`, but layout, border, radius, text color, and motion must stay on design tokens, with the named 4px spacing scale (`--space-xs`/`sm`/`md`/`lg`/`xl`/`2xl`) as the canonical vocabulary. diff --git a/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx b/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx index c7989f1fc4..ada5ed788d 100644 --- a/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx @@ -245,6 +245,12 @@ function expectLineChartPointsInsideViewBox(chart: Element): void { } } +function expectLineChartMarkersDistortionProof(chart: Element): void { + expect(chart.getAttribute("preserveAspectRatio")).toBe("xMidYMid meet"); + expect(chart.getAttribute("preserveAspectRatio")).not.toBe("none"); + expect(chart.querySelectorAll(".cc-line-chart-point").length).toBeGreaterThan(0); +} + describe("LineChart", () => { it("renders a populated finite SVG line with an accessible label", () => { render(); @@ -257,6 +263,22 @@ describe("LineChart", () => { expect(points).not.toBe(""); expect(points).not.toMatch(/NaN|Infinity/); expectLineChartPointsInsideViewBox(chart); + expectLineChartMarkersDistortionProof(chart); + }); + + it("uses uniform SVG scaling so marker circles cannot stretch into ovals", () => { + render(); + + const chart = screen.getByRole("img", { name: "mobile activity trend" }); + expectLineChartMarkersDistortionProof(chart); + }); + + it("keeps mobile sizing non-square while SVG geometry remains uniformly scaled", () => { + expect(chartCss).toMatch(/@media \(max-width: 768px\)\s*\{[^@]*\.cc-line-chart\s*\{[^}]*aspect-ratio:\s*auto;/s); + + render(); + + expectLineChartMarkersDistortionProof(screen.getByRole("img", { name: "narrow activity trend" })); }); it("renders all-zero values as valid baseline geometry without NaN or edge clipping", () => { diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx index 88e8772d65..8c2d202913 100644 --- a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx +++ b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx @@ -296,6 +296,14 @@ function expectSvgLinePointsInsideViewBox(testId: string, label: string): void { } } +function expectSvgLineMarkersUndistorted(testId: string, label: string): void { + const section = screen.getByTestId(testId); + const chart = within(section).getByRole("img", { name: label }); + expect(chart.getAttribute("preserveAspectRatio")).toBe("xMidYMid meet"); + expect(chart.getAttribute("preserveAspectRatio")).not.toBe("none"); + expect(chart.querySelectorAll(".cc-line-chart-point").length).toBeGreaterThan(0); +} + describe("useAnalyticsArea", () => { it("polls only when pollMs is provided and clears the interval on unmount", async () => { vi.useFakeTimers(); @@ -390,9 +398,13 @@ describe("ActivityArea", () => { expect(screen.getByRole("img", { name: "Activity trend" })).toHaveAttribute("data-scale-mode", "series"); expectRechartsWrapperWithin("cc-activity-pie", "Agent run outcome share"); expectSvgLinePointsInsideViewBox("cc-activity-line-messages", "Messages / day"); + expectSvgLineMarkersUndistorted("cc-activity-line-messages", "Messages / day"); expectSvgLinePointsInsideViewBox("cc-activity-line-agents", "Active agents / day"); + expectSvgLineMarkersUndistorted("cc-activity-line-agents", "Active agents / day"); expectSvgLinePointsInsideViewBox("cc-activity-line-nodes", "Active nodes / day"); + expectSvgLineMarkersUndistorted("cc-activity-line-nodes", "Active nodes / day"); expectSvgLinePointsInsideViewBox("cc-activity-line-throughput", "Throughput / day"); + expectSvgLineMarkersUndistorted("cc-activity-line-throughput", "Throughput / day"); expect(within(screen.getByTestId("cc-activity-agent-runs-sparkline")).getByRole("img", { name: "Agent runs / day" }).classList).toContain("cc-sparkline"); expectSparklineHeightsFinite("cc-activity-agent-runs-sparkline"); }); diff --git a/packages/dashboard/app/components/command-center/charts/LineChart.tsx b/packages/dashboard/app/components/command-center/charts/LineChart.tsx index cb1ba5c908..5748bef915 100644 --- a/packages/dashboard/app/components/command-center/charts/LineChart.tsx +++ b/packages/dashboard/app/components/command-center/charts/LineChart.tsx @@ -68,6 +68,9 @@ function computedMaxFor(series: LineChartSeries[], max?: number): number { * * FNXC:CommandCenterCharts 2026-06-19-05:24: * Activity line charts were clipping max/min points because the data domain mapped to the full SVG viewBox edge. Reserve plot padding equal to the rendered point/stroke margin so populated, single-point, zero, and max-value series stay inside the viewBox on desktop and mobile. + * + * FNXC:CommandCenterCharts 2026-06-20-22:59: + * FN-6818 requires Activity markers to render as true circles independent of the chart container's aspect ratio. `preserveAspectRatio="none"` on the square viewBox stretched circle coordinates into ovals on narrow/mobile layouts; `vectorEffect="non-scaling-stroke"` only protects stroke width, not coordinate geometry, so the SVG must use uniform scaling. */ export function LineChart({ series, ariaLabel, max }: LineChartProps) { const computedMax = computedMaxFor(series, max); @@ -78,7 +81,7 @@ export function LineChart({ series, ariaLabel, max }: LineChartProps) { role="img" aria-label={ariaLabel} viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`} - preserveAspectRatio="none" + preserveAspectRatio="xMidYMid meet" > {series.map((entry, seriesIndex) => { const points = pointsFor(entry.values, computedMax); diff --git a/packages/dashboard/app/components/command-center/charts/charts.css b/packages/dashboard/app/components/command-center/charts/charts.css index 252919e709..25b0eaedee 100644 --- a/packages/dashboard/app/components/command-center/charts/charts.css +++ b/packages/dashboard/app/components/command-center/charts/charts.css @@ -348,6 +348,9 @@ The token-over-time chart is live-updated and animated, but the motion is decora /* FNXC:CommandCenterStyling 2026-06-18-14:29: Line-chart motion is decorative, token-timed, and disabled for reduced-motion users; sizing and stroke colors stay on design tokens so the Activity area remains readable across desktop and mobile without chart-specific hardcoded colors or lengths. + +FNXC:CommandCenterCharts 2026-06-20-22:59: +FN-6818 keeps the CSS box wide/short for the Activity layout while the SVG now uses uniform scaling for geometry. Do not switch the square viewBox back to non-uniform scaling to fill this box: that made point markers render as ovals on mobile, and non-scaling stroke was insufficient because it did not protect circle coordinates. */ .cc-line-chart { display: block;