diff --git a/api/charts.api.md b/api/charts.api.md index 6eaa12c754..d9ec00faf2 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -773,6 +773,28 @@ export interface GroupBrushExtent { groupId: GroupId; } +// @alpha (undocumented) +export const GroupBy: React.FunctionComponent; + +// @alpha (undocumented) +export type GroupByAccessor = (spec: Spec, datum: any) => string | number; + +// @alpha (undocumented) +export type GroupByProps = Pick; + +// Warning: (ae-forgotten-export) The symbol "Predicate" needs to be exported by the entry point index.d.ts +// +// @alpha (undocumented) +export type GroupBySort = Predicate; + +// @alpha (undocumented) +export interface GroupBySpec extends Spec { + // (undocumented) + by: GroupByAccessor; + // (undocumented) + sort: GroupBySort; +} + // @public (undocumented) export type GroupId = string; @@ -935,8 +957,6 @@ export interface HeatmapSpec extends Spec { xAccessor: Accessor | AccessorFn; // (undocumented) xScaleType: SeriesScales['xScaleType']; - // Warning: (ae-forgotten-export) The symbol "Predicate" needs to be exported by the entry point index.d.ts - // // (undocumented) xSortPredicate: Predicate; // (undocumented) @@ -1635,6 +1655,25 @@ export interface SimplePadding { outer: number; } +// @alpha (undocumented) +export const SmallMultiples: React.FunctionComponent; + +// @alpha (undocumented) +export type SmallMultiplesProps = Partial>; + +// @alpha (undocumented) +export interface SmallMultiplesSpec extends Spec { + // (undocumented) + splitHorizontally?: string; + // (undocumented) + splitVertically?: string; + // (undocumented) + style?: { + verticalPanelPadding?: [number, number]; + horizontalPanelPadding?: [number, number]; + }; +} + // Warning: (ae-missing-release-tag) "Spec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1655,6 +1694,8 @@ export const SpecTypes: Readonly<{ Axis: "axis"; Annotation: "annotation"; Settings: "settings"; + IndexOrder: "index_order"; + SmallMultiples: "small_multiples"; }>; // @public (undocumented) @@ -1871,6 +1912,10 @@ export interface XYChartSeriesIdentifier extends SeriesIdentifier { // (undocumented) seriesKeys: (string | number)[]; // (undocumented) + smHorizontalAccessorValue?: string | number; + // (undocumented) + smVerticalAccessorValue?: string | number; + // (undocumented) splitAccessors: Map; // (undocumented) yAccessor: string | number; diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png index c32ca6545a..5994448118 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-many-tick-labels-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-many-tick-labels-visually-looks-correct-1-snap.png index 0ad4443723..0915d90137 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-many-tick-labels-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-many-tick-labels-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png index 9ad6c008b6..33411046ca 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png index e06b1fd964..085f597431 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-grid-lines-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-grid-lines-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..56d31cf0a8 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-grid-lines-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..b24d2d81ab Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-vertical-areas-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-vertical-areas-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..5bc3abb744 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-vertical-areas-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-correctly-rotated-ticks-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-correctly-rotated-ticks-1-snap.png index 4ab4dd75c8..01fb3a1683 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-correctly-rotated-ticks-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-correctly-rotated-ticks-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png index ccc0872acb..ff2ddff8c9 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png index 9ad6c008b6..33411046ca 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png index 44089e684f..13220478d6 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png index 1f0d2341c7..0e6dc0629e 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png index 78e1fc5b6e..0d0cf8c1bb 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png index 418d9ce35f..d85f5deace 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png index 6dd81cb92a..5a56fa776f 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png index 981774845e..fde82718c7 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png index 74f643fa83..7efc5689bf 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png index 29e5ad321d..bebc645842 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-render-corrent-tooltip-in-dark-theme-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-render-corrent-tooltip-in-dark-theme-1-snap.png index 85dac90e5a..3bd3a2c5a8 100644 Binary files a/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-render-corrent-tooltip-in-dark-theme-1-snap.png and b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-render-corrent-tooltip-in-dark-theme-1-snap.png differ diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts index c511134cbc..b4c0f97d67 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; import { MockStore } from '../../../../mocks/store'; import { ScaleType } from '../../../../scales/constants'; @@ -51,14 +52,15 @@ function expectAnnotationAtPosition( MockStore.addSpecs([settings, ...specs, annotation], store); const annotations = computeAnnotationDimensionsSelector(store.getState()); expect(annotations.get(annotation.id)).toEqual([ - { + MockAnnotationLineProps.default({ details: { detailsText: undefined, headerText: `${indexPosition}` }, linePathPoints: { - start: { x1: expectedLinePosition, y1: 0 }, - end: { x2: expectedLinePosition, y2: 100 }, + x1: expectedLinePosition, + y1: 0, + x2: expectedLinePosition, + y2: 100, }, - marker: undefined, - }, + }), ]); } @@ -144,14 +146,15 @@ describe('Render vertical line annotation within', () => { MockStore.addSpecs([settings, spec, annotation], store); const annotations = computeAnnotationDimensionsSelector(store.getState()); expect(annotations.get(annotation.id)).toEqual([ - { + MockAnnotationLineProps.default({ linePathPoints: { - start: { x1: 95, y1: 0 }, - end: { x2: 95, y2: 100 }, + x1: 95, + y1: 0, + x2: 95, + y2: 100, }, details: { detailsText: 'foo', headerText: '9.5' }, - marker: undefined, - }, + }), ]); }); }); diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.test.ts b/src/chart_types/xy_chart/annotations/line/dimensions.test.ts new file mode 100644 index 0000000000..0a873331c4 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/dimensions.test.ts @@ -0,0 +1,729 @@ +/* + * 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 { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; +import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; +import { ScaleType } from '../../../../scales/constants'; +import { Position } from '../../../../utils/commons'; +import { AnnotationId } from '../../../../utils/ids'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/theme'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { AnnotationDomainTypes } from '../../utils/specs'; +import { AnnotationDimensions } from '../types'; +import { AnnotationLineProps } from './types'; + +describe('Annotation utils', () => { + const groupId = 'foo-group'; + + const continuousBarChart = MockSeriesSpec.bar({ + xScaleType: ScaleType.Linear, + groupId, + data: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 3, y: 10 }, + { x: 4, y: 5 }, + { x: 9, y: 10 }, + ], + }); + + const ordinalBarChart = MockSeriesSpec.bar({ + xScaleType: ScaleType.Ordinal, + groupId, + data: [ + { x: 'a', y: 1 }, + { x: 'b', y: 0 }, + { x: 'c', y: 10 }, + { x: 'd', y: 5 }, + ], + }); + + const verticalAxisSpec = MockGlobalSpec.axis({ + id: 'vertical_axis', + groupId, + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + showGridLines: true, + }); + + test('should compute line annotation in x ordinal scale', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', + groupId, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + }); + + const rectAnnotation = MockAnnotationSpec.rect({ + id: 'rect', + groupId, + dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }], + }); + + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation, rectAnnotation], store); + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions = new Map(); + expectedDimensions.set('foo', [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]); + expectedDimensions.set('rect', [ + { + details: undefined, + rect: { x: 0, y: 50, width: 50, height: 20 }, + panel: { top: 0, left: 0, width: 100, height: 100 }, + }, + ]); + + expect(dimensions).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions also with missing axis', () => { + const store = MockStore.default({ width: 10, height: 20, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation], store); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + expect(dimensions.size).toEqual(1); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { + const panel = { width: 10, height: 100, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 10, + y2: 80, + }, + panel, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, right axis)', () => { + const store = MockStore.default({ width: 10, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Right, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 10, + y2: 80, + }, + panel: { width: 10, height: 100, top: 0, left: 0 }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 90)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 80, + x2: 100, + y2: 80, + }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should not compute line annotation dimensions for yDomain if no corresponding yScale', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(dimensions.size).toEqual(0); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, ordinal scale)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 'a', details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + details: { detailsText: 'foo', headerText: 'a' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, top axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, bottom axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 0 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 'a', details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + details: { detailsText: 'foo', headerText: 'a' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, continuous scale)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + panel, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain on a xScale (chartRotation -90, continuous scale)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: -90 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 12.5, + y1: 0, + x2: 12.5, + y2: 100, + }, + panel, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, top axis)', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Top, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 100, + }, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, bottom axis)', () => { + const panel = { width: 100, height: 50, top: 0, left: 0 }; + const store = MockStore.default(panel); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', + groupId: 'other-group', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs( + [ + settings, + continuousBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + position: Position.Bottom, + hide: true, + }), + ], + store, + ); + + const dimensions = computeAnnotationDimensionsSelector(store.getState()); + const expectedDimensions: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 25, + y1: 0, + x2: 25, + y2: 50, + }, + panel, + details: { detailsText: 'foo', headerText: '2' }, + }), + ]; + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); + }); + test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { + let store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ rotation: 180 }); + + const annotationId = 'foo-line'; + const invalidXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 'e', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + MockStore.addSpecs([settings, continuousBarChart, invalidXLineAnnotation], store); + const emptyXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyXDimensions.get('foo-line')).toHaveLength(0); + + const invalidStringXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: '', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidStringXLineAnnotation], store); + + const invalidStringXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(invalidStringXDimensions.get('foo-line')).toHaveLength(0); + + const outOfBoundsXLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: -999, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, outOfBoundsXLineAnnotation], store); + + const emptyOutOfBoundsXDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyOutOfBoundsXDimensions.get('foo-line')).toHaveLength(0); + + const invalidYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 'e', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidYLineAnnotation], store); + + const emptyOutOfBoundsYDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(emptyOutOfBoundsYDimensions.get('foo-line')).toHaveLength(0); + + const outOfBoundsYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: -999, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, outOfBoundsYLineAnnotation], store); + + const outOfBoundsYAnn = computeAnnotationDimensionsSelector(store.getState()); + + expect(outOfBoundsYAnn.get('foo-line')).toHaveLength(0); + + const invalidStringYLineAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: '', details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, invalidStringYLineAnnotation], store); + + const invalidStringYDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(invalidStringYDimensions.get('foo-line')).toHaveLength(0); + + const validHiddenAnnotation = MockAnnotationSpec.line({ + id: annotationId, + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + hideLines: true, + }); + + store = MockStore.default({ width: 100, height: 50, top: 0, left: 0 }); + MockStore.addSpecs([settings, continuousBarChart, validHiddenAnnotation], store); + + const hiddenAnnotationDimensions = computeAnnotationDimensionsSelector(store.getState()); + + expect(hiddenAnnotationDimensions.size).toBe(0); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.ts b/src/chart_types/xy_chart/annotations/line/dimensions.ts index 240628bd6a..3a17155010 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -17,16 +17,19 @@ * under the License. */ +import { Line } from '../../../../geoms/types'; import { Scale } from '../../../../scales'; import { isContinuousScale, isBandScale } from '../../../../scales/types'; -import { Position, Rotation } from '../../../../utils/commons'; -import { Dimensions } from '../../../../utils/dimensions'; +import { isNil, Position, Rotation } from '../../../../utils/commons'; +import { Dimensions, Size } from '../../../../utils/dimensions'; import { GroupId } from '../../../../utils/ids'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation } from '../../state/utils/common'; import { computeXScaleOffset } from '../../state/utils/utils'; +import { getPanelSize } from '../../utils/panel'; import { AnnotationDomainTypes, LineAnnotationSpec, LineAnnotationDatum } from '../../utils/specs'; import { AnnotationMarker } from '../types'; -import { AnnotationLineProps, AnnotationLinePathPoints } from './types'; +import { AnnotationLineProps } from './types'; /** @internal */ export const DEFAULT_LINE_OVERFLOW = 0; @@ -34,8 +37,8 @@ export const DEFAULT_LINE_OVERFLOW = 0; function computeYDomainLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, yScale: Scale, + { vertical, horizontal }: SmallMultipleScales, chartRotation: Rotation, - chartDimensions: Dimensions, lineColor: string, axisPosition?: Position, ): AnnotationLineProps[] { @@ -51,6 +54,9 @@ function computeYDomainLineAnnotationDimensions( const anchorPosition = getAnchorPosition(false, isHorizontalChartRotation, specMarkerPosition, axisPosition); const lineProps: AnnotationLineProps[] = []; + const [domainStart, domainEnd] = yScale.domain; + + const panelSize = getPanelSize({ vertical, horizontal }); dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; @@ -66,39 +72,56 @@ function computeYDomainLineAnnotationDimensions( return; } - const [domainStart, domainEnd] = yScale.domain; // avoid rendering annotation with values outside the scale domain if (dataValue < domainStart || dataValue > domainEnd) { return; } - const markerPosition = getMarkerPositionForYAnnotation( - chartDimensions, - chartRotation, - markerDimensions, - anchorPosition, - annotationValueYPosition, - ); - const linePathPoints = getYLinePath(chartDimensions, annotationValueYPosition, chartRotation); - - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: markerPosition, - } - : undefined; - const lineProp: AnnotationLineProps = { - linePathPoints, - marker: annotationMarker, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }, - }; - - lineProps.push(lineProp); + vertical.domain.forEach((verticalValue) => { + horizontal.domain.forEach((horizontalValue) => { + const topPos = vertical.scaleOrThrow(verticalValue); + const leftPos = horizontal.scaleOrThrow(horizontalValue); + + const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; + const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; + + const markerPosition = getMarkerPositionForYAnnotation( + panelSize, + chartRotation, + markerDimensions, + anchorPosition, + annotationValueYPosition, + ); + const linePathPoints = getYLinePath({ width, height }, annotationValueYPosition); + + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: { ...markerDimensions }, + position: { + top: markerPosition.top, + left: markerPosition.left, + }, + } + : undefined; + const lineProp: AnnotationLineProps = { + linePathPoints, + marker: annotationMarker, + panel: { + ...panelSize, + top: topPos, + left: leftPos, + }, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, + }; + + lineProps.push(lineProp); + }); + }); }); return lineProps; @@ -107,8 +130,8 @@ function computeYDomainLineAnnotationDimensions( function computeXDomainLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, xScale: Scale, + { vertical, horizontal }: SmallMultipleScales, chartRotation: Rotation, - chartDimensions: Dimensions, lineColor: string, isHistogramMode: boolean, axisPosition?: Position, @@ -123,11 +146,12 @@ function computeXDomainLineAnnotationDimensions( const lineProps: AnnotationLineProps[] = []; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); const anchorPosition = getAnchorPosition(true, isHorizontalChartRotation, specMarkerPosition, axisPosition); + const panelSize = getPanelSize({ vertical, horizontal }); dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; let annotationValueXPosition = xScale.scale(dataValue); - if (annotationValueXPosition == null) { + if (isNil(annotationValueXPosition)) { return; } if (isContinuousScale(xScale) && typeof dataValue === 'number') { @@ -160,32 +184,54 @@ function computeXDomainLineAnnotationDimensions( return; } - const markerPosition = getMarkerPositionForXAnnotation( - chartDimensions, - chartRotation, - markerDimensions, - anchorPosition, - annotationValueXPosition, - ); - const linePathPoints = getXLinePath(chartDimensions, annotationValueXPosition, chartRotation); - - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: markerPosition, + vertical.domain.forEach((verticalValue) => { + horizontal.domain.forEach((horizontalValue) => { + if (annotationValueXPosition == null) { + return; } - : undefined; - const lineProp: AnnotationLineProps = { - linePathPoints, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }, - marker: annotationMarker, - }; - lineProps.push(lineProp); + + const topPos = vertical.scaleOrThrow(verticalValue); + const leftPos = horizontal.scaleOrThrow(horizontalValue); + const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; + const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; + + const markerPosition = getMarkerPositionForXAnnotation( + panelSize, + chartRotation, + markerDimensions, + anchorPosition, + annotationValueXPosition, + ); + + const linePathPoints = getXLinePath({ width, height }, annotationValueXPosition); + + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: { ...markerDimensions }, + position: { + top: markerPosition.top, + left: markerPosition.left, + }, + } + : undefined; + const lineProp: AnnotationLineProps = { + linePathPoints, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, + marker: annotationMarker, + panel: { + ...panelSize, + top: topPos, + left: leftPos, + }, + }; + lineProps.push(lineProp); + }); + }); }); return lineProps; @@ -194,10 +240,10 @@ function computeXDomainLineAnnotationDimensions( /** @internal */ export function computeLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, - chartDimensions: Dimensions, chartRotation: Rotation, yScales: Map, xScale: Scale, + smallMultipleScales: SmallMultipleScales, isHistogramMode: boolean, axisPosition?: Position, ): AnnotationLineProps[] | null { @@ -215,8 +261,8 @@ export function computeLineAnnotationDimensions( return computeXDomainLineAnnotationDimensions( annotationSpec, xScale, + smallMultipleScales, chartRotation, - chartDimensions, lineColor, isHistogramMode, axisPosition, @@ -232,8 +278,8 @@ export function computeLineAnnotationDimensions( return computeYDomainLineAnnotationDimensions( annotationSpec, yScale, + smallMultipleScales, chartRotation, - chartDimensions, lineColor, axisPosition, ); @@ -276,43 +322,28 @@ function getDefaultMarkerPositionFromAxis( return Position.Bottom; } -function getXLinePath( - { width, height }: Pick, - value: number, - rotation: Rotation, -): AnnotationLinePathPoints { +function getXLinePath({ height }: Size, value: number): Line { return { - start: { - x1: value, - y1: 0, - }, - end: { - x2: value, - y2: rotation === -90 || rotation === 90 ? width : height, - }, + x1: value, + y1: 0, + x2: value, + y2: height, }; } -function getYLinePath( - { width, height }: Pick, - value: number, - rotation: Rotation, -): AnnotationLinePathPoints { + +function getYLinePath({ width }: Size, value: number): Line { return { - start: { - x1: 0, - y1: value, - }, - end: { - x2: rotation === -90 || rotation === 90 ? height : width, - y2: value, - }, + x1: 0, + y1: value, + x2: width, + y2: value, }; } -function getMarkerPositionForXAnnotation( - { width, height }: Pick, +export function getMarkerPositionForXAnnotation( + { width, height }: Size, rotation: Rotation, - { width: mWidth, height: mHeight }: Pick, + { width: mWidth, height: mHeight }: Size, position: Position, value: number, ): Pick { @@ -342,9 +373,9 @@ function getMarkerPositionForXAnnotation( } function getMarkerPositionForYAnnotation( - { width, height }: Pick, + { width, height }: Size, rotation: Rotation, - { width: mWidth, height: mHeight }: Pick, + { width: mWidth, height: mHeight }: Size, position: Position, value: number, ): { @@ -364,7 +395,7 @@ function getMarkerPositionForYAnnotation( }; case Position.Top: return { - top: 0 - mHeight, + top: -mHeight, left: rotation === 90 ? width - value - mWidth / 2 : value - mWidth / 2, }; case Position.Bottom: diff --git a/src/chart_types/xy_chart/annotations/line/line.test.tsx b/src/chart_types/xy_chart/annotations/line/line.test.tsx index 8c6459050c..f06fa1e3ec 100644 --- a/src/chart_types/xy_chart/annotations/line/line.test.tsx +++ b/src/chart_types/xy_chart/annotations/line/line.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { Store } from 'redux'; +import { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; import { MockStore } from '../../../../mocks/store'; import { ScaleType } from '../../../../scales/constants'; @@ -75,16 +76,12 @@ describe('annotation marker', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ - { + MockAnnotationLineProps.default({ linePathPoints: { - start: { - x1: 0, - y1: 80, - }, - end: { - x2: 100, - y2: 80, - }, + x1: 0, + y1: 80, + x2: 100, + y2: 80, }, details: { detailsText: 'foo', headerText: '2' }, @@ -94,7 +91,7 @@ describe('annotation marker', () => { dimension: { width: 0, height: 0 }, position: { left: -0, top: 80 }, }, - }, + }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); }); @@ -117,16 +114,12 @@ describe('annotation marker', () => { // so this position at 80 pixel right now, is a 20 pixel from top // when rotated 180 degrees const expectedDimensions: AnnotationLineProps[] = [ - { + MockAnnotationLineProps.default({ linePathPoints: { - start: { - x1: 0, - y1: 80, - }, - end: { - x2: 100, - y2: 80, - }, + x1: 0, + y1: 80, + x2: 100, + y2: 80, }, details: { detailsText: 'foo', headerText: '2' }, marker: { @@ -135,7 +128,7 @@ describe('annotation marker', () => { dimension: { width: 0, height: 0 }, position: { left: -0, top: 20 }, }, - }, + }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); }); @@ -154,17 +147,13 @@ describe('annotation marker', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ - { + MockAnnotationLineProps.default({ details: { detailsText: 'foo', headerText: '2' }, linePathPoints: { - start: { - x1: 20, - y1: 0, - }, - end: { - x2: 20, - y2: 100, - }, + x1: 20, + y1: 0, + x2: 20, + y2: 100, }, marker: { icon:
, @@ -172,7 +161,7 @@ describe('annotation marker', () => { dimension: { width: 0, height: 0 }, position: { top: 100, left: 20 }, }, - }, + }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); }); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.test.ts b/src/chart_types/xy_chart/annotations/line/tooltip.test.ts new file mode 100644 index 0000000000..6a959e510b --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/tooltip.test.ts @@ -0,0 +1,364 @@ +/* + * 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 React from 'react'; + +import { ChartTypes } from '../../..'; +import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; +import { MockGlobalSpec } from '../../../../mocks/specs/specs'; +import { SpecTypes } from '../../../../specs/constants'; +import { Position, Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationId } from '../../../../utils/ids'; +import { Point } from '../../../../utils/point'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/theme'; +import { + AnnotationDomainTypes, + AnnotationSpec, + AnnotationTypes, + AxisSpec, + LineAnnotationSpec, + RectAnnotationSpec, +} from '../../utils/specs'; +import { computeAnnotationTooltipState } from '../tooltip'; +import { AnnotationDimensions, AnnotationTooltipState } from '../types'; +import { computeLineAnnotationTooltipState } from './tooltip'; +import { AnnotationLineProps } from './types'; + +describe('Annotation tooltips', () => { + const groupId = 'foo-group'; + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + const horizontalAxisSpec = MockGlobalSpec.axis({ + groupId, + position: Position.Bottom, + }); + const verticalAxisSpec = MockGlobalSpec.axis({ + groupId, + position: Position.Left, + }); + test('should compute the tooltip state for an annotation line', () => { + const cursorPosition: Point = { x: 16, y: 7 }; + const annotationLines: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 1, + y1: 2, + x2: 3, + y2: 4, + }, + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, + }), + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 0, + y1: 10, + x2: 20, + y2: 10, + }, + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 20, height: 20 }, + position: { top: 0, left: 0 }, + }, + }), + ]; + const localAxesSpecs: AxisSpec[] = []; + // missing annotation axis (xDomain) + const missingTooltipState = computeLineAnnotationTooltipState( + cursorPosition, + annotationLines, + groupId, + AnnotationDomainTypes.XDomain, + localAxesSpecs, + chartDimensions, + ); + + expect(missingTooltipState).toBeNull(); + + // add axis for xDomain annotation + localAxesSpecs.push(horizontalAxisSpec); + + const xDomainTooltipState = computeLineAnnotationTooltipState( + cursorPosition, + annotationLines, + groupId, + AnnotationDomainTypes.XDomain, + localAxesSpecs, + chartDimensions, + ); + const expectedXDomainTooltipState = { + isVisible: true, + annotationType: AnnotationTypes.Line, + anchor: { + height: 10, + left: 15, + top: 5, + width: 10, + }, + }; + expect(xDomainTooltipState).toMatchObject(expectedXDomainTooltipState); + + // rotated xDomain + const xDomainRotatedTooltipState = computeLineAnnotationTooltipState( + { x: 24, y: 23 }, + annotationLines, + groupId, + AnnotationDomainTypes.XDomain, + localAxesSpecs, + chartDimensions, + ); + const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { + isVisible: true, + anchor: { + left: 15, + top: 5, + }, + annotationType: AnnotationTypes.Line, + }; + + expect(xDomainRotatedTooltipState).toMatchObject(expectedXDomainRotatedTooltipState); + + // add axis for yDomain annotation + localAxesSpecs.push(verticalAxisSpec); + + const yDomainTooltipState = computeLineAnnotationTooltipState( + cursorPosition, + annotationLines, + groupId, + AnnotationDomainTypes.YDomain, + localAxesSpecs, + chartDimensions, + ); + const expectedYDomainTooltipState: AnnotationTooltipState = { + isVisible: true, + anchor: { + left: 15, + top: 5, + }, + annotationType: AnnotationTypes.Line, + }; + + expect(yDomainTooltipState).toMatchObject(expectedYDomainTooltipState); + + const flippedYDomainTooltipState = computeLineAnnotationTooltipState( + { x: 24, y: 23 }, + annotationLines, + groupId, + AnnotationDomainTypes.YDomain, + localAxesSpecs, + chartDimensions, + ); + const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { + isVisible: true, + anchor: { + left: 15, + top: 5, + }, + annotationType: AnnotationTypes.Line, + }; + + expect(flippedYDomainTooltipState).toMatchObject(expectedFlippedYDomainTooltipState); + + const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( + { x: 25, y: 15 }, + annotationLines, + groupId, + AnnotationDomainTypes.YDomain, + localAxesSpecs, + chartDimensions, + ); + const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { + isVisible: true, + anchor: { + left: 15, + top: 5, + }, + annotationType: AnnotationTypes.Line, + }; + + expect(rotatedYDomainTooltipState).toMatchObject(expectedRotatedYDomainTooltipState); + }); + + test('should compute the tooltip state for an annotation', () => { + const annotations: AnnotationSpec[] = []; + const annotationId = 'foo'; + const lineAnnotation: LineAnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + id: annotationId, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + }; + + const cursorPosition: Point = { x: 16, y: 7 }; + + const annotationLines: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + linePathPoints: { + x1: 1, + y1: 2, + x2: 3, + y2: 4, + }, + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, + }), + ]; + const chartRotation: Rotation = 0; + const localAxesSpecs: AxisSpec[] = []; + + const annotationDimensions = new Map(); + annotationDimensions.set(annotationId, annotationLines); + + // missing annotations + const missingSpecTooltipState = computeAnnotationTooltipState( + cursorPosition, + annotationDimensions, + annotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(missingSpecTooltipState).toBe(null); + + // add valid annotation axis + annotations.push(lineAnnotation); + localAxesSpecs.push(verticalAxisSpec); + + // hide tooltipState + lineAnnotation.hideTooltips = true; + + const hideTooltipState = computeAnnotationTooltipState( + cursorPosition, + annotationDimensions, + annotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(hideTooltipState).toBe(null); + + // show tooltipState, hide lines + lineAnnotation.hideTooltips = false; + lineAnnotation.hideLines = true; + + const hideLinesTooltipState = computeAnnotationTooltipState( + cursorPosition, + annotationDimensions, + annotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(hideLinesTooltipState).toBe(null); + + // show tooltipState & lines + lineAnnotation.hideTooltips = false; + lineAnnotation.hideLines = false; + + const tooltipState = computeAnnotationTooltipState( + cursorPosition, + annotationDimensions, + annotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + const expectedTooltipState = { + isVisible: true, + annotationType: AnnotationTypes.Line, + anchor: { + height: 10, + left: 15, + top: 5, + width: 10, + }, + }; + + expect(tooltipState).toMatchObject(expectedTooltipState); + + // rect annotation tooltip + const annotationRectangle: RectAnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + id: 'rect', + groupId, + annotationType: AnnotationTypes.Rectangle, + dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], + }; + + const rectAnnotations: RectAnnotationSpec[] = []; + rectAnnotations.push(annotationRectangle); + + annotationDimensions.set(annotationRectangle.id, [ + MockAnnotationRectProps.default({ rect: { x: 2, y: 3, width: 3, height: 5 } }), + ]); + + const rectTooltipState = computeAnnotationTooltipState( + { x: 18, y: 9 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectTooltipState).toMatchObject({ + isVisible: true, + annotationType: AnnotationTypes.Rectangle, + anchor: { + left: 18, + top: 9, + }, + }); + annotationRectangle.hideTooltips = true; + + const rectHideTooltipState = computeAnnotationTooltipState( + { x: 3, y: 4 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectHideTooltipState).toBe(null); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.ts b/src/chart_types/xy_chart/annotations/line/tooltip.ts index 5f35741472..d4112a8119 100644 --- a/src/chart_types/xy_chart/annotations/line/tooltip.ts +++ b/src/chart_types/xy_chart/annotations/line/tooltip.ts @@ -43,13 +43,13 @@ export function computeLineAnnotationTooltipState( if (!annotationAxis) { return null; } - + // get cursor point relative to the rendering area (within the chartDimension margins) const projectedPointer = getTransformedCursor(cursorPosition, chartDimensions, null, true); const totalAnnotationLines = annotationLines.length; for (let i = 0; i < totalAnnotationLines; i++) { const line = annotationLines[i]; - if (isWithinLineMarkerBounds(projectedPointer, line.marker)) { + if (isWithinLineMarkerBounds(projectedPointer, line.panel, line.marker)) { const position = invertTranformedCursor( { x: line.marker.position.left, @@ -63,8 +63,8 @@ export function computeLineAnnotationTooltipState( annotationType: AnnotationTypes.Line, isVisible: true, anchor: { - top: position.y, - left: position.x, + top: position.y + line.panel.top, + left: position.x + line.panel.left, ...line.marker.dimension, }, ...(line.details && { header: line.details.headerText }), @@ -79,15 +79,26 @@ export function computeLineAnnotationTooltipState( /** * Checks if the cursorPosition is within the line annotation marker * @param cursorPosition the cursor position relative to the projected area + * @param panel * @param marker the line annotation marker */ -function isWithinLineMarkerBounds(cursorPosition: Point, marker?: AnnotationMarker): marker is AnnotationMarker { +function isWithinLineMarkerBounds( + cursorPosition: Point, + panel: Dimensions, + marker?: AnnotationMarker, +): marker is AnnotationMarker { if (!marker) { return false; } - - const { top, left } = marker.position; - const { width, height } = marker.dimension; - const markerRect: Bounds = { startX: left, startY: top, endX: left + width, endY: top + height }; + const { + position: { top, left }, + dimension: { width, height }, + } = marker; + const markerRect: Bounds = { + startX: left + panel.left, + startY: top + panel.top, + endX: left + panel.left + width, + endY: top + panel.top + height, + }; return isWithinRectBounds(cursorPosition, markerRect); } diff --git a/src/chart_types/xy_chart/annotations/line/types.ts b/src/chart_types/xy_chart/annotations/line/types.ts index 0885f41f17..5ad78b9cfb 100644 --- a/src/chart_types/xy_chart/annotations/line/types.ts +++ b/src/chart_types/xy_chart/annotations/line/types.ts @@ -17,31 +17,17 @@ * under the License. */ +import { Line } from '../../../../geoms/types'; +import { Dimensions } from '../../../../utils/dimensions'; import { AnnotationDetails, AnnotationMarker } from '../types'; -/** - * Start and end points of a line annotation - * @internal - */ -export interface AnnotationLinePathPoints { - /** x1,y1 the start point anchored to the linked axis */ - start: { - x1: number; - y1: number; - }; - /** x2,y2 the end point */ - end: { - x2: number; - y2: number; - }; -} - /** @internal */ export interface AnnotationLineProps { /** * The path points of a line annotation */ - linePathPoints: AnnotationLinePathPoints; + linePathPoints: Line; details: AnnotationDetails; marker?: AnnotationMarker; + panel: Dimensions; } diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts new file mode 100644 index 0000000000..1eb90d7d9f --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { ScaleType } from '../../../../scales/constants'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { isWithinRectBounds } from './dimensions'; +import { AnnotationRectProps } from './types'; + +describe('Rect Annotation Dimensions', () => { + const continuousBarChart = MockSeriesSpec.area({ + xScaleType: ScaleType.Linear, + data: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 3, y: 10 }, + { x: 4, y: 5 }, + { x: 10, y: 10 }, + ], + }); + + test('should skip computing rectangle annotation dimensions when annotation data invalid', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const annotationRectangle = MockAnnotationSpec.rect({ + id: 'rect', + dataValues: [ + { coordinates: { x0: 1, x1: 2, y0: -10, y1: 5 } }, + { coordinates: { x0: null, x1: null, y0: null, y1: null } }, + ], + }); + + MockStore.addSpecs([settings, continuousBarChart, annotationRectangle], store); + const skippedInvalid = computeAnnotationDimensionsSelector(store.getState()); + expect(skippedInvalid.size).toBe(1); + }); + + test('should compute rectangle dimensions shifted for histogram mode', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + + const annotationRectangle = MockAnnotationSpec.rect({ + id: 'rect', + dataValues: [ + { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, + { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, + { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, + { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, + ], + }); + + MockStore.addSpecs([settings, continuousBarChart, annotationRectangle], store); + const [dims1, dims2, dims3, dims4] = computeAnnotationDimensionsSelector(store.getState()).get( + 'rect', + ) as AnnotationRectProps[]; + + expect(dims1.rect.x).toBe(10); + expect(dims1.rect.width).toBeCloseTo(90); + expect(dims1.rect.y).toBe(0); + expect(dims1.rect.height).toBe(100); + + expect(dims2.rect.x).toBe(0); + expect(dims2.rect.width).toBe(10); + expect(dims2.rect.y).toBe(0); + expect(dims2.rect.height).toBe(100); + + expect(dims3.rect.x).toBe(0); + expect(dims3.rect.width).toBe(100); + expect(dims3.rect.y).toBe(0); + expect(dims3.rect.height).toBe(90); + + expect(dims4.rect.x).toBe(0); + expect(dims4.rect.width).toBeCloseTo(100); + expect(dims4.rect.y).toBe(90); + expect(dims4.rect.height).toBe(10); + }); + + test('should determine if a point is within a rectangle annotation', () => { + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 3, endY: 5 })).toBe(true); + // TODO check I've a doubt that this should be an error + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 5, endY: 3 })).toBe(false); + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 2, endX: 4, startY: 5, endY: 6 })).toBe(false); + + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 4, endX: 5, startY: 3, endY: 5 })).toBe(false); + expect(isWithinRectBounds({ x: 3, y: 4 }, { startX: 4, endX: 2, startY: 3, endY: 5 })).toBe(false); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.ts index 783eca1512..f5ff20195e 100644 --- a/src/chart_types/xy_chart/annotations/rect/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -20,10 +20,11 @@ import { Scale, ScaleBand, ScaleContinuous } from '../../../../scales'; import { isBandScale, isContinuousScale } from '../../../../scales/types'; import { isDefined } from '../../../../utils/commons'; -import { Dimensions } from '../../../../utils/dimensions'; import { GroupId } from '../../../../utils/ids'; import { Point } from '../../../../utils/point'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; +import { getPanelSize } from '../../utils/panel'; import { RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs'; import { Bounds } from '../types'; import { AnnotationRectProps } from './types'; @@ -39,16 +40,17 @@ export function isWithinRectBounds({ x, y }: Point, { startX, endX, startY, endY /** @internal */ export function computeRectAnnotationDimensions( annotationSpec: RectAnnotationSpec, - chartDimensions: Dimensions, yScales: Map, xScale: Scale, + smallMultiplesScales: SmallMultipleScales, isHistogram: boolean = false, ): AnnotationRectProps[] | null { const { dataValues } = annotationSpec; const { groupId } = annotationSpec; const yScale = yScales.get(groupId); - const rectsProps: AnnotationRectProps[] = []; + const rectsProps: Omit[] = []; + const panelSize = getPanelSize(smallMultiplesScales); dataValues.forEach((dataValue: RectAnnotationDatum) => { const { x0: initialX0, x1: initialX1, y0: initialY0, y1: initialY1 } = dataValue.coordinates; @@ -80,7 +82,7 @@ export function computeRectAnnotationDimensions( const rectDimensions = { ...xAndWidth, y: 0, - height: chartDimensions.height, + height: panelSize.height, }; rectsProps.push({ @@ -106,7 +108,7 @@ export function computeRectAnnotationDimensions( // if the annotation height is 0 override it with the height from chart dimension and if the values in the domain are the same if (height === 0 && yScale.domain.length === 2 && yScale.domain[0] === yScale.domain[1]) { // eslint-disable-next-line prefer-destructuring - height = chartDimensions.height; + height = panelSize.height; scaledY1 = 0; } @@ -122,7 +124,20 @@ export function computeRectAnnotationDimensions( }); }); - return rectsProps; + return rectsProps.reduce((acc, props) => { + const duplicated: AnnotationRectProps[] = []; + smallMultiplesScales.vertical.domain.forEach((vDomainValue) => { + smallMultiplesScales.horizontal.domain.forEach((hDomainValue) => { + const panel = { + ...panelSize, + top: smallMultiplesScales.vertical.scaleOrThrow(vDomainValue), + left: smallMultiplesScales.horizontal.scaleOrThrow(hDomainValue), + }; + duplicated.push({ ...props, panel }); + }); + }); + return [...acc, ...duplicated]; + }, []); } function scaleXonBandScale( diff --git a/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts b/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts new file mode 100644 index 0000000000..f4012ea97b --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationTypes } from '../../utils/specs'; +import { AnnotationTooltipState } from '../types'; +import { computeRectAnnotationTooltipState } from './tooltip'; + +describe('Rect annotation tooltip', () => { + test('should compute tooltip state for rect annotation', () => { + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + const cursorPosition = { x: 18, y: 9 }; + const annotationRects = [ + { rect: { x: 2, y: 3, width: 3, height: 5 }, panel: { top: 0, left: 0, width: 10, height: 20 } }, + ]; + + const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); + const expectedVisibleTooltipState: AnnotationTooltipState = { + isVisible: true, + annotationType: AnnotationTypes.Rectangle, + anchor: { + top: cursorPosition.y, + left: cursorPosition.x, + }, + }; + + expect(visibleTooltip).toEqual(expectedVisibleTooltipState); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/rect/tooltip.ts b/src/chart_types/xy_chart/annotations/rect/tooltip.ts index 4a978f0634..cb8d3defbe 100644 --- a/src/chart_types/xy_chart/annotations/rect/tooltip.ts +++ b/src/chart_types/xy_chart/annotations/rect/tooltip.ts @@ -17,12 +17,13 @@ * under the License. */ +import { Rect } from '../../../../geoms/types'; import { Rotation } from '../../../../utils/commons'; import { Dimensions } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; +import { isHorizontalRotation } from '../../state/utils/common'; import { AnnotationTypes } from '../../utils/specs'; import { AnnotationTooltipState, Bounds } from '../types'; -import { getTransformedCursor } from '../utils'; import { isWithinRectBounds } from './dimensions'; import { AnnotationRectProps } from './types'; @@ -30,20 +31,23 @@ import { AnnotationRectProps } from './types'; export function computeRectAnnotationTooltipState( cursorPosition: Point, annotationRects: AnnotationRectProps[], - chartRotation: Rotation, + rotation: Rotation, chartDimensions: Dimensions, ): AnnotationTooltipState | null { - const rotatedProjectedCursorPosition = getTransformedCursor(cursorPosition, chartDimensions, chartRotation, true); const totalAnnotationRect = annotationRects.length; + for (let i = 0; i < totalAnnotationRect; i++) { const rectProps = annotationRects[i]; - const { rect, details } = rectProps; - const startX = rect.x; + const { details, panel } = rectProps; + + const rect = transformRotateRect(rectProps.rect, rotation, panel); + + const startX = rect.x + chartDimensions.left + panel.left; const endX = startX + rect.width; - const startY = rect.y; + const startY = rect.y + chartDimensions.top + panel.top; const endY = startY + rect.height; const bounds: Bounds = { startX, endX, startY, endY }; - const isWithinBounds = isWithinRectBounds(rotatedProjectedCursorPosition, bounds); + const isWithinBounds = isWithinRectBounds(cursorPosition, bounds); if (isWithinBounds) { return { isVisible: true, @@ -59,3 +63,36 @@ export function computeRectAnnotationTooltipState( return null; } + +function transformRotateRect(rect: Rect, rotation: Rotation, dim: Dimensions): Rect { + const isHorizontalRotated = isHorizontalRotation(rotation); + const width = isHorizontalRotated ? dim.width : dim.height; + const height = isHorizontalRotated ? dim.height : dim.width; + + switch (rotation) { + case 90: + return { + x: height - rect.height - rect.y, + y: rect.x, + width: rect.height, + height: rect.width, + }; + case -90: + return { + x: rect.y, + y: width - rect.x - rect.width, + width: rect.height, + height: rect.width, + }; + case 180: + return { + x: width - rect.x - rect.width, + y: height - rect.y - rect.height, + width: rect.width, + height: rect.height, + }; + case 0: + default: + return rect; + } +} diff --git a/src/chart_types/xy_chart/annotations/rect/types.ts b/src/chart_types/xy_chart/annotations/rect/types.ts index de9638df57..0102654f6a 100644 --- a/src/chart_types/xy_chart/annotations/rect/types.ts +++ b/src/chart_types/xy_chart/annotations/rect/types.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { Dimensions } from '../../../../utils/dimensions'; + export interface AnnotationRectProps { rect: { x: number; @@ -23,5 +25,6 @@ export interface AnnotationRectProps { width: number; height: number; }; + panel: Dimensions; details?: string; } diff --git a/src/chart_types/xy_chart/annotations/tooltip.ts b/src/chart_types/xy_chart/annotations/tooltip.ts index 87f04c3b1e..c5e9c32edb 100644 --- a/src/chart_types/xy_chart/annotations/tooltip.ts +++ b/src/chart_types/xy_chart/annotations/tooltip.ts @@ -39,12 +39,13 @@ export function computeAnnotationTooltipState( chartDimensions: Dimensions, ): AnnotationTooltipState | null { // allow picking up the last spec added as the top most or use it's zIndex value - const sortedSpecs = annotationSpecs + const sortedAnnotationSpecs = annotationSpecs .slice() .reverse() .sort(({ zIndex: a = Number.MIN_SAFE_INTEGER }, { zIndex: b = Number.MIN_SAFE_INTEGER }) => b - a); - // eslint-disable-next-line no-restricted-syntax - for (const spec of sortedSpecs) { + + for (let i = 0; i < sortedAnnotationSpecs.length; i++) { + const spec = sortedAnnotationSpecs[i]; const annotationDimension = annotationDimensions.get(spec.id); if (spec.hideTooltips || !annotationDimension) { continue; diff --git a/src/chart_types/xy_chart/annotations/utils.test.ts b/src/chart_types/xy_chart/annotations/utils.test.ts index aedaf5814c..007292c29f 100644 --- a/src/chart_types/xy_chart/annotations/utils.test.ts +++ b/src/chart_types/xy_chart/annotations/utils.test.ts @@ -17,1124 +17,31 @@ * under the License. */ -import { RecursivePartial } from '@elastic/eui'; -import React from 'react'; - -import { ChartTypes } from '../..'; -import { MockGlobalSpec, MockSeriesSpec, MockAnnotationSpec } from '../../../mocks/specs'; -import { MockStore } from '../../../mocks/store'; -import { Scale, ScaleBand, ScaleContinuous } from '../../../scales'; -import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; +import { MockGlobalSpec } from '../../../mocks/specs'; import { Position, Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; -import { GroupId, AnnotationId } from '../../../utils/ids'; -import { Point } from '../../../utils/point'; -import { DEFAULT_ANNOTATION_LINE_STYLE, AxisStyle } from '../../../utils/themes/theme'; -import { computeAnnotationDimensionsSelector } from '../state/selectors/compute_annotations'; -import { - AnnotationDomainTypes, - AnnotationSpec, - AxisSpec, - LineAnnotationSpec, - RectAnnotationSpec, - AnnotationTypes, -} from '../utils/specs'; -import { computeLineAnnotationDimensions } from './line/dimensions'; -import { computeLineAnnotationTooltipState } from './line/tooltip'; -import { AnnotationLineProps } from './line/types'; -import { computeRectAnnotationDimensions, isWithinRectBounds } from './rect/dimensions'; -import { computeRectAnnotationTooltipState } from './rect/tooltip'; -import { computeAnnotationTooltipState } from './tooltip'; -import { AnnotationDimensions, AnnotationTooltipState, Bounds } from './types'; -import { computeAnnotationDimensions, getAnnotationAxis, getTransformedCursor, invertTranformedCursor } from './utils'; - -describe('annotation utils', () => { - const minRange = 0; - const maxRange = 100; - - const continuousData = [0, 10]; - const continuousScale = new ScaleContinuous( - { - type: ScaleType.Linear, - domain: continuousData, - range: [minRange, maxRange], - }, - { bandwidth: 10, minInterval: 1 }, - ); - - const ordinalData = ['a', 'b', 'c', 'd', 'a', 'b', 'c']; - const ordinalScale = new ScaleBand(ordinalData, [minRange, maxRange]); - - const chartDimensions: Dimensions = { - width: 10, - height: 20, - top: 5, - left: 15, - }; +import { AnnotationDomainTypes } from '../utils/specs'; +import { getAnnotationAxis, getTransformedCursor, invertTranformedCursor } from './utils'; +describe('Annotation utils', () => { const groupId = 'foo-group'; - const style: RecursivePartial = { - tickLine: { - size: 10, - padding: 10, - }, - }; - const axesSpecs: AxisSpec[] = []; - const verticalAxisSpec: AxisSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Axis, + const verticalAxisSpec = MockGlobalSpec.axis({ id: 'vertical_axis', groupId, - hide: false, - showOverlappingTicks: false, - showOverlappingLabels: false, position: Position.Left, - style, - tickFormat: (value: any) => value.toString(), - showGridLines: true, - }; - const horizontalAxisSpec: AxisSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Axis, - id: 'horizontal_axis', + }); + const horizontalAxisSpec = MockGlobalSpec.axis({ + id: 'vertical_axis', groupId, - hide: false, - showOverlappingTicks: false, - showOverlappingLabels: false, position: Position.Bottom, - style, - tickFormat: (value: any) => value.toString(), - showGridLines: true, - }; - - axesSpecs.push(verticalAxisSpec); - - test('should compute rect annotation in x ordinal scale', () => { - const store = MockStore.default(); - const settings = MockGlobalSpec.settingsNoMargins(); - const spec = MockSeriesSpec.bar({ - xScaleType: ScaleType.Ordinal, - groupId, - data: [ - { x: 'a', y: 1 }, - { x: 'b', y: 0 }, - { x: 'c', y: 10 }, - { x: 'd', y: 5 }, - ], - }); - - const lineAnnotation = MockAnnotationSpec.line({ - id: 'foo', - groupId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - }); - - const rectAnnotation = MockAnnotationSpec.rect({ - id: 'rect', - groupId, - dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }], - }); - - MockStore.addSpecs([settings, spec, lineAnnotation, rectAnnotation], store); - const dimensions = computeAnnotationDimensionsSelector(store.getState()); - - const expectedDimensions = new Map(); - expectedDimensions.set('foo', [ - { - linePathPoints: { - start: { x1: 0, y1: 80 }, - end: { x2: 100, y2: 80 }, - }, - marker: undefined, - details: { detailsText: 'foo', headerText: '2' }, - }, - ]); - expectedDimensions.set('rect', [{ details: undefined, rect: { x: 0, y: 50, width: 50, height: 20 } }]); - - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute annotation dimensions also with missing axis', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotations: AnnotationSpec[] = []; - const id = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - annotations.push(lineAnnotation); - - const dimensions = computeAnnotationDimensions( - annotations, - chartDimensions, - chartRotation, - yScales, - xScale, - [], // empty axesSpecs - false, - ); - expect(dimensions.size).toEqual(1); - }); - - test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const id = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 10, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, right axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 10, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 90)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 20, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should not compute line annotation dimensions for yDomain if no corresponding yScale', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - expect(dimensions).toEqual(null); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, ordinal scale)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'a', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 12.5, y1: 0 }, - end: { x2: 12.5, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: 'a' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, top axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Top, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, bottom axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Bottom, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'a', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 12.5, y1: 0 }, - end: { x2: 12.5, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: 'a' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, continuous scale)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation -90, continuous scale)', () => { - const chartRotation: Rotation = -90; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, top axis)', () => { - const chartRotation: Rotation = 180; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Top, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, bottom axis)', () => { - const chartRotation: Rotation = 180; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Bottom, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - details: { detailsText: 'foo', headerText: '2' }, - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - 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); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const invalidXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'e', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyXDimensions = computeLineAnnotationDimensions( - invalidXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(emptyXDimensions).toEqual([]); - - const invalidStringXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: '', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const invalidStringXDimensions = computeLineAnnotationDimensions( - invalidStringXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(invalidStringXDimensions).toEqual([]); - - const outOfBoundsXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: -999, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyOutOfBoundsXDimensions = computeLineAnnotationDimensions( - outOfBoundsXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - expect(emptyOutOfBoundsXDimensions).toHaveLength(0); - - const invalidYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 'e', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( - invalidYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(emptyOutOfBoundsYDimensions).toHaveLength(0); - - const outOfBoundsYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: -999, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const outOfBoundsYAnn = computeLineAnnotationDimensions( - outOfBoundsYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(outOfBoundsYAnn).toHaveLength(0); - - const invalidStringYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: '', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const invalidStringYDimensions = computeLineAnnotationDimensions( - invalidStringYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(invalidStringYDimensions).toEqual([]); - - const validHiddenAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - hideLines: true, - }; - - const hiddenAnnotationDimensions = computeLineAnnotationDimensions( - validHiddenAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(hiddenAnnotationDimensions).toEqual(null); - }); - - test('should compute the tooltip state for an annotation line', () => { - const cursorPosition: Point = { x: 16, y: 7 }; - const annotationLines: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 1, y1: 2 }, - end: { x2: 3, y2: 4 }, - }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }, - { - linePathPoints: { - start: { x1: 0, y1: 10 }, - end: { x2: 20, y2: 10 }, - }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 20, height: 20 }, - position: { top: 0, left: 0 }, - }, - }, - ]; - - const localAxesSpecs: AxisSpec[] = []; - - // missing annotation axis (xDomain) - const missingTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - - expect(missingTooltipState).toBeNull(); - - // add axis for xDomain annotation - localAxesSpecs.push(horizontalAxisSpec); - - const xDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - expect(xDomainTooltipState).toMatchObject(expectedXDomainTooltipState); - - // rotated xDomain - const xDomainRotatedTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(xDomainRotatedTooltipState).toMatchObject(expectedXDomainRotatedTooltipState); - - // add axis for yDomain annotation - localAxesSpecs.push(verticalAxisSpec); - - const yDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(yDomainTooltipState).toMatchObject(expectedYDomainTooltipState); - - const flippedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(flippedYDomainTooltipState).toMatchObject(expectedFlippedYDomainTooltipState); - - const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 25, y: 15 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(rotatedYDomainTooltipState).toMatchObject(expectedRotatedYDomainTooltipState); - }); - - test('should compute the tooltip state for an annotation', () => { - const annotations: AnnotationSpec[] = []; - const annotationId = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const cursorPosition: Point = { x: 16, y: 7 }; - - const annotationLines: AnnotationLineProps[] = [ - { - linePathPoints: { start: { x1: 1, y1: 2 }, end: { x2: 3, y2: 4 } }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }, - ]; - const chartRotation: Rotation = 0; - const localAxesSpecs: AxisSpec[] = []; - - const annotationDimensions = new Map(); - annotationDimensions.set(annotationId, annotationLines); - - // missing annotations - const missingSpecTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(missingSpecTooltipState).toBe(null); - - // add valid annotation axis - annotations.push(lineAnnotation); - localAxesSpecs.push(verticalAxisSpec); - - // hide tooltipState - lineAnnotation.hideTooltips = true; - - const hideTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideTooltipState).toBe(null); - - // show tooltipState, hide lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = true; - - const hideLinesTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideLinesTooltipState).toBe(null); - - // show tooltipState & lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = false; - - const tooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - const expectedTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - - expect(tooltipState).toMatchObject(expectedTooltipState); - - // rect annotation tooltip - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const rectAnnotations: RectAnnotationSpec[] = []; - rectAnnotations.push(annotationRectangle); - - const rectAnnotationDimensions = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; - annotationDimensions.set(annotationRectangle.id, rectAnnotationDimensions); - - const rectTooltipState = computeAnnotationTooltipState( - { x: 18, y: 9 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectTooltipState).toMatchObject({ - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - left: 18, - top: 9, - }, - }); - annotationRectangle.hideTooltips = true; - - const rectHideTooltipState = computeAnnotationTooltipState( - { x: 3, y: 4 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectHideTooltipState).toBe(null); }); test('should get associated axis for an annotation', () => { - const localAxesSpecs: AxisSpec[] = []; - - const noAxis = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); + const noAxis = getAnnotationAxis([], groupId, AnnotationDomainTypes.XDomain, 0); expect(noAxis).toBeUndefined(); - localAxesSpecs.push(horizontalAxisSpec); - localAxesSpecs.push(verticalAxisSpec); + const localAxesSpecs = [horizontalAxisSpec, verticalAxisSpec]; const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); expect(xAnnotationAxisPosition).toEqual(Position.Bottom); @@ -1142,161 +49,15 @@ describe('annotation utils', () => { const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.YDomain, 0); expect(yAnnotationAxisPosition).toEqual(Position.Left); }); - test('should not compute rectangle annotation dimensions when no yScale', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId: 'foo', - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const noYScale = computeRectAnnotationDimensions(annotationRectangle, chartDimensions, yScales, xScale); - - expect(noYScale).toEqual([]); - }); - test('should skip computing rectangle annotation dimensions when annotation data invalid', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [ - { coordinates: { x0: 1, x1: 2, y0: -10, y1: 5 } }, - { coordinates: { x0: null, x1: null, y0: null, y1: null } }, - ], - }; - - const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, chartDimensions, yScales, xScale); - - expect(skippedInvalid).toHaveLength(1); - }); - test('should compute rectangle dimensions shifted for histogram mode', () => { - const yScales: Map = new Map(); - yScales.set( - groupId, - new ScaleContinuous( - { - type: ScaleType.Linear, - domain: continuousData, - range: [minRange, maxRange], - }, - { bandwidth: 0, minInterval: 1 }, - ), - ); - - const xScale: Scale = new ScaleContinuous( - { type: ScaleType.Linear, domain: continuousData, range: [minRange, maxRange] }, - { bandwidth: 72, minInterval: 1 }, - ); - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [ - { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, - { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, - { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, - { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, - ], - }; - - const dimensions = computeRectAnnotationDimensions(annotationRectangle, chartDimensions, yScales, xScale); - - const [dims1, dims2, dims3, dims4] = dimensions; - expect(dims1.rect.x).toBe(10); - expect(dims1.rect.y).toBe(100); - expect(dims1.rect.height).toBe(100); - expect(dims1.rect.width).toBeCloseTo(100); - - expect(dims2.rect.x).toBe(0); - expect(dims2.rect.y).toBe(100); - expect(dims2.rect.width).toBe(20); - expect(dims2.rect.height).toBe(100); - - expect(dims3.rect.x).toBe(0); - expect(dims3.rect.y).toBe(100); - expect(dims3.rect.width).toBeCloseTo(110); - expect(dims3.rect.height).toBe(90); - - expect(dims4.rect.x).toBe(0); - expect(dims4.rect.y).toBe(10); - expect(dims4.rect.width).toBeCloseTo(110); - expect(dims4.rect.height).toBe(10); - }); - - test('should determine if a point is within a rectangle annotation', () => { - const cursorPosition = { x: 3, y: 4 }; - - const outOfXBounds: Bounds = { startX: 4, endX: 5, startY: 3, endY: 5 }; - const outOfYBounds: Bounds = { startX: 2, endX: 4, startY: 5, endY: 6 }; - const withinBounds: Bounds = { startX: 2, endX: 4, startY: 3, endY: 5 }; - const withinBoundsReverseXScale: Bounds = { startX: 4, endX: 2, startY: 3, endY: 5 }; - const withinBoundsReverseYScale: Bounds = { startX: 2, endX: 4, startY: 5, endY: 3 }; - - // chart rotation 0 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation 180 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation 90 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation -90 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - }); - test('should compute tooltip state for rect annotation', () => { - const cursorPosition = { x: 18, y: 9 }; - const annotationRects = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; - - const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); - const expectedVisibleTooltipState: AnnotationTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - top: cursorPosition.y, - left: cursorPosition.x, - }, - }; - - expect(visibleTooltip).toEqual(expectedVisibleTooltipState); - }); test('should get rotated cursor position', () => { const cursorPosition = { x: 1, y: 2 }; - + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; expect(getTransformedCursor(cursorPosition, chartDimensions, 0)).toEqual(cursorPosition); expect(getTransformedCursor(cursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 9 }); expect(getTransformedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); @@ -1305,7 +66,12 @@ describe('annotation utils', () => { describe('#invertTranformedCursor', () => { const cursorPosition = { x: 1, y: 2 }; - + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; it.each([0, 90, -90, 180])('Should invert rotated cursor - rotation %d', (rotation) => { expect( invertTranformedCursor( diff --git a/src/chart_types/xy_chart/annotations/utils.ts b/src/chart_types/xy_chart/annotations/utils.ts index a495eee154..26d0396aa9 100644 --- a/src/chart_types/xy_chart/annotations/utils.ts +++ b/src/chart_types/xy_chart/annotations/utils.ts @@ -22,6 +22,7 @@ import { Rotation, Position } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { AnnotationId, GroupId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation } from '../state/utils/common'; import { getAxesSpecForSpecId } from '../state/utils/spec'; import { @@ -30,7 +31,6 @@ import { AnnotationSpec, AxisSpec, isLineAnnotation, - isRectAnnotation, } from '../utils/specs'; import { computeLineAnnotationDimensions } from './line/dimensions'; import { computeRectAnnotationDimensions } from './rect/dimensions'; @@ -139,42 +139,41 @@ export function computeAnnotationDimensions( xScale: Scale, axesSpecs: AxisSpec[], isHistogramModeEnabled: boolean, + smallMultipleScales: SmallMultipleScales, ): Map { - const annotationDimensions = new Map(); - - annotations.forEach((annotationSpec) => { + return annotations.reduce>((annotationDimensions, annotationSpec) => { const { id } = annotationSpec; + if (isLineAnnotation(annotationSpec)) { const { groupId, domainType } = annotationSpec; const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); const dimensions = computeLineAnnotationDimensions( annotationSpec, - chartDimensions, chartRotation, yScales, xScale, + smallMultipleScales, isHistogramModeEnabled, annotationAxisPosition, ); - - if (dimensions) { - annotationDimensions.set(id, dimensions); - } - } else if (isRectAnnotation(annotationSpec)) { - const dimensions = computeRectAnnotationDimensions( - annotationSpec, - chartDimensions, - yScales, - xScale, - isHistogramModeEnabled, - ); - if (dimensions) { annotationDimensions.set(id, dimensions); } + return annotationDimensions; } - }); - return annotationDimensions; + const dimensions = computeRectAnnotationDimensions( + annotationSpec, + yScales, + xScale, + smallMultipleScales, + isHistogramModeEnabled, + ); + + if (dimensions) { + annotationDimensions.set(id, dimensions); + } + return annotationDimensions; + }, new Map()); } diff --git a/src/chart_types/xy_chart/axes/axes_sizes.ts b/src/chart_types/xy_chart/axes/axes_sizes.ts new file mode 100644 index 0000000000..7953359a07 --- /dev/null +++ b/src/chart_types/xy_chart/axes/axes_sizes.ts @@ -0,0 +1,112 @@ +/* + * 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 { Position } from '../../../utils/commons'; +import { getSimplePadding } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { AxisStyle, Theme } from '../../../utils/themes/theme'; +import { getSpecsById } from '../state/utils/spec'; +import { AxisTicksDimensions, shouldShowTicks } from '../utils/axis_utils'; +import { AxisSpec } from '../utils/specs'; + +/** + * Compute the axes required size around the chart + * @param chartTheme the theme style of the chart + * @param axisDimensions the axis dimensions + * @param axesStyles a map with all the custom axis styles + * @param axisSpecs the axis specs + * @internal + */ +export function computeAxesSizes( + { axes: sharedAxesStyles, chartMargins }: Theme, + axisDimensions: Map, + axesStyles: Map, + axisSpecs: AxisSpec[], +): { left: number; right: number; top: number; bottom: number; margin: { left: number } } { + const axisMainSize = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + const axisLabelOverflow = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + + axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0, isHidden }, id) => { + const axisSpec = getSpecsById(axisSpecs, id); + if (!axisSpec || isHidden) { + return; + } + const { tickLine, axisTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const { position, title } = axisSpec; + const titlePadding = getSimplePadding(axisTitle.padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const titleHeight = + title !== undefined && axisTitle.visible ? axisTitle.fontSize + titlePadding.outer + titlePadding.inner : 0; + const axisDimension = labelPaddingSum + tickDimension + titleHeight; + const maxAxisHeight = tickLabel.visible ? maxLabelBboxHeight + axisDimension : axisDimension; + const maxAxisWidth = tickLabel.visible ? maxLabelBboxWidth + axisDimension : axisDimension; + + switch (position) { + case Position.Top: + axisMainSize.top += maxAxisHeight + chartMargins.top; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Bottom: + axisMainSize.bottom += maxAxisHeight + chartMargins.bottom; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Right: + axisMainSize.right += maxAxisWidth + chartMargins.right; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + break; + case Position.Left: + default: + axisMainSize.left += maxAxisWidth + chartMargins.left; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + } + }); + const left = Math.max(axisLabelOverflow.left + chartMargins.left, axisMainSize.left); + return { + margin: { + left: left - axisMainSize.left, + }, + left, + right: Math.max(axisLabelOverflow.right + chartMargins.right, axisMainSize.right), + top: Math.max(axisLabelOverflow.top + chartMargins.top, axisMainSize.top), + bottom: Math.max(axisLabelOverflow.bottom + chartMargins.bottom, axisMainSize.bottom), + }; +} diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts index 4e5651b351..75aac8b180 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts @@ -25,11 +25,6 @@ import { Point } from '../../../utils/point'; import { isHorizontalRotation, isVerticalRotation } from '../state/utils/common'; import { ChartDimensions } from '../utils/dimensions'; -export interface SnappedPosition { - position: number; - band: number; -} - export const DEFAULT_SNAP_POSITION_BAND = 1; /** @internal */ @@ -71,7 +66,7 @@ export function getCursorLinePosition( const { left, top, width, height } = chartDimensions; const isHorizontalRotated = isHorizontalRotation(chartRotation); if (isHorizontalRotated) { - const crosshairTop = projectedPointerPosition.y + top; + const crosshairTop = y + top; return { left, width, @@ -79,7 +74,7 @@ export function getCursorLinePosition( height: 0, }; } - const crosshairLeft = projectedPointerPosition.x + left; + const crosshairLeft = x + left; return { top, @@ -92,7 +87,7 @@ export function getCursorLinePosition( /** @internal */ export function getCursorBandPosition( chartRotation: Rotation, - chartDimensions: Dimensions, + panel: Dimensions, cursorPosition: Point, invertedValue: { value: any; @@ -102,7 +97,7 @@ export function getCursorBandPosition( xScale: Scale, totalBarsInCluster?: number, ): Dimensions & { visible: boolean } { - const { top, left, width, height } = chartDimensions; + const { top, left, width, height } = panel; const { x, y } = cursorPosition; const isHorizontalRotated = isHorizontalRotation(chartRotation); const chartWidth = isHorizontalRotated ? width : height; @@ -169,26 +164,15 @@ export function getCursorBandPosition( /** @internal */ export function getTooltipAnchorPosition( - { chartDimensions, offset }: ChartDimensions, + { offset }: ChartDimensions, chartRotation: Rotation, cursorBandPosition: Dimensions, cursorPosition: { x: number; y: number }, + panel: Dimensions, ): TooltipAnchorPosition { const isRotated = isVerticalRotation(chartRotation); - const hPosition = getHorizontalTooltipPosition( - cursorPosition.x, - cursorBandPosition, - chartDimensions, - offset.left, - isRotated, - ); - const vPosition = getVerticalTooltipPosition( - cursorPosition.y, - cursorBandPosition, - chartDimensions, - offset.top, - isRotated, - ); + const hPosition = getHorizontalTooltipPosition(cursorPosition.x, cursorBandPosition, panel, offset.left, isRotated); + const vPosition = getVerticalTooltipPosition(cursorPosition.y, cursorBandPosition, panel, offset.top, isRotated); return { isRotated, ...vPosition, @@ -199,7 +183,7 @@ export function getTooltipAnchorPosition( function getHorizontalTooltipPosition( cursorXPosition: number, cursorBandPosition: Dimensions, - chartDimensions: Dimensions, + panel: Dimensions, globalOffset: number, isRotated: boolean, ): { x0?: number; x1: number } { @@ -211,15 +195,15 @@ function getHorizontalTooltipPosition( } return { // NOTE: x0 set to zero blocks tooltip placement on left when rotated 90 deg - // Delete this comment before merging and verifing this doesn't break anything. - x1: chartDimensions.left + cursorXPosition + globalOffset, + // Delete this comment before merging and verifying this doesn't break anything. + x1: panel.left + cursorXPosition + globalOffset, }; } function getVerticalTooltipPosition( cursorYPosition: number, cursorBandPosition: Dimensions, - chartDimensions: Dimensions, + panel: Dimensions, globalOffset: number, isRotated: boolean, ): { @@ -227,7 +211,7 @@ function getVerticalTooltipPosition( y1: number; } { if (!isRotated) { - const y = cursorYPosition + chartDimensions.top + globalOffset; + const y = cursorYPosition + panel.top + globalOffset; return { y0: y, y1: y, diff --git a/src/chart_types/xy_chart/domains/x_domain.test.ts b/src/chart_types/xy_chart/domains/x_domain.test.ts index e89e4db67d..6372a3589d 100644 --- a/src/chart_types/xy_chart/domains/x_domain.test.ts +++ b/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -21,7 +21,7 @@ import { ChartTypes } from '../..'; import { MockSeriesSpecs } from '../../../mocks/specs'; import { ScaleType } from '../../../scales/constants'; import { SpecTypes, Direction, BinAgg } from '../../../specs/constants'; -import { getDataSeriesBySpecId } from '../utils/series'; +import { getDataSeriesFromSpecs } from '../utils/series'; import { BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { convertXScaleTypes, findMinInterval, mergeXDomain } from './x_domain'; @@ -222,7 +222,7 @@ describe('X Domain', () => { ], }; const specDataSeries: BasicSeriesSpec[] = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -269,7 +269,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -316,7 +316,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -367,7 +367,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -418,7 +418,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -475,7 +475,7 @@ describe('X Domain', () => { min: 0, }; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const getResult = () => mergeXDomain( [ @@ -535,7 +535,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -586,7 +586,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -637,7 +637,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -682,7 +682,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ @@ -891,12 +891,12 @@ describe('X Domain', () => { ]); it('should sort ordinal xValues by descending sum by default', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], {}); + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], {}); expect(xValues).toEqual(new Set(['c', 'd', 'b', 'a'])); }); it('should sort ordinal xValues by descending sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Descending, }); @@ -904,7 +904,7 @@ describe('X Domain', () => { }); it('should sort ordinal xValues by ascending sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Ascending, }); @@ -912,12 +912,12 @@ describe('X Domain', () => { }); it('should NOT sort ordinal xValues sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], undefined); + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], undefined); expect(xValues).toEqual(new Set(['a', 'b', 'c', 'd'])); }); it('should NOT sort ordinal xValues sum when undefined', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Descending, }); @@ -925,7 +925,7 @@ describe('X Domain', () => { }); it('should NOT sort linear xValue by descending sum', () => { - const { xValues } = getDataSeriesBySpecId(linearSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(linearSpecs, [], { direction: Direction.Descending, }); expect(xValues).toEqual(new Set([1, 2, 3, 4])); diff --git a/src/chart_types/xy_chart/domains/x_domain.ts b/src/chart_types/xy_chart/domains/x_domain.ts index 55f07d745f..1a3a054930 100644 --- a/src/chart_types/xy_chart/domains/x_domain.ts +++ b/src/chart_types/xy_chart/domains/x_domain.ts @@ -30,6 +30,7 @@ import { XDomain } from './types'; * @param specs an array of [{ seriesType, xScaleType }] * @param xValues a set of unique x values from all specs * @param customXDomain if specified, a custom xDomain + * @param fallbackScale * @returns a merged XDomain between all series. * @internal */ diff --git a/src/chart_types/xy_chart/domains/y_domain.test.ts b/src/chart_types/xy_chart/domains/y_domain.test.ts index 7402c714ff..cea56ed189 100644 --- a/src/chart_types/xy_chart/domains/y_domain.test.ts +++ b/src/chart_types/xy_chart/domains/y_domain.test.ts @@ -26,7 +26,7 @@ import { Position } from '../../../utils/commons'; import { BARCHART_1Y0G } from '../../../utils/data_samples/test_dataset'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; import { BasicSeriesSpec, SeriesTypes, DEFAULT_GLOBAL_ID, StackMode } from '../utils/specs'; -import { coerceYScaleTypes, splitSpecsByGroupId } from './y_domain'; +import { coerceYScaleTypes, groupSeriesByYGroup } from './y_domain'; const DEMO_AREA_SPEC_1 = { id: 'a', @@ -243,7 +243,7 @@ describe('Y Domain', () => { yAccessors: ['y'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); @@ -280,7 +280,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); @@ -317,7 +317,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group']); @@ -365,7 +365,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2, spec3]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2, spec3]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index 265edd8fac..54bc28e499 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -25,7 +25,8 @@ import { computeContinuousDataDomain } from '../../../utils/domain'; import { GroupId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; -import { DataSeries, FormattedDataSeries } from '../utils/series'; +import { groupBy } from '../utils/group_data_series'; +import { DataSeries } from '../utils/series'; import { BasicSeriesSpec, YDomainRange, DEFAULT_GLOBAL_ID, SeriesTypes, StackMode } from '../utils/specs'; import { YDomain } from './types'; @@ -34,84 +35,48 @@ export type YBasicSeriesSpec = Pick< 'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'useDefaultGroupDomain' > & { stackMode?: StackMode; enableHistogramMode?: boolean }; -interface GroupSpecs { - stackMode?: StackMode; - stacked: YBasicSeriesSpec[]; - nonStacked: YBasicSeriesSpec[]; -} - /** @internal */ -export function mergeYDomain( - { - stacked, - nonStacked, - }: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, - specs: YBasicSeriesSpec[], - domainsByGroupId: Map, -): YDomain[] { - // group specs by group ids - const specsByGroupIds = splitSpecsByGroupId(specs); - const specsByGroupIdsEntries = [...specsByGroupIds.entries()]; - const globalId = DEFAULT_GLOBAL_ID; - - const yDomains = specsByGroupIdsEntries.map(([groupId, groupSpecs]) => { - const customDomain = domainsByGroupId.get(groupId); - const emptyDS: FormattedDataSeries = { - dataSeries: [], - groupId, - counts: { area: 0, bubble: 0, bar: 0, line: 0 }, - }; - const stackedDS = stacked.find((d) => d.groupId === groupId) ?? emptyDS; - const nonStackedDS = nonStacked.find((d) => d.groupId === groupId) ?? emptyDS; - const nonZeroBaselineSpecs = - stackedDS.counts.bar + stackedDS.counts.area + nonStackedDS.counts.bar + nonStackedDS.counts.area; - return mergeYDomainForGroup( - stackedDS.dataSeries, - nonStackedDS.dataSeries, - groupId, - groupSpecs, - nonZeroBaselineSpecs > 0, - customDomain, - ); - }); +export function mergeYDomain(dataSeries: DataSeries[], domainsByGroupId: Map): YDomain[] { + const dataSeriesByGroupId = groupBy( + dataSeries, + ({ spec: { useDefaultGroupDomain, groupId } }) => { + return useDefaultGroupDomain ? DEFAULT_GLOBAL_ID : groupId; + }, + true, + ); - const globalGroupIds: Set = specs.reduce>((acc, { groupId, useDefaultGroupDomain }) => { - if (groupId !== globalId && useDefaultGroupDomain) { - acc.add(groupId); - } - return acc; - }, new Set()); - globalGroupIds.add(globalId); + return dataSeriesByGroupId.reduce((acc, groupedDataSeries) => { + const [{ groupId }] = groupedDataSeries; - const globalYDomains = yDomains.filter((domain) => globalGroupIds.has(domain.groupId)); - let globalYDomain = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]; - globalYDomains.forEach((domain) => { - globalYDomain = [Math.min(globalYDomain[0], domain.domain[0]), Math.max(globalYDomain[1], domain.domain[1])]; - }); - return yDomains.map((domain) => { - if (globalGroupIds.has(domain.groupId)) { - return { - ...domain, - domain: globalYDomain, - }; + const stacked = groupedDataSeries.filter(({ isStacked }) => isStacked); + const nonStacked = groupedDataSeries.filter(({ isStacked }) => !isStacked); + const customDomain = domainsByGroupId.get(groupId); + const hasNonZeroBaselineTypes = groupedDataSeries.some( + ({ seriesType }) => seriesType === SeriesTypes.Bar || seriesType === SeriesTypes.Area, + ); + const domain = mergeYDomainForGroup(stacked, nonStacked, hasNonZeroBaselineTypes, customDomain); + if (!domain) { + return acc; } - return domain; - }); + return [...acc, domain]; + }, []); } function mergeYDomainForGroup( stacked: DataSeries[], nonStacked: DataSeries[], - groupId: GroupId, - groupSpecs: GroupSpecs, hasZeroBaselineSpecs: boolean, customDomain?: YDomainRange, -): YDomain { - const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); - const { stackMode } = groupSpecs; +): YDomain | null { + const dataSeries = [...stacked, ...nonStacked]; + if (dataSeries.length === 0) { + return null; + } + const yScaleTypes = dataSeries.map(({ spec: { yScaleType } }) => ({ + yScaleType, + })); + const groupYScaleType = coerceYScaleTypes(yScaleTypes); + const [{ stackMode, groupId }] = dataSeries; let domain: number[]; if (stackMode === StackMode.Percentage) { @@ -119,13 +84,10 @@ function mergeYDomainForGroup( } else { // TODO remove when removing yScaleToDataExtent const newCustomDomain = customDomain ? { ...customDomain } : {}; - const shouldScaleToExtent = - groupSpecs.stacked.some(({ yScaleToDataExtent }) => yScaleToDataExtent) || - groupSpecs.nonStacked.some(({ yScaleToDataExtent }) => yScaleToDataExtent); + const shouldScaleToExtent = dataSeries.some(({ spec: { yScaleToDataExtent } }) => yScaleToDataExtent); if (customDomain?.fit !== true && shouldScaleToExtent) { newCustomDomain.fit = true; } - // compute stacked domain const stackedDomain = computeYDomain(stacked, hasZeroBaselineSpecs); @@ -163,15 +125,16 @@ function mergeYDomainForGroup( }; } -function computeYDomain(dataseries: DataSeries[], hasZeroBaselineSpecs: boolean) { +function computeYDomain(dataSeries: DataSeries[], hasZeroBaselineSpecs: boolean) { const yValues = new Set(); - dataseries.forEach((ds) => { - ds.data.forEach((datum) => { + dataSeries.forEach(({ data }) => { + for (let i = 0; i < data.length; i++) { + const datum = data[i]; yValues.add(datum.y1); if (hasZeroBaselineSpecs && datum.y0 != null) { yValues.add(datum.y0); } - }); + } }); if (yValues.size === 0) { return []; @@ -180,17 +143,13 @@ function computeYDomain(dataseries: DataSeries[], hasZeroBaselineSpecs: boolean) } /** @internal */ -export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { +export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) { const specsByGroupIds = new Map< GroupId, { stackMode: StackMode | undefined; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } >(); - // After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount - // in MobX version, the stackAccessors was programmatically added to every histogram specs - // in ReduX version, we left untouched the specs, so we have to manually check that - const isHistogramEnabled = specs.some( - ({ seriesType, enableHistogramMode }) => seriesType === SeriesTypes.Bar && enableHistogramMode, - ); + + const histogramEnabled = isHistogramEnabled(specs); // split each specs by groupId and by stacked or not specs.forEach((spec) => { const group = specsByGroupIds.get(spec.groupId) || { @@ -198,12 +157,8 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { stacked: [], nonStacked: [], }; - // stack every bars if using histogram mode - // independenyly from lines and areas - if ( - (spec.seriesType === SeriesTypes.Bar && isHistogramEnabled) || - (spec.stackAccessors && spec.stackAccessors.length > 0) - ) { + + if (isStackedSpec(spec, histogramEnabled)) { group.stacked.push(spec); } else { group.nonStacked.push(spec); @@ -220,6 +175,53 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { return specsByGroupIds; } +/** + * Histogram mode is forced on every specs if at least one specs has that prop flagged + * @remarks + * After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount + * in MobX version, the stackAccessors was programmatically added to every histogram specs + * in ReduX version, we left untouched the specs, so we have to manually check that + * @param specs + * @internal + */ +export function isHistogramEnabled(specs: YBasicSeriesSpec[]) { + return specs.some(({ seriesType, enableHistogramMode }) => seriesType === SeriesTypes.Bar && enableHistogramMode); +} + +/** + * Return true if the passed spec needs to be rendered as stack + * @param spec + * @param histogramEnabled + * @internal + */ +export function isStackedSpec(spec: YBasicSeriesSpec, histogramEnabled: boolean) { + const isBarAndHistogram = spec.seriesType === SeriesTypes.Bar && histogramEnabled; + const hasStackAccessors = spec.stackAccessors && spec.stackAccessors.length > 0; + return isBarAndHistogram || hasStackAccessors; +} + +/** + * Get the stack mode for every groupId + * @param specs + * @internal + */ +export function getStackModeForYGroup(specs: YBasicSeriesSpec[]) { + return specs.reduce>((acc, { groupId, stackMode }) => { + if (!acc[groupId]) { + acc[groupId] = undefined; + } + + if (acc[groupId] === undefined && stackMode !== undefined) { + acc[groupId] = stackMode; + } + if (stackMode !== undefined && acc[groupId] !== stackMode) { + Logger.warn(`Is not possible to mix different stackModes, please align all stackMode on the same GroupId + to the same mode. The default behaviour will be to use the first encountered stackMode on the series`); + } + return acc; + }, {}); +} + /** * Coerce the scale types of a set of specification to a generic one. * If there is at least one bar series type, than the response will specity @@ -228,13 +230,13 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { * If there are multiple continuous scale types, is coerced to linear. * If there are at least one Ordinal scale type, is coerced to ordinal. * If none of the above, than coerce to the specified scale. - * @returns {ChartScaleType} + * @returns {ScaleContinuousType} * @internal */ -export function coerceYScaleTypes(specs: Pick[]): ScaleContinuousType { +export function coerceYScaleTypes(scales: { yScaleType: ScaleContinuousType }[]): ScaleContinuousType { const scaleTypes = new Set(); - specs.forEach((spec) => { - scaleTypes.add(spec.yScaleType); + scales.forEach(({ yScaleType }) => { + scaleTypes.add(yScaleType); }); return coerceYScale(scaleTypes); } @@ -247,3 +249,13 @@ function coerceYScale(scaleTypes: Set): ScaleContinuousType } return ScaleType.Linear; } + +export function getYScaleTypeByGroupId(specs: BasicSeriesSpec[]): Map { + const groups = groupBy(specs, ['groupId'], true); + return groups.reduce((acc, group) => { + const scaleType = coerceYScaleTypes(group); + const [{ groupId }] = group; + acc.set(groupId, scaleType); + return acc; + }, new Map()); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts index 1c8beea5fc..8f8f27684f 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Rotation } from '../../../../../utils/commons'; +import { Dimensions } from '../../../../../utils/dimensions'; import { AnnotationId } from '../../../../../utils/ids'; import { mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect } from '../../../../../utils/themes/theme'; import { AnnotationLineProps } from '../../../annotations/line/types'; @@ -30,16 +32,16 @@ import { renderRectAnnotations } from './rect'; interface AnnotationProps { annotationDimensions: Map; annotationSpecs: AnnotationSpec[]; + rotation: Rotation; + renderingArea: Dimensions; } /** @internal */ export function renderAnnotations( ctx: CanvasRenderingContext2D, - props: AnnotationProps, + { annotationDimensions, annotationSpecs, rotation, renderingArea }: AnnotationProps, renderOnBackground: boolean = true, ) { - const { annotationDimensions, annotationSpecs } = props; - annotationDimensions.forEach((annotation, id) => { const spec = getSpecsById(annotationSpecs, id); if (!spec) { @@ -49,10 +51,10 @@ export function renderAnnotations( if ((isBackground && renderOnBackground) || (!isBackground && !renderOnBackground)) { if (isLineAnnotation(spec)) { const lineStyle = mergeWithDefaultAnnotationLine(spec.style); - renderLineAnnotations(ctx, annotation as AnnotationLineProps[], lineStyle); + renderLineAnnotations(ctx, annotation as AnnotationLineProps[], lineStyle, rotation, renderingArea); } else if (isRectAnnotation(spec)) { const rectStyle = mergeWithDefaultAnnotationRect(spec.style); - renderRectAnnotations(ctx, annotation as AnnotationRectProps[], rectStyle); + renderRectAnnotations(ctx, annotation as AnnotationRectProps[], rectStyle, rotation, renderingArea); } } }); diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts index e9bbab59c0..abefd0e690 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -17,30 +17,23 @@ * under the License. */ -import { Stroke, Line } from '../../../../../geoms/types'; +import { Stroke } from '../../../../../geoms/types'; +import { Rotation } from '../../../../../utils/commons'; +import { Dimensions } from '../../../../../utils/dimensions'; import { LineAnnotationStyle } from '../../../../../utils/themes/theme'; import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { AnnotationLineProps } from '../../../annotations/line/types'; -import { renderMultiLine } from '../primitives/line'; +import { renderLine } from '../primitives/line'; +import { withPanelTransform } from '../utils/panel_transform'; /** @internal */ export function renderLineAnnotations( ctx: CanvasRenderingContext2D, annotations: AnnotationLineProps[], lineStyle: LineAnnotationStyle, + rotation: Rotation, + renderingArea: Dimensions, ) { - const lines = annotations.map((annotation) => { - const { - start: { x1, y1 }, - end: { x2, y2 }, - } = annotation.linePathPoints; - return { - x1, - y1, - x2, - y2, - }; - }); const strokeColor = stringToRGB(lineStyle.line.stroke); strokeColor.opacity *= lineStyle.line.opacity; const stroke: Stroke = { @@ -49,5 +42,9 @@ export function renderLineAnnotations( dash: lineStyle.line.dash, }; - renderMultiLine(ctx, lines, stroke); + annotations.forEach(({ linePathPoints, panel }) => { + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderLine(ctx, linePathPoints, stroke); + }); + }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts index bb5c7e22a6..5c9704f8da 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts @@ -17,20 +17,23 @@ * under the License. */ -import { Rect, Fill, Stroke } from '../../../../../geoms/types'; -import { withContext } from '../../../../../renderers/canvas'; +import { Fill, Stroke } from '../../../../../geoms/types'; +import { Rotation } from '../../../../../utils/commons'; +import { Dimensions } from '../../../../../utils/dimensions'; import { RectAnnotationStyle } from '../../../../../utils/themes/theme'; import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { AnnotationRectProps } from '../../../annotations/rect/types'; import { renderRect } from '../primitives/rect'; +import { withPanelTransform } from '../utils/panel_transform'; /** @internal */ export function renderRectAnnotations( ctx: CanvasRenderingContext2D, annotations: AnnotationRectProps[], rectStyle: RectAnnotationStyle, + rotation: Rotation, + renderingArea: Dimensions, ) { - const rects = annotations.map(({ rect }) => rect); const fillColor = stringToRGB(rectStyle.fill); fillColor.opacity *= rectStyle.opacity; const fill: Fill = { @@ -43,11 +46,11 @@ export function renderRectAnnotations( width: rectStyle.strokeWidth, }; - const rectsLength = rects.length; + const rectsLength = annotations.length; for (let i = 0; i < rectsLength; i++) { - const rect = rects[i]; - withContext(ctx, (ctx) => { + const { rect, panel } = annotations[i]; + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { renderRect(ctx, rect, fill, stroke); }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/areas.ts b/src/chart_types/xy_chart/renderer/canvas/areas.ts index 8710b84161..91e24a7f2c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/areas.ts +++ b/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -19,64 +19,77 @@ import { LegendItem } from '../../../../commons/legend'; import { Rect } from '../../../../geoms/types'; -import { withClip, withContext } from '../../../../renderers/canvas'; -import { AreaGeometry } from '../../../../utils/geometry'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AreaGeometry, PerPanel } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/rendering'; import { renderPoints } from './points'; import { renderLinePaths, renderAreaPath } from './primitives/path'; import { buildAreaStyles } from './styles/area'; import { buildLineStyles } from './styles/line'; +import { withPanelTransform } from './utils/panel_transform'; interface AreaGeometriesProps { - areas: AreaGeometry[]; + areas: Array>; sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; + rotation: Rotation; + renderingArea: Dimensions; + highlightedLegendItem?: LegendItem; clippings: Rect; } /** @internal */ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometriesProps) { - withContext(ctx, (ctx) => { - const { sharedStyle, highlightedLegendItem, areas, clippings } = props; - - withClip(ctx, clippings, (ctx: CanvasRenderingContext2D) => { - ctx.save(); + const { sharedStyle, highlightedLegendItem, areas, clippings, rotation, renderingArea } = props; - // eslint-disable-next-line no-restricted-syntax - for (const glyph of areas) { - const { seriesAreaLineStyle, seriesAreaStyle } = glyph; - if (seriesAreaStyle.visible) { - withContext(ctx, () => { - renderArea(ctx, glyph, sharedStyle, highlightedLegendItem, clippings); - }); - } - if (seriesAreaLineStyle.visible) { - withContext(ctx, () => { - renderAreaLines(ctx, glyph, sharedStyle, highlightedLegendItem, clippings); - }); - } + withContext(ctx, (ctx) => { + areas.forEach(({ panel, value: area }) => { + const { seriesAreaLineStyle, seriesAreaStyle } = area; + if (seriesAreaStyle.visible) { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderArea(ctx, area, sharedStyle, clippings, highlightedLegendItem); + }, + { area: clippings, shouldClip: true }, + ); } - ctx.rect(clippings.x, clippings.y, clippings.width, clippings.height); - ctx.clip(); - ctx.restore(); - }); - - areas.forEach((area) => { - const { seriesPointStyle, seriesIdentifier } = area; - if (seriesPointStyle.visible) { - const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); - withClip( + if (seriesAreaLineStyle.visible) { + withPanelTransform( ctx, - clippings, + panel, + rotation, + renderingArea, (ctx) => { - renderPoints(ctx, area.points, seriesPointStyle, geometryStateStyle); + renderAreaLines(ctx, area, sharedStyle, clippings, highlightedLegendItem); }, - // TODO: add padding over clipping - area.points[0]?.value.mark !== null, + { area: clippings, shouldClip: true }, ); } }); + + areas.forEach(({ panel, value: area }) => { + const { seriesPointStyle, seriesIdentifier } = area; + if (!seriesPointStyle.visible) { + return; + } + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderPoints(ctx, area.points, seriesPointStyle, geometryStateStyle); + }, + { area: clippings, shouldClip: area.points[0]?.value.mark !== null }, + ); + }); }); } @@ -84,24 +97,24 @@ function renderArea( ctx: CanvasRenderingContext2D, glyph: AreaGeometry, sharedStyle: SharedGeometryStateStyle, - highlightedLegendItem: LegendItem | null, clippings: Rect, + highlightedLegendItem?: LegendItem, ) { const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges, hideClippedRanges } = glyph; - const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); const fill = buildAreaStyles(color, seriesAreaStyle, geometryStateStyle); - renderAreaPath(ctx, transform.x, area, fill, clippedRanges, clippings, hideClippedRanges); + renderAreaPath(ctx, transform, area, fill, clippedRanges, clippings, hideClippedRanges); } function renderAreaLines( ctx: CanvasRenderingContext2D, glyph: AreaGeometry, sharedStyle: SharedGeometryStateStyle, - highlightedLegendItem: LegendItem | null, clippings: Rect, + highlightedLegendItem?: LegendItem, ) { const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges, hideClippedRanges } = glyph; - const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); const stroke = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle); - renderLinePaths(ctx, transform.x, lines, stroke, clippedRanges, clippings, hideClippedRanges); + renderLinePaths(ctx, transform, lines, stroke, clippedRanges, clippings, hideClippedRanges); } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts index f73a6f1648..35b1cbd34f 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -18,10 +18,12 @@ */ import { withContext } from '../../../../../renderers/canvas'; -import { Dimensions } from '../../../../../utils/dimensions'; -import { AxisId } from '../../../../../utils/ids'; +import { Dimensions, Size } from '../../../../../utils/dimensions'; +import { Point } from '../../../../../utils/point'; import { AxisStyle } from '../../../../../utils/themes/theme'; +import { PerPanelAxisGeoms } from '../../../state/selectors/compute_per_panel_axes_geoms'; import { getSpecsById } from '../../../state/utils/spec'; +import { isVerticalAxis } from '../../../utils/axis_type_utils'; import { AxisTick, AxisTicksDimensions, shouldShowTicks } from '../../../utils/axis_utils'; import { AxisSpec } from '../../../utils/specs'; import { renderDebugRect } from '../utils/debug'; @@ -32,73 +34,80 @@ import { renderTitle } from './title'; /** @internal */ export interface AxisProps { + title?: string; + panelAnchor: Point; axisStyle: AxisStyle; axisSpec: AxisSpec; - axisTicksDimensions: AxisTicksDimensions; - axisPosition: Dimensions; + size: Size; + anchorPoint: Point; + dimension: AxisTicksDimensions; ticks: AxisTick[]; debug: boolean; - chartDimensions: Dimensions; + renderingArea: Dimensions; } /** @internal */ export interface AxesProps { - axesVisibleTicks: Map; axesSpecs: AxisSpec[]; - axesTicksDimensions: Map; - axesPositions: Map; + perPanelAxisGeoms: PerPanelAxisGeoms[]; axesStyles: Map; sharedAxesStyle: AxisStyle; debug: boolean; - chartDimensions: Dimensions; + renderingArea: Dimensions; } /** @internal */ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { - const { - axesVisibleTicks, - axesSpecs, - axesTicksDimensions, - axesPositions, - axesStyles, - sharedAxesStyle, - debug, - chartDimensions, - } = props; - axesVisibleTicks.forEach((ticks, axisId) => { - const axisSpec = getSpecsById(axesSpecs, axisId); - const axisTicksDimensions = axesTicksDimensions.get(axisId); - const axisPosition = axesPositions.get(axisId); - - if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition || axisSpec.hide) { - return; - } + const { axesSpecs, perPanelAxisGeoms, axesStyles, sharedAxesStyle, debug, renderingArea } = props; + perPanelAxisGeoms.forEach(({ axesGeoms, panelAnchor }) => { + withContext(ctx, (ctx) => { + axesGeoms.forEach((geometry) => { + const { + axis: { title, id, position }, + anchorPoint, + size, + dimension, + visibleTicks: ticks, + } = geometry; + const axisSpec = getSpecsById(axesSpecs, id); - const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; + if (!axisSpec || !dimension || !position || axisSpec.hide) { + return; + } - renderAxis(ctx, { - axisSpec, - axisTicksDimensions, - axisPosition, - ticks, - axisStyle, - debug, - chartDimensions, + const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; + renderAxis(ctx, { + title, + panelAnchor, + axisSpec, + anchorPoint, + size, + dimension, + ticks, + axisStyle, + debug, + renderingArea, + }); + }); }); }); } function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { withContext(ctx, (ctx) => { - const { ticks, axisPosition, debug, axisStyle, axisSpec } = props; + const { ticks, size, anchorPoint, debug, axisStyle, axisSpec, panelAnchor } = props; const showTicks = shouldShowTicks(axisStyle.tickLine, axisSpec.hide); - ctx.translate(axisPosition.left, axisPosition.top); + const isVertical = isVerticalAxis(axisSpec.position); + const translate = { + y: isVertical ? anchorPoint.y + panelAnchor.y : anchorPoint.y, + x: isVertical ? anchorPoint.x : anchorPoint.x + panelAnchor.x, + }; + ctx.translate(translate.x, translate.y); if (debug) { renderDebugRect(ctx, { x: 0, y: 0, - width: axisPosition.width, - height: axisPosition.height, + ...size, }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts index d0b09fc069..a0d4109c2b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -24,7 +24,7 @@ import { isVerticalAxis } from '../../../utils/axis_type_utils'; /** @internal */ export function renderLine( ctx: CanvasRenderingContext2D, - { axisSpec: { position }, axisPosition, axisStyle: { axisLine } }: AxisProps, + { axisSpec: { position }, size, axisStyle: { axisLine } }: AxisProps, ) { if (!axisLine.visible) { return; @@ -32,15 +32,15 @@ export function renderLine( const lineProps: number[] = []; if (isVerticalAxis(position)) { - lineProps[0] = position === Position.Left ? axisPosition.width : 0; - lineProps[2] = position === Position.Left ? axisPosition.width : 0; + lineProps[0] = position === Position.Left ? size.width : 0; + lineProps[2] = position === Position.Left ? size.width : 0; lineProps[1] = 0; - lineProps[3] = axisPosition.height; + lineProps[3] = size.height; } else { lineProps[0] = 0; - lineProps[2] = axisPosition.width; - lineProps[1] = position === Position.Top ? axisPosition.height : 0; - lineProps[3] = position === Position.Top ? axisPosition.height : 0; + lineProps[2] = size.width; + lineProps[1] = position === Position.Top ? size.height : 0; + lineProps[3] = position === Position.Top ? size.height : 0; } ctx.beginPath(); ctx.moveTo(lineProps[0], lineProps[1]); diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts index 4ef06739ab..e26832a53b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -29,13 +29,13 @@ import { renderLine } from '../primitives/line'; export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { const { axisSpec: { position }, - axisPosition, + size, axisStyle: { tickLine }, } = props; if (isVerticalAxis(position)) { - renderVerticalTick(ctx, position, axisPosition.width, tickLine.size, tick.position, tickLine); + renderVerticalTick(ctx, position, size.width, tickLine.size, tick.position, tickLine); } else { - renderHorizontalTick(ctx, position, axisPosition.height, tickLine.size, tick.position, tickLine); + renderHorizontalTick(ctx, position, size.height, tickLine.size, tick.position, tickLine); } } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts index c4b646e61d..b855ac8418 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -28,8 +28,8 @@ import { renderDebugRectCenterRotated } from '../utils/debug'; export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, showTicks: boolean, props: AxisProps) { const { axisSpec: { position, labelFormat }, - axisTicksDimensions, - axisPosition, + dimension: axisTicksDimensions, + size, debug, axisStyle, } = props; @@ -41,7 +41,7 @@ export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, s axisStyle, tick.position, position, - axisPosition, + size, axisTicksDimensions, showTicks, offset, diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts index 8cf282a903..c000b0df5c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -43,11 +43,12 @@ export function renderTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { - axisPosition: { height }, - axisSpec: { title, position, hide: hideAxis }, - axisTicksDimensions: { maxLabelBboxWidth }, + size: { height }, + axisSpec: { position, hide: hideAxis }, + dimension: { maxLabelBboxWidth }, axisStyle: { axisTitle, tickLine, tickLabel }, debug, + title, } = props; if (!title) { return null; @@ -84,11 +85,12 @@ function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { } function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { - axisPosition: { width }, - axisSpec: { title, position, hide: hideAxis }, - axisTicksDimensions: { maxLabelBboxHeight }, + size: { width }, + axisSpec: { position, hide: hideAxis }, + dimension: { maxLabelBboxHeight }, axisStyle: { axisTitle, tickLine, tickLabel }, debug, + title, } = props; if (!title) { diff --git a/src/chart_types/xy_chart/renderer/canvas/bars.ts b/src/chart_types/xy_chart/renderer/canvas/bars.ts index 8cc18dbb3c..750b668422 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -19,35 +19,58 @@ import { LegendItem } from '../../../../commons/legend'; import { Rect } from '../../../../geoms/types'; -import { withContext, withClip } from '../../../../renderers/canvas'; -import { BarGeometry } from '../../../../utils/geometry'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { BarGeometry, PerPanel } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/rendering'; import { renderRect } from './primitives/rect'; import { buildBarStyles } from './styles/bar'; +import { withPanelTransform } from './utils/panel_transform'; /** @internal */ export function renderBars( ctx: CanvasRenderingContext2D, - barGeometries: BarGeometry[], + barGeometries: Array>, sharedStyle: SharedGeometryStateStyle, clippings: Rect, + renderingArea: Dimensions, highlightedLegendItem?: LegendItem, + rotation?: Rotation, ) { withContext(ctx, (ctx) => { - withClip(ctx, clippings, (ctx: CanvasRenderingContext2D) => { - // ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y - barGeometries.forEach((barGeometry) => { - const { x, y, width, height, color, seriesStyle } = barGeometry; - const geometryStateStyle = getGeometryStateStyle( - barGeometry.seriesIdentifier, - highlightedLegendItem || null, - sharedStyle, - ); - const { fill, stroke } = buildBarStyles(color, seriesStyle.rect, seriesStyle.rectBorder, geometryStateStyle); - const rect = { x, y, width, height }; - renderRect(ctx, rect, fill, stroke); - }); - }); + const barRenderer = renderPerPanelBars(ctx, clippings, sharedStyle, renderingArea, highlightedLegendItem, rotation); + barGeometries.forEach(barRenderer); }); } + +function renderPerPanelBars( + ctx: CanvasRenderingContext2D, + clippings: Rect, + sharedStyle: SharedGeometryStateStyle, + renderingArea: Dimensions, + highlightedLegendItem?: LegendItem, + rotation: Rotation = 0, +) { + return ({ panel, value: bars }: PerPanel) => { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + bars.forEach((barGeometry) => { + const { x, y, width, height, color, seriesStyle, seriesIdentifier } = barGeometry; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); + const { fill, stroke } = buildBarStyles(color, seriesStyle.rect, seriesStyle.rectBorder, geometryStateStyle); + const rect = { x, y, width, height }; + withContext(ctx, (ctx) => { + renderRect(ctx, rect, fill, stroke); + }); + }); + }, + { area: clippings, shouldClip: true }, + ); + }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/bubbles.ts b/src/chart_types/xy_chart/renderer/canvas/bubbles.ts index ae8b6de34f..fe3ea4d0c8 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bubbles.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bubbles.ts @@ -20,42 +20,54 @@ import { LegendItem } from '../../../../commons/legend'; import { SeriesKey } from '../../../../commons/series_id'; import { Rect } from '../../../../geoms/types'; -import { withContext, withClip } from '../../../../renderers/canvas'; -import { BubbleGeometry, PointGeometry } from '../../../../utils/geometry'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { BubbleGeometry, PerPanel, PointGeometry } from '../../../../utils/geometry'; import { SharedGeometryStateStyle, GeometryStateStyle, PointStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/rendering'; import { renderPointGroup } from './points'; interface BubbleGeometriesDataProps { animated?: boolean; - bubbles: BubbleGeometry[]; + bubbles: Array>; sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; + highlightedLegendItem?: LegendItem; clippings: Rect; + rotation: Rotation; + renderingArea: Dimensions; } /** @internal */ export function renderBubbles(ctx: CanvasRenderingContext2D, props: BubbleGeometriesDataProps) { withContext(ctx, (ctx) => { - const { bubbles, sharedStyle, highlightedLegendItem, clippings } = props; + const { bubbles, sharedStyle, highlightedLegendItem, clippings, rotation, renderingArea } = props; const geometryStyles: Record = {}; const pointStyles: Record = {}; - const allPoints = bubbles.reduce((acc, { seriesIdentifier, seriesPointStyle, points }) => { - const geometryStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); - geometryStyles[seriesIdentifier.key] = geometryStyle; - pointStyles[seriesIdentifier.key] = seriesPointStyle; + const allPoints = bubbles.reduce( + (acc, { value: { seriesIdentifier, seriesPointStyle, points } }) => { + geometryStyles[seriesIdentifier.key] = getGeometryStateStyle( + seriesIdentifier, + sharedStyle, + highlightedLegendItem, + ); + pointStyles[seriesIdentifier.key] = seriesPointStyle; - acc.push(...points); - return acc; - }, []); + acc.push(...points); + return acc; + }, + [], + ); - withClip( + renderPointGroup( ctx, + allPoints, + pointStyles, + geometryStyles, + rotation, + renderingArea, clippings, - (ctx) => { - renderPointGroup(ctx, allPoints, pointStyles, geometryStyles); - }, // TODO: add padding over clipping allPoints[0]?.value.mark !== null, ); diff --git a/src/chart_types/xy_chart/renderer/canvas/grids.ts b/src/chart_types/xy_chart/renderer/canvas/grids.ts index 22e056ca92..bb911fb530 100644 --- a/src/chart_types/xy_chart/renderer/canvas/grids.ts +++ b/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -17,56 +17,37 @@ * under the License. */ -import { Line, Stroke } from '../../../../geoms/types'; import { withContext } from '../../../../renderers/canvas'; -import { mergePartial } from '../../../../utils/commons'; import { Dimensions } from '../../../../utils/dimensions'; -import { AxisId } from '../../../../utils/ids'; import { AxisStyle } from '../../../../utils/themes/theme'; -import { stringToRGB } from '../../../partition_chart/layout/utils/color_library_wrappers'; -import { getSpecsById } from '../../state/utils/spec'; -import { isVerticalGrid } from '../../utils/axis_type_utils'; -import { AxisLinePosition } from '../../utils/axis_utils'; +import { LinesGrid } from '../../utils/grid_lines'; import { AxisSpec } from '../../utils/specs'; -import { renderMultiLine, MIN_STROKE_WIDTH } from './primitives/line'; +import { renderMultiLine } from './primitives/line'; interface GridProps { sharedAxesStyle: AxisStyle; - axesGridLinesPositions: Map; + perPanelGridLines: Array; axesSpecs: AxisSpec[]; - chartDimensions: Dimensions; + renderingArea: Dimensions; axesStyles: Map; } /** @internal */ export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { - const { axesGridLinesPositions, axesSpecs, chartDimensions, sharedAxesStyle, axesStyles } = props; + const { + perPanelGridLines, + renderingArea: { left, top }, + } = props; withContext(ctx, (ctx) => { - ctx.translate(chartDimensions.left, chartDimensions.top); - axesGridLinesPositions.forEach((axisGridLinesPositions, axisId) => { - const axisSpec = getSpecsById(axesSpecs, axisId); - if (axisSpec && axisGridLinesPositions.length > 0) { - const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; - const themeConfig = isVerticalGrid(axisSpec.position) - ? axisStyle.gridLine.vertical - : axisStyle.gridLine.horizontal; + ctx.translate(left, top); - const axisSpecConfig = axisSpec.gridLine; - const gridLine = axisSpecConfig ? mergePartial(themeConfig, axisSpecConfig) : themeConfig; - if (!gridLine.stroke || !gridLine.strokeWidth || gridLine.strokeWidth < MIN_STROKE_WIDTH) { - return; - } - const strokeColor = stringToRGB(gridLine.stroke); - strokeColor.opacity = - gridLine.opacity !== undefined ? strokeColor.opacity * gridLine.opacity : strokeColor.opacity; - const stroke: Stroke = { - color: strokeColor, - width: gridLine.strokeWidth, - dash: gridLine.dash, - }; - const lines = axisGridLinesPositions.map(([x1, y1, x2, y2]) => ({ x1, y1, x2, y2 })); - renderMultiLine(ctx, lines, stroke); - } + perPanelGridLines.forEach(({ lineGroups, panelAnchor: { x, y } }) => { + withContext(ctx, (ctx) => { + ctx.translate(x, y); + lineGroups.forEach(({ lines, stroke }) => { + renderMultiLine(ctx, lines, stroke); + }); + }); }); }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/lines.ts b/src/chart_types/xy_chart/renderer/canvas/lines.ts index 4a38acda3f..db631a2b8f 100644 --- a/src/chart_types/xy_chart/renderer/canvas/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -19,46 +19,53 @@ import { LegendItem } from '../../../../commons/legend'; import { Rect } from '../../../../geoms/types'; -import { withContext, withClip } from '../../../../renderers/canvas'; -import { LineGeometry } from '../../../../utils/geometry'; +import { withContext } from '../../../../renderers/canvas'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { LineGeometry, PerPanel } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/rendering'; import { renderPoints } from './points'; import { renderLinePaths } from './primitives/path'; import { buildLineStyles } from './styles/line'; +import { withPanelTransform } from './utils/panel_transform'; interface LineGeometriesDataProps { animated?: boolean; - lines: LineGeometry[]; + lines: Array>; + renderingArea: Dimensions; + rotation: Rotation; sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; + highlightedLegendItem?: LegendItem; clippings: Rect; } /** @internal */ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometriesDataProps) { withContext(ctx, (ctx) => { - const { lines, sharedStyle, highlightedLegendItem, clippings } = props; + const { lines, sharedStyle, highlightedLegendItem, clippings, renderingArea, rotation } = props; - lines.forEach((line) => { + lines.forEach(({ panel, value: line }) => { const { seriesLineStyle, seriesPointStyle } = line; if (seriesLineStyle.visible) { - withContext(ctx, (ctx) => { - renderLine(ctx, line, highlightedLegendItem, sharedStyle, clippings); + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderLine(ctx, line, sharedStyle, clippings, highlightedLegendItem); }); } if (seriesPointStyle.visible) { - withClip( + withPanelTransform( ctx, - clippings, + panel, + rotation, + renderingArea, (ctx) => { - const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, highlightedLegendItem, sharedStyle); + const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, sharedStyle, highlightedLegendItem); renderPoints(ctx, line.points, line.seriesPointStyle, geometryStyle); }, // TODO: add padding over clipping - line.points[0]?.value.mark !== null, + { area: clippings, shouldClip: line.points[0]?.value.mark !== null }, ); } }); @@ -68,12 +75,12 @@ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometries function renderLine( ctx: CanvasRenderingContext2D, line: LineGeometry, - highlightedLegendItem: LegendItem | null, sharedStyle: SharedGeometryStateStyle, clippings: Rect, + highlightedLegendItem?: LegendItem, ) { const { color, transform, seriesIdentifier, seriesLineStyle, clippedRanges, hideClippedRanges } = line; - const geometryStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const geometryStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); const stroke = buildLineStyles(color, seriesLineStyle, geometryStyle); - renderLinePaths(ctx, transform.x, [line.line], stroke, clippedRanges, clippings, hideClippedRanges); + renderLinePaths(ctx, transform, [line.line], stroke, clippedRanges, clippings, hideClippedRanges); } diff --git a/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts b/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts new file mode 100644 index 0000000000..20a87cf018 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts @@ -0,0 +1,43 @@ +/* + * 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 { withContext } from '../../../../../renderers/canvas'; +import { Point } from '../../../../../utils/point'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; +import { PanelGeoms } from '../../../state/selectors/compute_panels'; +import { renderRect } from '../primitives/rect'; + +/** @internal */ +export function renderGridPanels(ctx: CanvasRenderingContext2D, chartAnchor: Point, panels: PanelGeoms) { + withContext(ctx, (ctx) => { + ctx.translate(chartAnchor.x, chartAnchor.y); + panels.forEach((panel) => { + withContext(ctx, (ctx) => { + ctx.translate(panel.panelAnchor.x, panel.panelAnchor.y); + withContext(ctx, (ctx) => { + renderRect( + ctx, + { x: 0, y: 0, ...panel }, + { color: stringToRGB('#00000000') }, + { color: stringToRGB('#000000'), width: 1 }, + ); + }); + }); + }); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/points.ts b/src/chart_types/xy_chart/renderer/canvas/points.ts index 3db4866d2c..42a9817f2b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/points.ts +++ b/src/chart_types/xy_chart/renderer/canvas/points.ts @@ -18,11 +18,14 @@ */ import { SeriesKey } from '../../../../commons/series_id'; -import { Circle, Stroke, Fill } from '../../../../geoms/types'; +import { Circle, Stroke, Fill, Rect } from '../../../../geoms/types'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; import { PointGeometry } from '../../../../utils/geometry'; import { PointStyle, GeometryStateStyle } from '../../../../utils/themes/theme'; import { renderCircle } from './primitives/arc'; import { buildPointStyles } from './styles/point'; +import { withPanelTransform } from './utils/panel_transform'; /** * Renders points from single series @@ -48,7 +51,7 @@ export function renderPoints( const circle: Circle = { x: x + transform.x, - y, + y: y + transform.y, radius, }; @@ -68,9 +71,13 @@ export function renderPointGroup( points: PointGeometry[], themeStyles: Record, geometryStateStyles: Record, + rotation: Rotation, + renderingArea: Dimensions, + clippings: Rect, + shouldClip: boolean, ) { points - .map<[Circle, Fill, Stroke]>((point) => { + .map<[Circle, Fill, Stroke, Dimensions]>((point) => { const { x, y, @@ -79,6 +86,7 @@ export function renderPointGroup( transform, styleOverrides, seriesIdentifier: { key }, + panel, } = point; const { fill, stroke, radius } = buildPointStyles( color, @@ -94,8 +102,19 @@ export function renderPointGroup( radius, }; - return [circle, fill, stroke]; + return [circle, fill, stroke, panel]; }) .sort(([{ radius: a }], [{ radius: b }]) => b - a) - .forEach((args) => renderCircle(ctx, ...args)); + .forEach(([circle, fill, stroke, panel]) => { + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderCircle(ctx, circle, fill, stroke); + }, + { area: clippings, shouldClip }, + ); + }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts index 092d17211a..46855fb4cd 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts @@ -29,21 +29,7 @@ export const MIN_STROKE_WIDTH = 0.001; /** @internal */ export function renderLine(ctx: CanvasRenderingContext2D, line: Line, stroke: Stroke) { - if (stroke.width < MIN_STROKE_WIDTH) { - return; - } - withContext(ctx, (ctx) => { - if (stroke.dash) { - ctx.setLineDash(stroke.dash); - } - const { x1, y1, x2, y2 } = line; - ctx.strokeStyle = RGBtoString(stroke.color); - ctx.lineWidth = stroke.width; - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - }); + renderMultiLine(ctx, [line], stroke); } /** @internal */ diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts index 399ca82e3d..c16fa5c8c1 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -20,13 +20,14 @@ import { Rect, Stroke, Fill } from '../../../../../geoms/types'; import { withContext, withClipRanges } from '../../../../../renderers/canvas'; import { ClippedRanges } from '../../../../../utils/geometry'; +import { Point } from '../../../../../utils/point'; import { RGBtoString } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { MIN_STROKE_WIDTH } from './line'; /** @internal */ export function renderLinePaths( context: CanvasRenderingContext2D, - transformX: number, + transform: Point, linePaths: Array, stroke: Stroke, clippedRanges: ClippedRanges, @@ -35,7 +36,7 @@ export function renderLinePaths( ) { if (clippedRanges.length > 0) { withClipRanges(context, clippedRanges, clippings, false, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); linePaths.forEach((path) => { renderPathStroke(ctx, path, stroke); }); @@ -44,7 +45,7 @@ export function renderLinePaths( return; } withClipRanges(context, clippedRanges, clippings, true, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); linePaths.forEach((path) => { renderPathStroke(ctx, path, { ...stroke, dash: [5, 5] }); }); @@ -53,7 +54,7 @@ export function renderLinePaths( } withContext(context, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); linePaths.forEach((path) => { renderPathStroke(ctx, path, stroke); }); @@ -63,7 +64,7 @@ export function renderLinePaths( /** @internal */ export function renderAreaPath( ctx: CanvasRenderingContext2D, - transformX: number, + transform: Point, area: string, fill: Fill, clippedRanges: ClippedRanges, @@ -72,14 +73,14 @@ export function renderAreaPath( ) { if (clippedRanges.length > 0) { withClipRanges(ctx, clippedRanges, clippings, false, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); renderPathFill(ctx, area, fill); }); if (hideClippedRanges) { return; } withClipRanges(ctx, clippedRanges, clippings, true, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); const { opacity } = fill.color; const color = { ...fill.color, @@ -90,7 +91,7 @@ export function renderAreaPath( return; } withContext(ctx, (ctx) => { - ctx.translate(transformX, 0); + ctx.translate(transform.x, transform.y); renderPathFill(ctx, area, fill); }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 03556e2404..071cc583b0 100644 --- a/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -27,6 +27,7 @@ import { renderBars } from './bars'; import { renderBubbles } from './bubbles'; import { renderGrids } from './grids'; import { renderLines } from './lines'; +import { renderGridPanels } from './panels/panels'; import { renderDebugRect } from './utils/debug'; import { renderBarValues } from './values/bar'; import { ReactiveChartStateProps } from './xy_chart'; @@ -42,25 +43,25 @@ export function renderXYChartCanvas2d( // let's set the devicePixelRatio once and for all; then we'll never worry about it again ctx.scale(dpr, dpr); const { - chartDimensions, + renderingArea, chartTransform, - chartRotation, + rotation, geometries, geometriesIndex, - theme, + theme: { axes: sharedAxesStyle, sharedStyle, barSeriesStyle }, highlightedLegendItem, annotationDimensions, annotationSpecs, - axisTickPositions, + perPanelAxisGeoms, + perPanelGridLines, axesSpecs, - axesTicksDimensions, axesStyles, - axesGridLinesPositions, debug, + panelGeoms, } = props; const transform = { - x: chartDimensions.left + chartTransform.x, - y: chartDimensions.top + chartTransform.y, + x: renderingArea.left + chartTransform.x, + y: renderingArea.top + chartTransform.y, }; // painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first // (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but @@ -69,36 +70,39 @@ export function renderXYChartCanvas2d( renderLayers(ctx, [ // clear the canvas (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000), - + // render panel grid + (ctx: CanvasRenderingContext2D) => { + if (debug) { + renderGridPanels(ctx, transform, panelGeoms); + } + }, (ctx: CanvasRenderingContext2D) => { renderAxes(ctx, { - axesPositions: axisTickPositions.axisPositions, axesSpecs, - axesTicksDimensions, - axesVisibleTicks: axisTickPositions.axisVisibleTicks, - chartDimensions, + perPanelAxisGeoms, + renderingArea, debug, axesStyles, - sharedAxesStyle: theme.axes, + sharedAxesStyle, }); }, (ctx: CanvasRenderingContext2D) => { renderGrids(ctx, { axesSpecs, - chartDimensions, - axesGridLinesPositions, + renderingArea, + perPanelGridLines, axesStyles, - sharedAxesStyle: theme.axes, + sharedAxesStyle, }); }, // rendering background annotations (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); renderAnnotations( ctx, { + rotation, + renderingArea, annotationDimensions, annotationSpecs, }, @@ -110,73 +114,70 @@ export function renderXYChartCanvas2d( // rendering bars (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); - renderBars(ctx, geometries.bars, theme.sharedStyle, clippings, highlightedLegendItem); + renderBars(ctx, geometries.bars, sharedStyle, clippings, renderingArea, highlightedLegendItem, rotation); }); }, // rendering areas (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); renderAreas(ctx, { areas: geometries.areas, clippings, - highlightedLegendItem: highlightedLegendItem || null, - sharedStyle: theme.sharedStyle, + renderingArea, + rotation, + highlightedLegendItem, + sharedStyle, }); }); }, // rendering lines (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); renderLines(ctx, { lines: geometries.lines, clippings, - highlightedLegendItem: highlightedLegendItem || null, - sharedStyle: theme.sharedStyle, + renderingArea, + rotation, + highlightedLegendItem, + sharedStyle, }); }); }, // rendering bubbles (ctx: CanvasRenderingContext2D) => { - withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); - renderBubbles(ctx, { - bubbles: geometries.bubbles, - clippings, - highlightedLegendItem: highlightedLegendItem || null, - sharedStyle: theme.sharedStyle, - }); + renderBubbles(ctx, { + bubbles: geometries.bubbles, + clippings, + highlightedLegendItem, + sharedStyle, + rotation, + renderingArea, }); }, (ctx: CanvasRenderingContext2D) => { - withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); - renderBarValues(ctx, { - bars: geometries.bars, - chartDimensions, - chartRotation, - debug, - theme, + geometries.bars.forEach(({ value: bars, panel }) => { + withContext(ctx, (ctx) => { + renderBarValues(ctx, { + bars, + panel, + renderingArea, + rotation, + debug, + barSeriesStyle, + }); }); }); }, // rendering foreground annotations (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - ctx.translate(transform.x, transform.y); - ctx.rotate((chartRotation * Math.PI) / 180); renderAnnotations( ctx, { annotationDimensions, annotationSpecs, + rotation, + renderingArea, }, false, ); @@ -188,7 +189,7 @@ export function renderXYChartCanvas2d( return; } withContext(ctx, (ctx) => { - const { left, top, width, height } = chartDimensions; + const { left, top, width, height } = renderingArea; renderDebugRect( ctx, diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts b/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts new file mode 100644 index 0000000000..06182b0232 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts @@ -0,0 +1,55 @@ +/* + * 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 { Rect } from '../../../../../geoms/types'; +import { withContext } from '../../../../../renderers/canvas'; +import { Rotation } from '../../../../../utils/commons'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { computeChartTransform } from '../../../state/utils/utils'; + +export function withPanelTransform( + context: CanvasRenderingContext2D, + panel: Dimensions, + rotation: Rotation, + renderingArea: Dimensions, + fn: (ctx: CanvasRenderingContext2D) => void, + clippings?: { + area: Rect; + shouldClip?: boolean; + }, +) { + const transform = computeChartTransform(panel, rotation); + const left = renderingArea.left + panel.left + transform.x; + const top = renderingArea.top + panel.top + transform.y; + withContext(context, (ctx) => { + ctx.translate(left, top); + ctx.rotate((rotation * Math.PI) / 180); + + if (clippings?.shouldClip) { + const { x, y, width, height } = clippings.area; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, width, height); + ctx.clip(); + } + fn(ctx); + if (clippings?.shouldClip) { + ctx.restore(); + } + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts index 561eed7d15..cb41d9930b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -28,13 +28,15 @@ import { colorIsDark, getTextColorIfTextInvertible } from '../../../../partition import { getFillTextColor } from '../../../../partition_chart/layout/viewmodel/fill_text_layout'; import { renderText, wrapLines } from '../primitives/text'; import { renderDebugRect } from '../utils/debug'; +import { withPanelTransform } from '../utils/panel_transform'; interface BarValuesProps { - theme: Theme; - chartDimensions: Dimensions; - chartRotation: Rotation; + barSeriesStyle: Theme['barSeriesStyle']; + renderingArea: Dimensions; + rotation: Rotation; debug: boolean; bars: BarGeometry[]; + panel: Dimensions; } const CHART_DIRECTION: Record = { @@ -46,8 +48,8 @@ const CHART_DIRECTION: Record = { /** @internal */ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesProps) { - const { bars, debug, chartRotation, chartDimensions, theme } = props; - const { fontFamily, fontStyle, fill, alignment } = theme.barSeriesStyle.displayValue; + const { bars, debug, rotation, renderingArea, barSeriesStyle, panel } = props; + const { fontFamily, fontStyle, fill, alignment } = barSeriesStyle.displayValue; const barsLength = bars.length; for (let i = 0; i < barsLength; i++) { const { displayValue } = bars[i]; @@ -72,16 +74,16 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP const { x, y, align, baseline, rect } = positionText( bars[i], displayValue, - chartRotation, - theme.barSeriesStyle.displayValue, + rotation, + barSeriesStyle.displayValue, alignment, ); if (displayValue.isValueContainedInElement) { - const width = chartRotation === 0 || chartRotation === 180 ? bars[i].width : bars[i].height; + const width = rotation === 0 || rotation === 180 ? bars[i].width : bars[i].height; textLines = wrapLines(ctx, textLines.lines[0], font, fontSize, width, 100); } - if (displayValue.hideClippedValue && isOverflow(rect, chartDimensions, chartRotation)) { + if (displayValue.hideClippedValue && isOverflow(rect, renderingArea, rotation)) { continue; } if (debug) { @@ -94,24 +96,26 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP for (let j = 0; j < linesLength; j++) { const textLine = textLines.lines[j]; - const origin = repositionTextLine({ x, y }, chartRotation, j, linesLength, { height, width }); - renderText( - ctx, - origin, - textLine, - { - ...font, - fill: fillColor, - fontSize, - align, - baseline, - shadow: shadowColor, - shadowSize, - }, - -chartRotation, - undefined, - fontScale, - ); + const origin = repositionTextLine({ x, y }, rotation, j, linesLength, { height, width }); + withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { + renderText( + ctx, + origin, + textLine, + { + ...font, + fill: fillColor, + fontSize, + align, + baseline, + shadow: shadowColor, + shadowSize, + }, + -rotation, + undefined, + fontScale, + ); + }); } } } diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 55a7328d29..e10783d74a 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -37,17 +37,21 @@ import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme, AxisStyle } from '../../../../utils/themes/theme'; import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; -import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; -import { AxisVisibleTicks, computeAxisVisibleTicksSelector } from '../../state/selectors/compute_axis_visible_ticks'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; +import { computePerPanelGridLinesSelector } from '../../state/selectors/compute_grid_lines'; +import { computePanelsSelectors, PanelGeoms } from '../../state/selectors/compute_panels'; +import { + computePerPanelAxesGeomsSelector, + PerPanelAxisGeoms, +} from '../../state/selectors/compute_per_panel_axes_geoms'; import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; import { getAxesStylesSelector } from '../../state/selectors/get_axis_styles'; import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; import { Geometries, Transform } from '../../state/utils/types'; -import { AxisLinePosition, AxisTicksDimensions } from '../../utils/axis_utils'; +import { LinesGrid } from '../../utils/grid_lines'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; import { AxisSpec, AnnotationSpec } from '../../utils/specs'; import { renderXYChartCanvas2d } from './renderers'; @@ -61,17 +65,17 @@ export interface ReactiveChartStateProps { geometriesIndex: IndexedGeometryMap; theme: Theme; chartContainerDimensions: Dimensions; - chartRotation: Rotation; - chartDimensions: Dimensions; + rotation: Rotation; + renderingArea: Dimensions; chartTransform: Transform; highlightedLegendItem?: LegendItem; axesSpecs: AxisSpec[]; - axesTicksDimensions: Map; + perPanelAxisGeoms: Array; + perPanelGridLines: Array; axesStyles: Map; - axisTickPositions: AxisVisibleTicks; - axesGridLinesPositions: Map; annotationDimensions: Map; annotationSpecs: AnnotationSpec[]; + panelGeoms: PanelGeoms; } interface ReactiveChartDispatchProps { @@ -123,12 +127,12 @@ class XYChartComponent extends React.Component { private drawCanvas() { if (this.ctx) { - const { chartDimensions, chartRotation } = this.props; + const { renderingArea, rotation } = this.props; const clippings = { x: 0, y: 0, - width: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width, - height: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height, + width: [90, -90].includes(rotation) ? renderingArea.height : renderingArea.width, + height: [90, -90].includes(rotation) ? renderingArea.width : renderingArea.height, }; renderXYChartCanvas2d(this.ctx, this.devicePixelRatio, clippings, this.props); } @@ -194,8 +198,8 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { left: 0, top: 0, }, - chartRotation: 0 as const, - chartDimensions: { + rotation: 0 as const, + renderingArea: { width: 0, height: 0, left: 0, @@ -208,17 +212,12 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { }, axesSpecs: [], - axisTickPositions: { - axisGridLinesPositions: new Map(), - axisPositions: new Map(), - axisTicks: new Map(), - axisVisibleTicks: new Map(), - }, - axesTicksDimensions: new Map(), + perPanelAxisGeoms: [], + perPanelGridLines: [], axesStyles: new Map(), - axesGridLinesPositions: new Map(), annotationDimensions: new Map(), annotationSpecs: [], + panelGeoms: [], }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -237,16 +236,16 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { theme: getChartThemeSelector(state), chartContainerDimensions: getChartContainerDimensionsSelector(state), highlightedLegendItem: getHighlightedSeriesSelector(state), - chartRotation: getChartRotationSelector(state), - chartDimensions: computeChartDimensionsSelector(state).chartDimensions, + rotation: getChartRotationSelector(state), + renderingArea: computeChartDimensionsSelector(state).chartDimensions, chartTransform: computeChartTransformSelector(state), axesSpecs: getAxisSpecsSelector(state), - axisTickPositions: computeAxisVisibleTicksSelector(state), - axesTicksDimensions: computeAxisTicksDimensionsSelector(state), + perPanelAxisGeoms: computePerPanelAxesGeomsSelector(state), + perPanelGridLines: computePerPanelGridLinesSelector(state), axesStyles: getAxesStylesSelector(state), - axesGridLinesPositions: computeAxisVisibleTicksSelector(state).axisGridLinesPositions, annotationDimensions: computeAnnotationDimensionsSelector(state), annotationSpecs: getAnnotationSpecsSelector(state), + panelGeoms: computePanelsSelectors(state), }; }; diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx b/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx index 2fb369842c..135780d5f0 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx @@ -64,16 +64,15 @@ function renderAnnotationLineMarkers( annotationLines: AnnotationLineProps[], id: AnnotationId, ) { - return annotationLines.reduce((markers, { marker }: AnnotationLineProps, index: number) => { + return annotationLines.reduce((markers, { marker, panel }: AnnotationLineProps, index: number) => { if (!marker) { return markers; } - const { icon, color, position } = marker; const style = { color, - top: chartDimensions.top + position.top, - left: chartDimensions.left + position.left, + top: chartDimensions.top + position.top + panel.top, + left: chartDimensions.left + position.left + panel.left, }; markers.push( diff --git a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index c3f749342b..d3bdb54f1b 100644 --- a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -31,6 +31,7 @@ import { computeChartDimensionsSelector } from '../../state/selectors/compute_ch import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; import { getHighlightedGeomsSelector } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; import { Transform } from '../../state/utils/types'; +import { computeChartTransform } from '../../state/utils/utils'; interface HighlighterProps { initialized: boolean; @@ -41,13 +42,16 @@ interface HighlighterProps { chartRotation: Rotation; } +function getTransformForPanel(panel: Dimensions, rotation: Rotation, { left, top }: Dimensions) { + const { x, y } = computeChartTransform(panel, rotation); + return `translate(${left + panel.left + x}, ${top + panel.top + y}) rotate(${rotation})`; +} + class HighlighterComponent extends React.Component { static displayName = 'Highlighter'; render() { - const { highlightedGeometries, chartTransform, chartDimensions, chartRotation, chartId } = this.props; - const left = chartDimensions.left + chartTransform.x; - const top = chartDimensions.top + chartTransform.y; + const { highlightedGeometries, chartDimensions, chartRotation, chartId } = this.props; const clipWidth = [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width; const clipHeight = [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height; const clipPathId = `echHighlighterClipPath__${chartId}`; @@ -58,18 +62,23 @@ class HighlighterComponent extends React.Component { - + {highlightedGeometries.map((geom, i) => { - const { color, x, y } = geom; + const { color, panel } = geom; + const geomTransform = getTransformForPanel(panel, chartRotation, chartDimensions); + const x = geom.x + geom.transform.x; + const y = geom.y + geom.transform.y; + if (isPointGeometry(geom)) { return ( @@ -82,6 +91,7 @@ class HighlighterComponent extends React.Component { y={y} width={geom.width} height={geom.height} + transform={geomTransform} className="echHighlighterOverlay__fill" clipPath={`url(#${clipPathId})`} /> diff --git a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts index d7e74ce6ec..642f30b58a 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -17,132 +17,85 @@ * under the License. */ -import { ChartTypes } from '../..'; -import { MockSeriesSpec } from '../../../mocks/specs'; +import { Store } from 'redux'; + +import { MockPointGeometry } from '../../../mocks/geometries'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; -import { CurveType } from '../../../utils/curves'; +import { Spec } from '../../../specs'; +import { GlobalChartState } from '../../../state/chart_state'; import { PointGeometry, AreaGeometry } from '../../../utils/geometry'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; +import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; +import { ComputedGeometries } from '../state/utils/types'; import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { AreaSeriesSpec, SeriesTypes, StackMode } from '../utils/specs'; -import { renderArea } from './rendering'; +import { AreaSeriesSpec, StackMode } from '../utils/specs'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; -describe('Rendering points - areas', () => { - describe('Empty line for missing data', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ - yDomains: pointSeriesDomains.yDomain, - range: [100, 0], - }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; +function initStore(specs: Spec[], vizColors: string[] = ['red'], width = 100): Store { + const store = MockStore.default({ width, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + ...specs, + MockGlobalSpec.settingsNoMargins({ + theme: { + colors: { + vizColors, + }, + }, + }), + ], + store, + ); + return store; +} - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - { ...pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], data: [] }, - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - }); - test('Render geometry but empty upper and lower lines and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines.length).toBe(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); +describe('Rendering points - areas', () => { + test('Missing geometry if no data', () => { + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [], + }), + ]); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); + expect(areas).toHaveLength(0); }); describe('Single series area chart - ordinal', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let areaGeometry: AreaGeometry; + let geometriesIndex: IndexedGeometryMap; beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + ]); + const geometries = computeSeriesGeometriesSelector(store.getState()); + [{ value: areaGeometry }] = geometries.geometries.areas; + geometriesIndex = geometries.geometriesIndex; }); test('Can render an line and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; + const { lines, area, color, seriesIdentifier, transform } = areaGeometry; expect(lines[0]).toBe('M0,0L50,50'); expect(area).toBe('M0,0L50,50L50,100L0,100Z'); expect(color).toBe('red'); @@ -152,11 +105,7 @@ describe('Rendering points - areas', () => { }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - + const { points } = areaGeometry; expect(points[0]).toEqual(({ x: 0, y: 0, @@ -167,7 +116,10 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -181,6 +133,12 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); expect(points[1]).toEqual(({ x: 50, @@ -192,7 +150,10 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -206,124 +167,86 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series area chart - ordinal', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let geometries: ComputedGeometries; beforeEach(() => { - firstLine = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 20], + [1, 10], + ], + }), + ], + ['red', 'blue'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); test('Can render two ordinal areas', () => { - expect(firstLine.areaGeometry.lines[0]).toBe('M0,50L50,75'); - expect(firstLine.areaGeometry.area).toBe('M0,50L50,75L50,100L0,100Z'); - expect(firstLine.areaGeometry.color).toBe('red'); - expect(firstLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.areaGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.areaGeometry.transform).toEqual({ x: 25, y: 0 }); + const { areas } = geometries.geometries; + const [{ value: firstArea }, { value: secondArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,50L50,75'); + expect(firstArea.area).toBe('M0,50L50,75L50,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 25, y: 0 }); - expect(secondLine.areaGeometry.lines[0]).toBe('M0,0L50,50'); - expect(secondLine.areaGeometry.area).toBe('M0,0L50,50L50,100L0,100Z'); - expect(secondLine.areaGeometry.color).toBe('blue'); - expect(secondLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.areaGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.areaGeometry.transform).toEqual({ x: 25, y: 0 }); + expect(secondArea.lines[0]).toBe('M0,0L50,50'); + expect(secondArea.area).toBe('M0,0L50,50L50,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 25, y: 0 }); }); test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual(({ x: 0, y: 50, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -337,18 +260,27 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 50, y: 75, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -362,26 +294,32 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -395,18 +333,27 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(secondArea.points[1]).toEqual(({ x: 50, y: 50, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, styleOverrides: undefined, value: { @@ -420,652 +367,406 @@ describe('Rendering points - areas', () => { x: 25, y: 0, }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + }); + test('has the right number of geometry in the indexes', () => { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); }); + describe('Single series area chart - linear', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], data: [ [0, 10], [1, 5], ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([spec], ['red']); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a linear area', () => { - expect(renderedArea.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(renderedArea.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); }); + describe('Multi series area chart - linear', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, + let geometries: ComputedGeometries; + const spec1 = MockSeriesSpec.area({ + id: 'spec_1', groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], data: [ [0, 10], [1, 5], ], - xAccessor: 0, - yAccessors: [1], + }); + const spec2 = MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xAccessor: 0, + yAccessors: [1], data: [ [0, 20], [1, 10], ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - beforeEach(() => { - firstLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([spec1, spec2], ['red', 'blue']); + geometries = computeSeriesGeometriesSelector(store.getState()); }); test('can render two linear areas', () => { - expect(firstLine.areaGeometry.lines[0]).toBe('M0,50L100,75'); - expect(firstLine.areaGeometry.area).toBe('M0,50L100,75L100,100L0,100Z'); - expect(firstLine.areaGeometry.color).toBe('red'); - expect(firstLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.areaGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [{ value: firstArea }, { value: secondArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,50L100,75'); + expect(firstArea.area).toBe('M0,50L100,75L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); - expect(secondLine.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(secondLine.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(secondLine.areaGeometry.color).toBe('blue'); - expect(secondLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.areaGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + expect(secondArea.lines[0]).toBe('M0,0L100,50'); + expect(secondArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 0, y: 0 }); }); test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); }); }); describe('Single series area chart - time', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], data: [ [1546300800000, 10], [1546387200000, 5], ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([spec], ['red']); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a time area', () => { - expect(renderedArea.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(renderedArea.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - transform: { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); }); describe('Multi series area chart - time', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, + let geometries: ComputedGeometries; + const spec1 = MockSeriesSpec.area({ + id: 'spec_1', groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], data: [ [1546300800000, 10], [1546387200000, 5], ], - xAccessor: 0, - yAccessors: [1], + }); + const spec2 = MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xAccessor: 0, + yAccessors: [1], data: [ [1546300800000, 20], [1546387200000, 10], ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - beforeEach(() => { - firstLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([spec1, spec2], ['red', 'blue']); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - transform: { + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec1), + + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546300800000, - y: 20, - mark: null, - datum: [1546300800000, 20], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1546387200000, - y: 10, - mark: null, - datum: [1546387200000, 10], - }, - transform: { + const { areas } = geometries.geometries; + const [, { value: secondArea }] = areas; + + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec2), + + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + }), + ); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); }); }); describe('Single series area chart - y log', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, + let geometries: ComputedGeometries; + const spec = MockSeriesSpec.area({ + id: 'spec_1', groupId: GROUP_ID, - seriesType: SeriesTypes.Area, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Log, + xAccessor: 0, + yAccessors: [1], data: [ [0, 10], [1, 5], @@ -1077,63 +778,40 @@ describe('Rendering points - areas', () => { [7, 10], [8, 10], ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Log, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 90], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([spec], ['red'], 90); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a splitted area and line', () => { - // expect(renderedArea.lineGeometry.line).toBe('ss'); - expect(renderedArea.areaGeometry.lines[0].split('M').length - 1).toBe(3); - expect(renderedArea.areaGeometry.area.split('M').length - 1).toBe(3); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [{ value: firstArea }] = areas; + expect(firstArea.lines[0].split('M').length - 1).toBe(3); + expect(firstArea.area.split('M').length - 1).toBe(3); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render points', () => { const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; + geometriesIndex, + geometries: { areas }, + } = geometries; + const [ + { + value: { points }, + }, + ] = areas; // all the points minus the undefined ones on a log scale expect(points.length).toBe(7); // all the points expect null geometries - expect(indexedGeometryMap.size).toEqual(8); - const nullIndexdGeometry = indexedGeometryMap.find(2)!; + expect(geometriesIndex.size).toEqual(8); + const nullIndexdGeometry = geometriesIndex.find(2)!; expect(nullIndexdGeometry).toEqual([]); - const zeroValueIndexdGeometry = indexedGeometryMap.find(5)!; + const zeroValueIndexdGeometry = geometriesIndex.find(5)!; expect(zeroValueIndexdGeometry).toBeDefined(); expect(zeroValueIndexdGeometry.length).toBe(1); // moved to the bottom of the chart @@ -1169,8 +847,11 @@ describe('Rendering points - areas', () => { stackAccessors: [0], stackMode: StackMode.Percentage, }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toMatchObject([ + + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ { datum: [1546300800000, 0], initialY0: null, @@ -1216,8 +897,10 @@ describe('Rendering points - areas', () => { yScaleType: ScaleType.Linear, stackAccessors: [0], }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toMatchObject([ + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ { datum: [1546300800000, null], initialY0: null, @@ -1238,7 +921,7 @@ describe('Rendering points - areas', () => { }, ]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[1].data).toEqual([ + expect(domains.formattedDataSeries[1].data).toEqual([ { datum: [1546300800000, 3], initialY0: null, @@ -1260,90 +943,90 @@ describe('Rendering points - areas', () => { ]); }); - describe('Error guards for scaled values', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - }); - - describe('xScale values throw error', () => { - beforeAll(() => { - jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - test('Should include no lines nor area', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines).toHaveLength(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); - }); - - describe('yScale values throw error', () => { - beforeAll(() => { - jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - test('Should include no lines nor area', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines).toHaveLength(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); - }); - }); + // describe('Error guards for scaled values', () => { + // const pointSeriesSpec: AreaSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Area, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const pointSeriesMap = [pointSeriesSpec]; + // const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: pointSeriesDomains.xDomain, + // totalBarsInCluster: pointSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + // let renderedArea: { + // areaGeometry: AreaGeometry; + // indexedGeometryMap: IndexedGeometryMap; + // }; + // + // beforeEach(() => { + // renderedArea = renderArea( + // 25, // adding a ideal 25px shift, generally applied by renderGeometries + // pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // CurveType.LINEAR, + // false, + // 0, + // LIGHT_THEME.areaSeriesStyle, + // { + // enabled: false, + // }, + // ); + // }); + // + // describe('xScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // + // describe('yScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts index 0134440e1a..7263c1b3d2 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -17,88 +17,20 @@ * under the License. */ -import { ChartTypes } from '../..'; -import { MockPointGeometry } from '../../../mocks'; +import { MockBarGeometry, MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; -import { CurveType } from '../../../utils/curves'; -import { AreaGeometry, PointGeometry } from '../../../utils/geometry'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; -import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { AreaSeriesSpec, BarSeriesSpec, SeriesTypes } from '../utils/specs'; -import { renderArea, renderBars } from './rendering'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering bands - areas', () => { - describe('Empty line for missing data', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 2, 10], - [1, 3, 5], - ], - xAccessor: 0, - y0Accessors: [1], - yAccessors: [2], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - { ...pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], data: [] }, - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - true, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - }); - test('Render geometry but empty upper and lower lines and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines.length).toBe(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([2]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); - }); describe('Single band area chart', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.area({ id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, data: [ [0, 2, 10], [1, 3, 5], @@ -108,40 +40,20 @@ describe('Rendering bands - areas', () => { yAccessors: [2], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - true, - 0, - LIGHT_THEME.areaSeriesStyle, + test('Can render upper and lower lines and area paths', () => { + const [ { - enabled: false, + value: { lines, area, color, seriesIdentifier, transform }, }, - ); - }); - test('Can render upper and lower lines and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; + ] = areas; expect(lines.length).toBe(2); expect(lines[0]).toBe('M0,0L50,50'); expect(lines[1]).toBe('M0,80L50,70'); @@ -153,120 +65,103 @@ describe('Rendering bands - areas', () => { }); test('Can render two points', () => { - const { - areaGeometry: { points }, - } = renderedArea; - expect(points.length).toBe(4); - expect(points[0]).toEqual(({ - x: 0, - y: 80, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', + const [ + { + value: { points }, }, - styleOverrides: undefined, - value: { - accessor: 'y0', + ] = areas; + expect(points.length).toBe(4); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 2, - mark: null, - datum: [0, 2, 10], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); + y: 80, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + styleOverrides: undefined, + value: { + accessor: 'y0', + x: 0, + y: 2, + mark: null, + datum: [0, 2, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); - expect(points[1]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', + expect(points[1]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 2, 10], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[2]).toEqual(({ - x: 50, - y: 70, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - value: { - accessor: 'y0', - x: 1, - y: 3, - mark: null, - datum: [1, 3, 5], - }, - styleOverrides: undefined, - transform: { - x: 25, y: 0, - }, - } as unknown) as PointGeometry); - expect(points[3]).toEqual(({ - x: 50, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 3, 5], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[2]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 70, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y0', + x: 1, + y: 3, + mark: null, + datum: [1, 3, 5], + }, + styleOverrides: undefined, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[3]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 3, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); }); }); describe('Single band area chart with null values', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.area({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Area, data: [ [0, 2, 10], [1, 2, null], @@ -278,40 +173,20 @@ describe('Rendering bands - areas', () => { yAccessors: [2], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - true, - 0, - LIGHT_THEME.areaSeriesStyle, + test('Can render upper and lower lines and area paths', () => { + const [ { - enabled: false, + value: { lines, area, color, seriesIdentifier, transform }, }, - ); - }); - test('Can render upper and lower lines and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; + ] = areas; expect(lines.length).toBe(2); expect(lines[0]).toBe('M0,0ZM50,50L75,50'); expect(lines[1]).toBe('M0,80ZM50,70L75,70'); @@ -319,13 +194,15 @@ describe('Rendering bands - areas', () => { expect(color).toBe('red'); expect(seriesIdentifier.seriesKeys).toEqual([2]); expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); + expect(transform).toEqual({ x: 12.5, y: 0 }); }); test('Can render two points', () => { - const { - areaGeometry: { points }, - } = renderedArea; + const [ + { + value: { points }, + }, + ] = areas; expect(points.length).toBe(6); const getPointGeo = MockPointGeometry.fromBaseline( { @@ -341,7 +218,7 @@ describe('Rendering bands - areas', () => { datum: [0, 2, 10], }, transform: { - x: 25, + x: 12.5, y: 0, }, }, @@ -426,12 +303,9 @@ describe('Rendering bands - areas', () => { }); }); describe('Single series band bar chart - ordinal', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const barSeriesSpec = MockSeriesSpec.bar({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, data: [ [0, 2, 10], [1, 3, null], @@ -443,146 +317,126 @@ describe('Rendering bands - areas', () => { yAccessors: [2], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([barSeriesSpec, settings], store); + const { + geometries: { + bars: [{ value: bars }], + }, + } = computeSeriesGeometriesSelector(store.getState()); test('Can render two bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toBe(3); - expect(barGeometries[0]).toEqual({ - x: 0, - y: 0, - width: 25, - height: 80, - color: 'red', - value: { - accessor: 'y1', + expect(bars.length).toBe(3); + expect(bars[0]).toEqual( + MockBarGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 2, 10], - }, - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - displayValue: undefined, - seriesStyle: { - displayValue: { - fill: '#777', - fontFamily: 'sans-serif', - fontSize: 8, - fontStyle: 'normal', - offsetX: 0, - offsetY: 0, - padding: 0, - }, - rect: { - opacity: 1, - }, - rectBorder: { - strokeWidth: 0, - visible: false, - }, - }, - }); - expect(barGeometries[1]).toEqual({ - x: 50, - y: 50, - width: 25, - height: 20, - color: 'red', - value: { - accessor: 'y1', - x: 2, - y: 5, - mark: null, - datum: [2, 3, 5], - }, - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - displayValue: undefined, - seriesStyle: { - displayValue: { - fill: '#777', - fontFamily: 'sans-serif', - fontSize: 8, - fontStyle: 'normal', - offsetX: 0, - offsetY: 0, - padding: 0, + y: 0, + width: 25, + height: 80, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 2, 10], }, - rect: { - opacity: 1, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 0, + visible: false, + }, }, - rectBorder: { - strokeWidth: 0, - visible: false, + }), + ); + expect(bars[1]).toEqual( + MockBarGeometry.default({ + x: 50, + y: 50, + width: 25, + height: 20, + color: 'red', + value: { + accessor: 'y1', + x: 2, + y: 5, + mark: null, + datum: [2, 3, 5], }, - }, - }); - expect(barGeometries[2]).toEqual({ - x: 75, - y: 20, - width: 25, - height: 40, - color: 'red', - value: { - accessor: 'y1', - x: 3, - y: 8, - mark: null, - datum: [3, 4, 8], - }, - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 2, - splitAccessors: new Map(), - seriesKeys: [2], - key: 'spec{spec_1}yAccessor{2}splitAccessors{}', - }, - displayValue: undefined, - seriesStyle: { - displayValue: { - fill: '#777', - fontFamily: 'sans-serif', - fontSize: 8, - fontStyle: 'normal', - offsetX: 0, - offsetY: 0, - padding: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 0, + visible: false, + }, }, - rect: { - opacity: 1, + }), + ); + expect(bars[2]).toEqual( + MockBarGeometry.default({ + x: 75, + y: 20, + width: 25, + height: 40, + color: 'red', + value: { + accessor: 'y1', + x: 3, + y: 8, + mark: null, + datum: [3, 4, 8], }, - rectBorder: { - strokeWidth: 0, - visible: false, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec), + displayValue: undefined, + seriesStyle: { + displayValue: { + fill: '#777', + fontFamily: 'sans-serif', + fontSize: 8, + fontStyle: 'normal', + offsetX: 0, + offsetY: 0, + padding: 0, + }, + rect: { + opacity: 1, + }, + rectBorder: { + strokeWidth: 0, + visible: false, + }, }, - }, - }); + }), + ); }); }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts index 1d479f1a96..5cccbccc8e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -17,196 +17,39 @@ * under the License. */ -import { ChartTypes } from '../..'; import { MockBarGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; import { identity } from '../../../utils/commons'; -import { GroupId } from '../../../utils/ids'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { BarSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; -import { renderBars } from './rendering'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering bars', () => { - describe('Single series bar chart - ordinal', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + test('Can render two bars within domain', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const spec = MockSeriesSpec.bar({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], data: [ [-200, 0], [0, 10], [1, 5], ], // first datum should be skipped as it's out of domain - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec]; - const customDomain = [0, 1]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map(), [], customDomain); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render two bars within domain', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 50, - width: 50, - height: 50, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - expect(barGeometries.length).toBe(2); - }); - test('Can render bars with value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isAlternatingValueLabel: true }, - ); - expect(barGeometries[0].displayValue).toBeDefined(); - }); - - test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - {}, - ); - expect(barGeometries[0].displayValue).toBeUndefined(); - }); - - test('Can render bars with alternating value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isAlternatingValueLabel: true }, - ); - expect(barGeometries[0].displayValue?.text).toBeDefined(); - expect(barGeometries[1].displayValue?.text).toBeUndefined(); }); + MockStore.addSpecs( + [spec, MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } })], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); - test('Can render bars with contained value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isValueContainedInElement: true }, - ); - expect(barGeometries[0].displayValue?.width).toBe(50); - }); - }); - describe('Multi series bar chart - ordinal', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); const getBarGeometry = MockBarGeometry.fromBaseline( { x: 0, @@ -221,640 +64,824 @@ describe('Rendering bars', () => { mark: null, datum: [0, 10], }, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), }, 'displayValue', ); - - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual( - getBarGeometry({ - x: 0, - y: 50, - width: 25, - height: 50, - value: { - x: 0, - y: 10, - datum: [0, 10], - }, - }), - ); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 0, - y: 10, - datum: [0, 10], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual( - getBarGeometry({ - x: 25, - y: 0, - width: 25, - height: 100, - value: { - x: 0, - y: 20, - datum: [0, 20], - }, - }), - ); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - value: { - x: 1, - y: 10, - datum: [1, 10], - }, - }), - ); - }); - }); - describe('Single series bar chart - linear', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render two bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, + expect(geometries.bars[0].value[0]).toEqual(getBarGeometry()); + expect(geometries.bars[0].value[1]).toEqual( + getBarGeometry({ + x: 50, + y: 50, + width: 50, + height: 50, + value: { + x: 1, + y: 5, + datum: [1, 5], }, - 'displayValue', - ); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 50, - width: 50, - height: 50, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - }); - describe('Single series bar chart - log', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1, 0], - [2, 1], - [3, 2], - [4, 3], - [5, 4], - [6, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Log, - }; - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render correct bar height', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toBe(6); - expect(barGeometries[0].height).toBe(0); - expect(barGeometries[1].height).toBe(0); - expect(barGeometries[2].height).toBeGreaterThan(0); - expect(barGeometries[3].height).toBeGreaterThan(0); - }); + }), + ); + expect(geometries.bars[0].value.length).toBe(2); }); - describe('Multi series bar chart - linear', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 50, - width: 25, - height: 50, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - datum: [0, 10], - }, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 25, - y: 0, - width: 25, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 0, - y: 20, - datum: [0, 20], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - color: 'blue', - value: { - x: 1, - y: 10, - datum: [1, 10], - }, - }), - ); - }); - }); - describe('Multi series bar chart - time', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1546300800000, 10], - [1546387200000, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1546300800000, 20], - [1546387200000, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], + describe('Single series bar chart - ordinal', () => { + test('Can render bars with value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].value[0].displayValue).toBeDefined(); }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 50, - width: 25, - height: 50, - color: 'red', - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - datum: [1546300800000, 10], - }, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - }), - ); + test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: false, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].value[0].displayValue).toBeUndefined(); }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 25, - y: 0, - width: 25, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 1546300800000, - y: 20, - mark: null, - datum: [1546300800000, 20], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - value: { - accessor: 'y1', - x: 1546387200000, - y: 10, - mark: null, - datum: [1546387200000, 10], - }, - }), - ); - }); - }); - describe('Remove points datum is not in domain', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 0], - [1, 1], - [2, 10], - [3, 3], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const customYDomain = new Map(); - customYDomain.set(GROUP_ID, { - max: 1, - }); - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain, [], { - max: 2, - }); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + test('Can render bars with alternating value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); - test('Can render 3 bars', () => { - const { barGeometries, indexedGeometryMap } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toBe(3); - // will be cut by the clipping areas in the rendering component - expect(barGeometries[2].height).toBe(1000); - expect(indexedGeometryMap.size).toBe(3); + expect(geometries.bars[0].value[0].displayValue?.text).toBeDefined(); + expect(geometries.bars[0].value[1].displayValue?.text).toBeUndefined(); }); - }); - describe('Renders minBarHeight', () => { - const minBarHeight = 8; - const data = [ - [1, -100000], - [2, -10000], - [3, -1000], - [4, -100], - [5, -10], - [6, -1], - [7, 0], - [8, -1], - [9, 0], - [10, 0], - [11, 1], - [12, 0], - [13, 1], - [14, 10], - [15, 100], - [16, 1000], - [17, 10000], - [18, 100000], - ]; - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data, - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - minBarHeight, - }; - const customYDomain = new Map(); - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - const expected = [-50, -8, -8, -8, -8, -8, 0, -8, 0, 0, 8, 0, 8, 8, 8, 8, 8, 50]; + test('Can render bars with contained value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isValueContainedInElement: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); - it('should render correct heights with positive minBarHeight', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - undefined, - undefined, - minBarHeight, - ); - const barHeights = barGeometries.map(({ height }) => height); - expect(barHeights).toEqual(expected); - }); - it('should render correct heights with negative minBarHeight', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - undefined, - undefined, - -minBarHeight, - ); - const barHeights = barGeometries.map(({ height }) => height); - expect(barHeights).toEqual(expected); + expect(geometries.bars[0].value[0].displayValue?.width).toBe(50); }); }); + // describe('Multi series bar chart - ordinal', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 20], + // [1, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // mark: null, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual( + // getBarGeometry({ + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // value: { + // x: 0, + // y: 10, + // }, + // }), + // ); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual( + // getBarGeometry({ + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // value: { + // x: 0, + // y: 20, + // }, + // }), + // ); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // value: { + // x: 1, + // y: 10, + // }, + // }), + // ); + // }); + // }); + // describe('Single series bar chart - linear', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render two bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // mark: null, + // }, + // seriesIdentifier: { + // specId: SPEC_ID, + // key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 50, + // width: 50, + // height: 50, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // }); + // describe('Single series bar chart - log', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1, 0], + // [2, 1], + // [3, 2], + // [4, 3], + // [5, 4], + // [6, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Log, + // }; + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render correct bar height', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(6); + // expect(barGeometries[0].height).toBe(0); + // expect(barGeometries[1].height).toBe(0); + // expect(barGeometries[2].height).toBeGreaterThan(0); + // expect(barGeometries[3].height).toBeGreaterThan(0); + // }); + // }); + // describe('Multi series bar chart - linear', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 20], + // [1, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 0, + // y: 20, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // color: 'blue', + // value: { + // x: 1, + // y: 10, + // }, + // }), + // ); + // }); + // }); + // describe('Multi series bar chart - time', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1546300800000, 10], + // [1546387200000, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1546300800000, 20], + // [1546387200000, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 5, + // mark: null, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 20, + // mark: null, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 10, + // mark: null, + // }, + // }), + // ); + // }); + // }); + // describe('Remove points datum is not in domain', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 0], + // [1, 1], + // [2, 10], + // [3, 3], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const customYDomain = new Map(); + // customYDomain.set(GROUP_ID, { + // max: 1, + // }); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain, [], { + // max: 2, + // }); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render 3 bars', () => { + // const { barGeometries, indexedGeometryMap } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(3); + // // will be cut by the clipping areas in the rendering component + // expect(barGeometries[2].height).toBe(1000); + // expect(indexedGeometryMap.size).toBe(3); + // }); + // }); + // describe('Renders minBarHeight', () => { + // const minBarHeight = 8; + // const data = [ + // [1, -100000], + // [2, -10000], + // [3, -1000], + // [4, -100], + // [5, -10], + // [6, -1], + // [7, 0], + // [8, -1], + // [9, 0], + // [10, 0], + // [11, 1], + // [12, 0], + // [13, 1], + // [14, 10], + // [15, 100], + // [16, 1000], + // [17, 10000], + // [18, 100000], + // ]; + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data, + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // minBarHeight, + // }; + // + // const customYDomain = new Map(); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // const expected = [-50, -8, -8, -8, -8, -8, 0, -8, 0, 0, 8, 0, 8, 8, 8, 8, 8, 50]; + // + // it('should render correct heights with positive minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // it('should render correct heights with negative minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // -minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts b/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts index 57727865c9..6f0116443e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bubble.test.ts @@ -17,81 +17,23 @@ * under the License. */ -import { ChartTypes } from '../..'; +import { MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; -import { BubbleGeometry, PointGeometry } from '../../../utils/geometry'; -import { GroupId } from '../../../utils/ids'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; -import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { BubbleSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; -import { renderBubble } from './rendering'; +import { Position } from '../../../utils/commons'; +import { PointGeometry } from '../../../utils/geometry'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering points - bubble', () => { - describe('Empty bubble for missing data', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - { ...pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], data: [] }, - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - }); - test('Can render the geometry without a bubble', () => { - const { bubbleGeometry } = renderedBubble; - expect(bubbleGeometry.points).toHaveLength(0); - expect(bubbleGeometry.color).toBe('red'); - expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - }); - }); describe('Single series bubble chart - ordinal', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const spec = MockSeriesSpec.bubble({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 10], [1, 5], @@ -100,109 +42,78 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([spec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); test('Can render a bubble', () => { - const { bubbleGeometry } = renderedBubble; + const [{ value: bubbleGeometry }] = bubbles; expect(bubbleGeometry.points).toHaveLength(2); expect(bubbleGeometry.color).toBe('red'); expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); }); test('Can render two points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = renderedBubble; - - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + { + value: { points }, }, - styleOverrides: undefined, - value: { - accessor: 'y1', + ] = bubbles; + + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(spec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series bubble chart - ordinal', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.bubble({ id: spec1Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 10], [1, 5], @@ -211,13 +122,10 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 20], [1, 10], @@ -226,185 +134,130 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - firstBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - secondBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); test('Can render two ordinal bubbles', () => { - expect(firstBubble.bubbleGeometry.points).toHaveLength(2); - expect(firstBubble.bubbleGeometry.color).toBe('red'); - expect(firstBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(spec1Id); + const [{ value: firstBubble }, { value: secondBubble }] = bubbles; + expect(firstBubble.points).toHaveLength(2); + expect(firstBubble.color).toBe('red'); + expect(firstBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstBubble.seriesIdentifier.specId).toEqual(spec1Id); - expect(secondBubble.bubbleGeometry.points).toHaveLength(2); - expect(secondBubble.bubbleGeometry.color).toBe('blue'); - expect(secondBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondBubble.points).toHaveLength(2); + expect(secondBubble.color).toBe('blue'); + expect(secondBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondBubble.seriesIdentifier.specId).toEqual(spec2Id); + expect(geometriesIndex.size).toEqual(4); }); test('can render first spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = firstBubble; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + { + value: { points }, }, - value: { - accessor: 'y1', + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 75, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); }); test('can render second spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = secondBubble; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + , + { + value: { points }, }, - value: { - accessor: 'y1', + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 50, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], - }, - transform: { - x: 25, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); }); }); describe('Single series bubble chart - linear', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.bubble({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 10], [1, 5], @@ -413,107 +266,77 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - }); test('Can render a linear bubble', () => { - expect(renderedBubble.bubbleGeometry.points).toHaveLength(2); - expect(renderedBubble.bubbleGeometry.color).toBe('red'); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + const [{ value: bubbleGeometry }] = bubbles; + expect(bubbleGeometry.points).toHaveLength(2); + expect(bubbleGeometry.color).toBe('red'); + expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); + expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); }); test('Can render two points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = renderedBubble; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], + const [ + { + value: { points }, }, - transform: { + ] = bubbles; + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series bubble chart - linear', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.bubble({ id: spec1Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 10], [1, 5], @@ -522,13 +345,10 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 20], [1, 10], @@ -537,184 +357,131 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let firstBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - firstBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - secondBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - }); test('can render two linear bubbles', () => { - expect(firstBubble.bubbleGeometry.points).toHaveLength(2); - expect(firstBubble.bubbleGeometry.color).toBe('red'); - expect(firstBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(spec1Id); + const [{ value: firstBubble }, { value: secondBubble }] = bubbles; + expect(firstBubble.points).toHaveLength(2); + expect(firstBubble.color).toBe('red'); + expect(firstBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstBubble.seriesIdentifier.specId).toEqual(spec1Id); - expect(secondBubble.bubbleGeometry.points).toHaveLength(2); - expect(secondBubble.bubbleGeometry.color).toBe('blue'); - expect(secondBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondBubble.points).toHaveLength(2); + expect(secondBubble.color).toBe('blue'); + expect(secondBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondBubble.seriesIdentifier.specId).toEqual(spec2Id); + expect(geometriesIndex.size).toEqual(4); }); test('can render first spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = firstBubble; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], + const [ + { + value: { points }, }, - transform: { + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); }); test('can render second spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = secondBubble; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], + const [ + , + { + value: { points }, }, - transform: { + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); }); }); describe('Single series bubble chart - time', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.bubble({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [1546300800000, 10], [1546387200000, 5], @@ -723,107 +490,77 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - }); test('Can render a time bubble', () => { - expect(renderedBubble.bubbleGeometry.points).toHaveLength(2); - expect(renderedBubble.bubbleGeometry.color).toBe('red'); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + const [{ value: renderedBubble }] = bubbles; + expect(renderedBubble.points).toHaveLength(2); + expect(renderedBubble.color).toBe('red'); + expect(renderedBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedBubble.seriesIdentifier.specId).toEqual(SPEC_ID); }); test('Can render two points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = renderedBubble; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], + const [ + { + value: { points }, }, - transform: { + ] = bubbles; + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series bubble chart - time', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.bubble({ id: spec1Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [1546300800000, 10], [1546387200000, 5], @@ -832,13 +569,10 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.bubble({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [1546300800000, 20], [1546387200000, 10], @@ -847,173 +581,117 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - firstBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - secondBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - false, - LIGHT_THEME.bubbleSeriesStyle, + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('can render first spec points', () => { + const [ { - enabled: false, + value: { points }, }, - false, - ); - }); - test('can render first spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = firstBubble; + ] = bubbles; expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - transform: { + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(4); }); test('can render second spec points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = secondBubble; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 20, - mark: null, - datum: [1546300800000, 20], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 10, - mark: null, - datum: [1546387200000, 10], + const [ + , + { + value: { points }, }, - transform: { + ] = bubbles; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + transform: { + x: 0, + y: 0, + }, + }), + ); }); }); describe('Single series bubble chart - y log', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.bubble({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 10], [1, 5], @@ -1029,53 +707,34 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Log, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 90], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedBubble = renderBubble( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, - ); - }); test('Can render a splitted bubble', () => { - expect(renderedBubble.bubbleGeometry.points).toHaveLength(7); - expect(renderedBubble.bubbleGeometry.color).toBe('red'); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedBubble.bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); + const [{ value: renderedBubble }] = bubbles; + expect(renderedBubble.points).toHaveLength(7); + expect(renderedBubble.color).toBe('red'); + expect(renderedBubble.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedBubble.seriesIdentifier.specId).toEqual(SPEC_ID); }); test('Can render points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = renderedBubble; + const [ + { + value: { points }, + }, + ] = bubbles; // all the points minus the undefined ones on a log scale expect(points.length).toBe(7); // all the points expect null geometries - expect(indexedGeometryMap.size).toEqual(8); + expect(geometriesIndex.size).toEqual(8); - const zeroValueIndexdGeometry = indexedGeometryMap.find(null, { + const zeroValueIndexdGeometry = geometriesIndex.find(null, { x: 56.25, y: 100, }); @@ -1089,12 +748,8 @@ describe('Rendering points - bubble', () => { }); }); describe('Remove points datum is not in domain', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.bubble({ id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, data: [ [0, 0], [1, 1], @@ -1105,175 +760,60 @@ describe('Rendering points - bubble', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const customYDomain = new Map(); - customYDomain.set(GROUP_ID, { - max: 1, - }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec], customYDomain, [], { - max: 2, - }); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { max: 2 }, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }); + const axis = MockGlobalSpec.axis({ position: Position.Left, hide: true, domain: { max: 1 } }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs([pointSeriesSpec, axis, settings], store); + const { + geometries: { bubbles }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('Can render two points', () => { + const [ { - enabled: false, + value: { points }, }, - false, - ); - }); - test('Can render two points', () => { - const { - bubbleGeometry: { points }, - indexedGeometryMap, - } = renderedBubble; + ] = bubbles; // will not render the 3rd point that is out of y domain expect(points.length).toBe(2); // will keep the 3rd point as an indexedGeometry - expect(indexedGeometryMap.size).toEqual(3); - expect(points[0]).toEqual(({ - x: 0, - y: 100, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', + expect(geometriesIndex.size).toEqual(3); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, + y: 100, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: [0, 0], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, y: 0, - mark: null, - datum: [0, 0], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 1, - mark: null, - datum: [1, 1], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - }); - }); - - describe('Error guards for scaled values', () => { - const pointSeriesSpec: BubbleSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bubble, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedBubble: { - bubbleGeometry: BubbleGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedBubble = renderBubble( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - false, - LIGHT_THEME.bubbleSeriesStyle, - { - enabled: false, - }, - false, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 1, + mark: null, + datum: [1, 1], + }, + }), ); }); - - describe('xScale values throw error', () => { - beforeAll(() => { - jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - it('Should have empty bubble', () => { - const { bubbleGeometry } = renderedBubble; - expect(bubbleGeometry.points).toHaveLength(2); - expect(bubbleGeometry.color).toBe('red'); - expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - }); - }); - - describe('yScale values throw error', () => { - beforeAll(() => { - jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - it('Should have empty bubble', () => { - const { bubbleGeometry } = renderedBubble; - expect(bubbleGeometry.points).toHaveLength(2); - expect(bubbleGeometry.color).toBe('red'); - expect(bubbleGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(bubbleGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - }); - }); }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts index 1fd525240f..c6772af436 100644 --- a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts @@ -17,84 +17,24 @@ * under the License. */ -import { ChartTypes } from '../..'; +import { MockPointGeometry } from '../../../mocks'; +import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; -import { CurveType } from '../../../utils/curves'; -import { LineGeometry, PointGeometry } from '../../../utils/geometry'; -import { GroupId } from '../../../utils/ids'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; -import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { LineSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; -import { renderLine } from './rendering'; +import { Position } from '../../../utils/commons'; +import { PointGeometry } from '../../../utils/geometry'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; +import { SeriesTypes } from '../utils/specs'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering points - line', () => { - describe('Empty line for missing data', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Line, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - { ...pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], data: [] }, - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); - test('Can render the geometry without a line', () => { - const { lineGeometry } = renderedLine; - expect(lineGeometry.line).toBe(''); - expect(lineGeometry.color).toBe('red'); - expect(lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(lineGeometry.transform).toEqual({ x: 25, y: 0 }); - }); - }); describe('Single series line chart - ordinal', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.line({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 10], [1, 5], @@ -103,38 +43,17 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('Can render a line', () => { - const { lineGeometry } = renderedLine; + const [{ value: lineGeometry }] = lines; expect(lineGeometry.line).toBe('M0,0L50,50'); expect(lineGeometry.color).toBe('red'); expect(lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); @@ -142,69 +61,60 @@ describe('Rendering points - line', () => { expect(lineGeometry.transform).toEqual({ x: 25, y: 0 }); }); test('Can render two points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = renderedLine; - - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + { + value: { points }, }, - styleOverrides: undefined, - value: { - accessor: 'y1', + ] = lines; + + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series line chart - ordinal', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.line({ id: spec1Id, groupId: GROUP_ID, seriesType: SeriesTypes.Line, @@ -216,13 +126,10 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 20], [1, 10], @@ -231,189 +138,133 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let firstLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - firstLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); test('Can render two ordinal lines', () => { - expect(firstLine.lineGeometry.line).toBe('M0,50L50,75'); - expect(firstLine.lineGeometry.color).toBe('red'); - expect(firstLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.lineGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.lineGeometry.transform).toEqual({ x: 25, y: 0 }); + const [{ value: firstLine }, { value: secondLine }] = lines; + expect(firstLine.color).toBe('red'); + expect(firstLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstLine.seriesIdentifier.specId).toEqual(spec1Id); + expect(firstLine.transform).toEqual({ x: 25, y: 0 }); - expect(secondLine.lineGeometry.line).toBe('M0,0L50,50'); - expect(secondLine.lineGeometry.color).toBe('blue'); - expect(secondLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.lineGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.lineGeometry.transform).toEqual({ x: 25, y: 0 }); + expect(secondLine.line).toBe('M0,0L50,50'); + expect(secondLine.color).toBe('blue'); + expect(secondLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondLine.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondLine.transform).toEqual({ x: 25, y: 0 }); }); test('can render first spec points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + { + value: { points }, }, - value: { - accessor: 'y1', + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 75, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); test('can render second spec points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], + const [ + , + { + value: { points }, }, - value: { - accessor: 'y1', + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 50, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], - }, - transform: { - x: 25, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Single series line chart - linear', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.line({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 10], [1, 5], @@ -422,109 +273,71 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('Can render a linear line', () => { - expect(renderedLine.lineGeometry.line).toBe('M0,0L100,50'); - expect(renderedLine.lineGeometry.color).toBe('red'); - expect(renderedLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedLine.lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedLine.lineGeometry.transform).toEqual({ x: 0, y: 0 }); + const [{ value: renderedLine }] = lines; + expect(renderedLine.line).toBe('M0,0L100,50'); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = renderedLine; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - color: 'red', - radius: 0, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], + const [ + { + value: { points }, }, - transform: { + ] = lines; + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'red', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series line chart - linear', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.line({ id: spec1Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 10], [1, 5], @@ -533,13 +346,10 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 20], [1, 10], @@ -548,188 +358,117 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - let firstLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - firstLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('can render two linear lines', () => { - expect(firstLine.lineGeometry.line).toBe('M0,50L100,75'); - expect(firstLine.lineGeometry.color).toBe('red'); - expect(firstLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.lineGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.lineGeometry.transform).toEqual({ x: 0, y: 0 }); + const [{ value: firstLine }, { value: secondLine }] = lines; + expect(firstLine.line).toBe('M0,50L100,75'); + expect(firstLine.color).toBe('red'); + expect(firstLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstLine.seriesIdentifier.specId).toEqual(spec1Id); + expect(firstLine.transform).toEqual({ x: 0, y: 0 }); - expect(secondLine.lineGeometry.line).toBe('M0,0L100,50'); - expect(secondLine.lineGeometry.color).toBe('blue'); - expect(secondLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.lineGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.lineGeometry.transform).toEqual({ x: 0, y: 0 }); + expect(secondLine.line).toBe('M0,0L100,50'); + expect(secondLine.color).toBe('blue'); + expect(secondLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondLine.seriesIdentifier.specId).toEqual(spec2Id); + expect(secondLine.transform).toEqual({ x: 0, y: 0 }); }); test('can render first spec points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], + const [ + { + value: { points }, }, - transform: { + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); test('can render second spec points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - color: 'blue', - radius: 0, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], + const [ + , + { + value: { points }, }, - transform: { + ] = lines; + expect(points.length).toEqual(2); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + color: 'blue', + radius: 0, + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Single series line chart - time', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.line({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [1546300800000, 10], [1546387200000, 5], @@ -738,109 +477,71 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - renderedLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('Can render a time line', () => { - expect(renderedLine.lineGeometry.line).toBe('M0,0L100,50'); - expect(renderedLine.lineGeometry.color).toBe('red'); - expect(renderedLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedLine.lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedLine.lineGeometry.transform).toEqual({ x: 0, y: 0 }); + const [{ value: renderedLine }] = lines; + expect(renderedLine.line).toBe('M0,0L100,50'); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = renderedLine; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], + const [ + { + value: { points }, }, - transform: { + ] = lines; + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series line chart - time', () => { const spec1Id = 'point1'; const spec2Id = 'point2'; - const pointSeriesSpec1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec1 = MockSeriesSpec.line({ id: spec1Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [1546300800000, 10], [1546387200000, 5], @@ -849,13 +550,10 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const pointSeriesSpec2 = MockSeriesSpec.line({ id: spec2Id, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [1546300800000, 20], [1546387200000, 10], @@ -864,175 +562,100 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec1, pointSeriesSpec2, settings], store); + const { + geometries: { + lines: [firstLine, secondLine], + }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - beforeEach(() => { - firstLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('can render first spec points', () => { const { - lineGeometry: { points }, - indexedGeometryMap, + value: { points }, } = firstLine; expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - mark: null, - datum: [1546300800000, 10], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: spec1Id, - key: 'spec{point1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - transform: { + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546300800000, + y: 10, + mark: null, + datum: [1546300800000, 10], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec1), + value: { + accessor: 'y1', + x: 1546387200000, + y: 5, + mark: null, + datum: [1546387200000, 5], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); test('can render second spec points', () => { const { - lineGeometry: { points }, - indexedGeometryMap, + value: { points }, } = secondLine; expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546300800000, - y: 20, - mark: null, - datum: [1546300800000, 20], - }, - transform: { - x: 0, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 100, - y: 50, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: spec2Id, - key: 'spec{point2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1546387200000, - y: 10, - mark: null, - datum: [1546387200000, 10], - }, - transform: { + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, y: 0, - }, - } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546300800000, + y: 20, + mark: null, + datum: [1546300800000, 20], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 100, + y: 50, + radius: 0, + color: 'blue', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec2), + value: { + accessor: 'y1', + x: 1546387200000, + y: 10, + mark: null, + datum: [1546387200000, 10], + }, + }), + ); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Single series line chart - y log', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.line({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesTypes.Line, data: [ [0, 10], [1, 5], @@ -1048,58 +671,37 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Log, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 90], }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + const store = MockStore.default({ width: 90, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }); + MockStore.addSpecs([pointSeriesSpec, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedLine = renderLine( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, - ); - }); test('Can render a splitted line', () => { - // expect(renderedLine.lineGeometry.line).toBe('ss'); - expect(renderedLine.lineGeometry.line.split('M').length - 1).toBe(3); - expect(renderedLine.lineGeometry.color).toBe('red'); - expect(renderedLine.lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedLine.lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedLine.lineGeometry.transform).toEqual({ x: 0, y: 0 }); + const [{ value: renderedLine }] = lines; + expect(renderedLine.line.split('M').length - 1).toBe(3); + expect(renderedLine.color).toBe('red'); + expect(renderedLine.seriesIdentifier.seriesKeys).toEqual([1]); + expect(renderedLine.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(renderedLine.transform).toEqual({ x: 0, y: 0 }); }); test('Can render points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = renderedLine; + const [ + { + value: { points }, + }, + ] = lines; // all the points minus the undefined ones on a log scale expect(points.length).toBe(7); // all the points expect null geometries - expect(indexedGeometryMap.size).toEqual(8); - const nullIndexdGeometry = indexedGeometryMap.find(2)!; + expect(geometriesIndex.size).toEqual(8); + const nullIndexdGeometry = geometriesIndex.find(2)!; expect(nullIndexdGeometry).toEqual([]); - const zeroValueIndexdGeometry = indexedGeometryMap.find(5)!; + const zeroValueIndexdGeometry = geometriesIndex.find(5)!; expect(zeroValueIndexdGeometry).toBeDefined(); expect(zeroValueIndexdGeometry.length).toBe(1); // moved to the bottom of the chart @@ -1109,12 +711,9 @@ describe('Rendering points - line', () => { }); }); describe('Remove points datum is not in domain', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const pointSeriesSpec = MockSeriesSpec.line({ id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Line, + // groupId: GROUP_ID, data: [ [0, 0], [1, 1], @@ -1125,179 +724,60 @@ describe('Rendering points - line', () => { yAccessors: [1], xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, - }; - const customYDomain = new Map(); - customYDomain.set(GROUP_ID, { - max: 1, - }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec], customYDomain, [], { - max: 2, }); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { max: 2 }, + theme: { colors: { vizColors: ['red', 'blue'] } }, + }); + const axis = MockGlobalSpec.axis({ position: Position.Left, hide: true, domain: { max: 1 } }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs([pointSeriesSpec, axis, settings], store); + const { + geometries: { lines }, + geometriesIndex, + } = computeSeriesGeometriesSelector(store.getState()); + test('Can render two points', () => { + const [ { - enabled: false, + value: { points }, }, - ); - }); - test('Can render two points', () => { - const { - lineGeometry: { points }, - indexedGeometryMap, - } = renderedLine; + ] = lines; // will not render the 3rd point that is out of y domain expect(points.length).toBe(2); // will keep the 3rd point as an indexedGeometry - expect(indexedGeometryMap.size).toEqual(3); - expect(points[0]).toEqual(({ - x: 0, - y: 100, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', + expect(geometriesIndex.size).toEqual(3); + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, + y: 100, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 0, + y: 0, + mark: null, + datum: [0, 0], + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, y: 0, - mark: null, - datum: [0, 0], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - value: { - accessor: 'y1', - x: 1, - y: 1, - mark: null, - datum: [1, 1], - }, - transform: { - x: 25, - y: 0, - }, - } as unknown) as PointGeometry); - }); - }); - - describe('Error guards for scaled values', () => { - const pointSeriesSpec: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Line, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedLine: { - lineGeometry: LineGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedLine = renderLine( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.lineSeriesStyle, - { - enabled: false, - }, + radius: 0, + color: 'red', + seriesIdentifier: MockSeriesIdentifier.fromSpec(pointSeriesSpec), + value: { + accessor: 'y1', + x: 1, + y: 1, + mark: null, + datum: [1, 1], + }, + }), ); }); - - describe('xScale values throw error', () => { - beforeAll(() => { - jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - it('Should have empty line', () => { - const { lineGeometry } = renderedLine; - expect(lineGeometry.line).toBe(''); - expect(lineGeometry.color).toBe('red'); - expect(lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(lineGeometry.transform).toEqual({ x: 25, y: 0 }); - }); - }); - - describe('yScale values throw error', () => { - beforeAll(() => { - jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - it('Should have empty line', () => { - const { lineGeometry } = renderedLine; - expect(lineGeometry.line).toBe(''); - expect(lineGeometry.color).toBe('red'); - expect(lineGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(lineGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(lineGeometry.transform).toEqual({ x: 25, y: 0 }); - }); - }); }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.test.ts b/src/chart_types/xy_chart/rendering/rendering.test.ts index 9a33546ae2..d41f737711 100644 --- a/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -18,10 +18,9 @@ */ import { LegendItem } from '../../../commons/legend'; -import { MockDataSeries } from '../../../mocks'; +import { MockBarGeometry, MockDataSeries, MockPointGeometry } from '../../../mocks'; import { MockScale } from '../../../mocks/scale'; import { mergePartial, RecursivePartial } from '../../../utils/commons'; -import { BarGeometry, PointGeometry } from '../../../utils/geometry'; import { BarSeriesStyle, SharedGeometryStateStyle, PointStyle } from '../../../utils/themes/theme'; import { DataSeriesDatum, XYChartSeriesIdentifier } from '../utils/series'; import { @@ -53,7 +52,7 @@ describe('Rendering utils', () => { }, }; - const geometry: BarGeometry = { + const geometry = MockBarGeometry.default({ color: 'red', seriesIdentifier: { specId: 'id', @@ -74,7 +73,7 @@ describe('Rendering utils', () => { width: 10, height: 10, seriesStyle, - }; + }); expect(isPointOnGeometry(0, 0, geometry)).toBe(true); expect(isPointOnGeometry(10, 10, geometry)).toBe(true); expect(isPointOnGeometry(0, 10, geometry)).toBe(true); @@ -84,7 +83,7 @@ describe('Rendering utils', () => { expect(isPointOnGeometry(11, 11, geometry)).toBe(false); }); test('check if point is on point geometry', () => { - const geometry: PointGeometry = { + const geometry = MockPointGeometry.default({ color: 'red', seriesIdentifier: { specId: 'id', @@ -107,7 +106,7 @@ describe('Rendering utils', () => { x: 0, y: 0, radius: 10, - }; + }); // with buffer expect(isPointOnGeometry(10, 10, geometry, 10)).toBe(true); expect(isPointOnGeometry(20, 20, geometry, 5)).toBe(false); @@ -169,36 +168,36 @@ describe('Rendering utils', () => { }; it('no highlighted elements', () => { - const defaultStyle = getGeometryStateStyle(seriesIdentifier, null, sharedThemeStyle); + const defaultStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle); expect(defaultStyle).toBe(sharedThemeStyle.default); }); it('should equal highlighted opacity', () => { - const highlightedStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedThemeStyle); + const highlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, highlightedLegendItem); expect(highlightedStyle).toBe(sharedThemeStyle.highlighted); }); it('should equal unhighlighted when not highlighted item', () => { - const unhighlightedStyle = getGeometryStateStyle(seriesIdentifier, unhighlightedLegendItem, sharedThemeStyle); + const unhighlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, unhighlightedLegendItem); expect(unhighlightedStyle).toBe(sharedThemeStyle.unhighlighted); }); it('should equal custom spec highlighted opacity', () => { - const customHighlightedStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedThemeStyle); + const customHighlightedStyle = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, highlightedLegendItem); expect(customHighlightedStyle).toBe(sharedThemeStyle.highlighted); }); it('unhighlighted elements remain unchanged with custom opacity', () => { const customUnhighlightedStyle = getGeometryStateStyle( seriesIdentifier, - unhighlightedLegendItem, sharedThemeStyle, + unhighlightedLegendItem, ); expect(customUnhighlightedStyle).toBe(sharedThemeStyle.unhighlighted); }); it('has individual highlight', () => { - const hasIndividualHighlight = getGeometryStateStyle(seriesIdentifier, null, sharedThemeStyle, { + const hasIndividualHighlight = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { hasHighlight: true, hasGeometryHover: true, }); @@ -206,7 +205,7 @@ describe('Rendering utils', () => { }); it('no highlight', () => { - const noHighlight = getGeometryStateStyle(seriesIdentifier, null, sharedThemeStyle, { + const noHighlight = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { hasHighlight: false, hasGeometryHover: true, }); @@ -214,7 +213,7 @@ describe('Rendering utils', () => { }); it('no geometry hover', () => { - const noHover = getGeometryStateStyle(seriesIdentifier, null, sharedThemeStyle, { + const noHover = getGeometryStateStyle(seriesIdentifier, sharedThemeStyle, undefined, { hasHighlight: true, hasGeometryHover: false, }); diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 805e89ffc4..665470cc96 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -27,6 +27,7 @@ import { MarkBuffer, StackMode } from '../../../specs'; import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; import { mergePartial, Color, getDistance } from '../../../utils/commons'; import { CurveType, getCurveFactory } from '../../../utils/curves'; +import { Dimensions } from '../../../utils/dimensions'; import { PointGeometry, BarGeometry, @@ -227,13 +228,13 @@ function renderPoints( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, lineStyle: LineStyle, hasY0Accessors: boolean, markSizeOptions: MarkSizeOptions, styleAccessor?: PointStyleAccessor, spatial = false, - stackMode?: StackMode, ): { pointGeometries: PointGeometry[]; indexedGeometryMap: IndexedGeometryMap; @@ -281,13 +282,15 @@ function renderPoints( if (y === null) { return acc; } - const originalY = getDatumYValue(datum, index === 0, hasY0Accessors, stackMode); + const originalY = getDatumYValue(datum, index === 0, hasY0Accessors, dataSeries.stackMode); const seriesIdentifier: XYChartSeriesIdentifier = { key: dataSeries.key, specId: dataSeries.specId, yAccessor: dataSeries.yAccessor, splitAccessors: dataSeries.splitAccessors, seriesKeys: dataSeries.seriesKeys, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, }; const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, styleAccessor); const pointGeometry: PointGeometry = { @@ -308,6 +311,7 @@ function renderPoints( }, seriesIdentifier, styleOverrides, + panel, }; indexedGeometryMap.set(pointGeometry, geometryType); // use the geometry only if the yDatum in contained in the current yScale domain @@ -362,6 +366,7 @@ export function renderBars( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, sharedSeriesStyle: BarSeriesStyle, displayValueSettings?: DisplayValueSpec, @@ -416,7 +421,6 @@ export function renderBars( if (y === null || y0Scaled === null) { return; } - let height = y0Scaled - y; // handle minBarHeight adjustment @@ -492,6 +496,8 @@ export function renderBars( yAccessor: dataSeries.yAccessor, splitAccessors: dataSeries.splitAccessors, seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }; const seriesStyle = getBarStyleOverrides(datum, seriesIdentifier, sharedSeriesStyle, styleAccessor); @@ -499,7 +505,11 @@ export function renderBars( const barGeometry: BarGeometry = { displayValue, x, - y, // top most value + y, + transform: { + x: 0, + y: 0, + }, width, height, color, @@ -512,6 +522,7 @@ export function renderBars( }, seriesIdentifier, seriesStyle, + panel, }; indexedGeometryMap.set(barGeometry); barGeometries.push(barGeometry); @@ -531,6 +542,7 @@ export function renderLine( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, curve: CurveType, hasY0Accessors: boolean, @@ -544,7 +556,6 @@ export function renderLine( indexedGeometryMap: IndexedGeometryMap; } { const isLogScale = isLogarithmicScale(yScale); - const pathGenerator = line() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y((datum) => { @@ -562,14 +573,13 @@ export function renderLine( return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x); }) .curve(getCurveFactory(curve)); - const y = 0; - const x = shift; const { pointGeometries, indexedGeometryMap } = renderPoints( shift - xScaleOffset, dataSeries, xScale, yScale, + panel, color, seriesStyle.line, hasY0Accessors, @@ -592,8 +602,8 @@ export function renderLine( points: pointGeometries, color, transform: { - x, - y, + x: shift, + y: 0, }, seriesIdentifier: { key: dataSeries.key, @@ -601,6 +611,8 @@ export function renderLine( yAccessor: dataSeries.yAccessor, splitAccessors: dataSeries.splitAccessors, seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }, seriesLineStyle: seriesStyle.line, seriesPointStyle: seriesStyle.point, @@ -620,7 +632,9 @@ export function renderBubble( xScale: Scale, yScale: Scale, color: Color, + panel: Dimensions, hasY0Accessors: boolean, + xScaleOffset: number, seriesStyle: BubbleSeriesStyle, markSizeOptions: MarkSizeOptions, isMixedChart: boolean, @@ -630,10 +644,11 @@ export function renderBubble( indexedGeometryMap: IndexedGeometryMap; } { const { pointGeometries, indexedGeometryMap } = renderPoints( - shift, + shift - xScaleOffset, dataSeries, xScale, yScale, + panel, color, seriesStyle.point, hasY0Accessors, @@ -651,6 +666,8 @@ export function renderBubble( yAccessor: dataSeries.yAccessor, splitAccessors: dataSeries.splitAccessors, seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }, seriesPointStyle: seriesStyle.point, }; @@ -667,6 +684,7 @@ export function renderArea( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, curve: CurveType, hasY0Accessors: boolean, @@ -676,12 +694,12 @@ export function renderArea( isStacked = false, pointStyleAccessor?: PointStyleAccessor, hasFit?: boolean, - stackMode?: StackMode, ): { areaGeometry: AreaGeometry; indexedGeometryMap: IndexedGeometryMap; } { const isLogScale = isLogarithmicScale(yScale); + const pathGenerator = area() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y1((datum) => { @@ -693,11 +711,7 @@ export function renderArea( return yScale.isInverted ? yScale.range[1] : yScale.range[0]; }) .y0(({ y0 }) => { - if (y0 === null || (isLogScale && y0 <= 0)) { - return yScale.range[0]; - } - - return yScale.scaleOrThrow(y0); + return y0 === null || (isLogScale && y0 <= 0) ? yScale.range[0] : yScale.scaleOrThrow(y0); }) .defined((datum) => { const yValue = getYValue(datum); @@ -739,13 +753,13 @@ export function renderArea( dataSeries, xScale, yScale, + panel, color, seriesStyle.line, hasY0Accessors, markSizeOptions, pointStyleAccessor, false, - stackMode, ); let areaPath: string; @@ -772,6 +786,8 @@ export function renderArea( yAccessor: dataSeries.yAccessor, splitAccessors: dataSeries.splitAccessors, seriesKeys: dataSeries.seriesKeys, + smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, + smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }, seriesAreaStyle: seriesStyle.area, seriesAreaLineStyle: seriesStyle.line, @@ -800,17 +816,18 @@ export function isDatumFilled({ filled, initialY1 }: DataSeriesDatum) { * @param dataset * @param xScale * @param xScaleOffset + * @param panel * @internal */ export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xScaleOffset: number): ClippedRanges { let firstNonNullX: number | null = null; let hasNull = false; - return dataset.reduce((acc, data) => { const xScaled = xScale.scale(data.x); if (xScaled === null) { return acc; } + const xValue = xScaled - xScaleOffset + xScale.bandwidth / 2; if (isDatumFilled(data)) { @@ -838,13 +855,13 @@ export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xSca /** @internal */ export function getGeometryStateStyle( seriesIdentifier: XYChartSeriesIdentifier, - highlightedLegendItem: LegendItem | null, sharedGeometryStyle: SharedGeometryStateStyle, + highlightedLegendItem?: LegendItem, individualHighlight?: { [key: string]: boolean }, ): GeometryStateStyle { const { default: defaultStyles, highlighted, unhighlighted } = sharedGeometryStyle; - if (highlightedLegendItem != null) { + if (highlightedLegendItem) { const isPartOfHighlightedSeries = seriesIdentifier.key === highlightedLegendItem.seriesIdentifier.key; return isPartOfHighlightedSeries ? highlighted : unhighlighted; @@ -870,17 +887,18 @@ export function isPointOnGeometry( ) { const { x, y } = indexedGeometry; if (isPointGeometry(indexedGeometry)) { - const { radius, transform } = indexedGeometry; + const { radius } = indexedGeometry; const distance = getDistance( { x: xCoordinate, y: yCoordinate, }, { - x: x + transform.x, + x, y, }, ); + const radiusBuffer = typeof buffer === 'number' ? buffer : buffer(radius); if (radiusBuffer === Infinity) { diff --git a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts index 746fcb23c5..8e07720558 100644 --- a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -165,7 +165,7 @@ describe('Chart state pointer interactions', () => { const { geometries } = computeSeriesGeometriesSelector(store.getState()); expect(geometries).toBeDefined(); expect(geometries.bars).toBeDefined(); - expect(geometries.bars.length).toBe(2); + expect(geometries.bars[0].value.length).toBe(2); }); test('can convert/limit mouse pointer positions relative to chart projection', () => { @@ -398,7 +398,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { expect(tooltipInfo.tooltip.values).toEqual([]); store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 0 }, 0)); let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 0, y: 0 }); + expect(projectedPointerPosition).toMatchObject({ x: 0, y: 0 }); const cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 0); @@ -420,18 +420,21 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [0, 10], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); store.dispatch(onPointerMove({ x: chartLeft - 1, y: chartTop - 1 }, 1)); projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: -1, y: -1 }); + expect(projectedPointerPosition).toMatchObject({ x: -1, y: -1 }); isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible.visible).toBe(false); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); @@ -444,7 +447,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { test('can hover bottom-left corner of the first bar', () => { store.dispatch(onPointerMove({ x: chartLeft + 0, y: chartTop + 89 }, 0)); let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 0, y: 89 }); + expect(projectedPointerPosition).toMatchObject({ x: 0, y: 89 }); const cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 0); @@ -466,17 +469,21 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [0, 10], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); store.dispatch(onPointerMove({ x: chartLeft - 1, y: chartTop + 89 }, 1)); projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: -1, y: 89 }); + expect(projectedPointerPosition).toMatchObject({ x: -1, y: 89 }); isTooltipVisible = isTooltipVisibleSelector(store.getState()); expect(isTooltipVisible.visible).toBe(false); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); @@ -493,7 +500,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { } store.dispatch(onPointerMove({ x: chartLeft + 44 + scaleOffset, y: chartTop + 0 }, 0)); let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 44 + scaleOffset, y: 0 }); + expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 0 }); let cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 0); @@ -515,18 +522,21 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [0, 10], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); store.dispatch(onPointerMove({ x: chartLeft + 45 + scaleOffset, y: chartTop + 0 }, 1)); projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 45 + scaleOffset, y: 0 }); + expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 0 }); cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 45); @@ -547,7 +557,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { } store.dispatch(onPointerMove({ x: chartLeft + 44 + scaleOffset, y: chartTop + 89 }, 0)); let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 44 + scaleOffset, y: 89 }); + expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 89 }); let cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 0); @@ -569,18 +579,21 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [(spec.data[0] as Array)[0], (spec.data[0] as Array)[1]], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); store.dispatch(onPointerMove({ x: chartLeft + 45 + scaleOffset, y: chartTop + 89 }, 1)); projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 45 + scaleOffset, y: 89 }); + expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 89 }); cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 45); @@ -602,11 +615,14 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [(spec.data[1] as Array)[0], (spec.data[1] as Array)[1]], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); @@ -625,7 +641,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { store.dispatch(onPointerMove({ x: chartLeft + 89, y: chartTop + 0 }, 0)); const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); - expect(projectedPointerPosition).toEqual({ x: 89, y: 0 }); + expect(projectedPointerPosition).toMatchObject({ x: 89, y: 0 }); const cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 45); @@ -678,7 +694,7 @@ function mouseOverTestSuite(scaleType: XScaleType) { store.dispatch(onPointerMove({ x: chartLeft + 89, y: chartTop + 89 }, 0)); const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); // store.setCursorPosition(chartLeft + 99, chartTop + 99); - expect(projectedPointerPosition).toEqual({ x: 89, y: 89 }); + expect(projectedPointerPosition).toMatchObject({ x: 89, y: 89 }); const cursorBandPosition = getCursorBandPositionSelector(store.getState()); expect(cursorBandPosition).toBeDefined(); expect(cursorBandPosition?.left).toBe(chartLeft + 45); @@ -699,11 +715,14 @@ function mouseOverTestSuite(scaleType: XScaleType) { datum: [1, 5], }, { - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', seriesKeys: [1], specId: 'spec_1', splitAccessors: new Map(), yAccessor: 1, + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', }, ], ]); diff --git a/src/chart_types/xy_chart/state/chart_state.test.ts b/src/chart_types/xy_chart/state/chart_state.test.ts index f1e288419c..9aaa1a3fc2 100644 --- a/src/chart_types/xy_chart/state/chart_state.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.test.ts @@ -19,12 +19,13 @@ import { ChartTypes } from '../..'; import { LegendItem } from '../../../commons/legend'; +import { MockBarGeometry } from '../../../mocks'; import { ScaleContinuous, ScaleBand } from '../../../scales'; import { ScaleType } from '../../../scales/constants'; import { SpecTypes, TooltipType } from '../../../specs/constants'; import { TooltipValue } from '../../../specs/settings'; import { Position, RecursivePartial } from '../../../utils/commons'; -import { IndexedGeometry, GeometryValue, BandedAccessorType } from '../../../utils/geometry'; +import { GeometryValue, BandedAccessorType } from '../../../utils/geometry'; import { AxisId } from '../../../utils/ids'; import { AxisStyle } from '../../../utils/themes/theme'; import { AxisTicksDimensions, isDuplicateAxis } from '../utils/axis_utils'; @@ -120,6 +121,7 @@ describe.skip('Chart Store', () => { maxLabelBboxHeight: 1, maxLabelTextWidth: 1, maxLabelTextHeight: 1, + isHidden: false, }; let tickMap: Map; let specMap: AxisSpec[]; @@ -911,7 +913,7 @@ describe.skip('Chart Store', () => { padding: 2, }, }; - const geom1: IndexedGeometry = { + const geom1 = MockBarGeometry.default({ color: 'red', seriesIdentifier: { specId: 'specId1', @@ -932,8 +934,8 @@ describe.skip('Chart Store', () => { width: 0, height: 0, seriesStyle: barStyle, - }; - const geom2: IndexedGeometry = { + }); + const geom2 = MockBarGeometry.default({ color: 'blue', seriesIdentifier: { specId: 'specId2', @@ -954,7 +956,7 @@ describe.skip('Chart Store', () => { width: 0, height: 0, seriesStyle: barStyle, - }; + }); const clickListener = jest.fn((): void => undefined); store.setOnElementClickListener(clickListener); @@ -1129,7 +1131,7 @@ describe.skip('Chart Store', () => { store.cursorPosition.x = 10; store.cursorPosition.y = 10; store.onBrushEndListener = brushEndListener; - const geom1: IndexedGeometry = { + const geom1 = MockBarGeometry.default({ color: 'red', seriesIdentifier: { specId: 'specId1', @@ -1166,7 +1168,7 @@ describe.skip('Chart Store', () => { padding: 2, }, }, - }; + }); store.highlightedGeometries.replace([geom1]); expect(store.chartCursor.get()).toBe('crosshair'); store.onElementClickListener = jest.fn(); diff --git a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts index 282963a0d3..bb4be94f85 100644 --- a/src/chart_types/xy_chart/state/chart_state.timescales.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.timescales.test.ts @@ -85,7 +85,7 @@ describe('Render chart', () => { expect(geometries).toBeDefined(); expect(geometries.lines).toBeDefined(); expect(geometries.lines.length).toBe(1); - expect(geometries.lines[0].points.length).toBe(3); + expect(geometries.lines[0].value.points.length).toBe(3); }); test('check mouse position correctly return inverted value', () => { store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip @@ -159,7 +159,7 @@ describe('Render chart', () => { expect(geometries).toBeDefined(); expect(geometries.lines).toBeDefined(); expect(geometries.lines.length).toBe(1); - expect(geometries.lines[0].points.length).toBe(3); + expect(geometries.lines[0].value.points.length).toBe(3); }); test('check mouse position correctly return inverted value', () => { store.dispatch(onPointerMove({ x: 15, y: 10 }, 0)); // check first valid tooltip @@ -232,7 +232,7 @@ describe('Render chart', () => { expect(geometries).toBeDefined(); expect(geometries.lines).toBeDefined(); expect(geometries.lines.length).toBe(1); - expect(geometries.lines[0].points.length).toBe(3); + expect(geometries.lines[0].value.points.length).toBe(3); }); test('check scale values', () => { const xValues = [date1, date2, date3]; diff --git a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts index beea55da97..b151d68aa9 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts @@ -26,6 +26,7 @@ import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensions } from '../../annotations/utils'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; @@ -38,6 +39,7 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( computeSeriesGeometriesSelector, getAxisSpecsSelector, isHistogramModeEnabledSelector, + computeSmallMultipleScalesSelector, ], ( annotationSpecs, @@ -46,6 +48,7 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( { scales: { yScales, xScale } }, axesSpecs, isHistogramMode, + smallMultipleScales, ): Map => computeAnnotationDimensions( annotationSpecs, @@ -55,5 +58,6 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( xScale, axesSpecs, isHistogramMode, + smallMultipleScales, ), )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts b/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts similarity index 82% rename from src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts rename to src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts index 8e764b0386..2d1640e86a 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts @@ -22,12 +22,12 @@ import createCachedSelector from 're-reselect'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Dimensions } from '../../../../utils/dimensions'; -import { AxisId } from '../../../../utils/ids'; -import { getAxisTicksPositions, AxisTick, AxisLinePosition, defaultTickFormatter } from '../../utils/axis_utils'; +import { getAxesGeometries, AxisGeometry, defaultTickFormatter } from '../../utils/axis_utils'; +import { getPanelSize } from '../../utils/panel'; import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { getAxesStylesSelector } from './get_axis_styles'; import { getBarPaddingsSelector } from './get_bar_paddings'; @@ -35,14 +35,7 @@ import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; /** @internal */ -export interface AxisVisibleTicks { - axisPositions: Map; - axisTicks: Map; - axisVisibleTicks: Map; - axisGridLinesPositions: Map; -} -/** @internal */ -export const computeAxisVisibleTicksSelector = createCachedSelector( +export const computeAxesGeometriesSelector = createCachedSelector( [ computeChartDimensionsSelector, getChartThemeSelector, @@ -55,6 +48,7 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( isHistogramModeEnabledSelector, getBarPaddingsSelector, getSeriesSpecsSelector, + computeSmallMultipleScalesSelector, ], ( chartDimensions, @@ -68,10 +62,13 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( isHistogramMode, barsPadding, seriesSpecs, - ): AxisVisibleTicks => { + scales, + ): AxisGeometry[] => { const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; const { xDomain, yDomain } = seriesDomainsAndData; - return getAxisTicksPositions( + const panel = getPanelSize(scales); + + return getAxesGeometries( chartDimensions, chartTheme, settingsSpec.rotation, @@ -80,6 +77,7 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( axesStyles, xDomain, yDomain, + panel, totalBarsInCluster, isHistogramMode, fallBackTickFormatter, diff --git a/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts b/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts new file mode 100644 index 0000000000..646bdf2747 --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/compute_grid_lines.ts @@ -0,0 +1,35 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getGridLines, LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computePerPanelGridLinesSelector = createCachedSelector( + [getAxisSpecsSelector, getChartThemeSelector, computeAxesGeometriesSelector, computeSmallMultipleScalesSelector], + (axesSpecs, chartTheme, axesGeoms, scales): Array => { + return getGridLines(axesSpecs, axesGeoms, chartTheme.axes, scales); + }, +)(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_panels.ts b/src/chart_types/xy_chart/state/selectors/compute_panels.ts new file mode 100644 index 0000000000..13263523a7 --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/compute_panels.ts @@ -0,0 +1,38 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { Size } from '../../../../utils/dimensions'; +import { getPanelSize } from '../../utils/panel'; +import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; + +/** @internal */ +export type PanelGeoms = Array; + +/** @internal */ +export const computePanelsSelectors = createCachedSelector( + [computeSmallMultipleScalesSelector], + (scales): PanelGeoms => { + const panelSize = getPanelSize(scales); + return getPerPanelMap(scales, () => panelSize); + }, +)(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts b/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts new file mode 100644 index 0000000000..93c7854253 --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts @@ -0,0 +1,73 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; +import { AxisGeometry } from '../../utils/axis_utils'; +import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; + +/** @internal */ +export type PerPanelAxisGeoms = { + axesGeoms: AxisGeometry[]; +} & PerPanelMap; + +/** @internal */ +export const computePerPanelAxesGeomsSelector = createCachedSelector( + [computeAxesGeometriesSelector, computeSmallMultipleScalesSelector], + (axesGeoms, scales): Array => { + const { horizontal, vertical } = scales; + return getPerPanelMap(scales, (anchor, h, v) => { + const lastLine = horizontal.domain.includes(h) && vertical.domain[vertical.domain.length - 1] === v; + const firstColumn = horizontal.domain[0] === h; + if (firstColumn || lastLine) { + return { + axesGeoms: axesGeoms + .filter(({ axis: { position } }) => { + if (firstColumn && lastLine) { + return true; + } + return firstColumn ? isVerticalAxis(position) : isHorizontalAxis(position); + }) + .map((geom) => { + const { + axis: { position, title }, + } = geom; + const panelTitle = isVerticalAxis(position) ? `${v}` : `${h}`; + const useSmallMultiplePanelTitles = isVerticalAxis(position) + ? vertical.domain.length > 1 + : horizontal.domain.length > 1; + return { + ...geom, + axis: { + ...geom.axis, + title: useSmallMultiplePanelTitles ? panelTitle : title, + }, + }; + }), + }; + } + + return null; + }); + }, +)(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts index f3bd0fe840..12f3edc396 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -24,16 +24,22 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { SeriesDomainsAndData } from '../utils/types'; import { computeSeriesDomains } from '../utils/utils'; -import { getSeriesSpecsSelector } from './get_specs'; +import { getSeriesSpecsSelector, getSmallMultiplesIndexOrderSelector } from './get_specs'; import { mergeYCustomDomainsByGroupIdSelector } from './merge_y_custom_domains'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; /** @internal */ export const computeSeriesDomainsSelector = createCachedSelector( - [getSeriesSpecsSelector, mergeYCustomDomainsByGroupIdSelector, getDeselectedSeriesSelector, getSettingsSpecSelector], - (seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, settingsSpec): SeriesDomainsAndData => { - const domains = computeSeriesDomains( + [ + getSeriesSpecsSelector, + mergeYCustomDomainsByGroupIdSelector, + getDeselectedSeriesSelector, + getSettingsSpecSelector, + getSmallMultiplesIndexOrderSelector, + ], + (seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, settingsSpec, smallMultiples): SeriesDomainsAndData => { + return computeSeriesDomains( seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, @@ -41,7 +47,7 @@ export const computeSeriesDomainsSelector = createCachedSelector( settingsSpec.orderOrdinalBinsBy, // @ts-ignore blind sort option for vislib settingsSpec.enableVislibSeriesSort, + smallMultiples, ); - return domains; }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts b/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts index 5b63d1983e..689444acb5 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts @@ -24,8 +24,8 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { ComputedGeometries } from '../utils/types'; import { computeSeriesGeometries } from '../utils/utils'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getSeriesColorsSelector } from './get_series_color_map'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; @@ -38,8 +38,8 @@ export const computeSeriesGeometriesSelector = createCachedSelector( computeSeriesDomainsSelector, getSeriesColorsSelector, getChartThemeSelector, - computeChartDimensionsSelector, getAxisSpecsSelector, + computeSmallMultipleScalesSelector, isHistogramModeEnabledSelector, ], ( @@ -48,21 +48,18 @@ export const computeSeriesGeometriesSelector = createCachedSelector( seriesDomainsAndData, seriesColors, chartTheme, - chartDimensions, axesSpecs, + smallMultiplesScales, isHistogramMode, ): ComputedGeometries => { - const { xDomain, yDomain, formattedDataSeries } = seriesDomainsAndData; return computeSeriesGeometries( seriesSpecs, - xDomain, - yDomain, - formattedDataSeries, + seriesDomainsAndData, seriesColors, chartTheme, - chartDimensions.chartDimensions, settingsSpec.rotation, axesSpecs, + smallMultiplesScales, isHistogramMode, ); }, diff --git a/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts b/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts new file mode 100644 index 0000000000..82af6d6edd --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts @@ -0,0 +1,73 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { ChartTypes } from '../../..'; +import { ScaleBand } from '../../../../scales'; +import { SpecTypes } from '../../../../specs/constants'; +import { + DEFAULT_SINGLE_PANEL_SM_VALUE, + DEFAULT_SM_PANEL_PADDING, + SmallMultiplesSpec, +} from '../../../../specs/small_multiples'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { Domain } from '../../../../utils/domain'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +/** @internal */ +export interface SmallMultipleScales { + horizontal: ScaleBand; + vertical: ScaleBand; +} + +const getSmallMultipleSpec = (state: GlobalChartState) => { + const smallMultiples = getSpecsFromStore( + state.specs, + ChartTypes.Global, + SpecTypes.SmallMultiples, + ); + if (smallMultiples.length !== 1) { + return undefined; + } + return smallMultiples[0]; +}; + +/** + * Return the small multiple scales for horizontal and vertical grids + * @internal + */ +export const computeSmallMultipleScalesSelector = createCachedSelector( + [computeSeriesDomainsSelector, computeChartDimensionsSelector, getSmallMultipleSpec], + ({ smHDomain, smVDomain }, { chartDimensions: { width, height } }, smSpec): SmallMultipleScales => { + return { + horizontal: getScale(smHDomain, width, smSpec?.style?.horizontalPanelPadding), + vertical: getScale(smVDomain, height, smSpec?.style?.verticalPanelPadding), + }; + }, +)(getChartIdSelector); + +function getScale(domain: Domain, maxRange: number, padding = DEFAULT_SM_PANEL_PADDING) { + const singlePanelSmallMultiple = domain.length <= 1; + const defaultDomain = domain.length === 0 ? [DEFAULT_SINGLE_PANEL_SM_VALUE] : domain; + return new ScaleBand(defaultDomain, [0, maxRange], undefined, singlePanelSmallMultiple ? 0 : padding); +} diff --git a/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts b/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts index 903eab8027..fd8cfc809a 100644 --- a/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts +++ b/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts @@ -19,17 +19,45 @@ import createCachedSelector from 're-reselect'; +import { SeriesTypes } from '../../../../specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { countBarsInCluster } from '../../utils/scales'; +import { groupBy } from '../../utils/group_data_series'; +import { SeriesDomainsAndData } from '../utils/types'; +import { getBarIndexKey } from '../utils/utils'; import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; /** @internal */ export const countBarsInClusterSelector = createCachedSelector( - [computeSeriesDomainsSelector], - (seriesDomainsAndData): number => { - const { formattedDataSeries } = seriesDomainsAndData; - - const { totalBarsInCluster } = countBarsInCluster(formattedDataSeries.stacked, formattedDataSeries.nonStacked); - return totalBarsInCluster; - }, + [computeSeriesDomainsSelector, isHistogramModeEnabledSelector], + countBarsInCluster, )(getChartIdSelector); + +/** @internal */ +export function countBarsInCluster({ formattedDataSeries }: SeriesDomainsAndData, isHistogramEnabled: boolean): number { + const barDataSeries = formattedDataSeries.filter(({ seriesType }) => seriesType === SeriesTypes.Bar); + + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); + + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, isHistogramEnabled); + }, + false, + ); + + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); + + return Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); +} diff --git a/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts b/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts index ec732a09a3..5f71e088b0 100644 --- a/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts +++ b/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts @@ -24,6 +24,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { mergePartial, RecursivePartial } from '../../../../utils/commons'; import { AxisId } from '../../../../utils/ids'; import { AxisStyle } from '../../../../utils/themes/theme'; +import { isVerticalAxis } from '../../utils/axis_type_utils'; import { getAxisSpecsSelector } from './get_specs'; /** @@ -35,9 +36,16 @@ export const getAxesStylesSelector = createCachedSelector( [getAxisSpecsSelector, getChartThemeSelector], (axesSpecs, { axes: sharedAxesStyle }): Map => { const axesStyles = new Map(); - axesSpecs.forEach(({ id, style }) => { + axesSpecs.forEach(({ id, style, gridLine, position }) => { + const isVertical = isVerticalAxis(position); + const axisStyleMerge: RecursivePartial = { + ...style, + }; + if (gridLine) { + axisStyleMerge.gridLine = { [isVertical ? 'vertical' : 'horizontal']: gridLine }; + } const newStyle = style - ? mergePartial(sharedAxesStyle, style as RecursivePartial, { + ? mergePartial(sharedAxesStyle, axisStyleMerge, { mergeOptionalPartialValues: true, }) : null; diff --git a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts index 2c8ac9e2ab..978eed52c7 100644 --- a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts +++ b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts @@ -21,20 +21,22 @@ import createCachedSelector from 're-reselect'; import { Scale } from '../../../../scales'; import { SettingsSpec, PointerEvent } from '../../../../specs/settings'; +import { DEFAULT_SINGLE_PANEL_SM_VALUE } from '../../../../specs/small_multiples'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { Dimensions } from '../../../../utils/dimensions'; import { isValidPointerOverEvent } from '../../../../utils/events'; -import { Point } from '../../../../utils/point'; import { getCursorBandPosition } from '../../crosshair/crosshair_utils'; import { BasicSeriesSpec } from '../../utils/specs'; import { isLineAreaOnlyChart } from '../utils/common'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; import { getSeriesSpecsSelector } from './get_specs'; import { isTooltipSnapEnableSelector } from './is_tooltip_snap_enabled'; @@ -52,6 +54,7 @@ export const getCursorBandPositionSelector = createCachedSelector( countBarsInClusterSelector, isTooltipSnapEnableSelector, getGeometriesIndexKeysSelector, + computeSmallMultipleScalesSelector, ], ( orientedProjectedPointerPosition, @@ -63,6 +66,7 @@ export const getCursorBandPositionSelector = createCachedSelector( totalBarsInCluster, isTooltipSnapEnabled, geometriesIndexKeys, + smallMultipleScales, ) => getCursorBand( orientedProjectedPointerPosition, @@ -74,11 +78,12 @@ export const getCursorBandPositionSelector = createCachedSelector( totalBarsInCluster, isTooltipSnapEnabled, geometriesIndexKeys, + smallMultipleScales, ), )(getChartIdSelector); function getCursorBand( - orientedProjectedPoinerPosition: Point, + orientedProjectedPointerPosition: PointerPosition, externalPointerEvent: PointerEvent | null, chartDimensions: Dimensions, settingsSpec: SettingsSpec, @@ -87,37 +92,54 @@ function getCursorBand( totalBarsInCluster: number, isTooltipSnapEnabled: boolean, geometriesIndexKeys: (string | number)[], + smallMultipleScales: SmallMultipleScales, ): (Dimensions & { visible: boolean; fromExternalEvent: boolean }) | undefined { - // update che cursorBandPosition based on chart configuration - const isLineAreaOnly = isLineAreaOnlyChart(seriesSpecs); if (!xScale) { return; } - let pointerPosition = orientedProjectedPoinerPosition; + // update che cursorBandPosition based on chart configuration + const isLineAreaOnly = isLineAreaOnlyChart(seriesSpecs); + + let pointerPosition = { ...orientedProjectedPointerPosition }; + let xValue; let fromExternalEvent = false; - // external pointer events takes precendence over the current mouse pointer + // external pointer events takes precedence over the current mouse pointer if (isValidPointerOverEvent(xScale, externalPointerEvent)) { fromExternalEvent = true; const x = xScale.pureScale(externalPointerEvent.value); if (x == null || x > chartDimensions.width || x < 0) { return; } - pointerPosition = { x, y: 0 }; + pointerPosition = { + x, + y: 0, + verticalPanelValue: DEFAULT_SINGLE_PANEL_SM_VALUE, + horizontalPanelValue: DEFAULT_SINGLE_PANEL_SM_VALUE, + }; xValue = { value: externalPointerEvent.value, withinBandwidth: true, }; } else { - xValue = xScale.invertWithStep(orientedProjectedPoinerPosition.x, geometriesIndexKeys); + xValue = xScale.invertWithStep(orientedProjectedPointerPosition.x, geometriesIndexKeys); if (!xValue) { return; } } + const { horizontal, vertical } = smallMultipleScales; + const topPos = vertical.scale(pointerPosition.verticalPanelValue) || 0; + const leftPos = horizontal.scale(pointerPosition.horizontalPanelValue) || 0; + const panel = { + width: horizontal.bandwidth, + height: vertical.bandwidth, + top: chartDimensions.top + topPos, + left: chartDimensions.left + leftPos, + }; const cursorBand = getCursorBandPosition( settingsSpec.rotation, - chartDimensions, + panel, pointerPosition, { value: xValue.value, diff --git a/src/chart_types/xy_chart/state/selectors/get_debug_state.ts b/src/chart_types/xy_chart/state/selectors/get_debug_state.ts index fac3519cad..05ad347158 100644 --- a/src/chart_types/xy_chart/state/selectors/get_debug_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_debug_state.ts @@ -20,6 +20,7 @@ import createCachedSelector from 're-reselect'; import { LegendItem } from '../../../../commons/legend'; +import { Line } from '../../../../geoms/types'; import { AxisSpec } from '../../../../specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { @@ -31,12 +32,15 @@ import { DebugStateBar, DebugStateLegend, } from '../../../../state/types'; -import { AreaGeometry, BandedAccessorType, LineGeometry, BarGeometry } from '../../../../utils/geometry'; +import { AreaGeometry, BandedAccessorType, LineGeometry, BarGeometry, PerPanel } from '../../../../utils/geometry'; import { FillStyle, Visible, StrokeStyle, Opacity } from '../../../../utils/themes/theme'; import { isVerticalAxis } from '../../utils/axis_type_utils'; -import { computeAxisVisibleTicksSelector, AxisVisibleTicks } from './compute_axis_visible_ticks'; +import { AxisGeometry } from '../../utils/axis_utils'; +import { LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; import { computeLegendSelector } from './compute_legend'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeGridLinesSelector } from './get_grid_lines'; import { getAxisSpecsSelector } from './get_specs'; /** @@ -44,13 +48,19 @@ import { getAxisSpecsSelector } from './get_specs'; * @internal */ export const getDebugStateSelector = createCachedSelector( - [computeSeriesGeometriesSelector, computeLegendSelector, computeAxisVisibleTicksSelector, getAxisSpecsSelector], - ({ geometries }, legend, axes, axesSpecs): DebugState => { + [ + computeSeriesGeometriesSelector, + computeLegendSelector, + computeAxesGeometriesSelector, + computeGridLinesSelector, + getAxisSpecsSelector, + ], + ({ geometries }, legend, axes, gridLines, axesSpecs): DebugState => { const seriesNameMap = getSeriesNameMap(legend); return { legend: getLegendState(legend), - axes: getAxes(axes, axesSpecs), + axes: getAxes(axes, axesSpecs, gridLines), areas: geometries.areas.map(getAreaState(seriesNameMap)), lines: geometries.lines.map(getLineState(seriesNameMap)), bars: getBarsState(seriesNameMap, geometries.bars), @@ -58,18 +68,33 @@ export const getDebugStateSelector = createCachedSelector( }, )(getChartIdSelector); -const getAxes = (ticks: AxisVisibleTicks, axesSpecs: AxisSpec[]): DebugStateAxes | undefined => { +function getAxes(axesGeoms: AxisGeometry[], axesSpecs: AxisSpec[], gridLines: LinesGrid[]): DebugStateAxes | undefined { if (axesSpecs.length === 0) { return; } return axesSpecs.reduce( (acc, { position, title, id }) => { - const axisTicks = ticks.axisVisibleTicks.get(id) ?? []; - const labels = axisTicks.map(({ label }) => label); - const values = axisTicks.map(({ value }) => value); - const grids = ticks.axisGridLinesPositions.get(id) ?? []; - const gridlines = grids.map(([x, y]) => ({ x, y })); + const geom = axesGeoms.find(({ axis }) => axis.id === id); + if (!geom) { + return acc; + } + + const { ticks } = geom; + const labels = ticks.map(({ label }) => label); + const values = ticks.map(({ value }) => value); + + const gridlines = gridLines + .reduce((accLines, { lineGroups }) => { + const groupLines = lineGroups.find(({ axisId }) => { + return axisId === geom.axis.id; + }); + if (!groupLines) { + return accLines; + } + return [...accLines, ...groupLines.lines]; + }, []) + .map(({ x1, y1 }) => ({ x: x1, y: y1 })); if (isVerticalAxis(position)) { acc.y.push({ @@ -99,12 +124,17 @@ const getAxes = (ticks: AxisVisibleTicks, axesSpecs: AxisSpec[]): DebugStateAxes x: [], }, ); -}; +} -const getBarsState = (seriesNameMap: Map, barGeometries: BarGeometry[]): DebugStateBar[] => { +function getBarsState( + seriesNameMap: Map, + barGeometries: Array>, +): DebugStateBar[] { const buckets = new Map(); - - barGeometries.forEach( + const bars = barGeometries.reduce((acc, bars) => { + return [...acc, ...bars.value]; + }, []); + bars.forEach( ({ color, seriesIdentifier: { key }, @@ -136,86 +166,94 @@ const getBarsState = (seriesNameMap: Map, barGeometries: BarGeom ); return [...buckets.values()]; -}; - -const getLineState = (seriesNameMap: Map) => ({ - line: path, - points, - color, - seriesIdentifier: { key }, - seriesLineStyle, - seriesPointStyle, -}: LineGeometry): DebugStateLine => { - const name = seriesNameMap.get(key) ?? ''; - - return { - path, - color, - key, - name, - visible: hasVisibleStyle(seriesLineStyle), - visiblePoints: hasVisibleStyle(seriesPointStyle), - points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), - }; -}; - -const getAreaState = (seriesNameMap: Map) => ({ - area: path, - lines, - points, - color, - seriesIdentifier: { key }, - seriesAreaStyle, - seriesPointStyle, - seriesAreaLineStyle, -}: AreaGeometry): DebugStateArea => { - const [y1Path, y0Path] = lines; - const linePoints = points.reduce<{ - y0: DebugStateValue[]; - y1: DebugStateValue[]; - }>( - (acc, { value: { accessor, ...value } }) => { - if (accessor === BandedAccessorType.Y0) { - acc.y0.push(value); - } else { - acc.y1.push(value); - } +} - return acc; +function getLineState(seriesNameMap: Map) { + return ({ + value: { + line: path, + points, + color, + seriesIdentifier: { key }, + seriesLineStyle, + seriesPointStyle, }, - { - y0: [], - y1: [], + }: PerPanel): DebugStateLine => { + const name = seriesNameMap.get(key) ?? ''; + + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesLineStyle), + visiblePoints: hasVisibleStyle(seriesPointStyle), + points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), + }; + }; +} + +function getAreaState(seriesNameMap: Map) { + return ({ + value: { + area: path, + lines, + points, + color, + seriesIdentifier: { key }, + seriesAreaStyle, + seriesPointStyle, + seriesAreaLineStyle, }, - ); - const lineVisible = hasVisibleStyle(seriesAreaLineStyle); - const visiblePoints = hasVisibleStyle(seriesPointStyle); - const name = seriesNameMap.get(key) ?? ''; - - return { - path, - color, - key, - name, - visible: hasVisibleStyle(seriesAreaStyle), - lines: { - y0: y0Path - ? { - visible: lineVisible, - path: y0Path, - points: linePoints.y0, - visiblePoints, - } - : undefined, - y1: { - visible: lineVisible, - path: y1Path, - points: linePoints.y1, - visiblePoints, + }: PerPanel): DebugStateArea => { + const [y1Path, y0Path] = lines; + const linePoints = points.reduce<{ + y0: DebugStateValue[]; + y1: DebugStateValue[]; + }>( + (acc, { value: { accessor, ...value } }) => { + if (accessor === BandedAccessorType.Y0) { + acc.y0.push(value); + } else { + acc.y1.push(value); + } + + return acc; }, - }, + { + y0: [], + y1: [], + }, + ); + const lineVisible = hasVisibleStyle(seriesAreaLineStyle); + const visiblePoints = hasVisibleStyle(seriesPointStyle); + const name = seriesNameMap.get(key) ?? ''; + + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesAreaStyle), + lines: { + y0: y0Path + ? { + visible: lineVisible, + path: y0Path, + points: linePoints.y0, + visiblePoints, + } + : undefined, + y1: { + visible: lineVisible, + path: y1Path, + points: linePoints.y1, + visiblePoints, + }, + }, + }; }; -}; +} /** * returns series key to name mapping diff --git a/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts b/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts index facacd197a..4966e32f4c 100644 --- a/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts +++ b/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts @@ -22,10 +22,9 @@ import createCachedSelector from 're-reselect'; import { PointerEvent } from '../../../../specs'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { Dimensions } from '../../../../utils/dimensions'; import { isValidPointerOverEvent } from '../../../../utils/events'; import { IndexedGeometry } from '../../../../utils/geometry'; -import { Point } from '../../../../utils/point'; +import { ChartDimensions } from '../../utils/dimensions'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; import { ComputedScales } from '../utils/types'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; @@ -33,6 +32,7 @@ import { getComputedScalesSelector } from './get_computed_scales'; import { getGeometriesIndexSelector } from './get_geometries_index'; import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer; @@ -50,16 +50,12 @@ export const getElementAtCursorPositionSelector = createCachedSelector( )(getChartIdSelector); function getElementAtCursorPosition( - orientedProjectedPoinerPosition: Point, + orientedProjectedPointerPosition: PointerPosition, scales: ComputedScales, geometriesIndexKeys: (string | number)[], geometriesIndex: IndexedGeometryMap, externalPointerEvent: PointerEvent | null, - { - chartDimensions, - }: { - chartDimensions: Dimensions; - }, + { chartDimensions }: ChartDimensions, ): IndexedGeometry[] { if (isValidPointerOverEvent(scales.xScale, externalPointerEvent)) { const x = scales.xScale.pureScale(externalPointerEvent.value); @@ -70,10 +66,15 @@ function getElementAtCursorPosition( // TODO: Handle external event with spatial points return geometriesIndex.find(externalPointerEvent.value, { x: -1, y: -1 }); } - const xValue = scales.xScale.invertWithStep(orientedProjectedPoinerPosition.x, geometriesIndexKeys); + const xValue = scales.xScale.invertWithStep(orientedProjectedPointerPosition.x, geometriesIndexKeys); if (!xValue) { return []; } // get the elements at cursor position - return geometriesIndex.find(xValue?.value, orientedProjectedPoinerPosition); + return geometriesIndex.find( + xValue?.value, + orientedProjectedPointerPosition, + orientedProjectedPointerPosition.horizontalPanelValue, + orientedProjectedPointerPosition.verticalPanelValue, + ); } diff --git a/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts b/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts new file mode 100644 index 0000000000..23efeab584 --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts @@ -0,0 +1,35 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getGridLines, LinesGrid } from '../../utils/grid_lines'; +import { computeAxesGeometriesSelector } from './compute_axes_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; +import { getAxisSpecsSelector } from './get_specs'; + +/** @internal */ +export const computeGridLinesSelector = createCachedSelector( + [getChartThemeSelector, getAxisSpecsSelector, computeAxesGeometriesSelector, computeSmallMultipleScalesSelector], + (chartTheme, axesSpecs, axesGeoms, scales): LinesGrid[] => { + return getGridLines(axesSpecs, axesGeoms, chartTheme.axes, scales); + }, +)(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts b/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts index 096538757a..40cc23c896 100644 --- a/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts +++ b/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts @@ -22,30 +22,28 @@ import createCachedSelector from 're-reselect'; import { SettingsSpec } from '../../../../specs/settings'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Dimensions } from '../../../../utils/dimensions'; -import { Point } from '../../../../utils/point'; import { getOrientedXPosition, getOrientedYPosition } from '../../utils/interactions'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; +import { getPanelSize } from '../../utils/panel'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { getProjectedPointerPositionSelector, PointerPosition } from './get_projected_pointer_position'; /** @internal */ export const getOrientedProjectedPointerPositionSelector = createCachedSelector( - [getProjectedPointerPositionSelector, computeChartDimensionsSelector, getSettingsSpecSelector], + [getProjectedPointerPositionSelector, getSettingsSpecSelector, computeSmallMultipleScalesSelector], getOrientedProjectedPointerPosition, )(getChartIdSelector); function getOrientedProjectedPointerPosition( - projectedPointerPosition: Point, - chartDimensions: { chartDimensions: Dimensions }, + { x, y, horizontalPanelValue, verticalPanelValue }: PointerPosition, settingsSpec: SettingsSpec, -): Point { - const xPos = projectedPointerPosition.x; - const yPos = projectedPointerPosition.y; + scales: SmallMultipleScales, +): PointerPosition { // get the oriented projected pointer position - const x = getOrientedXPosition(xPos, yPos, settingsSpec.rotation, chartDimensions.chartDimensions); - const y = getOrientedYPosition(xPos, yPos, settingsSpec.rotation, chartDimensions.chartDimensions); + const panel = getPanelSize(scales); return { - x, - y, + x: getOrientedXPosition(x, y, settingsSpec.rotation, panel), + y: getOrientedYPosition(x, y, settingsSpec.rotation, panel), + horizontalPanelValue, + verticalPanelValue, }; } diff --git a/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts b/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts index b946d66724..e4f4e02eec 100644 --- a/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts +++ b/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts @@ -19,40 +19,84 @@ import createCachedSelector from 're-reselect'; +import { ScaleBand } from '../../../../scales/scale_band'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { Dimensions } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; +import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; -/** @internal */ +export type PointerPosition = Point & { horizontalPanelValue: PrimitiveValue; verticalPanelValue: PrimitiveValue }; +/** + * Get the x and y pointer position relative to the chart projection area + * @internal + */ export const getProjectedPointerPositionSelector = createCachedSelector( - [getCurrentPointerPosition, computeChartDimensionsSelector], - (currentPointerPosition, chartDimensions): Point => - getProjectedPointerPosition(currentPointerPosition, chartDimensions.chartDimensions), + [getCurrentPointerPosition, computeChartDimensionsSelector, computeSmallMultipleScalesSelector], + (currentPointerPosition, { chartDimensions }, smallMultipleScales): PointerPosition => + getProjectedPointerPosition(currentPointerPosition, chartDimensions, smallMultipleScales), )(getChartIdSelector); /** * Get the x and y pointer position relative to the chart projection area * @param chartAreaPointerPosition the pointer position relative to the chart area + * @param horizontal SmallMultipleScales horizontal panel scale + * @param vertical SmallMultipleScales vertical panel scale * @param chartAreaDimensions the chart dimensions */ -function getProjectedPointerPosition(chartAreaPointerPosition: Point, chartAreaDimensions: Dimensions): Point { +function getProjectedPointerPosition( + chartAreaPointerPosition: Point, + { left, top, width, height }: Dimensions, + { horizontal, vertical }: SmallMultipleScales, +): PointerPosition { const { x, y } = chartAreaPointerPosition; // get positions relative to chart - let xPos = x - chartAreaDimensions.left; - let yPos = y - chartAreaDimensions.top; + let xPos = x - left; + let yPos = y - top; + // limit cursorPosition to the chart area - if (xPos < 0 || xPos >= chartAreaDimensions.width) { + if (xPos < 0 || xPos >= width) { xPos = -1; } - if (yPos < 0 || yPos >= chartAreaDimensions.height) { + if (yPos < 0 || yPos >= height) { yPos = -1; } + const h = getPosRelativeToPanel(horizontal, xPos); + const v = getPosRelativeToPanel(vertical, yPos); + + return { + x: h.pos, + y: v.pos, + horizontalPanelValue: h.value, + verticalPanelValue: v.value, + }; +} + +function getPosRelativeToPanel(panelScale: ScaleBand, pos: number): { pos: number; value: PrimitiveValue } { + const outerPadding = panelScale.outerPadding * panelScale.step; + const innerPadding = panelScale.innerPadding * panelScale.step; + const numOfDomainSteps = panelScale.domain.length; + const rangeWithoutOuterPaddings = numOfDomainSteps * panelScale.bandwidth + (numOfDomainSteps - 1) * innerPadding; + + if (pos < outerPadding || pos > outerPadding + rangeWithoutOuterPaddings) { + return { pos: -1, value: null }; + } + const posWOInitialOuterPadding = pos - outerPadding; + const minEqualSteps = (numOfDomainSteps - 1) * panelScale.step; + if (posWOInitialOuterPadding <= minEqualSteps) { + const relativePosIndex = Math.floor(posWOInitialOuterPadding / panelScale.step); + const relativePos = posWOInitialOuterPadding - panelScale.step * relativePosIndex; + if (relativePos > panelScale.bandwidth) { + return { pos: -1, value: null }; + } + return { pos: relativePos, value: panelScale.domain[relativePosIndex] }; + } return { - x: xPos, - y: yPos, + pos: posWOInitialOuterPadding - panelScale.step * (numOfDomainSteps - 1), + value: panelScale.domain[numOfDomainSteps - 1], }; } diff --git a/src/chart_types/xy_chart/state/selectors/get_specs.ts b/src/chart_types/xy_chart/state/selectors/get_specs.ts index 23e5c9cf62..8b3203dddc 100644 --- a/src/chart_types/xy_chart/state/selectors/get_specs.ts +++ b/src/chart_types/xy_chart/state/selectors/get_specs.ts @@ -20,6 +20,7 @@ import createCachedSelector from 're-reselect'; import { ChartTypes } from '../../..'; +import { GroupBySpec, SmallMultiplesSpec } from '../../../../specs'; import { SpecTypes } from '../../../../specs/constants'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; @@ -35,11 +36,35 @@ export const getAxisSpecsSelector = createCachedSelector([getSpecs], (specs): Ax /** @internal */ export const getSeriesSpecsSelector = createCachedSelector([getSpecs], (specs) => { - const seriesSpec = getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Series); - return seriesSpec; + return getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Series); })(getChartIdSelector); /** @internal */ export const getAnnotationSpecsSelector = createCachedSelector([getSpecs], (specs) => getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Annotation), )(getChartIdSelector); + +/** @internal */ +export const getSmallMultiplesIndexOrderSelector = createCachedSelector([getSpecs], (specs) => { + const smallMultiples = getSpecsFromStore(specs, ChartTypes.Global, SpecTypes.SmallMultiples); + if (smallMultiples.length !== 1) { + return undefined; + } + const indexOrders = getSpecsFromStore(specs, ChartTypes.Global, SpecTypes.IndexOrder); + const [smallMultiplesConfig] = smallMultiples; + + let vertical: GroupBySpec | undefined; + let horizontal: GroupBySpec | undefined; + + if (smallMultiplesConfig.splitVertically) { + vertical = indexOrders.find((d) => d.id === smallMultiplesConfig.splitVertically); + } + if (smallMultiplesConfig.splitHorizontally) { + horizontal = indexOrders.find((d) => d.id === smallMultiplesConfig.splitHorizontally); + } + + return { + vertical, + horizontal, + }; +})(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts index bc58216492..97b6f6faad 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts @@ -21,10 +21,10 @@ import createCachedSelector from 're-reselect'; import { TooltipAnchorPosition } from '../../../../components/tooltip/types'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getTooltipAnchorPosition } from '../../crosshair/crosshair_utils'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getCursorBandPositionSelector } from './get_cursor_band'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; @@ -35,13 +35,35 @@ export const getTooltipAnchorPositionSelector = createCachedSelector( getSettingsSpecSelector, getCursorBandPositionSelector, getProjectedPointerPositionSelector, - getLegendSizeSelector, + computeSmallMultipleScalesSelector, ], - (chartDimensions, settings, cursorBandPosition, projectedPointerPosition): TooltipAnchorPosition | null => { + ( + chartDimensions, + settings, + cursorBandPosition, + projectedPointerPosition, + { horizontal, vertical }, + ): TooltipAnchorPosition | null => { if (!cursorBandPosition) { return null; } - return getTooltipAnchorPosition(chartDimensions, settings.rotation, cursorBandPosition, projectedPointerPosition); + const topPos = vertical.scale(projectedPointerPosition.verticalPanelValue) || 0; + const leftPos = horizontal.scale(projectedPointerPosition.horizontalPanelValue) || 0; + + const panel = { + width: horizontal.bandwidth, + height: vertical.bandwidth, + top: chartDimensions.chartDimensions.top + topPos, + left: chartDimensions.chartDimensions.left + leftPos, + }; + + return getTooltipAnchorPosition( + chartDimensions, + settings.rotation, + cursorBandPosition, + projectedPointerPosition, + panel, + ); }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index 240dc18fbd..734eae4edb 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -199,7 +199,8 @@ function getTooltipAndHighlightFromValue( return { tooltip: { header, - values, + // to avoid creating a breaking change because of a different sorting order on tooltip + values: values.reverse(), }, highlightedGeometries, }; diff --git a/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts b/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts index ba3c8728f8..1514dea375 100644 --- a/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts +++ b/src/chart_types/xy_chart/state/selectors/on_pointer_move_caller.ts @@ -27,10 +27,10 @@ import { PointerEventType } from '../../../../specs/constants'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Point } from '../../../../utils/point'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; +import { PointerPosition } from './get_projected_pointer_position'; const getPointerEventSelector = createCachedSelector( [ @@ -45,7 +45,7 @@ const getPointerEventSelector = createCachedSelector( function getPointerEvent( chartId: string, - orientedProjectedPoinerPosition: Point, + orientedProjectedPointerPosition: PointerPosition, xScale: Scale | undefined, geometriesIndexKeys: any[], ): PointerEvent { @@ -56,7 +56,7 @@ function getPointerEvent( type: PointerEventType.Out, }; } - const { x, y } = orientedProjectedPoinerPosition; + const { x, y } = orientedProjectedPointerPosition; if (x === -1 || y === -1) { return { chartId, @@ -122,7 +122,7 @@ export function createOnPointerMoveCaller(): (state: GlobalChartState) => void { const tempPrev = { ...prevPointerEvent, }; - // we have to update the prevPointerEvents before possiibly calling the onPointerUpdate + // we have to update the prevPointerEvents before possibly calling the onPointerUpdate // to avoid a recursive loop of calls caused by the impossibility to update the prevPointerEvent prevPointerEvent = nextPointerEvent; if (settings && settings.onPointerUpdate && hasPointerEventChanged(tempPrev, nextPointerEvent)) { diff --git a/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap b/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap index da1db14e80..ad27ac9981 100644 --- a/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap +++ b/src/chart_types/xy_chart/state/utils/__snapshots__/utils.test.ts.snap @@ -3,144 +3,216 @@ exports[`Chart State utils should compute and format specifications for non stacked chart 1`] = ` Array [ Object { - "counts": Object { - "area": 0, - "bar": 0, - "bubble": 0, - "line": 1, - }, - "dataSeries": Array [ + "data": Array [ Object { - "data": Array [ - Object { - "datum": Object { - "x": 0, - "y": 1, - }, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 0, - "y0": null, - "y1": 1, - }, - Object { - "datum": Object { - "x": 1, - "y": 2, - }, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 1, - "y0": null, - "y1": 2, - }, - Object { - "datum": Object { - "x": 2, - "y": 10, - }, - "initialY0": null, - "initialY1": 10, - "mark": null, - "x": 2, - "y0": null, - "y1": 10, - }, - Object { - "datum": Object { - "x": 3, - "y": 6, - }, - "initialY0": null, - "initialY1": 6, - "mark": null, - "x": 3, - "y0": null, - "y1": 6, - }, - ], - "key": "spec{spec1}yAccessor{y}splitAccessors{}", - "seriesKeys": Array [ - "y", - ], - "specId": "spec1", - "splitAccessors": Map {}, - "yAccessor": "y", + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, + "y0": null, + "y1": 6, }, ], "groupId": "group1", + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y", }, Object { - "counts": Object { - "area": 0, - "bar": 0, - "bubble": 0, - "line": 1, - }, - "dataSeries": Array [ + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, + "y0": null, + "y1": 2, + }, Object { - "data": Array [ - Object { - "datum": Object { - "x": 0, - "y": 1, - }, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 0, - "y0": null, - "y1": 1, - }, - Object { - "datum": Object { - "x": 1, - "y": 2, - }, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 1, - "y0": null, - "y1": 2, - }, - Object { - "datum": Object { - "x": 2, - "y": 10, - }, - "initialY0": null, - "initialY1": 10, - "mark": null, - "x": 2, - "y0": null, - "y1": 10, - }, - Object { - "datum": Object { - "x": 3, - "y": 6, - }, - "initialY0": null, - "initialY1": 6, - "mark": null, - "x": 3, - "y0": null, - "y1": 6, - }, - ], - "key": "spec{spec2}yAccessor{y}splitAccessors{}", - "seriesKeys": Array [ - "y", - ], - "specId": "spec2", - "splitAccessors": Map {}, - "yAccessor": "y", + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, + "y0": null, + "y1": 6, }, ], "groupId": "group2", + "isStacked": false, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map {}, + "stackMode": undefined, + "yAccessor": "y", }, ] `; @@ -148,156 +220,282 @@ Array [ exports[`Chart State utils should compute and format specifications for stacked chart 1`] = ` Array [ Object { - "counts": Object { - "area": 0, - "bar": 0, - "bubble": 0, - "line": 2, + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y": 1, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 0, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 0, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 2, + "y0": 0, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 3, + "y0": 0, + "y1": 4, + }, + ], + "groupId": "group2", + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "a", + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", }, - "dataSeries": Array [ + "specId": "spec2", + "splitAccessors": Map { + "g" => "a", + }, + "stackMode": undefined, + "yAccessor": "y", + }, + Object { + "data": Array [ Object { - "data": Array [ - Object { - "datum": Object { - "g": "a", - "x": 0, - "y": 1, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 0, - "y0": 0, - "y1": 1, - }, - Object { - "datum": Object { - "g": "a", - "x": 1, - "y": 2, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 1, - "y0": 0, - "y1": 2, - }, - Object { - "datum": Object { - "g": "a", - "x": 2, - "y": 3, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 3, - "mark": null, - "x": 2, - "y0": 0, - "y1": 3, - }, - Object { - "datum": Object { - "g": "a", - "x": 3, - "y": 4, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 4, - "mark": null, - "x": 3, - "y0": 0, - "y1": 4, - }, - ], - "key": "spec{spec2}yAccessor{y}splitAccessors{g-a}", - "seriesKeys": Array [ - "a", - "y", - ], - "specId": "spec2", - "splitAccessors": Map { - "g" => "a", - }, - "yAccessor": "y", + "datum": Object { + "g": "b", + "x": 0, + "y": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 0, + "y0": 1, + "y1": 3, }, Object { - "data": Array [ - Object { - "datum": Object { - "g": "b", - "x": 0, - "y": 2, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 0, - "y0": 1, - "y1": 3, - }, - Object { - "datum": Object { - "g": "b", - "x": 1, - "y": 3, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 3, - "mark": null, - "x": 1, - "y0": 2, - "y1": 5, - }, - Object { - "datum": Object { - "g": "b", - "x": 2, - "y": 4, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 4, - "mark": null, - "x": 2, - "y0": 3, - "y1": 7, - }, - Object { - "datum": Object { - "g": "b", - "x": 3, - "y": 5, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 5, - "mark": null, - "x": 3, - "y0": 4, - "y1": 9, - }, - ], - "key": "spec{spec2}yAccessor{y}splitAccessors{g-b}", - "seriesKeys": Array [ - "b", - "y", - ], - "specId": "spec2", - "splitAccessors": Map { - "g" => "b", - }, - "yAccessor": "y", + "datum": Object { + "g": "b", + "x": 1, + "y": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 1, + "y0": 2, + "y1": 5, + }, + Object { + "datum": Object { + "g": "b", + "x": 2, + "y": 4, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 4, + "mark": null, + "x": 2, + "y0": 3, + "y1": 7, + }, + Object { + "datum": Object { + "g": "b", + "x": 3, + "y": 5, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 5, + "mark": null, + "x": 3, + "y0": 4, + "y1": 9, }, ], "groupId": "group2", + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "b", + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec2", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec2", + "splitAccessors": Map { + "g" => "b", + }, "stackMode": undefined, + "yAccessor": "y", }, ] `; @@ -305,147 +503,284 @@ Array [ exports[`Chart State utils should compute and format specifications for stacked chart 2`] = ` Array [ Object { - "counts": Object { - "area": 0, - "bar": 0, - "bubble": 0, - "line": 2, + "data": Array [ + Object { + "datum": Object { + "g": "a", + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "g": "a", + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "a", + "x": 2, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, + "y0": null, + "y1": 3, + }, + Object { + "datum": Object { + "g": "a", + "x": 3, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, + "y0": null, + "y1": 4, + }, + ], + "groupId": "group1", + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "a", + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", }, - "dataSeries": Array [ + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "stackMode": undefined, + "yAccessor": "y", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "g": "b", + "x": 0, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "g": "b", + "x": 1, + "y": 3, + }, + "initialY0": null, + "initialY1": 3, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, + "y0": null, + "y1": 3, + }, Object { - "data": Array [ - Object { - "datum": Object { - "g": "a", - "x": 0, - "y": 1, - }, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 0, - "y0": null, - "y1": 1, - }, - Object { - "datum": Object { - "g": "a", - "x": 1, - "y": 2, - }, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 1, - "y0": null, - "y1": 2, - }, - Object { - "datum": Object { - "g": "a", - "x": 2, - "y": 3, - }, - "initialY0": null, - "initialY1": 3, - "mark": null, - "x": 2, - "y0": null, - "y1": 3, - }, - Object { - "datum": Object { - "g": "a", - "x": 3, - "y": 4, - }, - "initialY0": null, - "initialY1": 4, - "mark": null, - "x": 3, - "y0": null, - "y1": 4, - }, - ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g-a}", - "seriesKeys": Array [ - "a", - "y", - ], - "specId": "spec1", - "splitAccessors": Map { - "g" => "a", - }, - "yAccessor": "y", + "datum": Object { + "g": "b", + "x": 2, + "y": 4, + }, + "initialY0": null, + "initialY1": 4, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, + "y0": null, + "y1": 4, }, Object { - "data": Array [ - Object { - "datum": Object { - "g": "b", - "x": 0, - "y": 2, - }, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 0, - "y0": null, - "y1": 2, - }, - Object { - "datum": Object { - "g": "b", - "x": 1, - "y": 3, - }, - "initialY0": null, - "initialY1": 3, - "mark": null, - "x": 1, - "y0": null, - "y1": 3, - }, - Object { - "datum": Object { - "g": "b", - "x": 2, - "y": 4, - }, - "initialY0": null, - "initialY1": 4, - "mark": null, - "x": 2, - "y0": null, - "y1": 4, - }, - Object { - "datum": Object { - "g": "b", - "x": 3, - "y": 5, - }, - "initialY0": null, - "initialY1": 5, - "mark": null, - "x": 3, - "y0": null, - "y1": 5, - }, - ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g-b}", - "seriesKeys": Array [ - "b", - "y", - ], - "specId": "spec1", - "splitAccessors": Map { - "g" => "b", - }, - "yAccessor": "y", + "datum": Object { + "g": "b", + "x": 3, + "y": 5, + }, + "initialY0": null, + "initialY1": 5, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, + "y0": null, + "y1": 5, }, ], "groupId": "group1", + "isStacked": false, + "key": "groupId{group1}spec{spec1}yAccessor{y}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "b", + "y", + ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "g": "a", + "x": 0, + "y": 1, + }, + Object { + "g": "b", + "x": 0, + "y": 2, + }, + Object { + "g": "a", + "x": 1, + "y": 2, + }, + Object { + "g": "b", + "x": 1, + "y": 3, + }, + Object { + "g": "a", + "x": 2, + "y": 3, + }, + Object { + "g": "b", + "x": 2, + "y": 4, + }, + Object { + "g": "a", + "x": 3, + "y": 4, + }, + Object { + "g": "b", + "x": 3, + "y": 5, + }, + ], + "groupId": "group1", + "hideInLegend": false, + "histogramModeAlignment": "center", + "id": "spec1", + "seriesType": "line", + "specType": "series", + "splitSeriesAccessors": Array [ + "g", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, + "specId": "spec1", + "splitAccessors": Map { + "g" => "b", + }, + "stackMode": undefined, + "yAccessor": "y", }, ] `; diff --git a/src/chart_types/xy_chart/state/utils/common.ts b/src/chart_types/xy_chart/state/utils/common.ts index aadc5184ee..492982b43d 100644 --- a/src/chart_types/xy_chart/state/utils/common.ts +++ b/src/chart_types/xy_chart/state/utils/common.ts @@ -21,7 +21,9 @@ import { LegendItem } from '../../../../commons/legend'; import { Rotation } from '../../../../utils/commons'; import { BasicSeriesSpec, SeriesTypes } from '../../utils/specs'; import { GeometriesCounts } from './types'; -import { MAX_ANIMATABLE_BARS, MAX_ANIMATABLE_LINES_AREA_POINTS } from './utils'; + +export const MAX_ANIMATABLE_BARS = 300; +export const MAX_ANIMATABLE_LINES_AREA_POINTS = 600; /** @internal */ export function isHorizontalRotation(chartRotation: Rotation) { diff --git a/src/chart_types/xy_chart/state/utils/types.ts b/src/chart_types/xy_chart/state/utils/types.ts index 27352a0ac9..c524e0f4b9 100644 --- a/src/chart_types/xy_chart/state/utils/types.ts +++ b/src/chart_types/xy_chart/state/utils/types.ts @@ -18,11 +18,19 @@ */ import { SeriesKey } from '../../../../commons/series_id'; import { Scale } from '../../../../scales'; -import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, BubbleGeometry } from '../../../../utils/geometry'; +import { Domain } from '../../../../utils/domain'; +import { + PointGeometry, + BarGeometry, + AreaGeometry, + LineGeometry, + BubbleGeometry, + PerPanel, +} from '../../../../utils/geometry'; import { GroupId } from '../../../../utils/ids'; import { XDomain, YDomain } from '../../domains/types'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; -import { SeriesCollectionValue, FormattedDataSeries } from '../../utils/series'; +import { SeriesCollectionValue, DataSeries } from '../../utils/series'; /** @internal */ export interface Transform { @@ -52,10 +60,10 @@ export interface ComputedScales { /** @internal */ export interface Geometries { points: PointGeometry[]; - bars: BarGeometry[]; - areas: AreaGeometry[]; - lines: LineGeometry[]; - bubbles: BubbleGeometry[]; + bars: Array>; + areas: Array>; + lines: Array>; + bubbles: Array>; } /** @internal */ @@ -70,10 +78,9 @@ export interface ComputedGeometries { export interface SeriesDomainsAndData { xDomain: XDomain; yDomain: YDomain[]; - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }; + smVDomain: Domain; + smHDomain: Domain; + formattedDataSeries: DataSeries[]; seriesCollection: Map; } diff --git a/src/chart_types/xy_chart/state/utils/utils.test.ts b/src/chart_types/xy_chart/state/utils/utils.test.ts index 3002ea1421..12ae7a9b87 100644 --- a/src/chart_types/xy_chart/state/utils/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils/utils.test.ts @@ -17,33 +17,20 @@ * under the License. */ -import { ChartTypes } from '../../..'; import { MockSeriesCollection } from '../../../../mocks/series/series_identifiers'; -import { MockSeriesSpecs, MockSeriesSpec } from '../../../../mocks/specs'; +import { MockSeriesSpecs, MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { MockStore } from '../../../../mocks/store'; import { SeededDataGenerator } from '../../../../mocks/utils'; import { ScaleContinuous } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; -import { SpecTypes } from '../../../../specs/constants'; -import { ColorOverrides } from '../../../../state/chart_state'; +import { Spec } from '../../../../specs'; import { BARCHART_1Y0G, BARCHART_1Y1G } from '../../../../utils/data_samples/test_dataset'; -import { IndexedGeometry, BandedAccessorType } from '../../../../utils/geometry'; import { SpecId } from '../../../../utils/ids'; -import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { SeriesCollectionValue, getSeriesIndex, getSeriesColors } from '../../utils/series'; -import { - AreaSeriesSpec, - AxisSpec, - BarSeriesSpec, - BasicSeriesSpec, - HistogramModeAlignments, - LineSeriesSpec, - SeriesTypes, - SeriesColorAccessorFn, -} from '../../utils/specs'; -import { mergeYCustomDomainsByGroupId } from '../selectors/merge_y_custom_domains'; +import { SeriesCollectionValue, getSeriesIndex } from '../../utils/series'; +import { BasicSeriesSpec, HistogramModeAlignments, SeriesColorAccessorFn } from '../../utils/specs'; +import { computeSeriesGeometriesSelector } from '../selectors/compute_series_geometries'; import { computeSeriesDomains, - computeSeriesGeometries, computeXScaleOffset, isHistogramModeEnabled, setBarSeriesAccessors, @@ -51,37 +38,40 @@ import { updateDeselectedDataSeries, } from './utils'; -describe('Chart State utils', () => { - const emptySeriesOverrides: ColorOverrides = { - temporary: {}, - persisted: {}, - }; +function getGeometriesFromSpecs(specs: Spec[]) { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins({ + theme: { + colors: { + vizColors: ['violet', 'green', 'blue'], + defaultVizColor: 'red', + }, + }, + }); + MockStore.addSpecs([...specs, settings], store); + return computeSeriesGeometriesSelector(store.getState()); +} +describe('Chart State utils', () => { it('should compute and format specifications for non stacked chart', () => { - const spec1: BasicSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const spec1 = MockSeriesSpec.line({ id: 'spec1', groupId: 'group1', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], data: BARCHART_1Y0G, - }; - const spec2: BasicSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const spec2 = MockSeriesSpec.line({ id: 'spec2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], data: BARCHART_1Y0G, - }; + }); const domains = computeSeriesDomains([spec1, spec2], new Map()); expect(domains.xDomain).toEqual({ domain: [0, 3], @@ -106,29 +96,22 @@ describe('Chart State utils', () => { type: 'yDomain', }, ]); - expect(domains.formattedDataSeries.stacked).toEqual([]); - expect(domains.formattedDataSeries.nonStacked).toMatchSnapshot(); + expect(domains.formattedDataSeries).toMatchSnapshot(); }); it('should compute and format specifications for stacked chart', () => { - const spec1: BasicSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const spec1 = MockSeriesSpec.line({ id: 'spec1', groupId: 'group1', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const spec2: BasicSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const spec2 = MockSeriesSpec.line({ id: 'spec2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -136,7 +119,7 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; + }); const domains = computeSeriesDomains([spec1, spec2], new Map()); expect(domains.xDomain).toEqual({ domain: [0, 3], @@ -147,22 +130,22 @@ describe('Chart State utils', () => { }); expect(domains.yDomain).toEqual([ { - domain: [0, 5], + domain: [0, 9], scaleType: ScaleType.Log, - groupId: 'group1', + groupId: 'group2', isBandScale: false, type: 'yDomain', }, { - domain: [0, 9], + domain: [0, 5], scaleType: ScaleType.Log, - groupId: 'group2', + groupId: 'group1', isBandScale: false, type: 'yDomain', }, ]); - expect(domains.formattedDataSeries.stacked).toMatchSnapshot(); - expect(domains.formattedDataSeries.nonStacked).toMatchSnapshot(); + expect(domains.formattedDataSeries.filter(({ isStacked }) => isStacked)).toMatchSnapshot(); + expect(domains.formattedDataSeries.filter(({ isStacked }) => !isStacked)).toMatchSnapshot(); }); it('should check if a SeriesCollectionValue item exists in a list of SeriesCollectionValue', () => { const dataSeriesValuesA: SeriesCollectionValue = { @@ -248,7 +231,8 @@ describe('Chart State utils', () => { const dg = new SeededDataGenerator(); // 4 groups generated const data = dg.generateGroupedSeries(50, 4); - const targetKey = 'spec{bar1}yAccessor{y}splitAccessors{g-b}'; + const targetKey = + 'groupId{__global__}spec{bar1}yAccessor{y}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}'; describe('empty series collection and specs', () => { it('it should return an empty map', () => { @@ -328,7 +312,6 @@ describe('Chart State utils', () => { it('it should return color from color function', () => { const actual = getCustomSeriesColors(barSeriesSpecs, barSeriesCollection); - expect(actual.size).toBe(1); expect(actual.get(targetKey)).toBe('aquamarine'); }); @@ -338,25 +321,19 @@ describe('Chart State utils', () => { describe('Geometries counts', () => { test('can compute stacked geometries counts', () => { - const area: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area = MockSeriesSpec.area({ id: 'area', groupId: 'group1', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line = MockSeriesSpec.line({ id: 'line', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -364,13 +341,10 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; - const bar: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bar = MockSeriesSpec.bar({ id: 'bar', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -378,197 +352,99 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [area, line, bar]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + const geometries = getGeometriesFromSpecs([area, line, bar]); + expect(geometries.geometriesCounts.bars).toBe(8); expect(geometries.geometriesCounts.linePoints).toBe(8); expect(geometries.geometriesCounts.areasPoints).toBe(8); expect(geometries.geometriesCounts.lines).toBe(2); expect(geometries.geometriesCounts.areas).toBe(2); }); + test('can compute non stacked geometries indexes', () => { - const line1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const line1 = MockSeriesSpec.line({ id: 'line1', groupId: 'group1', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Ordinal, xAccessor: 'x', yAccessors: ['y'], data: BARCHART_1Y0G, - }; - const line2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line2 = MockSeriesSpec.line({ id: 'line2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Ordinal, xAccessor: 'x', yAccessors: ['y'], data: BARCHART_1Y0G, - }; - const seriesSpecs: BasicSeriesSpec[] = [line1, line2]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + const geometries = getGeometriesFromSpecs([line1, line2]); + expect(geometries.geometriesIndex.size).toBe(4); expect(geometries.geometriesIndex.find(0)?.length).toBe(2); expect(geometries.geometriesIndex.find(1)?.length).toBe(2); expect(geometries.geometriesIndex.find(2)?.length).toBe(2); expect(geometries.geometriesIndex.find(3)?.length).toBe(2); }); + test('can compute stacked geometries indexes', () => { - const line1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const line1 = MockSeriesSpec.line({ id: 'line1', groupId: 'group1', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Ordinal, xAccessor: 'x', yAccessors: ['y'], stackAccessors: ['x'], data: BARCHART_1Y0G, - }; - const line2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line2 = MockSeriesSpec.line({ id: 'line2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Ordinal, xAccessor: 'x', yAccessors: ['y'], stackAccessors: ['x'], data: BARCHART_1Y0G, - }; - const seriesSpecs: BasicSeriesSpec[] = [line1, line2]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + + const geometries = getGeometriesFromSpecs([line1, line2]); + expect(geometries.geometriesIndex.size).toBe(4); expect(geometries.geometriesIndex.find(0)?.length).toBe(2); expect(geometries.geometriesIndex.find(1)?.length).toBe(2); expect(geometries.geometriesIndex.find(2)?.length).toBe(2); expect(geometries.geometriesIndex.find(3)?.length).toBe(2); }); + test('can compute non stacked geometries counts', () => { - const area: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area = MockSeriesSpec.area({ id: 'area', groupId: 'group1', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line = MockSeriesSpec.line({ id: 'line', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const bar: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bar = MockSeriesSpec.bar({ id: 'bar', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -588,36 +464,9 @@ describe('Chart State utils', () => { displayValueSettings: { showValueLabel: true, }, - }; - const seriesSpecs: BasicSeriesSpec[] = [area, line, bar]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + const geometries = getGeometriesFromSpecs([area, line, bar]); + expect(geometries.geometriesCounts.bars).toBe(8); expect(geometries.geometriesCounts.linePoints).toBe(8); expect(geometries.geometriesCounts.areasPoints).toBe(8); @@ -625,74 +474,39 @@ describe('Chart State utils', () => { expect(geometries.geometriesCounts.areas).toBe(2); }); test('can compute line geometries counts', () => { - const line1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const line1 = MockSeriesSpec.line({ id: 'line1', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line2 = MockSeriesSpec.line({ id: 'line2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line3: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line3 = MockSeriesSpec.line({ id: 'line3', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [line1, line2, line3]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + + const geometries = getGeometriesFromSpecs([line1, line2, line3]); + expect(geometries.geometriesCounts.bars).toBe(0); expect(geometries.geometriesCounts.linePoints).toBe(24); expect(geometries.geometriesCounts.areasPoints).toBe(0); @@ -700,74 +514,39 @@ describe('Chart State utils', () => { expect(geometries.geometriesCounts.areas).toBe(0); }); test('can compute area geometries counts', () => { - const area1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area1 = MockSeriesSpec.area({ id: 'area1', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const area2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const area2 = MockSeriesSpec.area({ id: 'area2', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const area3: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const area3 = MockSeriesSpec.area({ id: 'area3', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [area1, area2, area3]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + + const geometries = getGeometriesFromSpecs([area1, area2, area3]); + expect(geometries.geometriesCounts.bars).toBe(0); expect(geometries.geometriesCounts.linePoints).toBe(0); expect(geometries.geometriesCounts.areasPoints).toBe(24); @@ -775,12 +554,9 @@ describe('Chart State utils', () => { expect(geometries.geometriesCounts.areas).toBe(6); }); test('can compute line geometries with custom style', () => { - const line1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const line1 = MockSeriesSpec.line({ id: 'line1', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -795,69 +571,37 @@ describe('Chart State utils', () => { }, }, data: BARCHART_1Y1G, - }; - const line2: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line2 = MockSeriesSpec.line({ id: 'line2', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line3: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line3 = MockSeriesSpec.line({ id: 'line3', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [line1, line2, line3]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); - expect(geometries.geometries.lines[0].color).toBe('violet'); - expect(geometries.geometries.lines[0].seriesLineStyle).toEqual({ + }); + + const geometries = getGeometriesFromSpecs([line1, line2, line3]); + + expect(geometries.geometries.lines[0].value.color).toBe('violet'); + expect(geometries.geometries.lines[0].value.seriesLineStyle).toEqual({ visible: true, strokeWidth: 100, // the override strokeWidth opacity: 1, }); - expect(geometries.geometries.lines[0].seriesPointStyle).toEqual({ + expect(geometries.geometries.lines[0].value.seriesPointStyle).toEqual({ visible: true, fill: 'green', // the override strokeWidth opacity: 1, @@ -866,12 +610,9 @@ describe('Chart State utils', () => { }); }); test('can compute area geometries with custom style', () => { - const area1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area1 = MockSeriesSpec.area({ id: 'area1', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -890,74 +631,42 @@ describe('Chart State utils', () => { opacity: 0.2, }, }, - }; - const area2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const area2 = MockSeriesSpec.area({ id: 'area2', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const area3: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const area3 = MockSeriesSpec.area({ id: 'area3', groupId: 'group2', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [area1, area2, area3]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); - expect(geometries.geometries.areas[0].color).toBe('violet'); - expect(geometries.geometries.areas[0].seriesAreaStyle).toEqual({ + }); + + const geometries = getGeometriesFromSpecs([area1, area2, area3]); + + expect(geometries.geometries.areas[0].value.color).toBe('violet'); + expect(geometries.geometries.areas[0].value.seriesAreaStyle).toEqual({ visible: true, fill: 'area-fill-custom-color', opacity: 0.2, }); - expect(geometries.geometries.areas[0].seriesAreaLineStyle).toEqual({ + expect(geometries.geometries.areas[0].value.seriesAreaLineStyle).toEqual({ visible: true, strokeWidth: 100, opacity: 1, }); - expect(geometries.geometries.areas[0].seriesPointStyle).toEqual({ + expect(geometries.geometries.areas[0].value.seriesPointStyle).toEqual({ visible: false, fill: 'point-fill-custom-color', // the override strokeWidth opacity: 1, @@ -966,74 +675,39 @@ describe('Chart State utils', () => { }); }); test('can compute bars geometries counts', () => { - const bars1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const bars1 = MockSeriesSpec.bar({ id: 'bars1', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const bars2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bars2 = MockSeriesSpec.bar({ id: 'bars2', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const bars3: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bars3 = MockSeriesSpec.bar({ id: 'bars3', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [bars1, bars2, bars3]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { ...LIGHT_THEME, colors: chartColors }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); + }); + + const geometries = getGeometriesFromSpecs([bars1, bars2, bars3]); + expect(geometries.geometriesCounts.bars).toBe(24); expect(geometries.geometriesCounts.linePoints).toBe(0); expect(geometries.geometriesCounts.areasPoints).toBe(0); @@ -1041,111 +715,33 @@ describe('Chart State utils', () => { expect(geometries.geometriesCounts.areas).toBe(0); }); test('can compute the bar offset in mixed charts', () => { - const line1: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const line1 = MockSeriesSpec.line({ id: 'line1', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const bar1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bar1 = MockSeriesSpec.bar({ id: 'line3', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const seriesSpecs: BasicSeriesSpec[] = [line1, bar1]; - const axesSpecs: AxisSpec[] = []; - const chartRotation = 0; - const chartDimensions = { width: 100, height: 100, top: 0, left: 0 }; - const chartColors = { - vizColors: ['violet', 'green', 'blue'], - defaultVizColor: 'red', - }; - const chartTheme = { - ...LIGHT_THEME, - scales: { - barsPadding: 0, - histogramPadding: 0, - }, - }; - const domainsByGroupId = mergeYCustomDomainsByGroupId(axesSpecs, chartRotation); - const seriesDomains = computeSeriesDomains(seriesSpecs, domainsByGroupId); - const seriesColorMap = getSeriesColors( - seriesDomains.seriesCollection, - chartColors, - new Map(), - emptySeriesOverrides, - ); - const geometries = computeSeriesGeometries( - seriesSpecs, - seriesDomains.xDomain, - seriesDomains.yDomain, - seriesDomains.formattedDataSeries, - seriesColorMap, - chartTheme, - chartDimensions, - chartRotation, - axesSpecs, - false, - ); - expect(geometries.geometries.bars[0].x).toBe(0); + }); + + const geometries = getGeometriesFromSpecs([line1, bar1]); + + expect(geometries.geometries.bars[0].value[0].x).toBe(0); }); }); - test.skip('can merge geometry indexes', () => { - const map1 = new Map(); - map1.set('a', [ - { - radius: 10, - x: 0, - y: 0, - color: '#1EA593', - value: { x: 0, y: 5, accessor: BandedAccessorType.Y1, mark: null, datum: { x: 0, y: 5 } }, - transform: { x: 0, y: 0 }, - seriesIdentifier: { - specId: 'line1', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: [], - key: '', - }, - }, - ]); - const map2 = new Map(); - map2.set('a', [ - { - radius: 10, - x: 0, - y: 175.8, - color: '#2B70F7', - value: { x: 0, y: 2, accessor: BandedAccessorType.Y1, mark: null, datum: { x: 0, y: 5 } }, - transform: { x: 0, y: 0 }, - seriesIdentifier: { - specId: 'line2', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: [], - key: '', - }, - }, - ]); - // const merged = mergeGeometriesIndexes(map1, map2); - // expect(merged.get('a')).toBeDefined(); - // expect(merged.get('a')?.length).toBe(2); - }); + test('can compute xScaleOffset dependent on histogram mode', () => { const domain = [0, 10]; const range: [number, number] = [0, 100]; @@ -1168,25 +764,19 @@ describe('Chart State utils', () => { expect(computeXScaleOffset(scale, histogramModeEnabled, HistogramModeAlignments.End)).toBe(-5); }); test('can determine if histogram mode is enabled', () => { - const area: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area = MockSeriesSpec.area({ id: 'area', groupId: 'group1', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line = MockSeriesSpec.line({ id: 'line', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -1194,13 +784,10 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; - const basicBar: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const basicBar = MockSeriesSpec.bar({ id: 'bar', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -1208,22 +795,17 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; - const histogramBar: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const histogramBar = MockSeriesSpec.histogramBar({ id: 'histo', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], - stackAccessors: ['x'], data: BARCHART_1Y1G, - enableHistogramMode: true, - }; + }); let seriesMap: BasicSeriesSpec[] = [area, line, basicBar, histogramBar]; expect(isHistogramModeEnabled(seriesMap)).toBe(true); @@ -1237,25 +819,19 @@ describe('Chart State utils', () => { test('can set the bar series accessors dependent on histogram mode', () => { const isNotHistogramEnabled = false; const isHistogramEnabled = true; - const area: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const area = MockSeriesSpec.area({ id: 'area', groupId: 'group1', - seriesType: SeriesTypes.Area, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], data: BARCHART_1Y1G, - }; - const line: LineSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const line = MockSeriesSpec.line({ id: 'line', groupId: 'group2', - seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -1263,13 +839,10 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['x'], data: BARCHART_1Y1G, - }; - const bar: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + }); + const bar = MockSeriesSpec.bar({ id: 'bar', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', @@ -1277,7 +850,7 @@ describe('Chart State utils', () => { splitSeriesAccessors: ['g'], stackAccessors: ['foo'], data: BARCHART_1Y1G, - }; + }); const seriesMap = new Map([ [area.id, area], [line.id, line], @@ -1292,19 +865,16 @@ describe('Chart State utils', () => { setBarSeriesAccessors(isHistogramEnabled, seriesMap); expect(bar.stackAccessors).toEqual(['foo', 'g']); // add another bar - const bar2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const bar2 = MockSeriesSpec.bar({ id: 'bar2', groupId: 'group2', - seriesType: SeriesTypes.Bar, yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['bar'], data: BARCHART_1Y1G, - }; + }); seriesMap.set(bar2.id, bar2); setBarSeriesAccessors(isHistogramEnabled, seriesMap); expect(bar2.stackAccessors).toEqual(['y', 'bar']); diff --git a/src/chart_types/xy_chart/state/utils/utils.ts b/src/chart_types/xy_chart/state/utils/utils.ts index 7d5300d194..b2ace9fa1d 100644 --- a/src/chart_types/xy_chart/state/utils/utils.ts +++ b/src/chart_types/xy_chart/state/utils/utils.ts @@ -19,31 +19,40 @@ import { SeriesKey, SeriesIdentifier } from '../../../../commons/series_id'; import { Scale } from '../../../../scales'; +import { GroupBySpec } from '../../../../specs'; import { OrderBy } from '../../../../specs/settings'; import { mergePartial, Rotation, Color, isUniqueArray } from '../../../../utils/commons'; import { CurveType } from '../../../../utils/curves'; -import { Dimensions } from '../../../../utils/dimensions'; +import { Dimensions, Size } from '../../../../utils/dimensions'; import { Domain } from '../../../../utils/domain'; -import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, BubbleGeometry } from '../../../../utils/geometry'; +import { + PointGeometry, + BarGeometry, + AreaGeometry, + LineGeometry, + BubbleGeometry, + PerPanel, +} from '../../../../utils/geometry'; import { GroupId, SpecId } from '../../../../utils/ids'; import { ColorConfig, Theme } from '../../../../utils/themes/theme'; -import { XDomain, YDomain } from '../../domains/types'; +import { getPredicateFn, Predicate } from '../../../heatmap/utils/commons'; +import { XDomain } from '../../domains/types'; import { mergeXDomain } from '../../domains/x_domain'; -import { mergeYDomain, splitSpecsByGroupId } from '../../domains/y_domain'; +import { isStackedSpec, mergeYDomain } from '../../domains/y_domain'; import { renderArea, renderBars, renderLine, renderBubble, isDatumFilled } from '../../rendering/rendering'; import { defaultTickFormatter } from '../../utils/axis_utils'; import { fillSeries } from '../../utils/fill_series'; +import { groupBy } from '../../utils/group_data_series'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; -import { computeXScale, computeYScales, countBarsInCluster } from '../../utils/scales'; +import { computeXScale, computeYScales } from '../../utils/scales'; import { DataSeries, SeriesCollectionValue, getSeriesIndex, - FormattedDataSeries, - getFormattedDataseries, - getDataSeriesBySpecId, - getSeriesKey, + getFormattedDataSeries, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, + getSeriesKey, } from '../../utils/series'; import { AxisSpec, @@ -59,15 +68,13 @@ import { FitConfig, isBubbleSeriesSpec, YDomainRange, - SeriesTypes, StackMode, } from '../../utils/specs'; +import { SmallMultipleScales } from '../selectors/compute_small_multiple_scales'; +import { isHorizontalRotation } from './common'; import { getSpecsById, getAxesSpecForSpecId } from './spec'; import { SeriesDomainsAndData, ComputedGeometries, GeometriesCounts, Transform, LastValues } from './types'; -export const MAX_ANIMATABLE_BARS = 300; -export const MAX_ANIMATABLE_LINES_AREA_POINTS = 600; - /** * Adds or removes series from array or series * @param series @@ -90,10 +97,9 @@ export function updateDeselectedDataSeries( } /** - * Return map assocition between `seriesKey` and only the custom colors string + * Return map association between `seriesKey` and only the custom colors string * @param seriesSpecs * @param seriesCollection - * @param seriesColorOverrides color override from legend * @internal */ export function getCustomSeriesColors( @@ -130,76 +136,41 @@ export function getCustomSeriesColors( return updatedCustomSeriesColors; } -function getLastValues( - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, - xDomain: XDomain, -): Map { +function getLastValues(dataSeries: DataSeries[], xDomain: XDomain): Map { const lastValues = new Map(); // we need to get the latest - formattedDataSeries.stacked.forEach(({ dataSeries, stackMode }) => { - dataSeries.forEach((series) => { - if (series.data.length === 0) { - return; - } - - const last = series.data[series.data.length - 1]; - if (!last) { - return; - } - if (isDatumFilled(last)) { - return; - } - - if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { - // we have a dataset that is not filled with all x values - // and the last value of the series is not the last value for every series - // let's skip it - return; - } - - const { y0, y1, initialY0, initialY1 } = last; - const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier); - - if (stackMode === StackMode.Percentage) { - const y1InPercentage = y1 === null || y0 === null ? null : y1 - y0; - lastValues.set(seriesKey, { y0, y1: y1InPercentage }); - return; - } - if (initialY0 !== null || initialY1 !== null) { - lastValues.set(seriesKey, { y0: initialY0, y1: initialY1 }); - } - }); - }); + dataSeries.forEach((series) => { + if (series.data.length === 0) { + return; + } - formattedDataSeries.nonStacked.forEach(({ dataSeries }) => { - dataSeries.forEach((series) => { - if (series.data.length === 0) { - return; - } - const last = series.data[series.data.length - 1]; - if (!last) { - return; - } - if (isDatumFilled(last)) { - return; - } + const last = series.data[series.data.length - 1]; + if (!last) { + return; + } + if (isDatumFilled(last)) { + return; + } - if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { - // we have a dataset that is not filled with all x values - // and the last value of the series is not the last value for every series - // let's skip it - return; - } + if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { + // we have a dataset that is not filled with all x values + // and the last value of the series is not the last value for every series + // let's skip it + return; + } - const { initialY1, initialY0 } = last; - const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier); + const { y0, y1, initialY0, initialY1 } = last; + const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier, series.groupId); + if (series.stackMode === StackMode.Percentage) { + const y1InPercentage = y1 === null || y0 === null ? null : y1 - y0; + lastValues.set(seriesKey, { y0, y1: y1InPercentage }); + return; + } + if (initialY0 !== null || initialY1 !== null) { lastValues.set(seriesKey, { y0: initialY0, y1: initialY1 }); - }); + } }); return lastValues; } @@ -215,6 +186,7 @@ function getLastValues( * @param enableVislibSeriesSort is optional; if not specified in , * then all series will be factored into computations. Otherwise, selectedDataSeries * is used to restrict the computation for just the selected series + * @param smallMultiples * @returns `SeriesDomainsAndData` * @internal */ @@ -225,39 +197,27 @@ export function computeSeriesDomains( customXDomain?: DomainRange | Domain, orderOrdinalBinsBy?: OrderBy, enableVislibSeriesSort?: boolean, + smallMultiples?: { vertical?: GroupBySpec; horizontal?: GroupBySpec }, ): SeriesDomainsAndData { - const { dataSeriesBySpecId, xValues, seriesCollection, fallbackScale } = getDataSeriesBySpecId( + const { dataSeries, xValues, seriesCollection, fallbackScale, smHValues, smVValues } = getDataSeriesFromSpecs( seriesSpecs, deselectedDataSeries, orderOrdinalBinsBy, enableVislibSeriesSort, + smallMultiples, ); // compute the x domain merging any custom domain const xDomain = mergeXDomain(seriesSpecs, xValues, customXDomain, fallbackScale); - const specsByGroupIds = splitSpecsByGroupId(seriesSpecs); - // fill series with missing x values - const filledDataSeriesBySpecId = fillSeries( - dataSeriesBySpecId, - xValues, - seriesSpecs, - xDomain.scaleType, - specsByGroupIds, - ); + const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.scaleType); - const formattedDataSeries = getFormattedDataseries( - filledDataSeriesBySpecId, - xValues, - xDomain.scaleType, - seriesSpecs, - specsByGroupIds, - ); + const formattedDataSeries = getFormattedDataSeries(seriesSpecs, filledDataSeries, xValues, xDomain.scaleType); // let's compute the yDomain after computing all stacked values - const yDomain = mergeYDomain(formattedDataSeries, seriesSpecs, customYDomainsByGroupId); + const yDomain = mergeYDomain(formattedDataSeries, customYDomainsByGroupId); - // we need to get the last values from the formatted dataseries + // we need to get the last values from the formattedDataSeries // because we change the format if we are on percentage mode const lastValues = getLastValues(formattedDataSeries, xDomain); const updatedSeriesCollection = new Map(); @@ -270,9 +230,18 @@ export function computeSeriesDomains( updatedSeriesCollection.set(key, updatedColorSet); }); + // sort small multiples values + const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; + const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate)); + + const verticalPredicate = smallMultiples?.vertical?.sort ?? Predicate.DataIndex; + const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate)); + return { xDomain, yDomain, + smHDomain, + smVDomain, formattedDataSeries, seriesCollection: updatedSeriesCollection, }; @@ -281,156 +250,79 @@ export function computeSeriesDomains( /** @internal */ export function computeSeriesGeometries( seriesSpecs: BasicSeriesSpec[], - xDomain: XDomain, - yDomain: YDomain[], - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, + { xDomain, yDomain, formattedDataSeries }: SeriesDomainsAndData, seriesColorMap: Map, chartTheme: Theme, - chartDims: Dimensions, chartRotation: Rotation, axesSpecs: AxisSpec[], + smallMultiplesScales: SmallMultipleScales, enableHistogramMode: boolean, ): ComputedGeometries { const chartColors: ColorConfig = chartTheme.colors; - const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding; - const width = [0, 180].includes(chartRotation) ? chartDims.width : chartDims.height; - const height = [0, 180].includes(chartRotation) ? chartDims.height : chartDims.width; - // const { width, height } = chartDims; - const { stacked, nonStacked } = formattedDataSeries; + const barDataSeries = formattedDataSeries.filter(({ spec }) => isBarSeriesSpec(spec)); + // compute max bar in cluster per panel + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); - // compute how many series are clustered - const { stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster(stacked, nonStacked); - // compute scales - const xScale = computeXScale({ - xDomain, - totalBarsInCluster, - range: [0, width], - barsPadding, - enableHistogramMode, - }); - const yScales = computeYScales({ yDomains: yDomain, range: [height, 0] }); + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, enableHistogramMode); + }, + false, + ); - // compute colors + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); - // compute geometries - const points: PointGeometry[] = []; - const areas: AreaGeometry[] = []; - const bars: BarGeometry[] = []; - const lines: LineGeometry[] = []; - const bubbles: BubbleGeometry[] = []; - const geometriesIndex = new IndexedGeometryMap(); - let orderIndex = 0; - const geometriesCounts: GeometriesCounts = { - points: 0, - bars: 0, - areas: 0, - areasPoints: 0, - lines: 0, - linePoints: 0, - bubbles: 0, - bubblePoints: 0, - }; - - formattedDataSeries.stacked.forEach((dataSeriesGroup) => { - const { groupId, dataSeries, counts, stackMode } = dataSeriesGroup; - const yScale = yScales.get(groupId); - if (!yScale) { - return; - } + const { horizontal, vertical } = smallMultiplesScales; - const geometries = renderGeometries( - orderIndex, - totalBarsInCluster, - true, - dataSeries, - xScale, - yScale, - seriesSpecs, - seriesColorMap, - chartColors.defaultVizColor, - axesSpecs, - chartTheme, - enableHistogramMode, - chartRotation, - stackMode, - ); - orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + 1 : orderIndex; - areas.push(...geometries.areas); - lines.push(...geometries.lines); - bars.push(...geometries.bars); - bubbles.push(...geometries.bubbles); - points.push(...geometries.points); - geometriesIndex.merge(geometries.indexedGeometryMap); - // update counts - geometriesCounts.points += geometries.geometriesCounts.points; - geometriesCounts.bars += geometries.geometriesCounts.bars; - geometriesCounts.areas += geometries.geometriesCounts.areas; - geometriesCounts.areasPoints += geometries.geometriesCounts.areasPoints; - geometriesCounts.lines += geometries.geometriesCounts.lines; - geometriesCounts.linePoints += geometries.geometriesCounts.linePoints; - geometriesCounts.bubbles += geometries.geometriesCounts.bubbles; - geometriesCounts.bubblePoints += geometries.geometriesCounts.bubblePoints; + const yScales = computeYScales({ + yDomains: yDomain, + range: [isHorizontalRotation(chartRotation) ? vertical.bandwidth : horizontal.bandwidth, 0], }); - orderIndex = 0; - formattedDataSeries.nonStacked.forEach((dataSeriesGroup) => { - const { groupId, dataSeries, counts } = dataSeriesGroup; - const yScale = yScales.get(groupId); - if (!yScale) { - return; - } - const geometries = renderGeometries( - stackedBarsInCluster + orderIndex, - totalBarsInCluster, - false, - dataSeries, - xScale, - yScale, - seriesSpecs, - seriesColorMap, - chartColors.defaultVizColor, - axesSpecs, - chartTheme, - enableHistogramMode, - chartRotation, - ); - orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + counts[SeriesTypes.Bar] : orderIndex; - - areas.push(...geometries.areas); - lines.push(...geometries.lines); - bars.push(...geometries.bars); - bubbles.push(...geometries.bubbles); - points.push(...geometries.points); - - geometriesIndex.merge(geometries.indexedGeometryMap); - // update counts - geometriesCounts.points += geometries.geometriesCounts.points; - geometriesCounts.bars += geometries.geometriesCounts.bars; - geometriesCounts.areas += geometries.geometriesCounts.areas; - geometriesCounts.areasPoints += geometries.geometriesCounts.areasPoints; - geometriesCounts.lines += geometries.geometriesCounts.lines; - geometriesCounts.linePoints += geometries.geometriesCounts.linePoints; - geometriesCounts.bubbles += geometries.geometriesCounts.bubbles; - geometriesCounts.bubblePoints += geometries.geometriesCounts.bubblePoints; + const computedGeoms = renderGeometries( + formattedDataSeries, + xDomain, + yScales, + vertical, + horizontal, + barIndexByPanel, + seriesSpecs, + seriesColorMap, + chartColors.defaultVizColor, + axesSpecs, + chartTheme, + enableHistogramMode, + chartRotation, + ); + + const totalBarsInCluster = Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); + + const xScale = computeXScale({ + xDomain, + totalBarsInCluster, + range: [0, isHorizontalRotation(chartRotation) ? horizontal.bandwidth : vertical.bandwidth], + barsPadding: enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding, + enableHistogramMode, }); + return { scales: { xScale, yScales, }, - geometries: { - points, - areas, - bars, - lines, - bubbles, - }, - geometriesIndex, - geometriesCounts, + ...computedGeoms, }; } @@ -486,37 +378,28 @@ export function computeXScaleOffset( } function renderGeometries( - indexOffset: number, - clusteredCount: number, - isStacked: boolean, dataSeries: DataSeries[], - xScale: Scale, - yScale: Scale, + xDomain: XDomain, + yScales: Map, + smVScale: Scale, + smHScale: Scale, + barIndexOrderPerPanel: Record, seriesSpecs: BasicSeriesSpec[], seriesColorsMap: Map, defaultColor: string, axesSpecs: AxisSpec[], chartTheme: Theme, enableHistogramMode: boolean, - chartRotation: number, - stackMode?: StackMode, -): { - points: PointGeometry[]; - bars: BarGeometry[]; - areas: AreaGeometry[]; - lines: LineGeometry[]; - bubbles: BubbleGeometry[]; - indexedGeometryMap: IndexedGeometryMap; - geometriesCounts: GeometriesCounts; -} { + chartRotation: Rotation, +): Omit { const len = dataSeries.length; let i; const points: PointGeometry[] = []; - const bars: BarGeometry[] = []; - const areas: AreaGeometry[] = []; - const lines: LineGeometry[] = []; - const bubbles: BubbleGeometry[] = []; - const indexedGeometryMap = new IndexedGeometryMap(); + const bars: Array> = []; + const areas: Array> = []; + const lines: Array> = []; + const bubbles: Array> = []; + const geometriesIndex = new IndexedGeometryMap(); const isMixedChart = isUniqueArray(seriesSpecs, ({ seriesType }) => seriesType) && seriesSpecs.length > 1; const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; const geometriesCounts: GeometriesCounts = { @@ -529,18 +412,52 @@ function renderGeometries( bubbles: 0, bubblePoints: 0, }; - let barIndexOffset = 0; + const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding; + for (i = 0; i < len; i++) { const ds = dataSeries[i]; const spec = getSpecsById(seriesSpecs, ds.specId); if (spec === undefined) { continue; } + // compute the y scale + const yScale = yScales.get(ds.groupId); + if (!yScale) { + continue; + } + // compute the panel unique key + const barPanelKey = [ds.smVerticalAccessorValue, ds.smHorizontalAccessorValue].join('|'); + const barIndexOrder = barIndexOrderPerPanel[barPanelKey]; + // compute x scale + const xScale = computeXScale({ + xDomain, + totalBarsInCluster: barIndexOrder?.length ?? 0, + range: [0, isHorizontalRotation(chartRotation) ? smHScale.bandwidth : smVScale.bandwidth], + barsPadding, + enableHistogramMode, + }); - const color = seriesColorsMap.get(getSeriesKey(ds)) || defaultColor; + const { stackMode } = ds; + + const leftPos = smHScale.scale(ds.smHorizontalAccessorValue) || 0; + const topPos = smVScale.scale(ds.smVerticalAccessorValue) || 0; + const panel: Dimensions = { + width: smHScale.bandwidth, + height: smVScale.bandwidth, + top: topPos, + left: leftPos, + }; + + const color = seriesColorsMap.get(ds.key) || defaultColor; if (isBarSeriesSpec(spec)) { - const shift = isStacked ? indexOffset : indexOffset + barIndexOffset; + const key = getBarIndexKey(ds, enableHistogramMode); + const shift = barIndexOrder.indexOf(key); + + if (shift === -1) { + // skip bar dataSeries if index is not available + continue; + } const barSeriesStyle = mergePartial(chartTheme.barSeriesStyle, spec.barSeriesStyle, { mergeOptionalPartialValues: true, }); @@ -557,6 +474,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, barSeriesStyle, displayValueSettings, @@ -565,22 +483,27 @@ function renderGeometries( stackMode, chartRotation, ); - indexedGeometryMap.merge(renderedBars.indexedGeometryMap); - bars.push(...renderedBars.barGeometries); + geometriesIndex.merge(renderedBars.indexedGeometryMap); + bars.push({ + panel, + value: renderedBars.barGeometries, + }); geometriesCounts.bars += renderedBars.barGeometries.length; - barIndexOffset += 1; } else if (isBubbleSeriesSpec(spec)) { - const bubbleShift = clusteredCount > 0 ? clusteredCount : 1; + const bubbleShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const bubbleSeriesStyle = spec.bubbleSeriesStyle ? mergePartial(chartTheme.bubbleSeriesStyle, spec.bubbleSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.bubbleSeriesStyle; + const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode); const renderedBubbles = renderBubble( (xScale.bandwidth * bubbleShift) / 2, ds, xScale, yScale, color, + panel, isBandedSpec(spec.y0Accessors), + xScaleOffset, bubbleSeriesStyle, { enabled: spec.markSizeAccessor !== undefined, @@ -589,12 +512,15 @@ function renderGeometries( isMixedChart, spec.pointStyleAccessor, ); - indexedGeometryMap.merge(renderedBubbles.indexedGeometryMap); - bubbles.push(renderedBubbles.bubbleGeometry); + geometriesIndex.merge(renderedBubbles.indexedGeometryMap); + bubbles.push({ + panel, + value: renderedBubbles.bubbleGeometry, + }); geometriesCounts.bubblePoints += renderedBubbles.bubbleGeometry.points.length; geometriesCounts.bubbles += 1; } else if (isLineSeriesSpec(spec)) { - const lineShift = clusteredCount > 0 ? clusteredCount : 1; + const lineShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const lineSeriesStyle = spec.lineSeriesStyle ? mergePartial(chartTheme.lineSeriesStyle, spec.lineSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.lineSeriesStyle; @@ -607,6 +533,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, spec.curve || CurveType.LINEAR, isBandedSpec(spec.y0Accessors), @@ -619,12 +546,16 @@ function renderGeometries( spec.pointStyleAccessor, hasFitFnConfigured(spec.fit), ); - indexedGeometryMap.merge(renderedLines.indexedGeometryMap); - lines.push(renderedLines.lineGeometry); + + geometriesIndex.merge(renderedLines.indexedGeometryMap); + lines.push({ + panel, + value: renderedLines.lineGeometry, + }); geometriesCounts.linePoints += renderedLines.lineGeometry.points.length; geometriesCounts.lines += 1; } else if (isAreaSeriesSpec(spec)) { - const areaShift = clusteredCount > 0 ? clusteredCount : 1; + const areaShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const areaSeriesStyle = spec.areaSeriesStyle ? mergePartial(chartTheme.areaSeriesStyle, spec.areaSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.areaSeriesStyle; @@ -635,6 +566,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, spec.curve || CurveType.LINEAR, isBandedSpec(spec.y0Accessors), @@ -644,34 +576,38 @@ function renderGeometries( enabled: spec.markSizeAccessor !== undefined, ratio: chartTheme.markSizeRatio, }, - isStacked, + spec.stackAccessors ? spec.stackAccessors.length > 0 : false, spec.pointStyleAccessor, hasFitFnConfigured(spec.fit), - stackMode, ); - indexedGeometryMap.merge(renderedAreas.indexedGeometryMap); - areas.push(renderedAreas.areaGeometry); + geometriesIndex.merge(renderedAreas.indexedGeometryMap); + areas.push({ + panel, + value: renderedAreas.areaGeometry, + }); geometriesCounts.areasPoints += renderedAreas.areaGeometry.points.length; geometriesCounts.areas += 1; } } return { - points, - bars, - areas, - lines, - bubbles, - indexedGeometryMap, + geometries: { + points, + bars, + areas, + lines, + bubbles, + }, + geometriesIndex, geometriesCounts, }; } /** @internal */ -export function computeChartTransform(chartDimensions: Dimensions, chartRotation: Rotation): Transform { +export function computeChartTransform({ width, height }: Size, chartRotation: Rotation): Transform { if (chartRotation === 90) { return { - x: chartDimensions.width, + x: width, y: 0, rotate: 90, }; @@ -679,14 +615,14 @@ export function computeChartTransform(chartDimensions: Dimensions, chartRotation if (chartRotation === -90) { return { x: 0, - y: chartDimensions.height, + y: height, rotate: -90, }; } if (chartRotation === 180) { return { - x: chartDimensions.width, - y: chartDimensions.height, + x: width, + y: height, rotate: 180, }; } @@ -700,3 +636,16 @@ export function computeChartTransform(chartDimensions: Dimensions, chartRotation function hasFitFnConfigured(fit?: Fit | FitConfig) { return Boolean(fit && ((fit as FitConfig).type || fit) !== Fit.None); } + +/** @internal */ +export function getBarIndexKey( + { spec, specId, groupId, yAccessor, splitAccessors }: DataSeries, + histogramModeEnabled: boolean, +) { + const isStacked = isStackedSpec(spec, histogramModeEnabled); + if (isStacked) { + return [groupId, '__stacked__'].join('__-__'); + } + + return [groupId, specId, ...splitAccessors.values(), yAccessor].join('__-__'); +} diff --git a/src/chart_types/xy_chart/tooltip/tooltip.test.ts b/src/chart_types/xy_chart/tooltip/tooltip.test.ts index a743e062c2..e1e2cb0722 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.test.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.test.ts @@ -18,12 +18,14 @@ */ import { ChartTypes } from '../..'; +import { MockBarGeometry } from '../../../mocks'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; import { ScaleType } from '../../../scales/constants'; import { SpecTypes } from '../../../specs/constants'; import { Position, RecursivePartial } from '../../../utils/commons'; import { BarGeometry } from '../../../utils/geometry'; import { AxisStyle } from '../../../utils/themes/theme'; -import { AxisSpec, BarSeriesSpec, SeriesTypes, TickFormatter } from '../utils/specs'; +import { AxisSpec, BarSeriesSpec, TickFormatter } from '../utils/specs'; import { formatTooltip } from './tooltip'; const style: RecursivePartial = { @@ -36,23 +38,20 @@ const style: RecursivePartial = { describe('Tooltip formatting', () => { const SPEC_ID_1 = 'bar_1'; const SPEC_GROUP_ID_1 = 'bar_group_1'; - const SPEC_1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, + const SPEC_1 = MockSeriesSpec.bar({ id: SPEC_ID_1, groupId: SPEC_GROUP_ID_1, - seriesType: SeriesTypes.Bar, data: [], xAccessor: 0, yAccessors: [1], yScaleType: ScaleType.Linear, xScaleType: ScaleType.Linear, - }; - const bandedSpec = { + }); + const bandedSpec = MockSeriesSpec.bar({ ...SPEC_1, y0Accessors: [1], - }; - const YAXIS_SPEC: AxisSpec = { + }); + const YAXIS_SPEC = MockGlobalSpec.axis({ chartType: ChartTypes.XYAxis, specType: SpecTypes.Axis, id: 'axis_1', @@ -63,7 +62,7 @@ describe('Tooltip formatting', () => { showOverlappingTicks: false, style, tickFormat: jest.fn((d) => `${d}`), - }; + }); const seriesStyle = { rect: { opacity: 1, @@ -81,7 +80,7 @@ describe('Tooltip formatting', () => { padding: 2, }, }; - const indexedGeometry: BarGeometry = { + const indexedGeometry = MockBarGeometry.default({ x: 0, y: 0, width: 0, @@ -102,8 +101,8 @@ describe('Tooltip formatting', () => { datum: { x: 1, y: 10 }, }, seriesStyle, - }; - const indexedBandedGeometry: BarGeometry = { + }); + const indexedBandedGeometry = MockBarGeometry.default({ x: 0, y: 0, width: 0, @@ -124,7 +123,7 @@ describe('Tooltip formatting', () => { datum: { x: 1, y: 10 }, }, seriesStyle, - }; + }); test('format simple tooltip', () => { const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC); diff --git a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap index ba2414c70e..7bb5d77a3a 100644 --- a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap +++ b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap @@ -10,101 +10,64 @@ Array [ "y": 1, }, "initialY0": null, - "initialY1": null, + "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, - "y1": null, + "y1": 1, }, - ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{y-1}", - "seriesKeys": Array [ - 1, - "y1", - ], - "specId": "spec1", - "splitAccessors": Map { - "y" => 1, - }, - "yAccessor": "y1", - }, - Object { - "data": Array [ Object { "datum": Object { "x": 1, "y": 2, }, "initialY0": null, - "initialY1": null, + "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, - "y1": null, + "y1": 2, }, - ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{y-2}", - "seriesKeys": Array [ - 2, - "y1", - ], - "specId": "spec1", - "splitAccessors": Map { - "y" => 2, - }, - "yAccessor": "y1", - }, - Object { - "data": Array [ Object { "datum": Object { "x": 2, "y": 10, }, "initialY0": null, - "initialY1": null, + "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, - "y1": null, + "y1": 10, }, - ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{y-10}", - "seriesKeys": Array [ - 10, - "y1", - ], - "specId": "spec1", - "splitAccessors": Map { - "y" => 10, - }, - "yAccessor": "y1", - }, - Object { - "data": Array [ Object { "datum": Object { "x": 3, "y": 6, }, "initialY0": null, - "initialY1": null, + "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, - "y1": null, + "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{y-6}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ - 6, - "y1", + "y", ], "specId": "spec1", - "splitAccessors": Map { - "y" => 6, - }, - "yAccessor": "y1", + "splitAccessors": Map {}, + "yAccessor": "y", }, ] `; @@ -122,85 +85,113 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, }, Object { "datum": Object { - "g": "b", - "x": 0, + "g": "a", + "x": 1, "y": 2, }, "initialY0": null, "initialY1": 2, "mark": null, - "x": 0, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, "y0": null, "y1": 2, }, Object { "datum": Object { "g": "a", - "x": 1, - "y": 2, + "x": 2, + "y": 3, }, "initialY0": null, - "initialY1": 2, + "initialY1": 3, "mark": null, - "x": 1, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, "y0": null, - "y1": 2, + "y1": 3, }, Object { "datum": Object { - "g": "b", - "x": 1, - "y": 3, + "g": "a", + "x": 3, + "y": 4, }, "initialY0": null, - "initialY1": 3, + "initialY1": 4, "mark": null, - "x": 1, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, "y0": null, - "y1": 3, + "y1": 4, }, + ], + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "a", + "y", + ], + "specId": "spec1", + "splitAccessors": Map { + "g" => "a", + }, + "yAccessor": "y", + }, + Object { + "data": Array [ Object { "datum": Object { - "g": "a", - "x": 2, - "y": 3, + "g": "b", + "x": 0, + "y": 2, }, "initialY0": null, - "initialY1": 3, + "initialY1": 2, "mark": null, - "x": 2, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, "y0": null, - "y1": 3, + "y1": 2, }, Object { "datum": Object { "g": "b", - "x": 2, - "y": 4, + "x": 1, + "y": 3, }, "initialY0": null, - "initialY1": 4, + "initialY1": 3, "mark": null, - "x": 2, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, "y0": null, - "y1": 4, + "y1": 3, }, Object { "datum": Object { - "g": "a", - "x": 3, + "g": "b", + "x": 2, "y": 4, }, "initialY0": null, "initialY1": 4, "mark": null, - "x": 3, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, "y0": null, "y1": 4, }, @@ -213,17 +204,22 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 5, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ + "b", "y", ], "specId": "spec1", - "splitAccessors": Map {}, + "splitAccessors": Map { + "g" => "b", + }, "yAccessor": "y", }, ] @@ -243,6 +239,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -257,6 +255,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -271,6 +271,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -285,12 +287,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-s}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-s}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "s", @@ -315,6 +319,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -329,6 +335,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -343,6 +351,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 2, @@ -357,12 +367,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-p}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-a|g2-p}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "p", @@ -387,6 +399,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -401,6 +415,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -415,6 +431,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -429,12 +447,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-s}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-s}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "s", @@ -459,6 +479,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -473,6 +495,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -487,6 +511,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 4, @@ -501,12 +527,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-p}", + "key": "groupId{__global__}spec{spec1}yAccessor{y}splitAccessors{g1-b|g2-p}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "p", @@ -535,6 +563,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -548,6 +578,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -561,6 +593,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -574,12 +608,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y1", ], @@ -598,6 +634,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -611,6 +649,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 7, @@ -624,6 +664,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 2, @@ -637,12 +679,14 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 10, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y2", ], @@ -667,6 +711,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -681,6 +727,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -695,6 +743,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -709,12 +759,14 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 7, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -737,6 +789,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 4, @@ -751,6 +805,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -765,6 +821,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 5, @@ -779,12 +837,14 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 3, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y2", @@ -807,6 +867,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -821,6 +883,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -835,6 +899,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -849,12 +915,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -877,6 +945,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 6, @@ -891,6 +961,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 5, @@ -905,6 +977,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -919,12 +993,14 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y2", @@ -953,6 +1029,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -968,6 +1046,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -983,6 +1063,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -998,6 +1080,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 7, @@ -1013,12 +1097,14 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 7, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "direct-cdn", @@ -1044,6 +1130,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 4, @@ -1059,6 +1147,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -1074,6 +1164,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 5, @@ -1089,6 +1181,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 3, @@ -1104,12 +1198,14 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 3, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "direct-cdn", @@ -1135,6 +1231,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -1150,6 +1248,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -1165,6 +1265,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -1180,6 +1282,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 7, @@ -1195,12 +1299,14 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 7, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "indirect-cdn", @@ -1226,6 +1332,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 4, @@ -1241,6 +1349,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -1256,6 +1366,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 5, @@ -1271,6 +1383,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 3, @@ -1286,12 +1400,14 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 3, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "indirect-cdn", @@ -1317,6 +1433,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -1332,6 +1450,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -1347,6 +1467,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -1362,6 +1484,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, @@ -1377,12 +1501,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "direct-cdn", @@ -1408,6 +1534,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 6, @@ -1423,6 +1551,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 5, @@ -1438,6 +1568,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -1453,6 +1585,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 4, @@ -1468,12 +1602,14 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "direct-cdn", @@ -1499,6 +1635,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -1514,6 +1652,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -1529,6 +1669,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -1544,6 +1686,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, @@ -1559,12 +1703,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "indirect-cdn", @@ -1590,6 +1736,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 6, @@ -1605,6 +1753,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 5, @@ -1620,6 +1770,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -1635,6 +1787,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 4, @@ -1650,12 +1804,14 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "indirect-cdn", @@ -21866,7 +22022,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -21936,7 +22092,7 @@ Array [ "y1": 8, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22006,7 +22162,7 @@ Array [ "y1": 12, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-c}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-c}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "c", "y1", @@ -22076,7 +22232,7 @@ Array [ "y1": 16, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-d}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-d}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "d", "y1", @@ -22149,7 +22305,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -22215,7 +22371,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22288,7 +22444,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -22354,7 +22510,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22430,7 +22586,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -22504,7 +22660,7 @@ Array [ "y1": 8, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22580,7 +22736,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -22654,7 +22810,7 @@ Array [ "y1": 8, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22727,7 +22883,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", "y1", @@ -22793,7 +22949,7 @@ Array [ "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", "y1", @@ -22822,6 +22978,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -22837,6 +22995,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -22852,6 +23012,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -22867,6 +23029,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 7, @@ -22882,12 +23046,14 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 7, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "direct-cdn", @@ -22913,6 +23079,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 4, @@ -22928,6 +23096,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -22943,6 +23113,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 5, @@ -22958,6 +23130,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 3, @@ -22973,12 +23147,14 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 3, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "direct-cdn", @@ -23004,6 +23180,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -23019,6 +23197,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -23034,6 +23214,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -23049,6 +23231,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 7, @@ -23064,12 +23248,14 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 7, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "indirect-cdn", @@ -23095,6 +23281,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 4, @@ -23110,6 +23298,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -23125,6 +23315,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 5, @@ -23140,6 +23332,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 3, @@ -23155,12 +23349,14 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 3, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cdn.google.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cdn.google.com", "indirect-cdn", @@ -23186,6 +23382,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -23201,6 +23399,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -23216,6 +23416,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -23231,6 +23433,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, @@ -23246,12 +23450,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "direct-cdn", @@ -23277,6 +23483,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 6, @@ -23292,6 +23500,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 5, @@ -23307,6 +23517,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -23322,6 +23534,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 4, @@ -23337,12 +23551,14 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-direct-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "direct-cdn", @@ -23368,6 +23584,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -23383,6 +23601,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -23398,6 +23618,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 3, @@ -23413,6 +23635,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, @@ -23428,12 +23652,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "indirect-cdn", @@ -23459,6 +23685,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 6, @@ -23474,6 +23702,8 @@ Array [ "initialY0": null, "initialY1": 5, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 5, @@ -23489,6 +23719,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -23504,6 +23736,8 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 4, @@ -23519,12 +23753,14 @@ Array [ "initialY0": null, "initialY1": 4, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 6, "y0": null, "y1": 4, }, ], - "key": "spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}", + "key": "groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g1-cloudflare.com|g2-indirect-cdn}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "cloudflare.com", "indirect-cdn", @@ -23555,6 +23791,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 1, @@ -23570,6 +23808,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 1, @@ -23585,6 +23825,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 3, @@ -23600,6 +23842,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 3, @@ -23615,6 +23859,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 2, @@ -23630,6 +23876,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 2, @@ -23645,6 +23893,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 2, @@ -23660,6 +23910,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 2, @@ -23675,6 +23927,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 10, @@ -23690,6 +23944,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 10, @@ -23705,6 +23961,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 3, @@ -23720,6 +23978,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 3, @@ -23735,6 +23995,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 7, @@ -23750,6 +24012,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 7, @@ -23765,6 +24029,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 6, @@ -23780,6 +24046,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 6, @@ -23795,6 +24063,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 7, @@ -23810,6 +24080,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 7, @@ -23825,6 +24097,8 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 6, @@ -23840,12 +24114,14 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": "_all", "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y1}splitAccessors{}", + "key": "groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y1", ], @@ -23869,6 +24145,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -23882,6 +24160,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -23895,12 +24175,14 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, }, ], - "key": "spec{spec1}yAccessor{1}splitAccessors{2-a}", + "key": "groupId{__global__}spec{spec1}yAccessor{1}splitAccessors{2-a}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "a", 1, @@ -23922,6 +24204,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -23935,6 +24219,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 1, @@ -23948,12 +24234,14 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, }, ], - "key": "spec{spec1}yAccessor{1}splitAccessors{2-b}", + "key": "groupId{__global__}spec{spec1}yAccessor{1}splitAccessors{2-b}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "b", 1, @@ -23970,150 +24258,205 @@ Array [ exports[`Series should compute data series for stacked specs 1`] = ` Array [ Object { - "counts": Object { - "area": 0, - "bar": 0, - "bubble": 0, - "line": 2, - }, - "dataSeries": Array [ - Object { - "data": Array [ - Object { - "datum": Object { - "x": 0, - "y1": 1, - "y2": 3, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 0, - "y0": 0, - "y1": 1, - }, - Object { - "datum": Object { - "x": 1, - "y1": 2, - "y2": 7, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 1, - "y0": 0, - "y1": 2, - }, - Object { - "datum": Object { - "x": 2, - "y1": 1, - "y2": 2, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 1, - "mark": null, - "x": 2, - "y0": 0, - "y1": 1, - }, - Object { - "datum": Object { - "x": 3, - "y1": 6, - "y2": 10, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 6, - "mark": null, - "x": 3, - "y0": 0, - "y1": 6, - }, - ], - "key": "spec{spec2}yAccessor{y1}splitAccessors{}", - "seriesKeys": Array [ - "y1", - ], - "specId": "spec2", - "splitAccessors": Map {}, - "yAccessor": "y1", - }, - Object { - "data": Array [ - Object { - "datum": Object { - "x": 0, - "y1": 1, - "y2": 3, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 3, - "mark": null, - "x": 0, - "y0": 1, - "y1": 4, - }, - Object { - "datum": Object { - "x": 1, - "y1": 2, - "y2": 7, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 7, - "mark": null, - "x": 1, - "y0": 2, - "y1": 9, - }, - Object { - "datum": Object { - "x": 2, - "y1": 1, - "y2": 2, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 2, - "mark": null, - "x": 2, - "y0": 1, - "y1": 3, - }, - Object { - "datum": Object { - "x": 3, - "y1": 6, - "y2": 10, - }, - "filled": undefined, - "initialY0": null, - "initialY1": 10, - "mark": null, - "x": 3, - "y0": 6, - "y1": 16, - }, - ], - "key": "spec{spec2}yAccessor{y2}splitAccessors{}", - "seriesKeys": Array [ - "y2", - ], - "specId": "spec2", - "splitAccessors": Map {}, - "yAccessor": "y2", + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 0, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 1, + "y0": 0, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 1, + "mark": null, + "x": 2, + "y0": 0, + "y1": 1, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 6, + "mark": null, + "x": 3, + "y0": 0, + "y1": 6, }, ], - "groupId": "group2", - "stackMode": undefined, + "key": "groupId{group2}spec{spec2}yAccessor{y1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "y1", + ], + "specId": "spec2", + "splitAccessors": Map {}, + "yAccessor": "y1", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 3, + "mark": null, + "x": 0, + "y0": 1, + "y1": 4, + }, + Object { + "datum": Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 7, + "mark": null, + "x": 1, + "y0": 2, + "y1": 9, + }, + Object { + "datum": Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 2, + "mark": null, + "x": 2, + "y0": 1, + "y1": 3, + }, + Object { + "datum": Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + "filled": undefined, + "initialY0": null, + "initialY1": 10, + "mark": null, + "x": 3, + "y0": 6, + "y1": 16, + }, + ], + "key": "groupId{group2}spec{spec2}yAccessor{y2}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "y2", + ], + "specId": "spec2", + "splitAccessors": Map {}, + "yAccessor": "y2", + }, + Object { + "data": Array [ + Object { + "datum": Object { + "x": 0, + "y": 1, + }, + "initialY0": null, + "initialY1": 1, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 0, + "y0": null, + "y1": 1, + }, + Object { + "datum": Object { + "x": 1, + "y": 2, + }, + "initialY0": null, + "initialY1": 2, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 1, + "y0": null, + "y1": 2, + }, + Object { + "datum": Object { + "x": 2, + "y": 10, + }, + "initialY0": null, + "initialY1": 10, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 2, + "y0": null, + "y1": 10, + }, + Object { + "datum": Object { + "x": 3, + "y": 6, + }, + "initialY0": null, + "initialY1": 6, + "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "x": 3, + "y0": null, + "y1": 6, + }, + ], + "key": "groupId{group}spec{spec1}yAccessor{y}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", + "seriesKeys": Array [ + "y", + ], + "specId": "spec1", + "splitAccessors": Map {}, + "yAccessor": "y", }, ] `; @@ -24130,6 +24473,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -24142,6 +24487,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -24154,6 +24501,8 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 10, @@ -24166,17 +24515,57 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec1}yAccessor{y}splitAccessors{}", + "groupId": "group", + "isStacked": false, + "key": "groupId{group}spec{spec1}yAccessor{y}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y", ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y": 1, + }, + Object { + "x": 1, + "y": 2, + }, + Object { + "x": 2, + "y": 10, + }, + Object { + "x": 3, + "y": 6, + }, + ], + "groupId": "group", + "hideInLegend": false, + "id": "spec1", + "seriesType": "line", + "specType": "series", + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y", + ], + "yScaleType": "log", + }, "specId": "spec1", "splitAccessors": Map {}, + "stackMode": undefined, "yAccessor": "y", }, ] @@ -24195,6 +24584,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 1, @@ -24208,6 +24599,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 2, @@ -24221,6 +24614,8 @@ Array [ "initialY0": null, "initialY1": 1, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 1, @@ -24234,17 +24629,65 @@ Array [ "initialY0": null, "initialY1": 6, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 6, }, ], - "key": "spec{spec2}yAccessor{y1}splitAccessors{}", + "groupId": "group2", + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y1", ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "id": "spec2", + "seriesType": "line", + "specType": "series", + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y1", + "y2", + ], + "yScaleType": "log", + }, "specId": "spec2", "splitAccessors": Map {}, + "stackMode": undefined, "yAccessor": "y1", }, Object { @@ -24258,6 +24701,8 @@ Array [ "initialY0": null, "initialY1": 3, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 0, "y0": null, "y1": 3, @@ -24271,6 +24716,8 @@ Array [ "initialY0": null, "initialY1": 7, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 1, "y0": null, "y1": 7, @@ -24284,6 +24731,8 @@ Array [ "initialY0": null, "initialY1": 2, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 2, "y0": null, "y1": 2, @@ -24297,17 +24746,65 @@ Array [ "initialY0": null, "initialY1": 10, "mark": null, + "smH": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smV": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", "x": 3, "y0": null, "y1": 10, }, ], - "key": "spec{spec2}yAccessor{y2}splitAccessors{}", + "groupId": "group2", + "isStacked": true, + "key": "groupId{group2}spec{spec2}yAccessor{y2}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}", "seriesKeys": Array [ "y2", ], + "seriesType": "line", + "smHorizontalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "smVerticalAccessorValue": "__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__", + "spec": Object { + "chartType": "xy_axis", + "data": Array [ + Object { + "x": 0, + "y1": 1, + "y2": 3, + }, + Object { + "x": 1, + "y1": 2, + "y2": 7, + }, + Object { + "x": 2, + "y1": 1, + "y2": 2, + }, + Object { + "x": 3, + "y1": 6, + "y2": 10, + }, + ], + "groupId": "group2", + "hideInLegend": false, + "id": "spec2", + "seriesType": "line", + "specType": "series", + "stackAccessors": Array [ + "x", + ], + "xAccessor": "x", + "xScaleType": "linear", + "yAccessors": Array [ + "y1", + "y2", + ], + "yScaleType": "log", + }, "specId": "spec2", "splitAccessors": Map {}, + "stackMode": undefined, "yAccessor": "y2", }, ] 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 00135cf422..54c3d69dc4 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -21,6 +21,8 @@ import { DateTime } from 'luxon'; import moment from 'moment-timezone'; import { ChartTypes } from '../..'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; +import { MockStore } from '../../../mocks/store/store'; import { Scale } from '../../../scales'; import { ScaleType } from '../../../scales/constants'; import { SpecTypes } from '../../../specs/constants'; @@ -32,23 +34,24 @@ import { AxisId, GroupId } from '../../../utils/ids'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { AxisStyle, TextOffset } from '../../../utils/themes/theme'; import { XDomain, YDomain } from '../domains/types'; +import { computeAxesGeometriesSelector } from '../state/selectors/compute_axes_geometries'; +import { computeAxisTicksDimensionsSelector } from '../state/selectors/compute_axis_ticks_dimensions'; +import { getAxesStylesSelector } from '../state/selectors/get_axis_styles'; +import { computeGridLinesSelector } from '../state/selectors/get_grid_lines'; import { mergeYCustomDomainsByGroupId } from '../state/selectors/merge_y_custom_domains'; import { AxisTick, AxisTicksDimensions, - computeAxisGridLinePositions, computeAxisTicksDimensions, computeRotatedLabelDimensions, getAvailableTicks, getAxisPosition, - getAxisTicksPositions, - getHorizontalAxisGridLineProps, + getAxesGeometries, getHorizontalAxisTickLineProps, getMaxLabelDimensions, getMinMaxRange, getScaleForAxisSpec, getTickLabelProps, - getVerticalAxisGridLineProps, getVerticalAxisTickLineProps, getVisibleTicks, isYDomain, @@ -90,7 +93,7 @@ describe('Axis computational utils', () => { () => (SVGElement.prototype.getBoundingClientRect = function() { const text = this.textContent || 0; - return { ...mockedRect, width: Number(text) * 10, heigh: Number(text) * 10 }; + return { ...mockedRect, width: Number(text) * 10, height: Number(text) * 10 }; }), ); afterEach(() => (SVGElement.prototype.getBoundingClientRect = originalGetBBox)); @@ -108,8 +111,9 @@ describe('Axis computational utils', () => { maxLabelBboxHeight: 10, maxLabelTextWidth: 10, maxLabelTextHeight: 10, + isHidden: false, }; - const verticalAxisSpec: AxisSpec = { + const verticalAxisSpec = MockGlobalSpec.axis({ chartType: ChartTypes.XYAxis, specType: SpecTypes.Axis, id: 'axis_1', @@ -121,9 +125,9 @@ describe('Axis computational utils', () => { style, showGridLines: true, integersOnly: false, - }; + }); - const horizontalAxisSpec: AxisSpec = { + const horizontalAxisSpec = MockGlobalSpec.axis({ chartType: ChartTypes.XYAxis, specType: SpecTypes.Axis, id: 'axis_2', @@ -134,9 +138,9 @@ describe('Axis computational utils', () => { position: Position.Top, style, integersOnly: false, - }; + }); - const verticalAxisSpecWTitle: AxisSpec = { + const verticalAxisSpecWTitle = MockGlobalSpec.axis({ chartType: ChartTypes.XYAxis, specType: SpecTypes.Axis, id: 'axis_1', @@ -149,8 +153,8 @@ describe('Axis computational utils', () => { style, showGridLines: true, integersOnly: false, - }; - const xAxisWithTime: AxisSpec = { + }); + const xAxisWithTime = MockGlobalSpec.axis({ chartType: ChartTypes.XYAxis, specType: SpecTypes.Axis, id: 'axis_1', @@ -164,7 +168,7 @@ describe('Axis computational utils', () => { tickFormat: niceTimeFormatter([1551438000000, 1551441300000]), showGridLines: true, integersOnly: false, - }; + }); // const horizontalAxisSpecWTitle: AxisSpec = { // id: ('axis_2'), @@ -179,7 +183,19 @@ describe('Axis computational utils', () => { // return `${value}`; // }, // }; - + const lineSeriesSpec = MockSeriesSpec.line({ + id: 'line', + groupId: 'group_1', + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + data: [ + [0, 0], + [0.5, 0.5], + [1, 1], + ], + }); const xDomain: XDomain = { type: 'xDomain', scaleType: ScaleType.Linear, @@ -346,6 +362,7 @@ describe('Axis computational utils', () => { maxLabelTextWidth: 100, tickLabels: [], tickValues: [], + isHidden: false, }; const offset: TextOffset = { x: 0, @@ -562,6 +579,7 @@ describe('Axis computational utils', () => { maxLabelBboxHeight: 20, maxLabelTextWidth: 10, maxLabelTextHeight: 20, + isHidden: false, }; const visibleTicks = getVisibleTicks(allTicks, verticalAxisSpec, axis2Dims); const expectedVisibleTicks = [ @@ -597,6 +615,7 @@ describe('Axis computational utils', () => { maxLabelBboxHeight: 20, maxLabelTextWidth: 10, maxLabelTextHeight: 20, + isHidden: false, }; verticalAxisSpec.showOverlappingTicks = true; @@ -639,8 +658,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Bottom, 0, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 0, maxRange: 100 }); }); @@ -648,8 +665,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Bottom, 90, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 0, maxRange: 100 }); }); @@ -657,8 +672,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Bottom, 180, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 100, maxRange: 0 }); }); @@ -666,8 +679,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Bottom, -90, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 100, maxRange: 0 }); }); @@ -675,8 +686,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Left, 90, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 0, maxRange: 50 }); }); @@ -684,8 +693,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Left, 180, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 0, maxRange: 50 }); }); @@ -693,8 +700,6 @@ describe('Axis computational utils', () => { const minMax = getMinMaxRange(Position.Right, -90, { width: 100, height: 50, - top: 0, - left: 0, }); expect(minMax).toEqual({ minRange: 50, maxRange: 0 }); }); @@ -921,7 +926,7 @@ describe('Axis computational utils', () => { const leftAxisTickLinePositions = getVerticalAxisTickLineProps(Position.Left, tickPadding, tickSize, tickPosition); - expect(leftAxisTickLinePositions).toEqual([5, 10, -5, 10]); + expect(leftAxisTickLinePositions).toEqual({ x1: 5, y1: 10, x2: -5, y2: 10 }); const rightAxisTickLinePositions = getVerticalAxisTickLineProps( Position.Right, @@ -930,11 +935,11 @@ describe('Axis computational utils', () => { tickPosition, ); - expect(rightAxisTickLinePositions).toEqual([0, 10, 10, 10]); + expect(rightAxisTickLinePositions).toEqual({ x1: 0, y1: 10, x2: 10, y2: 10 }); const topAxisTickLinePositions = getHorizontalAxisTickLineProps(Position.Top, axisHeight, tickSize, tickPosition); - expect(topAxisTickLinePositions).toEqual([10, 10, 10, 20]); + expect(topAxisTickLinePositions).toEqual({ x1: 10, y1: 10, x2: 10, y2: 20 }); const bottomAxisTickLinePositions = getHorizontalAxisTickLineProps( Position.Bottom, @@ -943,21 +948,7 @@ describe('Axis computational utils', () => { tickPosition, ); - expect(bottomAxisTickLinePositions).toEqual([10, 0, 10, 10]); - }); - - test('should compute axis grid line positions', () => { - const tickPosition = 10; - const chartWidth = 100; - const chartHeight = 200; - - const verticalAxisGridLinePositions = getVerticalAxisGridLineProps(tickPosition, chartWidth); - - expect(verticalAxisGridLinePositions).toEqual([0, 10, 100, 10]); - - const horizontalAxisGridLinePositions = getHorizontalAxisGridLineProps(tickPosition, chartHeight); - - expect(horizontalAxisGridLinePositions).toEqual([10, 0, 10, 200]); + expect(bottomAxisTickLinePositions).toEqual({ x1: 10, y1: 0, x2: 10, y2: 10 }); }); test('should compute axis ticks positions with title', () => { @@ -971,7 +962,7 @@ describe('Axis computational utils', () => { const axisDims = new Map(); axisDims.set(verticalAxisSpecWTitle.id, axis1Dims); - let axisTicksPosition = getAxisTicksPositions( + let axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -983,14 +974,18 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + chartDim, 1, false, (v) => `${v}`, ); - expect(axisTicksPosition.axisPositions.get(verticalAxisSpecWTitle.id)).toEqual({ - top: 0, - left: 10, + const verticalAxisGeoms = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpecWTitle.id); + expect(verticalAxisGeoms?.anchorPoint).toEqual({ + y: 0, + x: 10, + }); + expect(verticalAxisGeoms?.size).toEqual({ width: 50, height: 100, }); @@ -999,7 +994,7 @@ describe('Axis computational utils', () => { axisDims.set(verticalAxisSpec.id, axis1Dims); - axisTicksPosition = getAxisTicksPositions( + axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -1011,14 +1006,17 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + chartDim, 1, false, (v) => `${v}`, ); - - expect(axisTicksPosition.axisPositions.get(verticalAxisSpecWTitle.id)).toEqual({ - top: 0, - left: 10, + const verticalAxisSpecWTitleGeoms = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpecWTitle.id); + expect(verticalAxisSpecWTitleGeoms?.anchorPoint).toEqual({ + y: 0, + x: 10, + }); + expect(verticalAxisSpecWTitleGeoms?.size).toEqual({ width: 10, height: 100, }); @@ -1199,7 +1197,7 @@ describe('Axis computational utils', () => { const axisDims = new Map(); axisDims.set('not_a_mapped_one', axis1Dims); - const axisTicksPosition = getAxisTicksPositions( + const axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -1211,40 +1209,34 @@ describe('Axis computational utils', () => { axisStyles, xDomain, [yDomain], + chartDim, 1, false, (v) => `${v}`, ); - expect(axisTicksPosition.axisPositions.size).toBe(0); - expect(axisTicksPosition.axisTicks.size).toBe(0); - expect(axisTicksPosition.axisGridLinesPositions.size).toBe(0); - expect(axisTicksPosition.axisVisibleTicks.size).toBe(0); + expect(axisTicksPosition).toHaveLength(0); + // expect(axisTicksPosition.axisTicks.size).toBe(0); + // expect(axisTicksPosition.axisGridLinesPositions.size).toBe(0); + // expect(axisTicksPosition.axisVisibleTicks.size).toBe(0); }); test('should compute axis ticks positions', () => { - const chartRotation = 0; - - const axisSpecs = [verticalAxisSpec]; - const axisStyles = new Map(); - const axisDims = new Map(); - axisDims.set(verticalAxisSpec.id, axis1Dims); - - const axisTicksPosition = getAxisTicksPositions( - { - chartDimensions: chartDim, - leftMargin: 0, - }, - LIGHT_THEME, - chartRotation, - axisSpecs, - axisDims, - axisStyles, - xDomain, - [yDomain], - 1, - false, - (v) => `${v}`, + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + lineSeriesSpec, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + gridLine: { + visible: true, + }, + }), + ], + store, ); + const gridLines = computeGridLinesSelector(store.getState()); const expectedVerticalAxisGridLines = [ [0, 0, 100, 0], @@ -1260,49 +1252,37 @@ describe('Axis computational utils', () => { [0, 100, 100, 100], ]; - expect(axisTicksPosition.axisGridLinesPositions.get(verticalAxisSpec.id)).toEqual(expectedVerticalAxisGridLines); + const [{ lines }] = gridLines[0].lineGroups; - const axisTicksPositionWithTopLegend = getAxisTicksPositions( - { - chartDimensions: chartDim, - leftMargin: 0, - }, - LIGHT_THEME, - chartRotation, - axisSpecs, - axisDims, - axisStyles, - xDomain, - [yDomain], - 1, - false, - (v) => `${v}`, - ); + expect(lines.map(({ x1, y1, x2, y2 }) => [x1, y1, x2, y2])).toEqual(expectedVerticalAxisGridLines); - const expectedPositionWithTopLegend = { - height: 100, - width: 10, - left: 100, - top: 0, - }; - const verticalAxisWithTopLegendPosition = axisTicksPositionWithTopLegend.axisPositions.get(verticalAxisSpec.id); - expect(verticalAxisWithTopLegendPosition).toEqual(expectedPositionWithTopLegend); + const axisTicksPositionWithTopLegend = computeAxesGeometriesSelector(store.getState()); + + const verticalAxisWithTopLegendPosition = axisTicksPositionWithTopLegend.find( + ({ axis: { id } }) => id === verticalAxisSpec.id, + ); + // TODO check the root cause of having with at 10 on previous implementation + expect(verticalAxisWithTopLegendPosition?.size).toEqual({ height: 0, width: 0 }); + expect(verticalAxisWithTopLegendPosition?.anchorPoint).toEqual({ x: 100, y: 0 }); const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' }; const invalidSpecs = [ungroupedAxisSpec]; const computeScalelessSpec = () => { - getAxisTicksPositions( + const axisDims = computeAxisTicksDimensionsSelector(store.getState()); + const axisStyles = getAxesStylesSelector(store.getState()); + getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, }, LIGHT_THEME, - chartRotation, + 0, invalidSpecs, axisDims, axisStyles, xDomain, [yDomain], + chartDim, 1, false, (v) => `${v}`, @@ -1312,14 +1292,6 @@ describe('Axis computational utils', () => { expect(computeScalelessSpec).toThrowError('Cannot compute scale for axis spec axis_1'); }); - test('should compute positions for grid lines', () => { - const verticalAxisGridLines = computeAxisGridLinePositions(true, 25, chartDim); - expect(verticalAxisGridLines).toEqual([0, 25, 100, 25]); - - const horizontalAxisGridLines = computeAxisGridLinePositions(false, 25, chartDim); - expect(horizontalAxisGridLines).toEqual([25, 0, 25, 100]); - }); - test('should determine if axis belongs to yDomain', () => { const verticalY = isYDomain(Position.Left, 0); expect(verticalY).toBe(true); @@ -1690,13 +1662,13 @@ describe('Axis computational utils', () => { describe('Custom formatting', () => { it('should get custom labels for y axis', () => { - const customFotmatter = (v: any) => `${v} custom`; + const customFormatter = (v: any) => `${v} custom`; const axisSpecs = [verticalAxisSpec]; const axesStyles = new Map(); const axisDims = new Map(); axisDims.set(verticalAxisSpec.id, axis1Dims); - const axisTicksPosition = getAxisTicksPositions( + const axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -1708,16 +1680,18 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + chartDim, 1, false, - customFotmatter, + customFormatter, ); const expected = axis1Dims.tickValues .slice() .reverse() - .map(customFotmatter); - expect(axisTicksPosition.axisTicks.get(verticalAxisSpec.id)!.map(({ label }) => label)).toEqual(expected); + .map(customFormatter); + const axisPos = axisTicksPosition.find(({ axis: { id } }) => id === verticalAxisSpec.id); + expect(axisPos?.ticks.map(({ label }) => label)).toEqual(expected); }); it('should not use custom formatter with x axis', () => { @@ -1727,7 +1701,7 @@ describe('Axis computational utils', () => { const axisDims = new Map(); axisDims.set(horizontalAxisSpec.id, axis1Dims); - const axisTicksPosition = getAxisTicksPositions( + const axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -1739,13 +1713,16 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + chartDim, 1, false, customFotmatter, ); const expected = axis1Dims.tickValues.slice().map(defaultTickFormatter); - expect(axisTicksPosition.axisTicks.get(horizontalAxisSpec.id)!.map(({ label }) => label)).toEqual(expected); + expect( + axisTicksPosition.find(({ axis: { id } }) => id === horizontalAxisSpec.id)!.ticks.map(({ label }) => label), + ).toEqual(expected); }); it('should use custom axis tick formatter to get labels for x axis', () => { @@ -1760,7 +1737,7 @@ describe('Axis computational utils', () => { const axisDims = new Map(); axisDims.set(spec.id, axis1Dims); - const axisTicksPosition = getAxisTicksPositions( + const axisTicksPosition = getAxesGeometries( { chartDimensions: chartDim, leftMargin: 0, @@ -1772,13 +1749,16 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + chartDim, 1, false, customFotmatter, ); const expected = axis1Dims.tickValues.slice().map(customAxisFotmatter); - expect(axisTicksPosition.axisTicks.get(spec.id)!.map(({ label }) => label)).toEqual(expected); + expect(axisTicksPosition.find(({ axis: { id } }) => id === spec.id)!.ticks.map(({ label }) => label)).toEqual( + expected, + ); }); }); }); diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index a294aee923..9b2ef1192c 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Line } from '../../../geoms/types'; import { Scale } from '../../../scales'; import { BBox, BBoxCalculator } from '../../../utils/bbox/bbox_calculator'; import { @@ -26,11 +27,11 @@ import { VerticalAlignment, HorizontalAlignment, getPercentageValue, - mergePartial, } from '../../../utils/commons'; -import { Dimensions, Margins, getSimplePadding } from '../../../utils/dimensions'; +import { Dimensions, Margins, getSimplePadding, Size } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; +import { Point } from '../../../utils/point'; import { AxisStyle, Theme, TextAlignment, TextOffset } from '../../../utils/themes/theme'; import { XDomain, YDomain } from '../domains/types'; import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; @@ -39,8 +40,6 @@ import { isVerticalAxis } from './axis_type_utils'; import { computeXScale, computeYScales } from './scales'; import { AxisSpec, TickFormatterOptions, TickFormatter } from './specs'; -export type AxisLinePosition = [number, number, number, number]; - export interface AxisTick { value: number | string; label: string; @@ -54,6 +53,7 @@ export interface AxisTicksDimensions { maxLabelBboxHeight: number; maxLabelTextWidth: number; maxLabelTextHeight: number; + isHidden: boolean; } export interface TickLabelProps { @@ -78,6 +78,11 @@ export const defaultTickFormatter = (tick: any) => `${tick}`; * @param totalBarsInCluster the total number of grouped series * @param bboxCalculator an instance of the boundingbox calculator * @param chartRotation the rotation of the chart + * @param gridLine + * @param tickLabel + * @param fallBackTickFormatter + * @param barsPadding + * @param enableHistogramMode * @internal */ export function computeAxisTicksDimensions( @@ -92,7 +97,10 @@ export function computeAxisTicksDimensions( barsPadding?: number, enableHistogramMode?: boolean, ): AxisTicksDimensions | null { - if (axisSpec.hide && !gridLine.horizontal.visible && !gridLine.vertical.visible) { + const gridLineVisible = isVerticalAxis(axisSpec.position) ? gridLine.vertical.visible : gridLine.horizontal.visible; + + // don't compute anything on this axis if grid is hidden and axis is hidden + if (axisSpec.hide && !gridLineVisible) { return null; } @@ -124,6 +132,7 @@ export function computeAxisTicksDimensions( return { ...dimensions, + isHidden: axisSpec.hide && gridLineVisible, }; } @@ -349,18 +358,20 @@ function getVerticalAlign( /** * Gets the computed x/y coordinates & alignment properties for an axis tick label. * @param isVerticalAxis if the axis is vertical (in contrast to horizontal) - * @param tickSize length of tick line - * @param tickPadding amount of padding between label and tick line * @param tickPosition position of tick relative to axis line origin and other ticks along it * @param position position of where the axis sits relative to the visualization - * @param axisTicksDimensions computed axis dimensions and values (from computeTickDimensions) + * @param axisSize + * @param tickDimensions + * @param showTicks + * @param textOffset + * @param textAlignment * @internal */ export function getTickLabelProps( { tickLine, tickLabel }: AxisStyle, tickPosition: number, position: Position, - axisPosition: Dimensions, + axisSize: Size, tickDimensions: AxisTicksDimensions, showTicks: boolean, textOffset: TextOffset, @@ -379,7 +390,7 @@ export function getTickLabelProps( const textOffsetY = getVerticalTextOffset(maxLabelTextHeight, verticalAlign) + userOffsets.local.y; if (isVerticalAxis(position)) { - const x = isLeftAxis ? axisPosition.width - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner; + const x = isLeftAxis ? axisSize.width - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner; const offsetX = (isLeftAxis ? -1 : 1) * (maxLabelBboxWidth / 2); return { @@ -398,7 +409,7 @@ export function getTickLabelProps( return { x: tickPosition, - y: isAxisTop ? axisPosition.height - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner, + y: isAxisTop ? axisSize.height - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner, offsetX: userOffsets.global.x, offsetY: offsetY + userOffsets.global.y, textOffsetX, @@ -414,13 +425,13 @@ export function getVerticalAxisTickLineProps( axisWidth: number, tickSize: number, tickPosition: number, -): AxisLinePosition { +): Line { const isLeftAxis = position === Position.Left; const y = tickPosition; const x1 = isLeftAxis ? axisWidth : 0; const x2 = isLeftAxis ? axisWidth - tickSize : tickSize; - return [x1, y, x2, y]; + return { x1, y1: y, x2, y2: y }; } /** @internal */ @@ -429,35 +440,24 @@ export function getHorizontalAxisTickLineProps( axisHeight: number, tickSize: number, tickPosition: number, -): AxisLinePosition { +): Line { const isTopAxis = position === Position.Top; const x = tickPosition; const y1 = isTopAxis ? axisHeight - tickSize : 0; const y2 = isTopAxis ? axisHeight : tickSize; - return [x, y1, x, y2]; -} - -/** @internal */ -export function getVerticalAxisGridLineProps(tickPosition: number, chartWidth: number): AxisLinePosition { - return [0, tickPosition, chartWidth, tickPosition]; -} - -/** @internal */ -export function getHorizontalAxisGridLineProps(tickPosition: number, chartHeight: number): AxisLinePosition { - return [tickPosition, 0, tickPosition, chartHeight]; + return { x1: x, y1, x2: x, y2 }; } /** @internal */ export function getMinMaxRange( axisPosition: Position, chartRotation: Rotation, - chartDimensions: Dimensions, + { width, height }: Size, ): { minRange: number; maxRange: number; } { - const { width, height } = chartDimensions; switch (axisPosition) { case Position.Bottom: case Position.Top: @@ -678,8 +678,21 @@ export function shouldShowTicks({ visible, strokeWidth, size }: AxisStyle['tickL return !axisHidden && visible && size > 0 && strokeWidth >= MIN_STROKE_WIDTH; } +export interface AxisGeometry { + anchorPoint: Point; + size: Size; + axis: { + id: AxisId; + position: Position; + title?: string; + }; + dimension: AxisTicksDimensions; + ticks: AxisTick[]; + visibleTicks: AxisTick[]; +} + /** @internal */ -export function getAxisTicksPositions( +export function getAxesGeometries( computedChartDims: { chartDimensions: Dimensions; leftMargin: number; @@ -691,35 +704,94 @@ export function getAxisTicksPositions( axesStyles: Map, xDomain: XDomain, yDomain: YDomain[], + panel: Size, totalGroupsCount: number, enableHistogramMode: boolean, fallBackTickFormatter: TickFormatter, barsPadding?: number, -): { - axisPositions: Map; - axisTicks: Map; - axisVisibleTicks: Map; - axisGridLinesPositions: Map; -} { - const axisPositions: Map = new Map(); - const axisVisibleTicks: Map = new Map(); - const axisTicks: Map = new Map(); - const axisGridLinesPositions: Map = new Map(); +): Array { + const axesGeometries: Array = []; + const { chartDimensions } = computedChartDims; - let cumTopSum = 0; - let cumBottomSum = chartPaddings.bottom; - let cumLeftSum = computedChartDims.leftMargin; - let cumRightSum = chartPaddings.right; + + // compute the anchor point for every axis group + + const anchorPointByAxisGroups = [...axisDimensions.entries()].reduce( + (acc, [axisId, dimension]) => { + const axisSpec = getSpecsById(axisSpecs, axisId); + if (!axisSpec) { + return acc; + } + + const { axisTitle, tickLine, tickLabel } = axesStyles.get(axisId) ?? sharedAxesStyle; + const labelPadding = getSimplePadding(tickLabel.padding); + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const axisTitleHeight = axisSpec.title !== undefined ? axisTitle.fontSize : 0; + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = getAxisPosition( + chartDimensions, + chartMargins, + axisTitleHeight, + axisTitle, + axisSpec, + dimension, + acc.top, + acc.bottom, + acc.left, + acc.right, + labelPaddingSum, + tickDimension, + tickLabel.visible, + ); + const anchor = { + top: acc.top + topIncrement, + bottom: acc.bottom + bottomIncrement, + left: acc.left + leftIncrement, + right: acc.right + rightIncrement, + }; + acc.pos.set(axisId, { + anchor: { + top: acc.top, + left: acc.left, + right: acc.right, + bottom: acc.bottom, + }, + dimensions, + }); + return { + ...anchor, + pos: acc.pos, + }; + }, + { + top: 0, + bottom: chartPaddings.bottom, + left: computedChartDims.leftMargin, + right: chartPaddings.right, + pos: new Map< + AxisId, + { + anchor: { left: number; right: number; top: number; bottom: number }; + dimensions: Dimensions; + } + >(), + }, + ).pos; axisDimensions.forEach((axisDim, id) => { const axisSpec = getSpecsById(axisSpecs, id); - + const anchorPoint = anchorPointByAxisGroups.get(id); // Consider refactoring this so this condition can be tested // Given some of the values that get passed around, maybe re-write as a reduce instead of forEach? - if (!axisSpec) { + if (!axisSpec || !anchorPoint) { return; } - const minMaxRanges = getMinMaxRange(axisSpec.position, chartRotation, chartDimensions); + + const isVertical = isVerticalAxis(axisSpec.position); + + const minMaxRanges = getMinMaxRange(axisSpec.position, chartRotation, panel); const scale = getScaleForAxisSpec( axisSpec, @@ -739,8 +811,7 @@ export function getAxisTicksPositions( const tickFormatOptions = { timeZone: xDomain.timeZone, }; - const { axisTitle, tickLine, tickLabel, gridLine } = axesStyles.get(id) ?? sharedAxesStyle; - const isVertical = isVerticalAxis(axisSpec.position); + // TODO: Find the true cause of the this offset error const rotationOffset = enableHistogramMode && @@ -757,70 +828,31 @@ export function getAxisTicksPositions( rotationOffset, tickFormatOptions, ); - const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim); - const axisSpecConfig = axisSpec.gridLine; - const gridLineThemeStyles = isVertical ? gridLine.vertical : gridLine.horizontal; - const gridLineStyles = axisSpecConfig ? mergePartial(gridLineThemeStyles, axisSpecConfig) : gridLineThemeStyles; - - if (axisSpec.showGridLines ?? gridLineStyles.visible) { - const gridLines = visibleTicks.map( - (tick: AxisTick): AxisLinePosition => computeAxisGridLinePositions(isVertical, tick.position, chartDimensions), - ); - axisGridLinesPositions.set(id, gridLines); - } - - const labelPadding = getSimplePadding(tickLabel.padding); - const showTicks = shouldShowTicks(tickLine, axisSpec.hide); - const axisTitleHeight = axisSpec.title !== undefined ? axisTitle.fontSize : 0; - const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; - const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; - - const axisPosition = getAxisPosition( - chartDimensions, - chartMargins, - axisTitleHeight, - axisTitle, - axisSpec, - axisDim, - cumTopSum, - cumBottomSum, - cumLeftSum, - cumRightSum, - labelPaddingSum, - tickDimension, - tickLabel.visible, - ); - - cumTopSum += axisPosition.topIncrement; - cumBottomSum += axisPosition.bottomIncrement; - cumLeftSum += axisPosition.leftIncrement; - cumRightSum += axisPosition.rightIncrement; - axisPositions.set(id, axisPosition.dimensions); - axisVisibleTicks.set(id, visibleTicks); - axisTicks.set(id, allTicks); + const size = axisDim.isHidden + ? { width: 0, height: 0 } + : { + width: isVertical ? anchorPoint.dimensions.width : panel.width, + height: isVertical ? panel.height : anchorPoint.dimensions.height, + }; + axesGeometries.push({ + axis: { + id: axisSpec.id, + position: axisSpec.position, + title: axisSpec.title, + }, + anchorPoint: { + x: anchorPoint.dimensions.left, + y: anchorPoint.dimensions.top, + }, + size, + dimension: axisDim, + ticks: allTicks, + visibleTicks, + }); }); - - return { - axisPositions, - axisTicks, - axisVisibleTicks, - axisGridLinesPositions, - }; -} - -/** @internal */ -export function computeAxisGridLinePositions( - isVerticalAxis: boolean, - tickPosition: number, - chartDimensions: Dimensions, -): AxisLinePosition { - const positions = isVerticalAxis - ? getVerticalAxisGridLineProps(tickPosition, chartDimensions.width) - : getHorizontalAxisGridLineProps(tickPosition, chartDimensions.height); - - return positions; + return axesGeometries; } /** @internal */ diff --git a/src/chart_types/xy_chart/utils/dimensions.test.ts b/src/chart_types/xy_chart/utils/dimensions.test.ts index e6c795395e..11c03c4bc0 100644 --- a/src/chart_types/xy_chart/utils/dimensions.test.ts +++ b/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -59,6 +59,7 @@ describe('Computed chart dimensions', () => { maxLabelBboxHeight: 10, maxLabelTextWidth: 10, maxLabelTextHeight: 10, + isHidden: false, }; const axisLeftSpec: AxisSpec = { chartType: ChartTypes.XYAxis, diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index 7d4642d3b2..83024c7a27 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -17,12 +17,11 @@ * under the License. */ -import { Position } from '../../../utils/commons'; -import { Dimensions, getSimplePadding } from '../../../utils/dimensions'; +import { Dimensions } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; import { Theme, AxisStyle } from '../../../utils/themes/theme'; -import { getSpecsById } from '../state/utils/spec'; -import { AxisTicksDimensions, shouldShowTicks } from './axis_utils'; +import { computeAxesSizes } from '../axes/axes_sizes'; +import { AxisTicksDimensions } from './axis_utils'; import { AxisSpec } from './specs'; /** @@ -50,14 +49,16 @@ export interface ChartDimensions { * Compute the chart dimensions. It's computed removing from the parent dimensions * the axis spaces, the legend and any other specified style margin and padding. * @param parentDimensions the parent dimension - * @param chartTheme the theme style of the chart + * @param theme * @param axisDimensions the axis dimensions + * @param axesStyles * @param axisSpecs the axis specs + * @param legendSizing * @internal */ export function computeChartDimensions( parentDimensions: Dimensions, - { chartMargins, chartPaddings, axes: sharedAxesStyles }: Theme, + theme: Theme, axisDimensions: Map, axesStyles: Map, axisSpecs: AxisSpec[], @@ -82,64 +83,16 @@ export function computeChartDimensions( }; } - let vLeftAxisSpecWidth = 0; - let vRightAxisSpecWidth = 0; - let hTopAxisSpecHeight = 0; - let hBottomAxisSpecHeight = 0; - let horizontalEdgeLabelOverflow = 0; - let verticalEdgeLabelOverflow = 0; - axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0 }, id) => { - const axisSpec = getSpecsById(axisSpecs, id); - if (!axisSpec || axisSpec.hide) { - return; - } - const { tickLine, axisTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; - const showTicks = shouldShowTicks(tickLine, axisSpec.hide); - const { position, title } = axisSpec; - const titlePadding = getSimplePadding(axisTitle.padding); - const labelPadding = getSimplePadding(tickLabel.padding); - const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + const axisSizes = computeAxesSizes(theme, axisDimensions, axesStyles, axisSpecs); - const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; - const titleHeight = - title !== undefined && axisTitle.visible ? axisTitle.fontSize + titlePadding.outer + titlePadding.inner : 0; - const axisDimension = labelPaddingSum + tickDimension + titleHeight; - const maxAxisHeight = tickLabel.visible ? maxLabelBboxHeight + axisDimension : axisDimension; - const maxAxisWidth = tickLabel.visible ? maxLabelBboxWidth + axisDimension : axisDimension; - switch (position) { - case Position.Top: - hTopAxisSpecHeight += maxAxisHeight + chartMargins.top; - // find the max half label size to accomodate the left/right labels - horizontalEdgeLabelOverflow = Math.max(horizontalEdgeLabelOverflow, maxLabelBboxWidth / 2); - break; - case Position.Bottom: - hBottomAxisSpecHeight += maxAxisHeight + chartMargins.bottom; - // find the max half label size to accomodate the left/right labels - horizontalEdgeLabelOverflow = Math.max(horizontalEdgeLabelOverflow, maxLabelBboxWidth / 2); - break; - case Position.Right: - vRightAxisSpecWidth += maxAxisWidth + chartMargins.right; - verticalEdgeLabelOverflow = Math.max(verticalEdgeLabelOverflow, maxLabelBboxHeight / 2); - break; - case Position.Left: - default: - vLeftAxisSpecWidth += maxAxisWidth + chartMargins.left; - verticalEdgeLabelOverflow = Math.max(verticalEdgeLabelOverflow, maxLabelBboxHeight / 2); - } - }); - const chartLeftAxisMaxWidth = Math.max(vLeftAxisSpecWidth, horizontalEdgeLabelOverflow + chartMargins.left); - const chartRightAxisMaxWidth = Math.max(vRightAxisSpecWidth, horizontalEdgeLabelOverflow + chartMargins.right); - const chartTopAxisMaxHeight = Math.max(hTopAxisSpecHeight, verticalEdgeLabelOverflow + chartMargins.top); - const chartBottomAxisMaxHeight = Math.max(hBottomAxisSpecHeight, verticalEdgeLabelOverflow + chartMargins.bottom); - - const chartWidth = parentDimensions.width - chartLeftAxisMaxWidth - chartRightAxisMaxWidth; - const chartHeight = parentDimensions.height - chartTopAxisMaxHeight - chartBottomAxisMaxHeight; - - const top = chartTopAxisMaxHeight + chartPaddings.top; - const left = chartLeftAxisMaxWidth + chartPaddings.left; + const chartWidth = parentDimensions.width - axisSizes.left - axisSizes.right; + const chartHeight = parentDimensions.height - axisSizes.top - axisSizes.bottom; + const { chartPaddings } = theme; + const top = axisSizes.top + chartPaddings.top; + const left = axisSizes.left + chartPaddings.left; return { - leftMargin: chartLeftAxisMaxWidth - vLeftAxisSpecWidth, + leftMargin: axisSizes.margin.left, chartDimensions: { top, left, diff --git a/src/chart_types/xy_chart/utils/fill_series.ts b/src/chart_types/xy_chart/utils/fill_series.ts index dd8f318c4d..9553ac7288 100644 --- a/src/chart_types/xy_chart/utils/fill_series.ts +++ b/src/chart_types/xy_chart/utils/fill_series.ts @@ -17,85 +17,59 @@ * under the License. */ import { ScaleType } from '../../../scales/constants'; -import { SpecId, GroupId } from '../../../utils/ids'; -import { YBasicSeriesSpec } from '../domains/y_domain'; -import { getSpecsById } from '../state/utils/spec'; import { DataSeries } from './series'; -import { SeriesSpecs, StackMode, BasicSeriesSpec, isLineSeriesSpec, isAreaSeriesSpec } from './specs'; +import { BasicSeriesSpec, isLineSeriesSpec, isAreaSeriesSpec } from './specs'; /** - * Fill missing x values in all data series * @internal */ export function fillSeries( - series: Map, + dataSeries: DataSeries[], xValues: Set, - seriesSpecs: SeriesSpecs, groupScaleType: ScaleType, - specsByGroupIds: Map< - GroupId, - { - stackMode: StackMode | undefined; - stacked: YBasicSeriesSpec[]; - nonStacked: YBasicSeriesSpec[]; - } - >, -): Map { +): DataSeries[] { const sortedXValues = [...xValues.values()]; - const filledSeries: Map = new Map(); - series.forEach((dataSeries, key) => { - const spec = getSpecsById(seriesSpecs, key); - if (!spec) { - return; + return dataSeries.map((series) => { + const { spec, data, isStacked } = series; + + const noFillRequired = isXFillNotRequired(spec, groupScaleType, isStacked); + if (data.length === xValues.size || noFillRequired) { + return { + ...series, + data, + }; } - const group = specsByGroupIds.get(spec.groupId); - if (!group) { - return; + const filledData: typeof data = []; + const missingValues = new Set(xValues); + for (let i = 0; i < data.length; i++) { + const { x } = data[i]; + filledData.push(data[i]); + missingValues.delete(x); } - const isStacked = Boolean(group.stacked.find(({ id }) => id === key)); - const noFillRequired = isXFillNotRequired(spec, groupScaleType, isStacked); - - const filledDataSeries = dataSeries.map(({ data, ...rest }) => { - if (data.length === xValues.size || noFillRequired) { - return { - ...rest, - data, - }; - } - const filledData: typeof data = []; - const missingValues = new Set(xValues); - for (let i = 0; i < data.length; i++) { - const { x } = data[i]; - filledData.push(data[i]); - missingValues.delete(x); - } - const missingValuesArray = [...missingValues.values()]; - for (let i = 0; i < missingValuesArray.length; i++) { - const missingValue = missingValuesArray[i]; - const index = sortedXValues.indexOf(missingValue); + const missingValuesArray = [...missingValues.values()]; + for (let i = 0; i < missingValuesArray.length; i++) { + const missingValue = missingValuesArray[i]; + const index = sortedXValues.indexOf(missingValue); - filledData.splice(index, 0, { + filledData.splice(index, 0, { + x: missingValue, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + mark: null, + datum: undefined, + filled: { x: missingValue, - y1: null, - y0: null, - initialY1: null, - initialY0: null, - mark: null, - datum: undefined, - filled: { - x: missingValue, - }, - }); - } - return { - ...rest, - data: filledData, - }; - }); - filledSeries.set(key, filledDataSeries); + }, + }); + } + return { + ...series, + data: filledData, + }; }); - return filledSeries; } function isXFillNotRequired(spec: BasicSeriesSpec, groupScaleType: ScaleType, isStacked: boolean) { diff --git a/src/chart_types/xy_chart/utils/fit_function_utils.ts b/src/chart_types/xy_chart/utils/fit_function_utils.ts index d17dba29b8..5416f5f6d2 100644 --- a/src/chart_types/xy_chart/utils/fit_function_utils.ts +++ b/src/chart_types/xy_chart/utils/fit_function_utils.ts @@ -25,14 +25,11 @@ import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs, BasicSeriesSpec } from /** @internal */ export const applyFitFunctionToDataSeries = ( - dataseries: DataSeries[], + dataSeries: DataSeries[], seriesSpecs: SeriesSpecs, xScaleType: ScaleType, ): DataSeries[] => { - const len = dataseries.length; - const formattedValues: DataSeries[] = []; - for (let i = 0; i < len; i++) { - const { specId, data, ...rest } = dataseries[i]; + return dataSeries.map(({ specId, data, ...rest }) => { const spec = getSpecsById(seriesSpecs, specId); if ( @@ -43,14 +40,12 @@ export const applyFitFunctionToDataSeries = ( ) { const fittedData = fitFunction(data, spec.fit, xScaleType); - formattedValues.push({ + return { specId, ...rest, data: fittedData, - }); - } else { - formattedValues.push({ specId, data, ...rest }); + }; } - } - return formattedValues; + return { specId, data, ...rest }; + }); }; diff --git a/src/chart_types/xy_chart/utils/grid_lines.test.ts b/src/chart_types/xy_chart/utils/grid_lines.test.ts new file mode 100644 index 0000000000..96e236d0e5 --- /dev/null +++ b/src/chart_types/xy_chart/utils/grid_lines.test.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 { getGridLineForHorizontalAxisAt, getGridLineForVerticalAxisAt } from './grid_lines'; + +describe('Grid lines', () => { + test('should compute positions for grid lines', () => { + const tickPosition = 25; + const panel = { + width: 100, + height: 100, + top: 0, + left: 0, + }; + const verticalAxisGridLines = getGridLineForVerticalAxisAt(tickPosition, panel); + expect(verticalAxisGridLines).toEqual({ x1: 0, y1: 25, x2: 100, y2: 25 }); + + const horizontalAxisGridLines = getGridLineForHorizontalAxisAt(tickPosition, panel); + expect(horizontalAxisGridLines).toEqual({ x1: 25, y1: 0, x2: 25, y2: 100 }); + }); + + test('should compute axis grid line positions', () => { + const panel = { + width: 100, + height: 200, + top: 0, + left: 0, + }; + const tickPosition = 10; + + const verticalAxisGridLinePositions = getGridLineForVerticalAxisAt(tickPosition, panel); + + expect(verticalAxisGridLinePositions).toEqual({ x1: 0, y1: 10, x2: 100, y2: 10 }); + + const horizontalAxisGridLinePositions = getGridLineForHorizontalAxisAt(tickPosition, panel); + + expect(horizontalAxisGridLinePositions).toEqual({ x1: 10, y1: 0, x2: 10, y2: 200 }); + }); +}); diff --git a/src/chart_types/xy_chart/utils/grid_lines.ts b/src/chart_types/xy_chart/utils/grid_lines.ts new file mode 100644 index 0000000000..e057600207 --- /dev/null +++ b/src/chart_types/xy_chart/utils/grid_lines.ts @@ -0,0 +1,150 @@ +/* + * 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 { Line, Stroke } from '../../../geoms/types'; +import { mergePartial, RecursivePartial } from '../../../utils/commons'; +import { Size } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { Point } from '../../../utils/point'; +import { AxisStyle } from '../../../utils/themes/theme'; +import { stringToRGB } from '../../partition_chart/layout/utils/color_library_wrappers'; +import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; +import { isVerticalAxis } from './axis_type_utils'; +import { AxisGeometry, AxisTick } from './axis_utils'; +import { getPanelSize } from './panel'; +import { getPerPanelMap } from './panel_utils'; +import { AxisSpec } from './specs'; + +/** @internal */ +export interface GridLineGroup { + lines: Array; + stroke: Stroke; + axisId: AxisId; +} + +/** @internal */ +export type LinesGrid = { + panelAnchor: Point; + lineGroups: Array; +}; + +/** @internal */ +export function getGridLines( + axesSpecs: Array, + axesGeoms: Array, + themeAxisStyle: AxisStyle, + scales: SmallMultipleScales, +): Array { + const panelSize = getPanelSize(scales); + return getPerPanelMap(scales, () => { + // get grids per panel (depends on all the axis that exist + const lines = axesGeoms.reduce>((linesAcc, { axis, visibleTicks }) => { + const axisSpec = axesSpecs.find(({ id }) => id === axis.id); + if (!axisSpec) { + return linesAcc; + } + const linesForSpec = getGridLinesForSpec(axisSpec, visibleTicks, themeAxisStyle, panelSize); + if (!linesForSpec) { + return linesAcc; + } + return [...linesAcc, linesForSpec]; + }, []); + return { lineGroups: lines }; + }); +} + +/** + * Get grid lines for a specific axis + * @internal + * @param axisSpec + * @param visibleTicks + * @param themeAxisStyle + * @param panelSize + */ +export function getGridLinesForSpec( + axisSpec: AxisSpec, + visibleTicks: AxisTick[], + themeAxisStyle: AxisStyle, + panelSize: Size, +): GridLineGroup | null { + // vertical ==> horizontal grid lines + const isVertical = isVerticalAxis(axisSpec.position); + + // merge the axis configured style with the theme style + const axisStyle = mergePartial(themeAxisStyle, axisSpec.style as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + const gridLineThemeStyle = isVertical ? axisStyle.gridLine.vertical : axisStyle.gridLine.horizontal; + + // axis can have a configured grid line style + const gridLineStyles = axisSpec.gridLine ? mergePartial(gridLineThemeStyle, axisSpec.gridLine) : gridLineThemeStyle; + + const showGridLines = axisSpec.showGridLines ?? gridLineStyles.visible; + if (!showGridLines) { + return null; + } + + // compute all the lines points for the specific grid + const lines = visibleTicks.map((tick: AxisTick) => { + return isVertical + ? getGridLineForVerticalAxisAt(tick.position, panelSize) + : getGridLineForHorizontalAxisAt(tick.position, panelSize); + }); + + // define the stroke for the specific set of grid lines + if (!gridLineStyles.stroke || !gridLineStyles.strokeWidth || gridLineStyles.strokeWidth < MIN_STROKE_WIDTH) { + return null; + } + const strokeColor = stringToRGB(gridLineStyles.stroke); + strokeColor.opacity = + gridLineStyles.opacity !== undefined ? strokeColor.opacity * gridLineStyles.opacity : strokeColor.opacity; + const stroke: Stroke = { + color: strokeColor, + width: gridLineStyles.strokeWidth, + dash: gridLineStyles.dash, + }; + + return { + lines, + stroke, + axisId: axisSpec.id, + }; +} + +/** + * Get a horizontal grid line at `tickPosition` + * used for vertical axis specs + * @param tickPosition the position of the tick + * @param panelSize the size of the target panel + * @internal + */ +export function getGridLineForVerticalAxisAt(tickPosition: number, panelSize: Size): Line { + return { x1: 0, y1: tickPosition, x2: panelSize.width, y2: tickPosition }; +} + +/** + * Get a vertical grid line at `tickPosition` + * used for horizontal axis specs + * @param tickPosition the position of the tick + * @param panelSize the size of the target panel + * @internal + */ +export function getGridLineForHorizontalAxisAt(tickPosition: number, panelSize: Size): Line { + return { x1: tickPosition, y1: 0, x2: tickPosition, y2: panelSize.height }; +} diff --git a/src/chart_types/xy_chart/utils/group_data_series.ts b/src/chart_types/xy_chart/utils/group_data_series.ts new file mode 100644 index 0000000000..59c39e09cd --- /dev/null +++ b/src/chart_types/xy_chart/utils/group_data_series.ts @@ -0,0 +1,47 @@ +/* + * 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. + */ + +type Group = Record; +type GroupByKeyFn = (data: T) => string; +type GroupKeysOrKeyFn = Array | GroupByKeyFn; + +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: false): Group; +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: true): T[][]; +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: boolean): T[][] | Group { + const keyFn = Array.isArray(keysOrKeyFn) ? getUniqueKey(keysOrKeyFn) : keysOrKeyFn; + const grouped = data.reduce>((acc, curr) => { + const key = keyFn(curr); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(curr); + return acc; + }, {}); + return asArray ? Object.values(grouped) : grouped; +} + +export function getUniqueKey(keys: Array, concat = '|') { + return (data: T): string => { + return keys + .map((key) => { + return data[key]; + }) + .join(concat); + }; +} diff --git a/src/chart_types/xy_chart/utils/indexed_geometry_map.ts b/src/chart_types/xy_chart/utils/indexed_geometry_map.ts index 8cbdb03cb9..5b83c46548 100644 --- a/src/chart_types/xy_chart/utils/indexed_geometry_map.ts +++ b/src/chart_types/xy_chart/utils/indexed_geometry_map.ts @@ -22,6 +22,7 @@ import { $Values } from 'utility-types'; import { Bounds } from '../../../utils/d3-delaunay'; import { IndexedGeometry, isPointGeometry } from '../../../utils/geometry'; import { Point } from '../../../utils/point'; +import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup'; import { IndexedGeometryLinearMap } from './indexed_geometry_linear_map'; import { IndexedGeometrySpatialMap } from './indexed_geometry_spatial_map'; @@ -64,14 +65,25 @@ export class IndexedGeometryMap { } } - find(x: number | string | null, point?: Point): IndexedGeometry[] { + find( + x: number | string | null, + point?: Point, + smHorizontalValue?: PrimitiveValue, + smVerticalValue?: PrimitiveValue, + ): IndexedGeometry[] { if (x === null && !point) { return []; } const spatialValues = point === undefined ? [] : this.spatialMap.find(point); - return [...this.linearMap.find(x), ...spatialValues]; + const values = [...this.linearMap.find(x), ...spatialValues]; + if (!smHorizontalValue || !smVerticalValue) { + return values; + } + return values.filter(({ seriesIdentifier: { smHorizontalAccessorValue, smVerticalAccessorValue } }) => { + return smVerticalAccessorValue === smVerticalValue && smHorizontalAccessorValue === smHorizontalValue; + }); } getMergeData() { diff --git a/src/chart_types/xy_chart/utils/interactions.test.ts b/src/chart_types/xy_chart/utils/interactions.test.ts index e7753a476f..58096fbe34 100644 --- a/src/chart_types/xy_chart/utils/interactions.test.ts +++ b/src/chart_types/xy_chart/utils/interactions.test.ts @@ -17,10 +17,10 @@ * under the License. */ +import { MockBarGeometry, MockPointGeometry } from '../../../mocks'; import { isCrosshairTooltipType, isFollowTooltipType } from '../../../specs'; import { TooltipType } from '../../../specs/constants'; import { Dimensions } from '../../../utils/dimensions'; -import { IndexedGeometry, PointGeometry } from '../../../utils/geometry'; import { areIndexedGeometryArraysEquals, areIndexedGeomsEquals, @@ -46,7 +46,7 @@ const seriesStyle = { }, }; -const ig1: IndexedGeometry = { +const ig1 = MockBarGeometry.default({ color: 'red', seriesIdentifier: { specId: 'ig1', @@ -67,8 +67,8 @@ const ig1: IndexedGeometry = { width: 50, height: 50, seriesStyle, -}; -const ig2: IndexedGeometry = { +}); +const ig2 = MockBarGeometry.default({ seriesIdentifier: { specId: 'ig1', key: '', @@ -89,8 +89,8 @@ const ig2: IndexedGeometry = { width: 10, height: 10, seriesStyle, -}; -const ig3: IndexedGeometry = { +}); +const ig3 = MockBarGeometry.default({ seriesIdentifier: { specId: 'ig1', key: '', @@ -112,8 +112,8 @@ const ig3: IndexedGeometry = { width: 50, height: 50, seriesStyle, -}; -const ig4: IndexedGeometry = { +}); +const ig4 = MockBarGeometry.default({ seriesIdentifier: { specId: 'ig4', key: '', @@ -134,8 +134,8 @@ const ig4: IndexedGeometry = { width: 50, height: 50, seriesStyle, -}; -const ig5: IndexedGeometry = { +}); +const ig5 = MockBarGeometry.default({ seriesIdentifier: { specId: 'ig5', key: '', @@ -156,8 +156,8 @@ const ig5: IndexedGeometry = { width: 50, height: 50, seriesStyle, -}; -const ig6: PointGeometry = { +}); +const ig6 = MockPointGeometry.default({ seriesIdentifier: { specId: 'ig5', key: '', @@ -180,7 +180,8 @@ const ig6: PointGeometry = { x: 0, y: 0, }, -}; +}); + describe('Interaction utils', () => { const chartDimensions: Dimensions = { width: 200, diff --git a/src/chart_types/xy_chart/utils/interactions.ts b/src/chart_types/xy_chart/utils/interactions.ts index dbd384954f..6d6228d6fa 100644 --- a/src/chart_types/xy_chart/utils/interactions.ts +++ b/src/chart_types/xy_chart/utils/interactions.ts @@ -18,7 +18,7 @@ */ import { Rotation } from '../../../utils/commons'; -import { Dimensions } from '../../../utils/dimensions'; +import { Size } from '../../../utils/dimensions'; import { BarGeometry, PointGeometry, IndexedGeometry, isPointGeometry, isBarGeometry } from '../../../utils/geometry'; /** @@ -29,7 +29,7 @@ import { BarGeometry, PointGeometry, IndexedGeometry, isPointGeometry, isBarGeom * @param chartDimension the chart dimension * @internal */ -export function getOrientedXPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Dimensions) { +export function getOrientedXPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Size) { switch (chartRotation) { case 180: return chartDimension.width - xPos; @@ -44,7 +44,7 @@ export function getOrientedXPosition(xPos: number, yPos: number, chartRotation: } /** @internal */ -export function getOrientedYPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Dimensions) { +export function getOrientedYPosition(xPos: number, yPos: number, chartRotation: Rotation, chartDimension: Size) { switch (chartRotation) { case 180: return chartDimension.height - yPos; diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts index 471a99d0fd..281f74cd8b 100644 --- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts @@ -91,21 +91,15 @@ describe('Non-Stacked Series Utils', () => { test('empty data', () => { const store = MockStore.default(); MockStore.addSpecs(EMPTY_DATA_SET, store); - const { - formattedDataSeries: { nonStacked }, - } = computeSeriesDomainsSelector(store.getState()); - expect(nonStacked).toHaveLength(0); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toHaveLength(0); }); test('format data without nulls', () => { const store = MockStore.default(); MockStore.addSpecs(STANDARD_DATA_SET, store); - const { - formattedDataSeries: { - nonStacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 10, x: 0, @@ -113,7 +107,7 @@ describe('Non-Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 20, x: 0, @@ -121,7 +115,7 @@ describe('Non-Stacked Series Utils', () => { y1: 20, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: null, initialY1: 30, x: 0, @@ -133,13 +127,9 @@ describe('Non-Stacked Series Utils', () => { test('format data with nulls', () => { const store = MockStore.default(); MockStore.addSpecs(WITH_NULL_DATASET, store); - const { - formattedDataSeries: { - nonStacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -151,13 +141,9 @@ describe('Non-Stacked Series Utils', () => { test('format data without nulls with y0 values', () => { const store = MockStore.default(); MockStore.addSpecs(STANDARD_DATA_SET_WY0, store); - const { - formattedDataSeries: { - nonStacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: 2, initialY1: 10, x: 0, @@ -165,7 +151,7 @@ describe('Non-Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: 4, initialY1: 20, x: 0, @@ -173,7 +159,7 @@ describe('Non-Stacked Series Utils', () => { y1: 20, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: 6, initialY1: 30, x: 0, @@ -185,13 +171,9 @@ describe('Non-Stacked Series Utils', () => { test('format data with nulls - fit functions', () => { const store = MockStore.default(); MockStore.addSpecs(WITH_NULL_DATASET_WY0, store); - const { - formattedDataSeries: { - nonStacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: 2, initialY1: 10, x: 0, @@ -199,7 +181,7 @@ describe('Non-Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -207,7 +189,7 @@ describe('Non-Stacked Series Utils', () => { y0: null, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: 6, initialY1: 30, x: 0, @@ -219,19 +201,15 @@ describe('Non-Stacked Series Utils', () => { test('format data without nulls on second series', () => { const store = MockStore.default(); MockStore.addSpecs(DATA_SET_WITH_NULL_2, store); - const { - formattedDataSeries: { - nonStacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries.length).toBe(2); + expect(formattedDataSeries.length).toBe(2); // this because linear non stacked area/lines doesn't fill up the dataset // with missing x data points - expect(dataSeries[0].data.length).toBe(3); - expect(dataSeries[1].data.length).toBe(2); + expect(formattedDataSeries[0].data.length).toBe(3); + expect(formattedDataSeries[1].data.length).toBe(2); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 1, x: 1, @@ -239,7 +217,7 @@ describe('Non-Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(dataSeries[0].data[1]).toMatchObject({ + expect(formattedDataSeries[0].data[1]).toMatchObject({ initialY0: null, initialY1: 2, x: 2, @@ -247,7 +225,7 @@ describe('Non-Stacked Series Utils', () => { y1: 2, mark: null, }); - expect(dataSeries[0].data[2]).toMatchObject({ + expect(formattedDataSeries[0].data[2]).toMatchObject({ initialY0: null, initialY1: 4, x: 4, @@ -255,7 +233,7 @@ describe('Non-Stacked Series Utils', () => { y1: 4, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 21, x: 1, @@ -263,7 +241,7 @@ describe('Non-Stacked Series Utils', () => { y1: 21, mark: null, }); - expect(dataSeries[1].data[1]).toMatchObject({ + expect(formattedDataSeries[1].data[1]).toMatchObject({ initialY0: null, initialY1: 23, x: 3, diff --git a/src/chart_types/xy_chart/utils/panel.ts b/src/chart_types/xy_chart/utils/panel.ts new file mode 100644 index 0000000000..218f0894c9 --- /dev/null +++ b/src/chart_types/xy_chart/utils/panel.ts @@ -0,0 +1,25 @@ +/* + * 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 { Size } from '../../../utils/dimensions'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; + +/** @internal */ +export function getPanelSize({ horizontal, vertical }: SmallMultipleScales): Size { + return { width: horizontal.bandwidth, height: vertical.bandwidth }; +} diff --git a/src/chart_types/xy_chart/utils/panel_utils.ts b/src/chart_types/xy_chart/utils/panel_utils.ts new file mode 100644 index 0000000000..686f61e4a5 --- /dev/null +++ b/src/chart_types/xy_chart/utils/panel_utils.ts @@ -0,0 +1,59 @@ +/* + * 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 { Point } from '../../../utils/point'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; + +/** @internal */ +export interface PerPanelMap { + panelAnchor: Point; + horizontalValue: any; + verticalValue: any; +} + +/** @internal */ +export function getPerPanelMap( + scales: SmallMultipleScales, + fn: (panelAnchor: Point, horizontalValue: any, verticalValue: any, scales: SmallMultipleScales) => T | null, +): Array { + const { horizontal, vertical } = scales; + return vertical.domain.reduce>((acc, verticalValue) => { + return [ + ...acc, + ...horizontal.domain.reduce>((hAcc, horizontalValue) => { + const panelAnchor: Point = { + x: horizontal.scale(horizontalValue) ?? 0, + y: vertical.scale(verticalValue) ?? 0, + }; + const fnObj = fn(panelAnchor, horizontalValue, verticalValue, scales); + if (!fnObj) { + return hAcc; + } + return [ + ...hAcc, + { + panelAnchor, + horizontalValue, + verticalValue, + ...fnObj, + }, + ]; + }, []), + ]; + }, []); +} diff --git a/src/chart_types/xy_chart/utils/scales.test.ts b/src/chart_types/xy_chart/utils/scales.test.ts index 6f3454440b..e58de0c17a 100644 --- a/src/chart_types/xy_chart/utils/scales.test.ts +++ b/src/chart_types/xy_chart/utils/scales.test.ts @@ -19,8 +19,7 @@ import { ScaleType } from '../../../scales/constants'; import { XDomain } from '../domains/types'; -import { computeXScale, countBarsInCluster } from './scales'; -import { FormattedDataSeries } from './series'; +import { computeXScale } from './scales'; describe('Series scales', () => { const xDomainLinear: XDomain = { @@ -44,7 +43,7 @@ describe('Series scales', () => { const expectedBandwidth = 120 / 4; expect(scale.bandwidth).toBe(120 / 4); expect(scale.scale(0)).toBe(0); - expect(scale.scale(1)).toBe(expectedBandwidth * 1); + expect(scale.scale(1)).toBe(expectedBandwidth); expect(scale.scale(2)).toBe(expectedBandwidth * 2); expect(scale.scale(3)).toBe(expectedBandwidth * 3); }); @@ -55,8 +54,8 @@ describe('Series scales', () => { expect(scale.bandwidth).toBe(expectedBandwidth); expect(scale.scale(0)).toBe(expectedBandwidth * 3); expect(scale.scale(1)).toBe(expectedBandwidth * 2); - expect(scale.scale(2)).toBe(expectedBandwidth * 1); - expect(scale.scale(3)).toBe(expectedBandwidth * 0); + expect(scale.scale(2)).toBe(expectedBandwidth); + expect(scale.scale(3)).toBe(0); }); describe('computeXScale with single value domain', () => { @@ -121,70 +120,71 @@ describe('Series scales', () => { expect(zeroGroupScale.bandwidth).toBe(expectedBandwidth); }); - test('count bars required on a cluster', () => { - const stacked: FormattedDataSeries[] = [ - { - groupId: 'g1', - dataSeries: [], - counts: { - area: 10, - bar: 2, - line: 2, - bubble: 0, - }, - }, - { - groupId: 'g2', - dataSeries: [], - counts: { - area: 10, - bar: 20, - line: 2, - bubble: 0, - }, - }, - { - groupId: 'g3', - dataSeries: [], - counts: { - area: 10, - bar: 0, - line: 2, - bubble: 0, - }, - }, - ]; - const nonStacked: FormattedDataSeries[] = [ - { - groupId: 'g1', - dataSeries: [], - counts: { - area: 10, - bar: 5, - line: 2, - bubble: 0, - }, - }, - { - groupId: 'g2', - dataSeries: [], - counts: { - area: 10, - bar: 7, - line: 2, - bubble: 0, - }, - }, - ]; - const { nonStackedBarsInCluster, stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster( - stacked, - nonStacked, - ); - expect(nonStackedBarsInCluster).toBe(12); - // count one per group - expect(stackedBarsInCluster).toBe(2); - expect(totalBarsInCluster).toBe(14); - }); + // test('count bars required on a cluster', () => { + // const stacked: FormattedDataSeries[] = [ + // { + // groupId: 'g1', + // dataSeries: [], + // counts: { + // area: 10, + // bar: 2, + // line: 2, + // bubble: 0, + // }, + // }, + // { + // groupId: 'g2', + // dataSeries: [], + // counts: { + // area: 10, + // bar: 20, + // line: 2, + // bubble: 0, + // }, + // }, + // { + // groupId: 'g3', + // dataSeries: [], + // counts: { + // area: 10, + // bar: 0, + // line: 2, + // bubble: 0, + // }, + // }, + // ]; + // const nonStacked: FormattedDataSeries[] = [ + // { + // groupId: 'g1', + // dataSeries: [], + // counts: { + // area: 10, + // bar: 5, + // line: 2, + // bubble: 0, + // }, + // }, + // { + // groupId: 'g2', + // dataSeries: [], + // counts: { + // area: 10, + // bar: 7, + // line: 2, + // bubble: 0, + // }, + // }, + // ]; + // const { nonStackedBarsInCluster, stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster( + // stacked, + // nonStacked, + // ); + // expect(nonStackedBarsInCluster).toBe(12); + // // count one per group + // expect(stackedBarsInCluster).toBe(2); + // expect(totalBarsInCluster).toBe(14); + // }); + describe('bandwidth when totalBarsInCluster is greater than 0 or less than 0', () => { const xDomainLinear: XDomain = { type: 'xDomain', diff --git a/src/chart_types/xy_chart/utils/scales.ts b/src/chart_types/xy_chart/utils/scales.ts index 579bf4031e..bfc92ef7e1 100644 --- a/src/chart_types/xy_chart/utils/scales.ts +++ b/src/chart_types/xy_chart/utils/scales.ts @@ -21,37 +21,6 @@ import { Scale, ScaleBand, ScaleContinuous } from '../../../scales'; import { ScaleType } from '../../../scales/constants'; import { GroupId } from '../../../utils/ids'; import { XDomain, YDomain } from '../domains/types'; -import { FormattedDataSeries } from './series'; -import { SeriesTypes } from './specs'; - -/** - * Count the max number of bars in cluster value. - * Doesn't take in consideration areas, lines or points. - * @param stacked all the stacked formatted dataseries - * @param nonStacked all the non-stacked formatted dataseries - * @internal - */ -export function countBarsInCluster( - stacked: FormattedDataSeries[], - nonStacked: FormattedDataSeries[], -): { - nonStackedBarsInCluster: number; - stackedBarsInCluster: number; - totalBarsInCluster: number; -} { - // along x axis, we count one "space" per bar series. - // we ignore the points, areas, lines as they are - // aligned with the x value and doesn't occupy space - const nonStackedBarsInCluster = nonStacked.reduce((acc, ns) => acc + ns.counts[SeriesTypes.Bar], 0); - // count stacked bars groups as 1 per group - const stackedBarsInCluster = stacked.reduce((acc, ns) => acc + (ns.counts[SeriesTypes.Bar] > 0 ? 1 : 0), 0); - const totalBarsInCluster = nonStackedBarsInCluster + stackedBarsInCluster; - return { - nonStackedBarsInCluster, - stackedBarsInCluster, - totalBarsInCluster, - }; -} function getBandScaleRange( isInverse: boolean, diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 1404d6e040..65d2e00971 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -18,6 +18,7 @@ */ import { ChartTypes } from '../..'; +import { MockDataSeries } from '../../../mocks/series'; import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; @@ -28,113 +29,128 @@ import { AccessorFn } from '../../../utils/accessor'; import { Position } from '../../../utils/commons'; import * as TestDataset from '../../../utils/data_samples/test_dataset'; import { ColorConfig } from '../../../utils/themes/theme'; -import { splitSpecsByGroupId } from '../domains/y_domain'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; import { SeriesCollectionValue, - getFormattedDataseries, + getFormattedDataSeries, getSeriesColors, getSortedDataSeriesColorsValuesMap, - getDataSeriesBySpecId, - splitSeriesDataByAccessors, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, - extractYandMarkFromDatum, + extractYAndMarkFromDatum, getSeriesName, DataSeries, + splitSeriesDataByAccessors, } from './series'; import { BasicSeriesSpec, LineSeriesSpec, SeriesTypes, AreaSeriesSpec } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; const dg = new SeededDataGenerator(); +function matchOnlyDataSeriesLegacySnapshot(d: DataSeries) { + const { + spec, + groupId, + isStacked, + seriesType, + smVerticalAccessorValue, + smHorizontalAccessorValue, + stackMode, + ...rest + } = d; + return { + ...rest, + }; +} + describe('Series', () => { test('Can split dataset into 1Y0G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y0G, xAccessor: 'x', - yAccessors: ['y1'], - splitSeriesAccessors: ['y'], - }, + yAccessors: ['y'], + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset into 1Y1G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y1G, xAccessor: 'x', yAccessors: ['y'], - }, + splitSeriesAccessors: ['g'], + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset into 1Y2G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y2G, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset into 2Y0G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y0G, xAccessor: 'x', yAccessors: ['y1', 'y2'], - }, + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset into 2Y1G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y1G, xAccessor: 'x', yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g'], - }, + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset into 2Y2G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor: 'x', yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); it('should get sum of all xValues', () => { const xValueSums = new Map(); splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y1G_ORDINAL, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], - }, + }), xValueSums, ); expect(xValueSums).toEqual( @@ -167,15 +183,13 @@ describe('Series', () => { store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries).toMatchSnapshot(); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack multiple dataseries', () => { const dataSeries: DataSeries[] = [ - { + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -187,8 +201,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -200,8 +214,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -213,8 +227,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -226,11 +240,11 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, + }), ]; const xValues = new Set([1, 2, 3, 4]); const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); - expect(stackedValues).toMatchSnapshot(); + expect(stackedValues.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack unsorted dataseries', () => { const store = MockStore.default(); @@ -251,16 +265,14 @@ describe('Series', () => { }), store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries).toMatchSnapshot(); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack high volume of dataseries', () => { const maxArrayItems = 1000; const dataSeries: DataSeries[] = [ - { + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -269,8 +281,8 @@ describe('Series', () => { data: new Array(maxArrayItems) .fill(0) .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -279,11 +291,11 @@ describe('Series', () => { data: new Array(maxArrayItems) .fill(0) .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), - }, + }), ]; const xValues = new Set(new Array(maxArrayItems).fill(0).map((d, i) => i)); const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); - expect(stackedValues).toMatchSnapshot(); + expect(stackedValues.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack simple dataseries with scale to extent', () => { const store = MockStore.default(); @@ -312,8 +324,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack multiple dataseries with scale to extent', () => { const store = MockStore.default(); @@ -353,8 +365,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack simple dataseries with y0', () => { const store = MockStore.default(); @@ -386,8 +398,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can stack simple dataseries with scale to extent with y0', () => { const store = MockStore.default(); @@ -419,8 +431,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('should split an array of specs into data series', () => { @@ -452,9 +464,9 @@ describe('Series', () => { hideInLegend: false, }; - const splittedDataSeries = getDataSeriesBySpecId([spec1, spec2]); - expect(splittedDataSeries.dataSeriesBySpecId.get('spec1')).toMatchSnapshot(); - expect(splittedDataSeries.dataSeriesBySpecId.get('spec2')).toMatchSnapshot(); + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + expect(dataSeries.filter(({ specId }) => specId === 'spec1')).toMatchSnapshot(); + expect(dataSeries.filter(({ specId }) => specId === 'spec2')).toMatchSnapshot(); }); test('should compute data series for stacked specs', () => { const spec1: BasicSeriesSpec = { @@ -485,17 +497,11 @@ describe('Series', () => { hideInLegend: false, }; const xValues = new Set([0, 1, 2, 3]); - const splittedDataSeries = getDataSeriesBySpecId([spec1, spec2]); - const specsByGroupIds = splitSpecsByGroupId([spec1, spec2]); - - const stackedDataSeries = getFormattedDataseries( - splittedDataSeries.dataSeriesBySpecId, - xValues, - ScaleType.Linear, - [spec1, spec2], - specsByGroupIds, - ); - expect(stackedDataSeries.stacked).toMatchSnapshot(); + + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + const stackedDataSeries = getFormattedDataSeries([spec1, spec2], dataSeries, xValues, ScaleType.Linear); + + expect(stackedDataSeries.map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); describe('#getSeriesColors', () => { @@ -565,12 +571,12 @@ describe('Series', () => { }); }); test('should only include deselectedDataSeries when splitting series if deselectedDataSeries is defined', () => { - const specId = 'splitSpec'; + const id = 'splitSpec'; const splitSpec: BasicSeriesSpec = { specType: SpecTypes.Series, chartType: ChartTypes.XYAxis, - id: specId, + id, groupId: 'group', seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, @@ -582,23 +588,24 @@ describe('Series', () => { hideInLegend: false, }; - const allSeries = getDataSeriesBySpecId([splitSpec]); - expect(allSeries.dataSeriesBySpecId.get(specId)?.length).toBe(2); + const allSeries = getDataSeriesFromSpecs([splitSpec]); + expect(allSeries.dataSeries.filter(({ specId }) => specId === id)).toHaveLength(2); - const emptyDeselected = getDataSeriesBySpecId([splitSpec]); - expect(emptyDeselected.dataSeriesBySpecId.get(specId)?.length).toBe(2); + const emptyDeselected = getDataSeriesFromSpecs([splitSpec]); + expect(emptyDeselected.dataSeries.filter(({ specId }) => specId === id)).toHaveLength(2); const deselectedDataSeries: XYChartSeriesIdentifier[] = [ { - specId, + specId: id, yAccessor: splitSpec.yAccessors[0], splitAccessors: new Map(), seriesKeys: [], - key: 'spec{splitSpec}yAccessor{y1}splitAccessors{}', + key: + 'groupId{group}spec{splitSpec}yAccessor{y1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', }, ]; - const subsetSplit = getDataSeriesBySpecId([splitSpec], deselectedDataSeries); - expect(subsetSplit.dataSeriesBySpecId.get(specId)?.length).toBe(1); + const subsetSplit = getDataSeriesFromSpecs([splitSpec], deselectedDataSeries); + expect(subsetSplit.dataSeries.filter(({ specId }) => specId === id)).toHaveLength(1); }); test('should sort series color by series spec sort index', () => { @@ -675,26 +682,26 @@ describe('Series', () => { expect(getSortedDataSeriesColorsValuesMap(seriesCollection)).toEqual(undefinedSortedColorValues); }); test('clean datum shall parse string as number for y values', () => { - let datum = extractYandMarkFromDatum([0, 1, 2], 1, [], 2); + let datum = extractYAndMarkFromDatum([0, 1, 2], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, '1', 2], 1, [], 2); + datum = extractYAndMarkFromDatum([0, '1', 2], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, '1', '2'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, '1', '2'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, 1, '2'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, 1, '2'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, 'invalid', 'invalid'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, 'invalid', 'invalid'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(null); expect(datum?.y0).toBe(null); @@ -919,32 +926,32 @@ describe('Series', () => { test('Can split dataset into 2Y2G series', () => { const xAccessor: AccessorFn = (d) => d.x; const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor, yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()].length).toBe(8); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Can split dataset with custom _all xAccessor', () => { const xAccessor: AccessorFn = () => '_all'; const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor, yAccessors: ['y1'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()].length).toBe(1); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Shall ignore undefined values on splitSeriesAccessors', () => { @@ -966,7 +973,7 @@ describe('Series', () => { }); const splitSeries = splitSeriesDataByAccessors(spec, new Map()); expect([...splitSeries.dataSeries.values()].length).toBe(2); - expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); + expect([...splitSeries.dataSeries.values()].map(matchOnlyDataSeriesLegacySnapshot)).toMatchSnapshot(); }); test('Should ignore series if splitSeriesAccessors are defined but not contained in any datum', () => { const spec = MockSeriesSpec.bar({ diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index d00eff0764..67bc014396 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -19,17 +19,18 @@ import { SeriesIdentifier, SeriesKey } from '../../../commons/series_id'; import { ScaleType } from '../../../scales/constants'; -import { BinAgg, Direction, XScaleType } from '../../../specs'; +import { GroupBySpec, BinAgg, Direction, XScaleType, DEFAULT_SINGLE_PANEL_SM_VALUE } from '../../../specs'; import { OrderBy } from '../../../specs/settings'; import { ColorOverrides } from '../../../state/chart_state'; import { Accessor, AccessorFn, getAccessorValue } from '../../../utils/accessor'; -import { Datum, Color } from '../../../utils/commons'; -import { GroupId, SpecId } from '../../../utils/ids'; +import { Datum, Color, isNil } from '../../../utils/commons'; +import { GroupId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { ColorConfig } from '../../../utils/themes/theme'; -import { YBasicSeriesSpec } from '../domains/y_domain'; +import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec } from '../domains/y_domain'; import { LastValues } from '../state/utils/types'; import { applyFitFunctionToDataSeries } from './fit_function_utils'; +import { groupBy } from './group_data_series'; import { BasicSeriesSpec, SeriesTypes, SeriesSpecs, SeriesNameConfigOptions, StackMode } from './specs'; import { formatStackedDataSeriesValues, datumXSortPredicate } from './stacked_series_utils'; @@ -69,12 +70,19 @@ export interface DataSeriesDatum { export interface XYChartSeriesIdentifier extends SeriesIdentifier { yAccessor: string | number; splitAccessors: Map; // does the map have a size vs making it optional + smVerticalAccessorValue?: string | number; + smHorizontalAccessorValue?: string | number; seriesKeys: (string | number)[]; } /** @internal */ export type DataSeries = XYChartSeriesIdentifier & { + groupId: GroupId; + seriesType: SeriesTypes; data: DataSeriesDatum[]; + isStacked: boolean; + stackMode: StackMode | undefined; + spec: Exclude; }; /** @internal */ @@ -113,26 +121,33 @@ export function getSeriesIndex(series: SeriesIdentifier[], target: SeriesIdentif * @internal */ export function splitSeriesDataByAccessors( - { + spec: BasicSeriesSpec, + xValueSums: Map, + isStacked = false, + enableVislibSeriesSort = false, + stackMode?: StackMode, + smallMultiples?: { vertical?: GroupBySpec; horizontal?: GroupBySpec }, +): { + dataSeries: Map; + xValues: Array; + smVValues: Set; + smHValues: Set; +} { + const { + seriesType, id: specId, + groupId, data, xAccessor, yAccessors, y0Accessors, markSizeAccessor, splitSeriesAccessors = [], - }: Pick< - BasicSeriesSpec, - 'id' | 'data' | 'xAccessor' | 'yAccessors' | 'y0Accessors' | 'splitSeriesAccessors' | 'markSizeAccessor' - >, - xValueSums: Map, - enableVislibSeriesSort = false, -): { - dataSeries: Map; - xValues: Array; -} { + } = spec; const dataSeries = new Map(); const xValues: Array = []; + const smVValues: Set = new Set(); + const smHValues: Set = new Set(); const nonNumericValues: any[] = []; if (enableVislibSeriesSort) { @@ -142,29 +157,44 @@ export function splitSeriesDataByAccessors( * The difference from below is that it loops through all the yAsccessors before the data. */ yAccessors.forEach((accessor, index) => { - data.forEach((datum) => { + for (let i = 0; i < data.length; i++) { + const datum = data[i]; const splitAccessors = getSplitAccessors(datum, splitSeriesAccessors); // if splitSeriesAccessors are defined we should have at least one split value to include datum if (splitSeriesAccessors.length > 0 && splitAccessors.size < 1) { - return; + continue; } // skip if the datum is not an object or null if (typeof datum !== 'object' || datum === null) { - return; + continue; } - const x = getAccessorValue(datum, xAccessor); // skip if the x value is not a string or a number if (typeof x !== 'string' && typeof x !== 'number') { - return; + continue; } xValues.push(x); let sum = xValueSums.get(x) ?? 0; - const cleanedDatum = extractYandMarkFromDatum( + // extract small multiples aggregation values + const smH = smallMultiples?.horizontal?.by + ? smallMultiples.horizontal?.by(spec, datum) + : DEFAULT_SINGLE_PANEL_SM_VALUE; + if (!isNil(smH)) { + smHValues.add(smH); + } + + const smV = smallMultiples?.vertical?.by + ? smallMultiples.vertical.by(spec, datum) + : DEFAULT_SINGLE_PANEL_SM_VALUE; + if (!isNil(smV)) { + smVValues.add(smV); + } + + const cleanedDatum = extractYAndMarkFromDatum( datum, accessor, nonNumericValues, @@ -172,54 +202,74 @@ export function splitSeriesDataByAccessors( markSizeAccessor, ); const seriesKeys = [...splitAccessors.values(), accessor]; - const seriesKey = getSeriesKey({ + const seriesIdentifier = { specId, + groupId, + seriesType, yAccessor: accessor, splitAccessors, - }); + smVerticalAccessorValue: smV, + smHorizontalAccessorValue: smH, + stackMode, + }; + const seriesKey = getSeriesKey(seriesIdentifier, groupId); sum += cleanedDatum.y1 ?? 0; - const newDatum = { x, ...cleanedDatum }; + const newDatum = { x, ...cleanedDatum, smH, smV }; const series = dataSeries.get(seriesKey); if (series) { series.data.push(newDatum); } else { dataSeries.set(seriesKey, { - specId, - yAccessor: accessor, - splitAccessors, - data: [newDatum], - key: seriesKey, + ...seriesIdentifier, + isStacked, seriesKeys, + key: seriesKey, + data: [newDatum], + spec, }); } xValueSums.set(x, sum); - }); + } }); } else { - data.forEach((datum) => { + for (let i = 0; i < data.length; i++) { + const datum = data[i]; const splitAccessors = getSplitAccessors(datum, splitSeriesAccessors); // if splitSeriesAccessors are defined we should have at least one split value to include datum if (splitSeriesAccessors.length > 0 && splitAccessors.size < 1) { - return; + continue; } // skip if the datum is not an object or null if (typeof datum !== 'object' || datum === null) { - return; + continue; } - const x = getAccessorValue(datum, xAccessor); - // skip if the x value is not a string or a number if (typeof x !== 'string' && typeof x !== 'number') { - return; + continue; } xValues.push(x); let sum = xValueSums.get(x) ?? 0; + // extract small multiples aggregation values + const smH = smallMultiples?.horizontal?.by + ? smallMultiples.horizontal?.by(spec, datum) + : DEFAULT_SINGLE_PANEL_SM_VALUE; + if (!isNil(smH)) { + smHValues.add(smH); + } + + const smV = smallMultiples?.vertical?.by + ? smallMultiples.vertical.by(spec, datum) + : DEFAULT_SINGLE_PANEL_SM_VALUE; + if (!isNil(smV)) { + smVValues.add(smV); + } + yAccessors.forEach((accessor, index) => { - const cleanedDatum = extractYandMarkFromDatum( + const cleanedDatum = extractYAndMarkFromDatum( datum, accessor, nonNumericValues, @@ -227,29 +277,36 @@ export function splitSeriesDataByAccessors( markSizeAccessor, ); const seriesKeys = [...splitAccessors.values(), accessor]; - const seriesKey = getSeriesKey({ + const seriesIdentifier = { specId, + groupId, + seriesType, yAccessor: accessor, splitAccessors, - }); + smVerticalAccessorValue: smV, + smHorizontalAccessorValue: smH, + stackMode, + }; + const seriesKey = getSeriesKey(seriesIdentifier, groupId); sum += cleanedDatum.y1 ?? 0; - const newDatum = { x, ...cleanedDatum }; + const newDatum = { x, ...cleanedDatum, smH, smV }; const series = dataSeries.get(seriesKey); if (series) { series.data.push(newDatum); } else { dataSeries.set(seriesKey, { - specId, - yAccessor: accessor, - splitAccessors, - data: [newDatum], - key: seriesKey, + ...seriesIdentifier, + isStacked, seriesKeys, + key: seriesKey, + data: [newDatum], + spec, }); } + + xValueSums.set(x, sum); }); - xValueSums.set(x, sum); - }); + } } if (nonNumericValues.length > 0) { @@ -258,10 +315,11 @@ export function splitSeriesDataByAccessors( `(${nonNumericValues.map((v) => JSON.stringify(v)).join(', ')})`, ); } - return { dataSeries, xValues, + smVValues, + smHValues, }; } @@ -269,16 +327,26 @@ export function splitSeriesDataByAccessors( * Gets global series key to id any series as a string * @internal */ -export function getSeriesKey({ - specId, - yAccessor, - splitAccessors, -}: Pick): string { +export function getSeriesKey( + { + specId, + yAccessor, + splitAccessors, + smVerticalAccessorValue, + smHorizontalAccessorValue, + }: Pick< + XYChartSeriesIdentifier, + 'specId' | 'yAccessor' | 'splitAccessors' | 'smVerticalAccessorValue' | 'smHorizontalAccessorValue' + >, + groupId: GroupId, +): string { const joinedAccessors = [...splitAccessors.entries()] .sort(([a], [b]) => (a > b ? 1 : -1)) .map(([key, value]) => `${key}-${value}`) .join('|'); - return `spec{${specId}}yAccessor{${yAccessor}}splitAccessors{${joinedAccessors}}`; + const smV = smVerticalAccessorValue ? `smV{${smVerticalAccessorValue}}` : ''; + const smH = smHorizontalAccessorValue ? `smH{${smHorizontalAccessorValue}}` : ''; + return `groupId{${groupId}}spec{${specId}}yAccessor{${yAccessor}}splitAccessors{${joinedAccessors}}${smV}${smH}`; } /** @@ -302,7 +370,7 @@ function getSplitAccessors(datum: Datum, accessors: Accessor[] = []): Map, +export function getFormattedDataSeries( + seriesSpecs: SeriesSpecs, + availableDataSeries: DataSeries[], xValues: Set, xScaleType: ScaleType, - seriesSpecs: SeriesSpecs, - specsByGroupIdsEntries: Map< - GroupId, - { - stackMode: StackMode | undefined; - stacked: YBasicSeriesSpec[]; - nonStacked: YBasicSeriesSpec[]; - } - >, -): { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; -} { - const stackedFormattedDataSeries: { - groupId: GroupId; - dataSeries: DataSeries[]; - counts: DataSeriesCounts; - stackMode?: StackMode; - }[] = []; - const nonStackedFormattedDataSeries: { - groupId: GroupId; - dataSeries: DataSeries[]; - counts: DataSeriesCounts; - }[] = []; - - [...specsByGroupIdsEntries.entries()].forEach(([groupId, groupSpecs]) => { - const { stackMode } = groupSpecs; - // format stacked data series - const stackedDataSeries = getDataSeriesBySpecGroup(groupSpecs.stacked, availableDataSeries); - const fittedStackedDataSeries = applyFitFunctionToDataSeries( - getSortedDataSeries(stackedDataSeries.dataSeries, xValues, xScaleType), - seriesSpecs, - xScaleType, - ); - const fittedAndStackedDataSeries = formatStackedDataSeriesValues(fittedStackedDataSeries, xValues, stackMode); - - stackedFormattedDataSeries.push({ - groupId, - counts: stackedDataSeries.counts, - dataSeries: fittedAndStackedDataSeries, - stackMode, - }); +): DataSeries[] { + const histogramEnabled = isHistogramEnabled(seriesSpecs); + + // apply fit function to every data series + const fittedDataSeries = applyFitFunctionToDataSeries( + getSortedDataSeries(availableDataSeries, xValues, xScaleType), + seriesSpecs, + xScaleType, + ); - // format non stacked data series - const nonStackedDataSeries = getDataSeriesBySpecGroup(groupSpecs.nonStacked, availableDataSeries); - const fittedNonStackedDataSeries = applyFitFunctionToDataSeries( - getSortedDataSeries(nonStackedDataSeries.dataSeries, xValues, xScaleType), - seriesSpecs, - xScaleType, - ); - nonStackedFormattedDataSeries.push({ - groupId, - counts: nonStackedDataSeries.counts, - dataSeries: fittedNonStackedDataSeries, - }); - }); - return { - stacked: stackedFormattedDataSeries.filter((ds) => ds.dataSeries.length > 0), - nonStacked: nonStackedFormattedDataSeries.filter((ds) => ds.dataSeries.length > 0), - }; -} + // apply fitting for stacked DataSeries by YGroup, Panel + const stackedDataSeries = fittedDataSeries.filter(({ spec }) => isStackedSpec(spec, histogramEnabled)); + const stackedGroups = groupBy( + stackedDataSeries, + ['smHorizontalAccessorValue', 'smVerticalAccessorValue', 'groupId'], + true, + ); -function getDataSeriesBySpecGroup( - seriesSpecs: YBasicSeriesSpec[], - dataSeries: Map, -): { - dataSeries: DataSeries[]; - counts: DataSeriesCounts; -} { - return seriesSpecs.reduce<{ - dataSeries: DataSeries[]; - counts: DataSeriesCounts; - }>( - (acc, { id, seriesType }) => { - const ds = dataSeries.get(id); - if (!ds) { - return acc; - } + const fittedAndStackedDataSeries = stackedGroups.reduce((acc, dataSeries) => { + const [{ stackMode }] = dataSeries; + const formatted = formatStackedDataSeriesValues(dataSeries, xValues, stackMode); + return [...acc, ...formatted]; + }, []); + // get already fitted non stacked dataSeries + const nonStackedDataSeries = fittedDataSeries.filter(({ spec }) => !isStackedSpec(spec, histogramEnabled)); - acc.dataSeries.push(...ds); - acc.counts[seriesType] += ds.length; - return acc; - }, - { - dataSeries: [], - counts: { - [SeriesTypes.Bar]: 0, - [SeriesTypes.Area]: 0, - [SeriesTypes.Line]: 0, - [SeriesTypes.Bubble]: 0, - }, - }, - ); + return [...fittedAndStackedDataSeries, ...nonStackedDataSeries]; } /** @@ -450,28 +453,39 @@ function getDataSeriesBySpecGroup( * @param seriesSpecs the map for all the series spec * @param deselectedDataSeries the array of deselected/hidden data series * @param enableVislibSeriesSort is optional; if not specified in , + * @param smallMultiples * @internal */ -export function getDataSeriesBySpecId( +export function getDataSeriesFromSpecs( seriesSpecs: BasicSeriesSpec[], deselectedDataSeries: SeriesIdentifier[] = [], orderOrdinalBinsBy?: OrderBy, enableVislibSeriesSort?: boolean, + smallMultiples?: { vertical?: GroupBySpec; horizontal?: GroupBySpec }, ): { - dataSeriesBySpecId: Map; + dataSeries: DataSeries[]; seriesCollection: Map; xValues: Set; + smVValues: Set; + smHValues: Set; fallbackScale?: XScaleType; } { - const dataSeriesBySpecId = new Map(); + let globalDataSeries: DataSeries[] = []; const seriesCollection = new Map(); const mutatedXValueSums = new Map(); // the unique set of values along the x axis const globalXValues: Set = new Set(); + // the unique set of values along for the vertical small multiple grid + let globalSMVValues: Set = new Set(); + // the unique set of values along for the horizontal small multiple grid + let globalSMHValues: Set = new Set(); + let isNumberArray = true; let isOrdinalScale = false; + + const specsByYGroup = groupSeriesByYGroup(seriesSpecs); // eslint-disable-next-line no-restricted-syntax for (const spec of seriesSpecs) { // check scale type and cast to Ordinal if we found at least one series @@ -480,9 +494,18 @@ export function getDataSeriesBySpecId( isOrdinalScale = true; } - const { dataSeries, xValues } = splitSeriesDataByAccessors(spec, mutatedXValueSums, enableVislibSeriesSort); + const specGroup = specsByYGroup.get(spec.groupId); + const isStacked = Boolean(specGroup?.stacked.find(({ id }) => id === spec.id)); + const { dataSeries, xValues, smVValues, smHValues } = splitSeriesDataByAccessors( + spec, + mutatedXValueSums, + isStacked, + enableVislibSeriesSort, + specGroup?.stackMode, + smallMultiples, + ); - // filter deleselected dataseries + // filter deselected DataSeries let filteredDataSeries: DataSeries[] = [...dataSeries.values()]; if (deselectedDataSeries.length > 0) { filteredDataSeries = filteredDataSeries.filter( @@ -490,7 +513,7 @@ export function getDataSeriesBySpecId( ); } - dataSeriesBySpecId.set(spec.id, filteredDataSeries); + globalDataSeries = [...globalDataSeries, ...filteredDataSeries]; const banded = spec.y0Accessors && spec.y0Accessors.length > 0; @@ -513,6 +536,8 @@ export function getDataSeriesBySpecId( } globalXValues.add(xValue); } + globalSMVValues = new Set([...globalSMVValues, ...smVValues]); + globalSMHValues = new Set([...globalSMHValues, ...smHValues]); } const xValues = @@ -528,9 +553,12 @@ export function getDataSeriesBySpecId( ); return { - dataSeriesBySpecId, + dataSeries: globalDataSeries, seriesCollection, + // keep the user order for ordinal scales xValues, + smVValues: globalSMVValues, + smHValues: globalSMHValues, fallbackScale: !isOrdinalScale && !isNumberArray ? ScaleType.Ordinal : undefined, }; } diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index bbedb5e024..329146f700 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -459,6 +459,7 @@ export interface SeriesScales { } /** @public */ + export type BasicSeriesSpec = SeriesSpec & SeriesAccessors & SeriesScales & { diff --git a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts index e9b314ad96..aa8951d88f 100644 --- a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts @@ -70,18 +70,18 @@ describe('Stacked Series Utils', () => { store, ); const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const { stacked } = formattedDataSeries; - const [data0] = stacked[0].dataSeries[0].data; + + const [data0] = formattedDataSeries[0].data; expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY1).toBe(20); expect(data1.y0).toBe(0.1); expect(data1.y1).toBeCloseTo(0.3); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY1).toBe(70); expect(data2.y0).toBeCloseTo(0.3); expect(data2.y1).toBe(1); @@ -100,16 +100,14 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0); expect(data0.y1).toBe(0.25); - expect(stacked[0].dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -118,7 +116,7 @@ describe('Stacked Series Utils', () => { mark: null, }); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY1).toBe(30); expect(data2.y0).toBe(0.25); expect(data2.y1).toBe(1); @@ -138,23 +136,21 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY0).toBe(2); expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0.02); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY0).toBe(4); expect(data1.initialY1).toBe(20); expect(data1.y0).toBe(0.14); expect(data1.y1).toBeCloseTo(0.3, 5); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY0).toBe(6); expect(data2.initialY1).toBe(70); expect(data2.y0).toBeCloseTo(0.36); @@ -175,23 +171,21 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY0).toBe(2); expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0.02); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY0).toBe(null); expect(data1.initialY1).toBe(null); expect(data1.y0).toBe(0.1); expect(data1.y1).toBe(0.1); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY0).toBe(6); expect(data2.initialY1).toBe(90); expect(data2.y0).toBe(0.16); @@ -213,14 +207,12 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries.length).toBe(2); - expect(stacked[0].dataSeries[0].data.length).toBe(4); - expect(stacked[0].dataSeries[1].data.length).toBe(4); - expect(stacked[0].dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries).toHaveLength(2); + expect(formattedDataSeries[0].data).toHaveLength(4); + expect(formattedDataSeries[1].data).toHaveLength(4); + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 10, x: 1, @@ -228,7 +220,7 @@ describe('Stacked Series Utils', () => { y1: 0.1, mark: null, }); - expect(stacked[0].dataSeries[0].data[1]).toMatchObject({ + expect(formattedDataSeries[0].data[1]).toMatchObject({ initialY0: null, initialY1: 20, x: 2, @@ -236,7 +228,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[0].data[3]).toMatchObject({ + expect(formattedDataSeries[0].data[3]).toMatchObject({ initialY0: null, initialY1: 40, x: 4, @@ -244,7 +236,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 90, x: 1, @@ -252,7 +244,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[1].data[1]).toMatchObject({ + expect(formattedDataSeries[1].data[1]).toMatchObject({ initialY0: null, initialY1: null, x: 2, @@ -263,7 +255,7 @@ describe('Stacked Series Utils', () => { x: 2, }, }); - expect(stacked[0].dataSeries[1].data[2]).toMatchObject({ + expect(formattedDataSeries[1].data[2]).toMatchObject({ initialY0: null, initialY1: 30, x: 3, diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts index 3d672578f2..47b626f900 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts @@ -97,25 +97,19 @@ describe('Stacked Series Utils', () => { test('with empty values', () => { const store = MockStore.default(); MockStore.addSpecs(EMPTY_DATA_SET, store); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); - expect(stacked).toHaveLength(0); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toHaveLength(0); }); test('with basic values', () => { const store = MockStore.default(); MockStore.addSpecs(STANDARD_DATA_SET, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); const values = [ - dataSeries[0].data[0].y0, - dataSeries[0].data[0].y1, - dataSeries[1].data[0].y1, - dataSeries[2].data[0].y1, + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, ]; expect(values).toEqual([0, 10, 30, 60]); }); @@ -129,34 +123,26 @@ describe('Stacked Series Utils', () => { }), store, ); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); const values = [ - dataSeries[0].data[0].y0, - dataSeries[0].data[0].y1, - dataSeries[1].data[0].y1, - dataSeries[2].data[0].y1, + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, ]; expect(values).toEqual([0, 0.16666666666666666, 0.5, 1]); }); test('with null values', () => { const store = MockStore.default(); MockStore.addSpecs(WITH_NULL_DATASET, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); const values = [ - dataSeries[0].data[0].y0, - dataSeries[0].data[0].y1, - dataSeries[1].data[0].y1, - dataSeries[2].data[0].y1, + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, ]; expect(values).toEqual([0, 10, 10, 40]); }); @@ -169,17 +155,13 @@ describe('Stacked Series Utils', () => { }), store, ); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); const values = [ - dataSeries[0].data[0].y0, - dataSeries[0].data[0].y1, - dataSeries[1].data[0].y1, - dataSeries[2].data[0].y1, + formattedDataSeries[0].data[0].y0, + formattedDataSeries[0].data[0].y1, + formattedDataSeries[1].data[0].y1, + formattedDataSeries[2].data[0].y1, ]; expect(values).toEqual([0, 0.25, 0.25, 1]); }); @@ -188,13 +170,9 @@ describe('Stacked Series Utils', () => { test('format data without nulls', () => { const store = MockStore.default(); MockStore.addSpecs(STANDARD_DATA_SET, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 10, x: 0, @@ -202,7 +180,7 @@ describe('Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 20, x: 0, @@ -210,7 +188,7 @@ describe('Stacked Series Utils', () => { y1: 30, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: null, initialY1: 30, x: 0, @@ -222,13 +200,9 @@ describe('Stacked Series Utils', () => { test('format data with nulls', () => { const store = MockStore.default(); MockStore.addSpecs(WITH_NULL_DATASET, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -240,13 +214,9 @@ describe('Stacked Series Utils', () => { test('format data without nulls with y0 values', () => { const store = MockStore.default(); MockStore.addSpecs(STANDARD_DATA_SET_WY0, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: 2, initialY1: 10, x: 0, @@ -254,7 +224,7 @@ describe('Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: 4, initialY1: 20, x: 0, @@ -262,7 +232,7 @@ describe('Stacked Series Utils', () => { y1: 30, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: 6, initialY1: 30, x: 0, @@ -274,13 +244,9 @@ describe('Stacked Series Utils', () => { test('format data with nulls - missing points', () => { const store = MockStore.default(); MockStore.addSpecs(WITH_NULL_DATASET_WY0, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: 2, initialY1: 10, x: 0, @@ -288,7 +254,7 @@ describe('Stacked Series Utils', () => { y1: 10, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -296,7 +262,7 @@ describe('Stacked Series Utils', () => { y0: 10, mark: null, }); - expect(dataSeries[2].data[0]).toMatchObject({ + expect(formattedDataSeries[2].data[0]).toMatchObject({ initialY0: 6, initialY1: 30, x: 0, @@ -308,17 +274,13 @@ describe('Stacked Series Utils', () => { test('format data without nulls on second series', () => { const store = MockStore.default(); MockStore.addSpecs(DATA_SET_WITH_NULL_2, store); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries.length).toBe(2); - expect(dataSeries[0].data.length).toBe(4); - expect(dataSeries[1].data.length).toBe(4); + expect(formattedDataSeries).toHaveLength(2); + expect(formattedDataSeries[0].data).toHaveLength(4); + expect(formattedDataSeries[1].data).toHaveLength(4); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 1, x: 1, @@ -326,7 +288,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(dataSeries[0].data[1]).toMatchObject({ + expect(formattedDataSeries[0].data[1]).toMatchObject({ initialY0: null, initialY1: 2, x: 2, @@ -334,7 +296,7 @@ describe('Stacked Series Utils', () => { y1: 2, mark: null, }); - expect(dataSeries[0].data[3]).toMatchObject({ + expect(formattedDataSeries[0].data[3]).toMatchObject({ initialY0: null, initialY1: 4, x: 4, @@ -342,7 +304,7 @@ describe('Stacked Series Utils', () => { y1: 4, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 21, x: 1, @@ -350,7 +312,7 @@ describe('Stacked Series Utils', () => { y1: 22, mark: null, }); - expect(dataSeries[1].data[2]).toMatchObject({ + expect(formattedDataSeries[1].data[2]).toMatchObject({ initialY0: null, initialY1: 23, x: 3, @@ -375,13 +337,9 @@ describe('Stacked Series Utils', () => { }), store, ); - const { - formattedDataSeries: { - stacked: [{ dataSeries }], - }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 0, x: 1, @@ -389,7 +347,7 @@ describe('Stacked Series Utils', () => { y1: 0, mark: null, }); - expect(dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 0, x: 1, diff --git a/src/components/__snapshots__/chart.test.tsx.snap b/src/components/__snapshots__/chart.test.tsx.snap index 8cf358a601..47d294136b 100644 --- a/src/components/__snapshots__/chart.test.tsx.snap +++ b/src/components/__snapshots__/chart.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Chart should render the legend name test 1`] = `"
  • test
"`; +exports[`Chart should render the legend name test 1`] = `"
  • test
"`; diff --git a/src/mocks/annotations/annotations.ts b/src/mocks/annotations/annotations.ts new file mode 100644 index 0000000000..e0ae6596b9 --- /dev/null +++ b/src/mocks/annotations/annotations.ts @@ -0,0 +1,88 @@ +/* + * 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 { AnnotationLineProps } from '../../chart_types/xy_chart/annotations/line/types'; +import { AnnotationRectProps } from '../../chart_types/xy_chart/annotations/rect/types'; +import { mergePartial, RecursivePartial } from '../../utils/commons'; + +/** @internal */ +export class MockAnnotationLineProps { + private static readonly base: AnnotationLineProps = { + linePathPoints: { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + }, + panel: { top: 0, left: 0, width: 100, height: 100 }, + details: {}, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockAnnotationLineProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } + + static fromPoints(x1 = 0, y1 = 0, x2 = 0, y2 = 0): AnnotationLineProps { + return MockAnnotationLineProps.default({ + linePathPoints: { + x1, + y1, + x2, + y2, + }, + }); + } +} + +/** @internal */ +export class MockAnnotationRectProps { + private static readonly base: AnnotationRectProps = { + rect: { + x: 0, + y: 0, + width: 0, + height: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockAnnotationRectProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } + + static fromValues(x = 0, y = 0, width = 0, height = 0): AnnotationRectProps { + return MockAnnotationRectProps.default({ + rect: { + x, + y, + width, + height, + }, + }); + } +} diff --git a/src/mocks/geometries.ts b/src/mocks/geometries.ts index 4474e120a4..92df270a2a 100644 --- a/src/mocks/geometries.ts +++ b/src/mocks/geometries.ts @@ -43,9 +43,15 @@ export class MockPointGeometry { datum: { x: 0, y: 0 }, }, transform: { - x: 25, + x: 0, y: 0, }, + panel: { + width: 100, + height: 100, + left: 0, + top: 0, + }, }; static default(partial?: RecursivePartial) { @@ -53,7 +59,7 @@ export class MockPointGeometry { } static fromBaseline(baseline: RecursivePartial, omitKeys: string[] | string = []) { - return function(partial?: RecursivePartial) { + return (partial?: RecursivePartial) => { return omit( mergePartial(MockPointGeometry.base, partial, { mergeOptionalPartialValues: true }, [baseline]), omitKeys, @@ -69,14 +75,7 @@ export class MockBarGeometry { width: 0, height: 0, color, - displayValue: { - fontSize: 10, - text: '', - width: 0, - height: 0, - hideClippedValue: false, - isValueContainedInElement: false, - }, + displayValue: undefined, seriesIdentifier: MockSeriesIdentifier.default(), value: { accessor: 'y0', @@ -86,6 +85,16 @@ export class MockBarGeometry { datum: { x: 0, y: 0 }, }, seriesStyle: barSeriesStyle, + transform: { + x: 0, + y: 0, + }, + panel: { + width: 100, + height: 100, + left: 0, + top: 0, + }, }; static default(partial?: RecursivePartial) { @@ -93,7 +102,7 @@ export class MockBarGeometry { } static fromBaseline(baseline: RecursivePartial, omitKeys: string[] | string = []) { - return function(partial?: RecursivePartial) { + return (partial?: RecursivePartial) => { const geo = mergePartial(MockBarGeometry.base, partial, { mergeOptionalPartialValues: true }, [ baseline, ]); diff --git a/src/mocks/series/series.ts b/src/mocks/series/series.ts index 3351b38ea3..a7c3751547 100644 --- a/src/mocks/series/series.ts +++ b/src/mocks/series/series.ts @@ -26,8 +26,9 @@ import { XYChartSeriesIdentifier, FormattedDataSeries, } from '../../chart_types/xy_chart/utils/series'; -import { DEFAULT_GLOBAL_ID } from '../../specs'; +import { DEFAULT_GLOBAL_ID, SeriesTypes } from '../../specs'; import { mergePartial } from '../../utils/commons'; +import { MockSeriesSpec } from '../specs'; import { getRandomNumberGenerator } from '../utils'; import { fitFunctionData } from './data'; @@ -49,6 +50,11 @@ export class MockDataSeries { splitAccessors: new Map(), key: 'spec1', data: [], + groupId: 'group1', + seriesType: SeriesTypes.Bar, + stackMode: undefined, + spec: MockSeriesSpec.bar(), + isStacked: false, }; static default(partial?: Partial) { diff --git a/src/mocks/series/series_identifiers.ts b/src/mocks/series/series_identifiers.ts index f92688fd78..819b5e6338 100644 --- a/src/mocks/series/series_identifiers.ts +++ b/src/mocks/series/series_identifiers.ts @@ -19,10 +19,10 @@ import { SeriesCollectionValue, - getDataSeriesBySpecId, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, } from '../../chart_types/xy_chart/utils/series'; -import { BasicSeriesSpec } from '../../specs'; +import { BasicSeriesSpec, DEFAULT_SINGLE_PANEL_SM_VALUE } from '../../specs'; import { mergePartial } from '../../utils/commons'; type SeriesCollection = Map; @@ -34,7 +34,7 @@ export class MockSeriesCollection { } static fromSpecs(seriesSpecs: BasicSeriesSpec[]) { - const { seriesCollection } = getDataSeriesBySpecId(seriesSpecs, []); + const { seriesCollection } = getDataSeriesFromSpecs(seriesSpecs, []); return seriesCollection; } @@ -47,7 +47,9 @@ export class MockSeriesIdentifier { yAccessor: 'y', seriesKeys: ['a'], splitAccessors: new Map().set('g', 'a'), - key: 'spec{bars}yAccessor{y}splitAccessors{g-a}', + key: `spec{bars}yAccessor{y}splitAccessors{g-a}smV${DEFAULT_SINGLE_PANEL_SM_VALUE}smH${DEFAULT_SINGLE_PANEL_SM_VALUE}`, + smHorizontalAccessorValue: DEFAULT_SINGLE_PANEL_SM_VALUE, + smVerticalAccessorValue: DEFAULT_SINGLE_PANEL_SM_VALUE, }; static default(partial?: Partial) { @@ -57,8 +59,12 @@ export class MockSeriesIdentifier { } static fromSpecs(specs: BasicSeriesSpec[]): XYChartSeriesIdentifier[] { - const { seriesCollection } = getDataSeriesBySpecId(specs); + const { dataSeries } = getDataSeriesFromSpecs(specs); - return [...seriesCollection.values()].map(({ seriesIdentifier }) => seriesIdentifier); + return dataSeries.map(({ groupId, seriesType, data, isStacked, stackMode, spec, ...rest }) => rest); + } + + static fromSpec(specs: BasicSeriesSpec): XYChartSeriesIdentifier { + return MockSeriesIdentifier.fromSpecs([specs])[0]; } } diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index f30ebe6641..2159aca0d3 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -197,6 +197,12 @@ export class MockSeriesSpec { }); } + static bubble(partial?: Partial): BubbleSeriesSpec { + return mergePartial(MockSeriesSpec.bubbleBase, partial as RecursivePartial, { + mergeOptionalPartialValues: true, + }); + } + static sunburst(partial?: Partial): PartitionSpec { return mergePartial(MockSeriesSpec.sunburstBase, partial as RecursivePartial, { mergeOptionalPartialValues: true, @@ -272,7 +278,6 @@ export class MockGlobalSpec { showOverlappingTicks: false, showOverlappingLabels: false, position: Position.Left, - tickFormat: (tick: any) => `${tick}`, }; private static readonly settingsBaseNoMargings: SettingsSpec = { diff --git a/src/renderers/canvas/index.ts b/src/renderers/canvas/index.ts index 4f1c462027..58202c15c9 100644 --- a/src/renderers/canvas/index.ts +++ b/src/renderers/canvas/index.ts @@ -55,13 +55,13 @@ export function renderLayers(ctx: CanvasRenderingContext2D, layers: Array<(ctx: /** @internal */ export function withClip( ctx: CanvasRenderingContext2D, - clipppings: Rect, + clippings: Rect, fun: (ctx: CanvasRenderingContext2D) => void, shouldClip = true, ) { withContext(ctx, (ctx) => { if (shouldClip) { - const { x, y, width, height } = clipppings; + const { x, y, width, height } = clippings; ctx.beginPath(); ctx.rect(x, y, width, height); ctx.clip(); diff --git a/src/scales/scale_band.ts b/src/scales/scale_band.ts index d64dc33316..78bd3bb7e4 100644 --- a/src/scales/scale_band.ts +++ b/src/scales/scale_band.ts @@ -53,16 +53,24 @@ export class ScaleBand implements Scale { * A number between 0 and 1. * @defaultValue 0 */ - barsPadding = 0, + barsPadding: number | [number, number] = 0, ) { this.type = ScaleType.Ordinal; this.d3Scale = scaleBand>(); this.d3Scale.domain(domain); this.d3Scale.range(range); - const safeBarPadding = maxValueWithUpperLimit(barsPadding, 0, 1); - this.barsPadding = safeBarPadding; - this.d3Scale.paddingInner(safeBarPadding); - this.d3Scale.paddingOuter(safeBarPadding / 2); + let safeBarPadding = 0; + if (Array.isArray(barsPadding)) { + this.d3Scale.paddingInner(barsPadding[1]); + this.d3Scale.paddingOuter(barsPadding[0]); + this.barsPadding = barsPadding[1]; + } else { + safeBarPadding = maxValueWithUpperLimit(barsPadding, 0, 1); + this.d3Scale.paddingInner(safeBarPadding); + this.barsPadding = safeBarPadding; + this.d3Scale.paddingOuter(safeBarPadding / 2); + } + this.outerPadding = this.d3Scale.paddingOuter(); this.innerPadding = this.d3Scale.paddingInner(); this.bandwidth = this.d3Scale.bandwidth() || 0; diff --git a/src/specs/constants.ts b/src/specs/constants.ts index e4b691647c..c5e2333f6a 100644 --- a/src/specs/constants.ts +++ b/src/specs/constants.ts @@ -29,6 +29,8 @@ export const SpecTypes = Object.freeze({ Axis: 'axis' as const, Annotation: 'annotation' as const, Settings: 'settings' as const, + IndexOrder: 'index_order' as const, + SmallMultiples: 'small_multiples' as const, }); /** @public */ export type SpecTypes = $Values; diff --git a/src/specs/group_by.ts b/src/specs/group_by.ts new file mode 100644 index 0000000000..579851882a --- /dev/null +++ b/src/specs/group_by.ts @@ -0,0 +1,50 @@ +/* + * 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 React from 'react'; + +import { Spec } from '.'; +import { ChartTypes } from '../chart_types'; +import { Predicate } from '../chart_types/heatmap/utils/commons'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecTypes } from './constants'; + +/** @alpha */ +export type GroupByAccessor = (spec: Spec, datum: any) => string | number; +/** @alpha */ +export type GroupBySort = Predicate; + +/** @alpha */ +export interface GroupBySpec extends Spec { + by: GroupByAccessor; + sort: GroupBySort; +} +const DEFAULT_GROUP_BY_PROPS = { + chartType: ChartTypes.Global, + specType: SpecTypes.IndexOrder, +}; + +type DefaultGroupByProps = 'chartType' | 'specType'; + +/** @alpha */ +export type GroupByProps = Pick; + +/** @alpha */ +export const GroupBy: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_GROUP_BY_PROPS), +); diff --git a/src/specs/index.ts b/src/specs/index.ts index 057f1c7e5d..c1796ea319 100644 --- a/src/specs/index.ts +++ b/src/specs/index.ts @@ -28,6 +28,9 @@ export interface Spec { specType: string; } +export * from './group_by'; +export * from './small_multiples'; + export * from './settings'; export * from './constants'; export * from '../chart_types/specs'; diff --git a/src/specs/small_multiples.ts b/src/specs/small_multiples.ts new file mode 100644 index 0000000000..b3ef93770a --- /dev/null +++ b/src/specs/small_multiples.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 React from 'react'; + +import { Spec } from '.'; +import { ChartTypes } from '../chart_types'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecTypes } from './constants'; + +/** @internal */ +export const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + +/** @internal */ +export const DEFAULT_SM_PANEL_PADDING: [number, number] = [0, 0.1]; + +/** @alpha */ +export interface SmallMultiplesSpec extends Spec { + splitHorizontally?: string; + splitVertically?: string; + style?: { + verticalPanelPadding?: [number, number]; + horizontalPanelPadding?: [number, number]; + }; +} + +const DEFAULT_SMALL_MULTIPLES_PROPS = { + id: '__global__small_multiples___', + chartType: ChartTypes.Global, + specType: SpecTypes.SmallMultiples, +}; + +/** @alpha */ +export type SmallMultiplesProps = Partial>; + +/** @alpha */ +export const SmallMultiples: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_SMALL_MULTIPLES_PROPS), +); diff --git a/src/specs/specs_parser.tsx b/src/specs/specs_parser.tsx index 3b546734bd..fb33f1eba6 100644 --- a/src/specs/specs_parser.tsx +++ b/src/specs/specs_parser.tsx @@ -25,6 +25,7 @@ import { specParsed, specUnmounted } from '../state/actions/specs'; const SpecsParserComponent: React.FunctionComponent = (props) => { const injected = props as DispatchProps; + // clean all specs useEffect(() => { injected.specParsed(); }); diff --git a/src/utils/commons.ts b/src/utils/commons.ts index e82f32d59a..ba7f00392e 100644 --- a/src/utils/commons.ts +++ b/src/utils/commons.ts @@ -262,10 +262,15 @@ export function getAllKeys(object: any, objects: any[] = []): string[] { } /** @internal */ -export function isArrayOrSet(value: any): boolean { +export function isArrayOrSet(value: any): value is Array | Set { return Array.isArray(value) || value instanceof Set; } +/** @internal */ +export function isNil(value: any): value is null | undefined { + return value === null || value === undefined; +} + /** @internal */ export function hasPartialObjectToMerge( base: T, diff --git a/src/utils/dimensions.ts b/src/utils/dimensions.ts index db43736ea3..f621adcc12 100644 --- a/src/utils/dimensions.ts +++ b/src/utils/dimensions.ts @@ -24,6 +24,11 @@ export interface Dimensions { height: number; } +export interface Size { + width: number; + height: number; +} + // fixme consider switching from `number` to `Pixels` or similar, once nominal typing is added export interface PerSideDistance { top: number; diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index ed9ed1b5a0..46720c5dd6 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -21,6 +21,7 @@ import { $Values } from 'utility-types'; import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { Color } from './commons'; +import { Dimensions } from './dimensions'; import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, ArcStyle } from './themes/theme'; /** @@ -65,12 +66,24 @@ export interface PointGeometry { seriesIdentifier: XYChartSeriesIdentifier; value: GeometryValue; styleOverrides?: Partial; + panel: Dimensions; } + +export interface PerPanel { + panel: Dimensions; + value: T; +} + export interface BarGeometry { x: number; y: number; width: number; height: number; + transform: { + x: number; + y: number; + rotation?: number; + }; color: Color; displayValue?: { fontScale?: number; @@ -84,6 +97,7 @@ export interface BarGeometry { seriesIdentifier: XYChartSeriesIdentifier; value: GeometryValue; seriesStyle: BarSeriesStyle; + panel: Dimensions; } export interface LineGeometry { diff --git a/stories/axes/8_custom_domain.tsx b/stories/axes/8_custom_domain.tsx index c254698e60..3fe1134241 100644 --- a/stories/axes/8_custom_domain.tsx +++ b/stories/axes/8_custom_domain.tsx @@ -91,7 +91,6 @@ export const Example = () => { xAccessor="x" yAccessors={['y']} stackAccessors={['x']} - splitSeriesAccessors={['g']} data={[ { x: 0, y: 3 }, { x: 1, y: 2 }, diff --git a/stories/small_multiples/1_grid.tsx b/stories/small_multiples/1_grid.tsx new file mode 100644 index 0000000000..c371152cea --- /dev/null +++ b/stories/small_multiples/1_grid.tsx @@ -0,0 +1,232 @@ +/* + * 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 { boolean } from '@storybook/addon-knobs'; +import React, { useState } from 'react'; + +import { + ScaleType, + Position, + Chart, + Axis, + LineSeries, + GroupBy, + SmallMultiples, + Settings, + BarSeries, + AreaSeries, + Fit, + LineAnnotation, + BubbleSeries, + AnnotationDomainTypes, + Rotation, + RectAnnotation, +} from '../../src'; +import { getRandomNumberGenerator, SeededDataGenerator } from '../../src/mocks/utils'; +import { SB_SOURCE_PANEL } from '../utils/storybook'; + +const getRandomNumber = getRandomNumberGenerator(); +const dg = new SeededDataGenerator(); + +const data1 = dg.generateGroupedSeries(10, 3); +const data2 = dg.generateGroupedSeries(10, 3).map((d) => { + return getRandomNumber() > 0.95 ? { ...d, y: null } : d; +}); +const data3 = dg.generateGroupedSeries(10, 3).map((d) => { + return getRandomNumber() > 0.95 ? { ...d, y: null } : d; +}); + +export const Example = () => { + const splitVertically = boolean('vertical split', true); + const splitHorizontally = boolean('horizontal split', true); + const [rotationIndex, setRotationIndex] = useState(0); + const rot: Rotation = ([0, -90, 90, 0] as Rotation[])[rotationIndex]; + const showLegend = boolean('Show Legend', false); + return ( + <> + g + + + + + d.toFixed(2)} + /> + + { + return id; + }} + sort="alphaAsc" + /> + { + return g; + }} + sort="alphaAsc" + /> + + + + } + style={{ + line: { + stroke: 'red', + strokeWidth: 2, + opacity: 0.8, + }, + }} + zIndex={-10} + /> + + } + style={{ + line: { + stroke: 'blue', + strokeWidth: 5, + opacity: 0.8, + }, + }} + zIndex={-10} + /> + + + + + + + + ); +}; +Example.story = { + parameters: { + options: { selectedPanel: SB_SOURCE_PANEL }, + info: { + text: `If your data is in UTC timezone, your tooltip and axis labels can be configured + to visualize the time translated to your local timezone. You should be able to see the + first value on \`2019-01-01 01:00:00.000 \``, + }, + }, +}; diff --git a/stories/small_multiples/2_vertical_areas.tsx b/stories/small_multiples/2_vertical_areas.tsx new file mode 100644 index 0000000000..7aedf425d1 --- /dev/null +++ b/stories/small_multiples/2_vertical_areas.tsx @@ -0,0 +1,105 @@ +/* + * 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 { boolean } from '@storybook/addon-knobs'; +import { DateTime } from 'luxon'; +import React from 'react'; + +import { + ScaleType, + Position, + Chart, + Axis, + GroupBy, + SmallMultiples, + Settings, + AreaSeries, + LIGHT_THEME, + niceTimeFormatByDay, + timeFormatter, +} from '../../src'; +import { SeededDataGenerator } from '../../src/mocks/utils'; +import { SB_SOURCE_PANEL } from '../utils/storybook'; + +const dg = new SeededDataGenerator(); +const numOfDays = 60; +const data = dg.generateGroupedSeries(numOfDays, 6, 'metric ').map((d) => { + return { + ...d, + x: DateTime.fromISO('2020-01-01T00:00:00Z') + .plus({ days: d.x }) + .toMillis(), + }; +}); + +export const Example = () => { + const showLegend = boolean('Show Legend', false); + return ( + + + + d.toFixed(2)} + /> + + { + return g; + }} + sort="alphaDesc" + /> + + + + ); +}; +Example.story = { + parameters: { + options: { selectedPanel: SB_SOURCE_PANEL }, + info: { + text: `The above chart shows an example of small multiples technique that splits our dataset into multiple + sub-series vertically positioned one below the other. + The configuration is obtained by defining a \`\` operation component that define the property used to + divide/group my dataset(via to the \`by\` props) and using the specified \`id\` of that operation inside the + \`\` component. + +Each charts has the same vertical and horizontal axis scale. +`, + }, + }, +}; diff --git a/stories/small_multiples/3_grid_lines.tsx b/stories/small_multiples/3_grid_lines.tsx new file mode 100644 index 0000000000..5ef9316ba0 --- /dev/null +++ b/stories/small_multiples/3_grid_lines.tsx @@ -0,0 +1,163 @@ +/* + * 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 { boolean } from '@storybook/addon-knobs'; +import { DateTime } from 'luxon'; +import React from 'react'; + +import { + ScaleType, + Position, + Chart, + Axis, + LineSeries, + GroupBy, + SmallMultiples, + Settings, + LIGHT_THEME, + niceTimeFormatByDay, + timeFormatter, +} from '../../src'; +import { SeededDataGenerator } from '../../src/mocks/utils'; +import { SB_SOURCE_PANEL } from '../utils/storybook'; + +const dg = new SeededDataGenerator(); +const numOfDays = 90; +const groupNames = new Array(16).fill(0).map((d, i) => String.fromCharCode(97 + i)); +const data = dg.generateGroupedSeries(numOfDays, 16).map((d) => { + return { + y: d.y, + x: DateTime.fromISO('2020-01-01T00:00:00Z') + .plus({ days: d.x }) + .toMillis(), + g: d.g, + h: `host ${groupNames.indexOf(d.g) % 4}`, + v: `metric ${Math.floor(groupNames.indexOf(d.g) / 4)}`, + }; +}); + +export const Example = () => { + const showLegend = boolean('Show Legend', false); + return ( + + + + d.toFixed(2)} + /> + + { + return v; + }} + sort="numDesc" + /> + { + return h; + }} + sort="numAsc" + /> + + + { + const val = Number(`${smHorizontalAccessorValue}`.split('host ')[1]); + return LIGHT_THEME.colors.vizColors[val]; + }} + data={data} + /> + + ); +}; +Example.story = { + parameters: { + options: { selectedPanel: SB_SOURCE_PANEL }, + info: { + text: `It is possible to add either a vertical and horizontal \`\` operations to create a grid of +small multiples. +The assignment of the series colors can be handled by defining an accessor in the \`color\` prop of the series that +consider the \`smHorizontalAccessorValue\` or \`smVerticalAccessorValue\` values when returning the assigned color. +`, + }, + }, +}; diff --git a/stories/small_multiples/4_horizontal_bars.tsx b/stories/small_multiples/4_horizontal_bars.tsx new file mode 100644 index 0000000000..40d297e40f --- /dev/null +++ b/stories/small_multiples/4_horizontal_bars.tsx @@ -0,0 +1,159 @@ +/* + * 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 { boolean } from '@storybook/addon-knobs'; +import { DateTime } from 'luxon'; +import React from 'react'; + +import { + ScaleType, + Position, + Chart, + Axis, + GroupBy, + SmallMultiples, + Settings, + BarSeries, + LineAnnotation, + AnnotationDomainTypes, + LIGHT_THEME, +} from '../../src'; +import { SeededDataGenerator } from '../../src/mocks/utils'; +import { SB_SOURCE_PANEL } from '../utils/storybook'; + +const dg = new SeededDataGenerator(); +const numOfDays = 7; +function generateData() { + return dg.generateGroupedSeries(numOfDays, 2).map((d) => { + return { + ...d, + x: DateTime.fromFormat(`${d.x + 1}`, 'E').toFormat('EEEE'), + y: Math.floor(d.y * 10), + g: d.g === 'a' ? 'new user' : 'existing user', + }; + }); +} +const data1 = generateData(); +const data2 = generateData(); +const data3 = generateData(); + +export const Example = () => { + const marker = ( + + MIN + + ); + const showLegend = boolean('Show Legend', false); + + return ( + + + + + + { + return spec.id; + }} + sort="alphaAsc" + /> + + + + + + + ); +}; +Example.story = { + parameters: { + options: { selectedPanel: SB_SOURCE_PANEL }, + info: { + text: `Similarly to the Vertical Areas example, the above chart shows an example of small multiples technique +that splits our dataset into multiple sub-series horizontally positioned one aside the other. +In this case, the \`\` id is used to specify the horizontal split via the \`splitHorizontally\` prop. + +As for single charts, we can merge and handle multiple data-series together and specify a \`by\` accessor to consider +the specific case. An additional property \`sort\` is available to configure the sorting order of the vertical or +horizontal split. +`, + }, + }, +}; diff --git a/stories/small_multiples/small_multiples.stories.tsx b/stories/small_multiples/small_multiples.stories.tsx new file mode 100644 index 0000000000..de4b73c45f --- /dev/null +++ b/stories/small_multiples/small_multiples.stories.tsx @@ -0,0 +1,31 @@ +/* + * 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 { SB_KNOBS_PANEL } from '../utils/storybook'; + +export default { + title: 'Small Multiples (@alpha)', + parameters: { + options: { selectedPanel: SB_KNOBS_PANEL }, + }, +}; + +export { Example as verticalAreas } from './2_vertical_areas'; +export { Example as horizontalBars } from './4_horizontal_bars'; +export { Example as gridLines } from './3_grid_lines'; diff --git a/yarn.lock b/yarn.lock index 28e8ce9416..dc4d2422fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5520,9 +5520,9 @@ integrity sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ== "@types/d3-array@^1.2.6": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.7.tgz#34dc654d34fc058c41c31dbca1ed68071a8fcc17" - integrity sha512-51vHWuUyDOi+8XuwPrTw3cFqyh2Slg9y8COYkRfjCPG9TfYqY0hoNPzv/8BrcAy0FeQBzqEo/D/8Nk2caOQJnA== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.8.tgz#b852381cb68e31e46bfa23ee70a383cbc6d62146" + integrity sha512-wWV0wT6oLUGprrOR5LMK7Dh8EBiondhnqINsvazv6UucYfTdb2oaFF4knlqzZV2RKB9ZC9G7G1Iojt8b/wolsw== "@types/d3-collection@^1.0.8": version "1.0.8"