Skip to content

Commit

Permalink
feat(partition): add tooltip (opensearch-project#544)
Browse files Browse the repository at this point in the history
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 opensearch-project#246

Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com>
  • Loading branch information
monfera and markov00 committed Mar 2, 2020
1 parent 92f29e6 commit 0cffed4
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 164 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config } from './config_types';
import { Coordinate, Distance, PointObject, PointTuple, Radian } from './geometry_types';
import { Coordinate, Distance, Pixels, PointObject, PointTuple, Radian } from './geometry_types';
import { Font } from './types';
import { config } from '../config/config';
import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup';
Expand Down Expand Up @@ -54,23 +54,28 @@ export interface OutsideLinksViewModel {
points: Array<PointTuple>;
}

export type PickFunction = (x: Pixels, y: Pixels) => Array<QuadViewModel>;

export type ShapeViewModel = {
config: Config;
quadViewModel: QuadViewModel[];
rowSets: RowSet[];
linkLabelViewModels: LinkLabelVM[];
outsideLinksViewModel: OutsideLinksViewModel[];
diskCenter: PointObject;
pickQuads: PickFunction;
};

export const nullSectorViewModel = (): ShapeViewModel => ({
config,
export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({
config: specifiedConfig || config,
quadViewModel: [],
rowSets: [],
linkLabelViewModels: [],
outsideLinksViewModel: [],
diskCenter: { x: 0, y: 0 },
diskCenter: diskCenter || { x: 0, y: 0 },
pickQuads: () => [],
});

type TreeLevel = number;

interface AngleFromTo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { Datum } from '../../../../utils/commons';
export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate'
export const DEPTH_KEY = 'depth';
export const CHILDREN_KEY = 'children';
export const INPUT_KEY = 'inputIndex';
export const PARENT_KEY = 'parent';
export const SORT_INDEX_KEY = 'sortIndex';

interface NodeDescriptor {
[AGGREGATE_KEY]: number;
[DEPTH_KEY]: number;
[INPUT_KEY]?: Array<number>;
}

export type ArrayEntry = [Key, ArrayNode];
Expand Down Expand Up @@ -71,11 +73,13 @@ export function groupByRollup(
const keyExists = pointer.has(key);
const last = i === keyCount - 1;
const node = keyExists && pointer.get(key);
const inputIndices = node ? node[INPUT_KEY] : [];
const childrenMap = node ? node[CHILDREN_KEY] : new Map();
const aggregate = node ? node[AGGREGATE_KEY] : identity();
const reductionValue = reducer(aggregate, valueAccessor(n));
pointer.set(key, {
[AGGREGATE_KEY]: reductionValue,
[INPUT_KEY]: [...inputIndices, index],
[DEPTH_KEY]: i,
...(!last && { [CHILDREN_KEY]: childrenMap }),
});
Expand All @@ -91,7 +95,7 @@ export function groupByRollup(

function getRootArrayNode(): ArrayNode {
const children: HierarchyOfArrays = [];
const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children };
const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children, [INPUT_KEY]: [] as number[] };
Object.assign(bootstrap, { [PARENT_KEY]: bootstrap });
const result: ArrayNode = bootstrap as ArrayNode;
return result;
Expand All @@ -109,6 +113,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc
[DEPTH_KEY]: NaN,
[SORT_INDEX_KEY]: NaN,
[PARENT_KEY]: parent,
[INPUT_KEY]: [],
};
const newValue: ArrayNode = Object.assign(
resultNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { sunburst } from '../utils/sunburst';
import { IndexedAccessorFn } from '../../../../utils/accessor';
import { argsToRGBString, stringToRGB } from '../utils/d3_utils';
import {
nullShapeViewModel,
OutsideLinksViewModel,
PickFunction,
QuadViewModel,
RawTextGetter,
RowSet,
Expand Down Expand Up @@ -117,7 +119,7 @@ export function makeOutsideLinksViewModel(
})
.filter(({ points }: OutsideLinksViewModel) => points.length > 1);
}
// todo break up this long function

export function shapeViewModel(
textMeasure: TextMeasure,
config: Config,
Expand Down Expand Up @@ -158,14 +160,7 @@ export function shapeViewModel(
facts.some((n) => valueAccessor(n) < 0) ||
facts.reduce((p: number, n) => aggregator.reducer(p, valueAccessor(n)), aggregator.identity()) <= 0
) {
return {
config,
diskCenter,
quadViewModel: [],
rowSets: [],
linkLabelViewModels: [],
outsideLinksViewModel: [],
};
return nullShapeViewModel(config, diskCenter);
}

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

const pickQuads: PickFunction = (x, y) => {
return quadViewModel.filter(
treemapLayout
? ({ x0, y0, x1, y1 }) => x0 <= x && x <= x1 && y0 <= y && y <= y1
: ({ x0, y0px, x1, y1px }) => {
const angleX = (Math.atan2(y, x) + TAU / 4 + TAU) % TAU;
const yPx = Math.sqrt(x * x + y * y);
return x0 <= angleX && angleX <= x1 && y0px <= yPx && yPx <= y1px;
},
);
};

// combined viewModel
return {
config,
Expand All @@ -281,5 +288,6 @@ export function shapeViewModel(
rowSets,
linkLabelViewModels,
outsideLinksViewModel,
pickQuads,
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import React, { MouseEvent } from 'react';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { onChartRendered } from '../../../../state/actions/chart';
import { isInitialized } from '../../../../state/selectors/is_initialized';
import { GlobalChartState } from '../../../../state/chart_state';
import { Dimensions } from '../../../../utils/dimensions';
import { partitionGeometries } from '../../state/selectors/geometries';
import { nullSectorViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
import { nullShapeViewModel, QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
import { renderPartitionCanvas2d } from './canvas_renderers';
import { INPUT_KEY } from '../../layout/utils/group_by_rollup';

interface ReactiveChartStateProps {
initialized: boolean;
Expand Down Expand Up @@ -69,6 +70,38 @@ class PartitionComponent extends React.Component<PartitionProps> {
}
}

handleMouseMove(e: MouseEvent<HTMLCanvasElement>) {
const {
initialized,
chartContainerDimensions: { width, height },
} = this.props;
if (!this.canvasRef.current || !this.ctx || !initialized || width === 0 || height === 0) {
return;
}
const picker = this.props.geometries.pickQuads;
const box = this.canvasRef.current.getBoundingClientRect();
const diskCenter = this.props.geometries.diskCenter;
const x = e.clientX - box.left - diskCenter.x;
const y = e.clientY - box.top - diskCenter.y;
const pickedShapes: Array<QuadViewModel> = picker(x, y);
const datumIndices = new Set();
pickedShapes.forEach((shape) => {
const node = shape.parent;
const shapeNode = node.children.find(([key]) => key === shape.dataName);
if (shapeNode) {
const indices = shapeNode[1][INPUT_KEY] || [];
indices.forEach((i) => datumIndices.add(i));
}
});
/*
console.log(
pickedShapes.map((s) => s.value),
[...datumIndices.values()],
);
*/
return pickedShapes; // placeholder
}

render() {
const {
initialized,
Expand All @@ -84,6 +117,7 @@ class PartitionComponent extends React.Component<PartitionProps> {
className="echCanvasRenderer"
width={width * this.devicePixelRatio}
height={height * this.devicePixelRatio}
onMouseMove={this.handleMouseMove.bind(this)}
style={{
width,
height,
Expand All @@ -103,7 +137,7 @@ const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps =>

const DEFAULT_PROPS: ReactiveChartStateProps = {
initialized: false,
geometries: nullSectorViewModel(),
geometries: nullShapeViewModel(),
chartContainerDimensions: {
width: 0,
height: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from 'react';
import { InternalChartState } from '../../../state/chart_state';
import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state';
import { ChartTypes } from '../..';
import { Partition } from '../renderer/canvas/partition';
import { isTooltipVisibleSelector } from '../state/selectors/is_tooltip_visible';
import { getTooltipInfoSelector } from '../state/selectors/tooltip';
import { Tooltip } from '../../../components/tooltip';

const EMPTY_MAP = new Map();
export class PartitionState implements InternalChartState {
Expand All @@ -21,19 +24,29 @@ export class PartitionState implements InternalChartState {
getLegendItemsValues() {
return EMPTY_MAP;
}
chartRenderer() {
return <Partition />;
chartRenderer(containerRef: BackwardRef) {
return (
<>
<Tooltip getChartContainerRef={containerRef} />
<Partition />
</>
);
}
getPointerCursor() {
return 'default';
}
isTooltipVisible() {
return false;
isTooltipVisible(globalState: GlobalChartState) {
return isTooltipVisibleSelector(globalState);
}
getTooltipInfo() {
return undefined;
getTooltipInfo(globalState: GlobalChartState) {
return getTooltipInfoSelector(globalState);
}
getTooltipAnchor() {
return null;
getTooltipAnchor(state: GlobalChartState) {
const position = state.interactions.pointer.current.position;
return {
isRotated: false,
x1: position.x,
y1: position.y,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GlobalChartState } from '../../../../state/chart_state';
import { getSpecsFromStore } from '../../../../state/utils';
import { ChartTypes } from '../../..';
import { render } from './scenegraph';
import { nullSectorViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
import { PartitionSpec } from '../../specs/index';
import { SpecTypes } from '../../../../specs/settings';

Expand All @@ -15,6 +15,6 @@ export const partitionGeometries = createCachedSelector(
[getSpecs, getParentDimensions],
(specs, parentDimensions): ShapeViewModel => {
const pieSpecs = getSpecsFromStore<PartitionSpec>(specs, ChartTypes.Partition, SpecTypes.Series);
return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions) : nullSectorViewModel();
return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions) : nullShapeViewModel();
},
)((state) => state.chartId);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import createCachedSelector from 're-reselect';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';

import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { TooltipType, getTooltipType } from '../../../../specs';
import { getTooltipInfoSelector } from './tooltip';

/**
* The brush is available only for Ordinal xScales charts and
* if we have configured an onBrushEnd listener
*/
export const isTooltipVisibleSelector = createCachedSelector(
[getSettingsSpecSelector, getTooltipInfoSelector],
(settingsSpec, tooltipInfo): boolean => {
if (getTooltipType(settingsSpec) === TooltipType.None) {
return false;
}
return tooltipInfo.values.length > 0;
},
)(getChartIdSelector);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Dimensions } from '../../../../utils/dimensions';
import { shapeViewModel } from '../../layout/viewmodel/viewmodel';
import { measureText } from '../../layout/utils/measure';
import { ShapeTreeNode, ShapeViewModel, RawTextGetter } from '../../layout/types/viewmodel_types';
import { ShapeTreeNode, ShapeViewModel, RawTextGetter, nullShapeViewModel } from '../../layout/types/viewmodel_types';
import { DEPTH_KEY } from '../../layout/utils/group_by_rollup';
import { PartitionSpec, Layer } from '../../specs/index';
import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons';
Expand All @@ -23,14 +23,7 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension
const partialConfig: RecursivePartial<Config> = { ...specConfig, width, height };
const config: Config = mergePartial(defaultConfig, partialConfig);
if (!textMeasurerCtx) {
return {
config,
quadViewModel: [],
rowSets: [],
linkLabelViewModels: [],
outsideLinksViewModel: [],
diskCenter: { x: width / 2, y: height / 2 },
};
return nullShapeViewModel(config, { x: width / 2, y: height / 2 });
}
return shapeViewModel(
measureText(textMeasurerCtx),
Expand Down
Loading

0 comments on commit 0cffed4

Please sign in to comment.