Compare commits

...

2 Commits

Author SHA1 Message Date
Abhi Kumar
8e1916daa6 chore: minor cleanup 2026-04-16 00:53:40 +05:30
Abhi Kumar
7eb8806c0f chore: added changes for crosshair sync for tooltip 2026-04-16 00:50:16 +05:30
5 changed files with 84 additions and 3 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
import Legend from 'lib/uPlotV2/components/Legend/Legend';
import {
@@ -8,6 +8,7 @@ import {
import UPlotChart from 'lib/uPlotV2/components/UPlotChart/UPlotChart';
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
import { CursorSyncPanelMetadata } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import noop from 'lodash-es/noop';
import uPlot from 'uplot';
@@ -27,6 +28,7 @@ export default function ChartWrapper({
canPinTooltip = false,
syncMode,
syncKey,
yAxisUnit,
onDestroy = noop,
children,
layoutChildren,
@@ -34,6 +36,9 @@ export default function ChartWrapper({
pinnedTooltipElement,
'data-testid': testId,
}: ChartProps): JSX.Element {
const panelMetadata = useMemo<CursorSyncPanelMetadata>(() => ({ yAxisUnit }), [
yAxisUnit,
]);
const plotInstanceRef = useRef<uPlot | null>(null);
const legendComponent = useCallback(
@@ -99,6 +104,7 @@ export default function ChartWrapper({
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
)}
syncKey={syncKey}
panelMetadata={panelMetadata}
render={renderTooltipCallback}
pinnedTooltipElement={pinnedTooltipElement}
/>

View File

@@ -4,6 +4,7 @@ import cx from 'classnames';
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
import uPlot from 'uplot';
import { syncCursorRegistry } from './syncCursorRegistry';
import {
createInitialControllerState,
createSetCursorHandler,
@@ -40,6 +41,7 @@ export default function TooltipPlugin({
maxHeight = 600,
syncMode = DashboardCursorSync.None,
syncKey = '_tooltip_sync_global_',
panelMetadata,
pinnedTooltipElement,
canPinTooltip = false,
}: TooltipPluginProps): JSX.Element | null {
@@ -100,7 +102,29 @@ export default function TooltipPlugin({
// crosshair / tooltip can follow the dashboard-wide cursor.
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
config.setCursor({
sync: { key: syncKey, scales: ['x', null] },
sync: { key: syncKey, scales: ['x', 'y'] },
});
// Show the horizontal crosshair only when the receiving panel shares
// the same y-axis unit as the source panel. When this panel is the
// source (cursor.event != null) the line is always shown and this
// panel's metadata is written to the registry so receivers can read it.
config.addHook('setCursor', (u: uPlot): void => {
const yCursorEl = u.root.querySelector<HTMLElement>('.u-cursor-y');
if (!yCursorEl) {
return;
}
if (u.cursor.event != null) {
// This panel is the source — publish metadata and always show line.
syncCursorRegistry.setMetadata(syncKey, panelMetadata);
yCursorEl.style.display = '';
} else {
// This panel is receiving sync — show only if units match.
const sourceMeta = syncCursorRegistry.getMetadata(syncKey);
yCursorEl.style.display =
sourceMeta?.yAxisUnit === panelMetadata?.yAxisUnit ? '' : 'none';
}
});
}

View File

@@ -0,0 +1,30 @@
import type { CursorSyncPanelMetadata } from './types';
/**
* Module-level registry that tracks the metadata of the panel currently
* acting as the cursor source (the one being hovered) per sync group.
*
* uPlot fires the source panel's setCursor hook before broadcasting to
* receivers, so the registry is always populated before receivers read it.
*
* Receivers use this to make decisions such as:
* - Whether to show the horizontal crosshair line (matching yAxisUnit)
* - Future: what to render inside the tooltip (matching groupBy, etc.)
*/
const metadataBySyncKey = new Map<
string,
CursorSyncPanelMetadata | undefined
>();
export const syncCursorRegistry = {
setMetadata(
syncKey: string,
metadata: CursorSyncPanelMetadata | undefined,
): void {
metadataBySyncKey.set(syncKey, metadata);
},
getMetadata(syncKey: string): CursorSyncPanelMetadata | undefined {
return metadataBySyncKey.get(syncKey);
},
};

View File

@@ -4,11 +4,29 @@ import type {
ReactNode,
RefObject,
} from 'react';
import type { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type uPlot from 'uplot';
import type { TooltipRenderArgs } from '../../components/types';
import type { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
/**
* Per-panel metadata shared with the cursor sync registry so that
* panels receiving a synced cursor can make informed decisions about
* rendering (e.g. whether to show the horizontal crosshair, what to
* show in the tooltip for the source panel's context).
*
* Add new fields here as sync-aware features need them.
*/
export interface CursorSyncPanelMetadata {
/** Y-axis unit of this panel (e.g. 'ms', 'req/s'). Used to decide
* whether the horizontal crosshair should be shown on receiving panels. */
yAxisUnit?: string;
/** Label dimensions this panel groups by. Will be used to enrich
* tooltip content on receiving panels to show correlated series. */
groupBy?: BaseAutocompleteData[];
}
export const TOOLTIP_OFFSET = 10;
export enum DashboardCursorSync {
@@ -39,6 +57,9 @@ export interface TooltipPluginProps {
canPinTooltip?: boolean;
syncMode?: DashboardCursorSync;
syncKey?: string;
/** Metadata about this panel shared with the sync registry so that
* receiving panels can make context-aware rendering decisions. */
panelMetadata?: CursorSyncPanelMetadata;
render: (args: TooltipRenderArgs) => ReactNode;
pinnedTooltipElement?: (clickData: TooltipClickData) => ReactNode;
maxWidth?: number;

View File

@@ -516,7 +516,7 @@ describe('TooltipPlugin', () => {
);
expect(setCursorSpy).toHaveBeenCalledWith({
sync: { key: 'dashboard-sync', scales: ['x', null] },
sync: { key: 'dashboard-sync', scales: ['x', 'y'] },
});
});