mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Heatmap: implement cursor sync (#50271)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
da731a38cc
commit
ed6a9d65aa
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user