Skip to content

fix: preserve line continuity at zoom viewport edges - #53

Open
Fefedu973 wants to merge 2 commits into
TanStack:mainfrom
Fefedu973:fix/zoom-line-viewport-continuity
Open

fix: preserve line continuity at zoom viewport edges#53
Fefedu973 wants to merge 2 commits into
TanStack:mainfrom
Fefedu973:fix/zoom-line-viewport-continuity

Conversation

@Fefedu973

@Fefedu973Fefedu973 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Fix the zoomable time-window example so continuous lines reach the viewport boundaries correctly while zooming and panning.

Problem

The example currently filters the dataset strictly to points inside the visible time window before passing it to lineY.

When a viewport boundary falls between two observations, the nearest point outside the viewport is removed. The segment crossing that boundary can no longer be drawn, so the line stops at the first or last visible point instead of continuing naturally to the chart edge.

Changes

  • Add a helper that keeps the visible rows plus one neighboring row before and after the current viewport.
  • Use the extended dataset for lineY.
  • Keep dots restricted to rows actually inside the viewport.
  • Enable clip: true so the line geometry outside the plot area is clipped cleanly.

Result

Lines now remain visually continuous while zooming and panning without rendering offscreen dots or passing the entire dataset to the line mark.

This change only updates the official zoomable time-window example. The existing lineY and chart clipping primitives already support the required behavior.

Summary by CodeRabbit

  • New Features
    • Improved zoomed chart rendering by extending connecting lines to include adjacent data points.
    • Kept data markers limited to the currently visible time window.
    • Enabled chart clipping to keep visual elements within the chart area.

@coderabbitai

coderabbitaiBot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The zoomable time-window model now provides visible rows with bounded neighbors. The chart uses those rows for line rendering, keeps dots within the zoom window, and enables clipping.

Changes

Zoom window rendering

Layer / File(s)Summary
Neighboring zoom data
benchmarks/conformance/cases/90-zoomable-time-window/model.ts
Adds visibleZoomDataWithNeighbors, which returns visible rows plus one adjacent row on each side within array bounds.
Chart mark data selection
benchmarks/conformance/cases/90-zoomable-time-window/view.tsx
Uses visible rows for dots and neighboring rows for the line. Enables chart mark clipping.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers:tannerlinsley

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes preserving line continuity at zoom viewport edges, which is the main change in the pull request.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@benchmarks/conformance/cases/90-zoomable-time-window/model.ts`:
- Around line 56-60: The neighbor scan in visibleZoomDataWithNeighbors must not
break when a timestamp exceeds end, because rows may be out of order. Remove the
early termination and scan every row, preserving selection of the first visible
row and collecting neighbors based on each row’s timestamp within the window;
alternatively, explicitly enforce and document an ascending Date invariant
before retaining the break.
In `@benchmarks/conformance/cases/90-zoomable-time-window/view.tsx`:
- Around line 73-86: Update the line geometry bounds in the chart definition
around lineY and visibleZoomDataWithNeighbors to use the rendered lineRows data,
while keeping visibleRows for the dot mark. Build the role: 'line' bounds from
lineRows and clip the resulting rectangle to the chart area so it remains within
the geometry contract.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: acf98fd1-a1c1-4cdb-a976-6eb880e9f732

📥 Commits

Reviewing files that changed from the base of the PR and between 583129a and 9542886.

📒 Files selected for processing (2)
  • benchmarks/conformance/cases/90-zoomable-time-window/model.ts
  • benchmarks/conformance/cases/90-zoomable-time-window/view.tsx

Comment on lines +56 to +60
for (let index = 0; index < rows.length; index += 1) {
const timestamp = rows[index]!.Date.getTime()
if (timestamp < start) continue
if (timestamp > end) break
if (firstVisible < 0) firstVisible = index

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bashset -euo pipefail
ast-grep outline benchmarks/conformance/cases/90-zoomable-time-window/model.ts --items all
rg -n -C 8 '\bselectZoomRows\b|\bzoomRows\b' \
benchmarks/conformance/cases/90-zoomable-time-window
rg -n -C 6 'sort\(|Date\.getTime\(|Date' \
packages/charts-demo-data \
benchmarks/conformance/cases/90-zoomable-time-window

Repository: TanStack/charts

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bashset -euo pipefail
# Inspect the target file around the helper and data source usages.
sed -n '1,140p' benchmarks/conformance/cases/90-zoomable-time-window/model.ts
# Locate selectZoomRows definitions/usages narrowly.
rg -n -C 5 '\bsselectZoomRows\b|selectZoomRows\('.# Locate aapl dataset/exports and nearby file contents.
rg -n -C 6 '(aapl|Aapl|AAPL|AaplRow)' benchmarks/conformance/cases/90-zoomable-time-window packages/charts-demo-data | head -n 240
# Behavioral probe: implement the helper loop for representative orderings.
node - <<'JS'function selectZoomRows(rows, window) { const start = window.start.getTime() const end = window.end.getTime() let firstVisible = -1 const visible = [] for (let index = 0; index < rows.length; index += 1) { const timestamp = rows[index].Date.getTime() if (timestamp < start) continue if (timestamp > end) break if (firstVisible < 0) firstVisible = index visible.push(rows[index]) } return visible}const window = { start: new Date('2007-08-24T00:00:00Z'), end: new Date('2007-08-31T00:00:00Z') }const rows = [ {Date: new Date('2007-08-24Z')}, {Date: new Date('2007-08-27Z')}, {Date: new Date('2007-08-28Z')}, {Date: new Date('2007-08-25Z')}, {Date: new Date('2010-01-01Z')}]console.log(JSON.stringify({ ascendingSlice: selectZoomRows( rows.filter((_, i, a) => i < 3), window ), outOfOrderSlice: selectZoomRows(rows, window), descendingSlice: selectZoomRows(rows.slice(0, 3).reverse(), window)}))JS

Repository: TanStack/charts

Length of output: 29091


Fix the neighbor scan to be order-agnostic.

visibleZoomData selects rows by timestamp, but the new visibleZoomDataWithNeighbors stops the scan after timestamp > end. That can miss out-of-order rows still inside the window and return neighbors for only a prefix of the selected rows. Update it to scan all rows from both sides/nearest rows, or keep it dependent on an explicit ascending Date invariant.

🤖 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 `@benchmarks/conformance/cases/90-zoomable-time-window/model.ts` around lines
56 - 60, The neighbor scan in visibleZoomDataWithNeighbors must not break when a
timestamp exceeds end, because rows may be out of order. Remove the early
termination and scan every row, preserving selection of the first visible row
and collecting neighbors based on each row’s timestamp within the window;
alternatively, explicitly enforce and document an ascending Date invariant
before retaining the break.

Comment on lines +73 to +86
const visibleRows = visibleZoomData(zoomRows, state.window)
const lineRows = visibleZoomDataWithNeighbors(zoomRows, state.window)
const definition = useMemo(
() =>
defineChart(
defineChart({
marks: [
lineY(rows, {
lineY(lineRows, {
x: 'Date',
y: 'Close',
stroke: color,
strokeWidth: 2.5,
}),
dot(rows, {
dot(visibleRows, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bashset -euo pipefail
rg -n -C 8 'zoomGeometry|pointsBounds|geometry|role|visibleZoomData' \
benchmarks/conformance

Repository: TanStack/charts

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bashset -euo pipefail
echo"## target view.tsx relevant sections"
sed -n '1,180p' benchmarks/conformance/cases/90-zoomable-time-window/view.tsx
echo
sed -n '480,560p' benchmarks/conformance/cases/90-zoomable-time-window/view.tsx
echoecho"## local model exports"
sed -n '1,90p' benchmarks/conformance/cases/90-zoomable-time-window/model.ts
echoecho"## focused conformance files"
sed -n '20,30p' benchmarks/conformance/cases/90-zoomable-time-window/case.json
rg -n -C 4 'zoomGeometry|pointsBounds|geometry|role' benchmarks/conformance/cases/90-zoomable-time-window/view.tsx

Repository: TanStack/charts

Length of output: 13027


🏁 Script executed:

#!/usr/bin/env bashset -euo pipefail
echo"## rest of target view.tsx"
sed -n '560,630p' benchmarks/conformance/cases/90-zoomable-time-window/view.tsx
echoecho"## behavioral simulation of point/window/data splits"
node - <<'JS'const zoomFullDomain = [ new Date(Date.UTC(2018, 0, 2)), new Date(Date.UTC(2018, 0, 18)),]const row = (dayOffset) => ({ Date: new Date(zoomFullDomain[0].getTime() + dayOffset * 86400_000), Close: dayOffset,})const zoomRows = Array.from({ length: 17 }, (_, i) => row(i))const window = { start: zoomRows[6].Date, end: zoomRows[8].Date }function visibleZoomData(rows, window) { const start = window.start.getTime() const end = window.end.getTime() return rows.filter((row) => { const timestamp = row.Date.getTime() return timestamp >= start && timestamp <= end })}function visibleZoomDataWithNeighbors(rows, window) { const start = window.start.getTime() const end = window.end.getTime() let firstVisible = -1 let lastVisible = -1 for (let index = 0; index < rows.length; index += 1) { const timestamp = rows[index].Date.getTime() if (timestamp < start) continue if (timestamp > end) break if (firstVisible < 0) firstVisible = index lastVisible = index } if (firstVisible < 0) return [] return rows.slice(Math.max(0, firstVisible - 1), Math.min(rows.length, lastVisible + 2))}function pointsBounds(points, bounds, scaleX, scaleY, color) { if (points.length === 0) return null let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity const halfStroke = 1.25 for (const [px, py] of points) { minX = Math.min(minX, px) maxX = Math.max(maxX, px) minY = Math.min(minY, py) maxY = Math.max(maxY, py) } minX = Math.max(bounds.left, minX - halfStroke) maxX = Math.min(bounds.left + bounds.width, maxX + halfStroke) minY = Math.max(bounds.top, minY - halfStroke) maxY = Math.min(bounds.top + bounds.height, maxY + halfStroke) if (maxX - minX < 0 || maxY - minY < 0) return null return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, paint: color }}const modelStart = new Date('2018-01-07T00:00:00Z')const modelEnd = new Date('2018-01-09T00:00:00Z')const bounds = { left: 58, top: 56, width: 450, height: 340 }const scaleX = bounds.width / bounds.widthconst scaleY = bounds.height / bounds.heightconst visible = visibleZoomData(zoomRows, window)const lineData = visibleZoomDataWithNeighbors(zoomRows, window)console.log(JSON.stringify({ windowRange: [window.start.toISOString().slice(0,10), modelEnd.toISOString().slice(0,10)], visibleDataDays: visible.map(r => r.Date.toISOString().slice(0,10)), lineDataDays: lineData.map(r => r.Date.toISOString().slice(0,10)), visiblePointsCount: visible.length, linePointsCount: lineData.length, visibleBoundsIfUsed: pointsBounds(visible.map(r => [r.Date.getTime(), r.Close]), bounds, scaleX, scaleY, 'red'), lineBoundsIfUsed: pointsBounds(lineData.map(r => [r.Date.getTime(), r.Close]), bounds, scaleX, scaleY, 'red'),}, null, 2))JS

Repository: TanStack/charts

Length of output: 1545


Use the rendered line data for line geometry.

lineY renders lineRows, and those include the window neighbors needed for viewport-edge line segments. Keep visibleZoomData for role: 'dot', but build the role: 'line' bounds from visibleZoomDataWithNeighbors(zoomRows, state.window); clip the returned rectangle into the chart area to keep the geometry contract aligned with the rendered mark.

🤖 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 `@benchmarks/conformance/cases/90-zoomable-time-window/view.tsx` around lines
73 - 86, Update the line geometry bounds in the chart definition around lineY
and visibleZoomDataWithNeighbors to use the rendered lineRows data, while
keeping visibleRows for the dot mark. Build the role: 'line' bounds from
lineRows and clip the resulting rectangle to the chart area so it remains within
the geometry contract.

Sign up for freeto 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

@Fefedu973