diff --git a/CHANGELOG.md b/CHANGELOG.md index aa548fe9ac..41f19f196d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [28.2.0](https://github.com/elastic/elastic-charts/compare/v28.1.0...v28.2.0) (2021-04-15) + + +### Bug Fixes + +* **xy:** consider `useDefaultGroupDomain` on scale config ([#1119](https://github.com/elastic/elastic-charts/issues/1119)) ([c1b59f2](https://github.com/elastic/elastic-charts/commit/c1b59f249a1fdfc5d6f714de8db99cbf7a16c6eb)), closes [#1087](https://github.com/elastic/elastic-charts/issues/1087) + + +### Features + +* **a11y:** allow user to pass custom description for screen readers ([#1111](https://github.com/elastic/elastic-charts/issues/1111)) ([2ee1b91](https://github.com/elastic/elastic-charts/commit/2ee1b912f58cff4964786ce6586b07390bbed0b3)), closes [#1097](https://github.com/elastic/elastic-charts/issues/1097) +* **partition:** add debuggable state ([#1117](https://github.com/elastic/elastic-charts/issues/1117)) ([d7fc206](https://github.com/elastic/elastic-charts/commit/d7fc2068ca5febba06e25cf67b91cf9d203bc5d3)), closes [#917](https://github.com/elastic/elastic-charts/issues/917) + # [28.1.0](https://github.com/elastic/elastic-charts/compare/v28.0.1...v28.1.0) (2021-04-13) diff --git a/api/charts.api.md b/api/charts.api.md index 41861df03c..5cb49752ec 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -582,6 +582,10 @@ export interface DebugState { // // (undocumented) lines?: DebugStateLine[]; + // Warning: (ae-forgotten-export) The symbol "PartitionDebugState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + partition?: PartitionDebugState[]; } // @public (undocumented) diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-use-default-group-domain-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-use-default-group-domain-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..ffeffd75c0 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-use-default-group-domain-visually-looks-correct-1-snap.png differ diff --git a/package.json b/package.json index 2c917f354e..edf24a8521 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elastic/charts", "description": "Elastic-Charts data visualization library", - "version": "28.1.0", + "version": "28.2.0", "author": "Marco Vettorello ", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/src/chart_types/partition_chart/state/chart_state.tsx b/src/chart_types/partition_chart/state/chart_state.tsx index 5b07f9d404..b9a39b0578 100644 --- a/src/chart_types/partition_chart/state/chart_state.tsx +++ b/src/chart_types/partition_chart/state/chart_state.tsx @@ -27,6 +27,7 @@ import { DebugState } from '../../../state/types'; import { Dimensions } from '../../../utils/dimensions'; import { render } from '../renderer/dom/layered_partition_chart'; import { computeLegendSelector } from './selectors/compute_legend'; +import { getDebugStateSelector } from './selectors/get_debug_state'; import { getLegendItemsExtra } from './selectors/get_legend_items_extra'; import { getLegendItemsLabels } from './selectors/get_legend_items_labels'; import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; @@ -130,8 +131,7 @@ export class PartitionState implements InternalChartState { return null; } - // TODO - getDebugState(): DebugState { - return {}; + getDebugState(state: GlobalChartState): DebugState { + return getDebugStateSelector(state); } } diff --git a/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts b/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts new file mode 100644 index 0000000000..712f52fd06 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { + HeatmapElementEvent, + LayerValue, + PartitionElementEvent, + XYChartElementEvent, +} from '../../../../specs/settings'; +import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { DebugState, PartitionDebugState, SinglePartitionDebugState } from '../../../../state/types'; +import { PartitionLayout } from '../../layout/types/config_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { getDebugStateSelector } from './get_debug_state'; +import { createOnElementClickCaller } from './on_element_click_caller'; + +describe.each([ + [PartitionLayout.sunburst, 9, 9], + [PartitionLayout.treemap, 9, 6], + [PartitionLayout.flame, 9, 6], + [PartitionLayout.icicle, 9, 6], + [PartitionLayout.mosaic, 9, 6], +])('Partition - debug state %s', (partitionLayout, numberOfElements, numberOfCalls) => { + type TestDatum = { cat1: string; cat2: string; val: number }; + const specJSON = { + config: { + partitionLayout, + }, + data: [ + { cat1: 'Asia', cat2: 'Japan', val: 1 }, + { cat1: 'Asia', cat2: 'China', val: 1 }, + { cat1: 'Europe', cat2: 'Germany', val: 1 }, + { cat1: 'Europe', cat2: 'Italy', val: 1 }, + { cat1: 'North America', cat2: 'United States', val: 1 }, + { cat1: 'North America', cat2: 'Canada', val: 1 }, + ], + valueAccessor: (d: TestDatum) => d.val, + layers: [ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ], + }; + let store: Store; + let onClickListener: jest.Mock< + undefined, + Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]> + >; + let debugState: DebugState; + + beforeEach(() => { + onClickListener = jest.fn((): undefined => undefined); + store = MockStore.default({ width: 500, height: 500, top: 0, left: 0 }); + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + MockStore.addSpecs( + [ + MockSeriesSpec.sunburst(specJSON), + MockGlobalSpec.settings({ debugState: true, onElementClick: onClickListener }), + ], + store, + ); + debugState = getDebugStateSelector(store.getState()); + }); + + it('can compute debug state', () => { + // small multiple panels + expect(debugState.partition).toHaveLength(1); + // partition sectors + expect(debugState.partition![0].partitions).toHaveLength(numberOfElements); + }); + + it('can click on every sector', () => { + const [{ partitions }] = debugState.partition as PartitionDebugState[]; + let counter = 0; + for (let index = 0; index < partitions.length; index++) { + const partition = partitions[index]; + if (!isSunburst(partitionLayout) && partition.depth < 2) { + continue; + } + expectCorrectClickInfo(store, onClickListener, partition, counter); + counter++; + } + expect(onClickListener).toBeCalledTimes(numberOfCalls); + }); +}); + +function expectCorrectClickInfo( + store: Store, + onClickListener: jest.Mock>, + partition: SinglePartitionDebugState, + index: number, +) { + const { + depth, + value, + name, + coords: [x, y], + } = partition; + + store.dispatch(onPointerMove({ x, y }, index * 3)); + store.dispatch(onMouseDown({ x, y }, index * 3 + 1)); + store.dispatch(onMouseUp({ x, y }, index * 3 + 2)); + + expect(onClickListener).toBeCalledTimes(index + 1); + const obj = onClickListener.mock.calls[index][0][0][0] as LayerValue[]; + // pick the last element of the path + expect(obj[obj.length - 1]).toMatchObject({ + depth, + groupByRollup: name, + value, + }); +} diff --git a/src/chart_types/partition_chart/state/selectors/get_debug_state.ts b/src/chart_types/partition_chart/state/selectors/get_debug_state.ts new file mode 100644 index 0000000000..21d51190d1 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/get_debug_state.ts @@ -0,0 +1,72 @@ +/* + * 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 { TAU } from '../../../../common/constants'; +import { Pixels, PointObject } from '../../../../common/geometry'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DebugState, PartitionDebugState } from '../../../../state/types'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { partitionMultiGeometries } from './geometries'; + +/** @internal */ +export const getDebugStateSelector = createCachedSelector( + [partitionMultiGeometries], + (geoms): DebugState => { + return { + partition: geoms.reduce((acc, { panelTitle, config, quadViewModel, diskCenter }) => { + const partitions: PartitionDebugState['partitions'] = quadViewModel.map((model) => { + const { dataName, depth, fillColor, value } = model; + return { + name: dataName, + depth, + color: fillColor, + value, + coords: isSunburst(config.partitionLayout) + ? getCoordsForSector(model, diskCenter) + : getCoordsForRectangle(model, diskCenter), + }; + }); + acc.push({ + panelTitle, + partitions, + }); + return acc; + }, []), + }; + }, +)(getChartIdSelector); + +function getCoordsForSector({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + const cr = y0px + (y1px - y0px) / 2; + const angle = X0 + (X1 - X0) / 2; + const x = Math.round(Math.cos(angle) * cr + diskCenter.x); + const y = Math.round(Math.sin(angle) * cr + diskCenter.y); + return [x, y]; +} + +function getCoordsForRectangle({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const y = Math.round(y0px + (y1px - y0px) / 2 + diskCenter.y); + const x = Math.round(x0 + (x1 - x0) / 2 + diskCenter.x); + return [x, y]; +} diff --git a/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts new file mode 100644 index 0000000000..60c532d8ae --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { DEFAULT_GLOBAL_ID } from '../../utils/specs'; +import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs'; + +describe('GroupIds and useDefaultGroupId', () => { + it('use the specified useDefaultGroupId to compute scale configs', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + groupId: 'other', + useDefaultGroupDomain: 'a different one', + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(scaleConfigs.y['a different one']).toBeDefined(); + }); + + it('have 2 different y domains with 2 groups', () => { + const store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ id: 'one' }), + MockSeriesSpec.bar({ + id: 'two', + groupId: 'other', + useDefaultGroupDomain: 'a different one', + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(Object.keys(scaleConfigs.y)).toHaveLength(2); + expect(scaleConfigs.y['a different one']).toBeDefined(); + expect(scaleConfigs.y[DEFAULT_GLOBAL_ID]).toBeDefined(); + }); + + it('have 2 different y domains with 3 groups', () => { + const store = MockStore.default({ width: 120, height: 100, left: 0, top: 0 }); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins(), + MockSeriesSpec.bar({ id: 'one', data: [{ x: 1, y: 10 }] }), + MockSeriesSpec.bar({ + id: 'two', + groupId: 'other', + useDefaultGroupDomain: 'a different one', + data: [{ x: 1, y: 10 }], + }), + MockSeriesSpec.bar({ + id: 'three', + groupId: 'another again', + useDefaultGroupDomain: 'a different one', + data: [{ x: 1, y: 10 }], + }), + ], + store, + ); + const scaleConfigs = getScaleConfigsFromSpecsSelector(store.getState()); + expect(Object.keys(scaleConfigs.y)).toHaveLength(2); + expect(scaleConfigs.y['a different one']).toBeDefined(); + expect(scaleConfigs.y[DEFAULT_GLOBAL_ID]).toBeDefined(); + + const geoms = computeSeriesGeometriesSelector(store.getState()); + const { bars } = geoms.geometries; + expect(bars).toHaveLength(3); + expect(bars[0].value[0].width).toBe(40); + expect(bars[1].value[0].width).toBe(40); + expect(bars[2].value[0].width).toBe(40); + }); +}); diff --git a/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts index c66aa1077d..ecec96bd0e 100644 --- a/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts +++ b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts @@ -33,6 +33,7 @@ import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; import { groupBy } from '../../utils/group_data_series'; import { AxisSpec, BasicSeriesSpec, CustomXDomain, XScaleType, YDomainRange } from '../../utils/specs'; import { isHorizontalRotation } from '../utils/common'; +import { getSpecDomainGroupId } from '../utils/spec'; import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; import { mergeYCustomDomainsByGroupId } from './merge_y_custom_domains'; @@ -83,14 +84,15 @@ export function getScaleConfigsFromSpecs( }; // y axes - const scaleConfigsByGroupId = groupBy(seriesSpecs, ['groupId'], true).reduce< + const scaleConfigsByGroupId = groupBy(seriesSpecs, getSpecDomainGroupId, true).reduce< Record >((acc, series) => { const yScaleTypes = series.map(({ yScaleType, yNice }) => ({ nice: getYNiceFromSpec(yNice), type: getYScaleTypeFromSpec(yScaleType), })); - acc[series[0].groupId] = coerceYScaleTypes(yScaleTypes); + const groupId = getSpecDomainGroupId(series[0]); + acc[groupId] = coerceYScaleTypes(yScaleTypes); return acc; }, {}); diff --git a/src/components/chart_status.tsx b/src/components/chart_status.tsx index e68d88573a..6fa00baff7 100644 --- a/src/components/chart_status.tsx +++ b/src/components/chart_status.tsx @@ -66,13 +66,13 @@ class ChartStatusComponent extends React.Component { } const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => { - const settings = getSettingsSpecSelector(state); + const { onRenderChange, debugState } = getSettingsSpecSelector(state); return { rendered: state.chartRendered, renderedCount: state.chartRenderedCount, - onRenderChange: settings.onRenderChange, - debugState: settings.debugState ? getDebugStateSelector(state) : null, + onRenderChange, + debugState: debugState ? getDebugStateSelector(state) : null, }; }; diff --git a/src/state/types.ts b/src/state/types.ts index c38612512b..acc01f92a2 100644 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -18,6 +18,7 @@ */ import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; +import { Pixels } from '../common/geometry'; import type { Position } from '../utils/common'; import type { GeometryValue } from '../utils/geometry'; @@ -97,6 +98,21 @@ type HeatmapDebugState = { }; }; +/** @public */ +export type SinglePartitionDebugState = { + name: string; + depth: number; + color: string; + value: number; + coords: [Pixels, Pixels]; +}; + +/** @public */ +export type PartitionDebugState = { + panelTitle: string; + partitions: Array; +}; + /** * Describes _visible_ chart state for use in functional tests * @@ -111,4 +127,5 @@ export interface DebugState { bars?: DebugStateBar[]; /** Heatmap chart debug state */ heatmap?: HeatmapDebugState; + partition?: PartitionDebugState[]; } diff --git a/stories/bar/56_test_use_dfl_gdomain.tsx b/stories/bar/56_test_use_dfl_gdomain.tsx new file mode 100644 index 0000000000..0c308bc77d --- /dev/null +++ b/stories/bar/56_test_use_dfl_gdomain.tsx @@ -0,0 +1,52 @@ +/* + * 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 { Axis, BarSeries, Chart, Position, ScaleType } from '../../src'; + +export const Example = () => { + return ( + + + + + + + ); +}; diff --git a/stories/bar/bars.stories.tsx b/stories/bar/bars.stories.tsx index c492126d2d..b79a058670 100644 --- a/stories/bar/bars.stories.tsx +++ b/stories/bar/bars.stories.tsx @@ -85,3 +85,4 @@ export { Example as testMinHeightPositiveAndNegativeValues } from './46_test_min export { Example as testTooltipAndRotation } from './48_test_tooltip'; export { Example as tooltipBoundary } from './55_tooltip_boundary'; export { Example as testDualYAxis } from './49_test_dual_axis'; +export { Example as testUseDefaultGroupDomain } from './56_test_use_dfl_gdomain'; diff --git a/stories/mosaic/10_mosaic_simple.tsx b/stories/mosaic/10_mosaic_simple.tsx index 8a0bf240d7..af3289c283 100644 --- a/stories/mosaic/10_mosaic_simple.tsx +++ b/stories/mosaic/10_mosaic_simple.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { boolean } from '@storybook/addon-knobs'; +import { boolean, radios } from '@storybook/addon-knobs'; import React from 'react'; import { @@ -62,6 +62,15 @@ const productToColor = new Map( ); export const Example = () => { + const partitionLayout = radios( + 'Partition layout', + { + [PartitionLayout.mosaic]: PartitionLayout.mosaic, + [PartitionLayout.treemap]: PartitionLayout.treemap, + [PartitionLayout.sunburst]: PartitionLayout.sunburst, + }, + PartitionLayout.mosaic, + ); return ( @@ -78,7 +87,7 @@ export const Example = () => { fontWeight: 400, }, shape: { - fillColor: () => 'white', + fillColor: partitionLayout === PartitionLayout.sunburst ? 'lightgrey' : 'white', }, }, { @@ -103,7 +112,9 @@ export const Example = () => { }, ]} config={{ - partitionLayout: PartitionLayout.mosaic, + partitionLayout, + linkLabel: { maxCount: 0 }, // relevant for sunburst only + outerSizeRatio: 0.9, // relevant for sunburst only }} /> diff --git a/stories/sunburst/10_2_slice.tsx b/stories/sunburst/10_2_slice.tsx index e8a75d8781..1faf69f682 100644 --- a/stories/sunburst/10_2_slice.tsx +++ b/stories/sunburst/10_2_slice.tsx @@ -19,13 +19,14 @@ import React from 'react'; -import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; import { config } from '../../src/chart_types/partition_chart/layout/config'; import { mocks } from '../../src/mocks/hierarchical'; import { indexInterpolatedFillColor, interpolatorCET2s, productLookup } from '../utils/utils'; export const Example = () => ( + (d: any, i: number, a: any[]) => c export const Example = () => ( - +