diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx index d57e88cb35c..317fe648e1b 100644 --- a/public/app/core/components/TimeSeries/TimeSeries.tsx +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -32,9 +32,7 @@ export class UnthemedTimeSeries extends Component { tweakAxis, hoverProximity: options?.tooltip?.hoverProximity, orientation: options?.orientation, - xAxisConfig: { - ...calculateAnnotationLaneSizes(annotationLanes, options?.annotations), - }, + xAxisConfig: calculateAnnotationLaneSizes(annotationLanes, options?.annotations), }); }; diff --git a/public/app/core/components/TimeSeries/utils.test.ts b/public/app/core/components/TimeSeries/utils.test.ts index 583358c7c4c..c602a26b0e2 100644 --- a/public/app/core/components/TimeSeries/utils.test.ts +++ b/public/app/core/components/TimeSeries/utils.test.ts @@ -1,7 +1,7 @@ import { EventBus, FieldType } from '@grafana/data'; import { getTheme } from '@grafana/ui'; -import { preparePlotConfigBuilder } from './utils'; +import { calculateAnnotationLaneSizes, preparePlotConfigBuilder, UPLOT_DEFAULT_AXIS_GAP } from './utils'; describe('when fill below to option is used', () => { let eventBus: EventBus; @@ -272,3 +272,29 @@ describe('when fill below to option is used', () => { } }); }); + +describe('calculateAnnotationLaneSizes', () => { + it('should not regress', () => { + expect(calculateAnnotationLaneSizes()).toEqual({}); + expect(calculateAnnotationLaneSizes(6)).toEqual({}); + expect(calculateAnnotationLaneSizes(0, { multiLane: true })).toEqual({}); + expect(calculateAnnotationLaneSizes(1, { multiLane: true })).toEqual({}); + expect(calculateAnnotationLaneSizes(2, { multiLane: false })).toEqual({}); + }); + it('should return config to resize x-axis size, gap, and ticks size', () => { + expect(calculateAnnotationLaneSizes(2, { multiLane: true })).toEqual({ + gap: UPLOT_DEFAULT_AXIS_GAP, + size: 36, + ticks: { + size: 19, + }, + }); + expect(calculateAnnotationLaneSizes(3, { multiLane: true })).toEqual({ + gap: UPLOT_DEFAULT_AXIS_GAP, + size: 43, + ticks: { + size: 26, + }, + }); + }); +}); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index ae65dcc7749..494af48a2a6 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -75,8 +75,8 @@ import { import { ANNOTATION_LANE_SIZE } from '../../../plugins/panel/timeseries/plugins/utils'; // See UPlotAxisBuilder.ts::calculateAxisSize for default axis size calculation -const UPLOT_DEFAULT_AXIS_SIZE = 17; -const UPLOT_DEFAULT_AXIS_GAP = 5; +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)); @@ -721,7 +721,7 @@ export function calculateAnnotationLaneSizes( annotationLanes = 0, annotationConfig?: common.VizAnnotations ): Pick { - if (annotationConfig?.multiLane) { + if (annotationConfig?.multiLane && annotationLanes > 1) { const annotationLanesSize = annotationLanes * 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 = annotationLanes > 0 ? annotationLanesSize + UPLOT_DEFAULT_AXIS_GAP : 0; diff --git a/public/app/plugins/panel/timeseries/plugins/utils.test.ts b/public/app/plugins/panel/timeseries/plugins/utils.test.ts new file mode 100644 index 00000000000..67282bccead --- /dev/null +++ b/public/app/plugins/panel/timeseries/plugins/utils.test.ts @@ -0,0 +1,155 @@ +import { createDataFrame, DataFrame, DataTopic, FieldType } from '@grafana/data'; + +import { getAnnotationFrames } from './utils'; + +describe('getAnnotationFrames', () => { + 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 normalFrame = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const frames: DataFrame[] = [exemplarFrame, annotationRegionFrame, annotationFrame, normalFrame]; + + it('should filter non-annotation frames', () => { + expect(getAnnotationFrames(frames)).toEqual([annotationRegionFrame, annotationFrame]); + }); +}); diff --git a/public/app/plugins/panel/timeseries/plugins/utils.ts b/public/app/plugins/panel/timeseries/plugins/utils.ts index edd490b75ba..303433c463b 100644 --- a/public/app/plugins/panel/timeseries/plugins/utils.ts +++ b/public/app/plugins/panel/timeseries/plugins/utils.ts @@ -1,10 +1,20 @@ -import { DataFrame } from '@grafana/data'; +import { DataFrame, DataTopic } from '@grafana/data'; // Annotation points/regions are 5px with 1px of padding export const ANNOTATION_LANE_SIZE = 7; +/** + * Annotation frames: + * have a field named "time" + * have a frame meta dataTopic of "annotations" + * do not have a frame name of exemplar + * @param dataFrames + */ export function getAnnotationFrames(dataFrames: DataFrame[] = []) { return dataFrames.filter( - (frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time') + (frame) => + frame.name !== 'exemplar' && + frame.length > 0 && + frame.fields.some((f) => f.name === 'time' && frame.meta?.dataTopic === DataTopic.Annotations) ); }