fix: preserve line continuity at zoom viewport edges - #53
Conversation
📝 WalkthroughWalkthroughThe 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. ChangesZoom window rendering
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
benchmarks/conformance/cases/90-zoomable-time-window/model.tsbenchmarks/conformance/cases/90-zoomable-time-window/view.tsx
| 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 |
There was a problem hiding this comment.
🎯 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-windowRepository: 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)
}))
JSRepository: 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.
| 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, { |
There was a problem hiding this comment.
🎯 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/conformanceRepository: 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.tsxRepository: 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))
JSRepository: 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.
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
lineY.clip: trueso 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
lineYand chart clipping primitives already support the required behavior.Summary by CodeRabbit