feat(frontend): price-chart intro sweep (line + crosshair draw left→right) + remove tooltip price box#329
Merged
Merged
Conversation
…ight) + remove tooltip price box
Per user request (2026-05-30):
1. Remove the price tooltip BOX beside the crosshair — `Tooltip content={() => null}`
renders no popup panel; only the crosshair stays (vertical dashed cursor line
+ an activeDot on the Area at the hovered/parked point), so the line no longer
obscures the chart on hover/scrub.
2. Animate the line + crosshair sweeping left→right, ease-out (fast→slow,
decelerating into the latest point on the right), on TWO triggers:
(a) the chart first scrolling into view on the detail page, and
(b) every 1D-5Y period change.
Mechanism — a `.chart-sweep` wrapper around the ResponsiveContainer animates
`clip-path: inset(0 100% 0 0)` → `inset(0 0 0 0)` via a new @Keyframes
(cubic-bezier(0.22,1,0.36,1), 1100ms). One clip reveals the whole SVG at once —
line, gradient fill, and the parked crosshair — so they draw in together. The
wrapper is keyed by `sweepKey`; an IntersectionObserver (threshold 0.35,
one-shot) bumps it on first scroll-into-view, and a period-change effect bumps
it on every 1D-5Y switch. Renders at FINAL state with no class (SSR / no-JS),
and the reduced-motion guard forces `clip-path: none` so those users see the
full chart immediately (no stuck-clipped state).
Removed now-dead tooltip-box styling (tooltipContentStyle / tooltipLabelStyle /
fmtTooltip); isDark now drives only the crosshair cursor + activeDot colors.
Verified (Playwright, real chromium, frame-by-frame): scroll-into-view sweep
clips then reveals (30/30 frames, inset 22%→9%→0%); tooltip box removed (text
len 0) + crosshair line & activeDot present on hover; period change 1Y→1M
re-fires the sweep (20/20 frames re-clip); reduced-motion → clip-path none (full
chart immediately). tsc clean; next build 506 routes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… crosshair dot isn't half-clipped Self-caught during empirical right-edge verification: the parked crosshair activeDot sits at the FLUSH right edge and half-overflows the recharts-surface via `[&_.recharts-surface]:overflow-visible` (PR #322, so the last point's dot isn't clipped in half by the SVG viewport). But the new chart-sweep clip-path ended at `inset(0 0 0 0)` — which clips to the wrapper's box edge and IGNORES overflow-visible, so it re-cut that dot in half (confirmed via a zoom screenshot: a half-circle at the right edge). Fix: the keyframe now ends at `inset(0 -8px 0 -8px)` (negative right inset lets the overflowing dot paint fully; symmetric -8px left keeps the leftmost point safe too). Re-verified: the parked dot renders as a full circle, and the sweep still plays left→right on scroll-into-view + period change, reduced-motion still skips (clip none). https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…above-fold) + motion-rule notes frontend-design-reviewer WARNs (PR #329): - Double-sweep edge case: the `chart-sweep` class was baked into the static className, so the initial key=0 mount animated immediately AND the IntersectionObserver then bumped sweepKey→1 for a second play — a visible stutter when the chart is above the fold (the below-fold normal case hid it). Fix: gate the class on `sweepKey > 0`, so the key=0 mount renders WITHOUT the class (no animation) and the sweep plays exactly ONCE, driven by the IO when the chart scrolls into view (sweepKey 0→1). Period changes bump further, each a fresh single sweep. Adapts the gauge `usePlayOnMount` client-gate idea (here the IO is the play trigger, so sweepKey itself is the gate). Verified: an above-fold chart now plays exactly 1 sweep (was 2). - Motion Rule 1 (transform/opacity-only): added a globals.css note that clip-path is a documented extension — same posture as the gauge's stroke-dashoffset; clip-path:inset() causes no layout reflow and is GPU-compositable, honoring the rule's intent. - Motion Rule 2 (no re-fire on in-page interaction): added a comment that a period switch REPLACES the data series (new line arriving) rather than re-staggering existing content — the case Rule 2 targets — so the re-sweep is the intended "new data drawing in" affordance. activeDot at rest (crosshair anchor at the latest point): kept per user direction — it replaces the removed price box as the rest-state price marker. Verified (Playwright): single-sweep gate (above-fold = exactly 1 play; post-IO wrapper carries chart-sweep); full suite still 5/5 (scroll sweep clips→reveals, tooltip box gone + crosshair/dot present, period re-fire, reduced-motion none). tsc clean; next build 506 routes. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
… put (replace clip-sweep) Three user-reported fixes on the chart intro animation: 1. "crosshair กลืนกับกราฟและพื้นหลัง" — bolder color: slate-600 (light) / slate-300 (dark) + 1.5px solid-ish dash (was slate-400/500 1px, too faint). Applied to BOTH the Recharts parked cursor and the animated overlay so they look identical. 2. "ทำให้ crosshair เลื่อนจากซ้ายไปขวาตามกราฟ" — a self-drawn crosshair overlay (absolute SVG line + dot) now rides the price curve left→right in lockstep with the line draw, driven by rAF (ease-out, fast→slow). The dot's y is found on the real curve via getPointAtLength binary search, so it sits ON the line as it travels. At the end the overlay fades (opacity 0) and Recharts' parked cursor + activeDot take over at the latest point. 3. "เส้นแกนแนวนอนและวันที่ด้านล่างไม่ต้องเลื่อนตาม" — replaced the clip-path-on-the-wrapper sweep (which revealed the WHOLE SVG incl. the X-axis + date labels) with Recharts' own `isAnimationActive` area-draw, which animates ONLY the `.recharts-area` layer. The `.recharts-xAxis` is a sibling layer, so the axis + its date ticks render fully and stay PUT from frame 1. Mechanism: a sweep remount (sweepKey, bumped by the IntersectionObserver scroll-in + every period change) sets `playDraw=true`, which (a) turns on the Recharts area-draw animation for that remount only (rest/resize re-park remounts stay instant), (b) suppresses the Recharts cursor+activeDot so only the animated overlay shows, and (c) starts the rAF that drives the overlay L→R. On completion playDraw flips false. Reduced-motion skips the rAF + passes isAnimationActive=false → full chart immediately. Removed the now-dead `.chart-sweep` @Keyframes + reduced-motion clip override from globals.css. Verified (Playwright, frame-by-frame): X-axis ticks 0px drift during draw (firstTick [302,302], lastTick [932,932]); overlay line x rides 423→676 L→R with dot cy tracking the curve (11 distinct y); overlay opacity=1 during draw, =0 at rest with Recharts parked cursor+dot taking over; + a mid-draw screenshot confirming the full axis is present while the line is ~13% drawn. tsc clean; next build 506 routes. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…reviewer WARN) Cosmetic defensive clarity per the reviewer: at draw completion (p=1) the rAF loop ends without clearing drawRafRef.current, leaving a stale (already-executed) frame id in the ref. The next sweep's cleanup then calls cancelAnimationFrame on that stale id — a spec no-op, not a leak, but cleaner to null it. Set drawRafRef.current = null in the completion branch. No behavior change. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…kill stutter, robust scroll-in
Three user reports on the intro animation, one root cause: TWO animations ran on
separate timelines — Recharts' own area-draw (isAnimationActive) + a separate
rAF moving the crosshair — so they desynced ("ถึงขวาสุดไม่พร้อมกัน"), and
Recharts' per-frame React re-render during its draw made it stutter ("หน่วงและ
กระตุก").
Fix — drive the WHOLE intro from ONE rAF off a single eased progress:
- Recharts area animation OFF (static path, no per-frame React re-render).
- Each frame reveals the line+fill by clipping ONLY `.recharts-area` to [0,x]
(a sibling of `.recharts-xAxis`, so the axis + date labels stay PUT) AND
places the crosshair line+dot at the SAME x → they move and finish in exact
lockstep (verified maxGap=0px, both reach right edge at rx=688/cx=688).
- The dot's y comes from a ONE-TIME precomputed x→y table sampled off the now-
static curve (120 samples), so per-frame cost is an array lerp, not an
18-iteration getPointAtLength path walk → smooth (verified avgDt=16.1ms,
jank=0 over the draw).
- DRAW_MS 1100 → 850 (snappier per "หน่วง").
Robust scroll-in (complaint #1 "เล่นเฉพาะตอนเปลี่ยน period"): the IO threshold
0.35 could miss on a tall layout / slow scrub; lowered to 0.1 + rootMargin, plus
a one-frame getBoundingClientRect fallback so an already-in-view chart at mount
still fires. Verified: a real wheel scroll brings the chart in and the draw
plays (overlay opacity→1).
Cleanup-safe: if a period switch interrupts mid-draw, the effect cleanup clears
the area clip so the line is never left stuck hidden.
Verified (Playwright): scroll-in fires on real scroll; line+crosshair lockstep
(maxGap 0px) reaching the right edge together; 60fps (16.1ms avg, 0 jank);
X-axis 0px drift during draw. tsc clean; next build 506 routes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…57/sweep) + downsample 5Y path User report "เปิดหน้า details ค้างหนัก / กดเปลี่ยน 1D-5Y ก็ค้าง". Root-caused via 6× CPU-throttle profiling (the prior 60fps reading was a desktop-GPU artifact — no throttle). Two culprits, both in the iteration-3 draw: 1. **getPointAtLength is O(path-length) PER CALL.** The dot-y sampler walked the SVG path 121 samples × 16 binary-search iters ≈ 2057 calls per sweep. On a 5Y curve (56 KB `d` string) that was a ~47 s main-thread task under 6× throttle — the whole page froze (one long task measured 66 s). Replaced with PURE MATH: interpolate the in-memory close[] array at the swept x-fraction and map through the y-scale → O(1) per frame, zero path walks. Profiled: getPointAtLength calls/sweep 2057 → 0; 5Y sweep wall 47.5 s → 1.9 s (25×). The y-scale is read ONCE from the curve's getBBox (O(1), not a path walk) so the crosshair dot sits on the line (was 27 px off when I naively assumed [marginTop, surfaceHeight] — Recharts insets the plot by the ~51 px X-axis + ~29 px top pad; now derived from the real bbox → 6 px, sub-pixel on screen). 2. **5Y fed Recharts ~1260 daily points → a 56 KB path** whose ONE-TIME render is a ~380 ms task on a throttled phone. Added `downsample()` — caps the series to 260 points by even stride (always keeping first + last so the price range + latest price + flush-right crosshair stay exact). At 360-700 px chart widths there are far more points than pixels, so this is visually lossless. 5Y path `d` 56 KB → 12 KB (4.8×); shorter windows (≤260 pts) pass through untouched. Also: DRAW_MS 850 → 650 (snappier per "หน่วง"). Measured (6× CPU throttle): animation's share of page Total-Blocking-Time is now ~0 ms (identical TBT with animation vs reduced-motion) — the remaining ~1 s TBT is the detail page's React hydration (ScoreGauge/RankingTable/pillars), which predates this PR and is a separate perf item, not the animation. Verified (Playwright): scroll-in fires; line+crosshair lockstep (0 px) reaching the right edge together; X-axis 0 px drift; dot-on-line ≤6 px (screenshot confirms it rides the line tip); downsample keeps first+last and caps at 260. tsc clean; next build 506 routes. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…ewer + test-engineer) The `downsample` helper (exported from PriceHistoryChart.tsx in the freeze fix) had no test — frontend-design-reviewer flagged it. The project has NO frontend test runner (no vitest/jest, no frontend *.test.* files, CI Frontend job runs only next build), so rather than pull in a framework for one 8-line helper, test-engineer added a standalone node guard (node is already in the env). frontend/components/downsample.test.mjs — 14 assertions across 7 cases run via `node frontend/components/downsample.test.mjs`: identity passthrough (n≤max, max<2), exact length cap (261→260, 1260→260), first+last-point invariant, maxPoints=2 edge, no-duplicate-last on a distinct series, no-fabricated-points. RED phase confirmed against a copy that drops the exact-last push. 14/14 green. Transcription (not import) because there is no TS test harness; the header documents the sync obligation + the "delete + replace with a real .test.ts if vitest is ever added" follow-up. Not CI-wired (no frontend runner to wire into); it's a manual regression guard. next build still 506 routes (Next doesn't pick up the .mjs as a route); ruff clean. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…) + crosshair line within plot area Two user reports (2026-05-30): 1. "เส้น crosshair ล้นออกด้านบนและด้านล่าง" — the overlay crosshair line was y1=0..y2=surfaceHeight, but the surface includes a ~8px top pad + the ~50px X-axis date band below the plot. So the line overflowed above the chart top and down through the date labels. Fixed: span the line over the PLOT AREA only — [plotTop, plotTop+plotH], the same geometry already derived in prep() from the curve bbox + y-domain. Verified the line now runs [8,258] (the Recharts plot clip-rect) instead of [0,288]; screenshot confirms it starts at the chart top and ends exactly at the X-axis, no overflow. 2. "ไม่ต้องเล่น animation ตอนเข้าหน้า / ตอนรีเฟรช ... ให้เล่นเฉพาะตอนเปลี่ยน 1D-5Y" — removed the IntersectionObserver scroll-into-view trigger entirely. The intro now plays ONLY on a period switch: the period effect keeps its firstPeriodRender guard so the initial mount (and every refresh, which is a fresh mount) paints the chart statically, and only an explicit 1D-5Y button press sets playDraw + bumps sweepKey. Removed the now-unused hasSwept ref. Verified (Playwright): NO draw on page load (overlay stays opacity 0, area never clipped — chart fully shown on arrival); period change DOES fire the draw; crosshair line stays within the plot area (no top/bottom overflow). tsc clean; next build 506 routes. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…ntend-design-reviewer WARN The sweepKey/playDraw declaration comment still said the sweep fires on "IO scroll-in or period change", but the IntersectionObserver scroll-in trigger was removed in 2f0bee0 — the sweep now fires ONLY on a 1D-5Y period change. Comment-only; no behavior change. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…n scrub AND during the draw User 2026-05-30: "ทำให้ตัวเลขราคา ราคาที่เปลี่ยนแปลง และวันที่ เปลี่ยนไปตามจุดที่ crosshair อยู่ ทั้งตอนผู้ใช้ลากเองและตอนเล่น animation". Two drive paths, each chosen to avoid the per-frame-setState freeze that the earlier getPointAtLength bug taught us: - SCRUB (user drag): AreaChart `onMouseMove` → `setHoverIndex(activeTooltipIndex)`; the headline JSX renders `headlineAt(hoverIndex ?? lastIdx)` — price, change vs the window's first close, %, direction color, and date all track the dragged point. Cleared to null (→ snaps back to latest) on pointer up / leave / cancel (alongside the existing restKey re-park). State is fine here: hover events fire at pointer rate and Recharts already re-renders on scrub. - ANIMATION (the intro draw): the rAF writes the headline spans IMPERATIVELY via refs (priceRef/changeAbsRef/changePctRef/changeArrowRef/changeRowRef/asOfRef) — `writeHeadline(close, date)` each frame, NO setState — so the 60fps sweep never re-renders React (a per-frame setState would re-render Recharts 60×/s and re-freeze the page). hoverIndex stays null during the draw (onMouseMove early- returns on playDraw). The end-of-draw setPlayDraw(false) re-render restores the latest value via JSX = the last animated frame, so no visible snap. Shared formatting: hoisted a module-level `fmtDateLabel` so the in-render formatTooltipLabel and the out-of-render rAF writer format the date identically. The change is always measured from the window's FIRST close (Google-Finance convention), matching the static headline. Verified (Playwright): scrub moves price+change+date to the crosshair point ($214.25→$181.57@25%→$186.49@60%, dates Aug 27 2025 / Dec 31 2025); release snaps back to latest; animation counts the headline through 38 distinct prices across 39 frames ending on latest; mobile 6× throttle shows NO >250ms freeze from the headline writes (only Recharts' one-time 5Y render). tsc clean; next build 506. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…bing (frontend-design-reviewer WARNs)
Three frontend-design-reviewer WARNs on the headline-tracking commit:
- Items 1+2 (mid-draw reconcile fight): a ResizeObserver re-park (setLayoutKey)
firing DURING the ~650ms intro draw would reconcile the headline spans back to
the latest value for ~1 frame, fighting the rAF's imperative writes (a flicker
+ a possible 1-frame doubled color class). Guard: a `playDrawRef` mirror of
playDraw, checked inside the ResizeObserver's debounced timer — skip the
layoutKey bump while the draw runs (the draw is ≤650ms; the next genuine resize
or the rest state re-parks correctly anyway). Closes both WARNs with one guard.
- Item 3 (period label misleading while scrubbing): the `scrubbing` flag was
computed but unused — PERIOD_LABEL ("past year") stayed visible even when the
headline showed a scrubbed mid-window point, where the change is measured from
the window start to THAT point (so "past year" mislabels it). Now hidden while
scrubbing (`!scrubbing && <span>{PERIOD_LABEL[period]}</span>`); restored at
rest / during the animation where it correctly describes the full window.
- Item 4 (scrub re-render cost): profiled — a touch-drag scrub across a 5Y
(260-pt) chart at 6× CPU throttle produced ZERO long tasks (>100ms: 0, >250ms:
0). setHoverIndex's added re-render is negligible on top of Recharts' existing
per-move cursor re-render. No action needed.
Verified (Playwright): headline tracking regression suite still 6/6 green
(scrub price+change+date, release snap-back, animation count-up, end-on-latest,
no-freeze); scrub at 6× throttle = 0 long tasks. tsc clean; next build 506.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…node spans + no imperative classList
User-reported: scrubbing the crosshair OR toggling 1D-5Y back and forth threw an
app error, and scrub "เลื่อนได้บ้างไม่ได้บ้าง" (worked intermittently). A/B-tested
the committed build vs the fix — the committed build reproducibly throws
`NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed
is not a child of this node` on rapid scrub; the fix shows 0 errors across the
same paths.
ROOT CAUSE: the headline-tracking commit drove price/change/% spans imperatively
with `ref.textContent = …` (per animation frame), but those spans had MULTIPLE
JSX children (e.g. a literal "$" + an expression = two text nodes; "(" + expr +
"%)" = three). `textContent =` collapses all children to ONE text node, but
React's vdom still believes the original multi-node structure exists. The next
re-render (a scrub `setHoverIndex`, or a period change) reconciles against a text
node that's no longer in the DOM → removeChild/insertBefore on a detached node →
crash; when it didn't hard-throw it left the scrub silently not updating
("ได้บ้างไม่ได้บ้าง").
FIX:
- Every imperatively-written span now holds exactly ONE text node — a single
template-literal child: `{`$${hv.price.toFixed(2)}`}`, `{`${sign}${abs}`}`,
`{`(${sign}${pct}%)`}`. textContent replacement now matches React's vdom model,
so re-renders reconcile cleanly. (changeArrowRef / asOfRef were already single-
node.)
- Dropped the imperative classList color-swap on the change row (another
React-owns-className-vs-imperative conflict, and it lost to the globals.css
OKLCH !important anyway). The row color is left to React (correct per-point on
scrub; steady through the animation while the numbers count up — Google-Finance
behavior). changeRowRef stays attached but unread.
A/B verified (Playwright): committed build → removeChild crash on rapid scrub;
this build → 0 errors across desktop (scrub enter/leave cycles · hover+period
interleave · rapid scrub ×4) AND mobile touch (scrub · rapid period tap · scrub-
during-anim). Headline tracking regression still 6/6 (scrub price+change+date,
snap-back, animation count-up, no freeze). tsc clean; next build 506 routes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…ad of Recharts' (which snapped to defaultIndex)
User: the crosshair didn't follow the drag — slow scrub kept snapping it back to
the right edge, fast scrub left it stuck at the right and it only jumped to the
release point when you stopped, even though the price numbers tracked correctly.
ROOT CAUSE: every scrub fires setHoverIndex → a parent re-render → Recharts
re-applies the `<Tooltip defaultIndex={last}>` and snaps its BUILT-IN cursor back
to the latest point on every render. So the built-in cursor literally cannot
track a moving finger across the per-move re-renders (measured: cursorX pinned at
688px/right-edge while mouseX swept 69→619px; on fast-scrub-then-stop the cursor
sat at 688 and only moved to the release point after the renders stopped). The
headline was unaffected because it reads `hoverIndex` directly, not Recharts'
index.
FIX — stop using Recharts' cursor for the crosshair; draw our OWN:
- `<Tooltip cursor={false}>` + `<Area activeDot={false}>` — Recharts no longer
draws a competing crosshair. The Tooltip stays only to harvest the scrubbed
index via onMouseMove (state.activeTooltipIndex).
- A new effect positions the existing overlay SVG (the same line+dot the intro
animation uses) at `hoverIndex` on every hoverIndex/data/theme/remount change:
x = index/(n-1)·plotW, y = closeToY(close). hoverIndex null = rest → parks at
the latest point. Skipped while playDraw (the intro rAF owns the overlay). The
overlay tracks the index we control, so re-renders can't snap it back.
- Extracted the plot geometry (surface width + price→y over the real plot area,
from the curve bbox) into a shared module-level `measurePlot()` used by both
the intro rAF and this scrub effect.
- The intro draw now ENDS with the overlay left visible at the latest point
(was: hide + hand to Recharts' cursor) — seamless handoff to the rest crosshair.
Verified (Playwright): slow scrub cursorX tracks mouseX gap ≤2px (was 619px, 0/9
steps pinned-right now vs 9/9); fast-scrub-then-stop cursor lands at the release
point immediately (241px, was 688); screenshot shows the dashed line + on-curve
dot at the scrubbed point with the matching headline. Crash repro still 0 errors;
headline tracking 6/6; mobile touch 0 errors. tsc clean; next build 506 routes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…change + show rest crosshair at load (reviewer FAILs)
Two frontend-design-reviewer FAILs on the crosshair-follows-scrub commit, plus a
self-caught follow-on:
- FAIL Item 2 — rest dot half-clipped at the flush-right edge: the overlay <svg>
had no overflow attr, so the rest crosshair dot at cx=w (r=4) was cut by the
svg's own viewport. Added `overflow="visible"` (safe — globals.css
`html,body{overflow-x:clip}` is the document-level backstop).
- FAIL Item 4b — period label vanished after a period switch: hoverIndex kept the
stale index from the old window, so `scrubbing` stayed true and the
`{!scrubbing && PERIOD_LABEL}` conditional hid the label permanently. Added
`setHoverIndex(null)` to the period effect.
- Self-caught (surfaced verifying Item 2): the rest crosshair didn't appear on
INITIAL load — on first mount measurePlot returns null (Recharts hasn't painted
the curve yet) and nothing re-triggered the effect until the first hover.
Wrapped the positioning in a `place(tries)` that retries on rAF (capped 20)
until the curve is measurable, so the rest crosshair shows from load.
Verified (Playwright): rest crosshair visible at t=0.5s (opacity 1, cx=688 right
edge, overflow visible); period label hides on scrub + reappears ("past 6 months")
after a 1Y→6M switch; cursor still tracks mouse ≤2px; crash repro 0 errors;
headline tracking 6/6. tsc clean; next build 506 routes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per user request (2026-05-30): (1) remove the price tooltip box beside the crosshair (it obscured the chart on hover), and (2) animate the line + crosshair sweeping left→right, ease-out (fast→slow) on two triggers — when the chart first scrolls into view on the detail page, and on every 1D–5Y period change. Frontend-only; zero schema / compute change.
Changes
Tooltip content={() => null}renders no popup panel; only the crosshair remains (vertical dashedcursorline + anactiveDoton the Area at the hovered/parked point). The price line no longer hides behind a box on hover/scrub..chart-sweepwrapper around theResponsiveContaineranimatesclip-path: inset(0 100% 0 0)→inset(0 0 0 0)via a new@keyframes chart-sweep(cubic-bezier(0.22,1,0.36,1), 1100ms — ease-out, decelerating into the latest point on the right). One clip reveals the whole SVG at once — line, gradient fill, and the parked crosshair — so they draw in together.sweepKey. AnIntersectionObserver(threshold 0.35, one-shot via a ref gate) bumps it the first time the chart scrolls into view; aperiodeffect bumps it on every 1D–5Y switch (skipping the first render, which the observer owns).prefers-reduced-motionguard, ANDclip-pathis forced tononethere, so those users see the full chart immediately (no stuck-clipped state). SSR / no-JS render at the final state too.tooltipContentStyle/tooltipLabelStyle/fmtTooltip;isDarknow drives only the crosshair cursor + activeDot colors.Verification (Playwright, real chromium, frame-by-frame)
inset 22% → 9% → 0%right-side), settling to fully revealed ✅clip-path: none, full chart immediately ✅tsc --noEmitclean on the edited file;next build→ 506 routes. Lockstep viaPHASE_STATUS_INFLIGHT.md(per #237).https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
Generated by Claude Code