You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 globalsetSeries: 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:
Panels with a time override join a different sync key, so they sync only with each other.
The whole dashboard syncs by relative position (scales: [null, null]).
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
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.syncdoes not forward only the cursor.pubSyncpublishesmousedown,mousemove,mouseupanddblclick, andfilters.pub/filters.subboth default toretTrue. Verified in uPlot 1.6.32. So naively enabling sync plus drag-to-zoom gives you:mousemovewhile dragging, the subscriber copies the source's drag origin and callssetSelXwith the selection translated throughposToVal/valToPosX.mouseUprunsif (drag.setScale && hasSelect && chgSelect) … _setScale(...)regardless of whether the event came from elsewhere.drag.setScaledefaults totrue.setSelect. So a zoom handler hung onsetSelectpushes 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.dblClickcallsautoScaleX()before itse != nullguard, so a synced dblclick autoscales the subscriber to its own data extent, silently desyncing it from the dashboard range.The shape that works
plus a
setSelecthook readingu.select.left/width→u.posToVal→range.zoomTo(...)→ clear the selection, and an explicitdblclickhandler callingrange.reset(). Withpubfiltered tomousemove,setSelectonly 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
timeFromortimeShifthave their own x range (effectiveBoundsinqueryRunner.ts). Sync's defaultscales: [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
nullscale key syncs by relative position instead. Three options, and one must be chosen deliberately:scales: [null, null]).Silently syncing a shifted panel by value is the worst of the four possibilities.
Notes
destroy()callssync.unsub(self), so panel churn does not leak subscriptions — but uPlot's module-levelsyncsregistry never deletes a key. Keying on the dashboard uid is bounded; keying on anything per-mount is not.cursor.sync.setSeriesdefaults tofalsein the source, despite the.d.tscomment saying// true. Do not trust that comment elsewhere either.Acceptance criteria
timeFrom/timeShiftbehaves per whichever option above is chosen, and the choice is written down