Skip to content

Commit 6bf9a69

Browse files
monferamarkov00
andcommitted
feat(partition): add tooltip (#544)
This commit moves the `Datum`, `Rotation`, `Position` and `Color` into `utils/commons`. It decouples the legend from axis position and moves the `scales` to `utils/scales`. It decouples the tooltip component from the XY chart to allow Partition charts and other chart to use it. close #246 Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com>
1 parent 9114476 commit 6bf9a69

File tree

17 files changed

+286
-164
lines changed

17 files changed

+286
-164
lines changed
Loading

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Config } from './config_types';
2-
import { Coordinate, Distance, PointObject, PointTuple, Radian } from './geometry_types';
2+
import { Coordinate, Distance, Pixels, PointObject, PointTuple, Radian } from './geometry_types';
33
import { Font } from './types';
44
import { config } from '../config/config';
55
import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup';
@@ -54,23 +54,28 @@ export interface OutsideLinksViewModel {
5454
points: Array<PointTuple>;
5555
}
5656

57+
export type PickFunction = (x: Pixels, y: Pixels) => Array<QuadViewModel>;
58+
5759
export type ShapeViewModel = {
5860
config: Config;
5961
quadViewModel: QuadViewModel[];
6062
rowSets: RowSet[];
6163
linkLabelViewModels: LinkLabelVM[];
6264
outsideLinksViewModel: OutsideLinksViewModel[];
6365
diskCenter: PointObject;
66+
pickQuads: PickFunction;
6467
};
6568

66-
export const nullSectorViewModel = (): ShapeViewModel => ({
67-
config,
69+
export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({
70+
config: specifiedConfig || config,
6871
quadViewModel: [],
6972
rowSets: [],
7073
linkLabelViewModels: [],
7174
outsideLinksViewModel: [],
72-
diskCenter: { x: 0, y: 0 },
75+
diskCenter: diskCenter || { x: 0, y: 0 },
76+
pickQuads: () => [],
7377
});
78+
7479
type TreeLevel = number;
7580

7681
interface AngleFromTo {

src/chart_types/partition_chart/layout/utils/group_by_rollup.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { Datum } from '../../../../utils/commons';
44
export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate'
55
export const DEPTH_KEY = 'depth';
66
export const CHILDREN_KEY = 'children';
7+
export const INPUT_KEY = 'inputIndex';
78
export const PARENT_KEY = 'parent';
89
export const SORT_INDEX_KEY = 'sortIndex';
910

1011
interface NodeDescriptor {
1112
[AGGREGATE_KEY]: number;
1213
[DEPTH_KEY]: number;
14+
[INPUT_KEY]?: Array<number>;
1315
}
1416

1517
export type ArrayEntry = [Key, ArrayNode];
@@ -71,11 +73,13 @@ export function groupByRollup(
7173
const keyExists = pointer.has(key);
7274
const last = i === keyCount - 1;
7375
const node = keyExists && pointer.get(key);
76+
const inputIndices = node ? node[INPUT_KEY] : [];
7477
const childrenMap = node ? node[CHILDREN_KEY] : new Map();
7578
const aggregate = node ? node[AGGREGATE_KEY] : identity();
7679
const reductionValue = reducer(aggregate, valueAccessor(n));
7780
pointer.set(key, {
7881
[AGGREGATE_KEY]: reductionValue,
82+
[INPUT_KEY]: [...inputIndices, index],
7983
[DEPTH_KEY]: i,
8084
...(!last && { [CHILDREN_KEY]: childrenMap }),
8185
});
@@ -91,7 +95,7 @@ export function groupByRollup(
9195

9296
function getRootArrayNode(): ArrayNode {
9397
const children: HierarchyOfArrays = [];
94-
const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children };
98+
const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children, [INPUT_KEY]: [] as number[] };
9599
Object.assign(bootstrap, { [PARENT_KEY]: bootstrap });
96100
const result: ArrayNode = bootstrap as ArrayNode;
97101
return result;
@@ -109,6 +113,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc
109113
[DEPTH_KEY]: NaN,
110114
[SORT_INDEX_KEY]: NaN,
111115
[PARENT_KEY]: parent,
116+
[INPUT_KEY]: [],
112117
};
113118
const newValue: ArrayNode = Object.assign(
114119
resultNode,

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { sunburst } from '../utils/sunburst';
99
import { IndexedAccessorFn } from '../../../../utils/accessor';
1010
import { argsToRGBString, stringToRGB } from '../utils/d3_utils';
1111
import {
12+
nullShapeViewModel,
1213
OutsideLinksViewModel,
14+
PickFunction,
1315
QuadViewModel,
1416
RawTextGetter,
1517
RowSet,
@@ -117,7 +119,7 @@ export function makeOutsideLinksViewModel(
117119
})
118120
.filter(({ points }: OutsideLinksViewModel) => points.length > 1);
119121
}
120-
// todo break up this long function
122+
121123
export function shapeViewModel(
122124
textMeasure: TextMeasure,
123125
config: Config,
@@ -158,14 +160,7 @@ export function shapeViewModel(
158160
facts.some((n) => valueAccessor(n) < 0) ||
159161
facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0
160162
) {
161-
return {
162-
config,
163-
diskCenter,
164-
quadViewModel: [],
165-
rowSets: [],
166-
linkLabelViewModels: [],
167-
outsideLinksViewModel: [],
168-
};
163+
return nullShapeViewModel(config, diskCenter);
169164
}
170165

171166
// We can precompute things invariant of how the rectangle is divvied up.
@@ -273,6 +268,18 @@ export function shapeViewModel(
273268
valueFormatter,
274269
);
275270

271+
const pickQuads: PickFunction = (x, y) => {
272+
return quadViewModel.filter(
273+
treemapLayout
274+
? ({ x0, y0, x1, y1 }) => x0 <= x && x <= x1 && y0 <= y && y <= y1
275+
: ({ x0, y0px, x1, y1px }) => {
276+
const angleX = (Math.atan2(y, x) + TAU / 4 + TAU) % TAU;
277+
const yPx = Math.sqrt(x * x + y * y);
278+
return x0 <= angleX && angleX <= x1 && y0px <= yPx && yPx <= y1px;
279+
},
280+
);
281+
};
282+
276283
// combined viewModel
277284
return {
278285
config,
@@ -281,5 +288,6 @@ export function shapeViewModel(
281288
rowSets,
282289
linkLabelViewModels,
283290
outsideLinksViewModel,
291+
pickQuads,
284292
};
285293
}

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import React from 'react';
1+
import React, { MouseEvent } from 'react';
22
import { bindActionCreators, Dispatch } from 'redux';
33
import { connect } from 'react-redux';
44
import { onChartRendered } from '../../../../state/actions/chart';
55
import { isInitialized } from '../../../../state/selectors/is_initialized';
66
import { GlobalChartState } from '../../../../state/chart_state';
77
import { Dimensions } from '../../../../utils/dimensions';
88
import { partitionGeometries } from '../../state/selectors/geometries';
9-
import { nullSectorViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
9+
import { nullShapeViewModel, QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
1010
import { renderPartitionCanvas2d } from './canvas_renderers';
11+
import { INPUT_KEY } from '../../layout/utils/group_by_rollup';
1112

1213
interface ReactiveChartStateProps {
1314
initialized: boolean;
@@ -69,6 +70,38 @@ class PartitionComponent extends React.Component<PartitionProps> {
6970
}
7071
}
7172

73+
handleMouseMove(e: MouseEvent<HTMLCanvasElement>) {
74+
const {
75+
initialized,
76+
chartContainerDimensions: { width, height },
77+
} = this.props;
78+
if (!this.canvasRef.current || !this.ctx || !initialized || width === 0 || height === 0) {
79+
return;
80+
}
81+
const picker = this.props.geometries.pickQuads;
82+
const box = this.canvasRef.current.getBoundingClientRect();
83+
const diskCenter = this.props.geometries.diskCenter;
84+
const x = e.clientX - box.left - diskCenter.x;
85+
const y = e.clientY - box.top - diskCenter.y;
86+
const pickedShapes: Array<QuadViewModel> = picker(x, y);
87+
const datumIndices = new Set();
88+
pickedShapes.forEach((shape) => {
89+
const node = shape.parent;
90+
const shapeNode = node.children.find(([key]) => key === shape.dataName);
91+
if (shapeNode) {
92+
const indices = shapeNode[1][INPUT_KEY] || [];
93+
indices.forEach((i) => datumIndices.add(i));
94+
}
95+
});
96+
/*
97+
console.log(
98+
pickedShapes.map((s) => s.value),
99+
[...datumIndices.values()],
100+
);
101+
*/
102+
return pickedShapes; // placeholder
103+
}
104+
72105
render() {
73106
const {
74107
initialized,
@@ -84,6 +117,7 @@ class PartitionComponent extends React.Component<PartitionProps> {
84117
className="echCanvasRenderer"
85118
width={width * this.devicePixelRatio}
86119
height={height * this.devicePixelRatio}
120+
onMouseMove={this.handleMouseMove.bind(this)}
87121
style={{
88122
width,
89123
height,
@@ -103,7 +137,7 @@ const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps =>
103137

104138
const DEFAULT_PROPS: ReactiveChartStateProps = {
105139
initialized: false,
106-
geometries: nullSectorViewModel(),
140+
geometries: nullShapeViewModel(),
107141
chartContainerDimensions: {
108142
width: 0,
109143
height: 0,

src/chart_types/partition_chart/state/chart_state.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React from 'react';
2-
import { InternalChartState } from '../../../state/chart_state';
2+
import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state';
33
import { ChartTypes } from '../..';
44
import { Partition } from '../renderer/canvas/partition';
5+
import { isTooltipVisibleSelector } from '../state/selectors/is_tooltip_visible';
6+
import { getTooltipInfoSelector } from '../state/selectors/tooltip';
7+
import { Tooltip } from '../../../components/tooltip';
58

69
const EMPTY_MAP = new Map();
710
export class PartitionState implements InternalChartState {
@@ -21,19 +24,29 @@ export class PartitionState implements InternalChartState {
2124
getLegendItemsValues() {
2225
return EMPTY_MAP;
2326
}
24-
chartRenderer() {
25-
return <Partition />;
27+
chartRenderer(containerRef: BackwardRef) {
28+
return (
29+
<>
30+
<Tooltip getChartContainerRef={containerRef} />
31+
<Partition />
32+
</>
33+
);
2634
}
2735
getPointerCursor() {
2836
return 'default';
2937
}
30-
isTooltipVisible() {
31-
return false;
38+
isTooltipVisible(globalState: GlobalChartState) {
39+
return isTooltipVisibleSelector(globalState);
3240
}
33-
getTooltipInfo() {
34-
return undefined;
41+
getTooltipInfo(globalState: GlobalChartState) {
42+
return getTooltipInfoSelector(globalState);
3543
}
36-
getTooltipAnchor() {
37-
return null;
44+
getTooltipAnchor(state: GlobalChartState) {
45+
const position = state.interactions.pointer.current.position;
46+
return {
47+
isRotated: false,
48+
x1: position.x,
49+
y1: position.y,
50+
};
3851
}
3952
}

src/chart_types/partition_chart/state/selectors/geometries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { GlobalChartState } from '../../../../state/chart_state';
33
import { getSpecsFromStore } from '../../../../state/utils';
44
import { ChartTypes } from '../../..';
55
import { render } from './scenegraph';
6-
import { nullSectorViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
6+
import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
77
import { PartitionSpec } from '../../specs/index';
88
import { SpecTypes } from '../../../../specs/settings';
99

@@ -15,6 +15,6 @@ export const partitionGeometries = createCachedSelector(
1515
[getSpecs, getParentDimensions],
1616
(specs, parentDimensions): ShapeViewModel => {
1717
const pieSpecs = getSpecsFromStore<PartitionSpec>(specs, ChartTypes.Partition, SpecTypes.Series);
18-
return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions) : nullSectorViewModel();
18+
return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions) : nullShapeViewModel();
1919
},
2020
)((state) => state.chartId);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import createCachedSelector from 're-reselect';
2+
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
3+
4+
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
5+
import { TooltipType, getTooltipType } from '../../../../specs';
6+
import { getTooltipInfoSelector } from './tooltip';
7+
8+
/**
9+
* The brush is available only for Ordinal xScales charts and
10+
* if we have configured an onBrushEnd listener
11+
*/
12+
export const isTooltipVisibleSelector = createCachedSelector(
13+
[getSettingsSpecSelector, getTooltipInfoSelector],
14+
(settingsSpec, tooltipInfo): boolean => {
15+
if (getTooltipType(settingsSpec) === TooltipType.None) {
16+
return false;
17+
}
18+
return tooltipInfo.values.length > 0;
19+
},
20+
)(getChartIdSelector);

src/chart_types/partition_chart/state/selectors/scenegraph.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Dimensions } from '../../../../utils/dimensions';
22
import { shapeViewModel } from '../../layout/viewmodel/viewmodel';
33
import { measureText } from '../../layout/utils/measure';
4-
import { ShapeTreeNode, ShapeViewModel, RawTextGetter } from '../../layout/types/viewmodel_types';
4+
import { ShapeTreeNode, ShapeViewModel, RawTextGetter, nullShapeViewModel } from '../../layout/types/viewmodel_types';
55
import { DEPTH_KEY } from '../../layout/utils/group_by_rollup';
66
import { PartitionSpec, Layer } from '../../specs/index';
77
import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons';
@@ -23,14 +23,7 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension
2323
const partialConfig: RecursivePartial<Config> = { ...specConfig, width, height };
2424
const config: Config = mergePartial(defaultConfig, partialConfig);
2525
if (!textMeasurerCtx) {
26-
return {
27-
config,
28-
quadViewModel: [],
29-
rowSets: [],
30-
linkLabelViewModels: [],
31-
outsideLinksViewModel: [],
32-
diskCenter: { x: width / 2, y: height / 2 },
33-
};
26+
return nullShapeViewModel(config, { x: width / 2, y: height / 2 });
3427
}
3528
return shapeViewModel(
3629
measureText(textMeasurerCtx),

0 commit comments

Comments
 (0)