Midterms dashboard: race summary, seat medians, engagement block, insight cards - #5114
Midterms dashboard: race summary, seat medians, engagement block, insight cards#5114aseckin wants to merge 5 commits into
Conversation
…ight cards
Five changes to the /midterms-2026 hub:
- Governor tab gets a top-line summary ("10 of 19 races lean Democrat"),
rendered in a new absolutely-positioned header row shared with the chamber
tabs and legend so a wrapping summary never pushes the map down. Lean split
is >= 50%, matching the state tooltip so the two can't disagree.
- Both seat-distribution charts gain a median callout: a dot on the x-axis at
the median bin carrying an always-on VictoryTooltip. It hides while a bin is
hovered so it never stacks with the hover tooltip, and is suppressed entirely
when the median is exactly 0 (the EVEN bin already owns that spot). Hover
flyouts go to 90% opacity, the median's to 80%, both +1px padding-top and
~2px more gap between lines.
- Chamber control's pre-midterm baseline reads "Today:" instead of "Current:"
and drops its party colors for gray, so it reads as context rather than
competing with the live forecast above it. Also fixes an existing dark-mode
bug: those two spans hardcoded light-mode hexes with no dark variant.
- Community Insight cards carry their question title in a header strip, with
the comment as the visual hero and the author as a footer. Height is capped
rather than fixed, so short comments let the card shrink.
- Adds the Labor Hub's three engagement cards (contact / share / newsletter)
with midterms copy and its own Mailjet list. NewsletterSubscribeCard gained
an optional `copy` prop defaulting to its current English, so the Labor Hub
renders unchanged.
Median-marker note: Victory's voronoi handleMouseMove calls onActivated(new)
then onDeactivated(previous) back-to-back in the same tick, so an unconditional
onDeactivated handler silently undoes every activation. Guarded with a ref.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe midterms hub adds forecast timelines, race-lean and seat-median summaries, updated map and insight cards, localized messages, and contact, sharing, and newsletter engagement controls. ChangesMidterms forecasting and summaries
Engagement and newsletter
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MidtermsPage
participant ElectionsMapSection
participant BalanceOfPowerTimelines
participant buildControlTimeline
participant GroupChart
MidtermsPage->>ElectionsMapSection: render election section
ElectionsMapSection->>BalanceOfPowerTimelines: provide congressOutcome
BalanceOfPowerTimelines->>buildControlTimeline: build forecast timelines
buildControlTimeline-->>BalanceOfPowerTimelines: return chart series
BalanceOfPowerTimelines->>GroupChart: render interactive timelines
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Preview EnvironmentYour preview environment is ready!
Details
ℹ️ Preview Environment InfoIsolation:
Limitations:
Cleanup:
|
The hub's right panel showed only point-in-time numbers, so a reader couldn't tell whether "Democrats 86.3% to hold the House" was a sharp move or had been flat for months. The panel now shows three community-prediction timelines — House, Senate, and the four-way congressional outcome — covering ~18 months of history (333 aggregation points back to Jan 2025). No new fetching: getPostsWithCP already sets include_cp_history, so the history for #34484 was arriving and being discarded. - helpers/build_control_timelines.ts turns that history into ChoiceItem series, summing options per series. House = DD+RD / RR+DR and Senate = DD+DR / RR+RD, the same sums chamber_control_card.tsx uses, so each line's last point lands exactly on the percentage the cards showed (verified: 86.3/13.7, 37.3/62.7). Deliberately not built on generateChoiceItemsFromMultipleChoiceForecast, which routes through getAllOptionsHistory — that returns only the last entry of question.options when options_history is absent and would silently collapse a four-option question to one series. - Rendered with the shared GroupChart: individual lines, a dot pinned to each endpoint that tracks the cursor on hover, and no axes. Values live in the legend instead, showing the current figure at rest and the hovered point's figure while the cursor is in the plot. - GroupChart gains additive hideYAxis / hideXAxis props (both default false, so no existing call site changes). Its axis tick styles are set inline on the VictoryAxis and can't be reached via extraTheme, hence props rather than a theme override. hideXAxis makes permanent what the component already did while the cursor was active; pair it with showCursorLabel so the hovered date is the only x value ever shown. - The four congressional outcomes use MULTIPLE_CHOICE_COLOR_SCALE; two blues and two reds were indistinguishable as four lines. - Layout: one rule divides the map from the panel and runs the full card height, and the map column now carries the same padding as the panel so it's inset equally on all sides rather than butting against the rule. The snapshot cards (chamber_control_card, congress_outcome_card) are no longer rendered but are left in place unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blue-300 read heavier than the rest of the section's chrome now that it's the only structural line in the card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@front_end/messages/en.json`:
- Around line 2394-2395: Update the plural wording in
midtermsHubSeatAdvantageTooltip and midtermsHubSeatAdvantageOverTooltip so
counts above one render “seats advantage” instead of “seat advantage,” while
preserving the existing count interpolation and greater-than prefix.
In
`@front_end/src/app/`(main)/midterms-2026/components/balance_of_power_timelines.tsx:
- Around line 214-215: Update the percentage rendering in the balance-of-power
timeline component to use useLocale() and Intl.NumberFormat with style "percent"
and one minimum and maximum fraction digit. Preserve the existing em-dash for
null values, and format non-null values through the locale-aware formatter so
locales such as Spanish use the correct decimal and spacing conventions.
In `@front_end/src/app/`(main)/midterms-2026/components/insight_card.tsx:
- Around line 17-19: Update the excerpt generation in InsightCard, including the
related logic at the alternate excerpt usage, so the EXCERPT_CHARS limit is
applied at the last whitespace boundary at or before the limit rather than
slicing through a word. Preserve the existing line-clamp-6 rendering and
fallback behavior.
- Around line 33-36: Update the Link in insight_card to provide a localized
screen-reader-only cue announcing that it opens in a new tab, using
useTranslations() rather than hardcoded English; preserve the existing
target="_blank" behavior and ensure the cue is included in the link’s accessible
name.
In `@front_end/src/app/`(main)/midterms-2026/helpers/build_control_timelines.ts:
- Around line 70-73: Update the timeline-building flow around the timestamp loop
and findCoveringForecast so forecast coverage is computed once using a sorted,
monotonic cursor over history rather than calling history.find per timestamp and
series. Reuse the resulting timestamp-to-forecast mapping for sumCenters and
forecaster counts across all series, preserving existing aggregation values and
fallback behavior.
In `@front_end/src/app/`(main)/midterms-2026/sections/engagement_section.tsx:
- Line 58: Extend useCopyUrl to accept caller-provided localized success and
failure messages, displaying both through the existing toast/status feedback
path instead of relying on its hardcoded English success text and logging-only
failure handling. In engagement_section.tsx, update the useCopyUrl call to pass
the appropriate translated success and failure strings from the available i18n
source.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9ad9ec80-9eac-4267-81d2-8806164bc92c
📒 Files selected for processing (21)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/src/app/(main)/actions.tsfront_end/src/app/(main)/labor-hub/components/newsletter_subscribe_card.tsxfront_end/src/app/(main)/midterms-2026/components/balance_of_power_timelines.tsxfront_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsxfront_end/src/app/(main)/midterms-2026/components/geographic_map.tsxfront_end/src/app/(main)/midterms-2026/components/insight_card.tsxfront_end/src/app/(main)/midterms-2026/components/race_lean_summary.tsxfront_end/src/app/(main)/midterms-2026/components/responsive_map.tsxfront_end/src/app/(main)/midterms-2026/components/seat_distribution_chart.tsxfront_end/src/app/(main)/midterms-2026/helpers/build_control_timelines.tsfront_end/src/app/(main)/midterms-2026/helpers/post_utils.tsfront_end/src/app/(main)/midterms-2026/page.tsxfront_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsxfront_end/src/app/(main)/midterms-2026/sections/engagement_section.tsxfront_end/src/components/charts/group_chart.tsx
| <Link | ||
| href={`/questions/${sourcePost.id}/#comment-${comment.id}`} | ||
| target="_blank" | ||
| rel="noreferrer" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Announce the new-tab behavior accessibly.
target="_blank" opens the question in a new tab, but the link has no accessible text that announces this behavior. If opening a new tab is intentional, add a localized screen-reader-only cue. Otherwise, remove target="_blank".
Based on learnings, use useTranslations() for the cue instead of hardcoding English in TSX.
Suggested fix
<Link
href={`/questions/${sourcePost.id}/#comment-${comment.id}`}
target="_blank"
rel="noreferrer"
>
+ <span className="sr-only">{t("midtermsHubOpensInNewTab")}</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Link | |
| href={`/questions/${sourcePost.id}/#comment-${comment.id}`} | |
| target="_blank" | |
| rel="noreferrer" | |
| <Link | |
| href={`/questions/${sourcePost.id}/#comment-${comment.id}`} | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| <span className="sr-only">{t("midtermsHubOpensInNewTab")}</span> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front_end/src/app/`(main)/midterms-2026/components/insight_card.tsx around
lines 33 - 36, Update the Link in insight_card to provide a localized
screen-reader-only cue announcing that it opens in a new tab, using
useTranslations() rather than hardcoded English; preserve the existing
target="_blank" behavior and ensure the cue is included in the link’s accessible
name.
Source: Learnings
| for (const timestamp of timestamps) { | ||
| const entry = findCoveringForecast(history, timestamp); | ||
| aggregationValues.push(sumCenters(entry, optionIndices)); | ||
| aggregationForecasterCounts.push(entry?.forecaster_count ?? 0); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Remove the repeated history scans.
Line 71 performs a linear history.find() for every timestamp and every series. The three timelines create eight series, so this becomes O(series × N²). Long aggregation histories can block rendering in the sidebar.
Build the timestamp-to-forecast coverage once with a sorted monotonic cursor. Reuse that result for each series.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front_end/src/app/`(main)/midterms-2026/helpers/build_control_timelines.ts
around lines 70 - 73, Update the timeline-building flow around the timestamp
loop and findCoveringForecast so forecast coverage is computed once using a
sorted, monotonic cursor over history rather than calling history.find per
timestamp and series. Reuse the resulting timestamp-to-forecast mapping for
sumCenters and forecaster counts across all series, preserving existing
aggregation values and fallback behavior.
|
|
||
| function ShareCard() { | ||
| const t = useTranslations(); | ||
| const copyUrl = useCopyUrl({ includeHash: false }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Provide localized clipboard status feedback.
useCopyUrl() emits an English success toast and only logs clipboard failures. This leaves the new localized share card with an untranslated success message and no failure message.
Extend the hook with caller-provided status messages, then pass translated success and failure text at Line 58.
Based on learnings: prefer i18n strings for UI text.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front_end/src/app/`(main)/midterms-2026/sections/engagement_section.tsx at
line 58, Extend useCopyUrl to accept caller-provided localized success and
failure messages, displaying both through the existing toast/status feedback
path instead of relying on its hardcoded English success text and logging-only
failure handling. In engagement_section.tsx, update the useCopyUrl call to pass
the appropriate translated success and failure strings from the available i18n
source.
Source: Learnings
…lized copy toast - Insight cards navigate in the current tab instead of opening a new one. Their corner glyph changes from the open-in-new-window arrow to a plain right arrow, which is what the link now actually does. - build_control_timelines resolves each timestamp's covering forecast window once with a monotonic cursor and shares the result across every series, replacing a history.find scan run per timestamp per series. Both sequences ascend, so this is a single pass rather than series x timestamps x history. Verified behavior-identical against the previous implementation: 3 charts x 7 sample indices x 8 series, plus timestamps, labels and forecaster counts — zero mismatches, zero nulls across 333 points. It assumes non-overlapping aggregation windows, which is how CP history is built; the tradeoff is noted in the helper's comment. - useCopyUrl accepts optional localized success/failure messages. It previously hardcoded an English success toast and dropped failures into console.error, so on the fully-translated midterms hub "Copy Link" showed English, and a denied clipboard write left the button looking broken. Both defaults are unchanged, so the other three callers behave exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five improvements to the
/midterms-2026hub.1. Governor-tab race summary
A top-line read on the races — "10 of 19 races lean Democrat" — sitting between the chamber tabs and the legend. The count and party name are party-colored and follow whichever side leads, so this flips to red on its own if forecasts move.
The tabs and legend used to be two independent corner overlays; they're now one absolutely-positioned header row with the summary between them. Because the row is absolute, a summary that wraps grows over the map instead of pushing it down — verified the map's
svgTopis byte-identical across both tabs at 1024px and 1440px, including with a forced 3-line string.Lean threshold is
demWinPct >= 50, the same splitstate_tooltip.tsxalready uses, so the summary can never disagree with a tooltip. Governor tab only.2. Seat-distribution medians
Both charts now surface a median: a dot on the x-axis at the median bin carrying an always-on flyout, using the same
VictoryTooltipshape as the hover tooltips.computeQuartilesFromCDFwas already running every render and its result was being thrown away — the quartile lines that consumed it are gated behind!isDiscrete, and both questions became Discrete in e82e6a9.P(seat advantage = 0); the median is where the CDF crosses 50%. They coincide on Senate today only because the community's median forecast genuinely is a tied Senate. The callout returns on its own as soon as the median moves off zero.3. "Today:" baseline
Chamber control's pre-midterm seat counts read
Today:instead ofCurrent:and drop their party colors for gray, so the static baseline reads as context rather than competing with the live forecast directly above it. Also fixes a latent dark-mode bug: those two spans hardcoded light-mode hexes with no dark variant.4. Community Insight cards
Cards now carry the question they're about. Three zones: question-title strip → comment as the visual hero → author footer. Height is capped rather than fixed, so a short comment lets the card shrink; the title strip wraps to a second line only when it needs to.
Rewritten as a self-contained
<Link>rather than wrappingActivityCard, which renders username and subtitle together at the top and forcespr-12for its corner arrow — both fight this layout, and a 340px card can't spare 48px of right padding once the comment is the hero.ActivityCardis untouched, so the Labor Hub is unaffected.5. Engagement block
The Labor Hub's three cards (contact / share / newsletter) carried over with midterms copy and its own Mailjet list, slotted between Community Insights and the footer disclaimer.
NewsletterSubscribeCardgained an optionalcopyprop defaulting to its current English strings, so the Labor Hub renders unchanged while Midterms gets translated copy.All new strings are in
en,es,cs,pt,zh,zh-TW, translated rather than English-filled, reusing the existingcontactUs/copyLink/emailAddresskeys. Seat counts use ICU plurals so+1doesn't read "1 seats".Worth a reviewer's attention
A Victory footgun.
victory-voronoi-container'shandleMouseMovecallsonActivated(newPoints)and thenonDeactivated(previousPoints)back-to-back in the same tick whenever the active bin changes. An unconditionalonDeactivatedhandler therefore undoes every activation and the state never sticks. Guarded with a ref that lets only the genuine clears (out-of-bounds, pointer off the container) through. Caught this because the median visibly failed to hide on hover.The median marker must stay in
voronoiBlacklist. The container isvoronoiDimension="x", so without it the marker becomes its own hover target and renders a second, wrong tooltip.Verification
bun run lint(eslint + tsc) exits 0 with 165 warnings — the same count as before these changes, so no new warnings.bun run formatclean.Manually verified against the dev server in both light and dark mode: median inks invert correctly against the flyout, hover gating survives moving between bins and restores on exit, Senate keeps EVEN with no median while House keeps its callout, all 14 insight cards land within the height cap with zero text clipping, and the summary wraps without shifting the map.
Out of scope
Noticed while testing: the page scrolls ~74px horizontally at 1024px width. The cause is the global site footer (
gap-[122px] sm:gap-[218px]), outside<main>and unrelated to this work — worth a separate fix.Summary by CodeRabbit
New Features
Enhancements