[Perf] Axis tick labels are keyed by value, so relative ticks on a rolling window recreate every label
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- Half a day
- Newbie friendliness
- 45/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- d3, typescript
- Domain
- data-visualization
Research direction
The label key is built in scene.ts around line 1882 and written to data-ts-key in svg-renderer.ts, then matched by reconcile.ts around lines 344-345. Start by running the repro in packages/charts-core/test-repro/10-rolling-windows.test.ts to see the 4 new label nodes. Done means that test passes with a tick-index key, and that the default 'value' behaviour and the existing tests still pass.
Written by the indexing model from the issue text.
Description
Version: @tanstack/charts 1.1.0 (57774e1).
Problem
A rolling line chart (path: { update: 'rolling' }) with x ticks relative to the newest sample (ticks.values: "now - 30 s", "now - 20 s", ...) gets new tick values every step. Tick labels are keyed by value, so every label node is removed and re-created each step.
With default "nice" ticks the values persist, so this only shows with relative or per-step tick values.
Why
- Label key is the tick value:
scene.ts:1882. - The renderer writes it to
data-ts-key(svg-renderer.ts:192) and the reconciler matches nodes by it (reconcile.ts:344-345). No match means remove and re-create. - No
tickLabelsoption changes the key (types.ts:391-405).
Repro
Save as packages/charts-core/src/repro.test.ts, run pnpm vitest run packages/charts-core/src/repro.test.ts.
import { expect, it, vi } from 'vitest'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { lineY } from './line'
import { motion } from './motion'
import { createChartScene, defineChart } from './scene'
const T0 = Date.UTC(2026, 0, 1)
const scene = (start: number) => {
const rows = Array.from({ length: 40 }, (_, i) => ({
id: `r${start + i}`, time: new Date(T0 + (start + i) * 1000), value: 50 + 20 * Math.sin(i / 2),
}))
const end = rows[39]!.time.getTime()
return createChartScene(defineChart({
motion: { path: { update: 'rolling', x: 'shift' }, transition: { type: 'tween', duration: 100 } },
marks: [lineY(rows, { id: 's', x: 'time', y: 'value', key: 'id' })],
scales: {
x: {
scale: scaleUtc().domain([rows[2]!.time, rows[39]!.time]),
axis: { ticks: { values: [30, 20, 10, 0].map((s) => new Date(end - s * 1000)) } },
},
y: { scale: scaleLinear().domain([0, 100]) },
},
clip: true,
}), { width: 300, height: 200 })
}
it('x tick label nodes are reused across a one-step rolling update', () => {
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(() => 1)
const container = document.createElement('div')
const surface = motion({ initial: false }).mount(container, () => {})
const labels = () => [...container.querySelectorAll('text[data-ts-key^="x-tick-label:"]')]
surface.render(scene(0), { ariaLabel: 'rolling' })
const before = labels()
surface.render(scene(1), { ariaLabel: 'rolling' })
expect(labels().filter((node) => !before.includes(node))).toHaveLength(0)
})
Output: AssertionError: expected [ SVGElement{ …(2) }, …(3) ] to have a length of +0 but got 4. Without values the same test passes.
Proposed
tickLabels.key: 'value' | 'index' (default 'value'), switching the key at scene.ts:1882 to the tick index, so a fixed set of label nodes is reused and only text and position update.
Downstream measurement (bklit migration, Chrome, 10 s rolling line with relative x ticks): custom x labels keyed by slot index cut element additions from about 500 to 1.
- Dominant language
- TypeScript
- Stars
- 819
- Forks
- 49
- Avg merge
- 12h 9m
- Merged PRs (30d)
- 6
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TanStack/charts
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Renderer treats a sub-pixel width difference as a resize, which cancels the `motion()` entranceOpen
Difficulty 1/5 Under an hour Newbie friendliness 85/100
TanStack/charts#182 · 1 comment ·
Maintainers usually reply within 1 day
-
brushX with `values` can shorten the selection range when it’s dragged to the plot edgePossibly taken @andersk claimed this 6 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 42/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 57/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
NaturalIntelligence/fast-xml-parser#890 · 1 comment ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
zsviczian/obsidian-excalidraw-plugin#3000 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
pnpm/get.pnpm.io#68 ·
-
[BUG] Cloudflare KV binding 模式下 kvDriver.list() 沿用 EdgeOne 语义,字段名不匹配导致 key 格式下初始化 500 并耗尽 list 配额Possibly taken @wumingzhinu claimed this today. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
OpenListTeam/OpenList-Worker#125 ·
Maintainers usually reply within 2 days
-
Android (vision-camera-location): HybridLocationManager never quits its HandlerThread on disposeOpen
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
margelo/react-native-vision-camera#4217 ·
Maintainers usually reply within 4 days