Heatmap: implement cursor sync (#50271)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Ryan McKinley 2022-06-15 12:45:54 -07:00 committed by GitHub
parent da731a38cc
commit ed6a9d65aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 30 deletions

View File

@ -8,6 +8,7 @@ import {
Portal, Portal,
ScaleDistribution, ScaleDistribution,
UPlotChart, UPlotChart,
usePanelContext,
useStyles2, useStyles2,
useTheme2, useTheme2,
VizLayout, VizLayout,
@ -34,11 +35,13 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
height, height,
options, options,
fieldConfig, fieldConfig,
eventBus,
onChangeTimeRange, onChangeTimeRange,
replaceVariables, replaceVariables,
}) => { }) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { sync } = usePanelContext();
// ugh // ugh
let timeRangeRef = useRef<TimeRange>(timeRange); let timeRangeRef = useRef<TimeRange>(timeRange);
@ -113,6 +116,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
return prepConfig({ return prepConfig({
dataRef, dataRef,
theme, theme,
eventBus,
onhover: onhover, onhover: onhover,
onclick: options.tooltip.show ? onclick : null, onclick: options.tooltip.show ? onclick : null,
onzoom: (evt) => { onzoom: (evt) => {
@ -124,6 +128,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
isToolTipOpen, isToolTipOpen,
timeZone, timeZone,
getTimeRange: () => timeRangeRef.current, getTimeRange: () => timeRangeRef.current,
sync,
palette, palette,
cellGap: options.cellGap, cellGap: options.cellGap,
hideLE: options.filterValues?.le, hideLE: options.filterValues?.le,

View File

@ -1,8 +1,13 @@
import { MutableRefObject, RefObject } from 'react'; import { MutableRefObject, RefObject } from 'react';
import uPlot from 'uplot'; import uPlot, { Cursor } from 'uplot';
import { import {
DashboardCursorSync,
DataFrameType, DataFrameType,
DataHoverClearEvent,
DataHoverEvent,
DataHoverPayload,
EventBus,
formattedValueToString, formattedValueToString,
getValueFormat, getValueFormat,
GrafanaTheme2, GrafanaTheme2,
@ -55,6 +60,7 @@ export interface HeatmapZoomEvent {
interface PrepConfigOpts { interface PrepConfigOpts {
dataRef: RefObject<HeatmapData>; dataRef: RefObject<HeatmapData>;
theme: GrafanaTheme2; theme: GrafanaTheme2;
eventBus: EventBus;
onhover?: null | ((evt?: HeatmapHoverEvent | null) => void); onhover?: null | ((evt?: HeatmapHoverEvent | null) => void);
onclick?: null | ((evt?: any) => void); onclick?: null | ((evt?: any) => void);
onzoom?: null | ((evt: HeatmapZoomEvent) => void); onzoom?: null | ((evt: HeatmapZoomEvent) => void);
@ -70,12 +76,14 @@ interface PrepConfigOpts {
valueMax?: number; valueMax?: number;
yAxisConfig: YAxisConfig; yAxisConfig: YAxisConfig;
ySizeDivisor?: number; ySizeDivisor?: number;
sync?: () => DashboardCursorSync;
} }
export function prepConfig(opts: PrepConfigOpts) { export function prepConfig(opts: PrepConfigOpts) {
const { const {
dataRef, dataRef,
theme, theme,
eventBus,
onhover, onhover,
onclick, onclick,
onzoom, onzoom,
@ -90,8 +98,12 @@ export function prepConfig(opts: PrepConfigOpts) {
valueMax, valueMax,
yAxisConfig, yAxisConfig,
ySizeDivisor, ySizeDivisor,
sync,
} = opts; } = opts;
const xScaleKey = 'x';
const xScaleUnit = 'time';
const pxRatio = devicePixelRatio; const pxRatio = devicePixelRatio;
let heatmapType = dataRef.current?.heatmap?.meta?.type; let heatmapType = dataRef.current?.heatmap?.meta?.type;
@ -131,8 +143,8 @@ export function prepConfig(opts: PrepConfigOpts) {
onzoom && onzoom &&
builder.addHook('setSelect', (u) => { builder.addHook('setSelect', (u) => {
onzoom({ onzoom({
xMin: u.posToVal(u.select.left, 'x'), xMin: u.posToVal(u.select.left, xScaleKey),
xMax: u.posToVal(u.select.left + u.select.width, 'x'), xMax: u.posToVal(u.select.left + u.select.width, xScaleKey),
}); });
u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false); 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 // 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() // scales.x.range() typically reads back from drilled-down panelProps.timeRange via getTimeRange()
builder.addHook('setData', (u) => { 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; let { min: xMin, max: xMax } = u.scales!.x;
@ -149,7 +161,7 @@ export function prepConfig(opts: PrepConfigOpts) {
if (xMin !== min || xMax !== max) { if (xMin !== min || xMax !== max) {
queueMicrotask(() => { queueMicrotask(() => {
u.setScale('x', { min, max }); u.setScale(xScaleKey, { min, max });
}); });
} }
}); });
@ -159,6 +171,14 @@ export function prepConfig(opts: PrepConfigOpts) {
rect = r; rect = r;
}); });
const payload: DataHoverPayload = {
point: {
[xScaleUnit]: null,
},
data: dataRef.current?.heatmap,
};
const hoverEvent = new DataHoverEvent(payload);
let pendingOnleave = 0; let pendingOnleave = 0;
onhover && onhover &&
@ -166,20 +186,25 @@ export function prepConfig(opts: PrepConfigOpts) {
if (u.cursor.idxs != null) { if (u.cursor.idxs != null) {
for (let i = 0; i < u.cursor.idxs.length; i++) { for (let i = 0; i < u.cursor.idxs.length; i++) {
const sel = u.cursor.idxs[i]; const sel = u.cursor.idxs[i];
if (sel != null && !isToolTipOpen.current) { if (sel != null) {
if (pendingOnleave) { const { left, top } = u.cursor;
clearTimeout(pendingOnleave); payload.rowIndex = sel;
pendingOnleave = 0; 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!,
});
} }
return;
onhover({
seriesIdx: i,
dataIdx: sel,
pageX: rect.left + u.cursor.left!,
pageY: rect.top + u.cursor.top!,
});
return; // only show the first one
} }
} }
} }
@ -187,7 +212,12 @@ export function prepConfig(opts: PrepConfigOpts) {
if (!isToolTipOpen.current) { if (!isToolTipOpen.current) {
// if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms) // if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms)
if (!pendingOnleave) { 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.setMode(2);
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: xScaleKey,
isTime: true, isTime: true,
orientation: ScaleOrientation.Horizontal, orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right, direction: ScaleDirection.Right,
@ -220,7 +250,7 @@ export function prepConfig(opts: PrepConfigOpts) {
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: xScaleKey,
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
isTime: true, isTime: true,
theme: theme, theme: theme,
@ -231,8 +261,12 @@ export function prepConfig(opts: PrepConfigOpts) {
const yAxisReverse = Boolean(yAxisConfig.reverse); const yAxisReverse = Boolean(yAxisConfig.reverse);
const shouldUseLogScale = yScale.type !== ScaleDistribution.Linear || heatmapType === DataFrameType.HeatmapSparse; 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({ builder.addScale({
scaleKey: 'y', scaleKey: yScaleKey,
isTime: false, isTime: false,
// distribution: ScaleDistribution.Ordinal, // does not work with facets/scatter yet // distribution: ScaleDistribution.Ordinal, // does not work with facets/scatter yet
orientation: ScaleOrientation.Vertical, orientation: ScaleOrientation.Vertical,
@ -248,7 +282,7 @@ export function prepConfig(opts: PrepConfigOpts) {
(u, dataMin, dataMax) => { (u, dataMin, dataMax) => {
// logarithmic expansion // logarithmic expansion
if (shouldUseLogScale) { if (shouldUseLogScale) {
let yExp = u.scales['y'].log!; let yExp = u.scales[yScaleKey].log!;
let minExpanded = false; let minExpanded = false;
let maxExpanded = false; let maxExpanded = false;
@ -312,7 +346,7 @@ export function prepConfig(opts: PrepConfigOpts) {
const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short'); const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short');
builder.addAxis({ builder.addAxis({
scaleKey: 'y', scaleKey: yScaleKey,
show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden, show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden,
placement: yAxisConfig.axisPlacement || AxisPlacement.Left, placement: yAxisConfig.axisPlacement || AxisPlacement.Left,
size: yAxisConfig.axisWidth || null, size: yAxisConfig.axisWidth || null,
@ -373,12 +407,12 @@ export function prepConfig(opts: PrepConfigOpts) {
builder.addSeries({ builder.addSeries({
facets: [ facets: [
{ {
scale: 'x', scale: xScaleKey,
auto: true, auto: true,
sorted: 1, sorted: 1,
}, },
{ {
scale: 'y', scale: yScaleKey,
auto: true, auto: true,
}, },
], ],
@ -426,12 +460,12 @@ export function prepConfig(opts: PrepConfigOpts) {
builder.addSeries({ builder.addSeries({
facets: [ facets: [
{ {
scale: 'x', scale: xScaleKey,
auto: true, auto: true,
sorted: 1, sorted: 1,
}, },
{ {
scale: 'y', scale: yScaleKey,
auto: true, auto: true,
}, },
], ],
@ -454,7 +488,7 @@ export function prepConfig(opts: PrepConfigOpts) {
scaleKey: '', // facets' scales used (above) scaleKey: '', // facets' scales used (above)
}); });
builder.setCursor({ const cursor: Cursor = {
drag: { drag: {
x: true, x: true,
y: false, 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; return builder;
} }