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,
|
||||
ScaleDistribution,
|
||||
UPlotChart,
|
||||
usePanelContext,
|
||||
useStyles2,
|
||||
useTheme2,
|
||||
VizLayout,
|
||||
@ -34,11 +35,13 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
height,
|
||||
options,
|
||||
fieldConfig,
|
||||
eventBus,
|
||||
onChangeTimeRange,
|
||||
replaceVariables,
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { sync } = usePanelContext();
|
||||
|
||||
// ugh
|
||||
let timeRangeRef = useRef<TimeRange>(timeRange);
|
||||
@ -113,6 +116,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
return prepConfig({
|
||||
dataRef,
|
||||
theme,
|
||||
eventBus,
|
||||
onhover: onhover,
|
||||
onclick: options.tooltip.show ? onclick : null,
|
||||
onzoom: (evt) => {
|
||||
@ -124,6 +128,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
isToolTipOpen,
|
||||
timeZone,
|
||||
getTimeRange: () => timeRangeRef.current,
|
||||
sync,
|
||||
palette,
|
||||
cellGap: options.cellGap,
|
||||
hideLE: options.filterValues?.le,
|
||||
|
@ -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<HeatmapData>;
|
||||
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 (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 + u.cursor.left!,
|
||||
pageY: rect.top + u.cursor.top!,
|
||||
pageX: rect.left + left!,
|
||||
pageY: rect.top + 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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user