bug-reports-landing-charts-sized-for-widget-tile-not-page Reports landing pages render their charts at widget-tile proportions — Distribution donut is too small (lost in whitespace); Created-vs-Resolved line chart is too big (fills viewport width, runs off the bottom)
Type: bug · Parent: Epic 6 · Surfaces: reports landing pages (/reports/distribution — Subtask 6.5.2; /reports/created-vs-resolved — Subtask 6.5.3) · Status: open · Reported by: Yue.
On both full-page report landings under /reports/*, the chart visual is sized as if it were a dashboard widget tile, not a page-level data visualization. The two symptoms look opposite but share one root cause:
- Distribution (
/reports/distribution) — the donut renders at ~170 px wide on a 1400+ px page, floating in a vast empty plot area. The "419 工作项" total + segment legend dominate the small ring; the whole upper half of the page is empty. - Created vs Resolved (
/reports/created-vs-resolved) — the line/area chart stretches to the FULL page width and proportionally grows TALL (the SVG usesblock w-full h-auto, so width scales the viewBox and height comes along). With sparse data (a single spike at the right edge, Y axis up to 500), the chart fills the viewport horizontally AND vertically — the legend / Y-axis label "工作项" sit fine, but the X axis and the rest of the line run below the fold; on a 1280 px page the chart paints at ~1280 × ~600 px, well past a typical laptop fold.
Repro. Sign in as zhuyue@motir.co / !QAZ1qaz, open /reports/distribution on the moooon / motir project — observe the donut is small relative to the page; lots of whitespace above and around the ring (per screenshot 1). Then open /reports/created-vs-resolved and observe the chart fills the page width and the bottom of the plot area is below the fold (per screenshot 2). Resize the window: the donut stays the same pixel size (it's hard-coded), while the line chart scales WITH the window — both directions of wrong.
Root cause. Two distinct sizing strategies in components/ui/charts/, both tuned for the 6.3.3 dashboard-widget tile (~400 px wide column), neither correct for a full-width report page:
DonutChart.tsx— the SVG uses a fixed pixelstyle={{ width: size * 0.71 }}(lines ~101-108) wheresizeis a prop with a default of 220;DistributionReport.tsxpassessize={240}(line 160). That gives the SVG a hardwidth: 170px, independent of the container. So even on a 1400-px-wide report page the donut stays ~170 px — that is the "lost in whitespace" symptom. (preserveAspectRatioandh-autoare present but irrelevant becausewidthis hard-pixel.)ChartFrame.tsx(used byLineChart/BarChart/DifferenceAreaChart) usesclassName="block w-full h-auto"on the SVG withviewBox="0 0 ${width} ${height}"(lines ~85-94). That means the SVG scales responsively to container width, preserving the aspect ratio implied by thewidth/heightprops.CreatedVsResolvedReport.tsxpasseswidth={680} height={320}(ratio ~2.125:1). On a 1280-px page that's ~1280 × ~602 px; on a 1400-px page it's ~1400 × ~659 px — both push the X axis below the fold. Thewidth/heightprops are doing the WRONG job: they read like fixed canvas dimensions but actually only set the aspect ratio.
Both report pages embed the dashboard-widget-tuned chart components directly with no max-width / aspect-ratio override; the chrome (app/(authed)/reports/_components/ReportPageChrome.tsx / page wrappers) inherits the authed-shell padding-only container (app/(authed)/layout.tsx:117, no max-w-*), so the chart inherits the full page width.
Why it survived 6.5.2 / 6.5.3. The chart primitives were originally built for the 6.3.3 dashboard-widget surface where each widget sits in a tile ~400 px wide — and at THAT width both shapes are correct (the 170 px donut sits centered in the tile; the 680 × 320 line chart at w-full of a ~400 px tile renders ~400 × ~188 px, a sensible widget chart). The 6.5.x report-landing subtasks reused them on a full-page surface without giving the chart a bounded container, so a tile-tuned chart paints on a page-tuned canvas. Same mistake on BOTH chart families, opposite-direction symptoms — a strong tell that the chart API doesn't carry the surface (tile vs page) signal it needs.
Fix shapes (decide at fix time — both are mechanical):
- Give the report-landing chart container a bounded width, e.g. wrap each chart in a
max-w-3xl(ormax-w-[56rem]) centered block — the same shape we already use for narrow forms. For the donut, also raise thesizeprop (e.g.size={360}or420) so the ring is presence-sized for a page-level visualization rather than a tile thumbnail. For the line chart, leave the responsivew-fullbehaviour, but the bounded container caps its tall growth (56-rem ≈ 896 px max width × 2.125:1 aspect ≈ 422 px tall — fits one fold). - Add a
surfaceprop (orvariant) to the chart primitives —surface="tile"(default, current behaviour) vssurface="page"(caps the rendered size at a sensible page-level maximum withmax-width/aspect-ratioCSS instead of relying on the consumer to wrap it). This is the durable shape — the chart component owns the responsive contract; consumers pick the surface intent. Option 2 is the cleaner of the two and matches howPill/Card/ other primitives carry size variants already. Either way the fix is contained to the chart primitives + the two report-page wrappers; no data / service / DTO change.
Verify via design. design/reports/ should already specify the page-level donut diameter and the line-chart max width/height (the 6.3.3 widget tile shape and the 6.5.x landing shape are DIFFERENT design surfaces — confirm by listing the area folder and reading design-notes.md before picking a number). If the landing shape was not specified in design/reports/, that is a design-gate miss on the 6.5.x stories — the fix should ADD a type: design subtask first (mirror 1.0.5 / 1.2.1 / 1.3.3 / 1.5.1 for output convention; produce a *.mock.html + design-notes.md), then the code fix follows.
Test gap that let it ship. Existing report-page tests likely assert data rendering (legend totals, table rows) but not chart geometry. The fix MUST add either a render-test or Playwright assertion that asserts the rendered SVG's offsetWidth / offsetHeight (via getBoundingClientRect) fits sensible caps at a 1280-px viewport — e.g. donut diameter between 280–420 px on the distribution page; line chart total height ≤ 480 px on the created-vs-resolved page. Same measurement posture as the Epic-3 swimlane / Epic-6 detail-overflow bugs — measure rendered geometry via getBoundingClientRect, not CSS rules.
Acceptance criteria
- On
/reports/distribution, the donut renders at a page-level size (diameter in the 280–420 px range at typical laptop widths 1280–1440 px), not the 170-px widget-tile size; the ring + center total + legend feel like a primary page visualization, not an afterthought. - On
/reports/created-vs-resolved, the line/area chart fits within one fold at typical laptop widths (1280–1440 × 800–900 px viewport) — total chart height (plus legend + scope/period chrome) ≤ ~480 px so the X axis and plot area are visible without scrolling. - The chart components carry their sizing intent explicitly (a
surfaceprop, or a documented bounded-container contract) so a future report page or widget tile can't accidentally render the wrong size; the dashboard-widget (6.3.3) surfaces continue to render at their existing tile size (guard against regression of the tile shape that was correct). - Both report pages respect the design reference under
design/reports/(if present); if no page-level chart sizing exists there, atype: designsubtask is added first per the design-gate (MOTIR.md § Design-reference rule). - A render-test or Playwright regression asserts the rendered chart geometry fits the caps above at a 1280-px viewport — measured via
getBoundingClientRect, not CSS rules. - AA contrast preserved; data-table fallback (
View data table) still works; next-intl strings unchanged; no service / DTO / route change.
Context refs
components/ui/charts/DonutChart.tsx(lines ~95-145) — the SVG withstyle={{ width: size * 0.71 }}(the hard-pixel-width sizing strategy)components/ui/charts/ChartFrame.tsx(lines ~85-94) — the SVG withblock w-full h-auto+viewBox(the responsive aspect-ratio sizing strategy used byLineChart/BarChart/DifferenceAreaChart)app/(authed)/reports/_components/DistributionReport.tsx(line 160) — passessize={240}to<DonutChart />(the widget-tuned size on a page-level surface)app/(authed)/reports/_components/CreatedVsResolvedReport.tsx(lines ~231-238) — passeswidth={680} height={320}to<DifferenceAreaChart />(the widget-tuned aspect ratio on a page-level surface)app/(authed)/reports/_components/ReportPageChrome.tsx+ the per-reportpage.tsxfiles — where a bounded-width wrapper would land if going with fix shape 1design/reports/(verify the page-level chart spec exists; if not, the fix opens atype: designsubtask first per the design-gate)bug-issue-detail-eyebrow-overflows-viewport(sibling Epic 6 bug) — the recurring "Epic 6 surfaces stretch to the viewport" pattern; this one has the opposite manifestation but the same authed-shell uncapped-width root- 6.3.3 widget tile + 6.5.2 / 6.5.3 report-landing — the two consumer surfaces whose differing requirements the chart API has to encode
motir-core/CLAUDE.md— colour via--el-*, shape via element-shape tokens (applies to whatever wrapper / prop the fix introduces)
Closed (2026-06-13): PR #836 merged. The report-landing charts now size for the page, not the widget tile: the Distribution donut renders at a page-level diameter and the Created-vs-Resolved line/area chart is bounded so it fits within one fold, while the 6.3.3 dashboard-widget tiles keep their existing tile size. The bounded report card uses max-w-[48rem] — the named max-w-3xl resolves to ~40px under motir-core’s @theme (which clears the container scale) and collapsed the layout. Full CI green (sharded Playwright E2E + Vitest). The PR also carried, at Yue’s request, the “issue” → “work item” terminology sweep across the app copy and the E2E selectors that key on the renamed aria-labels.