mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
HeatmapNG: implement zooming & fix heatmap-buckets rendering (#47231)
This commit is contained in:
@@ -38,11 +38,12 @@ export const heatmapTransformer: SynchronousDataTransformerInfo<HeatmapTransform
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sortAscStrInf(aName?: string | null, bName?: string | null) {
|
function parseNumeric(v?: string | null) {
|
||||||
let aBound = aName === '+Inf' ? Infinity : +(aName ?? 0);
|
return v === '+Inf' ? Infinity : v === '-Inf' ? -Infinity : +(v ?? 0);
|
||||||
let bBound = bName === '+Inf' ? Infinity : +(bName ?? 0);
|
}
|
||||||
|
|
||||||
return aBound - bBound;
|
export function sortAscStrInf(aName?: string | null, bName?: string | null) {
|
||||||
|
return parseNumeric(aName) - parseNumeric(bName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Given existing buckets, create a values style frame */
|
/** Given existing buckets, create a values style frame */
|
||||||
@@ -54,7 +55,7 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
|||||||
const yField = frame.fields[1];
|
const yField = frame.fields[1];
|
||||||
|
|
||||||
// similar to initBins() below
|
// similar to initBins() below
|
||||||
const len = xValues.length * frames.length;
|
const len = xValues.length * (frame.fields.length - 1);
|
||||||
const xs = new Array(len);
|
const xs = new Array(len);
|
||||||
const ys = new Array(len);
|
const ys = new Array(len);
|
||||||
const counts2 = new Array(len);
|
const counts2 = new Array(len);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { formattedValueToString, GrafanaTheme2, PanelProps, reduceField, ReducerID } from '@grafana/data';
|
import { formattedValueToString, GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
Portal,
|
Portal,
|
||||||
UPlotChart,
|
UPlotChart,
|
||||||
@@ -37,6 +37,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// ugh
|
||||||
|
let timeRangeRef = useRef<TimeRange>(timeRange);
|
||||||
|
timeRangeRef.current = timeRange;
|
||||||
|
|
||||||
const info = useMemo(() => prepareHeatmapData(data.series, options, theme), [data, options, theme]);
|
const info = useMemo(() => prepareHeatmapData(data.series, options, theme), [data, options, theme]);
|
||||||
|
|
||||||
const facets = useMemo(() => [null, info.heatmap?.fields.map((f) => f.values.toArray())], [info.heatmap]);
|
const facets = useMemo(() => [null, info.heatmap?.fields.map((f) => f.values.toArray())], [info.heatmap]);
|
||||||
@@ -70,19 +74,22 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
[options, data.structureRev]
|
[options, data.structureRev]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ugh
|
||||||
const dataRef = useRef<HeatmapData>(info);
|
const dataRef = useRef<HeatmapData>(info);
|
||||||
|
|
||||||
dataRef.current = info;
|
dataRef.current = info;
|
||||||
|
|
||||||
const builder = useMemo(() => {
|
const builder = useMemo(() => {
|
||||||
return prepConfig({
|
return prepConfig({
|
||||||
dataRef,
|
dataRef,
|
||||||
theme,
|
theme,
|
||||||
onhover: options.tooltip.show ? onhover : () => {},
|
onhover: options.tooltip.show ? onhover : null,
|
||||||
onclick: options.tooltip.show ? onclick : () => {},
|
onclick: options.tooltip.show ? onclick : null,
|
||||||
|
onzoom: (evt) => {
|
||||||
|
onChangeTimeRange({ from: evt.xMin, to: evt.xMax });
|
||||||
|
},
|
||||||
isToolTipOpen,
|
isToolTipOpen,
|
||||||
timeZone,
|
timeZone,
|
||||||
timeRange,
|
getTimeRange: () => timeRangeRef.current,
|
||||||
palette,
|
palette,
|
||||||
cellGap: options.cellGap,
|
cellGap: options.cellGap,
|
||||||
hideThreshold: options.hideThreshold,
|
hideThreshold: options.hideThreshold,
|
||||||
|
|||||||
@@ -27,22 +27,39 @@ export interface HeatmapHoverEvent {
|
|||||||
pageY: number;
|
pageY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HeatmapZoomEvent {
|
||||||
|
xMin: number;
|
||||||
|
xMax: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface PrepConfigOpts {
|
interface PrepConfigOpts {
|
||||||
dataRef: RefObject<HeatmapData>;
|
dataRef: RefObject<HeatmapData>;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
onhover: (evt?: HeatmapHoverEvent | null) => void;
|
onhover?: null | ((evt?: HeatmapHoverEvent | null) => void);
|
||||||
onclick: (evt?: any) => void;
|
onclick?: null | ((evt?: any) => void);
|
||||||
|
onzoom?: null | ((evt: HeatmapZoomEvent) => void);
|
||||||
isToolTipOpen: MutableRefObject<boolean>;
|
isToolTipOpen: MutableRefObject<boolean>;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
timeRange: TimeRange; // should be getTimeRange() cause dynamic?
|
getTimeRange: () => TimeRange;
|
||||||
palette: string[];
|
palette: string[];
|
||||||
cellGap?: number | null; // in css pixels
|
cellGap?: number | null; // in css pixels
|
||||||
hideThreshold?: number;
|
hideThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepConfig(opts: PrepConfigOpts) {
|
export function prepConfig(opts: PrepConfigOpts) {
|
||||||
const { dataRef, theme, onhover, onclick, isToolTipOpen, timeZone, timeRange, palette, cellGap, hideThreshold } =
|
const {
|
||||||
opts;
|
dataRef,
|
||||||
|
theme,
|
||||||
|
onhover,
|
||||||
|
onclick,
|
||||||
|
onzoom,
|
||||||
|
isToolTipOpen,
|
||||||
|
timeZone,
|
||||||
|
getTimeRange,
|
||||||
|
palette,
|
||||||
|
cellGap,
|
||||||
|
hideThreshold,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
let qt: Quadtree;
|
let qt: Quadtree;
|
||||||
let hRect: Rect | null;
|
let hRect: Rect | null;
|
||||||
@@ -59,7 +76,46 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
u.over.addEventListener('click', onclick);
|
|
||||||
|
onclick &&
|
||||||
|
u.over.addEventListener(
|
||||||
|
'mouseup',
|
||||||
|
(e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
let isDragging: boolean = u.cursor.drag._x || u.cursor.drag._y;
|
||||||
|
|
||||||
|
if (!isDragging) {
|
||||||
|
onclick(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onzoom &&
|
||||||
|
builder.addHook('setSelect', (u) => {
|
||||||
|
onzoom({
|
||||||
|
xMin: u.posToVal(u.select.left, 'x'),
|
||||||
|
xMax: u.posToVal(u.select.left + u.select.width, 'x'),
|
||||||
|
});
|
||||||
|
u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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: xMin, max: xMax } = u.scales!.x;
|
||||||
|
|
||||||
|
let min = getTimeRange().from.valueOf();
|
||||||
|
let max = getTimeRange().to.valueOf();
|
||||||
|
|
||||||
|
if (xMin !== min || xMax !== max) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
u.setScale('x', { min, max });
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// rect of .u-over (grid area)
|
// rect of .u-over (grid area)
|
||||||
@@ -69,34 +125,35 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
|
|
||||||
let pendingOnleave = 0;
|
let pendingOnleave = 0;
|
||||||
|
|
||||||
builder.addHook('setLegend', (u) => {
|
onhover &&
|
||||||
if (u.cursor.idxs != null) {
|
builder.addHook('setLegend', (u) => {
|
||||||
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
if (u.cursor.idxs != null) {
|
||||||
const sel = u.cursor.idxs[i];
|
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
||||||
if (sel != null && !isToolTipOpen.current) {
|
const sel = u.cursor.idxs[i];
|
||||||
if (pendingOnleave) {
|
if (sel != null && !isToolTipOpen.current) {
|
||||||
clearTimeout(pendingOnleave);
|
if (pendingOnleave) {
|
||||||
pendingOnleave = 0;
|
clearTimeout(pendingOnleave);
|
||||||
|
pendingOnleave = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onhover({
|
||||||
|
index: sel,
|
||||||
|
pageX: rect.left + u.cursor.left!,
|
||||||
|
pageY: rect.top + u.cursor.top!,
|
||||||
|
});
|
||||||
|
|
||||||
|
return; // only show the first one
|
||||||
}
|
}
|
||||||
|
|
||||||
onhover({
|
|
||||||
index: sel,
|
|
||||||
pageX: rect.left + u.cursor.left!,
|
|
||||||
pageY: rect.top + u.cursor.top!,
|
|
||||||
});
|
|
||||||
|
|
||||||
return; // only show the first one
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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), 100) as any;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
builder.addHook('drawClear', (u) => {
|
builder.addHook('drawClear', (u) => {
|
||||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||||
@@ -120,7 +177,9 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
orientation: ScaleOrientation.Horizontal,
|
orientation: ScaleOrientation.Horizontal,
|
||||||
direction: ScaleDirection.Right,
|
direction: ScaleDirection.Right,
|
||||||
// TODO: expand by x bucket size and layout
|
// TODO: expand by x bucket size and layout
|
||||||
range: [timeRange.from.valueOf(), timeRange.to.valueOf()],
|
range: () => {
|
||||||
|
return [getTimeRange().from.valueOf(), getTimeRange().to.valueOf()];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
@@ -225,6 +284,11 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.setCursor({
|
builder.setCursor({
|
||||||
|
drag: {
|
||||||
|
x: true,
|
||||||
|
y: false,
|
||||||
|
setScale: false,
|
||||||
|
},
|
||||||
dataIdx: (u, seriesIdx) => {
|
dataIdx: (u, seriesIdx) => {
|
||||||
if (seriesIdx === 1) {
|
if (seriesIdx === 1) {
|
||||||
hRect = null;
|
hRect = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user