Skip to content

chore(charts): upgrade Recharts from 2.13 to 3.x#2610

Open
brandon-pereira wants to merge 13 commits into
mainfrom
brandon/upgrade-recharts-3
Open

chore(charts): upgrade Recharts from 2.13 to 3.x#2610
brandon-pereira wants to merge 13 commits into
mainfrom
brandon/upgrade-recharts-3

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Jul 9, 2026

Copy link
Copy Markdown
Member

Why

Bumps recharts from 2.13.3 to 3.x (^3.2.1). Recharts 3 is a major release that rewrote the library's internal state management. This upgrade keeps us on a maintained major and unblocks features that only exist in 3.x (portals, auto-width axes, per-axis tooltips), while preserving all existing chart behavior.

This branch is intentionally scoped to only the dependency upgrade. (The unrelated drill-down menu work that previously shared this worktree has been moved to its own branch.)

Recharts v3 Migration Guide

What changed

Recharts 3 removed CategoricalChartState (the chartX / chartY / activePayload fields) from chart event handlers, and removed the imperative chart.setState escape hatch. The chart components were updated to the new API with behavior preserved:

  • HDXMultiSeriesTimeChart — event handlers now derive the click/drag position from activeCoordinate (falling back to the native event's offsetX) and rebuild the drill-down per-series payload from graphResults. Zoom-brush selection and the click drill-down popover are unchanged from a user's perspective.
  • DBHistogramChart — the chart.setState tooltip-pin hack is replaced with the controlled active / defaultIndex Tooltip props. Click-to-pin, click-again-to-unpin, and Esc-to-unpin all still work.
  • PropertyComparisonChart — custom tooltip/shape typings updated to the renamed TooltipContentProps; the clicked bar is now selected via activeLabel + a chartData lookup.
  • app.scss — hides the browser focus ring that Recharts 3's default accessibilityLayer paints when a chart is clicked. Charts remain keyboard-focusable; only the visual outline is suppressed.
  • package.json / yarn.lockrecharts@^3.2.1 and react-is@^19.0.0 (added to the app to satisfy recharts' peer requirement under React 19).

Testing

I have done a lot of testing on this, but we have lots of charts and lots of edge cases. Please check charts generally, specific things that were refactored below

  • Time chart: hover tooltip (values, previous-period, nearest-series bolding), zoom-brush drag, click drill-down popover, reset zoom — on Search histogram + dashboard tiles, in both line and stacked-bar modes.
  • Histogram (Services dashboard): hover tooltip, click-to-pin / unpin, Esc to unpin.
  • Property comparison (Search → Delta analysis): bar colors, hover percentages, click-to-select.
  • Focus ring: clicking any chart shows no blue outline; keyboard Tab + arrow-key navigation still works.
  • Smoke-check pie tiles, number-tile sparklines, pattern-trend inline bars (syncId cursor sync), and alert reference lines.
  • CaptureActiveDot nearest-series bolding depends on recharts' internal render order; it type-checks and works in dev but is worth an explicit look on a multi-series line chart.
  • connectNulls null handling changed in 3.x (nulls treated as 0 when enabled) — verify series with gaps render as before.

Recharts 3 reworked its internal state management, which removes the
CategoricalChartState fields (chartX/chartY/activePayload) from chart
event handlers and the imperative chart.setState escape hatch. Update
the chart components to the new API while preserving existing behavior:

- HDXMultiSeriesTimeChart: derive click/drag position from activeCoordinate
  and rebuild the drill-down per-series payload from graphResults, keeping
  zoom-brush selection and the click drill-down popover working.
- DBHistogramChart: replace the chart.setState tooltip-pin hack with the
  controlled active/defaultIndex Tooltip props.
- PropertyComparisonChart: update custom tooltip/shape typings to the
  renamed TooltipContentProps and select the clicked bar via activeLabel.
- app.scss: hide the browser focus ring that Recharts 3's default
  accessibilityLayer paints when a chart is clicked, keeping charts
  keyboard-focusable.
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 10, 2026 6:08pm
hyperdx-storybook Ready Ready Preview, Comment Jul 10, 2026 6:08pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0bdf0f0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 527 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 7
  • Production lines changed: 527 (+ 160 in test files, excluded from tier calculation)
  • Branch: brandon/upgrade-recharts-3
  • Author: brandon-pereira

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 236 passed • 3 skipped • 1531s

Status Count
✅ Passed 236
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR upgrades Recharts from 2.13.3 to ^3.2.1, adapting to its new Immer-backed frozen store, the removal of CategoricalChartState chart event fields (chartX/chartY/activePayload), and the removal of the imperative chart.setState escape hatch. The scope is strictly the library upgrade with behavior preserved end-to-end.

  • HDXMultiSeriesTimeChart — array payloads are shallow-copied before sort (frozen Immer arrays), drag-to-zoom distance uses a stable containerRef-relative origin on both mousedown and mouseup, a suppressNextClickRef swallows the synthetic click that Recharts 3 fires after a zoom brush, and the drill-down payload is rebuilt from graphResults keyed by timestampKey rather than taken from the now-absent state.activePayload.
  • DBHistogramChart — replaces the chart.setState tooltip-pin hack with a controlled active + defaultIndex + trigger:'click' Tooltip, guarded by a key-based remount on each pin change so defaultIndex is always fresh; a useEffect on data clears the position-based pin whenever buckets change.
  • PropertyComparisonChart — bar-click selection is refactored into a pure resolveComparisonClick helper (exported and unit-tested); the TooltipProps type rename to TooltipContentProps is applied.

Confidence Score: 5/5

Safe to merge — all behavioral changes preserve existing user-facing functionality and the key edge cases (frozen Immer arrays, mixed coordinate spaces, synthetic post-zoom click, stale positional pin) are handled and unit-tested.

The upgrade adapts three complex chart components to Recharts 3's new API without introducing regressions. Each changed interaction path (drag-to-zoom, click drill-down, histogram pin, bar selection) has either been made more correct than the v2 version or is covered by new unit tests. No logic gaps were found.

No files require special attention. The most involved file is HDXMultiSeriesTimeChart.tsx, but its changes are well-commented and covered by tests.

Important Files Changed

Filename Overview
packages/app/src/HDXMultiSeriesTimeChart.tsx Major rewrite of chart interaction logic: containerRef-based drag measurement, suppressNextClickRef zoom guard, visibleLineData extraction for click payload, frozen-array copy-before-sort fixes. Well-commented and comprehensive.
packages/app/src/components/DBHistogramChart.tsx setState hack replaced with controlled Tooltip using active+defaultIndex+trigger:'click'; key-based remount ensures defaultIndex is always fresh; useEffect clears stale positional pin on data change.
packages/app/src/components/PropertyComparisonChart.tsx Bar-click selection extracted into testable resolveComparisonClick helper; TooltipProps renamed to TooltipContentProps; frozen payload copy-before-sort added.
packages/app/src/tests/HDXMultiSeriesTimeChart.test.ts New unit tests for getVisibleLineData and buildActiveClickSeries cover selection filtering, HARD_LINES_LIMIT truncation, numeric/null value handling, and zero-value preservation.
packages/app/src/components/tests/DBHistogramChart.test.tsx New resolvePinnedBarIndex tests cover all rejection cases (null, negative, float, NaN, whitespace-only string) alongside acceptance cases.
packages/app/src/components/tests/PropertyComparisonChart.test.ts New test file covering resolveComparisonClick: select, deselect, toggle-off, Other-bucket guard, missing-row guard, and switch selection.
packages/app/styles/app.scss Focus ring scoped to .recharts-surface via :focus:not(:focus-visible) — preserves keyboard Tab ring on wrapper while hiding click-triggered outline on the inner SVG.
packages/app/tests/e2e/page-objects/ChartExplorerPage.ts E2E locator updated to recharts-xAxis-tick-labels class (Recharts 3 hoists tick labels into a top-level z-index layer); comment explains the structural change.
packages/app/src/components/DBTimeChart.tsx ActiveClickPayload type de-duplicated by importing from HDXMultiSeriesTimeChart where it is now defined and exported.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    MD[onMouseDown\nrecord containerRef-relative X\n+ activeLabel as highlightStart] --> MM

    MM[onMouseMove\nupdate highlightEnd\nclear isClickActive] --> MU

    MU[onMouseUp\nmeasure drag distance\nusing same containerRef-relative X] --> DragCheck

    DragCheck{drag >\nMIN_DRAG_DISTANCE?}

    DragCheck -- Yes --> Zoom[call onTimeRangeSelect\nset suppressNextClickRef=true\nset justZoomedRef=true]
    DragCheck -- No,\nsame label --> ClearHL[clear highlightStart/End\nno zoom]
    DragCheck -- No,\ndifferent label --> ClearHL2[clear highlight\nno zoom]

    Zoom --> SyntheticClick

    SyntheticClick[onClick fires\nsuppressNextClickRef?] -- true --> Swallow[set false\nstopPropagation\nreturn]
    SyntheticClick -- false --> ClickGuard

    ClearHL --> ClickGuard
    ClearHL2 --> ClickGuard

    ClickGuard{activeCoordinate\n+ activeLabel\n+ highlightStart==null?}
    ClickGuard -- No --> ClearClick[setIsClickActive undefined]
    ClickGuard -- Yes --> BuildPayload[graphResults.find by timestampKey\nbuildActiveClickSeries from visibleLineData\nsetIsClickActive with x/y/xPerc/yPerc/activePayload]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    MD[onMouseDown\nrecord containerRef-relative X\n+ activeLabel as highlightStart] --> MM

    MM[onMouseMove\nupdate highlightEnd\nclear isClickActive] --> MU

    MU[onMouseUp\nmeasure drag distance\nusing same containerRef-relative X] --> DragCheck

    DragCheck{drag >\nMIN_DRAG_DISTANCE?}

    DragCheck -- Yes --> Zoom[call onTimeRangeSelect\nset suppressNextClickRef=true\nset justZoomedRef=true]
    DragCheck -- No,\nsame label --> ClearHL[clear highlightStart/End\nno zoom]
    DragCheck -- No,\ndifferent label --> ClearHL2[clear highlight\nno zoom]

    Zoom --> SyntheticClick

    SyntheticClick[onClick fires\nsuppressNextClickRef?] -- true --> Swallow[set false\nstopPropagation\nreturn]
    SyntheticClick -- false --> ClickGuard

    ClearHL --> ClickGuard
    ClearHL2 --> ClickGuard

    ClickGuard{activeCoordinate\n+ activeLabel\n+ highlightStart==null?}
    ClickGuard -- No --> ClearClick[setIsClickActive undefined]
    ClickGuard -- Yes --> BuildPayload[graphResults.find by timestampKey\nbuildActiveClickSeries from visibleLineData\nsetIsClickActive with x/y/xPerc/yPerc/activePayload]
Loading

Reviews (12): Last reviewed commit: "Merge branch 'main' into brandon/upgrade..." | Re-trigger Greptile

Comment thread packages/app/src/HDXMultiSeriesTimeChart.tsx
Comment thread packages/app/styles/app.scss Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. This is a well-executed major-version upgrade: event handlers are cleanly migrated off the removed chartX/chartY/setState APIs, frozen-payload copies ([...payload]) are added where Recharts 3's Immer store would otherwise throw, and the interactive helpers are extracted and unit-tested. No P0/P1 production breakage or correctness regression is introduced on any traced happy path.

🟡 P2 -- recommended

  • .husky/pre-commit:13 -- Adds a whole-project npx knip run to every commit; it is unrelated to the Recharts upgrade and will fail the commit on any pre-existing unused-code finding anywhere in the repo, adding latency and friction to every commit.
    • Fix: Move the unused-code check to pre-push or leave it to CI, and split it out of this dependency-upgrade PR.
  • packages/app/src/HDXMultiSeriesTimeChart.tsx:715 -- The <Area> series keeps connectNulls, but Recharts 3 changed that flag to treat nulls as 0; series with gaps can now render as drops-to-zero instead of bridged lines, and nothing in the diff exercises or verifies the prior appearance.
    • Fix: Verify a gapped/sparse series renders as before and drop or adjust connectNulls if the zero-fill changes the visualization.
🔵 P3 nitpicks (3)
  • packages/app/src/components/DBHistogramChart.tsx:23 -- resolvePinnedBarIndex accepts any non-negative integer without bounding it against data.length, relying solely on the [data] effect to clear stale pins; a pinned defaultIndex past the current bucket count would point at no bar.
    • Fix: Also reject indices where idx >= data.length (pass the bucket count in, or clamp at the call site).
  • packages/app/src/HDXMultiSeriesTimeChart.tsx:585 -- graphResults remains typed any[] through the typing-cleanup pass, and the click drill-down indexes row[timestampKey] on that any, bypassing type checking on the drill-down payload path.
    • Fix: Type graphResults as Record<string, unknown>[] and narrow at the read sites.
  • packages/app/src/components/DBHistogramChart.tsx:105 -- HDXHistogramChartTooltip props are typed any, leaving the tooltip content contract unchecked while the surrounding tooltip typings were tightened elsewhere in this PR.
    • Fix: Type the props with the Recharts 3 tooltip content-props type used in the other charts.

Reviewers (10): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, adversarial, api-contract, agent-native, learnings-researcher.

Testing gaps:

  • Interactive event-handler paths are untested: the drag-to-zoom suppressNextClickRef synthetic-click swallow, the onMouseUp drag-vs-click branch (including numeric range ordering), and the nearest-series bolding driven by activeCoordinate.y have no coverage -- only the extracted pure functions (getVisibleLineData, buildActiveClickSeries, resolvePinnedBarIndex, resolveComparisonClick) are unit-tested.
  • CaptureActiveDot nearest-series bolding depends on Recharts' internal render order and is exercised only manually; it is the most upgrade-fragile behavior and warrants an explicit manual check on a multi-series line chart.

- DBHistogramChart: lock the pinned tooltip to the clicked bar by adding
  trigger: 'click' alongside active/defaultIndex, so hovering another bar
  no longer swaps the tooltip's data and View Events link.
- HDXMultiSeriesTimeChart: measure zoom drag distance consistently from the
  native event's offsetX on both mouse down and up, avoiding a coordinate-
  space mismatch with activeCoordinate that could turn a click into a zoom.
- app.scss: scope the chart focus-ring suppression to the surface and its
  descendants, leaving the wrapper's keyboard focus ring intact (WCAG 2.4.11).
- Sparkline: type the height prop as ResponsiveContainer expects.
- HDXMultiSeriesTimeChart: rebuild the click drill-down payload from the
  same visible series set used to draw the lines (legend selection +
  HARD_LINES_LIMIT), so the popover never lists deselected or over-limit
  series. Extracts getVisibleLineData / getSeriesDisplayName helpers reused
  by the lines, payload, and (indirectly) the y-axis domain.
- HDXMultiSeriesTimeChart: measure zoom drag distance from activeCoordinate.x
  on both mouse down and up (offsetX is relative to whichever child SVG
  element is under the pointer, so it mismatched across bars/paths).
- HDXMultiSeriesTimeChart: export a shared ActiveClickPayload type and use it
  for isClickActive/setIsClickActive instead of any; DBTimeChart imports it.
- DBHistogramChart: ignore a non-integer activeIndex so a click resolving to
  no bar can't leave a stuck pin.
- app.scss: suppress the chart focus ring only for pointer focus
  (:focus:not(:focus-visible)), preserving the keyboard focus indicator
  (WCAG 2.4.11). Verified in a browser that click shows no ring.
- Add getVisibleLineData unit tests (selection filtering + line limit).
@github-actions github-actions Bot added review/tier-3 Standard — full human review required and removed review/tier-2 Low risk — AI review + quick human skim labels Jul 9, 2026
Knip flagged it as an unused export; it's only referenced by the exported
ActiveClickPayload, so keep it module-private.
…, nits)

- Zoom-drag distance is measured from a container-relative pointer X
  (clientX minus the chart container's bounding rect) captured on both mouse
  down and up, so it has one stable origin and is never dropped when the
  pointer maps to no data point.
- Histogram: reset the pinned tooltip when the buckets change (a background
  refetch would otherwise repoint the positional pin to the wrong time range)
  and remount the Tooltip on pin change so defaultIndex re-seeds. Ignore a
  non-integer activeIndex.
- Extract buildActiveClickSeries (pure) for the drill-down payload rebuild and
  getActiveLabel to normalize the event label; add unit tests for the payload
  builder and the visible-series set.
- Nits: numeric ReferenceLine x on the time axis, type-guard narrowing instead
  of an as-number cast, type the comparison-chart click arg, drop an
  unreachable String(... ?? '').
@brandon-pereira brandon-pereira requested review from a team and jordan-simonovski and removed request for a team July 9, 2026 20:07
- Debounce the chart ResponsiveContainer resize observer (50ms) so a
  resize -> re-render -> resize cycle can't keep the chart (and the tile
  editor form around it) from settling.
- Histogram pin: only accept a real, in-range integer bar index; null/''/-1
  clear the pin instead of defaulting to bar 0.
- Skip the synthetic click that ends a brush-to-zoom so it can't freeze a
  drill-down tooltip with stale pre-zoom data.
- Order the zoom range numerically (labels are epoch-second strings; a
  lexicographic compare misorders differing digit lengths).
- Doc fix: getVisibleLineData no longer claims to back the y-axis domain.
Recharts 3 keeps chart data, payloads, and graphical-item element props in an
Immer-backed store and freezes them. Three spots mutated those frozen objects,
which threw "This object has been frozen" and crashed the chart in production
builds (dev builds masked it), destabilizing the tile editor and failing the
dashboard/alert E2E suites:

- Tooltip and legend sorted the Recharts payload in place -> copy first
  ([...payload].sort()).
- The active-dot element received the capture Map as a prop, so Recharts froze
  it and dot.set() threw -> pass a stable onCapture callback that writes to the
  closed-over ref instead.

Verified in production E2E mode: dashboard.spec (30 passed) and alerts.spec
(15 passed) are green; the previously deterministic failures now pass.
- P1: only set justZoomedRef when onTimeRangeSelect is defined. On charts
  without it (side panels, alert preview, infra panels) a >=20px drag changed
  no date range, so the [dateRange] effect never reset the flag and the
  post-zoom onClick guard then suppressed every later click, permanently
  disabling the drill-down tooltip.
- ReferenceArea drag band now uses numeric x1/x2 on the numeric time axis,
  matching the click-marker ReferenceLine.
- Extract resolvePinnedBarIndex (histogram pin) and resolveComparisonClick
  (comparison bar selection) as pure exported helpers with unit tests
  (numeric/string/NaN/negative/other-bucket/toggle paths).
…oom click

The onClick guard that skips the synthetic click after a brush-zoom relied on
the [dateRange] effect to clear justZoomedRef, but that effect early-returns
without clearing when the post-zoom range is value-equal to the current one
(e.g. zooming at the min-granularity floor, or a parent that clamps to the same
range) — leaving the flag stuck and swallowing every later drill-down click.

Split the two concerns into separate refs:
- suppressNextClickRef: set on a completed zoom, consumed and cleared by the
  next onClick itself, so it can never leak regardless of the range change.
- justZoomedRef: still tells the [dateRange] effect to preserve zoomOrigin, and
  is now cleared even on the value-equal (no-change) path so it can't leak into
  a later unrelated range change.

Verified drag-to-zoom still passes in production E2E.
…rts-3

# Conflicts:
#	packages/app/src/components/DBTimeChart.tsx
Recharts 3 hoists axis tick labels into a dedicated
`.recharts-<axis>-tick-labels` z-index layer, so they are no longer
nested inside `.recharts-xAxis`. Update getBarLabels() to scope to the
new x-axis tick-labels group, fixing the timeout in the categorical bar
chart ORDER BY E2E test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant