From ed6a9d65aabe6924e8b800c068be8c4794b22f39 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 15 Jun 2022 12:45:54 -0700 Subject: [PATCH] Heatmap: implement cursor sync (#50271) Co-authored-by: Leon Sorokin --- .../panel/heatmap-new/HeatmapPanel.tsx | 5 + public/app/plugins/panel/heatmap-new/utils.ts | 117 +++++++++++++----- 2 files changed, 92 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 5c4d536c7bd..3f5c8aa1bda 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -8,6 +8,7 @@ import { Portal, ScaleDistribution, UPlotChart, + usePanelContext, useStyles2, useTheme2, VizLayout, @@ -34,11 +35,13 @@ export const HeatmapPanel: React.FC = ({ height, options, fieldConfig, + eventBus, onChangeTimeRange, replaceVariables, }) => { const theme = useTheme2(); const styles = useStyles2(getStyles); + const { sync } = usePanelContext(); // ugh let timeRangeRef = useRef(timeRange); @@ -113,6 +116,7 @@ export const HeatmapPanel: React.FC = ({ return prepConfig({ dataRef, theme, + eventBus, onhover: onhover, onclick: options.tooltip.show ? onclick : null, onzoom: (evt) => { @@ -124,6 +128,7 @@ export const HeatmapPanel: React.FC = ({ isToolTipOpen, timeZone, getTimeRange: () => timeRangeRef.current, + sync, palette, cellGap: options.cellGap, hideLE: options.filterValues?.le, diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts index 98cfdc150a0..d9be050641e 100644 --- a/public/app/plugins/panel/heatmap-new/utils.ts +++ b/public/app/plugins/panel/heatmap-new/utils.ts @@ -1,8 +1,13 @@ import { MutableRefObject, RefObject } from 'react'; -import uPlot from 'uplot'; +import uPlot, { Cursor } from 'uplot'; import { + DashboardCursorSync, DataFrameType, + DataHoverClearEvent, + DataHoverEvent, + DataHoverPayload, + EventBus, formattedValueToString, getValueFormat, GrafanaTheme2, @@ -55,6 +60,7 @@ export interface HeatmapZoomEvent { interface PrepConfigOpts { dataRef: RefObject; theme: GrafanaTheme2; + eventBus: EventBus; onhover?: null | ((evt?: HeatmapHoverEvent | null) => void); onclick?: null | ((evt?: any) => void); onzoom?: null | ((evt: HeatmapZoomEvent) => void); @@ -70,12 +76,14 @@ interface PrepConfigOpts { valueMax?: number; yAxisConfig: YAxisConfig; ySizeDivisor?: number; + sync?: () => DashboardCursorSync; } export function prepConfig(opts: PrepConfigOpts) { const { dataRef, theme, + eventBus, onhover, onclick, onzoom, @@ -90,8 +98,12 @@ export function prepConfig(opts: PrepConfigOpts) { valueMax, yAxisConfig, ySizeDivisor, + sync, } = opts; + const xScaleKey = 'x'; + const xScaleUnit = 'time'; + const pxRatio = devicePixelRatio; let heatmapType = dataRef.current?.heatmap?.meta?.type; @@ -131,8 +143,8 @@ export function prepConfig(opts: PrepConfigOpts) { onzoom && builder.addHook('setSelect', (u) => { onzoom({ - xMin: u.posToVal(u.select.left, 'x'), - xMax: u.posToVal(u.select.left + u.select.width, 'x'), + xMin: u.posToVal(u.select.left, xScaleKey), + xMax: u.posToVal(u.select.left + u.select.width, xScaleKey), }); u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false); }); @@ -140,7 +152,7 @@ export function prepConfig(opts: PrepConfigOpts) { // this is a tmp hack because in mode: 2, uplot does not currently call scales.x.range() for setData() calls // scales.x.range() typically reads back from drilled-down panelProps.timeRange via getTimeRange() builder.addHook('setData', (u) => { - //let [min, max] = (u.scales!.x!.range! as uPlot.Range.Function)(u, 0, 100, 'x'); + //let [min, max] = (u.scales!.x!.range! as uPlot.Range.Function)(u, 0, 100, xScaleKey); let { min: xMin, max: xMax } = u.scales!.x; @@ -149,7 +161,7 @@ export function prepConfig(opts: PrepConfigOpts) { if (xMin !== min || xMax !== max) { queueMicrotask(() => { - u.setScale('x', { min, max }); + u.setScale(xScaleKey, { min, max }); }); } }); @@ -159,6 +171,14 @@ export function prepConfig(opts: PrepConfigOpts) { rect = r; }); + const payload: DataHoverPayload = { + point: { + [xScaleUnit]: null, + }, + data: dataRef.current?.heatmap, + }; + const hoverEvent = new DataHoverEvent(payload); + let pendingOnleave = 0; onhover && @@ -166,20 +186,25 @@ export function prepConfig(opts: PrepConfigOpts) { if (u.cursor.idxs != null) { for (let i = 0; i < u.cursor.idxs.length; i++) { const sel = u.cursor.idxs[i]; - if (sel != null && !isToolTipOpen.current) { - if (pendingOnleave) { - clearTimeout(pendingOnleave); - pendingOnleave = 0; + if (sel != null) { + const { left, top } = u.cursor; + payload.rowIndex = sel; + payload.point[xScaleUnit] = u.posToVal(left!, xScaleKey); + eventBus.publish(hoverEvent); + + if (!isToolTipOpen.current) { + if (pendingOnleave) { + clearTimeout(pendingOnleave); + pendingOnleave = 0; + } + onhover({ + seriesIdx: i, + dataIdx: sel, + pageX: rect.left + left!, + pageY: rect.top + top!, + }); } - - onhover({ - seriesIdx: i, - dataIdx: sel, - pageX: rect.left + u.cursor.left!, - pageY: rect.top + u.cursor.top!, - }); - - return; // only show the first one + return; } } } @@ -187,7 +212,12 @@ export function prepConfig(opts: PrepConfigOpts) { if (!isToolTipOpen.current) { // if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms) if (!pendingOnleave) { - pendingOnleave = setTimeout(() => onhover(null), 100) as any; + pendingOnleave = setTimeout(() => { + onhover(null); + payload.rowIndex = undefined; + payload.point[xScaleUnit] = null; + eventBus.publish(hoverEvent); + }, 100) as any; } } }); @@ -209,7 +239,7 @@ export function prepConfig(opts: PrepConfigOpts) { builder.setMode(2); builder.addScale({ - scaleKey: 'x', + scaleKey: xScaleKey, isTime: true, orientation: ScaleOrientation.Horizontal, direction: ScaleDirection.Right, @@ -220,7 +250,7 @@ export function prepConfig(opts: PrepConfigOpts) { }); builder.addAxis({ - scaleKey: 'x', + scaleKey: xScaleKey, placement: AxisPlacement.Bottom, isTime: true, theme: theme, @@ -231,8 +261,12 @@ export function prepConfig(opts: PrepConfigOpts) { const yAxisReverse = Boolean(yAxisConfig.reverse); const shouldUseLogScale = yScale.type !== ScaleDistribution.Linear || heatmapType === DataFrameType.HeatmapSparse; + // random to prevent syncing y in other heatmaps + // TODO: try to match TimeSeries y keygen algo to sync with TimeSeries panels (when not isOrdianalY) + const yScaleKey = 'y_' + (Math.random() + 1).toString(36).substring(7); + builder.addScale({ - scaleKey: 'y', + scaleKey: yScaleKey, isTime: false, // distribution: ScaleDistribution.Ordinal, // does not work with facets/scatter yet orientation: ScaleOrientation.Vertical, @@ -248,7 +282,7 @@ export function prepConfig(opts: PrepConfigOpts) { (u, dataMin, dataMax) => { // logarithmic expansion if (shouldUseLogScale) { - let yExp = u.scales['y'].log!; + let yExp = u.scales[yScaleKey].log!; let minExpanded = false; let maxExpanded = false; @@ -312,7 +346,7 @@ export function prepConfig(opts: PrepConfigOpts) { const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short'); builder.addAxis({ - scaleKey: 'y', + scaleKey: yScaleKey, show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden, placement: yAxisConfig.axisPlacement || AxisPlacement.Left, size: yAxisConfig.axisWidth || null, @@ -373,12 +407,12 @@ export function prepConfig(opts: PrepConfigOpts) { builder.addSeries({ facets: [ { - scale: 'x', + scale: xScaleKey, auto: true, sorted: 1, }, { - scale: 'y', + scale: yScaleKey, auto: true, }, ], @@ -426,12 +460,12 @@ export function prepConfig(opts: PrepConfigOpts) { builder.addSeries({ facets: [ { - scale: 'x', + scale: xScaleKey, auto: true, sorted: 1, }, { - scale: 'y', + scale: yScaleKey, auto: true, }, ], @@ -454,7 +488,7 @@ export function prepConfig(opts: PrepConfigOpts) { scaleKey: '', // facets' scales used (above) }); - builder.setCursor({ + const cursor: Cursor = { drag: { x: true, y: false, @@ -489,7 +523,30 @@ export function prepConfig(opts: PrepConfigOpts) { }; }, }, - }); + }; + + if (sync && sync() !== DashboardCursorSync.Off) { + cursor.sync = { + key: '__global_', + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + if (x < 0) { + payload.point[xScaleUnit] = null; + eventBus.publish(new DataHoverClearEvent()); + } else { + payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); + eventBus.publish(hoverEvent); + } + + return true; + }, + }, + }; + + builder.setSync(); + } + + builder.setCursor(cursor); return builder; }