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) {
|
||||
let aBound = aName === '+Inf' ? Infinity : +(aName ?? 0);
|
||||
let bBound = bName === '+Inf' ? Infinity : +(bName ?? 0);
|
||||
function parseNumeric(v?: string | null) {
|
||||
return v === '+Inf' ? Infinity : v === '-Inf' ? -Infinity : +(v ?? 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 */
|
||||
@@ -54,7 +55,7 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
||||
const yField = frame.fields[1];
|
||||
|
||||
// similar to initBins() below
|
||||
const len = xValues.length * frames.length;
|
||||
const len = xValues.length * (frame.fields.length - 1);
|
||||
const xs = new Array(len);
|
||||
const ys = new Array(len);
|
||||
const counts2 = new Array(len);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
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 {
|
||||
Portal,
|
||||
UPlotChart,
|
||||
@@ -37,6 +37,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
const theme = useTheme2();
|
||||
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 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]
|
||||
);
|
||||
|
||||
// ugh
|
||||
const dataRef = useRef<HeatmapData>(info);
|
||||
|
||||
dataRef.current = info;
|
||||
|
||||
const builder = useMemo(() => {
|
||||
return prepConfig({
|
||||
dataRef,
|
||||
theme,
|
||||
onhover: options.tooltip.show ? onhover : () => {},
|
||||
onclick: options.tooltip.show ? onclick : () => {},
|
||||
onhover: options.tooltip.show ? onhover : null,
|
||||
onclick: options.tooltip.show ? onclick : null,
|
||||
onzoom: (evt) => {
|
||||
onChangeTimeRange({ from: evt.xMin, to: evt.xMax });
|
||||
},
|
||||
isToolTipOpen,
|
||||
timeZone,
|
||||
timeRange,
|
||||
getTimeRange: () => timeRangeRef.current,
|
||||
palette,
|
||||
cellGap: options.cellGap,
|
||||
hideThreshold: options.hideThreshold,
|
||||
|
||||
@@ -27,22 +27,39 @@ export interface HeatmapHoverEvent {
|
||||
pageY: number;
|
||||
}
|
||||
|
||||
export interface HeatmapZoomEvent {
|
||||
xMin: number;
|
||||
xMax: number;
|
||||
}
|
||||
|
||||
interface PrepConfigOpts {
|
||||
dataRef: RefObject<HeatmapData>;
|
||||
theme: GrafanaTheme2;
|
||||
onhover: (evt?: HeatmapHoverEvent | null) => void;
|
||||
onclick: (evt?: any) => void;
|
||||
onhover?: null | ((evt?: HeatmapHoverEvent | null) => void);
|
||||
onclick?: null | ((evt?: any) => void);
|
||||
onzoom?: null | ((evt: HeatmapZoomEvent) => void);
|
||||
isToolTipOpen: MutableRefObject<boolean>;
|
||||
timeZone: string;
|
||||
timeRange: TimeRange; // should be getTimeRange() cause dynamic?
|
||||
getTimeRange: () => TimeRange;
|
||||
palette: string[];
|
||||
cellGap?: number | null; // in css pixels
|
||||
hideThreshold?: number;
|
||||
}
|
||||
|
||||
export function prepConfig(opts: PrepConfigOpts) {
|
||||
const { dataRef, theme, onhover, onclick, isToolTipOpen, timeZone, timeRange, palette, cellGap, hideThreshold } =
|
||||
opts;
|
||||
const {
|
||||
dataRef,
|
||||
theme,
|
||||
onhover,
|
||||
onclick,
|
||||
onzoom,
|
||||
isToolTipOpen,
|
||||
timeZone,
|
||||
getTimeRange,
|
||||
palette,
|
||||
cellGap,
|
||||
hideThreshold,
|
||||
} = opts;
|
||||
|
||||
let qt: Quadtree;
|
||||
let hRect: Rect | null;
|
||||
@@ -59,7 +76,46 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
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)
|
||||
@@ -69,34 +125,35 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
|
||||
let pendingOnleave = 0;
|
||||
|
||||
builder.addHook('setLegend', (u) => {
|
||||
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;
|
||||
onhover &&
|
||||
builder.addHook('setLegend', (u) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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 tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms)
|
||||
if (!pendingOnleave) {
|
||||
pendingOnleave = setTimeout(() => onhover(null), 100) as any;
|
||||
if (!isToolTipOpen.current) {
|
||||
// if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms)
|
||||
if (!pendingOnleave) {
|
||||
pendingOnleave = setTimeout(() => onhover(null), 100) as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.addHook('drawClear', (u) => {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
@@ -120,7 +177,9 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
// 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({
|
||||
@@ -225,6 +284,11 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
});
|
||||
|
||||
builder.setCursor({
|
||||
drag: {
|
||||
x: true,
|
||||
y: false,
|
||||
setScale: false,
|
||||
},
|
||||
dataIdx: (u, seriesIdx) => {
|
||||
if (seriesIdx === 1) {
|
||||
hRect = null;
|
||||
|
||||
Reference in New Issue
Block a user