diff --git a/packages/osd-charts/.playground/playground.tsx b/packages/osd-charts/.playground/playground.tsx index deed09e9649c..7d1e4f864fc7 100644 --- a/packages/osd-charts/.playground/playground.tsx +++ b/packages/osd-charts/.playground/playground.tsx @@ -17,65 +17,10 @@ * under the License. */ import React from 'react'; -import { - Chart, - ScaleType, - Position, - Axis, - Settings, - PartitionElementEvent, - XYChartElementEvent, - BarSeries, -} from '../src'; +import { example } from '../stories/sunburst/12_very_small'; -export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { - onClick = (elements: Array) => { - // eslint-disable-next-line no-console - console.log(elements[0]); - }; +export class Playground extends React.Component { render() { - return ( - <> -
- - - - { - return `${d} $`; - }, - }} - /> - -
- - ); + return
{example()}
; } } diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png index 04f85d76c2ea..b9cb2c01dba6 100644 Binary files a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png new file mode 100644 index 000000000000..7c5e6a8cd481 Binary files /dev/null and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png new file mode 100644 index 000000000000..65578ffc5d89 Binary files /dev/null and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png index b537e7bc68e1..ef35a7ab424d 100644 Binary files a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-mid-two-layers-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-mid-two-layers-visually-looks-correct-1-snap.png index fdd399fe2eb8..fa3d98906bf1 100644 Binary files a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-mid-two-layers-visually-looks-correct-1-snap.png and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-mid-two-layers-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png index fe1ba504cc8e..3dca8e6f007c 100644 Binary files a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png and b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png differ diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png index 1e26297411bb..64b57b9bc2da 100644 Binary files a/packages/osd-charts/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png and b/packages/osd-charts/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png differ diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx index ae2994e94b25..1409e534e23e 100644 --- a/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/goal_chart/state/chart_state.tsx @@ -26,8 +26,12 @@ import { Tooltip } from '../../../components/tooltip'; import { createOnElementClickCaller } from './selectors/on_element_click_caller'; import { createOnElementOverCaller } from './selectors/on_element_over_caller'; import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { LegendItem } from '../../../commons/legend'; +import { LegendItemLabel } from '../../../state/selectors/get_legend_items_labels'; const EMPTY_MAP = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; +const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; /** @internal */ export class GoalState implements InternalChartState { @@ -51,9 +55,12 @@ export class GoalState implements InternalChartState { return false; } getLegendItems() { - return EMPTY_MAP; + return EMPTY_LEGEND_LIST; + } + getLegendItemsLabels() { + return EMPTY_LEGEND_ITEM_LIST; } - getLegendItemsValues() { + getLegendExtraValues() { return EMPTY_MAP; } chartRenderer(containerRef: BackwardRef) { diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts index 254edaa52aca..70080c0cd514 100644 --- a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts @@ -24,9 +24,9 @@ import { SettingsSpec, LayerValue } from '../../../../specs'; import { getPickedShapesLayerValues } from './picked_shapes'; import { getSpecOrNull } from './goal_spec'; import { ChartTypes } from '../../..'; -import { SeriesIdentifier } from '../../../xy_chart/utils/series'; import { isClicking } from '../../../../state/utils'; import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; +import { SeriesIdentifier } from '../../../../commons/series_id'; /** * Will call the onElementClick listener every time the following preconditions are met: diff --git a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts index 0cf12e34b302..488da5b7af15 100644 --- a/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts +++ b/packages/osd-charts/src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts @@ -25,7 +25,7 @@ import { ChartTypes } from '../../../index'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSpecOrNull } from './goal_spec'; import { getPickedShapesLayerValues } from './picked_shapes'; -import { SeriesIdentifier } from '../../../xy_chart/utils/series'; +import { SeriesIdentifier } from '../../../../commons/series_id'; function isOverElement(prevPickedShapes: Array> = [], nextPickedShapes: Array>) { if (nextPickedShapes.length === 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 5d119719ce9c..cf27b87548d9 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 @@ -86,6 +86,7 @@ export type ShapeViewModel = { outsideLinksViewModel: OutsideLinksViewModel[]; diskCenter: PointObject; pickQuads: PickFunction; + outerRadius: number; }; export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({ @@ -96,6 +97,7 @@ export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointO outsideLinksViewModel: [], diskCenter: diskCenter || { x: 0, y: 0 }, pickQuads: () => [], + outerRadius: 0, }); type TreeLevel = number; diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts new file mode 100644 index 000000000000..954e523e551d --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts @@ -0,0 +1,57 @@ +/* + * 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 { HierarchyOfArrays } from '../utils/group_by_rollup'; +import { Relation } from '../types/types'; +import { ValueAccessor } from '../../../../utils/commons'; +import { IndexedAccessorFn } from '../../../../utils/accessor'; +import { + aggregateComparator, + aggregators, + childOrders, + groupByRollup, + mapEntryValue, + mapsToArrays, +} from '../utils/group_by_rollup'; + +export function getHierarchyOfArrays( + rawFacts: Relation, + valueAccessor: ValueAccessor, + groupByRollupAccessors: IndexedAccessorFn[], +): HierarchyOfArrays { + const aggregator = aggregators.sum; + + const facts = rawFacts.filter((n) => { + const value = valueAccessor(n); + return Number.isFinite(value) && value >= 0; + }); + + // don't render anything if the total, the width or height is not positive + if (facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0) { + return []; + } + + // We can precompute things invariant of how the rectangle is divvied up. + // By introducing `scale`, we no longer need to deal with the dichotomy of + // size as data value vs size as number of pixels in the rectangle + + return mapsToArrays( + groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts), + aggregateComparator(mapEntryValue, childOrders.descending), + ); +} 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 983b341a6ae7..80655316aed4 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 @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Part, Relation, TextMeasure } from '../types/types'; +import { Part, TextMeasure } from '../types/types'; import { linkTextLayout } from './link_text_layout'; import { Config, PartitionLayout } from '../types/config_types'; import { TAU, trueBearingToStandardPositionAngle } from '../utils/math'; @@ -24,7 +24,6 @@ import { Distance, Pixels, Radius } from '../types/geometry_types'; import { meanAngle } from '../geometry'; import { treemap } from '../utils/treemap'; import { sunburst } from '../utils/sunburst'; -import { IndexedAccessorFn } from '../../../../utils/accessor'; import { argsToRGBString, stringToRGB } from '../utils/d3_utils'; import { nullShapeViewModel, @@ -49,20 +48,16 @@ import { } from './fill_text_layout'; import { aggregateAccessor, - aggregateComparator, - aggregators, ArrayEntry, - childOrders, depthAccessor, entryKey, entryValue, - groupByRollup, mapEntryValue, - mapsToArrays, parentAccessor, sortIndexAccessor, + HierarchyOfArrays, } from '../utils/group_by_rollup'; -import { StrokeStyle, ValueAccessor, ValueFormatter } from '../../../../utils/commons'; +import { StrokeStyle, ValueFormatter } from '../../../../utils/commons'; import { percentValueGetter } from '../config/config'; function paddingAccessor(n: ArrayEntry) { @@ -148,13 +143,11 @@ export function shapeViewModel( textMeasure: TextMeasure, config: Config, layers: Layer[], - rawFacts: Relation, rawTextGetter: RawTextGetter, - valueAccessor: ValueAccessor, specifiedValueFormatter: ValueFormatter, specifiedPercentFormatter: ValueFormatter, valueGetter: ValueGetterFunction, - groupByRollupAccessors: IndexedAccessorFn[], + tree: HierarchyOfArrays, ): ShapeViewModel { const { width, @@ -179,31 +172,11 @@ export function shapeViewModel( y: height * margin.top + innerHeight / 2, }; - const aggregator = aggregators.sum; - - const facts = rawFacts.filter((n) => { - const value = valueAccessor(n); - return Number.isFinite(value) && value >= 0; - }); - // don't render anything if the total, the width or height is not positive - if ( - facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0 || - !(width > 0) || - !(height > 0) - ) { + if (!(width > 0) || !(height > 0) || tree.length === 0) { return nullShapeViewModel(config, diskCenter); } - // We can precompute things invariant of how the rectangle is divvied up. - // By introducing `scale`, we no longer need to deal with the dichotomy of - // size as data value vs size as number of pixels in the rectangle - - const tree = mapsToArrays( - groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts), - aggregateComparator(mapEntryValue, childOrders.descending), - ); - const totalValue = tree.reduce((p: number, n: ArrayEntry): number => p + mapEntryValue(n), 0); const sunburstValueToAreaScale = TAU / totalValue; @@ -332,5 +305,6 @@ export function shapeViewModel( linkLabelViewModels, outsideLinksViewModel, pickQuads, + outerRadius, }; } diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts index 71c1641615b3..b27363c23c10 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts @@ -72,6 +72,7 @@ function renderTaperedBorder( ctx.arc(0, 0, y0px, X0, X0); ctx.arc(0, 0, y1px, X0, X1, false); ctx.arc(0, 0, y0px, X1, X0, true); + ctx.fill(); if (strokeWidth > 0.001 && !(x0 === 0 && x1 === TAU)) { // canvas2d uses a default of 1 if the lineWidth is assigned 0, so we use a small value to test, to avoid it 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 881196631a52..f9769b7b3c4c 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 @@ -27,6 +27,7 @@ import { partitionGeometries } from '../../state/selectors/geometries'; import { nullShapeViewModel, QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; import { renderPartitionCanvas2d } from './canvas_renderers'; import { INPUT_KEY } from '../../layout/utils/group_by_rollup'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; interface ReactiveChartStateProps { initialized: boolean; @@ -171,7 +172,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { return { initialized: true, geometries: partitionGeometries(state), - chartContainerDimensions: state.parentDimensions, + chartContainerDimensions: getChartContainerDimensionsSelector(state), }; }; diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx new file mode 100644 index 000000000000..701ed8494c6b --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter.tsx @@ -0,0 +1,205 @@ +/* + * 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 { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { TAU } from '../../layout/utils/math'; +import { PointObject } from '../../layout/types/geometry_types'; +import { PartitionLayout } from '../../layout/types/config_types'; +import { Dimensions } from '../../../../utils/dimensions'; + +/** @internal */ +export interface HighlighterProps { + chartId: string; + initialized: boolean; + canvasDimension: Dimensions; + partitionLayout: PartitionLayout; + geometries: QuadViewModel[]; + diskCenter: PointObject; + outerRadius: number; + renderAsOverlay: boolean; +} + +const EPSILON = 1e-6; + +interface SVGStyle { + color?: string; + fillClassName?: string; + strokeClassName?: string; +} + +/** + * This function return an SVG arc path from the same parameters of the canvas.arc function call + * @param x The horizontal coordinate of the arc's center + * @param y The vertical coordinate of the arc's center + * @param r The arc's radius. Must be positive + * @param a0 The angle at which the arc starts in radians, measured from the positive x-axis + * @param a1 The angle at which the arc ends in radians, measured from the positive x-axis + * @param ccw If 1, draws the arc counter-clockwise between the start and end angles + */ +function getSectorShapeFromCanvasArc(x: number, y: number, r: number, a0: number, a1: number, ccw: boolean): string { + const cw = Number(!ccw); + const da = ccw ? a0 - a1 : a1 - a0; + return `A${r},${r},0,${+(da >= Math.PI)},${cw},${x + r * Math.cos(a1)},${y + r * Math.sin(a1)}`; +} + +/** + * Renders an SVG Rect from a partition chart QuadViewModel + * @param geometry the QuadViewModel + * @param key the key to apply to the react element + * @param fillColor the optional fill color + */ +function renderRectangles(geometry: QuadViewModel, key: string, style: SVGStyle) { + const { x0, x1, y0px, y1px } = geometry; + const props = style.color ? { fill: style.color } : { className: style.fillClassName }; + return ; +} + +/** + * Render an SVG path or circle from a partition chart QuadViewModel + * @param geometry the QuadViewModel + * @param key the key to apply to the react element + * @param fillColor the optional fill color + */ +function renderSector(geometry: QuadViewModel, key: string, style: SVGStyle) { + const { x0, x1, y0px, y1px } = geometry; + if ((Math.abs(x0 - x1) + TAU) % TAU < EPSILON) { + const props = style.color ? { stroke: style.color } : { className: style.strokeClassName }; + return ; + } + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + const path = [ + `M${y0px * Math.cos(X0)},${y0px * Math.sin(X0)}`, + getSectorShapeFromCanvasArc(0, 0, y0px, X0, X1, false), + `L${y1px * Math.cos(X1)},${y1px * Math.sin(X1)}`, + getSectorShapeFromCanvasArc(0, 0, y1px, X1, X0, true), + 'Z', + ].join(' '); + const props = style.color ? { fill: style.color } : { className: style.fillClassName }; + return ; +} + +function renderGeometries(geometries: QuadViewModel[], partitionLayout: PartitionLayout, style: SVGStyle) { + let maxDepth = -1; + // we should render only the deepest geometries of the tree to avoid overlaying highlighted geometries + if (partitionLayout === PartitionLayout.treemap) { + maxDepth = geometries.reduce((acc, geom) => { + return Math.max(acc, geom.depth); + }, 0); + } + return geometries + .filter((geometry) => { + if (maxDepth !== -1) { + return geometry.depth >= maxDepth; + } + return true; + }) + .map((geometry, index) => { + if (partitionLayout === PartitionLayout.sunburst) { + return renderSector(geometry, `${index}`, style); + } + + return renderRectangles(geometry, `${index}`, style); + }); +} + +/** @internal */ +export class HighlighterComponent extends React.Component { + static displayName = 'Highlighter'; + + renderAsMask() { + const { + geometries, + diskCenter, + outerRadius, + partitionLayout, + chartId, + canvasDimension: { width, height }, + } = this.props; + const maskId = `echHighlighterMask__${chartId}`; + return ( + <> + + + + + {renderGeometries(geometries, partitionLayout, { color: 'black' })} + + + + {partitionLayout === PartitionLayout.sunburst && ( + + )} + {partitionLayout === PartitionLayout.treemap && ( + + )} + + ); + } + + renderAsOverlay() { + const { geometries, diskCenter, partitionLayout } = this.props; + return ( + + {renderGeometries(geometries, partitionLayout, { + fillClassName: 'echHighlighterOverlay__fill', + strokeClassName: 'echHighlighterOverlay__stroke', + })} + + ); + } + + render() { + const { geometries, renderAsOverlay } = this.props; + if (geometries.length === 0) { + return null; + } + return ( + + {renderAsOverlay ? this.renderAsOverlay() : this.renderAsMask()} + + ); + } +} + +/** @internal */ +export const DEFAULT_PROPS: HighlighterProps = { + chartId: 'empty', + initialized: false, + canvasDimension: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + geometries: [], + diskCenter: { + x: 0, + y: 0, + }, + outerRadius: 10, + renderAsOverlay: false, + partitionLayout: PartitionLayout.sunburst, +}; diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx new file mode 100644 index 000000000000..993db05547ce --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_hover.tsx @@ -0,0 +1,57 @@ +/* + * 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 { connect } from 'react-redux'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { isInitialized } from '../../../../state/selectors/is_initialized'; +import { partitionGeometries } from '../../state/selectors/geometries'; +import { getPickedShapes } from '../../state/selectors/picked_shapes'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { HighlighterComponent, HighlighterProps, DEFAULT_PROPS } from './highlighter'; + +const hoverMapStateToProps = (state: GlobalChartState): HighlighterProps => { + if (!isInitialized(state)) { + return DEFAULT_PROPS; + } + + const { chartId } = state; + const { + outerRadius, + diskCenter, + config: { partitionLayout }, + } = partitionGeometries(state); + + const geometries = getPickedShapes(state); + const canvasDimension = getChartContainerDimensionsSelector(state); + return { + chartId, + initialized: true, + renderAsOverlay: true, + canvasDimension, + diskCenter, + outerRadius, + geometries, + partitionLayout, + }; +}; + +/** + * Partition chart highlighter from mouse hover events + * @internal + */ +export const HighlighterFromHover = connect(hoverMapStateToProps)(HighlighterComponent); diff --git a/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx new file mode 100644 index 000000000000..1cdbb89e2d02 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx @@ -0,0 +1,57 @@ +/* + * 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 { connect } from 'react-redux'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { isInitialized } from '../../../../state/selectors/is_initialized'; +import { partitionGeometries } from '../../state/selectors/geometries'; +import { getHighlightedSectorsSelector } from '../../state/selectors/get_highlighted_shapes'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { HighlighterComponent, HighlighterProps, DEFAULT_PROPS } from './highlighter'; + +const legendMapStateToProps = (state: GlobalChartState): HighlighterProps => { + if (!isInitialized(state)) { + return DEFAULT_PROPS; + } + + const { chartId } = state; + const { + outerRadius, + diskCenter, + config: { partitionLayout }, + } = partitionGeometries(state); + + const geometries = getHighlightedSectorsSelector(state); + const canvasDimension = getChartContainerDimensionsSelector(state); + return { + chartId, + initialized: true, + renderAsOverlay: false, + canvasDimension, + geometries, + diskCenter, + outerRadius, + partitionLayout, + }; +}; + +/** + * Partition chart highlighter from legend events + * @internal + */ +export const HighlighterFromLegend = connect(legendMapStateToProps)(HighlighterComponent); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx index f297505c3546..2fbb97101c9a 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx @@ -26,6 +26,10 @@ import { Tooltip } from '../../../components/tooltip'; import { createOnElementClickCaller } from './selectors/on_element_click_caller'; import { createOnElementOverCaller } from './selectors/on_element_over_caller'; import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { computeLegendSelector } from './selectors/compute_legend'; +import { getLegendItemsLabels } from './selectors/get_legend_items_labels'; +import { HighlighterFromHover } from '../renderer/dom/highlighter_hover'; +import { HighlighterFromLegend } from '../renderer/dom/highlighter_legend'; const EMPTY_MAP = new Map(); @@ -50,10 +54,13 @@ export class PartitionState implements InternalChartState { isChartEmpty() { return false; } - getLegendItems() { - return EMPTY_MAP; + getLegendItemsLabels(globalState: GlobalChartState) { + return getLegendItemsLabels(globalState); + } + getLegendItems(globalState: GlobalChartState) { + return computeLegendSelector(globalState); } - getLegendItemsValues() { + getLegendExtraValues() { return EMPTY_MAP; } chartRenderer(containerRef: BackwardRef) { @@ -61,6 +68,8 @@ export class PartitionState implements InternalChartState { <> + + ); } diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts new file mode 100644 index 000000000000..455869916d4b --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -0,0 +1,99 @@ +/* + * 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 { LegendItem } from '../../../../commons/legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getPieSpecOrNull } from './pie_spec'; +import { partitionGeometries } from './geometries'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { PrimitiveValue } from '../../layout/utils/group_by_rollup'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { getFlatHierarchy } from './get_flat_hierarchy'; +import { Position } from '../../../../utils/commons'; + +/** @internal */ +export const computeLegendSelector = createCachedSelector( + [getPieSpecOrNull, getSettingsSpecSelector, partitionGeometries, getFlatHierarchy], + (pieSpec, settings, geoms, sortedItems): LegendItem[] => { + if (!pieSpec) { + return []; + } + const { id, layers: labelFormatters } = pieSpec; + + const uniqueNames = geoms.quadViewModel.reduce>((acc, { dataName, fillColor }) => { + const key = [dataName, fillColor].join('---'); + if (!acc[key]) { + acc[key] = 0; + } + acc[key] += 1; + return acc; + }, {}); + + const { flatLegend, legendMaxDepth, legendPosition } = settings; + const forceFlatLegend = flatLegend || legendPosition === Position.Bottom || legendPosition === Position.Top; + + const excluded: Set = new Set(); + let items = geoms.quadViewModel.filter(({ depth, dataName, fillColor }) => { + if (legendMaxDepth != null) { + return depth <= legendMaxDepth; + } + if (forceFlatLegend) { + const key = [dataName, fillColor].join('---'); + if (uniqueNames[key] > 1 && excluded.has(key)) { + return false; + } + excluded.add(key); + } + return true; + }); + + if (forceFlatLegend) { + items = items.sort(({ depth: a }, { depth: b }) => a - b); + } + + return items + .sort((a, b) => { + const aIndex = findIndex(sortedItems, a); + const bIndex = findIndex(sortedItems, b); + return aIndex - bIndex; + }) + .map(({ dataName, fillColor, depth }) => { + const labelFormatter = labelFormatters[depth - 1]; + const formatter = labelFormatter?.nodeLabel; + + return { + color: fillColor, + label: formatter ? formatter(dataName) : dataName, + dataName, + childId: dataName, + depth: forceFlatLegend ? 0 : depth - 1, + seriesIdentifier: { + key: dataName, + specId: id, + }, + }; + }); + }, +)(getChartIdSelector); + +function findIndex(items: Array<[PrimitiveValue, number, PrimitiveValue]>, child: QuadViewModel) { + return items.findIndex(([dataName, depth, value]) => { + return dataName === child.dataName && depth === child.depth && value === child.value; + }); +} 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 9514b50cb7bd..69766e7901de 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 @@ -24,16 +24,16 @@ import { render } from './scenegraph'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; import { PartitionSpec } from '../../specs/index'; import { SpecTypes } from '../../../../specs/settings'; +import { getTree } from './tree'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; const getSpecs = (state: GlobalChartState) => state.specs; -const getParentDimensions = (state: GlobalChartState) => state.parentDimensions; - /** @internal */ export const partitionGeometries = createCachedSelector( - [getSpecs, getParentDimensions], - (specs, parentDimensions): ShapeViewModel => { + [getSpecs, getChartContainerDimensionsSelector, getTree], + (specs, parentDimensions, tree): ShapeViewModel => { const pieSpecs = getSpecsFromStore(specs, ChartTypes.Partition, SpecTypes.Series); - return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions) : nullShapeViewModel(); + return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions, tree) : nullShapeViewModel(); }, )((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts new file mode 100644 index 000000000000..1c556ee0ddb8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts @@ -0,0 +1,46 @@ +/* + * 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 { getTree } from './tree'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { HierarchyOfArrays, PrimitiveValue } from '../../layout/utils/group_by_rollup'; + +/** @internal */ +export const getFlatHierarchy = createCachedSelector( + [getTree], + (tree): Array<[PrimitiveValue, number, PrimitiveValue]> => { + return flatHierarchy(tree); + }, +)(getChartIdSelector); + +function flatHierarchy(tree: HierarchyOfArrays, orderedList: Array<[PrimitiveValue, number, PrimitiveValue]> = []) { + for (let i = 0; i < tree.length; i++) { + const branch = tree[i]; + const [key, arrayNode] = branch; + const { children, depth, value } = arrayNode; + + if (key !== null) { + orderedList.push([key, depth, value]); + } + if (children.length > 0) { + flatHierarchy(children, orderedList); + } + } + return orderedList; +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts new file mode 100644 index 000000000000..b67a8ad869f8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.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 { GlobalChartState } from '../../../../state/chart_state'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { partitionGeometries } from './geometries'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; + +const getHighlightedLegendItemKey = (state: GlobalChartState) => state.interactions.highlightedLegendItemKey; + +/** @internal */ +export const getHighlightedSectorsSelector = createCachedSelector( + [getHighlightedLegendItemKey, partitionGeometries], + (highlightedLegendItemKey, geoms): QuadViewModel[] => { + if (!highlightedLegendItemKey) { + return []; + } + return geoms.quadViewModel.filter((geom) => { + return geom.dataName === highlightedLegendItemKey; + }); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..7c7b88b495be --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.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 { getTree } from './tree'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getPieSpecOrNull } from './pie_spec'; +import { HierarchyOfArrays, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; +import { Layer } from '../../specs'; +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; + +/** @internal */ +export const getLegendItemsLabels = createCachedSelector( + [getPieSpecOrNull, getSettingsSpecSelector, getTree], + (pieSpec, { legendMaxDepth }, tree): LegendItemLabel[] => { + if (!pieSpec || (typeof legendMaxDepth === 'number' && legendMaxDepth <= 0)) { + return []; + } + return flatSlicesNames(pieSpec.layers, 0, tree); + }, +)(getChartIdSelector); + +function flatSlicesNames( + layers: Layer[], + depth: number, + tree: HierarchyOfArrays, + keys: Map = new Map(), +): LegendItemLabel[] { + if (tree.length === 0) { + return []; + } + for (let i = 0; i < tree.length; i++) { + const branch = tree[i]; + const arrayNode = branch[1]; + const key = branch[0]; + + // format the key with the layer formatter + const layer = layers[depth - 1]; + const formatter = layer?.nodeLabel; + let formattedValue = ''; + if (key != null) { + formattedValue = formatter ? formatter(key) : `${key}`; + } + + // save only the max depth, so we can compute the the max extension of the legend + keys.set(formattedValue, Math.max(depth, keys.get(formattedValue) ?? 0)); + + const children = arrayNode[CHILDREN_KEY]; + flatSlicesNames(layers, depth + 1, children, keys); + } + return [...keys.keys()].map((k) => { + return { + label: k, + depth: keys.get(k) ?? 0, + }; + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts index b7daf998ec5e..dba7cc8f7b16 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -24,7 +24,7 @@ import { SettingsSpec, LayerValue } from '../../../../specs'; import { getPickedShapesLayerValues } from './picked_shapes'; import { getPieSpecOrNull } from './pie_spec'; import { ChartTypes } from '../../..'; -import { SeriesIdentifier } from '../../../xy_chart/utils/series'; +import { SeriesIdentifier } from '../../../../commons/series_id'; import { isClicking } from '../../../../state/utils'; import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts index 5180a3b7377a..61c4f0a77863 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts @@ -25,7 +25,7 @@ import { ChartTypes } from '../../../index'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getPieSpecOrNull } from './pie_spec'; import { getPickedShapesLayerValues } from './picked_shapes'; -import { SeriesIdentifier } from '../../../xy_chart/utils/series'; +import { SeriesIdentifier } from '../../../../commons/series_id'; function isOverElement(prevPickedShapes: Array> = [], nextPickedShapes: Array>) { if (nextPickedShapes.length === 0) { diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/scenegraph.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/scenegraph.ts index ba2ce352bcca..7a3c2b471dea 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/scenegraph.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/scenegraph.ts @@ -26,7 +26,7 @@ import { nullShapeViewModel, ValueGetter, } from '../../layout/types/viewmodel_types'; -import { DEPTH_KEY } from '../../layout/utils/group_by_rollup'; +import { DEPTH_KEY, HierarchyOfArrays } from '../../layout/utils/group_by_rollup'; import { PartitionSpec, Layer } from '../../specs/index'; import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons'; import { config as defaultConfig, VALUE_GETTERS } from '../../layout/config/config'; @@ -45,9 +45,13 @@ export function valueGetterFunction(valueGetter: ValueGetter) { } /** @internal */ -export function render(partitionSpec: PartitionSpec, parentDimensions: Dimensions): ShapeViewModel { +export function render( + partitionSpec: PartitionSpec, + parentDimensions: Dimensions, + tree: HierarchyOfArrays, +): ShapeViewModel { const { width, height } = parentDimensions; - const { layers, data: facts, config: specConfig } = partitionSpec; + const { layers, config: specConfig } = partitionSpec; const textMeasurer = document.createElement('canvas'); const textMeasurerCtx = textMeasurer.getContext('2d'); const partialConfig: RecursivePartial = { ...specConfig, width, height }; @@ -60,12 +64,10 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension measureText(textMeasurerCtx), config, layers, - facts, rawTextGetter(layers), - partitionSpec.valueAccessor, partitionSpec.valueFormatter, partitionSpec.percentFormatter, valueGetter, - [() => null, ...layers.map(({ groupByRollup }) => groupByRollup)], + tree, ); } 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 new file mode 100644 index 000000000000..037ce69de0e8 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/tree.ts @@ -0,0 +1,41 @@ +/* + * 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 { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { ChartTypes } from '../../..'; +import { PartitionSpec } from '../../specs/index'; +import { SpecTypes } from '../../../../specs/settings'; +import { getHierarchyOfArrays } from '../../layout/viewmodel/hierarchy_of_arrays'; +import { HierarchyOfArrays } from '../../layout/utils/group_by_rollup'; + +const getSpecs = (state: GlobalChartState) => state.specs; + +/** @internal */ +export const getTree = createCachedSelector( + [getSpecs], + (specs): HierarchyOfArrays => { + const pieSpecs = getSpecsFromStore(specs, ChartTypes.Partition, SpecTypes.Series); + if (pieSpecs.length !== 1) { + return []; + } + const { data, valueAccessor, layers } = pieSpecs[0]; + return getHierarchyOfArrays(data, valueAccessor, [() => null, ...layers.map(({ groupByRollup }) => groupByRollup)]); + }, +)((state) => state.chartId); diff --git a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts index c2b1a33a5442..35dcf3bb0758 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.test.ts @@ -23,16 +23,11 @@ import { AxisSpec, BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { Position } from '../../../utils/commons'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; +import { LegendItem } from '../../../commons/legend'; const nullDisplayValue = { - formatted: { - y0: null, - y1: null, - }, - raw: { - y0: null, - y1: null, - }, + formatted: null, + raw: null, }; const seriesCollectionValue1a = { seriesIdentifier: { @@ -132,22 +127,15 @@ describe('Legends', () => { it('compute legend for a single series', () => { seriesCollection.set('seriesCollectionValue1a', seriesCollectionValue1a); const legend = computeLegend(seriesCollection, seriesCollectionMap, specs, 'violet', axesSpecs); - const expected = [ + const expected: LegendItem[] = [ { color: 'red', - name: 'Spec 1 title', - seriesIdentifier: { - seriesKeys: ['y1'], - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - key: 'seriesCollectionValue1a', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue1a', - displayValue: nullDisplayValue, - banded: undefined, + label: 'Spec 1 title', + childId: 'y1', + seriesIdentifier: seriesCollectionValue1a.seriesIdentifier, + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, ]; expect(Array.from(legend.values())).toEqual(expected); @@ -156,38 +144,24 @@ describe('Legends', () => { seriesCollection.set('seriesCollectionValue1a', seriesCollectionValue1a); seriesCollection.set('seriesCollectionValue1b', seriesCollectionValue1b); const legend = computeLegend(seriesCollection, seriesCollectionMap, specs, 'violet', axesSpecs); - const expected = [ + const expected: LegendItem[] = [ { color: 'red', - name: 'Spec 1 title', - seriesIdentifier: { - seriesKeys: ['y1'], - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - key: 'seriesCollectionValue1a', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue1a', - displayValue: nullDisplayValue, - banded: undefined, + label: 'Spec 1 title', + seriesIdentifier: seriesCollectionValue1a.seriesIdentifier, + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, { color: 'blue', - name: 'a - b', - seriesIdentifier: { - seriesKeys: ['a', 'b', 'y1'], - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - key: 'seriesCollectionValue1b', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue1b', - displayValue: nullDisplayValue, - banded: undefined, + label: 'a - b', + seriesIdentifier: seriesCollectionValue1b.seriesIdentifier, + childId: 'y1', + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, ]; expect(Array.from(legend.values())).toEqual(expected); @@ -196,38 +170,24 @@ describe('Legends', () => { seriesCollection.set('seriesCollectionValue1a', seriesCollectionValue1a); seriesCollection.set('seriesCollectionValue2a', seriesCollectionValue2a); const legend = computeLegend(seriesCollection, seriesCollectionMap, specs, 'violet', axesSpecs); - const expected = [ + const expected: LegendItem[] = [ { color: 'red', - name: 'Spec 1 title', - seriesIdentifier: { - seriesKeys: ['y1'], - specId: 'spec1', - splitAccessors: new Map(), - yAccessor: 'y1', - key: 'seriesCollectionValue1a', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue1a', - displayValue: nullDisplayValue, - banded: undefined, + label: 'Spec 1 title', + childId: 'y1', + seriesIdentifier: seriesCollectionValue1a.seriesIdentifier, + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, { color: 'green', - name: 'spec2', - seriesIdentifier: { - seriesKeys: ['y1'], - specId: 'spec2', - yAccessor: 'y1', - splitAccessors: new Map(), - key: 'seriesCollectionValue2a', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue2a', - displayValue: nullDisplayValue, - banded: undefined, + label: 'spec2', + childId: 'y1', + seriesIdentifier: seriesCollectionValue2a.seriesIdentifier, + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, ]; expect(Array.from(legend.values())).toEqual(expected); @@ -235,28 +195,21 @@ describe('Legends', () => { it('empty legend for missing spec', () => { seriesCollection.set('seriesCollectionValue2b', seriesCollectionValue2b); const legend = computeLegend(seriesCollection, seriesCollectionMap, specs, 'violet', axesSpecs); - expect(legend.size).toEqual(0); + expect(legend.length).toEqual(0); }); it('compute legend with default color for missing series color', () => { seriesCollection.set('seriesCollectionValue1a', seriesCollectionValue1a); const emptyColorMap = new Map(); const legend = computeLegend(seriesCollection, emptyColorMap, specs, 'violet', axesSpecs); - const expected = [ + const expected: LegendItem[] = [ { color: 'violet', - name: 'Spec 1 title', - banded: undefined, - seriesIdentifier: { - seriesKeys: ['y1'], - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - key: 'seriesCollectionValue1a', - }, - isSeriesVisible: true, - isLegendItemVisible: true, - key: 'seriesCollectionValue1a', - displayValue: nullDisplayValue, + label: 'Spec 1 title', + childId: 'y1', + seriesIdentifier: seriesCollectionValue1a.seriesIdentifier, + isItemHidden: false, + isSeriesHidden: false, + defaultExtra: nullDisplayValue, }, ]; expect(Array.from(legend.values())).toEqual(expected); @@ -271,7 +224,7 @@ describe('Legends', () => { const legend = computeLegend(seriesCollection, emptyColorMap, specs, 'violet', axesSpecs); - const visibility = [...legend.values()].map((item) => item.isSeriesVisible); + const visibility = [...legend.values()].map((item) => !item.isSeriesHidden); expect(visibility).toEqual([true, true, true]); }); @@ -286,7 +239,7 @@ describe('Legends', () => { const legend = computeLegend(seriesCollection, emptyColorMap, specs, 'violet', axesSpecs, deselectedDataSeries); - const visibility = [...legend.values()].map((item) => item.isSeriesVisible); + const visibility = [...legend.values()].map((item) => !item.isSeriesHidden); expect(visibility).toEqual([false, false, true]); }); it('returns the right series name for a color series', () => { diff --git a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts index 85dd86ce518f..13594ce8d233 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/legend/legend.ts @@ -16,40 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -import { getAxesSpecForSpecId, LastValues, getSpecsById } from '../state/utils'; +import { getAxesSpecForSpecId, getSpecsById } from '../state/utils'; import { identity, Color } from '../../../utils/commons'; import { SeriesCollectionValue, getSeriesIndex, getSortedDataSeriesColorsValuesMap, getSeriesName, - XYChartSeriesIdentifier, - SeriesKey, } from '../utils/series'; +import { SeriesKey, SeriesIdentifier } from '../../../commons/series_id'; import { AxisSpec, BasicSeriesSpec, Postfixes, isAreaSeriesSpec, isBarSeriesSpec } from '../utils/specs'; import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; import { BandedAccessorType } from '../../../utils/geometry'; +import { LegendItem } from '../../../commons/legend'; -interface FormattedLastValues { +/** @internal */ +export interface FormattedLastValues { y0: number | string | null; y1: number | string | null; } -/** @internal */ -export type LegendItem = Postfixes & { - key: SeriesKey; - color: Color; - name: string; - seriesIdentifier: XYChartSeriesIdentifier; - isSeriesVisible?: boolean; - banded?: boolean; - isLegendItemVisible?: boolean; - displayValue: { - raw: LastValues; - formatted: FormattedLastValues; - }; -}; - function getPostfix(spec: BasicSeriesSpec): Postfixes { if (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec)) { const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec; @@ -63,15 +49,10 @@ function getPostfix(spec: BasicSeriesSpec): Postfixes { } /** @internal */ -export function getItemLabel( - { banded, name, y1AccessorFormat, y0AccessorFormat }: LegendItem, - yAccessor: BandedAccessorType, -) { - if (!banded) { - return name; - } - - return yAccessor === BandedAccessorType.Y1 ? `${name}${y1AccessorFormat}` : `${name}${y0AccessorFormat}`; +export function getBandedLegendItemLabel(name: string, yAccessor: BandedAccessorType, postfixes: Postfixes) { + return yAccessor === BandedAccessorType.Y1 + ? `${name}${postfixes.y1AccessorFormat}` + : `${name}${postfixes.y0AccessorFormat}`; } /** @internal */ @@ -81,9 +62,9 @@ export function computeLegend( specs: BasicSeriesSpec[], defaultColor: string, axesSpecs: AxisSpec[], - deselectedDataSeries: XYChartSeriesIdentifier[] = [], -): Map { - const legendItems: Map = new Map(); + deselectedDataSeries: SeriesIdentifier[] = [], +): LegendItem[] { + const legendItems: LegendItem[] = []; const sortedCollection = getSortedDataSeriesColorsValuesMap(seriesCollection); sortedCollection.forEach((series, key) => { @@ -92,39 +73,46 @@ export function computeLegend( const color = seriesColors.get(key) || defaultColor; const hasSingleSeries = seriesCollection.size === 1; const name = getSeriesName(seriesIdentifier, hasSingleSeries, false, spec); - const isSeriesVisible = deselectedDataSeries ? getSeriesIndex(deselectedDataSeries, seriesIdentifier) < 0 : true; + const isSeriesHidden = deselectedDataSeries ? getSeriesIndex(deselectedDataSeries, seriesIdentifier) >= 0 : false; if (name === '' || !spec) { return; } + const postFixes = getPostfix(spec); + const labelY1 = banded ? getBandedLegendItemLabel(name, BandedAccessorType.Y1, postFixes) : name; // Use this to get axis spec w/ tick formatter const { yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId); const formatter = yAxis ? yAxis.tickFormat : identity; const { hideInLegend } = spec; - const legendItem: LegendItem = { - key, + legendItems.push({ color, - name, - banded, + label: labelY1, seriesIdentifier, - isSeriesVisible, - isLegendItemVisible: !hideInLegend, - displayValue: { - raw: { - y0: lastValue && lastValue.y0 !== null ? lastValue.y0 : null, - y1: lastValue && lastValue.y1 !== null ? lastValue.y1 : null, - }, - formatted: { - y0: lastValue && lastValue.y0 !== null ? formatter(lastValue.y0) : null, - y1: lastValue && lastValue.y1 !== null ? formatter(lastValue.y1) : null, - }, + childId: BandedAccessorType.Y1, + isSeriesHidden, + isItemHidden: hideInLegend, + defaultExtra: { + raw: lastValue && lastValue.y1 !== null ? lastValue.y1 : null, + formatted: lastValue && lastValue.y1 !== null ? formatter(lastValue.y1) : null, }, - ...getPostfix(spec), - }; - - legendItems.set(key, legendItem); + }); + if (banded) { + const labelY0 = getBandedLegendItemLabel(name, BandedAccessorType.Y0, postFixes); + legendItems.push({ + color, + label: labelY0, + seriesIdentifier, + childId: BandedAccessorType.Y0, + isSeriesHidden, + isItemHidden: hideInLegend, + defaultExtra: { + raw: lastValue && lastValue.y0 !== null ? lastValue.y0 : null, + formatted: lastValue && lastValue.y0 !== null ? formatter(lastValue.y0) : null, + }, + }); + } }); return legendItems; } diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts index 009c191d3ecf..8ba5236562f0 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -19,7 +19,7 @@ import { getGeometryStateStyle } from '../../rendering/rendering'; import { AreaGeometry } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; -import { LegendItem } from '../../legend/legend'; +import { LegendItem } from '../../../../commons/legend'; import { withClip, withContext } from '../../../../renderers/canvas'; import { renderPoints } from './points'; import { renderLinePaths, renderAreaPath } from './primitives/path'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts index bd51af40a274..ba0f473ca383 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -21,7 +21,7 @@ import { BarGeometry } from '../../../../utils/geometry'; import { buildBarStyles } from './styles/bar'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/rendering'; -import { LegendItem } from '../../legend/legend'; +import { LegendItem } from '../../../../commons/legend'; import { renderRect } from './primitives/rect'; import { Rect } from '../../../../geoms/types'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts index fad0902a159a..226a8867f176 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -19,7 +19,7 @@ import { getGeometryStateStyle } from '../../rendering/rendering'; import { LineGeometry } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; -import { LegendItem } from '../../legend/legend'; +import { LegendItem } from '../../../../commons/legend'; import { withContext } from '../../../../renderers/canvas'; import { renderPoints } from './points'; import { renderLinePaths } from './primitives/path'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 52f5db4b65e6..d18fe2bc77e5 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -31,7 +31,7 @@ import { AnnotationId, AxisId } from '../../../../utils/ids'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme } from '../../../../utils/themes/theme'; import { AnnotationDimensions } from '../../annotations/annotation_utils'; -import { LegendItem } from '../../legend/legend'; +import { LegendItem } from '../../../../commons/legend'; 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'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss index 37fdd8f6f046..cb4b3f63a30f 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/_highlighter.scss @@ -10,6 +10,14 @@ height: 100%; } -.echHighlighter__rect { +.echHighlighterOverlay__fill { fill: transparentize($euiColorGhost, 0.8); } + +.echHighlighterOverlay__stroke { + stroke: transparentize($euiColorGhost, 0.8); +} + +.echHighlighter__mask { + fill: transparentize($euiColorEmptyShade, 0.5); +} diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index d241706f8c9c..7537d76cfacb 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -78,7 +78,7 @@ class HighlighterComponent extends React.Component { y={y} width={geom.width} height={geom.height} - className="echHighlighter__rect" + className="echHighlighterOverlay__fill" clipPath={`url(#${clipPathId})`} /> ); diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts index b0835e225f19..c70a50029371 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -29,7 +29,7 @@ import { mergePartial, RecursivePartial } from '../../../utils/commons'; import { BarGeometry, PointGeometry } from '../../../utils/geometry'; import { MockDataSeries } from '../../../mocks'; import { MockScale } from '../../../mocks/scale'; -import { LegendItem } from '../legend/legend'; +import { LegendItem } from '../../../commons/legend'; describe('Rendering utils', () => { test('check if point is in geometry', () => { @@ -121,21 +121,13 @@ describe('Rendering utils', () => { key: 'somekey', }; const highlightedLegendItem: LegendItem = { - key: 'somekey', color: '', - name: '', + label: '', seriesIdentifier, - isSeriesVisible: true, - isLegendItemVisible: true, - displayValue: { - formatted: { - y0: null, - y1: null, - }, - raw: { - y0: null, - y1: null, - }, + isSeriesHidden: false, + defaultExtra: { + formatted: null, + raw: null, }, }; diff --git a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.ts b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.ts index a25924ed566c..665a4890f180 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/rendering/rendering.ts @@ -41,7 +41,7 @@ import { BandedAccessorType, } from '../../../utils/geometry'; import { mergePartial, Color } from '../../../utils/commons'; -import { LegendItem } from '../legend/legend'; +import { LegendItem } from '../../../commons/legend'; /** @internal */ export function mutableIndexedGeometryMapUpsert( diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts index ca968bb46953..ddea1debb62a 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.specs.test.ts @@ -43,7 +43,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); const legendItems = getLegendItemsSelector(store.getState()); - const names = [...legendItems.values()].map((item) => item.name); + const names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['A', 'B', 'C']); }); it('the legend respect the insert order [B, A, C]', () => { @@ -53,7 +53,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(upsertSpec(MockSeriesSpec.bar({ id: 'C', data }))); store.dispatch(specParsed()); const legendItems = getLegendItemsSelector(store.getState()); - const names = [...legendItems.values()].map((item) => item.name); + const names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['B', 'A', 'C']); }); it('the legend respect the order when changing properties of existing specs', () => { @@ -64,7 +64,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); let legendItems = getLegendItemsSelector(store.getState()); - let names = [...legendItems.values()].map((item) => item.name); + let names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['A', 'B', 'C']); store.dispatch(specParsing()); @@ -74,7 +74,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); legendItems = getLegendItemsSelector(store.getState()); - names = [...legendItems.values()].map((item) => item.name); + names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['A', 'B updated', 'C']); }); it('the legend respect the order when changing the order of the specs', () => { @@ -85,7 +85,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); let legendItems = getLegendItemsSelector(store.getState()); - let names = [...legendItems.values()].map((item) => item.name); + let names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['A', 'B', 'C']); store.dispatch(specParsing()); @@ -95,7 +95,7 @@ describe('XYChart - specs ordering', () => { store.dispatch(specParsed()); legendItems = getLegendItemsSelector(store.getState()); - names = [...legendItems.values()].map((item) => item.name); + names = [...legendItems.values()].map((item) => item.label); expect(names).toEqual(['B', 'A', 'C']); }); }); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts index e67bcdba8519..15e4fbbc4125 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.test.ts @@ -30,7 +30,7 @@ import { ScaleType, ScaleContinuous, ScaleBand } from '../../../scales'; import { IndexedGeometry, GeometryValue, BandedAccessorType } from '../../../utils/geometry'; import { AxisTicksDimensions, isDuplicateAxis } from '../utils/axis_utils'; import { AxisId } from '../../../utils/ids'; -import { LegendItem } from '../legend/legend'; +import { LegendItem } from '../../../commons/legend'; import { ChartTypes } from '../..'; import { SpecTypes, TooltipValue, TooltipType } from '../../../specs/settings'; @@ -61,48 +61,28 @@ describe.skip('Chart Store', () => { }; const firstLegendItem: LegendItem = { - key: 'color1', color: 'foo', - name: 'bar', + label: 'bar', seriesIdentifier: { specId: SPEC_ID, - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: [], key: 'color1', }, - displayValue: { - raw: { - y1: null, - y0: null, - }, - formatted: { - y1: 'formatted-last', - y0: null, - }, + defaultExtra: { + raw: null, + formatted: 'formatted-last', }, }; const secondLegendItem: LegendItem = { - key: 'color2', color: 'baz', - name: 'qux', + label: 'qux', seriesIdentifier: { specId: SPEC_ID, - yAccessor: '', - splitAccessors: new Map(), - seriesKeys: [], key: 'color2', }, - displayValue: { - raw: { - y1: null, - y0: null, - }, - formatted: { - y1: 'formatted-last', - y0: null, - }, + defaultExtra: { + raw: null, + formatted: 'formatted-last', }, }; beforeEach(() => { @@ -303,14 +283,14 @@ describe.skip('Chart Store', () => { test.skip('can get highlighted legend item', () => { store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.highlightedLegendItemKey.set(null); expect(store.highlightedLegendItem.get()).toBe(null); - store.highlightedLegendItemKey.set(secondLegendItem.key); + store.highlightedLegendItemKey.set(secondLegendItem.seriesIdentifier.key); expect(store.highlightedLegendItem.get()).toEqual(secondLegendItem); }); @@ -320,16 +300,16 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.highlightedLegendItemKey.set(null); - store.onLegendItemOver(firstLegendItem.key); - expect(store.highlightedLegendItemKey.get()).toBe(firstLegendItem.key); + store.onLegendItemOver(firstLegendItem.seriesIdentifier.key); + expect(store.highlightedLegendItemKey.get()).toBe(firstLegendItem.seriesIdentifier.key); store.setOnLegendItemOverListener(legendListener); - store.onLegendItemOver(secondLegendItem.key); + store.onLegendItemOver(secondLegendItem.seriesIdentifier.key); expect(legendListener).toBeCalledWith(secondLegendItem.seriesIdentifier); store.onLegendItemOver(null); @@ -342,7 +322,7 @@ describe.skip('Chart Store', () => { test.skip('can respond to legend item mouseout event', () => { const outListener = jest.fn((): undefined => undefined); - store.highlightedLegendItemKey.set(firstLegendItem.key); + store.highlightedLegendItemKey.set(firstLegendItem.seriesIdentifier.key); store.setOnLegendItemOutListener(outListener); @@ -363,24 +343,24 @@ describe.skip('Chart Store', () => { store.setOnLegendItemOverListener(legendListener); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.deselectedDataSeries = []; store.highlightedLegendItemKey.set(null); - store.toggleSeriesVisibility(firstLegendItem.key); + store.toggleSeriesVisibility(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([firstLegendItem.seriesIdentifier]); expect(store.highlightedLegendItemKey.get()).toBe(null); - store.onLegendItemOver(firstLegendItem.key); + store.onLegendItemOver(firstLegendItem.seriesIdentifier.key); expect(store.highlightedLegendItemKey.get()).toBe(null); store.onLegendItemOut(); - store.toggleSeriesVisibility(firstLegendItem.key); - expect(store.highlightedLegendItemKey.get()).toEqual(firstLegendItem.key); + store.toggleSeriesVisibility(firstLegendItem.seriesIdentifier.key); + expect(store.highlightedLegendItemKey.get()).toEqual(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([]); - store.onLegendItemOver(firstLegendItem.key); - expect(store.highlightedLegendItemKey.get()).toBe(firstLegendItem.key); + store.onLegendItemOver(firstLegendItem.seriesIdentifier.key); + expect(store.highlightedLegendItemKey.get()).toBe(firstLegendItem.seriesIdentifier.key); store.removeOnLegendItemOutListener(); }); @@ -391,26 +371,26 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.selectedLegendItemKey.set(null); store.onLegendItemClickListener = undefined; - store.onLegendItemClick(firstLegendItem.key); + store.onLegendItemClick(firstLegendItem.seriesIdentifier.key); // TODO reenable this after re-configuring onLegendItemClick - // expect(store.selectedLegendItemKey.get()).toBe(firstLegendItem.key); + // expect(store.selectedLegendItemKey.get()).toBe(firstLegendItem.seriesIdentifier.key); expect(legendListener).not.toBeCalled(); store.setOnLegendItemClickListener(legendListener); - store.onLegendItemClick(firstLegendItem.key); + store.onLegendItemClick(firstLegendItem.seriesIdentifier.key); // TODO reenable this after re-configuring onLegendItemClick // expect(store.selectedLegendItemKey.get()).toBe(null); // expect(legendListener).toBeCalledWith(null); // store.setOnLegendItemClickListener(legendListener); - // store.onLegendItemClick(secondLegendItem.key); - // expect(store.selectedLegendItemKey.get()).toBe(secondLegendItem.key); + // store.onLegendItemClick(secondLegendItem.seriesIdentifier.key); + // expect(store.selectedLegendItemKey.get()).toBe(secondLegendItem.seriesIdentifier.key); expect(legendListener).toBeCalledWith(firstLegendItem.seriesIdentifier); }); @@ -420,8 +400,8 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.selectedLegendItemKey.set(null); store.onLegendItemPlusClickListener = undefined; @@ -433,7 +413,7 @@ describe.skip('Chart Store', () => { store.onLegendItemPlusClick(); expect(legendListener).toBeCalledWith(null); - store.selectedLegendItemKey.set(firstLegendItem.key); + store.selectedLegendItemKey.set(firstLegendItem.seriesIdentifier.key); store.onLegendItemPlusClick(); expect(legendListener).toBeCalledWith(firstLegendItem.seriesIdentifier); }); @@ -444,8 +424,8 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.selectedLegendItemKey.set(null); store.onLegendItemMinusClickListener = undefined; @@ -457,7 +437,7 @@ describe.skip('Chart Store', () => { store.onLegendItemMinusClick(); expect(legendListener).toBeCalledWith(null); - store.selectedLegendItemKey.set(firstLegendItem.key); + store.selectedLegendItemKey.set(firstLegendItem.seriesIdentifier.key); store.onLegendItemMinusClick(); expect(legendListener).toBeCalledWith(firstLegendItem.seriesIdentifier); }); @@ -468,8 +448,8 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.deselectedDataSeries = []; store.computeChart = computeChart; @@ -479,12 +459,12 @@ describe.skip('Chart Store', () => { expect(computeChart).not.toBeCalled(); store.deselectedDataSeries = [firstLegendItem.seriesIdentifier, secondLegendItem.seriesIdentifier]; - store.toggleSeriesVisibility(firstLegendItem.key); + store.toggleSeriesVisibility(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([secondLegendItem.seriesIdentifier]); expect(computeChart).toBeCalled(); store.deselectedDataSeries = [firstLegendItem.seriesIdentifier]; - store.toggleSeriesVisibility(firstLegendItem.key); + store.toggleSeriesVisibility(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([]); }); @@ -494,8 +474,8 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.deselectedDataSeries = []; store.computeChart = computeChart; @@ -504,10 +484,10 @@ describe.skip('Chart Store', () => { expect(store.deselectedDataSeries).toEqual([]); expect(computeChart).not.toBeCalled(); - store.toggleSingleSeries(firstLegendItem.key); + store.toggleSingleSeries(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([firstLegendItem.seriesIdentifier]); - store.toggleSingleSeries(firstLegendItem.key); + store.toggleSingleSeries(firstLegendItem.seriesIdentifier.key); expect(store.deselectedDataSeries).toEqual([secondLegendItem.seriesIdentifier]); }); @@ -736,27 +716,27 @@ describe.skip('Chart Store', () => { beforeEach(() => { store.computeChart = jest.fn(); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); }); it('should set color override', () => { - store.setSeriesColor(firstLegendItem.key, 'red'); + store.setSeriesColor(firstLegendItem.seriesIdentifier.key, 'red'); expect(store.computeChart).toBeCalled(); - expect(store.seriesColorOverrides.get(firstLegendItem.key)).toBe('red'); + expect(store.seriesColorOverrides.get(firstLegendItem.seriesIdentifier.key)).toBe('red'); }); it('should not set color override with empty color', () => { - store.setSeriesColor(firstLegendItem.key, ''); + store.setSeriesColor(firstLegendItem.seriesIdentifier.key, ''); expect(store.computeChart).not.toBeCalled(); - expect(store.seriesColorOverrides.get(firstLegendItem.key)).toBeUndefined(); + expect(store.seriesColorOverrides.get(firstLegendItem.seriesIdentifier.key)).toBeUndefined(); }); it('should not set color override with empty key', () => { store.setSeriesColor('', 'red'); expect(store.computeChart).not.toBeCalled(); - expect(store.seriesColorOverrides.get(firstLegendItem.key)).toBeUndefined(); + expect(store.seriesColorOverrides.get(firstLegendItem.seriesIdentifier.key)).toBeUndefined(); }); }); @@ -874,8 +854,8 @@ describe.skip('Chart Store', () => { }); store.legendItems = new Map([ - [firstLegendItem.key, firstLegendItem], - [secondLegendItem.key, secondLegendItem], + [firstLegendItem.seriesIdentifier.key, firstLegendItem], + [secondLegendItem.seriesIdentifier.key, secondLegendItem], ]); store.selectedLegendItemKey.set(null); store.onCursorUpdateListener = undefined; diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx index fb12a166f570..a6f1a756b9f4 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.tsx @@ -22,13 +22,12 @@ import { Highlighter } from '../renderer/dom/highlighter'; import { Crosshair } from '../renderer/dom/crosshair'; import { BrushTool } from '../renderer/dom/brush'; import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; -import { TooltipLegendValue } from '../tooltip/tooltip'; import { ChartTypes } from '../..'; import { AnnotationTooltip } from '../renderer/dom/annotation_tooltips'; import { isBrushAvailableSelector } from './selectors/is_brush_available'; import { isChartEmptySelector } from './selectors/is_chart_empty'; import { computeLegendSelector } from './selectors/compute_legend'; -import { getLegendTooltipValuesSelector } from './selectors/get_legend_tooltip_values'; +import { getHighlightedValuesSelector } from './selectors/get_highlighted_values'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; import { isBrushingSelector } from './selectors/is_brushing'; import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; @@ -36,12 +35,14 @@ import { getTooltipInfoSelector } from './selectors/get_tooltip_values_highlight import { htmlIdGenerator } from '../../../utils/commons'; import { Tooltip } from '../../../components/tooltip'; import { getTooltipAnchorPositionSelector } from './selectors/get_tooltip_position'; -import { SeriesKey } from '../utils/series'; +import { SeriesKey } from '../../../commons/series_id'; import { createOnElementClickCaller } from './selectors/on_element_click_caller'; import { createOnElementOverCaller } from './selectors/on_element_over_caller'; import { createOnElementOutCaller } from './selectors/on_element_out_caller'; import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; +import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels'; +import { LegendItemExtraValues } from '../../../commons/legend'; /** @internal */ export class XYAxisChartState implements InternalChartState { @@ -72,11 +73,14 @@ export class XYAxisChartState implements InternalChartState { isChartEmpty(globalState: GlobalChartState) { return isChartEmptySelector(globalState); } + getLegendItemsLabels(globalState: GlobalChartState) { + return getLegendItemsLabelsSelector(globalState); + } getLegendItems(globalState: GlobalChartState) { return computeLegendSelector(globalState); } - getLegendItemsValues(globalState: GlobalChartState): Map { - return getLegendTooltipValuesSelector(globalState); + getLegendExtraValues(globalState: GlobalChartState): Map { + return getHighlightedValuesSelector(globalState); } chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { return ( diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts index cf163a2683e0..0d7e4939f999 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/compute_legend.ts @@ -21,10 +21,10 @@ import { computeSeriesDomainsSelector } from './compute_series_domains'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getSeriesColorsSelector } from './get_series_color_map'; -import { computeLegend, LegendItem } from '../../legend/legend'; +import { computeLegend } from '../../legend/legend'; +import { LegendItem } from '../../../../commons/legend'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { SeriesKey } from '../../utils/series'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; @@ -38,14 +38,7 @@ export const computeLegendSelector = createCachedSelector( getAxisSpecsSelector, getDeselectedSeriesSelector, ], - ( - seriesSpecs, - seriesDomainsAndData, - chartTheme, - seriesColors, - axesSpecs, - deselectedDataSeries, - ): Map => { + (seriesSpecs, seriesDomainsAndData, chartTheme, seriesColors, axesSpecs, deselectedDataSeries): LegendItem[] => { return computeLegend( seriesDomainsAndData.seriesCollection, seriesColors, diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts index edb5830f610c..30c07e8b95d3 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_series.ts @@ -19,7 +19,7 @@ import createCachedSelector from 're-reselect'; import { GlobalChartState } from '../../../../state/chart_state'; import { computeLegendSelector } from './compute_legend'; -import { LegendItem } from '../../../../chart_types/xy_chart/legend/legend'; +import { LegendItem } from '../../../../commons/legend'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; const getHighlightedLegendItemKey = (state: GlobalChartState) => state.interactions.highlightedLegendItemKey; @@ -31,6 +31,6 @@ export const getHighlightedSeriesSelector = createCachedSelector( if (!highlightedLegendItemKey) { return undefined; } - return legendItems.get(highlightedLegendItemKey); + return legendItems.find(({ seriesIdentifier: { key } }) => key === highlightedLegendItemKey); }, )(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts similarity index 75% rename from packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts rename to packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts index 1c4f6b0fdb2f..0227e2830426 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_tooltip_values.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_highlighted_values.ts @@ -17,15 +17,16 @@ * under the License. */ import createCachedSelector from 're-reselect'; -import { getSeriesTooltipValues, TooltipLegendValue } from '../../tooltip/tooltip'; +import { getHighligthedValues } from '../../tooltip/tooltip'; import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { SeriesKey } from '../../utils/series'; +import { SeriesKey } from '../../../../commons/series_id'; +import { LegendItemExtraValues } from '../../../../commons/legend'; /** @internal */ -export const getLegendTooltipValuesSelector = createCachedSelector( +export const getHighlightedValuesSelector = createCachedSelector( [getTooltipInfoSelector], - ({ values }): Map => { - return getSeriesTooltipValues(values); + ({ values }): Map => { + return getHighligthedValues(values); }, )(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..3e9cdc21954e --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,37 @@ +/* + * 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 { computeLegendSelector } from './compute_legend'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; + +/** @internal */ +export const getLegendItemsLabelsSelector = createCachedSelector( + [computeLegendSelector, getSettingsSpecSelector], + (legendItems, { showLegendExtra }): LegendItemLabel[] => { + return legendItems.map(({ label, defaultExtra }) => { + if (defaultExtra?.formatted != null) { + return { label: `${label}${showLegendExtra ? defaultExtra.formatted : ''}`, depth: 0 }; + } else { + return { label, depth: 0 }; + } + }); + }, +)(getChartIdSelector); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts index 3756a6d3f48c..45e2aeb10f0a 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/selectors/get_series_color_map.ts @@ -19,7 +19,8 @@ import createCachedSelector from 're-reselect'; import { computeSeriesDomainsSelector } from './compute_series_domains'; import { getSeriesSpecsSelector } from './get_specs'; -import { getSeriesColors, SeriesKey } from '../../utils/series'; +import { getSeriesColors } from '../../utils/series'; +import { SeriesKey } from '../../../../commons/series_id'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getCustomSeriesColors } from '../utils'; diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils.test.ts index 6264a76d5df0..ab753e092936 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/utils.test.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils.test.ts @@ -47,7 +47,6 @@ import { import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; import { mergeYCustomDomainsByGroupId } from './selectors/merge_y_custom_domains'; import { updateDeselectedDataSeries } from './utils'; -import { LegendItem } from '../legend/legend'; import { ChartTypes } from '../..'; import { MockSeriesSpecs, MockSeriesSpec } from '../../../mocks/specs'; import { MockSeriesCollection } from '../../../mocks/series/series_identifiers'; @@ -55,6 +54,7 @@ import { SeededDataGenerator } from '../../../mocks/utils'; import { SeriesCollectionValue, getSeriesIndex, getSeriesColors } from '../utils/series'; import { SpecTypes } from '../../../specs/settings'; import { ColorOverrides } from '../../../state/chart_state'; +import { LegendItem } from '../../../commons/legend'; describe('Chart State utils', () => { const emptySeriesOverrides: ColorOverrides = { @@ -1447,67 +1447,53 @@ describe('Chart State utils', () => { expect(bar2.stackAccessors).toEqual(['y', 'bar']); }); test('displays no data availble if chart is empty', () => { - const legendItems1 = new Map(); - legendItems1.set('specId:{bars},colors:{a}', { - key: 'specId:{bars},colors:{a}', - color: '#1EA593', - name: 'a', - seriesIdentifier: { - specId: 'bars', - seriesKeys: ['a'], - key: '', - splitAccessors: new Map(), - yAccessor: 'y1', + const legendItems1: LegendItem[] = [ + { + color: '#1EA593', + label: 'a', + seriesIdentifier: { + key: 'specId:{bars},colors:{a}', + specId: 'bars', + }, + defaultExtra: { raw: 6, formatted: '6.00' }, + isSeriesHidden: true, }, - displayValue: { raw: { y0: null, y1: 6 }, formatted: { y0: null, y1: '6.00' } }, - isSeriesVisible: false, - }); - legendItems1.set('specId:{bars},colors:{b}', { - key: 'specId:{bars},colors:{b}', - color: '#2B70F7', - name: 'b', - seriesIdentifier: { - specId: 'bars', - seriesKeys: ['b'], - key: '', - splitAccessors: new Map(), - yAccessor: 'y1', + { + color: '#2B70F7', + label: 'b', + seriesIdentifier: { + key: 'specId:{bars},colors:{b}', + specId: 'bars', + }, + defaultExtra: { raw: 2, formatted: '2.00' }, + isSeriesHidden: true, }, - displayValue: { raw: { y0: null, y1: 2 }, formatted: { y0: null, y1: '2.00' } }, - isSeriesVisible: false, - }); + ]; expect(isAllSeriesDeselected(legendItems1)).toBe(true); }); test('displays data availble if chart is not empty', () => { - const legendItems2 = new Map(); - legendItems2.set('specId:{bars},colors:{a}', { - key: 'specId:{bars},colors:{a}', - color: '#1EA593', - name: 'a', - seriesIdentifier: { - specId: 'bars', - seriesKeys: ['a'], - key: '', - splitAccessors: new Map(), - yAccessor: 'y1', + const legendItems2: LegendItem[] = [ + { + color: '#1EA593', + label: 'a', + seriesIdentifier: { + key: 'specId:{bars},colors:{a}', + specId: 'bars', + }, + defaultExtra: { raw: 6, formatted: '6.00' }, + isSeriesHidden: false, }, - displayValue: { raw: { y0: null, y1: 6 }, formatted: { y0: null, y1: '6.00' } }, - isSeriesVisible: true, - }); - legendItems2.set('specId:{bars},colors:{b}', { - key: 'specId:{bars},colors:{b}', - color: '#2B70F7', - name: 'b', - seriesIdentifier: { - specId: 'bars', - seriesKeys: ['b'], - key: '', - splitAccessors: new Map(), - yAccessor: 'y1', + { + color: '#2B70F7', + label: 'b', + seriesIdentifier: { + key: 'specId:{bars},colors:{b}', + specId: 'bars', + }, + defaultExtra: { raw: 2, formatted: '2.00' }, + isSeriesHidden: true, }, - displayValue: { raw: { y0: null, y1: 2 }, formatted: { y0: null, y1: '2.00' } }, - isSeriesVisible: false, - }); + ]; expect(isAllSeriesDeselected(legendItems2)).toBe(false); }); }); diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/utils.ts b/packages/osd-charts/src/chart_types/xy_chart/state/utils.ts index afa379da24a7..cb5594c81d68 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/state/utils.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/state/utils.ts @@ -32,8 +32,8 @@ import { getSeriesKey, RawDataSeries, XYChartSeriesIdentifier, - SeriesKey, } from '../utils/series'; +import { SeriesKey, SeriesIdentifier } from '../../../commons/series_id'; import { AreaSeriesSpec, AxisSpec, @@ -57,7 +57,7 @@ import { Domain } from '../../../utils/domain'; import { GroupId, SpecId } from '../../../utils/ids'; import { Scale } from '../../../scales'; import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, IndexedGeometry } from '../../../utils/geometry'; -import { LegendItem } from '../legend/legend'; +import { LegendItem } from '../../../commons/legend'; import { Spec } from '../../../specs'; const MAX_ANIMATABLE_BARS = 300; @@ -182,6 +182,7 @@ export function getCustomSeriesColors( return updatedCustomSeriesColors; } +/** @internal */ export interface LastValues { y0: number | null; y1: number | null; @@ -240,7 +241,7 @@ function getLastValues(formattedDataSeries: { export function computeSeriesDomains( seriesSpecs: BasicSeriesSpec[], customYDomainsByGroupId: Map = new Map(), - deselectedDataSeries: XYChartSeriesIdentifier[] = [], + deselectedDataSeries: SeriesIdentifier[] = [], customXDomain?: DomainRange | Domain, ): SeriesDomainsAndData { const { splittedSeries, xValues, seriesCollection } = deselectedDataSeries @@ -731,9 +732,9 @@ export function isChartAnimatable(geometriesCounts: GeometriesCounts, animationE } /** @internal */ -export function isAllSeriesDeselected(legendItems: Map): boolean { - for (const [, legendItem] of legendItems) { - if (legendItem.isSeriesVisible) { +export function isAllSeriesDeselected(legendItems: LegendItem[]): boolean { + for (const legendItem of legendItems) { + if (!legendItem.isSeriesHidden) { return false; } } diff --git a/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts index 5e236712a08c..437565f2514b 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -26,37 +26,38 @@ import { } from '../utils/specs'; import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; import { getAccessorFormatLabel } from '../../../utils/accessor'; -import { getSeriesName, SeriesKey } from '../utils/series'; +import { getSeriesName } from '../utils/series'; +import { SeriesKey } from '../../../commons/series_id'; import { TooltipValue } from '../../../specs'; - -export interface TooltipLegendValue { - y0: any; - y1: any; -} +import { LegendItemExtraValues } from '../../../commons/legend'; export const Y0_ACCESSOR_POSTFIX = ' - lower'; export const Y1_ACCESSOR_POSTFIX = ' - upper'; /** @internal */ -export function getSeriesTooltipValues( +export function getHighligthedValues( tooltipValues: TooltipValue[], defaultValue?: string, -): Map { - // map from seriesKey to TooltipLegendValue - const seriesTooltipValues = new Map(); +): Map { + // map from seriesKey to LegendItemExtraValues + const seriesTooltipValues = new Map(); tooltipValues.forEach(({ value, seriesIdentifier, valueAccessor }) => { const seriesValue = defaultValue ? defaultValue : value; - const current = seriesTooltipValues.get(seriesIdentifier.key) || {}; - const tooltipValue: TooltipLegendValue = { - y0: defaultValue, - y1: defaultValue, - ...current, - }; - if (valueAccessor != null && (valueAccessor === 'y0' || valueAccessor === 'y1')) { - tooltipValue[valueAccessor] = seriesValue; + const current: LegendItemExtraValues = seriesTooltipValues.get(seriesIdentifier.key) ?? new Map(); + if (defaultValue) { + if (!current.has(BandedAccessorType.Y0)) { + current.set(BandedAccessorType.Y0, defaultValue); + } + if (!current.has(BandedAccessorType.Y1)) { + current.set(BandedAccessorType.Y1, defaultValue); + } + } + + if (valueAccessor != null && (valueAccessor === BandedAccessorType.Y0 || valueAccessor === BandedAccessorType.Y1)) { + current.set(valueAccessor, seriesValue); } - seriesTooltipValues.set(seriesIdentifier.key, tooltipValue); + seriesTooltipValues.set(seriesIdentifier.key, current); }); return seriesTooltipValues; } diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts index d17156531c6a..5ea0e5c03b9e 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/fit_function.ts @@ -25,24 +25,28 @@ import { ScaleType } from '../../../scales'; /** * Fit type that requires previous and/or next `non-nullable` values + * */ -export type BoundingFit = Exclude; +type BoundingFit = Exclude; /** * `DataSeriesDatum` with non-`null` value for `x` and `y1` + * @internal */ export type FullDataSeriesDatum = Omit & DeepNonNullable>; /** * Embellishes `FullDataSeriesDatum` with `fittingIndex` for ordinal scales + * @internal */ export type WithIndex = T & { fittingIndex: number }; /** * Returns `[x, y1]` values for a given datum with `fittingIndex` + * */ -export const getXYValues = ({ x, y1, fittingIndex }: WithIndex): [number, number] => { +const getXYValues = ({ x, y1, fittingIndex }: WithIndex): [number, number] => { return [typeof x === 'string' ? fittingIndex : x, y1]; }; diff --git a/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts b/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts index 2f56613d7126..c21fdd470a4f 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts +++ b/packages/osd-charts/src/chart_types/xy_chart/utils/series.ts @@ -27,9 +27,12 @@ import { ScaleType } from '../../../scales'; import { LastValues } from '../state/utils'; import { Datum, Color } from '../../../utils/commons'; import { ColorOverrides } from '../../../state/chart_state'; +import { SeriesIdentifier, SeriesKey } from '../../../commons/series_id'; +/** @internal */ export const SERIES_DELIMITER = ' - '; +/** @internal */ export interface FilledValues { /** the x value */ x?: number | string; @@ -50,6 +53,7 @@ export interface RawDataSeriesDatum { datum?: T; } +/** @internal */ export interface DataSeriesDatum { /** the x value */ x: number | string; @@ -67,24 +71,19 @@ export interface DataSeriesDatum { filled?: FilledValues; } -export type SeriesKey = string; - -export type SeriesIdentifier = { - specId: SpecId; - key: SeriesKey; -}; - export interface XYChartSeriesIdentifier extends SeriesIdentifier { yAccessor: string | number; splitAccessors: Map; // does the map have a size vs making it optional seriesKeys: (string | number)[]; } +/** @internal */ export type DataSeries = XYChartSeriesIdentifier & { // seriesColorKey: string; data: DataSeriesDatum[]; }; +/** @internal */ export type RawDataSeries = XYChartSeriesIdentifier & { // seriesColorKey: string; data: RawDataSeriesDatum[]; @@ -104,6 +103,7 @@ export interface DataSeriesCounts { areaSeries: number; } +/** @internal */ export type SeriesCollectionValue = { banded?: boolean; lastValue?: LastValues; @@ -112,7 +112,7 @@ export type SeriesCollectionValue = { }; /** @internal */ -export function getSeriesIndex(series: XYChartSeriesIdentifier[], target: XYChartSeriesIdentifier): number { +export function getSeriesIndex(series: SeriesIdentifier[], target: SeriesIdentifier): number { if (!series) { return -1; } @@ -381,7 +381,7 @@ function getRawDataSeries( */ export function getSplittedSeries( seriesSpecs: BasicSeriesSpec[], - deselectedDataSeries: XYChartSeriesIdentifier[] = [], + deselectedDataSeries: SeriesIdentifier[] = [], ): { splittedSeries: Map; seriesCollection: Map; diff --git a/packages/osd-charts/src/commons/legend.ts b/packages/osd-charts/src/commons/legend.ts new file mode 100644 index 000000000000..816d294b52d7 --- /dev/null +++ b/packages/osd-charts/src/commons/legend.ts @@ -0,0 +1,41 @@ +/* + * 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 { Color } from '../utils/commons'; +import { SeriesIdentifier } from './series_id'; +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +/** @internal */ +export type LegendItemChildId = string; + +/** @internal */ +export type LegendItem = { + seriesIdentifier: SeriesIdentifier; + childId?: LegendItemChildId; + depth?: number; + color: Color; + label: string; + isSeriesHidden?: boolean; + isItemHidden?: boolean; + defaultExtra?: { + raw: number | null; + formatted: number | string | null; + }; +}; + +/** @internal */ +export type LegendItemExtraValues = Map; diff --git a/packages/osd-charts/src/commons/series_id.ts b/packages/osd-charts/src/commons/series_id.ts new file mode 100644 index 000000000000..949eded811e2 --- /dev/null +++ b/packages/osd-charts/src/commons/series_id.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 { SpecId } from '../utils/ids'; + +/** + * A string key used to uniquely identify a series + */ +export type SeriesKey = string; + +/** + * A series identifier + */ +export type SeriesIdentifier = { + /** + * The SpecId, used to identify the spec + */ + specId: SpecId; + /** + * A string key used to uniquely identify a series + */ + key: SeriesKey; +}; diff --git a/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap b/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap index b4f88a8934b1..412cee4f75cd 100644 --- a/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap +++ b/packages/osd-charts/src/components/legend/__snapshots__/legend.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = `"
splita
splitb
splitc
splitd
"`; +exports[`Legend #legendColorPicker should match snapshot after onChange is called 1`] = `"
  • splita
  • splitb
  • splitc
  • splitd
  • "`; -exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = `"
    splita
    splitb
    splitc
    splitd
    "`; +exports[`Legend #legendColorPicker should match snapshot after onClose is called 1`] = `"
  • splita
  • splitb
  • splitc
  • splitd
  • "`; exports[`Legend #legendColorPicker should render colorPicker when color is clicked 1`] = `"
    Custom Color Picker
    "`; -exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = `"
    splita
    Custom Color Picker
    splitb
    splitc
    splitd
    "`; +exports[`Legend #legendColorPicker should render colorPicker when color is clicked 2`] = `"
  • splita
  • Custom Color Picker
  • splitb
  • splitc
  • splitd
  • "`; diff --git a/packages/osd-charts/src/components/legend/color.tsx b/packages/osd-charts/src/components/legend/color.tsx new file mode 100644 index 000000000000..3c09bb494207 --- /dev/null +++ b/packages/osd-charts/src/components/legend/color.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 classNames from 'classnames'; +import { Icon } from '../icons/icon'; + +interface ColorProps { + color: string; + isSeriesHidden?: boolean; + hasColorPicker: boolean; + onColorClick?: (event: React.MouseEvent) => void; +} +/** + * Color component used by the legend item + * @internal + */ +export function Color({ color, isSeriesHidden = false, hasColorPicker, onColorClick }: ColorProps) { + if (isSeriesHidden) { + return ( +
    + {/* changing the default viewBox for the eyeClosed icon to keep the same dimensions */} + +
    + ); + } + + const colorClasses = classNames('echLegendItem__color', { + 'echLegendItem__color--changable': hasColorPicker, + }); + + return ( +
    + +
    + ); +} diff --git a/packages/osd-charts/src/components/legend/extra.tsx b/packages/osd-charts/src/components/legend/extra.tsx new file mode 100644 index 000000000000..d2dc76cb7250 --- /dev/null +++ b/packages/osd-charts/src/components/legend/extra.tsx @@ -0,0 +1,36 @@ +/* + * 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 classNames from 'classnames'; + +/** + * @internal + * @param extra + * @param isSeriesHidden + */ +export function renderExtra(extra: string | number, isSeriesHidden?: boolean) { + const extraClassNames = classNames('echLegendItem__extra', { + ['echLegendItem__extra--hidden']: isSeriesHidden, + }); + return ( +
    + {extra} +
    + ); +} diff --git a/packages/osd-charts/src/components/legend/label.tsx b/packages/osd-charts/src/components/legend/label.tsx new file mode 100644 index 000000000000..5456d0168080 --- /dev/null +++ b/packages/osd-charts/src/components/legend/label.tsx @@ -0,0 +1,40 @@ +/* + * 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 classNames from 'classnames'; + +interface LabelProps { + label: string; + onLabelClick: (event: React.MouseEvent) => void; + hasLabelClickListener: boolean; +} +/** + * Label component used to display text in legend item + * @internal + */ +export function Label({ label, onLabelClick, hasLabelClickListener }: LabelProps) { + const labelClassNames = classNames('echLegendItem__label', { + ['echLegendItem__label--hasClickListener']: hasLabelClickListener, + }); + return ( +
    + {label} +
    + ); +} diff --git a/packages/osd-charts/src/components/legend/legend.tsx b/packages/osd-charts/src/components/legend/legend.tsx index 0dad8ea15692..a16463ee2eeb 100644 --- a/packages/osd-charts/src/components/legend/legend.tsx +++ b/packages/osd-charts/src/components/legend/legend.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { createRef } from 'react'; +import React from 'react'; import classNames from 'classnames'; import { Dispatch, bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -25,14 +25,13 @@ import { GlobalChartState } from '../../state/chart_state'; import { getLegendItemsSelector } from '../../state/selectors/get_legend_items'; import { getSettingsSpecSelector } from '../../state/selectors/get_settings_specs'; import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; -import { getLegendItemsValuesSelector } from '../../state/selectors/get_legend_items_values'; +import { getLegendExtraValuesSelector } from '../../state/selectors/get_legend_items_values'; import { getLegendSizeSelector } from '../../state/selectors/get_legend_size'; import { onToggleLegend } from '../../state/actions/legend'; import { LIGHT_THEME } from '../../utils/themes/light_theme'; -import { LegendListItem } from './legend_item'; +import { LegendItemProps } from './legend_item'; import { Theme } from '../../utils/themes/theme'; -import { TooltipLegendValue } from '../../chart_types/xy_chart/tooltip/tooltip'; -import { LegendItem, getItemLabel } from '../../chart_types/xy_chart/legend/legend'; +import { LegendItem, LegendItemExtraValues } from '../../commons/legend'; import { BBox } from '../../utils/bbox/bbox_calculator'; import { onToggleDeselectSeriesAction, @@ -40,25 +39,28 @@ import { onLegendItemOverAction, } from '../../state/actions/legend'; import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; -import { SettingsSpec } from '../../specs'; -import { BandedAccessorType } from '../../utils/geometry'; -import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { LegendItemListener, BasicListener, LegendColorPicker } from '../../specs'; +import { getLegendStyle, getLegendListStyle } from './style_utils'; +import { renderLegendItem } from './legend_item'; interface LegendStateProps { - legendItems: Map; - legendPosition: Position; - legendItemTooltipValues: Map; - showLegend: boolean; - legendCollapsed: boolean; debug: boolean; chartTheme: Theme; - legendSize: BBox; - settings?: SettingsSpec; + size: BBox; + position: Position; + collapsed: boolean; + items: LegendItem[]; + showExtra: boolean; + extraValues: Map; + colorPicker?: LegendColorPicker; + onItemOver?: LegendItemListener; + onItemOut?: BasicListener; + onItemClick?: LegendItemListener; } interface LegendDispatchProps { - onToggleLegend: typeof onToggleLegend; - onLegendItemOutAction: typeof onLegendItemOutAction; - onLegendItemOverAction: typeof onLegendItemOverAction; + onToggle: typeof onToggleLegend; + onItemOutAction: typeof onLegendItemOutAction; + onItemOverAction: typeof onLegendItemOverAction; onToggleDeselectSeriesAction: typeof onToggleDeselectSeriesAction; clearTemporaryColors: typeof clearTemporaryColors; setTemporaryColor: typeof setTemporaryColor; @@ -66,136 +68,66 @@ interface LegendDispatchProps { } type LegendProps = LegendStateProps & LegendDispatchProps; -interface LegendStyle { - maxHeight?: string; - maxWidth?: string; - width?: string; - height?: string; -} - -interface LegendListStyle { - paddingTop?: number | string; - paddingBottom?: number | string; - paddingLeft?: number | string; - paddingRight?: number | string; - gridTemplateColumns?: string; -} - -class LegendComponent extends React.Component { +/** + * @internal + */ +export class LegendComponent extends React.Component { static displayName = 'Legend'; - legendItemCount = 0; - - private echLegend = createRef(); render() { - const { legendItems, legendPosition, legendSize, showLegend, debug, chartTheme } = this.props; - if (!showLegend || legendItems.size === 0) { + const { + items, + position, + size, + debug, + chartTheme: { chartMargins, legend }, + } = this.props; + if (items.length === 0) { return null; } - const legendContainerStyle = this.getLegendStyle(legendPosition, legendSize); - const legendListStyle = this.getLegendListStyle(legendPosition, chartTheme); - const legendClasses = classNames('echLegend', `echLegend--${legendPosition}`, { + const legendContainerStyle = getLegendStyle(position, size); + const legendListStyle = getLegendListStyle(position, chartMargins, legend); + const legendClasses = classNames('echLegend', `echLegend--${position}`, { 'echLegend--debug': debug, }); + const itemProps: Omit = { + position, + totalItems: items.length, + extraValues: this.props.extraValues, + showExtra: this.props.showExtra, + onMouseOut: this.props.onItemOut, + onMouseOver: this.props.onItemOver, + onClick: this.props.onItemClick, + clearTemporaryColorsAction: this.props.clearTemporaryColors, + setPersistedColorAction: this.props.setPersistedColor, + setTemporaryColorAction: this.props.setTemporaryColor, + mouseOutAction: this.props.onItemOutAction, + mouseOverAction: this.props.onItemOverAction, + toggleDeselectSeriesAction: this.props.onToggleDeselectSeriesAction, + colorPicker: this.props.colorPicker, + }; return ( -
    +
    -
    - {[...legendItems.values()].map(this.renderLegendElement)} -
    +
      + {items.map((item, index) => { + return renderLegendItem(item, itemProps, items.length, index); + })} +
    ); } - - getLegendListStyle = (position: Position, { chartMargins, legend }: Theme): LegendListStyle => { - const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = chartMargins; - - if (position === Position.Bottom || position === Position.Top) { - return { - paddingLeft, - paddingRight, - gridTemplateColumns: `repeat(auto-fill, minmax(${legend.verticalWidth}px, 1fr))`, - }; - } - - return { - paddingTop, - paddingBottom, - }; - }; - - getLegendStyle = (position: Position, size: BBox): LegendStyle => { - if (position === Position.Left || position === Position.Right) { - const width = `${size.width}px`; - return { - width, - maxWidth: width, - }; - } - const height = `${size.height}px`; - return { - height, - maxHeight: height, - }; - }; - - private getLegendValues( - tooltipValues: Map | undefined, - key: SeriesKey, - banded: boolean = false, - ): any[] { - const values = tooltipValues && tooltipValues.get(key); - if (values === null || values === undefined) { - return banded ? ['', ''] : ['']; - } - - const { y0, y1 } = values; - return banded ? [y1, y0] : [y1]; - } - - private renderLegendElement = (item: LegendItem) => { - if (!this.props.settings) { - return null; - } - const { key, displayValue, banded } = item; - const { legendItemTooltipValues, settings } = this.props; - const { showLegendExtra, legendPosition, legendColorPicker } = settings; - const legendValues = this.getLegendValues(legendItemTooltipValues, key, banded); - return legendValues.map((value, index) => { - const yAccessor: BandedAccessorType = index === 0 ? BandedAccessorType.Y1 : BandedAccessorType.Y0; - return ( - - ); - }); - }; } const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => bindActionCreators( { - onToggleLegend, + onToggle: onToggleLegend, onToggleDeselectSeriesAction, - onLegendItemOutAction, - onLegendItemOverAction, + onItemOutAction: onLegendItemOutAction, + onItemOverAction: onLegendItemOverAction, clearTemporaryColors, setTemporaryColor, setPersistedColor, @@ -204,34 +136,45 @@ const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => ); const EMPTY_DEFAULT_STATE = { - legendItems: new Map(), - legendPosition: Position.Right, - showLegend: false, - legendCollapsed: false, - legendItemTooltipValues: new Map(), + items: [], + position: Position.Right, + collapsed: false, + extraValues: new Map(), debug: false, chartTheme: LIGHT_THEME, - legendSize: { width: 0, height: 0 }, + size: { width: 0, height: 0 }, + showExtra: false, }; const mapStateToProps = (state: GlobalChartState): LegendStateProps => { if (!state.specsInitialized) { return EMPTY_DEFAULT_STATE; } - const { legendPosition, showLegend, debug } = getSettingsSpecSelector(state); + const { + legendPosition, + showLegend, + showLegendExtra, + debug, + legendColorPicker, + onLegendItemOver: onItemOver, + onLegendItemOut: onItemOut, + onLegendItemClick: onItemClick, + } = getSettingsSpecSelector(state); if (!showLegend) { return EMPTY_DEFAULT_STATE; } - const legendItems = getLegendItemsSelector(state); return { - legendItems, - legendPosition, - showLegend, - legendCollapsed: state.interactions.legendCollapsed, - legendItemTooltipValues: getLegendItemsValuesSelector(state), debug, chartTheme: getChartThemeSelector(state), - legendSize: getLegendSizeSelector(state), - settings: getSettingsSpecSelector(state), + size: getLegendSizeSelector(state), + collapsed: state.interactions.legendCollapsed, + items: getLegendItemsSelector(state), + position: legendPosition, + showExtra: showLegendExtra, + extraValues: getLegendExtraValuesSelector(state), + colorPicker: legendColorPicker, + onItemOver, + onItemOut, + onItemClick, }; }; diff --git a/packages/osd-charts/src/components/legend/legend_item.tsx b/packages/osd-charts/src/components/legend/legend_item.tsx index 2d1ff58e4d27..7c89557ecd3c 100644 --- a/packages/osd-charts/src/components/legend/legend_item.tsx +++ b/packages/osd-charts/src/components/legend/legend_item.tsx @@ -19,69 +19,78 @@ import classNames from 'classnames'; import React, { Component, createRef } from 'react'; import { deepEqual } from '../../utils/fast_deep_equal'; -import { Icon } from '../icons/icon'; import { LegendItemListener, BasicListener, LegendColorPicker } from '../../specs/settings'; -import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; +import { LegendItem, LegendItemExtraValues } from '../../commons/legend'; import { onLegendItemOutAction, onLegendItemOverAction } from '../../state/actions/legend'; import { Position, Color } from '../../utils/commons'; -import { XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; -import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; - -interface LegendItemProps { - legendItem: LegendItem; - extra: string; - label?: string; - legendPosition: Position; - showExtra: boolean; - legendColorPicker?: LegendColorPicker; - onLegendItemClickListener?: LegendItemListener; - onLegendItemOutListener?: BasicListener; - onLegendItemOverListener?: LegendItemListener; - legendItemOutAction: typeof onLegendItemOutAction; - legendItemOverAction: typeof onLegendItemOverAction; - clearTemporaryColors: typeof clearTemporaryColors; - setTemporaryColor: typeof setTemporaryColor; - setPersistedColor: typeof setPersistedColor; - toggleDeselectSeriesAction: (legendItemId: XYChartSeriesIdentifier) => void; -} +import { SeriesIdentifier } from '../../commons/series_id'; +import { + clearTemporaryColors as clearTemporaryColorsAction, + setTemporaryColor as setTemporaryColorAction, + setPersistedColor as setPersistedColorAction, +} from '../../state/actions/colors'; +import { getExtra } from './utils'; +import { Color as ItemColor } from './color'; +import { Label as ItemLabel } from './label'; +import { renderExtra } from './extra'; -/** - * Create a div for the extra text - * @param extra - * @param isSeriesVisible - */ -function renderExtra(extra: string, isSeriesVisible: boolean | undefined) { - const extraClassNames = classNames('echLegendItem__extra', { - ['echLegendItem__extra--hidden']: !isSeriesVisible, - }); - return ( -
    - {extra} -
    - ); +/** @internal */ +export const LEGEND_HIERARCHY_MARGIN = 10; + +/** @internal */ +export interface LegendItemProps { + item: LegendItem; + totalItems: number; + position: Position; + extraValues: Map; + showExtra: boolean; + colorPicker?: LegendColorPicker; + onClick?: LegendItemListener; + onMouseOut?: BasicListener; + onMouseOver?: LegendItemListener; + mouseOutAction: typeof onLegendItemOutAction; + mouseOverAction: typeof onLegendItemOverAction; + clearTemporaryColorsAction: typeof clearTemporaryColorsAction; + setTemporaryColorAction: typeof setTemporaryColorAction; + setPersistedColorAction: typeof setPersistedColorAction; + toggleDeselectSeriesAction: (legendItemId: SeriesIdentifier) => void; } /** - * Create a div for the label - * @param label - * @param onLabelClick - * @param hasLabelClickListener + * @internal + * @param item + * @param props */ -function renderLabel( - onLabelClick: (event: React.MouseEvent) => void, - hasLabelClickListener: boolean, - label?: string, +export function renderLegendItem( + item: LegendItem, + props: Omit, + totalItems: number, + index: number, ) { - if (!label) { - return null; - } - const labelClassNames = classNames('echLegendItem__label', { - ['echLegendItem__label--hasClickListener']: hasLabelClickListener, - }); + const { + seriesIdentifier: { key }, + childId, + } = item; + return ( -
    - {label} -
    + ); } @@ -92,7 +101,7 @@ interface LegendItemState { /** @internal */ export class LegendListItem extends Component { static displayName = 'LegendItem'; - ref = createRef(); + ref = createRef(); state: LegendItemState = { isOpen: false, @@ -110,65 +119,28 @@ export class LegendListItem extends Component } : undefined; - /** - * Create a div for the color/eye icon - * @param color - * @param isSeriesVisible - */ - renderColor = (color?: string, isSeriesVisible = true) => { - if (!color) { - return null; - } - - if (!isSeriesVisible) { - return ( -
    - {/* changing the default viewBox for the eyeClosed icon to keep the same dimensions */} - -
    - ); - } - - const changable = Boolean(this.props.legendColorPicker); - const colorClasses = classNames('echLegendItem__color', { - 'echLegendItem__color--changable': changable, - }); - - return ( -
    - -
    - ); - }; - renderColorPicker() { const { - legendColorPicker: ColorPicker, - legendItem, - clearTemporaryColors, - setTemporaryColor, - setPersistedColor, + colorPicker: ColorPicker, + item, + clearTemporaryColorsAction, + setTemporaryColorAction, + setPersistedColorAction, } = this.props; - const { seriesIdentifier, color } = legendItem; + const { seriesIdentifier, color } = item; const handleClose = () => { - setPersistedColor(seriesIdentifier.key, color); - clearTemporaryColors(); + setPersistedColorAction(seriesIdentifier.key, color); + clearTemporaryColorsAction(); this.toggleIsOpen(); }; - if (ColorPicker && this.state.isOpen && this.ref.current) { return ( setTemporaryColor(seriesIdentifier.key, color)} + onChange={(color: Color) => setTemporaryColorAction(seriesIdentifier.key, color)} seriesIdentifier={seriesIdentifier} /> ); @@ -176,28 +148,42 @@ export class LegendListItem extends Component } render() { - const { extra, legendItem, legendPosition, label, showExtra, onLegendItemClickListener } = this.props; - const { color, isSeriesVisible, seriesIdentifier, isLegendItemVisible } = legendItem; + const { extraValues, item, showExtra, onClick, colorPicker, position, totalItems } = this.props; + const { color, isSeriesHidden, isItemHidden, seriesIdentifier, label } = item; const onLabelClick = this.onVisibilityClick(seriesIdentifier); - const hasLabelClickListener = Boolean(onLegendItemClickListener); + const hasLabelClickListener = Boolean(onClick); - const itemClassNames = classNames('echLegendItem', `echLegendItem--${legendPosition}`, { - 'echLegendItem--hidden': !isSeriesVisible, - 'echLegendItem__extra--hidden': !isLegendItemVisible, + const itemClassNames = classNames('echLegendItem', `echLegendItem--${position}`, { + 'echLegendItem--hidden': isSeriesHidden, + 'echLegendItem__extra--hidden': isItemHidden, }); + const hasColorPicker = Boolean(colorPicker); + const colorClick = this.handleColorClick(hasColorPicker); + const extra = getExtra(extraValues, item, totalItems); + const style = item.depth + ? { + marginLeft: LEGEND_HIERARCHY_MARGIN * (item.depth ?? 0), + } + : undefined; return ( <> -
    - {this.renderColor(color, isSeriesVisible)} - {renderLabel(onLabelClick, hasLabelClickListener, label)} - {showExtra && renderExtra(extra, isSeriesVisible)} -
    + + + {showExtra && extra != null && renderExtra(extra, isSeriesHidden)} + {this.renderColorPicker()} ); @@ -208,28 +194,28 @@ export class LegendListItem extends Component }; onLegendItemMouseOver = () => { - const { onLegendItemOverListener, legendItemOverAction, legendItem } = this.props; + const { onMouseOver, mouseOverAction, item } = this.props; // call the settings listener directly if available - if (onLegendItemOverListener) { - onLegendItemOverListener(legendItem.seriesIdentifier); + if (onMouseOver) { + onMouseOver(item.seriesIdentifier); } - legendItemOverAction(legendItem.key); + mouseOverAction(item.seriesIdentifier.key); }; onLegendItemMouseOut = () => { - const { onLegendItemOutListener, legendItemOutAction } = this.props; + const { onMouseOut, mouseOutAction } = this.props; // call the settings listener directly if available - if (onLegendItemOutListener) { - onLegendItemOutListener(); + if (onMouseOut) { + onMouseOut(); } - legendItemOutAction(); + mouseOutAction(); }; // TODO handle shift key - onVisibilityClick = (legendItemId: XYChartSeriesIdentifier) => () => { - const { onLegendItemClickListener, toggleDeselectSeriesAction } = this.props; - if (onLegendItemClickListener) { - onLegendItemClickListener(legendItemId); + onVisibilityClick = (legendItemId: SeriesIdentifier) => () => { + const { onClick, toggleDeselectSeriesAction } = this.props; + if (onClick) { + onClick(legendItemId); } toggleDeselectSeriesAction(legendItemId); }; diff --git a/packages/osd-charts/src/components/legend/style_utils.ts b/packages/osd-charts/src/components/legend/style_utils.ts new file mode 100644 index 000000000000..051b1e4f0d9a --- /dev/null +++ b/packages/osd-charts/src/components/legend/style_utils.ts @@ -0,0 +1,85 @@ +/* + * 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 { LegendStyle as ThemeLegendStyle } from '../../utils/themes/theme'; +import { Margins } from '../../utils/dimensions'; +import { Position } from '../../utils/commons'; +import { BBox } from '../../utils/bbox/bbox_calculator'; + +/** @internal */ +export interface LegendStyle { + maxHeight?: string; + maxWidth?: string; + width?: string; + height?: string; +} + +/** @internal */ +export interface LegendListStyle { + paddingTop?: number | string; + paddingBottom?: number | string; + paddingLeft?: number | string; + paddingRight?: number | string; + gridTemplateColumns?: string; +} +/** + * Get the legend list style + * @param position + * @param chartMarrings, legend from the Theme + * @internal + */ +export function getLegendListStyle( + position: Position, + chartMargins: Margins, + legendStyle: ThemeLegendStyle, +): LegendListStyle { + const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = chartMargins; + + if (position === Position.Bottom || position === Position.Top) { + return { + paddingLeft, + paddingRight, + gridTemplateColumns: `repeat(auto-fill, minmax(${legendStyle.verticalWidth}px, 1fr))`, + }; + } + + return { + paddingTop, + paddingBottom, + }; +} +/** + * Get the legend global style + * @param position the position of the legend + * @param size the computed size of the legend + * @internal + */ +export function getLegendStyle(position: Position, size: BBox): LegendStyle { + if (position === Position.Left || position === Position.Right) { + const width = `${size.width}px`; + return { + width, + maxWidth: width, + }; + } + const height = `${size.height}px`; + return { + height, + maxHeight: height, + }; +} diff --git a/packages/osd-charts/src/components/legend/utils.ts b/packages/osd-charts/src/components/legend/utils.ts new file mode 100644 index 000000000000..e0a4e86c1bb1 --- /dev/null +++ b/packages/osd-charts/src/components/legend/utils.ts @@ -0,0 +1,41 @@ +/* + * 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 { LegendItemExtraValues, LegendItem } from '../../commons/legend'; + +/** @internal */ +export function getExtra(extraValues: Map, item: LegendItem, totalItems: number) { + const { + seriesIdentifier: { key }, + defaultExtra, + childId, + } = item; + if (extraValues.size === 0) { + return defaultExtra?.formatted ?? ''; + } + const itemExtraValues = extraValues.get(key); + const actionExtra = (childId && itemExtraValues?.get(childId)) ?? null; + if (extraValues.size !== totalItems) { + if (actionExtra != null) { + return actionExtra; + } else { + return ''; + } + } else { + return actionExtra !== null ? actionExtra : defaultExtra?.formatted ?? ''; + } +} diff --git a/packages/osd-charts/src/index.ts b/packages/osd-charts/src/index.ts index e082defa8bf1..3d187a7ee254 100644 --- a/packages/osd-charts/src/index.ts +++ b/packages/osd-charts/src/index.ts @@ -29,9 +29,9 @@ export { SpecId, GroupId, AxisId, AnnotationId } from './utils/ids'; export * from './specs'; export { CurveType } from './utils/curves'; export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/data/formatters'; -export { SeriesCollectionValue } from './chart_types/xy_chart/utils/series'; export { Datum, Position, Rendering, Rotation } from './utils/commons'; -export { SeriesIdentifier, XYChartSeriesIdentifier } from './chart_types/xy_chart/utils/series'; +export { SeriesIdentifier } from './commons/series_id'; +export { XYChartSeriesIdentifier } from './chart_types/xy_chart/utils/series'; export { AnnotationTooltipFormatter } from './chart_types/xy_chart/annotations/annotation_utils'; export { GeometryValue } from './utils/geometry'; export { diff --git a/packages/osd-charts/src/specs/settings.tsx b/packages/osd-charts/src/specs/settings.tsx index 24c65aaaee60..d51dd2064331 100644 --- a/packages/osd-charts/src/specs/settings.tsx +++ b/packages/osd-charts/src/specs/settings.tsx @@ -27,7 +27,8 @@ import { Spec } from '.'; import { LIGHT_THEME } from '../utils/themes/light_theme'; import { ChartTypes } from '../chart_types'; import { GeometryValue } from '../utils/geometry'; -import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; +import { SeriesIdentifier } from '../commons/series_id'; import { Accessor } from '../utils/accessor'; import { Position, Rendering, Rotation, Color } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; @@ -44,7 +45,7 @@ export type PartitionElementEvent = [Array, SeriesIdentifier]; export type ElementClickListener = (elements: Array) => void; export type ElementOverListener = (elements: Array) => void; export type BrushEndListener = (min: number, max: number) => void; -export type LegendItemListener = (series: XYChartSeriesIdentifier | null) => void; +export type LegendItemListener = (series: SeriesIdentifier | null) => void; export type PointerUpdateListener = (event: PointerEvent) => void; /** * Listener to be called when chart render state changes @@ -144,7 +145,7 @@ export interface LegendColorPickerProps { /** * Anchor used to position picker */ - anchor: HTMLDivElement; + anchor: HTMLElement; /** * Current color of the given series */ @@ -160,7 +161,7 @@ export interface LegendColorPickerProps { /** * Series id for the active series */ - seriesIdentifier: XYChartSeriesIdentifier; + seriesIdentifier: SeriesIdentifier; } export type LegendColorPicker = ComponentType; @@ -195,6 +196,14 @@ export interface SettingsSpec extends Spec { * @default false */ showLegendExtra: boolean; + /** + * Limit the legend to a max depth when showing a hierarchical legend + */ + legendMaxDepth?: number; + /** + * Display the legend as a flat hierarchy + */ + flatLegend?: boolean; /** * Removes duplicate axes * diff --git a/packages/osd-charts/src/state/actions/colors.ts b/packages/osd-charts/src/state/actions/colors.ts index b7675f8a4524..618cf42deb33 100644 --- a/packages/osd-charts/src/state/actions/colors.ts +++ b/packages/osd-charts/src/state/actions/colors.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { SeriesKey } from '../../commons/series_id'; import { Color } from '../../utils/commons'; /** @internal */ diff --git a/packages/osd-charts/src/state/actions/legend.ts b/packages/osd-charts/src/state/actions/legend.ts index 2b677ef1ae7b..82eb927ebe68 100644 --- a/packages/osd-charts/src/state/actions/legend.ts +++ b/packages/osd-charts/src/state/actions/legend.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { SeriesIdentifier } from '../../commons/series_id'; /** @internal */ export const ON_TOGGLE_LEGEND = 'ON_TOGGLE_LEGEND'; @@ -43,7 +43,7 @@ interface LegendItemOutAction { interface ToggleDeselectSeriesAction { type: typeof ON_TOGGLE_DESELECT_SERIES; - legendItemId: XYChartSeriesIdentifier; + legendItemId: SeriesIdentifier; } /** @internal */ @@ -62,7 +62,7 @@ export function onLegendItemOutAction(): LegendItemOutAction { } /** @internal */ -export function onToggleDeselectSeriesAction(legendItemId: XYChartSeriesIdentifier): ToggleDeselectSeriesAction { +export function onToggleDeselectSeriesAction(legendItemId: SeriesIdentifier): ToggleDeselectSeriesAction { return { type: ON_TOGGLE_DESELECT_SERIES, legendItemId }; } diff --git a/packages/osd-charts/src/state/chart_state.ts b/packages/osd-charts/src/state/chart_state.ts index d34a154a77d6..b1f4e4bb3885 100644 --- a/packages/osd-charts/src/state/chart_state.ts +++ b/packages/osd-charts/src/state/chart_state.ts @@ -15,19 +15,19 @@ * 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_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC, REMOVE_SPEC, SPEC_PARSING } from './actions/specs'; import { SET_PERSISTED_COLOR, SET_TEMPORARY_COLOR, CLEAR_TEMPORARY_COLORS } from './actions/colors'; import { interactionsReducer } from './reducers/interactions'; import { ChartTypes } from '../chart_types'; import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; -import { XYChartSeriesIdentifier, SeriesKey } from '../chart_types/xy_chart/utils/series'; +import { SeriesKey, SeriesIdentifier } from '../commons/series_id'; import { Spec, PointerEvent } from '../specs'; import { DEFAULT_SETTINGS_SPEC } from '../specs/settings'; import { Dimensions } from '../utils/dimensions'; import { Point } from '../utils/point'; -import { LegendItem } from '../chart_types/xy_chart/legend/legend'; -import { TooltipLegendValue } from '../chart_types/xy_chart/tooltip/tooltip'; +import { LegendItem, LegendItemExtraValues } from '../commons/legend'; import { StateActions } from './actions'; import { CHART_RENDERED } from './actions/chart'; import { UPDATE_PARENT_DIMENSION } from './actions/chart_settings'; @@ -38,56 +38,65 @@ import { PartitionState } from '../chart_types/partition_chart/state/chart_state import { TooltipInfo } from '../components/tooltip/types'; import { TooltipAnchorPosition } from '../components/tooltip/utils'; import { Color } from '../utils/commons'; +import { LegendItemLabel } from './selectors/get_legend_items_labels'; export type BackwardRef = () => React.RefObject; /** - * A set of chart-type-dependant functions that required by all char types + * A set of chart-type-dependant functions that required by all chart type * @internal */ export interface InternalChartState { /** - * the chart type + * The chart type */ chartType: ChartTypes; /** - * returns a JSX element with the chart rendered (lenged excluded) + * Returns a JSX element with the chart rendered (lenged excluded) * @param containerRef * @param forwardStageRef */ chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject): JSX.Element | null; /** - * true if the brush is available for this chart type + * `true` if the brush is available for this chart type * @param globalState */ isBrushAvailable(globalState: GlobalChartState): boolean; /** - * true if the brush is available for this chart type + * `true` if the brush is available for this chart type * @param globalState */ isBrushing(globalState: GlobalChartState): boolean; /** - * true if the chart is empty (no data displayed) + * `true` if the chart is empty (no data displayed) * @param globalState */ isChartEmpty(globalState: GlobalChartState): boolean; + /** - * return the list of legend items + * Returns the list of legend items labels. Mainly used to compute the legend size + * based on labels and their hierarchy depth. * @param globalState */ - getLegendItems(globalState: GlobalChartState): Map; + getLegendItemsLabels(globalState: GlobalChartState): LegendItemLabel[]; + /** - * return the list of values for each legend item + * Returns the list of legend items. * @param globalState */ - getLegendItemsValues(globalState: GlobalChartState): Map; + getLegendItems(globalState: GlobalChartState): LegendItem[]; /** - * return the CSS pointer cursor depending on the internal chart state + * Returns the list of extra values for each legend item + * @param globalState + */ + getLegendExtraValues(globalState: GlobalChartState): Map; + /** + * Returns the CSS pointer cursor depending on the internal chart state * @param globalState */ getPointerCursor(globalState: GlobalChartState): string; /** - * true if the tooltip is visible, false otherwise + * `true` if the tooltip is visible, `false` otherwise * @param globalState */ isTooltipVisible(globalState: GlobalChartState): boolean; @@ -103,6 +112,10 @@ export interface InternalChartState { */ getTooltipAnchor(globalState: GlobalChartState): TooltipAnchorPosition | null; + /** + * Called on every state change to activate any event callback + * @param globalState + */ eventCallbacks(globalState: GlobalChartState): void; } @@ -138,7 +151,7 @@ export interface InteractionsState { highlightedLegendItemKey: string | null; legendCollapsed: boolean; invertDeselect: boolean; - deselectedDataSeries: XYChartSeriesIdentifier[]; + deselectedDataSeries: SeriesIdentifier[]; } /** @internal */ diff --git a/packages/osd-charts/src/state/reducers/interactions.ts b/packages/osd-charts/src/state/reducers/interactions.ts index ea5ba1e87cf6..d9ffcabe3dc4 100644 --- a/packages/osd-charts/src/state/reducers/interactions.ts +++ b/packages/osd-charts/src/state/reducers/interactions.ts @@ -25,7 +25,8 @@ import { LegendActions, } from '../actions/legend'; import { ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE, MouseActions } from '../actions/mouse'; -import { getSeriesIndex, XYChartSeriesIdentifier } from '../../chart_types/xy_chart/utils/series'; +import { getSeriesIndex } from '../../chart_types/xy_chart/utils/series'; +import { SeriesIdentifier } from '../../commons/series_id'; /** @internal */ export function interactionsReducer(state: InteractionsState, action: LegendActions | MouseActions): InteractionsState { @@ -126,10 +127,7 @@ export function interactionsReducer(state: InteractionsState, action: LegendActi } } -function toggleDeselectedDataSeries( - legendItem: XYChartSeriesIdentifier, - deselectedDataSeries: XYChartSeriesIdentifier[], -) { +function toggleDeselectedDataSeries(legendItem: SeriesIdentifier, deselectedDataSeries: SeriesIdentifier[]) { const index = getSeriesIndex(deselectedDataSeries, legendItem); if (index > -1) { return [...deselectedDataSeries.slice(0, index), ...deselectedDataSeries.slice(index + 1)]; diff --git a/packages/osd-charts/src/state/selectors/get_legend_items.ts b/packages/osd-charts/src/state/selectors/get_legend_items.ts index a99bd0088e11..3f0cb7247dcb 100644 --- a/packages/osd-charts/src/state/selectors/get_legend_items.ts +++ b/packages/osd-charts/src/state/selectors/get_legend_items.ts @@ -17,13 +17,12 @@ * under the License. */ import { GlobalChartState } from '../chart_state'; -import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; -import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { LegendItem } from '../../commons/legend'; -const EMPTY_LEGEND_LIST = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; /** @internal */ -export const getLegendItemsSelector = (state: GlobalChartState): Map => { +export const getLegendItemsSelector = (state: GlobalChartState): LegendItem[] => { if (state.internalChartState) { return state.internalChartState.getLegendItems(state); } else { diff --git a/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts new file mode 100644 index 000000000000..f0ce6521282d --- /dev/null +++ b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts @@ -0,0 +1,34 @@ +/* + * 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 { GlobalChartState } from '../chart_state'; + +/** @internal */ +export interface LegendItemLabel { + label: string; + depth: number; +} + +/** @internal */ +export const getLegendItemsLabelsSelector = (state: GlobalChartState): LegendItemLabel[] => { + if (state.internalChartState) { + return state.internalChartState.getLegendItemsLabels(state); + } else { + return []; + } +}; diff --git a/packages/osd-charts/src/state/selectors/get_legend_items_values.ts b/packages/osd-charts/src/state/selectors/get_legend_items_values.ts index cde2faf8387e..ca645cbb2fa0 100644 --- a/packages/osd-charts/src/state/selectors/get_legend_items_values.ts +++ b/packages/osd-charts/src/state/selectors/get_legend_items_values.ts @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { TooltipLegendValue } from '../../chart_types/xy_chart/tooltip/tooltip'; import { GlobalChartState } from '../chart_state'; -import { SeriesKey } from '../../chart_types/xy_chart/utils/series'; +import { SeriesKey } from '../../commons/series_id'; +import { LegendItemExtraValues } from '../../commons/legend'; -const EMPTY_ITEM_LIST = new Map(); +const EMPTY_ITEM_LIST = new Map(); /** @internal */ -export const getLegendItemsValuesSelector = (state: GlobalChartState): Map => { +export const getLegendExtraValuesSelector = (state: GlobalChartState): Map => { if (state.internalChartState) { - return state.internalChartState.getLegendItemsValues(state); + return state.internalChartState.getLegendExtraValues(state); } else { return EMPTY_ITEM_LIST; } diff --git a/packages/osd-charts/src/state/selectors/get_legend_size.ts b/packages/osd-charts/src/state/selectors/get_legend_size.ts index d82b52f9222f..f278377d496e 100644 --- a/packages/osd-charts/src/state/selectors/get_legend_size.ts +++ b/packages/osd-charts/src/state/selectors/get_legend_size.ts @@ -17,43 +17,18 @@ * under the License. */ import createCachedSelector from 're-reselect'; -import { getLegendItemsSelector } from './get_legend_items'; import { CanvasTextBBoxCalculator } from '../../utils/bbox/canvas_text_bbox_calculator'; import { BBox } from '../../utils/bbox/bbox_calculator'; import { getSettingsSpecSelector } from './get_settings_specs'; import { isVerticalAxis } from '../../chart_types/xy_chart/utils/axis_utils'; import { getChartThemeSelector } from './get_chart_theme'; import { GlobalChartState } from '../chart_state'; -import { getItemLabel } from '../../chart_types/xy_chart/legend/legend'; import { getChartIdSelector } from './get_chart_id'; +import { getLegendItemsLabelsSelector } from './get_legend_items_labels'; +import { LEGEND_HIERARCHY_MARGIN } from '../../components/legend/legend_item'; const getParentDimensionSelector = (state: GlobalChartState) => state.parentDimensions; -const legendItemLabelsSelector = createCachedSelector( - [getSettingsSpecSelector, getLegendItemsSelector], - (settings, legendItems): string[] => { - const labels: string[] = []; - const { showLegendExtra } = settings; - legendItems.forEach((item) => { - const labelY1 = getItemLabel(item, 'y1'); - if (item.displayValue.formatted.y1 !== null) { - labels.push(`${labelY1}${showLegendExtra ? item.displayValue.formatted.y1 : ''}`); - } else { - labels.push(labelY1); - } - if (item.banded) { - const labelY0 = getItemLabel(item, 'y0'); - if (item.displayValue.formatted.y0 !== null) { - labels.push(`${labelY0}${showLegendExtra ? item.displayValue.formatted.y0 : ''}`); - } else { - labels.push(labelY0); - } - } - }); - return labels; - }, -)(getChartIdSelector); - const MARKER_WIDTH = 16; // const MARKER_HEIGHT = 16; const MARKER_LEFT_MARGIN = 4; @@ -62,11 +37,11 @@ const VERTICAL_PADDING = 4; /** @internal */ export const getLegendSizeSelector = createCachedSelector( - [getSettingsSpecSelector, getChartThemeSelector, getParentDimensionSelector, legendItemLabelsSelector], + [getSettingsSpecSelector, getChartThemeSelector, getParentDimensionSelector, getLegendItemsLabelsSelector], (settings, theme, parentDimensions, labels): BBox => { const bboxCalculator = new CanvasTextBBoxCalculator(); const bbox = labels.reduce( - (acc, label) => { + (acc, { label, depth }) => { const bbox = bboxCalculator.compute( label, 1, @@ -75,6 +50,7 @@ export const getLegendSizeSelector = createCachedSelector( 1.5, 400, ); + bbox.width += depth * LEGEND_HIERARCHY_MARGIN; if (acc.height < bbox.height) { acc.height = bbox.height; } diff --git a/packages/osd-charts/stories/legend/10_sunburst.tsx b/packages/osd-charts/stories/legend/10_sunburst.tsx new file mode 100644 index 000000000000..fe640eebe51e --- /dev/null +++ b/packages/osd-charts/stories/legend/10_sunburst.tsx @@ -0,0 +1,118 @@ +/* + * 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 { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import { config } from '../../src/chart_types/partition_chart/layout/config/config'; +import React from 'react'; +import { ShapeTreeNode } from '../../src/chart_types/partition_chart/layout/types/viewmodel_types'; +import { + categoricalFillColor, + colorBrewerCategoricalStark9, + countryLookup, + productLookup, + regionLookup, +} from '../utils/utils'; +import { boolean, number } from '@storybook/addon-knobs'; + +export const example = () => { + const flatLegend = boolean('flatLegend', true); + const legendMaxDepth = number('legendMaxDepth', 2, { + min: 0, + max: 3, + step: 1, + }); + + return ( + + + d.exportVal as number} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => d.sitc1, + nodeLabel: (d: any) => productLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d: any) => regionLookup[d].regionName, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => d.dest, + nodeLabel: (d: any) => countryLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + valueFormatter: (d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, + fontStyle: 'italic', + textInvertible: true, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + + ); +}; + +example.story = { + parameters: { + info: { + text: `To flatten a hierarchical legend (like the rendered in a pie chart or a treemap when using a multi-layer configuration) you can +add the \`flatLegend\` prop into the \`\` component. + +To limit displayed hierarchy to a specific depth, you can use the \`legendMaxDepth\` prop. The first layer will have a depth of \`1\`.`, + }, + }, +}; diff --git a/packages/osd-charts/stories/legend/9_color_picker.tsx b/packages/osd-charts/stories/legend/9_color_picker.tsx index d8d9e5c0a4b8..60f3fc579bcf 100644 --- a/packages/osd-charts/stories/legend/9_color_picker.tsx +++ b/packages/osd-charts/stories/legend/9_color_picker.tsx @@ -23,7 +23,7 @@ import { EuiColorPicker, EuiWrappingPopover, EuiButton, EuiSpacer } from '@elast import { Axis, BarSeries, Chart, Position, ScaleType, Settings, LegendColorPicker } from '../../src/'; import { BARCHART_1Y1G } from '../../src/utils/data_samples/test_dataset'; -import { SeriesKey } from '../../src/chart_types/xy_chart/utils/series'; +import { SeriesKey } from '../../src/commons/series_id'; import { Color } from '../../src/utils/commons'; const onChangeAction = action('onChange'); diff --git a/packages/osd-charts/stories/legend/legend.stories.tsx b/packages/osd-charts/stories/legend/legend.stories.tsx index 390ecab1f7e1..0a0117232b71 100644 --- a/packages/osd-charts/stories/legend/legend.stories.tsx +++ b/packages/osd-charts/stories/legend/legend.stories.tsx @@ -34,3 +34,4 @@ export { example as hideLegendItemsBySeries } from './6_hide_legend'; export { example as displayValuesInLegendElements } from './7_display_values'; export { example as legendSpacingBuffer } from './8_spacing_buffer'; export { example as colorPicker } from './9_color_picker'; +export { example as piechart } from './10_sunburst'; diff --git a/packages/osd-charts/stories/sunburst/15_single_sunburst.tsx b/packages/osd-charts/stories/sunburst/15_single_sunburst.tsx new file mode 100644 index 000000000000..5c2fe94d49c9 --- /dev/null +++ b/packages/osd-charts/stories/sunburst/15_single_sunburst.tsx @@ -0,0 +1,97 @@ +/* + * 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 { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import { config } from '../../src/chart_types/partition_chart/layout/config/config'; +import React from 'react'; +import { ShapeTreeNode } from '../../src/chart_types/partition_chart/layout/types/viewmodel_types'; +import { + categoricalFillColor, + colorBrewerCategoricalStark9, + countryLookup, + productLookup, + regionLookup, +} from '../utils/utils'; + +export const example = () => ( + + + d.exportVal as number} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => d.sitc1, + nodeLabel: (d: any) => productLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d: any) => regionLookup[d].regionName, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => d.dest, + nodeLabel: (d: any) => countryLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + valueFormatter: (d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, + fontStyle: 'italic', + textInvertible: true, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + +); diff --git a/packages/osd-charts/stories/sunburst/9_sunburst_three_layers.tsx b/packages/osd-charts/stories/sunburst/9_sunburst_three_layers.tsx index a8ecbbaa4fd1..0fba47da0962 100644 --- a/packages/osd-charts/stories/sunburst/9_sunburst_three_layers.tsx +++ b/packages/osd-charts/stories/sunburst/9_sunburst_three_layers.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; import { mocks } from '../../src/mocks/hierarchical/index'; import { config } from '../../src/chart_types/partition_chart/layout/config/config'; import React from 'react'; @@ -31,6 +31,7 @@ import { export const example = () => ( + d.country, countryDimension); const interpolatorTurbo = hueInterpolator(palettes.turbo.map(([r, g, b]) => [r, g, b, 0.7])); export const example = () => ( - + + d.country, countryDimension); const interpolatorTurbo = hueInterpolator(palettes.turbo.map(([r, g, b]) => [r, g, b, 0.7])); export const example = () => ( - + +