diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md index e85da2828844..c411d71d461e 100644 --- a/packages/osd-charts/api/charts.api.md +++ b/packages/osd-charts/api/charts.api.md @@ -817,11 +817,9 @@ export interface GroupBrushExtent { // @alpha (undocumented) export const GroupBy: React_2.FunctionComponent; -// @alpha (undocumented) +// @public (undocumented) export type GroupByAccessor = (spec: Spec, datum: any) => string | number; -// Warning: (ae-incompatible-release-tags) The symbol "GroupByFormatter" is marked as @public, but its signature references "GroupByAccessor" which is marked as @alpha -// // @public export type GroupByFormatter = (value: ReturnType) => string; @@ -1073,6 +1071,7 @@ export interface LayerValue { depth: number; groupByRollup: PrimitiveValue; path: LegendPath; + smAccessorValue: ReturnType; sortIndex: number; value: number; } diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts index fb9e75252cc6..1ea06d9dc919 100644 --- a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts @@ -48,6 +48,7 @@ export const getPickedShapesLayerValues = createCachedSelector( const elements = pickedShapes.map>((model) => { const values: Array = []; values.push({ + smAccessorValue: '', groupByRollup: 'Actual', value: model.actual, sortIndex: 0, diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts index d4362f87022a..b1093afc5f59 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -29,6 +29,7 @@ import { SizeRatio, } from '../../../../common/geometry'; import { Font, VerticalAlignments } from '../../../../common/text_utils'; +import { GroupByAccessor } from '../../../../specs'; import { LegendPath } from '../../../../state/actions/legend'; import { Color } from '../../../../utils/common'; import { ContinuousDomainFocus } from '../../renderer/canvas/partition'; @@ -89,13 +90,14 @@ export interface RowSet { } /** @internal */ -export interface SmallMultiplesIndices { +export interface SmallMultiplesDescriptors { + smAccessorValue: ReturnType; index: number; innerIndex: number; } /** @internal */ -export interface QuadViewModel extends ShapeTreeNode, SmallMultiplesIndices { +export interface QuadViewModel extends ShapeTreeNode, SmallMultiplesDescriptors { strokeWidth: number; strokeStyle: string; fillColor: string; @@ -111,8 +113,9 @@ export interface OutsideLinksViewModel { export type PickFunction = (x: Pixels, y: Pixels, focus: ContinuousDomainFocus) => Array; /** @internal */ -export interface PartitionSmallMultiplesModel extends SmallMultiplesIndices { +export interface PartitionSmallMultiplesModel extends SmallMultiplesDescriptors { panelTitle: string; + smAccessorValue: number | string; partitionLayout: PartitionLayout; top: SizeRatio; left: SizeRatio; @@ -154,6 +157,7 @@ const defaultFont: Font = { export const nullPartitionSmallMultiplesModel = (partitionLayout: PartitionLayout): PartitionSmallMultiplesModel => ({ index: 0, innerIndex: 0, + smAccessorValue: '', panelTitle: '', top: 0, left: 0, diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts index 8c80c3e1e0d9..dae532926cd6 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/picked_shapes.ts @@ -38,19 +38,22 @@ export function pickShapesLayerValues(shapes: QuadViewModel[]): LayerValue[][] { return shapes .filter(({ depth }) => depth === maxDepth) // eg. lowest layer in a treemap, where layers overlap in screen space; doesn't apply to sunburst/flame .map>((viewModel) => { - const values: Array = []; - values.push({ - groupByRollup: viewModel.dataName, - value: viewModel[AGGREGATE_KEY], - depth: viewModel[DEPTH_KEY], - sortIndex: viewModel[SORT_INDEX_KEY], - path: viewModel[PATH_KEY], - }); + const values: Array = [ + { + smAccessorValue: viewModel.smAccessorValue, + groupByRollup: viewModel.dataName, + value: viewModel[AGGREGATE_KEY], + depth: viewModel[DEPTH_KEY], + sortIndex: viewModel[SORT_INDEX_KEY], + path: viewModel[PATH_KEY], + }, + ]; let node = viewModel[MODEL_KEY]; while (node[DEPTH_KEY] > 0) { const value = node[AGGREGATE_KEY]; const dataName = getNodeName(node); values.push({ + smAccessorValue: viewModel.smAccessorValue, groupByRollup: dataName, value, depth: node[DEPTH_KEY], diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts index 0b02a9fcfbc9..bb0d58213958 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts @@ -29,7 +29,7 @@ import { trueBearingToStandardPositionAngle, } from '../../../../common/geometry'; import { Part, TextMeasure } from '../../../../common/text_utils'; -import { SmallMultiplesStyle } from '../../../../specs'; +import { GroupByAccessor, SmallMultiplesStyle } from '../../../../specs'; import { StrokeStyle, ValueFormatter, Color, RecursivePartial } from '../../../../utils/common'; import { Layer } from '../../specs'; import { config as defaultConfig, MODEL_KEY, percentValueGetter } from '../config'; @@ -137,6 +137,7 @@ export function makeQuadViewModel( layers: Layer[], sectorLineWidth: Pixels, sectorLineStroke: StrokeStyle, + smAccessorValue: ReturnType, index: number, innerIndex: number, fillLabel: FillLabelConfig, @@ -159,7 +160,7 @@ export function makeQuadViewModel( !isSunburstLayout && textNegligible ? 'transparent' : fillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor); - return { index, innerIndex, strokeWidth, strokeStyle, fillColor, textColor: color, ...node }; + return { index, innerIndex, smAccessorValue, strokeWidth, strokeStyle, fillColor, textColor: color, ...node }; }); } @@ -360,6 +361,7 @@ export function shapeViewModel( layers, config.sectorLineWidth, config.sectorLineStroke, + panel.smAccessorValue, panel.index, panel.innerIndex, config.fillLabel, @@ -463,7 +465,7 @@ export function shapeViewModel( // combined viewModel return { partitionLayout: config?.partitionLayout ?? defaultConfig.partitionLayout, - + smAccessorValue: panel.smAccessorValue, panelTitle: panel.panelTitle, index: panel.index, innerIndex: panel.innerIndex, diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx index 588bd9234ecc..f993240c3b24 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx @@ -33,7 +33,7 @@ import { nullShapeViewModel, QuadViewModel, ShapeViewModel, - SmallMultiplesIndices, + SmallMultiplesDescriptors, } from '../../layout/types/viewmodel_types'; import { INPUT_KEY } from '../../layout/utils/group_by_rollup'; import { isSimpleLinear } from '../../layout/viewmodel/viewmodel'; @@ -50,7 +50,7 @@ export interface ContinuousDomainFocus { } /** @internal */ -export interface IndexedContinuousDomainFocus extends ContinuousDomainFocus, SmallMultiplesIndices {} +export interface IndexedContinuousDomainFocus extends ContinuousDomainFocus, SmallMultiplesDescriptors {} interface ReactiveChartStateProps { initialized: boolean; diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts index 8afcbd3e8b3b..0a81254ec21c 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/geometries.ts @@ -90,8 +90,7 @@ export const partitionMultiGeometries = createCachedSelector( const marginBottom = spec.config.margin?.bottom ?? config.margin.bottom; const chartHeight = getInterMarginSize(marginedHeight, marginTop, marginBottom); - - return trees.map(({ name, style, tree: t }: StyledTree, innerIndex, a) => { + return trees.map(({ name, smAccessorValue, style, tree: t }: StyledTree, innerIndex, a) => { const innerPanelCount = a.length; const outerPanelWidth = chartWidth * outerWidthRatio; const outerPanelHeight = chartHeight * outerHeightRatio; @@ -190,6 +189,7 @@ export const partitionMultiGeometries = createCachedSelector( innerIndex, partitionLayout: spec.config.partitionLayout ?? config.partitionLayout, panelTitle: String(name), + smAccessorValue, top: topOuterRatio + topInnerRatio, height: panelHeightRatio, left: leftOuterRatio + leftInnerRatio, @@ -227,10 +227,10 @@ export const partitionDrilldownFocus = createCachedSelector( (state) => state.interactions.prevDrilldown, ], (multiGeometries, chartDimensions, drilldown, prevDrilldown): IndexedContinuousDomainFocus[] => - multiGeometries.map(({ quadViewModel, index, innerIndex }) => { + multiGeometries.map(({ quadViewModel, smAccessorValue, index, innerIndex }) => { const { x0: currentFocusX0, x1: currentFocusX1 } = focusRect(quadViewModel, chartDimensions, drilldown); const { x0: prevFocusX0, x1: prevFocusX1 } = focusRect(quadViewModel, chartDimensions, prevDrilldown); - return { currentFocusX0, currentFocusX1, prevFocusX0, prevFocusX1, index, innerIndex }; + return { currentFocusX0, currentFocusX1, prevFocusX0, prevFocusX1, smAccessorValue, index, innerIndex }; }), )((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts index 7d9264e327e2..684a19b92f3e 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/picked_shapes.test.ts @@ -19,12 +19,21 @@ import { createStore, Store } from 'redux'; +import { Predicate } from '../../../../common/predicate'; import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; -import { SettingsSpec, XYChartElementEvent, PartitionElementEvent, HeatmapElementEvent } from '../../../../specs'; +import { + SettingsSpec, + XYChartElementEvent, + PartitionElementEvent, + HeatmapElementEvent, + GroupBySpec, + SmallMultiplesSpec, +} from '../../../../specs'; import { updateParentDimensions } from '../../../../state/actions/chart_settings'; import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; import { upsertSpec, specParsed } from '../../../../state/actions/specs'; import { chartStoreReducer, GlobalChartState } from '../../../../state/chart_state'; +import { Datum } from '../../../../utils/common'; import { HIERARCHY_ROOT_KEY } from '../../layout/utils/group_by_rollup'; import { PartitionSpec } from '../../specs'; import { partitionGeometries } from './geometries'; @@ -41,6 +50,20 @@ describe('Picked shapes selector', () => { store.dispatch(specParsed()); store.dispatch(updateParentDimensions({ width: 300, height: 300, top: 0, left: 0 })); } + function addSmallMultiplesSeries( + store: Store, + groupBy: Partial, + sm: Partial, + spec: PartitionSpec, + settings?: Partial, + ) { + store.dispatch(upsertSpec(MockGlobalSpec.settings(settings))); + store.dispatch(upsertSpec(MockGlobalSpec.groupBy(groupBy))); + store.dispatch(upsertSpec(MockGlobalSpec.smallMultiple(sm))); + store.dispatch(upsertSpec(spec)); + store.dispatch(specParsed()); + store.dispatch(updateParentDimensions({ width: 300, height: 300, top: 0, left: 0 })); + } let store: Store; let treemapSpec: PartitionSpec; let sunburstSpec: PartitionSpec; @@ -98,6 +121,7 @@ describe('Picked shapes selector', () => { [ [ { + smAccessorValue: '', groupByRollup: 'b', value: 2, depth: 1, @@ -108,6 +132,7 @@ describe('Picked shapes selector', () => { ], }, { + smAccessorValue: '', groupByRollup: 'b', value: 1, depth: 2, @@ -126,6 +151,74 @@ describe('Picked shapes selector', () => { ], ]); }); + test('small multiples pie chart check picked geometries', () => { + const onClickListener = jest.fn< + undefined, + Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]> + >((): undefined => undefined); + addSmallMultiplesSeries( + store, + { + id: 'splitGB', + by: (_, d: Datum) => d.g1, + sort: Predicate.AlphaAsc, + format: (d: Datum) => String(d), + }, + { id: 'sm', splitHorizontally: 'splitGB' }, + MockSeriesSpec.sunburst({ + smallMultiples: 'sm', + valueAccessor: (d: { v: number }) => d.v, + data: [ + { g1: 'a', g2: 'a', v: 1 }, + { g1: 'a', g2: 'b', v: 1 }, + { g1: 'b', g2: 'a', v: 1 }, + { g1: 'b', g2: 'b', v: 1 }, + ], + layers: [ + { + groupByRollup: (datum: { g2: string }) => datum.g2, + }, + ], + }), + { + onElementClick: onClickListener, + }, + ); + const geometries = partitionGeometries(store.getState())[0]; + expect(geometries.quadViewModel).toHaveLength(2); + + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + const x = 50; + const y = 150; + store.dispatch(onPointerMove({ x, y }, 0)); + store.dispatch(onMouseDown({ x, y }, 1)); + store.dispatch(onMouseUp({ x, y }, 2)); + expect(onClickListener).toBeCalled(); + expect(onClickListener.mock.calls[0][0]).toEqual([ + [ + [ + { + smAccessorValue: 'a', + groupByRollup: 'a', + value: 1, + depth: 1, + sortIndex: 0, + path: [ + { index: 0, value: HIERARCHY_ROOT_KEY }, + { index: 0, value: 'a' }, + ], + }, + ], + { + specId: sunburstSpec.id, + key: `spec{${sunburstSpec.id}}`, + }, + ], + ]); + }); test('sunburst check picked geometries', () => { const onClickListener = jest.fn< undefined, @@ -149,6 +242,7 @@ describe('Picked shapes selector', () => { [ [ { + smAccessorValue: '', groupByRollup: 'b', value: 2, depth: 1, @@ -159,6 +253,7 @@ describe('Picked shapes selector', () => { ], }, { + smAccessorValue: '', groupByRollup: 'b', value: 1, depth: 2, diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts index 1df1ceb1f7f7..e87a4ff04ba9 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts @@ -45,7 +45,12 @@ const getGroupBySpecs = createCachedSelector([getSpecs], (specs) => )(getChartIdSelector); /** @internal */ -export type StyledTree = { name: string | number; style: SmallMultiplesStyle; tree: HierarchyOfArrays }; +export type StyledTree = { + smAccessorValue: ReturnType; + name: string; + style: SmallMultiplesStyle; + tree: HierarchyOfArrays; +}; function getTreesForSpec( spec: PartitionSpec, @@ -81,6 +86,7 @@ function getTreesForSpec( .sort(getPredicateFn(sort)) .map(([groupKey, subData]) => ({ name: format(groupKey), + smAccessorValue: groupKey, style: smStyle, tree: partitionTree( subData, @@ -94,6 +100,7 @@ function getTreesForSpec( return [ { name: '', + smAccessorValue: '', style: smStyle, tree: partitionTree(data, valueAccessor, layers, configMetadata.partitionLayout.dflt, config.partitionLayout), }, diff --git a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts index 06aead7d010b..e72255406da5 100644 --- a/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts +++ b/packages/osd-charts/src/chart_types/wordcloud/state/selectors/picked_shapes.ts @@ -47,6 +47,7 @@ export const getPickedShapesLayerValues = createCachedSelector( const elements = pickedShapes.map>((model) => { const values: Array = []; values.push({ + smAccessorValue: '', groupByRollup: 'Word count', value: model.data.length, sortIndex: 0, diff --git a/packages/osd-charts/src/specs/group_by.ts b/packages/osd-charts/src/specs/group_by.ts index 59a5c42c2f34..0686106d16da 100644 --- a/packages/osd-charts/src/specs/group_by.ts +++ b/packages/osd-charts/src/specs/group_by.ts @@ -25,7 +25,7 @@ import { Predicate } from '../common/predicate'; import { getConnect, specComponentFactory } from '../state/spec_factory'; import { SpecType } from './constants'; -/** @alpha */ +/** @public */ export type GroupByAccessor = (spec: Spec, datum: any) => string | number; /** @alpha */ export type GroupBySort = Predicate; diff --git a/packages/osd-charts/src/specs/settings.tsx b/packages/osd-charts/src/specs/settings.tsx index 03bf16bec602..5eda3624487a 100644 --- a/packages/osd-charts/src/specs/settings.tsx +++ b/packages/osd-charts/src/specs/settings.tsx @@ -19,7 +19,7 @@ import React, { ComponentType, ReactChild } from 'react'; -import { CustomXDomain, Spec } from '.'; +import { CustomXDomain, GroupByAccessor, Spec } from '.'; import { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; import { LegendStrategy } from '../chart_types/partition_chart/layout/utils/highlighted_geoms'; @@ -52,6 +52,10 @@ export interface LayerValue { * The category value as retrieved by the `groupByRollup` callback */ groupByRollup: PrimitiveValue; + /** + * The small multiples `` `by` accessor value, to specify which small multiples panel is interacted with + */ + smAccessorValue: ReturnType; /** * Numerical value of the partition */ diff --git a/packages/osd-charts/stories/interactions/4_sunburst_slice_clicks.tsx b/packages/osd-charts/stories/interactions/4_sunburst_slice_clicks.tsx index 1ac43f5d46c2..2e05f5418d50 100644 --- a/packages/osd-charts/stories/interactions/4_sunburst_slice_clicks.tsx +++ b/packages/osd-charts/stories/interactions/4_sunburst_slice_clicks.tsx @@ -35,7 +35,9 @@ const onElementListeners = { onElementOver: action('onElementOver'), onElementOut: action('onElementOut'), }; + type PieDatum = [string, number, string, number]; + const pieData: Array = [ ['CN', 301, 'IN', 44], ['CN', 301, 'US', 24], diff --git a/packages/osd-charts/stories/small_multiples/7_sunbursts.tsx b/packages/osd-charts/stories/small_multiples/7_sunbursts.tsx index 65aad33b58c2..0017b7ffece4 100644 --- a/packages/osd-charts/stories/small_multiples/7_sunbursts.tsx +++ b/packages/osd-charts/stories/small_multiples/7_sunbursts.tsx @@ -17,6 +17,7 @@ * under the License. */ +import { action } from '@storybook/addon-actions'; import { boolean, select, number } from '@storybook/addon-knobs'; import React from 'react'; @@ -58,6 +59,12 @@ const countryToColor = new Map( }), ); +const onElementListeners = { + onElementClick: action('onElementClick'), + onElementOver: action('onElementOver'), + onElementOut: action('onElementOut'), +}; + export const Example = () => { const layout = select( 'Inner breakdown layout', @@ -76,6 +83,7 @@ export const Example = () => { legendStrategy={LegendStrategy.Key} flatLegend={boolean('Flat legend', true)} theme={STORYBOOK_LIGHT_THEME} + {...onElementListeners} />