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

@Fefedu973 Fefedu973 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

coderabbitai Bot 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 name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes preserving line continuity at zoom viewport edges, which is the main change in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot 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 bash
set -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 bash
set -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 bash
set -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 bash
set -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

echo
echo "## local model exports"
sed -n '1,90p' benchmarks/conformance/cases/90-zoomable-time-window/model.ts

echo
echo "## 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 bash
set -euo pipefail

echo "## rest of target view.tsx"
sed -n '560,630p' benchmarks/conformance/cases/90-zoomable-time-window/view.tsx

echo
echo "## 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.width
const scaleY = bounds.height / bounds.height
const 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant