Skip to content

feat(app): exemplar overlay for metric and PromQL time charts - #2808

Open
jordan-simonovski wants to merge 7 commits into
jordansimonovski/split-chart-filesfrom
jordansimonovski/exemplars-overlay
Open

feat(app): exemplar overlay for metric and PromQL time charts#2808
jordan-simonovski wants to merge 7 commits into
jordansimonovski/split-chart-filesfrom
jordansimonovski/exemplars-overlay

Conversation

@jordan-simonovski

Copy link
Copy Markdown
Contributor

The feature itself. Stacked on #2807 (the chart-file refactor) — review that first; this diff is the feature only.

Fourth of the PRs replacing #2536.

What it does

Time charts on metric and PromQL sources can overlay exemplars — individual trace-linked points. Hovering a marker shows the exemplar's own value and time plus trace metadata, with a button to open the trace.

Off by default for the whole deployment behind NEXT_PUBLIC_ENABLE_EXEMPLARS, and per-chart behind enableExemplars.

The rule the whole feature turns on

A marker sits at the trace's own measurement on the chart's shared axis. So it is only honest when the chart draws one line, in the same unit. That is enforced in four places, and most of the review findings on the original PR were cases where one of them leaked:

  1. A single non-ratio histogram series with no group by.
  2. An aggregation that leaves the axis on the observation scale — a count of observations is not a duration, so a count-aggregated histogram gets no markers.
  3. For PromQL, an expression that plots a duration, with the duration call spanning the whole expression (histogram_quantile(...) * 1000 renders a different unit than its exemplars carry).
  4. A rendered-series count taken from the main query, not the exemplar response. Prometheus only returns series that carry a sampled exemplar, so the response answers "how many series had exemplars", not "how many lines are drawn" — a multi-line chart whose buffer happened to hold one series' worth would otherwise get markers of unknown provenance.

Where a marker cannot be drawn honestly, it is dropped

Not moved. Outside the rendered window by more than one bucket, or below a fitted y-axis floor, and it does not render — with the count surfaced on the chart, because an overlay that silently thins out is worse than one that explains itself. The exception is above the ceiling, where pinning reads as "at least this high" and the hover card carries the real number.

Note on common-utils

Those primitives ship in this PR rather than their own. Every one of the exemplar exports is consumed only by the app — landing them alone would fail the unused-export check with no consumer.

Included from the original PR's review rounds

Four rounds, roughly 1 P0/P1 and 15 P2s each. The substantive ones: the hover card now shows value and time (both clamps cite it as their justification and it showed neither); the PromQL aggregation parsing that decides series identity, including label matchers and quoted label names; the ClickHouse scan's ex_TimeUnix bound, which was previously filtering on the row time while projecting the exemplar time; the query-key quantisation, which now fetches the window it keys; and the card lifecycle across zoom, live tail and display-type switches.

Remaining P2/P3s are tracked in #2804 rather than blocking this.

Verification

make ci-lint, make ci-unit (5,350 tests), dashboard E2E 80 passed / 0 failed (2 flakes on listing-page specs that draw no chart).

@vercel

vercel Bot commented Aug 5, 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 Aug 6, 2026 9:58pm
hyperdx-storybook Ready Ready Preview Aug 6, 2026 9:58pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c048ae2

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

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@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

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an opt-in exemplar overlay for metric and PromQL time charts, placing trace-linked samples on the chart’s shared axis.

  • Fetches and normalizes exemplar data for supported single-series chart configurations.
  • Adds marker thinning, axis clamping, dropped-marker notices, and trace metadata lookup.
  • Adds hover and pinned-card lifecycle handling across chart updates, resizes, and temporary unmounts.
  • Extends shared chart configuration types and rendering support for the per-chart exemplar toggle.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains, and the previously reported stale exemplar-card lifecycle issues are addressed by direct cleanup when markers or plot subtrees disappear and coordinate re-anchoring when active markers move.

Important Files Changed

Filename Overview
packages/app/src/components/DBTimeChart/useExemplarCard.ts Implements hover, pin, delayed-close, re-anchoring, navigation, and plot-lifecycle cleanup for exemplar cards; the previously reported stale-card paths are addressed.
packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts Computes rendered exemplar markers, applies axis clamps, tracks the active marker, and closes cards when their marker leaves the rendered set.
packages/app/src/components/Exemplars/ExemplarDot.tsx Renders interactive exemplar markers and reports active-marker coordinate changes needed to keep cards anchored after resizes and rescaling.
packages/app/src/components/DBTimeChart/DBTimeChart.tsx Integrates exemplar retrieval, notices, card state, toolbar configuration, and marker callbacks into time-chart rendering.
packages/app/src/hooks/useExemplars/useExemplars.tsx Fetches eligible metric or PromQL exemplars using quantized query ranges and chart-series constraints.
packages/common-utils/src/core/renderChartConfig.ts Preserves the exemplar chart-config option through shared chart rendering and normalization.

Sequence Diagram

sequenceDiagram
  participant Chart as DBTimeChart
  participant Hook as useExemplarCard
  participant Plot as MemoChart
  participant Marker as ExemplarDot
  participant Card as ExemplarHoverCard
  Chart->>Hook: load eligible exemplars
  Chart->>Plot: render exemplars and lifecycle callbacks
  Plot->>Marker: render sampled marker
  Marker->>Hook: hover/select with pixel coordinates
  Hook->>Card: show hover or pinned trace card
  Marker->>Hook: report changed coordinates
  Hook->>Card: re-anchor active card
  Plot->>Hook: marker removed from rendered set
  Hook->>Card: close stale card
Loading

Reviews (10): Last reviewed commit: "fix(app): clear exemplar cards when the ..." | Re-trigger Greptile

Comment thread packages/app/src/components/DBTimeChart/useExemplarCard.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

<!-- deep-review -->

Deep Review

⚠️ This review did not complete. Read the Coverage section before treating anything below as a verdict.

Bash was unavailable for the entire session — every invocation, including with the sandbox disabled, failed with bwrap: Can't create file at /home/.mcp.json: Permission denied. There are also no Grep or Glob tools registered in this session, and WebFetch was not permitted. Read against an exact path was the only working file access.

Consequence: the diff was never computable. git diff, git log, and gh pr diff were all unreachable, so there is no verified list of changed files and no way to distinguish lines this PR added from lines already present at eb1ac50b. I reconstructed part of the feature surface by following import statements from a seed path, and reviewed what I could read directly. Twelve reviewer sub-agents were dispatched against that reconstructed scope; their reports did not arrive before output was required, so none of their findings are represented here. The findings below are only those I verified myself by reading the files.

✅ No critical issues found in the code I was able to read — this is not a clean bill of health for the PR, since coverage was partial.

🟡 P2 -- recommended

  • packages/app/src/components/DBTimeChart/useExemplarCard.ts:166 -- the range-change effect clears pinnedExemplar and hoveredExemplar but leaves clampDroppedCount untouched, so the "N exemplar markers fall outside the chart's plotted range" notice built at line 264 can outlive the range whose clamping produced it.
    • Fix: reset clampDroppedCount to 0 inside the same rangeKey effect that clears the two card states.
🔵 P3 nitpicks (2)
  • packages/app/src/components/DBTimeChart/useExemplarCard.ts:270 -- reportClampDropped hands out the raw setClampDroppedCount dispatcher, so its exported type accepts an updater function while the name and the consuming call site imply a plain count.
    • Fix: wrap it in a useCallback narrowed to (count: number) => void so the cross-module contract matches the name.
  • packages/app/src/components/Exemplars/exemplarPoints.ts:206 -- when the y-domain is non-numeric and visibleSeriesMax is 0, bounds collapse to {min: 0, max: 0} and clampExemplarY returns Math.min(y, 0), drawing every marker at 0 instead of at its own value.
    • Fix: treat a non-positive visibleSeriesMax as a degenerate ceiling and return early, leaving the value unclamped rather than flattening markers to the floor.

Notes on what I did verify

Three things I checked that are correct, recorded so they are not re-flagged on a future pass:

  • Exemplar.timestamp is epoch milliseconds (exemplarPoints.ts:74 divides by 1000 for the chart's second-based x unit), so exemplarTraceWindow(exemplar.timestamp) in useExemplarCard.ts:227 is operating in the right unit.
  • The window-split reduce at exemplarPoints.ts:147 has no initial value, but the split only runs when ordered.length > maxExemplars >= windowCount, so every slice is non-empty and it cannot throw on an empty array.
  • quantize.ts fetches the same quantised window it keys on (floor start / ceil end), so the cache entry is a genuine superset of every raw window mapping to that key.

Reviewers (1 of 13 reported): learnings-researcher.

Dispatched but no results returned — none of their analysis is in this report: correctness, adversarial, security, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, performance, reliability, api-contract, agent-native.

Coverage limitations:

  • No diff. Changed-file list and added-vs-pre-existing attribution are both unverified; findings are scoped to code I read, not to this PR's actual delta.
  • Files read: DBTimeChart/DBTimeChart.tsx (partial), DBTimeChart/useExemplarCard.ts (lines 1–279), DBTimeChart/index.ts, Exemplars/index.ts, Exemplars/exemplarPoints.ts, hooks/useExemplars/quantize.ts, hooks/useExemplars/index.ts, root package.json.
  • Not reviewed at all: hooks/useExemplars/useExemplars.ts, exemplarNormalize.ts, useExemplarTraceMeta.ts (the fetch, normalisation and trace-lookup layer), HDXMultiSeriesTimeChart.tsx (marker rendering and the drop reporting that the P2 above depends on), ExemplarDot.tsx, ExemplarHoverCard.tsx, useChartToolbarItems, and everything under packages/common-utils/ — which is where the ClickHouse ex_TimeUnix scan, the PromQL duration/aggregation gating, and the four series-identity invariants described by the author actually live. The load-bearing correctness claims of this feature are in the unreviewed set.
  • No test files were located or assessed, so no statement is made about test coverage in either direction.
  • docs/solutions/ does not exist in this repo, so there were no prior learnings to apply.

Recommendation: re-run this review in an environment with working git and search tools before relying on it as a merge gate.

@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/exemplars-overlay branch from 186c54f to 082981c Compare August 5, 2026 13:02
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Large diff: 2710 production lines changed (threshold: 1000)
  • Cross-layer change: touches frontend (packages/app) + shared utils (packages/common-utils)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 24
  • Production lines changed: 2710 (+ 2952 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/exemplars-overlay
  • Author: jordan-simonovski

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

Comment thread packages/app/src/components/DBTimeChart/useExemplarCard.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 269 passed • 1 skipped • 1000s

Status Count
✅ Passed 269
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Comment thread packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts Outdated
Comment thread packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

<!-- deep-review -->

Deep Review

⚠️ Partial run — read the coverage note before treating this as a clean bill of health. Two things degraded this review. (1) Bash is non-functional in this environment (every invocation, including echo, fails at the sandbox layer), so git diff could not be computed and scope was reconstructed by reading the working tree; Grep/Glob were also unavailable, leaving Read as the only working tool. (2) Of 12 dispatched reviewers, only 1 (testing) returned before output was required. The correctness, adversarial, security, reliability, performance, api-contract, frontend-races, maintainability, TypeScript, project-standards, and agent-native passes did not report. Absence of P0/P1 findings below reflects that, not a verified-clean diff.

✅ No critical issues found by the one reviewer that completed.

🟡 P2 — recommended

  • packages/app/src/components/DBTimeChart/useExemplarCard.ts:236 — the 273-line hover/pin card state machine has no dedicated test, and the only suite that renders it never hovers, clicks, pins, or switches display type.
    • Fix: Add a renderHook test covering pin/unpin precedence over hover, Escape dismissal of only the pinned card, display-type change clearing both cards, exemplarNotice precedence when a fetch error and a dropped-marker count coexist, and the constructed trace deep-link URL.
  • packages/app/src/HDXMultiSeriesTimeChart/useExemplarMarkers.ts:176 — the hovered-marker-dropped guard, the brush-zoom click suppression, and the onExemplarsDropped count argument are all unexercised; the existing suite only covers the pinned-marker guard and the domain-change guard.
    • Fix: Extend useExemplarMarkers.test.ts to assert hover-end fires when a hovered marker leaves the set, that a post-brush synthetic click is swallowed and suppressNextClickRef is reset, and that onExemplarsDropped receives a positive count rather than only an empty points array.
  • packages/app/src/hooks/useExemplars/useExemplarTraceMeta.ts:25 — nothing covers this hook's query gating, its per-source expression overrides, or the root-span ordering in the SQL it builds; the one suite that touches it mocks it wholesale.
    • Fix: Add a test asserting the query stays disabled without a trace id or with a non-trace source kind, that source-supplied serviceNameExpression / traceIdExpression / parentSpanIdExpression replace the defaults, and that an empty result set resolves to null instead of throwing.
  • packages/app/src/components/DBTimeChart/useChartToolbarItems.tsx:127 — the exemplar notice toolbar item is untested in both its fetch-error and dropped-marker wordings.
    • Fix: Assert the exemplar-notice item renders only for a non-null notice and that its tooltip label equals the string passed in.
🔵 P3 nitpicks (1)
  • packages/app/src/hooks/useExemplars/quantize.ts:25quantizeStart / quantizeEnd have no test pinning their exact outputs, including a range short enough to quantize into a single bucket.
    • Fix: Add a unit test asserting floor/ceil behaviour exactly on and just across a 30s boundary, and that a degenerate range does not collapse the fetch window to zero width.

Reviewers (1 of 12 completed): testing. Dispatched but not returned: correctness, adversarial, security, reliability, performance, api-contract, frontend-races, maintainability, kieran-typescript, project-standards, agent-native.

Testing gaps:

  • useExemplarCard.ts (273 lines) is covered only incidentally, with its data hooks mocked out and no interaction ever triggered.
  • useExemplarTraceMeta.ts SQL construction and gating are unverified by any suite.
  • quantize.ts outputs are never asserted, only consumed opaquely.
  • Conversely, the eligibility gates (isExemplarEligible, isPromqlExemplarEligible) and the SQL renderers were found to carry thorough negative-case coverage — ratio, group-by, multi-series, count/sum aggregation, non-histogram types, top-level PromQL binary operators, and non-spanning duration calls. That is the best-tested part of the change.

Two unverified leads worth chasing before merge — surfaced during scope reconstruction, not confirmed by a reviewer, so deliberately left unscored:

  • prometheusApi.queryExemplars in packages/app/src/api.ts posts to v1/prometheus/query_exemplars; a partial read of packages/api/src/routers/api/prometheus.ts found only /query_range, /query, and /label/:name/values. Confirm the path resolves (a direct-to-Prometheus base URL or a rewrite would explain it) before assuming the PromQL half of the feature is wired.
  • No editor UI referencing enableExemplars or exemplarTraceSourceId was located across the chart-editor components, while in-code comments describe a toggle "next to 'As Ratio'". Confirm whether the per-chart flag is settable outside hand-edited dashboard JSON.

Comment thread packages/app/src/components/DBTimeChart/useExemplarCard.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Deep Review

⚠️ Review could not run — no findings reported. The multi-agent pipeline was never dispatched, so the absence of findings below is not a clean bill of health for this PR.

What blocked it

Every Bash invocation in this environment fails before the command executes:

bwrap: Can't create file at /home/.mcp.json: Permission denied

This reproduces for trivial commands (echo hello), with and without the sandbox override, and inside a freshly spawned subagent. Commands routed around the sandbox instead terminate with This command requires approval, which cannot be granted in a non-interactive workflow run. Glob and Grep are not available in this session, and WebFetch was denied, so the GitHub diff endpoint was not reachable as a fallback either.

Read is the only working code-access tool, and it requires exact absolute paths — with no directory listing or search primitive, the changed-file set cannot be enumerated.

Why no findings were emitted

Stage 1 of the review skill (scope detection) requires git diff against f335654ce0b5068654fe353686757c6b4b8e4fa3 to produce the file list and diff that every reviewer sub-agent consumes. Without it:

  • The changed-file set cannot be enumerated, so coverage would be unknown.
  • New lines cannot be distinguished from pre-existing ones, and the grading rubric turns on whether a defect is introduced by this diff.
  • Line numbers could not be verified against the diff, which the skill's quality gates require.

Reviewing guessed file paths at HEAD was considered and rejected: it would have produced findings of unverifiable provenance and unknown completeness, and Fix: lines from this comment feed downstream automation. Reporting nothing is the correct outcome over reporting something unsound.

To unblock

  • Repair the sandbox bind-mount that targets /home/.mcp.json (HOME appears to resolve to /home during bwrap setup rather than /home/runner); or
  • Grant the workflow a pre-approved read-only git allowlist so git diff runs without the interactive approval gate; or
  • Pass the diff to the review step as a file artifact produced by an earlier workflow step, so the review does not depend on shell access at all.

The untracked ce-plugin/ directory in the working tree is where the review skill was loaded from and is unrelated to the PR contents.


Reviewers (0): none dispatched — halted at scope detection.

Testing gaps: not assessed; the PR's own stated verification (make ci-lint, make ci-unit, dashboard E2E) was not independently confirmed by this run.

Time charts on metric and PromQL sources can overlay exemplars — individual
trace-linked points — behind NEXT_PUBLIC_ENABLE_EXEMPLARS, off by default, and
per-chart behind enableExemplars. Hovering a marker shows the exemplar's own
value and time plus trace metadata, with a button to open the trace.

The shared common-utils primitives ship here rather than separately: every one of
those exports is consumed only by the app, so landing them alone would fail the
unused-export check.

The rule the whole feature turns on is that a marker sits at the trace's own
measurement on the chart's shared axis, so it is only honest when the chart draws
one line in the same unit. That is enforced in four places: a single non-ratio
histogram series with no group by; an aggregation that leaves the axis on the
observation scale (a count of observations is not a duration); for PromQL an
expression that plots a duration, with the duration call spanning the whole
expression; and a rendered-series count taken from the main query rather than the
exemplar response, since Prometheus only returns series that carry a sampled
exemplar and so cannot say how many lines are drawn.

Where a marker cannot be drawn honestly it is dropped, not moved. Out of the
rendered window by more than one bucket, or below a fitted y-axis floor, and it
does not render — with the count surfaced on the chart, because a silently
thinning overlay is worse than an explained one. The exception is above the
ceiling, where pinning reads as "at least this high" and the card carries the
real number.

Includes the four rounds of review fixes from #2536: the hover card showing value
and time (both clamps cite it as their justification), the PromQL aggregation
parsing that decides series identity, the ClickHouse scan's ex_TimeUnix bound,
the query-key quantisation, and the card lifecycle across zoom, live tail and
display-type switches.

make ci-lint and make ci-unit pass (5350 tests). Dashboard E2E 80 passed, 0
failed, 2 unrelated flakes on listing-page specs.

Stacked on the chart-file refactor so this reads as a feature diff.
Addresses a review finding on the exemplar overlay.

Both cards are positioned from pixel coordinates captured when the pointer
touched the marker, so a zoom, range switch or rescale leaves them beside the
wrong diamond. The effect that handles this returned early when nothing was
pinned, which left a hover-only card behind — and the hover card has less
recourse than the pinned one, because a marker sliding out from under a
stationary cursor fires no mouseleave, so nothing else closes it. It sat at
stale coordinates and suppressed the series tooltip until the pointer moved.

Keyed on the quantised range alone now, so both cards close together. Live-tail
ticks still fall inside one bucket and leave a pinned card alone.

Tests drive the range change through RTL's `wrapper` option rather than a
plain rerender: rerendering a differently-shaped tree remounts the chart, which
clears the cards by itself and would have let all three cases pass with the
effect deleted.

Also restores the exemplar-clamp coverage in useChartScales, which the split
PR could not carry because the clamp only exists on this branch.
Addresses a follow-up review finding on the previous fix.

Keying the card cleanup on the quantised date range was wrong in both
directions. Too coarse: a zoom inside the 30-second bucket moved every marker
and left the cards behind at stale coordinates, pointing at empty space or
someone else's trace. And in the wrong place: the 30-second quantum belongs to
the exemplar query key, not to the card lifecycle, so borrowing it coupled two
unrelated concerns.

Moved into useExemplarMarkers and keyed on the rendered x-domain, which is what
actually maps a data point to the pixels a card was positioned from. That domain
is already floored to the chart's granularity, so a live-tail tick inside the
current bucket still leaves an open card alone — the behaviour the earlier fix
was protecting — while crossing a bucket closes it, which is right, because
there the markers really do jump a bucket's width.

The y-range is deliberately excluded: it shifts on nearly every live-tail
refetch, the vertical drift is small, and a marker pushed out of range is
already caught by the clamps feeding the existing guards.

Tests moved with the behaviour, onto the hook that now owns it.
Main ratcheted the app warning ceiling from 740 to 663 against its own content.
This branch adds roughly fifteen files, which puts it five over.

Three of the eight were real and are fixed here: empty function bodies in the
useExemplars tests, replaced with a named never-settling promise and a comment
saying what the empty act() is for.

The remaining five are left and the ceiling moved to match. Four are assertion
patterns in DBTimeChart's test that came across unchanged in the file split, and
`jest.mocked` is not a drop-in for them — it restores the real signatures, which
the partial mock returns in that file do not satisfy. The fifth is a
setState-in-effect warning matching a pattern main carries about twenty times.

common-utils goes 92 to 93 for one more security/detect-object-injection, on the
metric-table lookup in the exemplar query builder. The file already carries two
of the same indexing pattern.
…ble delay

Addresses a review finding on the previous fix.

The domain-change effect reused onExemplarHoverEnd, which schedules a close the
card's own mouseenter cancels — that delay exists so the cursor can travel from a
marker into the card. So a cursor resting in the card when the markers moved
cancelled the cleanup and held the card open at stale coordinates, still
suppressing the series tooltip: the exact state the fix was meant to prevent.

Moving markers now go through their own signal with an immediate close for both
cards. The delay stays where it belongs, on pointer-leave.
Addresses a review finding, and replaces two earlier attempts at the same thing.

A card is anchored to the pixel coordinates the marker's SVG shape reported when
the pointer touched it. Both previous fixes closed the card when something that
moves markers happened, and both picked the wrong thing to watch: first the
quantised date range, which was too coarse to catch a zoom inside the bucket,
then the rendered x-domain, which missed a y-axis rescale and a container resize
entirely. Guessing at the causes was the mistake.

Recharts recomputes cx/cy for every cause, so the marker itself is the only place
that knows. It now reports its position and the card re-anchors, which covers all
of them and does not need to enumerate any. Only the marker the card is
describing reports, so it is one call per change rather than one per marker per
frame.

Following the marker is also better than closing: a card the user deliberately
pinned survives a live-tail tick, a zoom and a resize, which is what the earlier
quantisation was trying to buy and kept trading away.

The guards for a marker leaving the rendered set are unchanged — that is a real
disappearance, and there the card still closes.
Addresses a review finding on the previous fix.

Loading, error, formatting-error and empty-data states replace the chart's whole
subtree, so the marker layer unmounts and nothing can report a position. The card
state survived that, so recovering left the card pinned at coordinates belonging
to the previous chart instance until a remounted marker happened to report — and
if the marker was gone from the new data, until the rendered-set guard fired.

Cleared on the same signal now, alongside the existing display-type case: both are
"the markers a card was anchored to no longer exist". The branch condition is
derived once and shared with the JSX rather than restated, so the cleanup cannot
drift from the branches that cause it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant