Skip to content

Time series visualisation updates#4738

Open
ncarazon wants to merge 8 commits into
feat/question-page-redesign-3rd-iterationfrom
feat/time-series-visualisation-updates-consumer-left-pane
Open

Time series visualisation updates#4738
ncarazon wants to merge 8 commits into
feat/question-page-redesign-3rd-iterationfrom
feat/time-series-visualisation-updates-consumer-left-pane

Conversation

@ncarazon
Copy link
Copy Markdown
Contributor

@ncarazon ncarazon commented May 18, 2026

Related to #4643

Summary

  • FanGraphChartPanel (new): Fan/Timeline toggle for FanGraph right pane, used in both consumer and forecaster shells.
  • ConsumerTimeSeriesPane (new): wraps TimeSeriesChart with context hover sync (hoveredBarLabel/onBarHover from ListChartExpandedContext).
  • TimeSeriesChart: refactored to per-question VictoryBar layers for per-bar opacity control; new hoveredBarLabel/onBarHover props + HighlightBar component for hover highlight.
  • FanChart: new externalHighlightedLabel prop dims non-hovered fan areas/lines when a bar is hovered in the left pane.
  • QuestionPageShell: consumer FanGraph mounts ConsumerTimeSeriesPane + FanGraphChartPanel; forecaster detects FanGraph and renders FanGraphChartPanel instead of DetailedGroupCard.
  • MultiChoicesChartView: fix normalizedChartHeight initialising to 0; handle missing legendContainerRef.
  • i18n: fanChart key added to all 6 locale files.

Demo video

forecaster-consumer-time-series-updates.mp4

Summary by CodeRabbit

  • New Features

    • Added Fan Chart visualization for group questions with toggle between fan and timeline views.
    • Enhanced mobile touch interactions for improved chart navigation.
    • Added bar hover effects in time series charts.
  • Documentation

    • Added "Fan Chart" translations across multiple languages (Czech, English, Spanish, Portuguese, Traditional Chinese, Simplified Chinese).

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6e5693b-d0ef-4e5f-b6fc-309e4adc11fa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: time series visualization updates across consumer and forecaster views, including new chart components, hover synchronization, and touch interactions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/time-series-visualisation-updates-consumer-left-pane

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4738-feat-time-series-visualisation-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-time-series-visualisation-updates-consumer-left-pane-9ae483d
🗄️ PostgreSQL NeonDB branch preview/pr-4738-feat-time-series-visualisation
Redis Fly Redis mtc-redis-pr-4738-feat-time-series-visualisation

Details

  • Commit: f4236f1a21a5ae7687ff0b00ba24275b58dfeb8f
  • Branch: feat/time-series-visualisation-updates-consumer-left-pane
  • Fly App: metaculus-pr-4738-feat-time-series-visualisation

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@ncarazon ncarazon force-pushed the feat/time-series-visualisation-updates-consumer-left-pane branch from 7dfc456 to e1c20ab Compare May 19, 2026 11:39
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/src/components/consumer_post_card/time_series_chart/index.tsx (1)

396-449: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve the real source index before filtering.

originalIndex is assigned after removing unsuccessfully resolved questions, so colorful mode shifts palette assignments whenever one of those questions is filtered out. That breaks cross-chart color alignment for every item after the gap.

Suggested fix
-  return [...questions]
-    .filter((question) => !isUnsuccessfullyResolved(question.resolution))
-    .map((question, index) => {
+  return questions
+    .map((question, originalIndex) => ({ question, originalIndex }))
+    .filter(({ question }) => !isUnsuccessfullyResolved(question.resolution))
+    .map(({ question, originalIndex }) => {
       const latest_centers =
         question.aggregations[question.default_aggregation_method].latest
           ?.centers?.[0];
@@
         isClosed: question.status === QuestionStatus.CLOSED,
         resolution: question.resolution,
         isEmpty: !hasData,
-        originalIndex: index,
+        originalIndex,
       };
     });
🤖 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/components/consumer_post_card/time_series_chart/index.tsx`
around lines 396 - 449, The bug is that originalIndex is assigned after
filtering out unsuccessfully resolved questions, shifting palette assignments;
to fix, capture each question's original index before you call .filter by
mapping the incoming questions to include an immutable originalIndex (e.g.,
attach a tmp property like __originalIndex using the index from the input array)
and then run the existing .filter/.map chain against that list, and finally use
that stored __originalIndex when setting originalIndex in the returned object
(replace references to index in the current map with the preserved value);
update references to originalIndex in this file (the mapping block that builds
x/y/label/isClosed/etc.) to read the preserved property instead of the
post-filter index.
🧹 Nitpick comments (1)
front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx (1)

305-305: ⚡ Quick win

Extract magic number to named constant.

The hardcoded slice(-4) appears twice without explanation. Extract it to a named constant to clarify intent and ensure consistency if this value needs to change.

♻️ Proposed refactor

At the top of the file or in a constants file:

const MAX_VISIBLE_FAN_GRAPH_QUESTIONS = 4;

Then use it at both call sites:

-                    <ConsumerTimeSeriesPane
-                      questions={fanGraphQuestions.slice(-4)}
-                    />
+                    <ConsumerTimeSeriesPane
+                      questions={fanGraphQuestions.slice(-MAX_VISIBLE_FAN_GRAPH_QUESTIONS)}
+                    />
-                      visibleQuestions={fanGraphQuestions?.slice(-4)}
+                      visibleQuestions={fanGraphQuestions?.slice(-MAX_VISIBLE_FAN_GRAPH_QUESTIONS)}

Also applies to: 324-324

🤖 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)/questions/[id]/components/question_page_shell/index.tsx
at line 305, The two occurrences of fanGraphQuestions.slice(-4) are using a
magic number; extract this into a named constant (e.g.
MAX_VISIBLE_FAN_GRAPH_QUESTIONS = 4) declared at the top of the file or in a
shared constants module, then replace both fanGraphQuestions.slice(-4) call
sites in question_page_shell (the props passed as questions) with
fanGraphQuestions.slice(-MAX_VISIBLE_FAN_GRAPH_QUESTIONS) to make intent
explicit and keep the value consistent.
🤖 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/src/app/`(main)/questions/[id]/components/multiple_choices_chart_view/index.tsx:
- Around line 296-305: Add a handler for the touchcancel event to clear the
stored touch coordinates so mobile tooltips can't get stuck: in the same
component where onTouchStartCapture, onTouchMoveCapture, onTouchEndCapture,
onMouseMoveCapture and setTouchCoords are used, add an onTouchCancelCapture (or
onTouchCancel) that calls setTouchCoords(undefined) so touchCoords is always
cleared when the touch sequence is canceled.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_view/consumer_question_view/consumer_time_series_pane.tsx:
- Around line 30-32: The onBarHover handler currently ignores null labels so the
hover state never clears; change the onBarHover prop implementation in
consumer_time_series_pane.tsx to call setHoveredChoiceName(label) for all values
(i.e., allow label to be null) so when TimeSeriesChart calls onBarHover(null)
the hovered state is cleared, and if the hoveredChoiceName state type doesn't
accept null update its declaration (or coerce to empty string) accordingly;
reference: onBarHover, setHoveredChoiceName, TimeSeriesChart.

In `@front_end/src/components/charts/group_chart.tsx`:
- Around line 405-475: Add an onTouchCancel handler next to
onTouchStartCapture/onTouchMoveCapture that mirrors the cleanup in onTouchEnd:
set inPlotRef.current = false, call setIsCursorActive(false), and
setLocalCursorTimestamp(null); also optionally invoke onCursorChange with the
last timestamp (timestamps.at(-1)) like onTouchEnd if you want identical
behavior — implement this within the same event props block (referencing
onTouchCancel, inPlotRef, setIsCursorActive, setLocalCursorTimestamp,
onCursorChange, timestamps).
- Around line 827-829: The pixel-to-timestamp conversion in pixelXToTimestamp
can produce NaN when plotWidth (computed as chartWidth - rightPadding -
leftPadding) is <= 0; add a guard that if plotWidth <= 0 (or if
Number.isNaN(plotWidth)), return a sensible fallback timestamp (for example
Number(xDomain[0]) or the midpoint of xDomain) instead of performing the ratio
math. Specifically, check plotWidth before computing ratio and only compute
ratio = Math.max(0, Math.min(1, (x - leftPadding) / plotWidth)) when plotWidth >
0; otherwise return the chosen fallback based on xDomain to avoid propagating
NaN.

In `@front_end/src/components/consumer_post_card/time_series_chart/index.tsx`:
- Around line 130-135: The highlight layer is only mounted when onBarHover is
provided, so externally setting hoveredBarLabel still resolves
effectiveHoveredIndex but never shows the highlight; update the mounting
condition to use effectiveHoveredIndex (or check onBarHover || hoveredBarLabel)
instead of only onBarHover so the highlight layer renders when
externalHoveredIndex/hoveredBarLabel is present; apply the same change to the
other occurrence noted (lines 255-265) and reference variables
adjustedChartData, externalHoveredIndex, hoveredIndex, effectiveHoveredIndex,
hoveredBarLabel and the onBarHover prop in the TimeSeriesChart component.

In `@front_end/src/hooks/use_chart_tooltip.ts`:
- Around line 30-31: The hoverActive state can become stale while forceOpen is
true because hover listeners are disabled; add a useEffect in
use_chart_tooltip.ts that watches forceOpen and calls setHoverActive(false)
whenever forceOpen becomes true (resetting hoverActive before entering forced
mode). Locate the hook where hoverActive and setHoverActive are defined
(useState) and add the useEffect referencing forceOpen to clear hoverActive,
ensuring isActive (computed as !!forceOpen || hoverActive) doesn’t stay true
after forced mode toggles off.

---

Outside diff comments:
In `@front_end/src/components/consumer_post_card/time_series_chart/index.tsx`:
- Around line 396-449: The bug is that originalIndex is assigned after filtering
out unsuccessfully resolved questions, shifting palette assignments; to fix,
capture each question's original index before you call .filter by mapping the
incoming questions to include an immutable originalIndex (e.g., attach a tmp
property like __originalIndex using the index from the input array) and then run
the existing .filter/.map chain against that list, and finally use that stored
__originalIndex when setting originalIndex in the returned object (replace
references to index in the current map with the preserved value); update
references to originalIndex in this file (the mapping block that builds
x/y/label/isClosed/etc.) to read the preserved property instead of the
post-filter index.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/index.tsx:
- Line 305: The two occurrences of fanGraphQuestions.slice(-4) are using a magic
number; extract this into a named constant (e.g. MAX_VISIBLE_FAN_GRAPH_QUESTIONS
= 4) declared at the top of the file or in a shared constants module, then
replace both fanGraphQuestions.slice(-4) call sites in question_page_shell (the
props passed as questions) with
fanGraphQuestions.slice(-MAX_VISIBLE_FAN_GRAPH_QUESTIONS) to make intent
explicit and keep the value consistent.
🪄 Autofix (Beta)

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

Run ID: cba0aff2-0fd3-4eb6-899f-d6db310372f7

📥 Commits

Reviewing files that changed from the base of the PR and between a99a0ec and e1c20ab.

📒 Files selected for processing (19)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/questions/[id]/components/multiple_choices_chart_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/consumer_group_chart.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/consumer_list_chart_shell.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/consumer_time_series_pane.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/fan_graph_chart_panel.tsx
  • front_end/src/components/charts/fan_chart.tsx
  • front_end/src/components/charts/group_chart.tsx
  • front_end/src/components/charts/multiple_choice_chart.tsx
  • front_end/src/components/charts/primitives/chart_fan_tooltip.tsx
  • front_end/src/components/consumer_post_card/time_series_chart/index.tsx
  • front_end/src/components/consumer_post_card/time_series_chart/time_series_label.tsx
  • front_end/src/hooks/use_chart_tooltip.ts

Comment thread front_end/src/components/charts/group_chart.tsx
Comment thread front_end/src/components/charts/group_chart.tsx
Comment thread front_end/src/hooks/use_chart_tooltip.ts
@ncarazon ncarazon marked this pull request as ready for review May 19, 2026 13:37
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