Skip to content

Commit 6a4247e

Browse files
authored
feat(partition): add legend and highlighters (elastic#616)
This commit adds the legend and highlighters to partition charts. Two new options are added to the <Settings/> components: `flatLegend` will flat the legend for pie/tree hierarchical legend and `legendMaxDepth` will limit the legend hierarchy to a maximum depth value: 1 is the root level (can be combined with the `flatLegend`). close elastic#486, close elastic#532
1 parent 59f0f6e commit 6a4247e

File tree

80 files changed

+1890
-832
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1890
-832
lines changed

.playground/playground.tsx

+3-58
Original file line numberDiff line numberDiff line change
@@ -17,65 +17,10 @@
1717
* under the License. */
1818

1919
import React from 'react';
20-
import {
21-
Chart,
22-
ScaleType,
23-
Position,
24-
Axis,
25-
Settings,
26-
PartitionElementEvent,
27-
XYChartElementEvent,
28-
BarSeries,
29-
} from '../src';
20+
import { example } from '../stories/sunburst/12_very_small';
3021

31-
export class Playground extends React.Component<{}, { isSunburstShown: boolean }> {
32-
onClick = (elements: Array<PartitionElementEvent | XYChartElementEvent>) => {
33-
// eslint-disable-next-line no-console
34-
console.log(elements[0]);
35-
};
22+
export class Playground extends React.Component {
3623
render() {
37-
return (
38-
<>
39-
<div className="chart">
40-
<Chart size={[300, 200]}>
41-
<Settings
42-
onElementClick={this.onClick}
43-
rotation={90}
44-
theme={{
45-
barSeriesStyle: {
46-
displayValue: {
47-
fontSize: 15,
48-
fill: 'black',
49-
offsetX: 5,
50-
offsetY: -8,
51-
},
52-
},
53-
}}
54-
/>
55-
<Axis id="y1" position={Position.Left} />
56-
<BarSeries
57-
id="amount"
58-
xScaleType={ScaleType.Ordinal}
59-
xAccessor="x"
60-
yAccessors={['y']}
61-
data={[
62-
{ x: 'trousers', y: 390, val: 1222 },
63-
{ x: 'watches', y: 0, val: 1222 },
64-
{ x: 'bags', y: 750, val: 1222 },
65-
{ x: 'cocktail dresses', y: 854, val: 1222 },
66-
]}
67-
displayValueSettings={{
68-
showValueLabel: true,
69-
isValueContainedInElement: true,
70-
hideClippedValue: true,
71-
valueFormatter: (d) => {
72-
return `${d} $`;
73-
},
74-
}}
75-
/>
76-
</Chart>
77-
</div>
78-
</>
79-
);
24+
return <div className="chart">{example()}</div>;
8025
}
8126
}

src/chart_types/goal_chart/state/chart_state.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ import { Tooltip } from '../../../components/tooltip';
2626
import { createOnElementClickCaller } from './selectors/on_element_click_caller';
2727
import { createOnElementOverCaller } from './selectors/on_element_over_caller';
2828
import { createOnElementOutCaller } from './selectors/on_element_out_caller';
29+
import { LegendItem } from '../../../commons/legend';
30+
import { LegendItemLabel } from '../../../state/selectors/get_legend_items_labels';
2931

3032
const EMPTY_MAP = new Map();
33+
const EMPTY_LEGEND_LIST: LegendItem[] = [];
34+
const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = [];
3135

3236
/** @internal */
3337
export class GoalState implements InternalChartState {
@@ -51,9 +55,12 @@ export class GoalState implements InternalChartState {
5155
return false;
5256
}
5357
getLegendItems() {
54-
return EMPTY_MAP;
58+
return EMPTY_LEGEND_LIST;
59+
}
60+
getLegendItemsLabels() {
61+
return EMPTY_LEGEND_ITEM_LIST;
5562
}
56-
getLegendItemsValues() {
63+
getLegendExtraValues() {
5764
return EMPTY_MAP;
5865
}
5966
chartRenderer(containerRef: BackwardRef) {

src/chart_types/goal_chart/state/selectors/on_element_click_caller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import { SettingsSpec, LayerValue } from '../../../../specs';
2424
import { getPickedShapesLayerValues } from './picked_shapes';
2525
import { getSpecOrNull } from './goal_spec';
2626
import { ChartTypes } from '../../..';
27-
import { SeriesIdentifier } from '../../../xy_chart/utils/series';
2827
import { isClicking } from '../../../../state/utils';
2928
import { getLastClickSelector } from '../../../../state/selectors/get_last_click';
29+
import { SeriesIdentifier } from '../../../../commons/series_id';
3030

3131
/**
3232
* Will call the onElementClick listener every time the following preconditions are met:

src/chart_types/goal_chart/state/selectors/on_element_over_caller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ChartTypes } from '../../../index';
2525
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
2626
import { getSpecOrNull } from './goal_spec';
2727
import { getPickedShapesLayerValues } from './picked_shapes';
28-
import { SeriesIdentifier } from '../../../xy_chart/utils/series';
28+
import { SeriesIdentifier } from '../../../../commons/series_id';
2929

3030
function isOverElement(prevPickedShapes: Array<Array<LayerValue>> = [], nextPickedShapes: Array<Array<LayerValue>>) {
3131
if (nextPickedShapes.length === 0) {

src/chart_types/partition_chart/layout/types/viewmodel_types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export type ShapeViewModel = {
8686
outsideLinksViewModel: OutsideLinksViewModel[];
8787
diskCenter: PointObject;
8888
pickQuads: PickFunction;
89+
outerRadius: number;
8990
};
9091

9192
export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({
@@ -96,6 +97,7 @@ export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointO
9697
outsideLinksViewModel: [],
9798
diskCenter: diskCenter || { x: 0, y: 0 },
9899
pickQuads: () => [],
100+
outerRadius: 0,
99101
});
100102

101103
type TreeLevel = number;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License. */
18+
19+
import { HierarchyOfArrays } from '../utils/group_by_rollup';
20+
import { Relation } from '../types/types';
21+
import { ValueAccessor } from '../../../../utils/commons';
22+
import { IndexedAccessorFn } from '../../../../utils/accessor';
23+
import {
24+
aggregateComparator,
25+
aggregators,
26+
childOrders,
27+
groupByRollup,
28+
mapEntryValue,
29+
mapsToArrays,
30+
} from '../utils/group_by_rollup';
31+
32+
export function getHierarchyOfArrays(
33+
rawFacts: Relation,
34+
valueAccessor: ValueAccessor,
35+
groupByRollupAccessors: IndexedAccessorFn[],
36+
): HierarchyOfArrays {
37+
const aggregator = aggregators.sum;
38+
39+
const facts = rawFacts.filter((n) => {
40+
const value = valueAccessor(n);
41+
return Number.isFinite(value) && value >= 0;
42+
});
43+
44+
// don't render anything if the total, the width or height is not positive
45+
if (facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0) {
46+
return [];
47+
}
48+
49+
// We can precompute things invariant of how the rectangle is divvied up.
50+
// By introducing `scale`, we no longer need to deal with the dichotomy of
51+
// size as data value vs size as number of pixels in the rectangle
52+
53+
return mapsToArrays(
54+
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts),
55+
aggregateComparator(mapEntryValue, childOrders.descending),
56+
);
57+
}

src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts

+6-32
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
* specific language governing permissions and limitations
1717
* under the License. */
1818

19-
import { Part, Relation, TextMeasure } from '../types/types';
19+
import { Part, TextMeasure } from '../types/types';
2020
import { linkTextLayout } from './link_text_layout';
2121
import { Config, PartitionLayout } from '../types/config_types';
2222
import { TAU, trueBearingToStandardPositionAngle } from '../utils/math';
2323
import { Distance, Pixels, Radius } from '../types/geometry_types';
2424
import { meanAngle } from '../geometry';
2525
import { treemap } from '../utils/treemap';
2626
import { sunburst } from '../utils/sunburst';
27-
import { IndexedAccessorFn } from '../../../../utils/accessor';
2827
import { argsToRGBString, stringToRGB } from '../utils/d3_utils';
2928
import {
3029
nullShapeViewModel,
@@ -49,20 +48,16 @@ import {
4948
} from './fill_text_layout';
5049
import {
5150
aggregateAccessor,
52-
aggregateComparator,
53-
aggregators,
5451
ArrayEntry,
55-
childOrders,
5652
depthAccessor,
5753
entryKey,
5854
entryValue,
59-
groupByRollup,
6055
mapEntryValue,
61-
mapsToArrays,
6256
parentAccessor,
6357
sortIndexAccessor,
58+
HierarchyOfArrays,
6459
} from '../utils/group_by_rollup';
65-
import { StrokeStyle, ValueAccessor, ValueFormatter } from '../../../../utils/commons';
60+
import { StrokeStyle, ValueFormatter } from '../../../../utils/commons';
6661
import { percentValueGetter } from '../config/config';
6762

6863
function paddingAccessor(n: ArrayEntry) {
@@ -148,13 +143,11 @@ export function shapeViewModel(
148143
textMeasure: TextMeasure,
149144
config: Config,
150145
layers: Layer[],
151-
rawFacts: Relation,
152146
rawTextGetter: RawTextGetter,
153-
valueAccessor: ValueAccessor,
154147
specifiedValueFormatter: ValueFormatter,
155148
specifiedPercentFormatter: ValueFormatter,
156149
valueGetter: ValueGetterFunction,
157-
groupByRollupAccessors: IndexedAccessorFn[],
150+
tree: HierarchyOfArrays,
158151
): ShapeViewModel {
159152
const {
160153
width,
@@ -179,31 +172,11 @@ export function shapeViewModel(
179172
y: height * margin.top + innerHeight / 2,
180173
};
181174

182-
const aggregator = aggregators.sum;
183-
184-
const facts = rawFacts.filter((n) => {
185-
const value = valueAccessor(n);
186-
return Number.isFinite(value) && value >= 0;
187-
});
188-
189175
// don't render anything if the total, the width or height is not positive
190-
if (
191-
facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0 ||
192-
!(width > 0) ||
193-
!(height > 0)
194-
) {
176+
if (!(width > 0) || !(height > 0) || tree.length === 0) {
195177
return nullShapeViewModel(config, diskCenter);
196178
}
197179

198-
// We can precompute things invariant of how the rectangle is divvied up.
199-
// By introducing `scale`, we no longer need to deal with the dichotomy of
200-
// size as data value vs size as number of pixels in the rectangle
201-
202-
const tree = mapsToArrays(
203-
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts),
204-
aggregateComparator(mapEntryValue, childOrders.descending),
205-
);
206-
207180
const totalValue = tree.reduce((p: number, n: ArrayEntry): number => p + mapEntryValue(n), 0);
208181

209182
const sunburstValueToAreaScale = TAU / totalValue;
@@ -332,5 +305,6 @@ export function shapeViewModel(
332305
linkLabelViewModels,
333306
outsideLinksViewModel,
334307
pickQuads,
308+
outerRadius,
335309
};
336310
}

src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function renderTaperedBorder(
7272
ctx.arc(0, 0, y0px, X0, X0);
7373
ctx.arc(0, 0, y1px, X0, X1, false);
7474
ctx.arc(0, 0, y0px, X1, X0, true);
75+
7576
ctx.fill();
7677
if (strokeWidth > 0.001 && !(x0 === 0 && x1 === TAU)) {
7778
// canvas2d uses a default of 1 if the lineWidth is assigned 0, so we use a small value to test, to avoid it

src/chart_types/partition_chart/renderer/canvas/partition.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { partitionGeometries } from '../../state/selectors/geometries';
2727
import { nullShapeViewModel, QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
2828
import { renderPartitionCanvas2d } from './canvas_renderers';
2929
import { INPUT_KEY } from '../../layout/utils/group_by_rollup';
30+
import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions';
3031

3132
interface ReactiveChartStateProps {
3233
initialized: boolean;
@@ -171,7 +172,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
171172
return {
172173
initialized: true,
173174
geometries: partitionGeometries(state),
174-
chartContainerDimensions: state.parentDimensions,
175+
chartContainerDimensions: getChartContainerDimensionsSelector(state),
175176
};
176177
};
177178

0 commit comments

Comments
 (0)