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',
|
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,21 +125,52 @@ 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);
|
||||||
|
|
||||||
|
if (frame.name === 'xymark') {
|
||||||
|
// 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++) {
|
for (let i = 0; i < vals.time.length; i++) {
|
||||||
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
|
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);
|
||||||
|
|
||||||
@ -159,6 +190,7 @@ export const AnnotationsPlugin2 = ({
|
|||||||
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
|
ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
Loading…
Reference in New Issue
Block a user