Skip to content

fix(web): charts use full card width on mobile - #1058

Merged
fiddur merged 1 commit into
developfrom
mobile-chart-width
Aug 21, 2026
Merged

fix(web): charts use full card width on mobile#1058
fiddur merged 1 commit into
developfrom
mobile-chart-width

Conversation

@fiddur

@fiddur fiddur commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Fredrik's report (with mobile screenshot): charts on the public feed, public profile, and activity pages don't use the available width on mobile.

Root cause: CombinedMetricChart (shared by all those pages) reserved a fixed CHART_MARGIN.right = 155 — space for its maximum of two right-side y-axes — regardless of how many are actually drawn. In the common feed-card case (one metric, left axis only, zero right axes) a ~360px phone card left the plot ~155px wide, under half the card, and the squeezed x-axis crammed its HH:mm ticks into an overlapping smear.

Fix:

  • Right margin computed per render from the right axes actually drawn: new pure countRightAxes (mirrors drawOverlays' axis-allocation walk exactly, including the axis-less-overlay-claims-left quirk) and chartRightMargin (14px bare edge; 45px per axis + 20px label headroom) in chart-utils.ts, both unit-tested. Single-metric mobile plot gains ~140px.
  • X-tick count clamps to ~60px per label instead of a fixed 6-tick hint, so narrow cards get 3–4 legible ticks.
  • CHART_MARGIN stays exported as the maximum for fetch-width sizing (ActivityChart uses it to pick bucket resolution) — a fetch sized with it is at worst slightly coarser than the drawn width, never too fine.

586 web tests green (17 in chart-utils, 6 new), whole-monorepo check green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CwoP1SqJhHgHiEEoEQtUjT

CombinedMetricChart reserved a fixed 155px right margin for two right
y-axes even when none were drawn — on a ~360px phone card the plot got
~155px (under half the width) in the common single-metric case, with
x-axis ticks overlapping in the squeeze (feed cards, public profile,
activity detail all share the component).

- right margin computed from the right axes actually drawn (new pure
  chartRightMargin/countRightAxes in chart-utils, mirroring
  drawOverlays' allocation walk; unit-tested)
- x-tick count clamped to ~60px per HH:mm label
- CHART_MARGIN stays exported as the maximum for fetch sizing (a fetch
  sized with it is at worst slightly coarser than the drawn width)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwoP1SqJhHgHiEEoEQtUjT
@fiddur
fiddur marked this pull request as ready for review August 21, 2026 18:57

@fiddur fiddur left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Solid, well-scoped fix. I traced countRightAxes against drawOverlays case by case and it is exactly equivalent: axisSide === 'left' iff !leftUsed && !hasHypnogram, the left branch only sets leftUsed, and the right branch increments only when showAxis && rightAxisCount < MAX_RIGHT_AXES — including the quirk that an axis-less first overlay still claims the left slot (covered by the [overlay(false), overlay(true)] test). Moving MAX_RIGHT_AXES into chart-utils.ts introduces no import cycle.

The margin numbers check out against what is actually drawn: with two right axes the outermost tick text ends around innerWidth + 73 and its unit label (text-anchor end) at innerWidth + 80, so 110 has headroom; with one axis ~35px is needed against 65. The 14px bare edge is tight but sufficient — the widest thing right of the plot is a centered HH:mm bottom tick (~12.5px half-width at d3's default 10px) or a terminal sparse diamond (~5px), and .chart-svg-container has overflow: hidden, so this is the one value with no slack. It reads as deliberate rather than accidental.

The CHART_MARGIN-as-maximum contract holds for ActivityChart's bucket sizing: actual marginRight <= 110, so the drawn innerWidth is always >= the measured chartWidthPx and the fetch can only be coarser than the plot, never finer. The tick clamp degrades sanely too (negative or tiny innerWidth floors to 3, wide stays at 6).

Non-blocking — fold into a later PR, don't re-roll this one

  • Axis allocation is now walked in two places. drawOverlays and countRightAxes encode the same rules; a future change to one silently desyncs the margin from the drawing. A single assignAxes(hasHypnogram, overlays) returning per-overlay { axisSide, showAxis, offset } (which drawOverlays consumes and whose right count feeds chartRightMargin) would make drift impossible. The unit tests mitigate this for now, which is why it isn't blocking.
  • Rationale repeated three times. "squeezed the plot to half a phone card's width" appears in the CHART_MARGIN docstring, the inline comment at renderChart, and the chartRightMargin docstring. One of the three (the chartRightMargin docstring, which needs the 45/20/14 breakdown anyway) is enough; the inline comment at the call site mostly restates the code.
  • chartRightMargin(0) === 14 / (1) === 65 / (2) === 110 just restate the formula, so they can't fail independently of it. The countRightAxes cases are the ones carrying real signal.
  • !!hasHypnogram on the new countRightAxes call is redundant — the destructured param is already boolean (pre-existing at the drawOverlays call too).
  • Bigger remaining mobile win, out of scope here: CHART_MARGIN.left = 50 is reserved unconditionally, but with 3+ enabled metrics the first overlay claims the left slot with showAxis: false and no left axis is drawn at all — another ~50px of dead width on a phone card.

@fiddur
fiddur merged commit bfc09f9 into develop Aug 21, 2026
4 checks passed
@fiddur
fiddur deleted the mobile-chart-width branch August 21, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant