fix(web): charts use full card width on mobile - #1058
Conversation
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
left a comment
There was a problem hiding this comment.
✅ 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.
drawOverlaysandcountRightAxesencode the same rules; a future change to one silently desyncs the margin from the drawing. A singleassignAxes(hasHypnogram, overlays)returning per-overlay{ axisSide, showAxis, offset }(whichdrawOverlaysconsumes and whose right count feedschartRightMargin) 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_MARGINdocstring, the inline comment atrenderChart, and thechartRightMargindocstring. One of the three (thechartRightMargindocstring, 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) === 110just restate the formula, so they can't fail independently of it. ThecountRightAxescases are the ones carrying real signal.!!hasHypnogramon the newcountRightAxescall is redundant — the destructured param is alreadyboolean(pre-existing at thedrawOverlayscall too).- Bigger remaining mobile win, out of scope here:
CHART_MARGIN.left = 50is reserved unconditionally, but with 3+ enabled metrics the first overlay claims the left slot withshowAxis: falseand no left axis is drawn at all — another ~50px of dead width on a phone card.
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 fixedCHART_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 itsHH:mmticks into an overlapping smear.Fix:
countRightAxes(mirrorsdrawOverlays' axis-allocation walk exactly, including the axis-less-overlay-claims-left quirk) andchartRightMargin(14px bare edge; 45px per axis + 20px label headroom) inchart-utils.ts, both unit-tested. Single-metric mobile plot gains ~140px.CHART_MARGINstays exported as the maximum for fetch-width sizing (ActivityChartuses 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