victory-voronoi-container
Version:
Interactive Voronoi Mouseover Component for Victory
216 lines (214 loc) • 7.01 kB
JavaScript
import React from "react";
import defaults from "lodash/defaults";
import pick from "lodash/pick";
import { VictoryTooltip } from "victory-tooltip";
import { Helpers, VictoryContainer } from "victory-core";
import { VoronoiHelpers } from "./voronoi-helpers";
export const VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS = {
activateData: true,
activateLabels: true,
labelComponent: /*#__PURE__*/React.createElement(VictoryTooltip, null),
voronoiPadding: 5
};
const getPoint = point => {
const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"];
return pick(point, whitelist);
};
export const useVictoryVoronoiContainer = initialProps => {
const props = {
...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS,
...initialProps
};
const {
children
} = props;
const getDimension = () => {
const {
horizontal,
voronoiDimension
} = props;
if (!horizontal || !voronoiDimension) {
return voronoiDimension;
}
return voronoiDimension === "x" ? "y" : "x";
};
const getLabelPosition = (labelProps, points) => {
const {
mousePosition,
mouseFollowTooltips
} = props;
const voronoiDimension = getDimension();
const point = getPoint(points[0]);
// @ts-expect-error scale is defined but the types do not reflect that
const basePosition = Helpers.scalePoint(props, point);
let center = mouseFollowTooltips ? mousePosition : undefined;
if (!voronoiDimension || points.length < 2) {
return {
...basePosition,
center: defaults({}, labelProps.center, center)
};
}
const x = voronoiDimension === "y" ? mousePosition.x : basePosition.x;
const y = voronoiDimension === "x" ? mousePosition.y : basePosition.y;
center = mouseFollowTooltips ? mousePosition : {
x,
y
};
return {
x,
y,
center: defaults({}, labelProps.center, center)
};
};
const getStyle = (points, type) => {
const {
labels,
labelComponent,
theme
} = props;
const componentProps = labelComponent.props || {};
const themeStyles = theme && theme.voronoi && theme.voronoi.style ? theme.voronoi.style : {};
const componentStyleArray = type === "flyout" ? componentProps.flyoutStyle : componentProps.style;
return points.reduce((memo, datum, index) => {
const labelProps = defaults({}, componentProps, {
datum,
active: true
});
const text = Helpers.isFunction(labels) ? labels(labelProps) : undefined;
const textArray = text !== undefined ? `${text}`.split("\n") : [];
const baseStyle = datum.style && datum.style[type] || {};
const componentStyle = Array.isArray(componentStyleArray) ? componentStyleArray[index] : componentStyleArray;
const style = Helpers.evaluateStyle(defaults({}, componentStyle, baseStyle, themeStyles[type]), labelProps);
const styleArray = textArray.length ? textArray.map(() => style) : [style];
return memo.concat(styleArray);
}, []);
};
const getDefaultLabelProps = points => {
const {
voronoiDimension,
horizontal,
mouseFollowTooltips
} = props;
const point = getPoint(points[0]);
const multiPoint = voronoiDimension && points.length > 1;
const y = point._y1 !== undefined ? point._y1 : point._y;
const defaultHorizontalOrientation = y < 0 ? "left" : "right";
const defaultOrientation = y < 0 ? "bottom" : "top";
const labelOrientation = horizontal ? defaultHorizontalOrientation : defaultOrientation;
const orientation = mouseFollowTooltips ? undefined : labelOrientation;
return {
orientation,
pointerLength: multiPoint ? 0 : undefined,
constrainToVisibleArea: multiPoint || mouseFollowTooltips ? true : undefined
};
};
const getLabelProps = points => {
const {
labels,
scale,
labelComponent,
theme,
width,
height
} = props;
const componentProps = labelComponent.props || {};
const text = points.reduce((memo, datum) => {
const labelProps = defaults({}, componentProps, {
datum,
active: true
});
const t = Helpers.isFunction(labels) ? labels(labelProps) : null;
if (t === null || t === undefined) {
return memo;
}
return memo.concat(`${t}`.split("\n"));
}, []);
// remove properties from first point to make datum
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
childName,
eventKey,
style,
continuous,
...datum
} = points[0];
const name = props.name === childName ? childName : `${props.name}-${childName}`;
const labelProps = defaults({
key: `${name}-${eventKey}-voronoi-tooltip`,
id: `${name}-${eventKey}-voronoi-tooltip`,
active: true,
renderInPortal: false,
activePoints: points,
datum,
scale,
theme
}, componentProps, {
text,
width,
height,
style: getStyle(points, "labels"),
flyoutStyle: getStyle(points, "flyout")[0]
}, getDefaultLabelProps(points));
const labelPosition = getLabelPosition(labelProps, points);
return defaults({}, labelPosition, labelProps);
};
const getTooltip = () => {
const {
labels,
activePoints,
labelComponent
} = props;
if (!labels) {
return null;
}
if (Array.isArray(activePoints) && activePoints.length) {
const labelProps = getLabelProps(activePoints);
const {
text
} = labelProps;
const showLabel = Array.isArray(text) ? text.filter(Boolean).length : text;
return showLabel ? /*#__PURE__*/React.cloneElement(labelComponent, labelProps) : null;
}
return null;
};
return {
props,
children: [...React.Children.toArray(children), getTooltip()]
};
};
export const VictoryVoronoiContainer = initialProps => {
const {
props,
children
} = useVictoryVoronoiContainer(initialProps);
return /*#__PURE__*/React.createElement(VictoryContainer, props, children);
};
VictoryVoronoiContainer.role = "container";
VictoryVoronoiContainer.defaultEvents = initialProps => {
const props = {
...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS,
...initialProps
};
const createEventHandler = (handler, disabled) =>
// eslint-disable-next-line max-params
(event, targetProps, eventKey, context) => disabled || props.disable ? {} : handler(event, {
...props,
...targetProps
}, eventKey, context);
return [{
target: "parent",
eventHandlers: {
onMouseLeave: createEventHandler(VoronoiHelpers.onMouseLeave),
onTouchCancel: createEventHandler(VoronoiHelpers.onMouseLeave),
onMouseMove: createEventHandler(VoronoiHelpers.onMouseMove),
onTouchMove: createEventHandler(VoronoiHelpers.onMouseMove)
}
}, {
target: "data",
eventHandlers: props.disable ? {} : {
onMouseOver: () => null,
onMouseOut: () => null,
onMouseMove: () => null
}
}];
};