From 053f169e3c6c45f1b1440fc5163b33df1ba727a6 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 21 Apr 2020 15:04:19 +0200 Subject: [PATCH] fix(annotations): fix alignment at the edges This commit fixed the alignment of rect and line annotations near the edge of a data domain. It aligns the x/y position specified by the dataValues of the annotation to the domain of the chart. BREAKING CHANGE: In the rectangular annotation, the `y0` parameter of the coordinates now refers to the minimum value and the `y1` value refers to the maximum value of the y domain. Previously, this was the opposite. fix #586 --- .gitignore | 1 + .playground/index.html | 3 + .playground/playground.tsx | 258 +++++++++++++++--- .../annotations/annotation_marker.test.tsx | 6 +- .../annotations/annotation_utils.test.ts | 99 +++---- .../xy_chart/annotations/annotation_utils.ts | 35 ++- .../annotations/line_annotation.test.ts | 115 ++++++++ .../annotations/line_annotation_tooltip.ts | 51 ++-- .../annotations/rect_annotation.test.ts | 249 +++++++++++++++++ .../annotations/rect_annotation_tooltip.ts | 38 ++- .../xy_chart/utils/axis_utils.test.ts | 6 +- src/chart_types/xy_chart/utils/specs.ts | 2 +- src/mocks/specs/specs.ts | 61 +++++ src/mocks/store/index.ts | 20 ++ src/mocks/store/store.ts | 54 ++++ 15 files changed, 845 insertions(+), 153 deletions(-) create mode 100644 src/chart_types/xy_chart/annotations/line_annotation.test.ts create mode 100644 src/chart_types/xy_chart/annotations/rect_annotation.test.ts create mode 100644 src/mocks/store/index.ts create mode 100644 src/mocks/store/store.ts diff --git a/.gitignore b/.gitignore index 53a06febdd..36759fa78d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ test/failure-screenshots/**/*.png reports/ tmp/ +.temp/ dist/ coverage/ .out/ diff --git a/.playground/index.html b/.playground/index.html index 3973665311..9ce5896129 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -33,6 +33,9 @@ height: 300px; margin: 20px; } + label { + display: block; + } diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 3a170dc677..8a8dd21c01 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -17,43 +17,235 @@ * under the License. */ import React from 'react'; -import { Chart, Partition, Settings, PartitionLayout, XYChartElementEvent, PartitionElementEvent } from '../src'; +import { + Chart, + ScaleType, + Settings, + RectAnnotation, + LineAnnotation, + TooltipType, + BarSeries, + LineSeries, + Axis, + Position, +} from '../src'; -export class Playground extends React.Component { - onElementClick = (elements: (XYChartElementEvent | PartitionElementEvent)[]) => { - // eslint-disable-next-line no-console - console.log(elements); +// const data = [ +// { x: 0, min: 0, max: 1 }, +// { x: 10, min: 0, max: 2 }, +// // { x: 2, min: 0, max: 3 }, +// ]; + +const data = new Array(11).fill(0).map((d, i) => { + return { + x: i, + max: Math.random() * 10, + }; +}); +interface State { + showRectAnnotation: boolean; + showLineXAnnotation: boolean; + showLineYAnnotation: boolean; + totalBars: number; + useLinearBar: boolean; + useOrdinalBar: boolean; + useHistogramBar: boolean; + totalLines: number; + useLinearLine: boolean; + useOrdinalLine: boolean; +} +export class Playground extends React.Component<{}, State> { + state: State = { + showRectAnnotation: false, + showLineXAnnotation: false, + showLineYAnnotation: false, + totalBars: 1, + totalLines: 1, + useLinearBar: true, + useOrdinalBar: false, + useHistogramBar: false, + useLinearLine: false, + useOrdinalLine: false, + }; + handleInputChange = (stateParam: keyof State) => (event: React.ChangeEvent) => { + const updatedValue = stateParam === 'totalBars' || stateParam === 'totalLines' ? Number(event.target.value) : 1; + this.setState((prevState: State) => { + return { + ...prevState, + [stateParam]: stateParam === 'totalBars' || stateParam === 'totalLines' ? updatedValue : !prevState[stateParam], + }; + }); }; render() { + const keys: Array = [ + 'showRectAnnotation', + 'showLineXAnnotation', + 'showLineYAnnotation', + 'useLinearBar', + 'useOrdinalBar', + 'useHistogramBar', + 'useLinearLine', + 'useOrdinalLine', + 'totalBars', + 'totalLines', + ]; return ( -
- - - { - return d.v; - }} - data={[ - { g1: 'a', g2: 'a', v: 1 }, - { g1: 'a', g2: 'b', v: 1 }, - { g1: 'b', g2: 'a', v: 1 }, - { g1: 'b', g2: 'b', v: 1 }, - ]} - layers={[ - { - groupByRollup: (datum: { g1: string }) => datum.g1, - }, - { - groupByRollup: (datum: { g2: string }) => datum.g2, - }, - ]} - /> - -
+ <> +
+
+ {keys.map((key) => { + return ( + + ); + })} +
+
+
+ + + + + {this.state.showRectAnnotation && ( + + )} + {this.state.showLineYAnnotation && ( +
} + /> + )} + {this.state.showLineXAnnotation && ( + } + /> + )} + {this.state.useLinearBar && + new Array(this.state.totalBars) + .fill(0) + .map((d, i) => ( + + ))} + + {this.state.useOrdinalBar && + new Array(this.state.totalBars) + .fill(0) + .map((d, i) => ( + + ))} + + {this.state.useHistogramBar && + new Array(this.state.totalBars) + .fill(0) + .map((d, i) => ( + + ))} + + {this.state.useOrdinalLine && + new Array(this.state.totalLines) + .fill(0) + .map((d, i) => ( + + ))} + + {this.state.useLinearLine && + new Array(this.state.totalLines) + .fill(0) + .map((d, i) => ( + + ))} + + + ); } } diff --git a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx index cbfd81d4b9..17b813236a 100644 --- a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx +++ b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx @@ -76,7 +76,7 @@ describe('annotation marker', () => { xScale, Position.Left, 0, - false, + 0, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -131,7 +131,7 @@ describe('annotation marker', () => { xScale, Position.Left, 0, - false, + 0, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -185,7 +185,7 @@ describe('annotation marker', () => { xScale, Position.Bottom, 0, - false, + 0, ); const expectedDimensions: AnnotationLineProps[] = [ { diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts index 4d885075ca..dda358f9c9 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts @@ -116,7 +116,7 @@ describe('annotation utils', () => { axesSpecs.push(verticalAxisSpec); - test('should compute annotation dimensions', () => { + test('should compute rect annotation in x ordinal scale', () => { const chartRotation: Rotation = 0; const yScales: Map = new Map(); yScales.set(groupId, continuousScale); @@ -171,15 +171,16 @@ describe('annotation utils', () => { start: { x1: 0, y1: 20 }, end: { x2: 10, y2: 20 }, }, + marker: undefined, details: { detailsText: 'foo', headerText: '2' }, }, ]); - expectedDimensions.set(rectAnnotationId, [{ rect: { x: 0, y: 30, width: 25, height: 20 } }]); + expectedDimensions.set(rectAnnotationId, [{ rect: { x: 0, y: 30, width: 50, height: 20 } }]); expect(dimensions).toEqual(expectedDimensions); }); - test('should not compute annotation dimensions if a corresponding axis does not exist', () => { + test('should compute annotation dimensions also with missing axis', () => { const chartRotation: Rotation = 0; const yScales: Map = new Map(); yScales.set(groupId, continuousScale); @@ -211,8 +212,7 @@ describe('annotation utils', () => { 1, false, ); - const expectedDimensions = new Map(); - expect(dimensions).toEqual(expectedDimensions); + expect(dimensions.size).toEqual(1); }); test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { @@ -242,7 +242,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -288,7 +287,6 @@ describe('annotation utils', () => { xScale, Position.Right, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -334,7 +332,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -379,7 +376,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); expect(dimensions).toEqual(null); }); @@ -409,7 +405,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -453,7 +448,6 @@ describe('annotation utils', () => { xScale, Position.Top, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -498,7 +492,6 @@ describe('annotation utils', () => { xScale, Position.Bottom, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -543,7 +536,7 @@ describe('annotation utils', () => { xScale, Position.Bottom, 0, - true, + 1, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -589,7 +582,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -635,7 +627,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -681,7 +672,6 @@ describe('annotation utils', () => { xScale, Position.Left, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -727,7 +717,6 @@ describe('annotation utils', () => { xScale, Position.Top, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -772,7 +761,6 @@ describe('annotation utils', () => { xScale, Position.Bottom, 0, - false, ); const expectedDimensions: AnnotationLineProps[] = [ { @@ -792,7 +780,7 @@ describe('annotation utils', () => { expect(dimensions).toEqual(expectedDimensions); }); - test('should not compute annotation line values for values outside of domain or AnnotationSpec.hideLines', () => { + test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { const chartRotation: Rotation = 0; const yScales: Map = new Map(); yScales.set(groupId, continuousScale); @@ -819,7 +807,6 @@ describe('annotation utils', () => { xScale, Position.Right, 0, - false, ); expect(emptyXDimensions).toEqual([]); @@ -843,7 +830,6 @@ describe('annotation utils', () => { continuousScale, Position.Right, 0, - false, ); expect(invalidStringXDimensions).toEqual([]); @@ -859,7 +845,7 @@ describe('annotation utils', () => { style: DEFAULT_ANNOTATION_LINE_STYLE, }; - const emptyOutOfBoundsXDimensions = computeLineAnnotationDimensions( + const cappedToMinX = computeLineAnnotationDimensions( outOfBoundsXLineAnnotation, chartDimensions, chartRotation, @@ -867,10 +853,8 @@ describe('annotation utils', () => { continuousScale, Position.Right, 0, - false, ); - - expect(emptyOutOfBoundsXDimensions).toEqual([]); + expect(cappedToMinX).toHaveLength(1); const invalidYLineAnnotation: AnnotationSpec = { chartType: ChartTypes.XYAxis, @@ -891,7 +875,6 @@ describe('annotation utils', () => { xScale, Position.Right, 0, - false, ); expect(emptyYDimensions).toEqual([]); @@ -907,7 +890,7 @@ describe('annotation utils', () => { style: DEFAULT_ANNOTATION_LINE_STYLE, }; - const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( + const cappedToMinY = computeLineAnnotationDimensions( outOfBoundsYLineAnnotation, chartDimensions, chartRotation, @@ -915,10 +898,9 @@ describe('annotation utils', () => { xScale, Position.Right, 0, - false, ); - expect(emptyOutOfBoundsYDimensions).toEqual([]); + expect(cappedToMinY).toHaveLength(1); const invalidStringYLineAnnotation: AnnotationSpec = { chartType: ChartTypes.XYAxis, @@ -939,7 +921,6 @@ describe('annotation utils', () => { continuousScale, Position.Right, 0, - false, ); expect(invalidStringYDimensions).toEqual([]); @@ -964,7 +945,6 @@ describe('annotation utils', () => { continuousScale, Position.Right, 0, - false, ); expect(hiddenAnnotationDimensions).toEqual(null); @@ -1379,7 +1359,7 @@ describe('annotation utils', () => { dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], }; - const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0, 1); expect(noYScale).toBe(null); }); @@ -1401,9 +1381,9 @@ describe('annotation utils', () => { ], }; - const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0, 1); - expect(skippedInvalid).toEqual([]); + expect(skippedInvalid).toHaveLength(1); }); test('should compute rectangle dimensions shifted for histogram mode', () => { const yScales: Map = new Map(); @@ -1411,7 +1391,7 @@ describe('annotation utils', () => { const xScale: Scale = new ScaleContinuous( { type: ScaleType.Linear, domain: continuousData, range: [minRange, maxRange] }, - { bandwidth: 1, minInterval: 1 }, + { bandwidth: 72, minInterval: 1 }, ); const annotationRectangle: RectAnnotationSpec = { @@ -1428,7 +1408,7 @@ describe('annotation utils', () => { ], }; - const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, true, 0); + const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, true, 0, 1); const [dims1, dims2, dims3, dims4] = dimensions; expect(dims1.rect.x).toBe(10); @@ -1438,18 +1418,18 @@ describe('annotation utils', () => { expect(dims2.rect.x).toBe(0); expect(dims2.rect.y).toBe(0); - expect(dims2.rect.width).toBe(10); + expect(dims2.rect.width).toBe(20); expect(dims2.rect.height).toBe(100); expect(dims3.rect.x).toBe(0); - expect(dims3.rect.y).toBe(0); + expect(dims3.rect.y).toBe(10); expect(dims3.rect.width).toBeCloseTo(110); - expect(dims3.rect.height).toBe(10); + expect(dims3.rect.height).toBe(90); expect(dims4.rect.x).toBe(0); - expect(dims4.rect.y).toBe(10); + expect(dims4.rect.y).toBe(0); expect(dims4.rect.width).toBeCloseTo(110); - expect(dims4.rect.height).toBe(90); + expect(dims4.rect.height).toBe(10); }); test('should compute rectangle dimensions when only a single coordinate defined', () => { const yScales: Map = new Map(); @@ -1471,13 +1451,13 @@ describe('annotation utils', () => { ], }; - const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0, 0); const expectedDimensions = [ - { rect: { x: 10, y: 0, width: 90, height: 100 } }, - { rect: { x: 0, y: 0, width: 10, height: 100 } }, - { rect: { x: 0, y: 0, width: 100, height: 10 } }, - { rect: { x: 0, y: 10, width: 100, height: 90 } }, + { rect: { x: 10, y: 0, width: 90, height: 100 }, details: undefined }, + { rect: { x: 0, y: 0, width: 10, height: 100 }, details: undefined }, + { rect: { x: 0, y: 10, width: 100, height: 90 }, details: undefined }, + { rect: { x: 0, y: 0, width: 100, height: 10 }, details: undefined }, ]; expect(dimensions).toEqual(expectedDimensions); @@ -1497,7 +1477,7 @@ describe('annotation utils', () => { dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], }; - const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0, 0); expect(unrotated).toEqual([{ rect: { x: 10, y: 30, width: 10, height: 20 } }]); }); @@ -1507,6 +1487,7 @@ describe('annotation utils', () => { const xScale: Scale = ordinalScale; + // will render a rectangle that inclused both a and b const annotationRectangle: RectAnnotationSpec = { chartType: ChartTypes.XYAxis, specType: SpecTypes.Annotation, @@ -1516,26 +1497,28 @@ describe('annotation utils', () => { dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 0, y1: 2 } }], }; - const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0, 1); - expect(unrotated).toEqual([{ rect: { x: 0, y: 0, width: 25, height: 20 } }]); + expect(unrotated).toEqual([{ rect: { x: 0, y: 0, width: 50, height: 20 } }]); }); test('should validate scaled dataValues', () => { // not aligned with tick - expect(scaleAndValidateDatum('', ordinalScale, false)).toBe(null); - expect(scaleAndValidateDatum('a', continuousScale, false)).toBe(null); - expect(scaleAndValidateDatum(-10, continuousScale, false)).toBe(null); - expect(scaleAndValidateDatum(20, continuousScale, false)).toBe(null); + expect(scaleAndValidateDatum('', ordinalScale, 0)).toBe(null); + expect(scaleAndValidateDatum('a', continuousScale, 0)).toBe(null); + + // valid value limited to min/max + expect(scaleAndValidateDatum(-10, continuousScale, 0)).toBe(0); + expect(scaleAndValidateDatum(20, continuousScale, 0)).toBe(100); // allow values within domainEnd + minInterval when not alignWithTick - expect(scaleAndValidateDatum(10.25, continuousScale, false)).toBeCloseTo(102.5); - expect(scaleAndValidateDatum(10.25, continuousScale, true)).toBe(null); + expect(scaleAndValidateDatum(10.25, continuousScale, 1)).toBeCloseTo(102.5); + expect(scaleAndValidateDatum(10.25, continuousScale, 0)).toBe(100); - expect(scaleAndValidateDatum('a', ordinalScale, false)).toBe(0); - expect(scaleAndValidateDatum(0, continuousScale, false)).toBe(0); + expect(scaleAndValidateDatum('a', ordinalScale, 0)).toBe(0); + expect(scaleAndValidateDatum(0, continuousScale, 0)).toBe(0); // aligned with tick - expect(scaleAndValidateDatum(1.25, continuousScale, true)).toBe(12.5); + expect(scaleAndValidateDatum(1.25, continuousScale, 0)).toBe(12.5); }); test('should determine if a point is within a rectangle annotation', () => { const cursorPosition = { x: 3, y: 4 }; diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts index 8cf152eeae..17c2061ee2 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.ts @@ -87,25 +87,36 @@ export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[] export type Bounds = { startX: number; endX: number; startY: number; endY: number }; /** @internal */ -export function scaleAndValidateDatum(dataValue: any, scale: Scale, alignWithTick: boolean): number | null { +export function scaleAndValidateDatum( + value: any, + scale: Scale, + totalBarsInCluster: number, + lastValue?: boolean, +): number | null { const isContinuous = scale.type !== ScaleType.Ordinal; - const scaledValue = scale.scale(dataValue); + const scaledValue = scale.scale(value); // d3.scale will return 0 for '', rendering the line incorrectly at 0 - if (scaledValue === null || (isContinuous && dataValue === '')) { + if (scaledValue === null || (isContinuous && value === '')) { return null; } if (isContinuous) { const [domainStart, domainEnd] = scale.domain; + const adjustedDomainEnd = domainEnd + (totalBarsInCluster > 0 ? scale.minInterval : 0); - // if we're not aligning the ticks, we need to extend the domain by one more tick for histograms - const domainEndOffset = alignWithTick ? 0 : scale.minInterval; - - if (domainStart > dataValue || domainEnd + domainEndOffset < dataValue) { - return null; + // limit the value to min or max of the domain + if (value < domainStart) { + return scale.scale(domainStart); } + if (value > adjustedDomainEnd) { + return scale.scale(adjustedDomainEnd); + } + return scaledValue; + } + // is ordinal scale + if (lastValue) { + return scaledValue + scale.bandwidth * totalBarsInCluster; } - return scaledValue; } @@ -187,9 +198,6 @@ export function computeAnnotationDimensions( const { groupId, domainType } = annotationSpec; const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); - if (!annotationAxisPosition) { - return; - } const dimensions = computeLineAnnotationDimensions( annotationSpec, chartDimensions, @@ -198,7 +206,7 @@ export function computeAnnotationDimensions( xScale, annotationAxisPosition, xScaleOffset - clusterOffset, - enableHistogramMode, + totalBarsInCluster, ); if (dimensions) { @@ -211,6 +219,7 @@ export function computeAnnotationDimensions( xScale, enableHistogramMode, barsPadding, + totalBarsInCluster, ); if (dimensions) { diff --git a/src/chart_types/xy_chart/annotations/line_annotation.test.ts b/src/chart_types/xy_chart/annotations/line_annotation.test.ts new file mode 100644 index 0000000000..f81140bedb --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line_annotation.test.ts @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { MockStore } from '../../../mocks/store'; +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../mocks/specs'; +import { computeAnnotationDimensionsSelector } from '../state/selectors/compute_annotations'; +import { ScaleType } from '../../../scales'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar', + indexPosition: number, + expectedLinePosition: number, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => { + return MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }); + }); + const annotation = MockAnnotationSpec.line({ + dataValues: [ + { + dataValue: indexPosition, + }, + ], + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + expect(annotations.get(annotation.id)).toEqual([ + { + anchor: { left: expectedLinePosition, position: 'bottom', top: 100 }, + details: { detailsText: undefined, headerText: `${indexPosition}` }, + linePathPoints: { + start: { x1: expectedLinePosition, y1: 100 }, + end: { x2: expectedLinePosition, y2: 0 }, + }, + marker: undefined, + }, + ]); +} + +describe('Render vertical line annotation within', () => { + it.each([ + [0, 1, 12.5], // middle of 1st bar + [1, 1, 37.5], // middle of 2nd bar + [2, 1, 62.5], // middle of 3rd bar + [3, 1, 87.5], // middle of 4th bar + [1, 2, 37.5], // middle of 2nd bar + [1, 3, 37.5], // middle of 2nd bar + ])('a bar at position %i, %i specs, all scales', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Time); + }); + + it.each([ + [0, 1, 0], // the start of the chart + [1, 1, 50], // the middle of the chart + [2, 1, 100], // the end of the chart + [1, 2, 50], // the middle of the chart + [1, 3, 50], // the middle of the chart + ])('line point at position %i, %i specs, linear scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs); + }); + + it.each([ + [0, 1, 12.5], // 1st ordinal line point + [1, 1, 37.5], // 2nd ordinal line point + [2, 1, 62.5], // 3rd ordinal line point + [3, 1, 87.5], // 4th ordinal line point + [1, 2, 37.5], // 2nd ordinal line point + [1, 3, 37.5], // 2nd ordinal line point + ])('line point at position %i, %i specs, Ordinal scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts index e9c72a1b35..2e819cfa6c 100644 --- a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts @@ -38,7 +38,6 @@ import { isHorizontalAxis } from '../utils/axis_utils'; import { Dimensions } from '../../../utils/dimensions'; import { Scale } from '../../../scales'; import { GroupId } from '../../../utils/ids'; -import { LineAnnotationStyle } from '../../../utils/themes/theme'; import { Point } from '../../../utils/point'; import { isWithinRectBounds } from './rect_annotation_tooltip'; @@ -85,7 +84,7 @@ function computeYDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], yScale: Scale, chartRotation: Rotation, - axisPosition: Position, + axisPosition: Position | null, chartDimensions: Dimensions, lineColor: string, marker?: JSX.Element, @@ -94,11 +93,14 @@ function computeYDomainLineAnnotationDimensions( const chartHeight = chartDimensions.height; const chartWidth = chartDimensions.width; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); - + // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis + // but we are displaying a line annotation + const anchorPosition = + axisPosition === null ? (isHorizontalChartRotation ? Position.Bottom : Position.Left) : axisPosition; const lineProps: AnnotationLineProps[] = []; dataValues.forEach((datum: LineAnnotationDatum) => { - const { dataValue } = datum; + let { dataValue } = datum; // avoid rendering invalid annotation value if (dataValue === null || dataValue === undefined || dataValue === '') { @@ -113,11 +115,15 @@ function computeYDomainLineAnnotationDimensions( const [domainStart, domainEnd] = yScale.domain; // avoid rendering annotation with values outside the scale domain - if (domainStart > dataValue || domainEnd < dataValue) { - return; + if (dataValue < domainStart) { + dataValue = domainStart; } + if (dataValue > domainEnd) { + dataValue = domainEnd; + } + const anchor = { - position: axisPosition, + position: anchorPosition, top: 0, left: 0, }; @@ -129,7 +135,7 @@ function computeYDomainLineAnnotationDimensions( // the Y axis is vertical, X axis is horizontal y|--x--|y if (isHorizontalChartRotation) { // y|__x__ - if (axisPosition === Position.Left) { + if (anchorPosition === Position.Left) { anchor.left = 0; markerPosition.left = -markerDimension.width; linePathPoints.start.x1 = 0; @@ -155,7 +161,7 @@ function computeYDomainLineAnnotationDimensions( // the Y axis is horizontal, X axis is vertical x|--y--|x } else { // ¯¯y¯¯ - if (axisPosition === Position.Top) { + if (anchorPosition === Position.Top) { anchor.top = 0; markerPosition.top = -markerDimension.height; linePathPoints.start.x1 = 0; @@ -208,11 +214,11 @@ function computeXDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], xScale: Scale, chartRotation: Rotation, - axisPosition: Position, + axisPosition: Position | null, chartDimensions: Dimensions, lineColor: string, xScaleOffset: number, - enableHistogramMode: boolean, + totalBarsInCluster: number, marker?: JSX.Element, markerDimension = { width: 0, height: 0 }, ): AnnotationLineProps[] { @@ -220,12 +226,15 @@ function computeXDomainLineAnnotationDimensions( const chartWidth = chartDimensions.width; const lineProps: AnnotationLineProps[] = []; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); + // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis + // but we are displaying a line annotation + const anchorPosition = + axisPosition === null ? (isHorizontalChartRotation ? Position.Bottom : Position.Left) : axisPosition; - const alignWithTick = xScale.bandwidth > 0 && !enableHistogramMode; dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; - const scaledXValue = scaleAndValidateDatum(dataValue, xScale, alignWithTick); + const scaledXValue = scaleAndValidateDatum(dataValue, xScale, totalBarsInCluster, false); if (scaledXValue == null) { return; @@ -240,14 +249,14 @@ function computeXDomainLineAnnotationDimensions( end: { x2: 0, y2: 0 }, }; const anchor = { - position: axisPosition, + position: anchorPosition, top: 0, left: 0, }; // the Y axis is vertical, X axis is horizontal y|--x--|y if (isHorizontalChartRotation) { // __x__ - if (axisPosition === Position.Bottom) { + if (anchorPosition === Position.Bottom) { linePathPoints.start.y1 = chartHeight; linePathPoints.end.y2 = 0; anchor.top = chartHeight; @@ -273,7 +282,7 @@ function computeXDomainLineAnnotationDimensions( // the Y axis is horizontal, X axis is vertical x|--y--|x } else { // x|--y-- - if (axisPosition === Position.Left) { + if (anchorPosition === Position.Left) { anchor.left = 0; markerPosition.left = -markerDimension.width; linePathPoints.start.x1 = annotationValueXposition; @@ -330,9 +339,9 @@ export function computeLineAnnotationDimensions( chartRotation: Rotation, yScales: Map, xScale: Scale, - axisPosition: Position, + axisPosition: Position | null, xScaleOffset: number, - enableHistogramMode: boolean, + totalBarsInCluster: number = 0, ): AnnotationLineProps[] | null { const { domainType, dataValues, marker, markerDimensions, hideLines } = annotationSpec; @@ -341,8 +350,8 @@ export function computeLineAnnotationDimensions( } // this type is guaranteed as this has been merged with default - const lineStyle = annotationSpec.style as LineAnnotationStyle; - const lineColor = lineStyle.line.stroke; + const lineStyle = annotationSpec.style; + const lineColor = lineStyle?.line?.stroke ?? 'red'; if (domainType === AnnotationDomainTypes.XDomain) { return computeXDomainLineAnnotationDimensions( @@ -353,7 +362,7 @@ export function computeLineAnnotationDimensions( chartDimensions, lineColor, xScaleOffset, - enableHistogramMode, + totalBarsInCluster, marker, markerDimensions, ); diff --git a/src/chart_types/xy_chart/annotations/rect_annotation.test.ts b/src/chart_types/xy_chart/annotations/rect_annotation.test.ts new file mode 100644 index 0000000000..e4838b25e1 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect_annotation.test.ts @@ -0,0 +1,249 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ +import { MockStore } from '../../../mocks/store'; +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../mocks/specs'; +import { computeAnnotationDimensionsSelector } from '../state/selectors/compute_annotations'; +import { ScaleType } from '../../../scales'; +import { RectAnnotationDatum } from '../utils/specs'; +import { AnnotationRectProps } from './rect_annotation_tooltip'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar', + dataValues: RectAnnotationDatum[], + expectedRect: { + x: number; + y: number; + width: number; + height: number; + }, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => { + return MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }); + }); + const annotation = MockAnnotationSpec.rect({ + dataValues, + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + const renderedAnnotations = annotations.get(annotation.id)!; + expect(renderedAnnotations.length).toBe(1); + const { rect } = renderedAnnotations[0] as AnnotationRectProps; + expect(rect.x).toBeCloseTo(expectedRect.x, 3); + expect(rect.y).toBeCloseTo(expectedRect.y, 3); + expect(rect.width).toBeCloseTo(expectedRect.width, 3); + expect(rect.height).toBeCloseTo(expectedRect.height, 3); +} + +describe('Render rect annotation within', () => { + it.each([ + // x0, numOfSpecs, x, width + [0, 1, 0, 100], + [1, 1, 25, 75], + [2, 1, 50, 50], + [3, 1, 75, 25], + [1, 2, 25, 75], + [2, 3, 50, 50], + ])('bars starting from %i, %i specs, all scales', (x0, numOfSpecs, x, width) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each([ + // x1, numOfSpecs, x, width + [0, 1, 0, 25], + [1, 1, 0, 50], + [2, 1, 0, 75], + [3, 1, 0, 100], + [1, 2, 0, 50], + [2, 2, 0, 75], + ])('bars starting ending at %i, %i specs, all scales', (x1, numOfSpecs, x, width) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each([ + // x0, x1, numOfSpecs, x, width + [0, 0, 1, 0, 25], + [0, 1, 1, 0, 50], + [1, 3, 1, 25, 75], + [0, 1, 2, 0, 50], + [1, 3, 3, 25, 75], + ])('bars starting starting at %i, ending at %i, %i specs, all scales', (x0, x1, numOfSpecs, x, width) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each([ + // x0, x1, numOfSpecs, x, width + [0, 0, 1, 0, 25], + [0, 1, 1, 0, 50], + [1, 3, 1, 25, 75], + [0, 1, 2, 0, 50], + [1, 3, 3, 25, 75], + ])('lines starting starting at %i, ending at %i, %i specs, ordinal scale', (x0, x1, numOfSpecs, x, width) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + }); + + it.each([ + // x0, x1, numOfSpecs, x, width + [0, 0, 1, 0, 0], + [0, 1, 1, 0, 50], + [1, 2, 1, 50, 50], + [0, 2, 1, 0, 100], + [0, 1, 2, 0, 50], + [1, 2, 3, 50, 50], + ])('lines starting starting at %i, ending at %i, %i specs, continuous scale', (x0, x1, numOfSpecs, x, width) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Linear); + }); + + it('out of bound annotations upper y', () => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { y1: 10 }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + }); + + it('out of bound annotations lower y', () => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { y0: -4 }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + }); + + it('out of bound annotations lower x', () => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0: -4 }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + }); + + it('out of bound annotations upper x', () => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x1: 5 }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts index b62a4210c9..dbbf667f62 100644 --- a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts @@ -20,7 +20,7 @@ import { AnnotationTypes, RectAnnotationDatum, RectAnnotationSpec } from '../uti import { Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { GroupId } from '../../../utils/ids'; -import { Scale } from '../../../scales'; +import { Scale, ScaleType } from '../../../scales'; import { Point } from '../../../utils/point'; import { AnnotationTooltipFormatter, @@ -98,14 +98,15 @@ export function computeRectAnnotationDimensions( xScale: Scale, enableHistogramMode: boolean, barsPadding: number, + totalBarsInCluster: number, ): AnnotationRectProps[] | null { const { dataValues } = annotationSpec; - const groupId = annotationSpec.groupId; const yScale = yScales.get(groupId); if (!yScale) { return null; } + const hasBars = totalBarsInCluster > 0; const xDomain = xScale.domain; const yDomain = yScale.domain; @@ -115,16 +116,14 @@ export function computeRectAnnotationDimensions( dataValues.forEach((dataValue: RectAnnotationDatum) => { let { x0, x1, y0, y1 } = dataValue.coordinates; - // if everything is null, return; otherwise we coerce the other coordinates if (x0 == null && x1 == null && y0 == null && y1 == null) { return; } - - if (x1 == null) { - // if x1 is defined, we want the rect to draw to the end of the scale - // if we're in histogram mode, extend domain end by min interval - x1 = enableHistogramMode && !xScale.isSingleValue() ? lastX + xMinInterval : lastX; + x1 = x1 == null ? lastX : x1; + if (hasBars && xScale.type !== ScaleType.Ordinal) { + // if bar chart cover fully the last bar + x1 = x1 + xMinInterval; } if (x0 == null) { @@ -133,22 +132,20 @@ export function computeRectAnnotationDimensions( } if (y0 == null) { - // if y0 is defined, we want the rect to draw to the end of the scale - y0 = yDomain[yDomain.length - 1]; + // if y0 is not defined, start from the beginning of the yScale + y0 = yDomain[0]; } if (y1 == null) { - // if y1 is defined, we want the rect to draw to the start of the scale - y1 = yDomain[0]; + // if y1 is not defined, end the annotation at the end of the yScale + y1 = yDomain[yDomain.length - 1]; } - - const alignWithTick = xScale.bandwidth > 0 && !enableHistogramMode; - - let x0Scaled = scaleAndValidateDatum(x0, xScale, alignWithTick); - let x1Scaled = scaleAndValidateDatum(x1, xScale, alignWithTick); - const y0Scaled = scaleAndValidateDatum(y0, yScale, false); - const y1Scaled = scaleAndValidateDatum(y1, yScale, false); - + const barSpaces = totalBarsInCluster > 0 ? totalBarsInCluster : xScale.type === ScaleType.Ordinal ? 1 : 0; + let x0Scaled = scaleAndValidateDatum(x0, xScale, barSpaces); + let x1Scaled = scaleAndValidateDatum(x1, xScale, barSpaces, true); + // we don't consider bars in y scale + const y0Scaled = scaleAndValidateDatum(y0, yScale, 0); + const y1Scaled = scaleAndValidateDatum(y1, yScale, 0, true); // TODO: surface this as a warning if (x0Scaled === null || x1Scaled === null || y0Scaled === null || y1Scaled === null) { return; @@ -187,6 +184,5 @@ export function computeRectAnnotationDimensions( details: dataValue.details, }); }); - return rectsProps; } diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index 60beaf4710..5526381cc7 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -18,7 +18,7 @@ import { XDomain } from '../domains/x_domain'; import { YDomain } from '../domains/y_domain'; -import { AxisSpec, DomainRange, AxisStyle } from './specs'; +import { AxisSpec, DomainRange, AxisStyle, DEFAULT_GLOBAL_ID } from './specs'; import { Position } from '../../../utils/commons'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { AxisId, GroupId } from '../../../utils/ids'; @@ -1452,7 +1452,7 @@ describe('Axis computational utils', () => { showDuplicatedTicks: false, chartType: 'xy_axis', specType: 'axis', - groupId: '__global__', + groupId: DEFAULT_GLOBAL_ID, hide: false, showOverlappingLabels: false, showOverlappingTicks: false, @@ -1492,7 +1492,7 @@ describe('Axis computational utils', () => { showDuplicatedTicks: true, chartType: 'xy_axis', specType: 'axis', - groupId: '__global__', + groupId: DEFAULT_GLOBAL_ID, hide: false, showOverlappingLabels: false, showOverlappingTicks: false, diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index e320aba0f2..6f8cf2b91b 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -668,7 +668,7 @@ export interface BaseAnnotationSpec< D extends RectAnnotationDatum | LineAnnotationDatum, S extends RectAnnotationStyle | LineAnnotationStyle > extends Spec { - chartType: ChartTypes; + chartType: typeof ChartTypes.XYAxis; specType: typeof SpecTypes.Annotation; /** * Annotation type: line, rectangle, text diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index 06d830f329..e033aa2127 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -28,6 +28,10 @@ import { BasicSeriesSpec, SeriesTypes, BubbleSeriesSpec, + LineAnnotationSpec, + RectAnnotationSpec, + AnnotationTypes, + AnnotationDomainTypes, } from '../../chart_types/xy_chart/utils/specs'; import { ScaleType } from '../../scales'; import { ChartTypes } from '../../chart_types'; @@ -221,6 +225,17 @@ export class MockSeriesSpec { return MockSeriesSpec.barBase; } } + static byTypePartial(type?: 'line' | 'bar' | 'area') { + switch (type) { + case 'line': + return MockSeriesSpec.line; + case 'area': + return MockSeriesSpec.area; + case 'bar': + default: + return MockSeriesSpec.bar; + } + } } export class MockSeriesSpecs { @@ -264,7 +279,53 @@ export class MockGlobalSpec { theme: LIGHT_THEME, }; + private static readonly settingsBaseNoMargings: SettingsSpec = { + ...MockGlobalSpec.settingsBase, + theme: { + ...LIGHT_THEME, + chartMargins: { top: 0, left: 0, right: 0, bottom: 0 }, + chartPaddings: { top: 0, left: 0, right: 0, bottom: 0 }, + scales: { + barsPadding: 0, + }, + }, + }; + static settings(partial?: Partial): SettingsSpec { return mergePartial(MockGlobalSpec.settingsBase, partial, { mergeOptionalPartialValues: true }); } + static settingsNoMargins(partial?: Partial): SettingsSpec { + return mergePartial(MockGlobalSpec.settingsBaseNoMargings, partial, { + mergeOptionalPartialValues: true, + }); + } +} + +/** @internal */ +export class MockAnnotationSpec { + private static readonly lineBase: LineAnnotationSpec = { + id: 'line_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + dataValues: [], + domainType: AnnotationDomainTypes.XDomain, + }; + + private static readonly rectBase: RectAnnotationSpec = { + id: 'rect_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Rectangle, + dataValues: [], + }; + + static line(partial?: Partial): LineAnnotationSpec { + return mergePartial(MockAnnotationSpec.lineBase, partial, { mergeOptionalPartialValues: true }); + } + static rect(partial?: Partial): RectAnnotationSpec { + return mergePartial(MockAnnotationSpec.rectBase, partial, { mergeOptionalPartialValues: true }); + } } diff --git a/src/mocks/store/index.ts b/src/mocks/store/index.ts new file mode 100644 index 0000000000..4702768221 --- /dev/null +++ b/src/mocks/store/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +/** @internal */ +export * from './store'; diff --git a/src/mocks/store/store.ts b/src/mocks/store/store.ts new file mode 100644 index 0000000000..14110e9398 --- /dev/null +++ b/src/mocks/store/store.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { chartStoreReducer, GlobalChartState } from '../../state/chart_state'; +import { createStore, Store } from 'redux'; +import { specParsing, upsertSpec, specParsed } from '../../state/actions/specs'; +import { Spec } from '../../specs'; +import { updateParentDimensions } from '../../state/actions/chart_settings'; + +/** @internal */ +export class MockStore { + static default( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + chartId = 'chartId', + ): Store { + const storeReducer = chartStoreReducer(chartId); + const store = createStore(storeReducer); + store.dispatch(updateParentDimensions({ width, height, top, left })); + return store; + } + + static addSpecs(specs: Spec | Array, store: Store) { + store.dispatch(specParsing()); + if (Array.isArray(specs)) { + const actions = specs.map(upsertSpec); + actions.forEach(store.dispatch); + } else { + store.dispatch(upsertSpec(specs)); + } + store.dispatch(specParsed()); + } + + static updateDimensions( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + store: Store, + ) { + store.dispatch(updateParentDimensions({ width, height, top, left })); + } +}