From ed140906de26216f7db661274b1da8582dc35788 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 1 Apr 2022 21:54:44 -0600 Subject: [PATCH] HeatmapNG: implement zooming & fix heatmap-buckets rendering (#47231) --- .../transformers/calculateHeatmap/heatmap.ts | 11 +- .../panel/heatmap-new/HeatmapPanel.tsx | 17 ++- public/app/plugins/panel/heatmap-new/utils.ts | 124 +++++++++++++----- 3 files changed, 112 insertions(+), 40 deletions(-) diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 3e01d84e70a..bac9bda5086 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -38,11 +38,12 @@ export const heatmapTransformer: SynchronousDataTransformerInfo = ({ const theme = useTheme2(); const styles = useStyles2(getStyles); + // ugh + let timeRangeRef = useRef(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 = ({ [options, data.structureRev] ); + // ugh const dataRef = useRef(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, diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts index 82c3084cb3e..7026df72f39 100644 --- a/public/app/plugins/panel/heatmap-new/utils.ts +++ b/public/app/plugins/panel/heatmap-new/utils.ts @@ -27,22 +27,39 @@ export interface HeatmapHoverEvent { pageY: number; } +export interface HeatmapZoomEvent { + xMin: number; + xMax: number; +} + interface PrepConfigOpts { dataRef: RefObject; 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; 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;