Annotations: Multi-lane annotations rendering (lane per frame) (#111559)
* feat: support multi-lane annotations panel option --------- Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
co-authored by
Leon Sorokin
parent
c7dd159db1
commit
8149f586b3
+1024
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -74,6 +74,7 @@
|
||||
"mostly-blank-dashboard": (import '../dev-dashboards/scenarios/mostly-blank-dashboard.json'),
|
||||
"mssql_fakedata": (import '../dev-dashboards/datasource-mssql/mssql_fakedata.json'),
|
||||
"mssql_unittest": (import '../dev-dashboards/datasource-mssql/mssql_unittest.json'),
|
||||
"multi-lane-annotations": (import '../dev-dashboards/annotations/multi-lane-annotations.json'),
|
||||
"mysql_fakedata": (import '../dev-dashboards/datasource-mysql/mysql_fakedata.json'),
|
||||
"mysql_unittest": (import '../dev-dashboards/datasource-mysql/mysql_unittest.json'),
|
||||
"new_features_in_v74": (import '../dev-dashboards/datasource-testdata/new_features_in_v74.json'),
|
||||
|
||||
+14
@@ -502,6 +502,20 @@ export enum VizOrientation {
|
||||
Vertical = 'vertical',
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks out each annotation frame into multiple lanes on the x-axis
|
||||
*/
|
||||
export interface VizAnnotations {
|
||||
multiLane?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
*/
|
||||
export interface OptionsWithAnnotations {
|
||||
annotations?: VizAnnotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
*/
|
||||
|
||||
@@ -158,6 +158,16 @@ ReduceDataOptions: {
|
||||
// TODO docs
|
||||
VizOrientation: "auto" | "vertical" | "horizontal" @cuetsy(kind="enum")
|
||||
|
||||
// Breaks out each annotation frame into multiple lanes on the x-axis
|
||||
VizAnnotations: {
|
||||
multiLane?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// TODO docs
|
||||
OptionsWithAnnotations: {
|
||||
annotations?: VizAnnotations
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// TODO docs
|
||||
OptionsWithTooltip: {
|
||||
tooltip: VizTooltipOptions
|
||||
|
||||
Generated
+1
-1
@@ -63,7 +63,7 @@ export const defaultCandlestickColors: Partial<CandlestickColors> = {
|
||||
up: 'green',
|
||||
};
|
||||
|
||||
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
|
||||
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithAnnotations {
|
||||
/**
|
||||
* Sets the style of the candlesticks
|
||||
*/
|
||||
|
||||
Generated
+1
@@ -188,6 +188,7 @@ export interface RowsHeatmapOptions {
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
annotations?: ui.VizAnnotations;
|
||||
/**
|
||||
* Controls if the heatmap should be calculated from data
|
||||
*/
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import * as ui from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "12.3.0-pre";
|
||||
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations {
|
||||
/**
|
||||
* Controls value alignment on the timelines
|
||||
*/
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import * as ui from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "12.3.0-pre";
|
||||
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations {
|
||||
/**
|
||||
* Controls the column width
|
||||
*/
|
||||
|
||||
Generated
+1
-1
@@ -12,7 +12,7 @@ import * as common from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "12.3.0-pre";
|
||||
|
||||
export interface Options extends common.OptionsWithTimezones {
|
||||
export interface Options extends common.OptionsWithTimezones, common.OptionsWithAnnotations {
|
||||
legend: common.VizLegendOptions;
|
||||
orientation?: common.VizOrientation;
|
||||
timeCompare?: common.TimeCompareOptions;
|
||||
|
||||
@@ -286,6 +286,7 @@ type UPlotConfigPrepOpts<T extends Record<string, unknown> = {}> = {
|
||||
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
|
||||
hoverProximity?: number;
|
||||
orientation?: VizOrientation;
|
||||
xAxisConfig?: Pick<AxisProps, 'size' | 'gap' | 'ticks'>;
|
||||
} & T;
|
||||
|
||||
/** @alpha */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Component } from 'react';
|
||||
import uPlot, { AlignedData } from 'uplot';
|
||||
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { DashboardCursorSync, VizLegendOptions } from '@grafana/schema';
|
||||
import { Themeable2, VizLayout } from '@grafana/ui';
|
||||
import { UPlotChart, AxisProps, Renderers, UPlotConfigBuilder, ScaleProps, pluginLog } from '@grafana/ui/internal';
|
||||
import { AxisProps, pluginLog, Renderers, ScaleProps, UPlotChart, UPlotConfigBuilder } from '@grafana/ui/internal';
|
||||
|
||||
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
||||
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
|
||||
@@ -40,7 +40,12 @@ export interface GraphNGProps extends Themeable2 {
|
||||
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
|
||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
||||
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
||||
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
||||
prepConfig: (
|
||||
alignedFrame: DataFrame,
|
||||
allFrames: DataFrame[],
|
||||
getTimeRange: () => TimeRange,
|
||||
annotationLanes?: number
|
||||
) => UPlotConfigBuilder;
|
||||
propsToDiff?: Array<string | PropDiffFn>;
|
||||
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
|
||||
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
|
||||
@@ -63,6 +68,9 @@ export interface GraphNGProps extends Themeable2 {
|
||||
* similar to structureRev. then we can drop propsToDiff entirely.
|
||||
*/
|
||||
options?: Record<string, any>;
|
||||
|
||||
// Annotation lanes count
|
||||
annotationLanes?: number;
|
||||
}
|
||||
|
||||
function sameProps<T extends Record<string, unknown>>(
|
||||
@@ -191,7 +199,7 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
||||
let config = this.state?.config;
|
||||
|
||||
if (withConfig) {
|
||||
config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange);
|
||||
config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange, this.props.annotationLanes);
|
||||
pluginLog('GraphNG', false, 'config prepared', config);
|
||||
}
|
||||
|
||||
@@ -229,7 +237,12 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
||||
propsChanged;
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
||||
newState.config = this.props.prepConfig(
|
||||
newState.alignedFrame,
|
||||
this.props.frames,
|
||||
this.getTimeRange,
|
||||
this.props.annotationLanes
|
||||
);
|
||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,19 @@ import { hasVisibleLegendSeries, PlotLegend, UPlotConfigBuilder } from '@grafana
|
||||
|
||||
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
|
||||
|
||||
import { preparePlotConfigBuilder } from './utils';
|
||||
import { getXAxisConfig, preparePlotConfigBuilder } from './utils';
|
||||
|
||||
const propsToDiff: Array<string | PropDiffFn> = ['legend', 'options', 'theme'];
|
||||
const propsToDiff: Array<string | PropDiffFn> = ['legend', 'options', 'annotationLanes', 'theme'];
|
||||
|
||||
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
|
||||
|
||||
export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
prepConfig = (
|
||||
alignedFrame: DataFrame,
|
||||
allFrames: DataFrame[],
|
||||
getTimeRange: () => TimeRange,
|
||||
annotationLanes?: number
|
||||
) => {
|
||||
const { theme, timeZone, options, renderers, tweakAxis, tweakScale } = this.props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
@@ -27,6 +32,7 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
|
||||
tweakAxis,
|
||||
hoverProximity: options?.tooltip?.hoverProximity,
|
||||
orientation: options?.orientation,
|
||||
xAxisConfig: getXAxisConfig(annotationLanes),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createDataFrame, dateTime, DateTimeInput, EventBus, FieldType } from '@grafana/data';
|
||||
import { getTheme } from '@grafana/ui';
|
||||
|
||||
import { preparePlotConfigBuilder } from './utils';
|
||||
import { getXAxisConfig, preparePlotConfigBuilder, UPLOT_DEFAULT_AXIS_GAP } from './utils';
|
||||
|
||||
describe('when fill below to option is used', () => {
|
||||
let eventBus: EventBus;
|
||||
@@ -375,3 +375,26 @@ describe('time axis units', () => {
|
||||
expect(config.axes![0]!.values(config, [1667406900000, 1761316576114], 0, 100, 1000)).toEqual(['11-02', '10-24']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateAnnotationLaneSizes', () => {
|
||||
it('should not regress', () => {
|
||||
expect(getXAxisConfig()).toEqual(undefined);
|
||||
expect(getXAxisConfig(0)).toEqual(undefined);
|
||||
});
|
||||
it('should return config to resize x-axis size, gap, and ticks size', () => {
|
||||
expect(getXAxisConfig(2)).toEqual({
|
||||
gap: UPLOT_DEFAULT_AXIS_GAP,
|
||||
size: 36,
|
||||
ticks: {
|
||||
size: 19,
|
||||
},
|
||||
});
|
||||
expect(getXAxisConfig(3)).toEqual({
|
||||
gap: UPLOT_DEFAULT_AXIS_GAP,
|
||||
size: 43,
|
||||
ticks: {
|
||||
size: 26,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,8 +68,15 @@ import {
|
||||
buildScaleKey,
|
||||
getStackingGroups,
|
||||
preparePlotData2,
|
||||
AxisProps,
|
||||
} from '@grafana/ui/internal';
|
||||
|
||||
import { ANNOTATION_LANE_SIZE } from '../../../plugins/panel/timeseries/plugins/utils';
|
||||
|
||||
// See UPlotAxisBuilder.ts::calculateAxisSize for default axis size calculation
|
||||
export const UPLOT_DEFAULT_AXIS_SIZE = 17;
|
||||
export const UPLOT_DEFAULT_AXIS_GAP = 5;
|
||||
|
||||
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
@@ -90,6 +97,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
||||
tweakAxis = (opts) => opts,
|
||||
hoverProximity,
|
||||
orientation = VizOrientation.Horizontal,
|
||||
xAxisConfig,
|
||||
}) => {
|
||||
// we want the Auto and Horizontal orientation to default to Horizontal
|
||||
const isHorizontal = orientation !== VizOrientation.Vertical;
|
||||
@@ -159,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
||||
formatValue: xField.config.unit?.startsWith('time:')
|
||||
? (v, decimals) => xField.display!(v, decimals).text
|
||||
: undefined,
|
||||
...xAxisConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -709,3 +718,25 @@ function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<str
|
||||
});
|
||||
return originNames;
|
||||
}
|
||||
|
||||
export function getXAxisConfig(lanes = 1): Pick<AxisProps, 'size' | 'gap' | 'ticks'> | undefined {
|
||||
if (lanes > 1) {
|
||||
const annotationLanesSize = lanes * ANNOTATION_LANE_SIZE;
|
||||
// Add an extra lane's worth of height below the annotation lanes in order to show the gridlines through the annotation lanes
|
||||
const axisSize = annotationLanesSize + UPLOT_DEFAULT_AXIS_GAP;
|
||||
// Consistent gap between gridlines and x-axis labels
|
||||
const gap = UPLOT_DEFAULT_AXIS_GAP;
|
||||
// Axis size is: default size + gap size + annotationLaneSize
|
||||
const size = UPLOT_DEFAULT_AXIS_SIZE + gap + annotationLanesSize;
|
||||
|
||||
return {
|
||||
size,
|
||||
gap,
|
||||
ticks: {
|
||||
size: axisSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
|
||||
import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema';
|
||||
import { TimelineValueAlignment, TooltipDisplayMode, VisibilityMode, VizTooltipOptions } from '@grafana/schema';
|
||||
import { UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui';
|
||||
|
||||
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||
import { getXAxisConfig } from '../TimeSeries/utils';
|
||||
|
||||
import { preparePlotConfigBuilder, TimelineMode } from './utils';
|
||||
|
||||
@@ -23,7 +24,16 @@ export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsT
|
||||
paginationRev?: string;
|
||||
}
|
||||
|
||||
const propsToDiff = ['rowHeight', 'colWidth', 'showValue', 'mergeValues', 'alignValue', 'tooltip', 'paginationRev'];
|
||||
const propsToDiff = [
|
||||
'rowHeight',
|
||||
'colWidth',
|
||||
'showValue',
|
||||
'mergeValues',
|
||||
'alignValue',
|
||||
'tooltip',
|
||||
'paginationRev',
|
||||
'annotationLanes',
|
||||
];
|
||||
|
||||
export const TimelineChart = (props: TimelineProps) => {
|
||||
const { frames, timeZone, rowHeight, tooltip, legend, legendItems } = props;
|
||||
@@ -60,6 +70,7 @@ export const TimelineChart = (props: TimelineProps) => {
|
||||
getValueColor: getValueColor,
|
||||
|
||||
hoverMulti: tooltip?.mode === TooltipDisplayMode.Multi,
|
||||
xAxisConfig: getXAxisConfig(props.annotationLanes),
|
||||
});
|
||||
},
|
||||
[frames, props, timeZone, rowHeight, getValueColor, tooltip]
|
||||
|
||||
@@ -93,6 +93,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
mergeValues,
|
||||
getValueColor,
|
||||
hoverMulti,
|
||||
xAxisConfig,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||
|
||||
@@ -167,7 +168,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
});
|
||||
|
||||
const xField = frame.fields[0];
|
||||
|
||||
const xAxisHidden = xField.config.custom.axisPlacement === AxisPlacement.Hidden;
|
||||
|
||||
builder.addAxis({
|
||||
@@ -181,6 +181,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
formatValue: xField.config.unit?.startsWith('time:')
|
||||
? (v, decimals) => xField.display!(v, decimals).text
|
||||
: undefined,
|
||||
...xAxisConfig,
|
||||
});
|
||||
|
||||
const yCustomConfig = frame.fields[1].config.custom;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
|
||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
|
||||
import { getXAnnotationFrames } from '../timeseries/plugins/utils';
|
||||
|
||||
import { prepareCandlestickFields } from './fields';
|
||||
import { Options, defaultCandlestickColors, VizDisplayMode } from './types';
|
||||
@@ -266,6 +267,7 @@ export const CandlestickPanel = ({
|
||||
replaceVariables={replaceVariables}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
cursorSync={cursorSync}
|
||||
annotationLanes={options.annotations?.multiLane ? getXAnnotationFrames(data.annotations).length : undefined}
|
||||
>
|
||||
{(uplotConfig, alignedFrame) => {
|
||||
return (
|
||||
@@ -322,6 +324,7 @@ export const CandlestickPanel = ({
|
||||
)}
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
multiLane={options.annotations?.multiLane}
|
||||
annotations={data.annotations ?? []}
|
||||
config={uplotConfig}
|
||||
timeZone={timeZone}
|
||||
|
||||
@@ -49,6 +49,7 @@ composableKinds: PanelCfg: {
|
||||
Options: {
|
||||
common.OptionsWithLegend
|
||||
common.OptionsWithTooltip
|
||||
common.OptionsWithAnnotations
|
||||
|
||||
// Sets which dimensions are used for the visualization
|
||||
mode: VizDisplayMode & (*"candles+volume" | _)
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ export const defaultCandlestickColors: Partial<CandlestickColors> = {
|
||||
up: 'green',
|
||||
};
|
||||
|
||||
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
|
||||
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithAnnotations {
|
||||
/**
|
||||
* Sets the style of the candlesticks
|
||||
*/
|
||||
|
||||
@@ -19,8 +19,10 @@ import { FacetedData, TimeRange2, TooltipHoverMode } from '@grafana/ui/internal'
|
||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
|
||||
import { getXAxisConfig } from '../../../core/components/TimeSeries/utils';
|
||||
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { getXAnnotationFrames } from '../timeseries/plugins/utils';
|
||||
|
||||
import { HeatmapTooltip } from './HeatmapTooltip';
|
||||
import { HeatmapData, prepareHeatmapData } from './fields';
|
||||
@@ -132,6 +134,8 @@ const HeatmapPanelViz = ({
|
||||
const dataRef = useRef(info);
|
||||
dataRef.current = info;
|
||||
|
||||
const annotationsLength = options.annotations?.multiLane ? getXAnnotationFrames(data.annotations).length : undefined;
|
||||
|
||||
const builder = useMemo(() => {
|
||||
const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution;
|
||||
|
||||
@@ -147,10 +151,11 @@ const HeatmapPanelViz = ({
|
||||
yAxisConfig: options.yAxis,
|
||||
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
|
||||
selectionMode: options.selectionMode,
|
||||
xAxisConfig: getXAxisConfig(annotationsLength),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options, timeZone, data.structureRev, cursorSync]);
|
||||
}, [options, timeZone, data.structureRev, cursorSync, annotationsLength]);
|
||||
|
||||
const renderLegend = () => {
|
||||
if (!options.legend.show) {
|
||||
@@ -242,6 +247,7 @@ const HeatmapPanelViz = ({
|
||||
)}
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
multiLane={options.annotations?.multiLane}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
||||
@@ -107,6 +107,7 @@ composableKinds: PanelCfg: lineage: {
|
||||
layout?: ui.HeatmapCellLayout
|
||||
} @cuetsy(kind="interface")
|
||||
Options: {
|
||||
annotations?: ui.VizAnnotations
|
||||
// Controls if the heatmap should be calculated from data
|
||||
calculate?: bool | *false
|
||||
// Calculation options for the heatmap
|
||||
|
||||
@@ -186,6 +186,7 @@ export interface RowsHeatmapOptions {
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
annotations?: ui.VizAnnotations;
|
||||
/**
|
||||
* Controls if the heatmap should be calculated from data
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getDisplayProcessor,
|
||||
} from '@grafana/data';
|
||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation, HeatmapCellLayout } from '@grafana/schema';
|
||||
import { UPlotConfigBuilder } from '@grafana/ui';
|
||||
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui';
|
||||
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
|
||||
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
||||
@@ -53,6 +53,7 @@ interface PrepConfigOpts {
|
||||
yAxisConfig: YAxisConfig;
|
||||
ySizeDivisor?: number;
|
||||
selectionMode?: HeatmapSelectionMode;
|
||||
xAxisConfig?: Parameters<UPlotConfigPrepFn>[0]['xAxisConfig'];
|
||||
}
|
||||
|
||||
export function prepConfig(opts: PrepConfigOpts) {
|
||||
@@ -67,6 +68,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
yAxisConfig,
|
||||
ySizeDivisor,
|
||||
selectionMode = HeatmapSelectionMode.X,
|
||||
xAxisConfig,
|
||||
} = opts;
|
||||
|
||||
const xScaleKey = 'x';
|
||||
@@ -182,6 +184,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
isTime && xField.config.unit?.startsWith('time:')
|
||||
? (v, decimals) => xField.display!(v, decimals).text
|
||||
: undefined,
|
||||
...xAxisConfig,
|
||||
});
|
||||
|
||||
const yField = dataRef.current?.heatmap?.fields[1]!;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
|
||||
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { getXAnnotationFrames } from '../timeseries/plugins/utils';
|
||||
import { getTimezones } from '../timeseries/utils';
|
||||
|
||||
import { StateTimelineTooltip } from './StateTimelineTooltip';
|
||||
@@ -87,11 +88,13 @@ export const StateTimelinePanel = ({
|
||||
width={width}
|
||||
height={height - paginationHeight}
|
||||
legendItems={legendItems}
|
||||
annotations={options.annotations}
|
||||
{...options}
|
||||
mode={TimelineMode.Changes}
|
||||
replaceVariables={replaceVariables}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
cursorSync={cursorSync}
|
||||
annotationLanes={options.annotations?.multiLane ? getXAnnotationFrames(data.annotations).length : undefined}
|
||||
>
|
||||
{(builder, alignedFrame) => {
|
||||
return (
|
||||
@@ -149,6 +152,7 @@ export const StateTimelinePanel = ({
|
||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
multiLane={options.annotations?.multiLane}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
||||
@@ -28,6 +28,7 @@ composableKinds: PanelCfg: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
ui.OptionsWithAnnotations
|
||||
|
||||
//Show timeline values on chart
|
||||
showValue: ui.VisibilityMode & (*"auto" | _)
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations {
|
||||
/**
|
||||
* Controls value alignment on the timelines
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,7 @@ import { usePagination } from '../state-timeline/hooks';
|
||||
import { containerStyles } from '../state-timeline/styles';
|
||||
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
|
||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { getXAnnotationFrames } from '../timeseries/plugins/utils';
|
||||
import { getTimezones } from '../timeseries/utils';
|
||||
|
||||
import { Options } from './panelcfg.gen';
|
||||
@@ -108,6 +109,7 @@ export const StatusHistoryPanel = ({
|
||||
replaceVariables={replaceVariables}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
cursorSync={cursorSync}
|
||||
annotationLanes={options.annotations?.multiLane ? getXAnnotationFrames(data.annotations).length : undefined}
|
||||
>
|
||||
{(builder, alignedFrame) => {
|
||||
return (
|
||||
@@ -165,6 +167,7 @@ export const StatusHistoryPanel = ({
|
||||
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
multiLane={options.annotations?.multiLane}
|
||||
annotations={data.annotations ?? []}
|
||||
config={builder}
|
||||
timeZone={timeZone}
|
||||
|
||||
@@ -28,6 +28,7 @@ composableKinds: PanelCfg: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
ui.OptionsWithAnnotations
|
||||
|
||||
//Set the height of the rows
|
||||
rowHeight: float32 & >=0 & <=1 | *0.9
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
|
||||
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations {
|
||||
/**
|
||||
* Controls the column width
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@ import { AnnotationsPlugin2 } from './plugins/AnnotationsPlugin2';
|
||||
import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
|
||||
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
|
||||
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
|
||||
import { getXAnnotationFrames } from './plugins/utils';
|
||||
import { getPrepareTimeseriesSuggestion } from './suggestions';
|
||||
import { getTimezones, prepareGraphableFields } from './utils';
|
||||
|
||||
@@ -131,6 +132,7 @@ export const TimeSeriesPanel = ({
|
||||
replaceVariables={replaceVariables}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
cursorSync={cursorSync}
|
||||
annotationLanes={options.annotations?.multiLane ? getXAnnotationFrames(data.annotations).length : undefined}
|
||||
>
|
||||
{(uplotConfig, alignedFrame) => {
|
||||
return (
|
||||
@@ -192,6 +194,7 @@ export const TimeSeriesPanel = ({
|
||||
<>
|
||||
<AnnotationsPlugin2
|
||||
replaceVariables={replaceVariables}
|
||||
multiLane={options.annotations?.multiLane}
|
||||
annotations={data.annotations ?? []}
|
||||
config={uplotConfig}
|
||||
timeZone={timeZone}
|
||||
|
||||
@@ -22,11 +22,15 @@ composableKinds: PanelCfg: lineage: {
|
||||
schemas: [{
|
||||
version: [0, 0]
|
||||
schema: {
|
||||
Options: common.OptionsWithTimezones & {
|
||||
Options: {
|
||||
common.OptionsWithTimezones
|
||||
common.OptionsWithAnnotations
|
||||
|
||||
legend: common.VizLegendOptions
|
||||
tooltip: common.VizTooltipOptions
|
||||
timeCompare?: common.TimeCompareOptions
|
||||
orientation?: common.VizOrientation
|
||||
annotations?: common.VizAnnotations
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
FieldConfig: common.GraphFieldConfig & {} @cuetsy(kind="interface")
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export interface Options extends common.OptionsWithTimezones {
|
||||
export interface Options extends common.OptionsWithTimezones, common.OptionsWithAnnotations {
|
||||
legend: common.VizLegendOptions;
|
||||
orientation?: common.VizOrientation;
|
||||
timeCompare?: common.TimeCompareOptions;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useReducer } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import uPlot from 'uplot';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
|
||||
import { ANNOTATION_LANE_SIZE, getXAnnotationFrames, getXYAnnotationFrames } from './utils';
|
||||
|
||||
// (copied from TooltipPlugin2)
|
||||
interface TimeRange2 {
|
||||
@@ -32,6 +33,7 @@ interface AnnotationsPluginProps {
|
||||
setNewRange: (newRage: TimeRange2 | null) => void;
|
||||
canvasRegionRendering?: boolean;
|
||||
replaceVariables: InterpolateFunction;
|
||||
multiLane?: boolean;
|
||||
}
|
||||
|
||||
// TODO: batch by color, use Path2D objects
|
||||
@@ -72,6 +74,7 @@ export const AnnotationsPlugin2 = ({
|
||||
setNewRange,
|
||||
replaceVariables,
|
||||
canvasRegionRendering = true,
|
||||
multiLane = false,
|
||||
}: AnnotationsPluginProps) => {
|
||||
const [plot, setPlot] = useState<uPlot>();
|
||||
|
||||
@@ -85,10 +88,9 @@ export const AnnotationsPlugin2 = ({
|
||||
const { canExecuteActions } = usePanelContext();
|
||||
const userCanExecuteActions = canExecuteActions?.() ?? false;
|
||||
|
||||
const annos = useMemo(() => {
|
||||
let annos = annotations.filter(
|
||||
(frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time')
|
||||
);
|
||||
const { xAnnos, xyAnnos } = useMemo(() => {
|
||||
let xAnnos = getXAnnotationFrames(annotations);
|
||||
let xyAnnos = getXYAnnotationFrames(annotations);
|
||||
|
||||
if (newRange) {
|
||||
let isRegion = newRange.to > newRange.from;
|
||||
@@ -109,18 +111,25 @@ export const AnnotationsPlugin2 = ({
|
||||
},
|
||||
};
|
||||
|
||||
annos.push(wipAnnoFrame);
|
||||
xAnnos.push(wipAnnoFrame);
|
||||
}
|
||||
|
||||
return annos;
|
||||
return {
|
||||
xAnnos,
|
||||
xyAnnos,
|
||||
};
|
||||
}, [annotations, newRange]);
|
||||
|
||||
const exitWipEdit = useCallback(() => {
|
||||
setNewRange(null);
|
||||
}, [setNewRange]);
|
||||
|
||||
const annoRef = useRef(annos);
|
||||
annoRef.current = annos;
|
||||
const xAnnoRef = useRef(xAnnos);
|
||||
xAnnoRef.current = xAnnos;
|
||||
|
||||
const xyAnnoRef = useRef(xyAnnos);
|
||||
xyAnnoRef.current = xyAnnos;
|
||||
|
||||
const newRangeRef = useRef(newRange);
|
||||
newRangeRef.current = newRange;
|
||||
|
||||
@@ -134,7 +143,8 @@ export const AnnotationsPlugin2 = ({
|
||||
});
|
||||
|
||||
config.addHook('draw', (u) => {
|
||||
let annos = annoRef.current;
|
||||
let xAnnos = xAnnoRef.current;
|
||||
let xyAnnos = xyAnnoRef.current;
|
||||
|
||||
const ctx = u.ctx;
|
||||
|
||||
@@ -144,40 +154,10 @@ export const AnnotationsPlugin2 = ({
|
||||
ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
ctx.clip();
|
||||
|
||||
annos.forEach((frame) => {
|
||||
// Multi-lane annotations do not support vertical lines or shaded regions
|
||||
xAnnos.forEach((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 {
|
||||
if (!multiLane) {
|
||||
let y0 = u.bbox.top;
|
||||
let y1 = y0 + u.bbox.height;
|
||||
|
||||
@@ -185,12 +165,14 @@ export const AnnotationsPlugin2 = ({
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
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);
|
||||
|
||||
let x0 = u.valToPos(vals.time[i], 'x', true);
|
||||
renderLine(ctx, y0, y1, x0, color);
|
||||
|
||||
if (vals.isRegion?.[i]) {
|
||||
// If dataframe does not have end times, let's omit rendering the region for now to prevent runtime error in valToPos
|
||||
// @todo do we want to fix isRegion to render a point (or use "to" as timeEnd) when we're missing timeEnd?
|
||||
if (vals.isRegion?.[i] && vals.timeEnd?.[i]) {
|
||||
let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
|
||||
renderLine(ctx, y0, y1, x1, color);
|
||||
|
||||
@@ -203,11 +185,44 @@ export const AnnotationsPlugin2 = ({
|
||||
}
|
||||
});
|
||||
|
||||
// xMin, xMax, yMin, yMax, color, lineWidth, lineStyle, fillOpacity, text
|
||||
xyAnnos.forEach((frame) => {
|
||||
let vals = getVals(frame);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
}, [config, canvasRegionRendering, getColorByName]);
|
||||
}, [config, canvasRegionRendering, getColorByName, multiLane]);
|
||||
|
||||
// ensure annos are re-drawn whenever they change
|
||||
// ensure xAnnos are re-drawn whenever they change
|
||||
useEffect(() => {
|
||||
if (plot) {
|
||||
plot.redraw();
|
||||
@@ -219,14 +234,17 @@ export const AnnotationsPlugin2 = ({
|
||||
forceUpdate();
|
||||
}, 0);
|
||||
}
|
||||
}, [annos, plot]);
|
||||
}, [xAnnos, plot]);
|
||||
|
||||
if (plot) {
|
||||
let markers = annos.flatMap((frame, frameIdx) => {
|
||||
let markers = xAnnos.flatMap((frame, frameIdx) => {
|
||||
let vals = getVals(frame);
|
||||
|
||||
let markers: React.ReactNode[] = [];
|
||||
|
||||
// Top offset for multi-lane annotations
|
||||
const top = multiLane ? frameIdx * ANNOTATION_LANE_SIZE : undefined;
|
||||
|
||||
for (let i = 0; i < vals.time.length; i++) {
|
||||
let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR);
|
||||
let left = Math.round(plot.valToPos(vals.time[i], 'x')) || 0; // handles -0
|
||||
@@ -243,14 +261,14 @@ export const AnnotationsPlugin2 = ({
|
||||
let clampedLeft = Math.max(0, left);
|
||||
let clampedRight = Math.min(plot.rect.width, right);
|
||||
|
||||
style = { left: clampedLeft, background: color, width: clampedRight - clampedLeft };
|
||||
style = { left: clampedLeft, background: color, width: clampedRight - clampedLeft, top };
|
||||
className = styles.annoRegion;
|
||||
}
|
||||
} else {
|
||||
isVisible = left >= 0 && left <= plot.rect.width;
|
||||
|
||||
if (isVisible) {
|
||||
style = { left, borderBottomColor: color };
|
||||
style = { left, borderBottomColor: color, top };
|
||||
className = styles.annoMarker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { arrayToDataFrame, createDataFrame, DataFrame, DataTopic, FieldType } from '@grafana/data';
|
||||
|
||||
import { getXAnnotationFrames, getXYAnnotationFrames } from './utils';
|
||||
|
||||
const exemplarFrame = createDataFrame({
|
||||
refId: 'A',
|
||||
name: 'exemplar',
|
||||
meta: {
|
||||
custom: {
|
||||
resultType: 'exemplar',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [30, 10, 40, 90, 14, 21],
|
||||
labels: { le: '6' },
|
||||
},
|
||||
{
|
||||
name: 'traceID',
|
||||
type: FieldType.string,
|
||||
values: ['unknown'],
|
||||
labels: { le: '6' },
|
||||
},
|
||||
],
|
||||
});
|
||||
const annotationRegionFrame: DataFrame = {
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: ['Milestones'],
|
||||
type: FieldType.string,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: ['#F2495C'],
|
||||
type: FieldType.string,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: [1720697881000],
|
||||
type: FieldType.time,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timeEnd',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: [1729081505000],
|
||||
type: FieldType.number,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 2,
|
||||
range: {
|
||||
min: 1729081505000,
|
||||
max: 1759857566000,
|
||||
delta: 30776061000,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: ['0.1.0'],
|
||||
type: FieldType.string,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: [true],
|
||||
type: FieldType.boolean,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'isRegion',
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: [true],
|
||||
type: FieldType.boolean,
|
||||
state: {
|
||||
displayName: null,
|
||||
seriesIndex: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
length: 1,
|
||||
meta: {
|
||||
dataTopic: DataTopic.Annotations,
|
||||
},
|
||||
};
|
||||
const annotationFrame: DataFrame = {
|
||||
...annotationRegionFrame,
|
||||
fields: [...annotationRegionFrame.fields.filter((f) => f.name !== 'timeEnd')],
|
||||
};
|
||||
const frames: DataFrame[] = [exemplarFrame, annotationRegionFrame, annotationFrame];
|
||||
const xymark = arrayToDataFrame([
|
||||
{
|
||||
time: 0,
|
||||
xMin: 0,
|
||||
xMax: 0,
|
||||
timeEnd: 0,
|
||||
yMin: 0,
|
||||
yMax: 100,
|
||||
isRegion: true,
|
||||
fillOpacity: 0.15,
|
||||
lineWidth: 1,
|
||||
lineStyle: 'solid',
|
||||
color: '#FF9930',
|
||||
text: 'Comparison selection',
|
||||
},
|
||||
]);
|
||||
xymark.name = 'xymark';
|
||||
|
||||
describe('getXAnnotationFrames', () => {
|
||||
it('should filter exemplar frames', () => {
|
||||
expect(getXAnnotationFrames(frames)).toEqual([annotationRegionFrame, annotationFrame]);
|
||||
});
|
||||
|
||||
it('should exclude xymark frames', () => {
|
||||
const framesWithxymark = [...frames, xymark];
|
||||
expect(getXAnnotationFrames(framesWithxymark)).toEqual([annotationRegionFrame, annotationFrame]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getXYAnnotationFrames', () => {
|
||||
it('should include xymark frames', () => {
|
||||
const framesWithxymark = [...frames, xymark];
|
||||
expect(getXYAnnotationFrames(framesWithxymark)).toEqual([xymark]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DataFrame, FieldType } from '@grafana/data';
|
||||
|
||||
// Annotation points/regions are 5px with 1px of padding
|
||||
export const ANNOTATION_LANE_SIZE = 7;
|
||||
|
||||
export function getXAnnotationFrames(dataFrames: DataFrame[] = []) {
|
||||
return dataFrames.filter(
|
||||
(frame) =>
|
||||
frame.name !== 'exemplar' &&
|
||||
frame.name !== 'xymark' &&
|
||||
frame.length > 0 &&
|
||||
frame.fields.some((f) => f.type === FieldType.time)
|
||||
);
|
||||
}
|
||||
|
||||
export function getXYAnnotationFrames(dataFrames: DataFrame[] = []) {
|
||||
return dataFrames.filter((frame) => frame.name === 'xymark');
|
||||
}
|
||||
Reference in New Issue
Block a user