Skip to content

feat(panel): add the shared crosshair, shared tooltip, and drag-to-zoom #83

Description

@xe-nvdk

Split out of #36. These three are one problem, not three, and they are the most intricate part of uPlot's API — keeping them in #36 would have made it unreviewable.

Why they are one problem

cursor.sync does not forward only the cursor. pubSync publishes mousedown, mousemove, mouseup and dblclick, and filters.pub/filters.sub both default to retTrue. Verified in uPlot 1.6.32. So naively enabling sync plus drag-to-zoom gives you:

  • A drag propagates as a selection. On a synced mousemove while dragging, the subscriber copies the source's drag origin and calls setSelX with the selection translated through posToVal/valToPosX.
  • Every synced panel zooms itself. mouseUp runs if (drag.setScale && hasSelect && chgSelect) … _setScale(...) regardless of whether the event came from elsewhere. drag.setScale defaults to true.
  • Every synced panel fires setSelect. So a zoom handler hung on setSelect pushes one history entry per panel for a single drag — and feat(panel): add the time series panel #36's acceptance criterion is that the zoom is undoable with one Back.
  • Double-click resets every panel's axis. dblClick calls autoScaleX() before its e != null guard, so a synced dblclick autoscales the subscriber to its own data extent, silently desyncing it from the dashboard range.

The shape that works

cursor: {
  sync: {
    key: dashboardUid,          // per dashboard, not global
    setSeries: false,
    filters: { pub: (t) => t === 'mousemove', sub: (t) => t === 'mousemove' },
  },
  drag: { x: true, y: false, setScale: false },
}

plus a setSelect hook reading u.select.left/width → u.posToVal → range.zoomTo(...) → clear the selection, and an explicit dblclick handler calling range.reset(). With pub filtered to mousemove, setSelect only ever fires on the panel that owns the pointer, so the N-history-entries problem is gone by construction rather than by guard.

The decision this needs

Panels with timeFrom or timeShift have their own x range (effectiveBounds in queryRunner.ts). Sync's default scales: [xScaleKey, …] syncs by value, so a week-over-week panel gets the crosshair placed at an absolute timestamp outside its own range — pinned to an edge, reading a wrong row.

uPlot documents the lever: a null scale key syncs by relative position instead. Three options, and one must be chosen deliberately:

  1. Panels with a time override join a different sync key, so they sync only with each other.
  2. The whole dashboard syncs by relative position (scales: [null, null]).
  3. Time-overridden panels do not sync at all.

Silently syncing a shifted panel by value is the worst of the four possibilities.

Notes

  • destroy() calls sync.unsub(self), so panel churn does not leak subscriptions — but uPlot's module-level syncs registry never deletes a key. Keying on the dashboard uid is bounded; keying on anything per-mount is not.
  • cursor.sync.setSeries defaults to false in the source, despite the .d.ts comment saying // true. Do not trust that comment elsewhere either.
  • A shared tooltip needs the same event filtering, and feat(panel): add the time series panel #36 already owns a per-panel tooltip that this can extend rather than replace.

Acceptance criteria

  • The crosshair tracks across every panel with no measurable lag
  • Drag-to-zoom updates the dashboard range and is undoable with one browser Back
  • Double-click returns to the previous range rather than autoscaling one panel
  • A panel with timeFrom/timeShift behaves per whichever option above is chosen, and the choice is written down

Activity

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

    dashboardsDashboarding and visualizationenhancementNew feature or requestpanelA dashboard panel/visualization type

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions