Show the city-wide field as a band, not a wall of names - #85
Conversation
/issues put every respondent's name under every question: thirty-odd plates filed into three or four blocks, two dozen times over. That is several hundred names on one page, none of them a ballot line any single reader votes on, and all of them standing between the reader and the thing the page exists to show — where the field agrees, and where it splits. So the city-wide cards now lead with the split. Each question draws as OptionBar, the charts package's 100% band, with the options in full underneath and their counts and shares beside them. The names are not gone: hovering a segment opens the candidates who gave that answer, each with the seat they are running for, so the roll call behind a share is one gesture away rather than printed thirty-three times. A band rather than the donut this was first drafted as. The work happens across thirty-odd cards at once, and a share read as a length against a common left edge compares between cards where an angle does not — and a good many of these questions are a straight Yes/No, which is the case a pie serves worst. The ward and mayoral pages keep the roll call. Their fields are four or five people, where the names ARE the comparison the reader came for. `QuestionnaireCards` picks between the two on a `chart` flag. Also here: - OptionBar grows optional segment pointer handlers. Leaving is reported from the bar as a whole rather than per segment: the segments sit a pixel apart to keep their joins visible, so per-segment handlers would fire leave-then-enter at every join and blink whatever the caller is driving. - The per-question denominator moves to the card foot and now prints on every card. Every share is against the people who answered THAT question, which is not the same number card to card, and two thirds of six is not two thirds of fifty-eight. - A hairline between candidates on the ward and mayoral cards, where several have written notes. Stacked, a plate and a paragraph and a plate and a paragraph run together into one column of prose with names in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile SummaryThe PR replaces city-wide questionnaire roll calls with proportional answer bands while retaining candidate details through an interactive disclosure. Follow-up changes make the SVG responsive without relying on client measurement and bound long candidate panels with scrolling.
Confidence Score: 5/5The PR appears safe to merge, with a non-blocking interaction issue in the scrollable candidate panel. The responsive bar and bounded overlay fixes address the two resolved previous findings. The candidate panel now closes on any click despite being scrollable, which makes some long-list interactions awkward but does not block the primary chart experience. The existing unanswered-versus-returned context remains hidden, as the current card still derives and presents only the answered total. Files Needing Attention: src/components/elections/QuestionSplitFigure.tsx
|
| Filename | Overview |
|---|---|
| src/app/toronto/vote/2026/issues/page.tsx | Enables chart cards for the city-wide issues view and updates explanatory copy. |
| src/components/charts/trilemma/OptionBar.tsx | Adds optional segment pointer callbacks and a responsive SVG view box. |
| src/components/elections/QuestionSplit.tsx | Converts grouped questionnaire answers into proportional slices and candidate lists. |
| src/components/elections/QuestionSplitFigure.tsx | Implements the interactive band, legend, and bounded names panel, but panel-wide click dismissal conflicts with its scrolling behavior. |
| src/components/elections/QuestionnaireCards.tsx | Selects between roll-call and chart card presentations through an additive flag. |
| src/components/elections/QuestionRollCall.tsx | Adds separators between candidate entries containing written responses. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
Page[City-wide issues page] --> Cards[QuestionnaireCards chart mode]
Cards --> Split[QuestionSplit]
Split --> Band[OptionBar answer-share band]
Split --> Legend[Focusable answer legend]
Band -->|Hover segment| Names[Candidate names panel]
Legend -->|Focus or select| Names
Names --> Seats[Candidate and ballot-seat details]
Prompt To Fix All With AI
### Issue 1
src/components/elections/QuestionSplitFigure.tsx:287
**Panel Clicks Close Scrolling**
The names panel is now scrollable for long candidate lists, but every click inside it calls `onDismiss`. Trying to drag the scrollbar or clicking within the list while reading closes the panel instead of allowing continued inspection. Limit dismissal to a dedicated control or backdrop rather than the scroll container itself.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (4): Last reviewed commit: "Make the band scale, and bound the names..." | Re-trigger Greptile
The ward and mayoral cards title each block with the wording the candidates were shown — serif, medium, the biggest thing inside the block. The legend on /issues carried the same sentence at 1.02rem regular, so the one piece of the card holding what somebody actually endorsed read as a caption, level with the names in the hover panel and the note at the foot. Same treatment as those cards now, a step down in size because a legend row is not a panel title. An answer reads as an answer wherever in the tracker you meet it. And ruled between rows rather than spaced. The wordings are full sentences and the long ones wrap, so four of them separated by 8px read as one block of prose with numbers down the side: where one answer stopped and the next began was something the reader worked out from the text rather than saw. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every card carried a line at its foot saying how many candidates had answered that particular question. It is a sentence of bookkeeping under all thirty-three cards, read by a reader who came for the shape of the split. Not lost with it: the legend prints a count beside every option, so the denominator is those added up. The method note at the foot of the page says so once, in the one place a reader goes looking for how to read the cards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A name in the panel is scanned rather than read — the reader is looking for one they know, or counting how many of a segment they recognise — and forty of them at 0.92rem is a block that stops sitting under the chart and starts being the card. 0.84rem, with a touch more leading to keep the columns readable at the smaller size. It also buys the columns back some width, which is what keeps a long name on one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| /** the id the scroll rail scrolls to — see QuestionRollCall */ | ||
| headingId?: string; | ||
| }) { | ||
| const { groups, verbatim } = rollCall(question); |
There was a problem hiding this comment.
Removing the card footer also discards rollCall's unanswered data. When candidates skip a question, the legend shows only per-option counts and shares. Readers can add those counts to recover how many candidates answered, but they cannot tell how many respondents omitted the question. This makes the practical difference between broad participation and heavy nonresponse invisible when interpreting similar shares. Please preserve the answered-versus-returned count or provide equivalent nonresponse context.
Knowledge Base Used: Municipal election engagement
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/elections/QuestionSplit.tsx
Line: 65
Comment:
**Nonresponse Context Is Hidden**
Removing the card footer also discards `rollCall`'s `unanswered` data. When candidates skip a question, the legend shows only per-option counts and shares. Readers can add those counts to recover how many candidates answered, but they cannot tell how many respondents omitted the question. This makes the practical difference between broad participation and heavy nonresponse invisible when interpreting similar shares. Please preserve the answered-versus-returned count or provide equivalent nonresponse context.
**Knowledge Base Used:** [Municipal election engagement](https://app.greptile.com/buildcanada/-/custom-context/knowledge-base/buildcanada/tradingpost/-/docs/municipal-elections.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Two things Greptile caught on the PR, both real. OptionBar drew at a fixed pixel width. `responsive` measures with a ResizeObserver, which does not run on a server and does not run at all where scripting is off, so until it reported — or for good, without JS — the bar was laid out at whatever width the caller asked for, overflowed a narrower card and was clipped by the page's hidden overflow. The 560px fallback I had picked was treating the symptom: it was a guess about the reader's screen, and wrong on a phone. It has a viewBox now and scales into its box, so the fallback width is a unit system rather than a promise. `preserveAspectRatio="none"` keeps the band's stated height while the width takes up the slack; a bar that shortened itself on a phone to hold its ratio would be a different chart. Once the observer reports, the viewBox and the box agree and the scale factor is 1, so segments and any type in them stay pixel-true. The names panel had no height cap — I dropped it when the panel went pointer-events-none, since a panel that cannot be scrolled gains nothing from a scrollbar. But an overlay in nobody's layout will happily run over the card below it, and a forty-name segment at two columns on a phone is twenty rows. Capped at 14rem and scrolling. Scrolling means taking the pointer again, which is safe now: the flicker that forced pointer-events-none came from the panel covering the legend rows back when THEY opened on hover. Hover lives on the band, which the panel starts below and never covers. It does still bury the row you would click a second time to release a latched panel, so clicking the panel closes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| /* The legend row that would let a latched panel go is underneath this, | ||
| so the panel itself is the way out. Harmless on a panel opened by | ||
| hover, which the pointer never reaches. */ | ||
| onClick={onDismiss} |
There was a problem hiding this comment.
The names panel is now scrollable for long candidate lists, but every click inside it calls onDismiss. Trying to drag the scrollbar or clicking within the list while reading closes the panel instead of allowing continued inspection. Limit dismissal to a dedicated control or backdrop rather than the scroll container itself.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/elections/QuestionSplitFigure.tsx
Line: 287
Comment:
**Panel Clicks Close Scrolling**
The names panel is now scrollable for long candidate lists, but every click inside it calls `onDismiss`. Trying to drag the scrollbar or clicking within the list while reading closes the panel instead of allowing continued inspection. Limit dismissal to a dedicated control or backdrop rather than the scroll container itself.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
What
/toronto/vote/2026/issuesput every respondent's name under every question — thirty-odd plates filed into three or four blocks, two dozen times over. Several hundred names on one page, none of them a ballot line any single reader votes on, and all of them standing between the reader and the thing the page exists to show: where the field agrees and where it splits.The city-wide cards now lead with the split. Each question draws as
OptionBar, the charts package's 100% band, with the options in full underneath and their counts and shares beside them.The names are not gone — hovering a segment opens the candidates who gave that answer, each with the seat they are running for. The roll call behind a share is one gesture away instead of printed thirty-three times.
The ward and mayoral pages keep the roll call. Their fields are four or five people, where the names are the comparison the reader came for.
QuestionnaireCardspicks between the two forms on achartflag.Why a band and not a pie
This was first drafted as a donut. Two reasons it isn't one:
Worth noting
FieldSentiment.tsxalready carries the same argument in a comment from an earlier round.Also in here
OptionBargrows optional segment pointer handlers (onSegmentEnter,onSegmentLeave). Additive; nothing changes for callers that don't pass them. Leaving is reported from the bar as a whole rather than per segment — the segments sit a pixel apart to keep their joins visible, so per-segment handlers would fire leave-then-enter at every join and blink whatever the caller is driving.Accessibility
The legend rows stay focusable buttons even though hover now lives on the band: a 7% segment is ~20px across on a phone, which is not a tap target, and no SVG segment is reachable by keyboard. Click latches a panel open, focus opens one, Escape and leaving the figure close it.
The name panel is
pointer-events-none— it hangs over the legend rows, and catching the pointer would bury the row you'd click again to close it.Testing
tsc --noEmitandnpm run lintclean (4 pre-existing warnings in unrelated files). Issues, ward, mayoral and survey pages all render 200; verified 33 bands, 96 segments and the legend markup in the served HTML.Not verified in a browser — the Chrome extension wasn't connected in this session, so the interaction was reasoned from the event flow rather than watched. Worth a click-through before merge, particularly the name panel's position on the right-hand column of the two-up grid.
One known rough edge:
OptionBar'sresponsivemeasures with aResizeObserver, which can't run server-side, so the band ships at a fixed width and snaps to the real one on hydration. I set the pre-measure width to 560 (roughly a card in the two-up grid) so it's a nudge rather than a jump from the library's 230px default. The proper fix is aviewBoxonOptionBar, left alone here since it's shared.🤖 Generated with Claude Code