AnnotationsPlugin2: Implement support for rectangular annotations in Heatmap (#88107)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Andre Pereira
2024-06-05 16:48:37 -05:00
committed by GitHub
co-authored by Leon Sorokin
parent 2403665998
commit 277067ac9d
9 changed files with 184 additions and 25 deletions
@@ -28,6 +28,15 @@ export enum HeatmapColorScale {
Linear = 'linear',
}
/**
* Controls which axis to allow selection on
*/
export enum HeatmapSelectionMode {
X = 'x',
Xy = 'xy',
Y = 'y',
}
/**
* Controls various color options
*/
@@ -222,6 +231,10 @@ export interface Options {
* Controls tick alignment and value name when not calculating from data
*/
rowsFrame?: RowsHeatmapOptions;
/**
* Controls which axis to allow selection on
*/
selectionMode?: HeatmapSelectionMode;
/**
* | *{
* layout: ui.HeatmapCellLayout & "auto" // TODO: fix after remove when https://github.com/grafana/cuetsy/issues/74 is fixed
@@ -265,6 +278,7 @@ export const defaultOptions: Partial<Options> = {
legend: {
show: true,
},
selectionMode: HeatmapSelectionMode.X,
showValue: ui.VisibilityMode.Auto,
tooltip: {
mode: ui.TooltipDisplayMode.Single,
@@ -13,7 +13,7 @@ import {
import { AdHocFilterItem } from '../Table/types';
import { SeriesVisibilityChangeMode } from './types';
import { OnSelectRangeCallback, SeriesVisibilityChangeMode } from './types';
/** @alpha */
export interface PanelContext {
@@ -43,6 +43,12 @@ export interface PanelContext {
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
onAnnotationDelete?: (id: string) => void;
/**
* Called when a user selects an area on the panel, if defined will override the default behavior of the panel,
* which is to update the time range
*/
onSelectRange?: OnSelectRangeCallback;
/**
* Used from visualizations like Table to add ad-hoc filters from cell values
*/
@@ -8,3 +8,15 @@ export enum SeriesVisibilityChangeMode {
ToggleSelection = 'select',
AppendToSelection = 'append',
}
export type OnSelectRangeCallback = (selections: RangeSelection2D[]) => void;
export interface RangeSelection1D {
from: number;
to: number;
}
export interface RangeSelection2D {
x?: RangeSelection1D;
y?: RangeSelection1D;
}
@@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { DashboardCursorSync } from '@grafana/schema';
import { useStyles2 } from '../../../themes';
import { RangeSelection1D, RangeSelection2D, OnSelectRangeCallback } from '../../PanelChrome';
import { getPortalContainer } from '../../Portal/Portal';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
@@ -37,6 +38,8 @@ interface TooltipPlugin2Props {
// y-only, via shiftKey
clientZoom?: boolean;
onSelectRange?: OnSelectRangeCallback;
render: (
u: uPlot,
dataIdxs: Array<number | null>,
@@ -107,6 +110,7 @@ export const TooltipPlugin2 = ({
render,
clientZoom = false,
queryZoom,
onSelectRange,
maxWidth,
syncMode = DashboardCursorSync.Off,
syncScope = 'global', // eventsScope
@@ -319,9 +323,65 @@ export const TooltipPlugin2 = ({
config.addHook('setSelect', (u) => {
const isXAxisHorizontal = u.scales.x.ori === 0;
if (!viaSync && (clientZoom || queryZoom != null)) {
if (maybeZoomAction(u.cursor!.event)) {
if (clientZoom && yDrag) {
if (onSelectRange != null) {
let selections: RangeSelection2D[] = [];
const yDrag = Boolean(u.cursor!.drag!.y);
const xDrag = Boolean(u.cursor!.drag!.x);
let xSel = null;
let ySels: RangeSelection1D[] = [];
// get x selection
if (xDrag) {
xSel = {
from: isXAxisHorizontal
? u.posToVal(u.select.left!, 'x')
: u.posToVal(u.select.top + u.select.height, 'x'),
to: isXAxisHorizontal
? u.posToVal(u.select.left! + u.select.width, 'x')
: u.posToVal(u.select.top, 'x'),
};
}
// get y selections
if (yDrag) {
config.scales.forEach((scale) => {
const key = scale.props.scaleKey;
if (key !== 'x') {
let ySel = {
from: isXAxisHorizontal
? u.posToVal(u.select.top + u.select.height, key)
: u.posToVal(u.select.left + u.select.width, key),
to: isXAxisHorizontal ? u.posToVal(u.select.top, key) : u.posToVal(u.select.left, key),
};
ySels.push(ySel);
}
});
}
if (xDrag) {
if (yDrag) {
// x + y
selections = ySels.map((ySel) => ({ x: xSel!, y: ySel }));
} else {
// x only
selections = [{ x: xSel! }];
}
} else {
if (yDrag) {
// y only
selections = ySels.map((ySel) => ({ y: ySel }));
}
}
onSelectRange(selections);
} else if (clientZoom && yDrag) {
if (u.select.height >= MIN_ZOOM_DIST) {
for (let key in u.scales!) {
if (key !== 'x') {