mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AnnotationsPlugin2: Implement support for rectangular annotations in Heatmap (#88107)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
2403665998
commit
277067ac9d
@ -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') {
|
||||
|
@ -45,7 +45,7 @@ export const HeatmapPanel = ({
|
||||
}: HeatmapPanelProps) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { sync, eventsScope, canAddAnnotations } = usePanelContext();
|
||||
const { sync, eventsScope, canAddAnnotations, onSelectRange } = usePanelContext();
|
||||
const cursorSync = sync?.() ?? DashboardCursorSync.Off;
|
||||
|
||||
// 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)',
|
||||
yAxisConfig: options.yAxis,
|
||||
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
|
||||
selectionMode: options.selectionMode,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -179,6 +180,7 @@ export const HeatmapPanel = ({
|
||||
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
|
||||
}
|
||||
queryZoom={onChangeTimeRange}
|
||||
onSelectRange={onSelectRange}
|
||||
syncMode={cursorSync}
|
||||
syncScope={eventsScope}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => {
|
||||
|
@ -26,6 +26,8 @@ composableKinds: PanelCfg: lineage: {
|
||||
HeatmapColorMode: "opacity" | "scheme" @cuetsy(kind="enum")
|
||||
// Controls the color scale of the heatmap
|
||||
HeatmapColorScale: "linear" | "exponential" @cuetsy(kind="enum")
|
||||
// Controls which axis to allow selection on
|
||||
HeatmapSelectionMode: "x" | "y" | "xy" @cuetsy(kind="enum")
|
||||
// Controls various color options
|
||||
HeatmapColorOptions: {
|
||||
// Sets the color mode
|
||||
@ -155,6 +157,8 @@ composableKinds: PanelCfg: lineage: {
|
||||
exemplars: ExemplarConfig | *{
|
||||
color: "rgba(255,0,255,0.7)"
|
||||
}
|
||||
// Controls which axis to allow selection on
|
||||
selectionMode?: HeatmapSelectionMode & (*"x" | _)
|
||||
} @cuetsy(kind="interface")
|
||||
FieldConfig: {
|
||||
ui.HideableFieldConfig
|
||||
|
@ -26,6 +26,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
|
||||
*/
|
||||
@ -220,6 +229,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
|
||||
@ -263,6 +276,7 @@ export const defaultOptions: Partial<Options> = {
|
||||
legend: {
|
||||
show: true,
|
||||
},
|
||||
selectionMode: HeatmapSelectionMode.X,
|
||||
showValue: ui.VisibilityMode.Auto,
|
||||
tooltip: {
|
||||
mode: ui.TooltipDisplayMode.Single,
|
||||
|
@ -18,7 +18,7 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra
|
||||
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
||||
|
||||
import { HeatmapData } from './fields';
|
||||
import { FieldConfig, YAxisConfig } from './types';
|
||||
import { FieldConfig, HeatmapSelectionMode, YAxisConfig } from './types';
|
||||
|
||||
interface PathbuilderOpts {
|
||||
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
||||
@ -51,10 +51,22 @@ interface PrepConfigOpts {
|
||||
hideGE?: number;
|
||||
yAxisConfig: YAxisConfig;
|
||||
ySizeDivisor?: number;
|
||||
selectionMode?: HeatmapSelectionMode;
|
||||
}
|
||||
|
||||
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';
|
||||
let isTime = true;
|
||||
@ -449,10 +461,13 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
scaleKey: '', // facets' scales used (above)
|
||||
});
|
||||
|
||||
const dragX = selectionMode === HeatmapSelectionMode.X || selectionMode === HeatmapSelectionMode.Xy;
|
||||
const dragY = selectionMode === HeatmapSelectionMode.Y || selectionMode === HeatmapSelectionMode.Xy;
|
||||
|
||||
const cursor: Cursor = {
|
||||
drag: {
|
||||
x: true,
|
||||
y: false,
|
||||
x: dragX,
|
||||
y: dragY,
|
||||
setScale: false,
|
||||
},
|
||||
dataIdx: (u, seriesIdx) => {
|
||||
|
@ -125,38 +125,70 @@ export const AnnotationsPlugin2 = ({
|
||||
|
||||
const ctx = u.ctx;
|
||||
|
||||
let y0 = u.bbox.top;
|
||||
let y1 = y0 + u.bbox.height;
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
ctx.clip();
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
annos.forEach((frame) => {
|
||||
let vals = getVals(frame);
|
||||
|
||||
for (let i = 0; i < vals.time.length; i++) {
|
||||
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
|
||||
if (frame.name === 'xymark') {
|
||||
// xMin, xMax, yMin, yMax, color, lineWidth, lineStyle, fillOpacity, text
|
||||
|
||||
let x0 = u.valToPos(vals.time[i], 'x', true);
|
||||
let xKey = config.scales[0].props.scaleKey;
|
||||
let yKey = config.scales[1].props.scaleKey;
|
||||
|
||||
if (!vals.isRegion?.[i]) {
|
||||
renderLine(ctx, y0, y1, x0, color);
|
||||
// renderUpTriangle(ctx, x0, y1, 8 * uPlot.pxRatio, 5 * uPlot.pxRatio, color);
|
||||
} else if (canvasRegionRendering) {
|
||||
renderLine(ctx, y0, y1, x0, color);
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
|
||||
|
||||
let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
|
||||
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);
|
||||
|
||||
renderLine(ctx, y0, y1, x1, color);
|
||||
ctx.fillStyle = colorManipulator.alpha(color, vals.fillOpacity[i]);
|
||||
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
|
||||
|
||||
ctx.fillStyle = colorManipulator.alpha(color, 0.1);
|
||||
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
|
||||
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);
|
||||
|
||||
if (!vals.isRegion?.[i]) {
|
||||
renderLine(ctx, y0, y1, x0, color);
|
||||
// renderUpTriangle(ctx, x0, y1, 8 * uPlot.pxRatio, 5 * uPlot.pxRatio, color);
|
||||
} else if (canvasRegionRendering) {
|
||||
renderLine(ctx, y0, y1, x0, color);
|
||||
|
||||
let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
|
||||
|
||||
renderLine(ctx, y0, y1, x1, color);
|
||||
|
||||
ctx.fillStyle = colorManipulator.alpha(color, 0.1);
|
||||
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user