Skip to content

[Bug] Crosshair: the spring restarts on every frame while data motion runs #208

Closed

Description

@toomingos

Version: @tanstack/charts 1.1.0 (57774e1). Split from #188 (section B) so it can be closed on its own.

While a data transition runs, a crosshair or focus guide with a spring transition never moves toward the pointer: every presentation frame cancels its track and starts a new one at the current position. motion.md:59-61 says retargeting "preserves the guide elements and incoming spring velocity".

Why

Repro

Save as packages/charts-core/src/repro.test.ts, run pnpm vitest run packages/charts-core/src/repro.test.ts.

import { scaleLinear } from 'd3-scale'
import { expect, it, vi } from 'vitest'
import { crosshair } from './crosshair'
import { lineY } from './line'
import { motion } from './motion'
import { createChartScene, defineChart } from './scene'

const make = (t: number) => {
  const data = Array.from({ length: 20 }, (_, i) => ({ x: i, y: i + t }))
  const scales = { x: { scale: scaleLinear().domain([0, 19]) }, y: { scale: scaleLinear().domain([0, 40]) } }
  return createChartScene(defineChart({ marks: [lineY(data, { x: 'x', y: 'y', key: 'x' }), crosshair({ x: true, y: false })], scales, guides: false }), { width: 400, height: 200 })
}
const focus = (p: any) => ({ primary: p, group: [p], source: 'pointer' as const, pinned: false })

it('a guide repaint during data motion keeps the crosshair spring running', () => {
  const frames: FrameRequestCallback[] = []
  vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => frames.push(cb))
  const container = document.createElement('div')
  const surface = motion({ transition: { type: 'spring', stiffness: 170, damping: 18 } }).mount(container, () => {})
  surface.render(make(0), { ariaLabel: 'repro' })
  frames.splice(0).forEach((cb) => cb(0)); frames.splice(0).forEach((cb) => cb(5000)) // entrance done
  const next = make(5)
  surface.render(next, { ariaLabel: 'repro' }) // data motion starts
  surface.paintFocus(focus(next.points[2]), null)
  const xs: number[] = []
  for (let i = 0, t = 5000; i < 8; i++) {
    surface.paintFocus(focus(next.points[15]), null) // what updateFocus(..., true) does per presentation frame
    frames.splice(0).forEach((cb) => cb((t += 16)))
    xs.push(Math.round(Number(container.querySelector('.ts-chart__focus-guide-layer line')?.getAttribute('x1'))))
  }
  expect(xs[7]).toBeGreaterThan(xs[0]! + 20)
})

Output on 57774e1:

AssertionError: expected 42 to be greater than 62      (xs = [42, 42, 42, 42, 42, 42, 42, 42])

Control (paintFocus called once): xs = [42, 62, 106, 158, 208, 250, 282, 305], a spring curve toward 316. The per-frame paintFocus stands in for the updateFocus(..., true) call.

Expected

A repaint caused only by data motion keeps the running tracks' start time and velocity. Fix: retarget the tracks instead of cancel + restart; the same focus and targets is a no-op.

Activity

  1. tannerlinsley commented on Oct 14, 2026

    @tannerlinsley
    Member

    Thanks @toomingos, the clock fix in #222 also fixes this freeze.

    I checked the data-frame repaint case in Chromium, Firefox and WebKit. On 1.1.0, the crosshair stays at x = 42.11 through eight repaints. On current main, it advances from 42.11 to 183.703 over those same frames, retaining the guide element and leaving no scheduled frames after cleanup. A dedicated regression test fails against 1.1.0 and passes on main; that follow-up test is still being prepared for merge.

    The fix is merged, but npm is still on 1.1.0. It is included in the pending 1.1.1 version PR #220. This does not eliminate the repeated guide-rendering work tracked in #188, which remains open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions