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

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
pull/87057/head^2
Andre Pereira 12 months ago committed by GitHub
parent 2403665998
commit 277067ac9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts
  2. 8
      packages/grafana-ui/src/components/PanelChrome/PanelContext.ts
  3. 12
      packages/grafana-ui/src/components/PanelChrome/types.ts
  4. 62
      packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx
  5. 4
      public/app/plugins/panel/heatmap/HeatmapPanel.tsx
  6. 4
      public/app/plugins/panel/heatmap/panelcfg.cue
  7. 14
      public/app/plugins/panel/heatmap/panelcfg.gen.ts
  8. 23
      public/app/plugins/panel/heatmap/utils.ts
  9. 68
      public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx

@ -28,6 +28,15 @@ export enum HeatmapColorScale {
Linear = 'linear', Linear = 'linear',
} }
/**
* Controls which axis to allow selection on
*/
export enum HeatmapSelectionMode {
X = 'x',
Xy = 'xy',
Y = 'y',
}
/** /**
* Controls various color options * Controls various color options
*/ */
@ -222,6 +231,10 @@ export interface Options {
* Controls tick alignment and value name when not calculating from data * Controls tick alignment and value name when not calculating from data
*/ */
rowsFrame?: RowsHeatmapOptions; 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 * 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: { legend: {
show: true, show: true,
}, },
selectionMode: HeatmapSelectionMode.X,
showValue: ui.VisibilityMode.Auto, showValue: ui.VisibilityMode.Auto,
tooltip: { tooltip: {
mode: ui.TooltipDisplayMode.Single, mode: ui.TooltipDisplayMode.Single,

@ -13,7 +13,7 @@ import {
import { AdHocFilterItem } from '../Table/types'; import { AdHocFilterItem } from '../Table/types';
import { SeriesVisibilityChangeMode } from './types'; import { OnSelectRangeCallback, SeriesVisibilityChangeMode } from './types';
/** @alpha */ /** @alpha */
export interface PanelContext { export interface PanelContext {
@ -43,6 +43,12 @@ export interface PanelContext {
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void; onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
onAnnotationDelete?: (id: string) => 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 * Used from visualizations like Table to add ad-hoc filters from cell values
*/ */

@ -8,3 +8,15 @@ export enum SeriesVisibilityChangeMode {
ToggleSelection = 'select', ToggleSelection = 'select',
AppendToSelection = 'append', 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 { DashboardCursorSync } from '@grafana/schema';
import { useStyles2 } from '../../../themes'; import { useStyles2 } from '../../../themes';
import { RangeSelection1D, RangeSelection2D, OnSelectRangeCallback } from '../../PanelChrome';
import { getPortalContainer } from '../../Portal/Portal'; import { getPortalContainer } from '../../Portal/Portal';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
@ -37,6 +38,8 @@ interface TooltipPlugin2Props {
// y-only, via shiftKey // y-only, via shiftKey
clientZoom?: boolean; clientZoom?: boolean;
onSelectRange?: OnSelectRangeCallback;
render: ( render: (
u: uPlot, u: uPlot,
dataIdxs: Array<number | null>, dataIdxs: Array<number | null>,
@ -107,6 +110,7 @@ export const TooltipPlugin2 = ({
render, render,
clientZoom = false, clientZoom = false,
queryZoom, queryZoom,
onSelectRange,
maxWidth, maxWidth,
syncMode = DashboardCursorSync.Off, syncMode = DashboardCursorSync.Off,
syncScope = 'global', // eventsScope syncScope = 'global', // eventsScope
@ -319,9 +323,65 @@ export const TooltipPlugin2 = ({
config.addHook('setSelect', (u) => { config.addHook('setSelect', (u) => {
const isXAxisHorizontal = u.scales.x.ori === 0; const isXAxisHorizontal = u.scales.x.ori === 0;
if (!viaSync && (clientZoom || queryZoom != null)) { if (!viaSync && (clientZoom || queryZoom != null)) {
if (maybeZoomAction(u.cursor!.event)) { 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) { if (u.select.height >= MIN_ZOOM_DIST) {
for (let key in u.scales!) { for (let key in u.scales!) {
if (key !== 'x') { if (key !== 'x') {

@ -45,7 +45,7 @@ export const HeatmapPanel = ({
}: HeatmapPanelProps) => { }: HeatmapPanelProps) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { sync, eventsScope, canAddAnnotations } = usePanelContext(); const { sync, eventsScope, canAddAnnotations, onSelectRange } = usePanelContext();
const cursorSync = sync?.() ?? DashboardCursorSync.Off; const cursorSync = sync?.() ?? DashboardCursorSync.Off;
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
@ -113,6 +113,7 @@ export const HeatmapPanel = ({
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)', exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
yAxisConfig: options.yAxis, yAxisConfig: options.yAxis,
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
selectionMode: options.selectionMode,
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -179,6 +180,7 @@ export const HeatmapPanel = ({
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
} }
queryZoom={onChangeTimeRange} queryZoom={onChangeTimeRange}
onSelectRange={onSelectRange}
syncMode={cursorSync} syncMode={cursorSync}
syncScope={eventsScope} syncScope={eventsScope}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => { render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => {

@ -26,6 +26,8 @@ composableKinds: PanelCfg: lineage: {
HeatmapColorMode: "opacity" | "scheme" @cuetsy(kind="enum") HeatmapColorMode: "opacity" | "scheme" @cuetsy(kind="enum")
// Controls the color scale of the heatmap // Controls the color scale of the heatmap
HeatmapColorScale: "linear" | "exponential" @cuetsy(kind="enum") HeatmapColorScale: "linear" | "exponential" @cuetsy(kind="enum")
// Controls which axis to allow selection on
HeatmapSelectionMode: "x" | "y" | "xy" @cuetsy(kind="enum")
// Controls various color options // Controls various color options
HeatmapColorOptions: { HeatmapColorOptions: {
// Sets the color mode // Sets the color mode
@ -155,6 +157,8 @@ composableKinds: PanelCfg: lineage: {
exemplars: ExemplarConfig | *{ exemplars: ExemplarConfig | *{
color: "rgba(255,0,255,0.7)" color: "rgba(255,0,255,0.7)"
} }
// Controls which axis to allow selection on
selectionMode?: HeatmapSelectionMode & (*"x" | _)
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
FieldConfig: { FieldConfig: {
ui.HideableFieldConfig ui.HideableFieldConfig

@ -26,6 +26,15 @@ export enum HeatmapColorScale {
Linear = 'linear', Linear = 'linear',
} }
/**
* Controls which axis to allow selection on
*/
export enum HeatmapSelectionMode {
X = 'x',
Xy = 'xy',
Y = 'y',
}
/** /**
* Controls various color options * Controls various color options
*/ */
@ -220,6 +229,10 @@ export interface Options {
* Controls tick alignment and value name when not calculating from data * Controls tick alignment and value name when not calculating from data
*/ */
rowsFrame?: RowsHeatmapOptions; 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 * layout: ui.HeatmapCellLayout & "auto" // TODO: fix after remove when https://github.com/grafana/cuetsy/issues/74 is fixed
@ -263,6 +276,7 @@ export const defaultOptions: Partial<Options> = {
legend: { legend: {
show: true, show: true,
}, },
selectionMode: HeatmapSelectionMode.X,
showValue: ui.VisibilityMode.Auto, showValue: ui.VisibilityMode.Auto,
tooltip: { tooltip: {
mode: ui.TooltipDisplayMode.Single, mode: ui.TooltipDisplayMode.Single,

@ -18,7 +18,7 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree'; import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
import { HeatmapData } from './fields'; import { HeatmapData } from './fields';
import { FieldConfig, YAxisConfig } from './types'; import { FieldConfig, HeatmapSelectionMode, YAxisConfig } from './types';
interface PathbuilderOpts { interface PathbuilderOpts {
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
@ -51,10 +51,22 @@ interface PrepConfigOpts {
hideGE?: number; hideGE?: number;
yAxisConfig: YAxisConfig; yAxisConfig: YAxisConfig;
ySizeDivisor?: number; ySizeDivisor?: number;
selectionMode?: HeatmapSelectionMode;
} }
export function prepConfig(opts: PrepConfigOpts) { export function prepConfig(opts: PrepConfigOpts) {
const { dataRef, theme, timeZone, getTimeRange, cellGap, hideLE, hideGE, yAxisConfig, ySizeDivisor } = opts; const {
dataRef,
theme,
timeZone,
getTimeRange,
cellGap,
hideLE,
hideGE,
yAxisConfig,
ySizeDivisor,
selectionMode = HeatmapSelectionMode.X,
} = opts;
const xScaleKey = 'x'; const xScaleKey = 'x';
let isTime = true; let isTime = true;
@ -449,10 +461,13 @@ export function prepConfig(opts: PrepConfigOpts) {
scaleKey: '', // facets' scales used (above) scaleKey: '', // facets' scales used (above)
}); });
const dragX = selectionMode === HeatmapSelectionMode.X || selectionMode === HeatmapSelectionMode.Xy;
const dragY = selectionMode === HeatmapSelectionMode.Y || selectionMode === HeatmapSelectionMode.Xy;
const cursor: Cursor = { const cursor: Cursor = {
drag: { drag: {
x: true, x: dragX,
y: false, y: dragY,
setScale: false, setScale: false,
}, },
dataIdx: (u, seriesIdx) => { dataIdx: (u, seriesIdx) => {

@ -125,38 +125,70 @@ export const AnnotationsPlugin2 = ({
const ctx = u.ctx; const ctx = u.ctx;
let y0 = u.bbox.top;
let y1 = y0 + u.bbox.height;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height); ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
ctx.clip(); ctx.clip();
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
annos.forEach((frame) => { annos.forEach((frame) => {
let vals = getVals(frame); let vals = getVals(frame);
for (let i = 0; i < vals.time.length; i++) { if (frame.name === 'xymark') {
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8); // xMin, xMax, yMin, yMax, color, lineWidth, lineStyle, fillOpacity, text
let xKey = config.scales[0].props.scaleKey;
let yKey = config.scales[1].props.scaleKey;
for (let i = 0; i < frame.length; i++) {
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
let x0 = u.valToPos(vals.xMin[i], xKey, true);
let x1 = u.valToPos(vals.xMax[i], xKey, true);
let y0 = u.valToPos(vals.yMax[i], yKey, true);
let y1 = u.valToPos(vals.yMin[i], yKey, true);
ctx.fillStyle = colorManipulator.alpha(color, vals.fillOpacity[i]);
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
ctx.lineWidth = Math.round(vals.lineWidth[i] * uPlot.pxRatio);
if (vals.lineStyle[i] === 'dash') {
// maybe extract this to vals.lineDash[i] in future?
ctx.setLineDash([5, 5]);
} else {
// solid
ctx.setLineDash([]);
}
ctx.strokeStyle = color;
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
}
} else {
let y0 = u.bbox.top;
let y1 = y0 + u.bbox.height;
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
for (let i = 0; i < vals.time.length; i++) {
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
let x0 = u.valToPos(vals.time[i], 'x', true); let x0 = u.valToPos(vals.time[i], 'x', true);
if (!vals.isRegion?.[i]) { if (!vals.isRegion?.[i]) {
renderLine(ctx, y0, y1, x0, color); renderLine(ctx, y0, y1, x0, color);
// renderUpTriangle(ctx, x0, y1, 8 * uPlot.pxRatio, 5 * uPlot.pxRatio, color); // renderUpTriangle(ctx, x0, y1, 8 * uPlot.pxRatio, 5 * uPlot.pxRatio, color);
} else if (canvasRegionRendering) { } else if (canvasRegionRendering) {
renderLine(ctx, y0, y1, x0, color); renderLine(ctx, y0, y1, x0, color);
let x1 = u.valToPos(vals.timeEnd[i], 'x', true); let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
renderLine(ctx, y0, y1, x1, color); renderLine(ctx, y0, y1, x1, color);
ctx.fillStyle = colorManipulator.alpha(color, 0.1); ctx.fillStyle = colorManipulator.alpha(color, 0.1);
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height); ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
}
} }
} }
}); });

Loading…
Cancel
Save