victory-voronoi-container
Version:
Interactive Voronoi Mouseover Component for Victory
266 lines (245 loc) • 8.12 kB
text/typescript
import { Collection, Selection, Data, Helpers } from "victory-core";
import isEmpty from "lodash/isEmpty";
import isRegExp from "lodash/isRegExp";
import throttle from "lodash/throttle";
import isEqual from "react-fast-compare";
import Delaunay from "delaunay-find/lib/index.js";
import React from "react";
const ON_MOUSE_MOVE_THROTTLE_MS = 32;
class VoronoiHelpersClass {
withinBounds(props, point) {
const { width, height, polar, origin, scale } = props;
const padding = Helpers.getPadding(props.voronoiPadding);
const { x, y } = point;
if (polar) {
const distanceSquared =
Math.pow(x - origin.x, 2) + Math.pow(y - origin.y, 2);
const radius = Math.max(...scale.y.range());
return distanceSquared < Math.pow(radius, 2);
}
return (
x >= padding.left &&
x <= width - padding.right &&
y >= padding.top &&
y <= height - padding.bottom
);
}
getDatasets(props) {
const minDomain = {
x: Collection.getMinValue(props.domain.x),
y: Collection.getMinValue(props.domain.y),
};
const children = React.Children.toArray(props.children);
const addMeta = (data, name?, child?) => {
const continuous = child && child.type && child.type.continuous;
const style = child ? child.props && child.props.style : props.style;
return data.map((datum, index) => {
const { x, y, y0, x0 } = Helpers.getPoint(datum);
const voronoiX = (Number(x) + Number(x0)) / 2;
const voronoiY = (Number(y) + Number(y0)) / 2;
return Object.assign(
{
_voronoiX: props.voronoiDimension === "y" ? minDomain.x : voronoiX,
_voronoiY: props.voronoiDimension === "x" ? minDomain.y : voronoiY,
eventKey: index,
childName: name,
continuous,
style,
},
datum,
);
});
};
if (props.data) {
return addMeta(props.data);
}
const getData = (childProps) => {
const data = Data.getData(childProps);
return Array.isArray(data) && data.length > 0 ? data : undefined;
};
const iteratee = (child, childName) => {
const childProps = child.props || {};
const name = childProps.name || childName;
const blacklist = props.voronoiBlacklist || [];
const blacklistStr = blacklist.filter(
(value) => !!value && typeof value.valueOf() === "string",
);
const blacklistRegExp = blacklist.filter(isRegExp);
const isRegExpMatch = blacklistRegExp.some((regExp) => regExp.test(name));
if (
!Data.isDataComponent(child) ||
blacklistStr.includes(name) ||
isRegExpMatch
) {
return null;
}
const getChildData =
child.type && Helpers.isFunction(child.type.getData)
? child.type.getData
: getData;
const childData = getChildData(child.props);
return childData ? addMeta(childData, name, child) : null;
};
return Helpers.reduceChildren(children, iteratee, props);
}
findPoints(datasets, point) {
return datasets.filter((d) => {
return point._voronoiX === d._voronoiX && point._voronoiY === d._voronoiY;
});
}
withinRadius(point, mousePosition, radius) {
if (!point) {
return false;
}
if (!radius) {
return true;
}
const { x, y } = mousePosition;
const distanceSquared =
Math.pow(x - point[0], 2) + Math.pow(y - point[1], 2);
return distanceSquared < Math.pow(radius, 2);
}
getVoronoiPoints(props, mousePosition) {
const datasets = this.getDatasets(props);
const scaledData = datasets.map((d) => {
const { x, y } = Helpers.scalePoint(props, d);
return [x, y];
});
const delaunay = Delaunay.from(scaledData);
const index = delaunay.find(mousePosition.x, mousePosition.y);
const withinRadius = this.withinRadius(
scaledData[index],
mousePosition,
props.radius,
);
const points = withinRadius
? this.findPoints(datasets, datasets[index])
: [];
return { points, index };
}
getActiveMutations(props, point) {
const { childName, continuous } = point;
const { activateData, activateLabels, labels } = props;
if (!activateData && !activateLabels) {
return [];
}
const defaultTarget = activateData ? ["data"] : [];
const targets =
labels && !activateLabels
? defaultTarget
: defaultTarget.concat("labels");
if (isEmpty(targets)) {
return [];
}
return targets.map((target) => {
const eventKey =
continuous === true && target === "data" ? "all" : point.eventKey;
return {
childName,
eventKey,
target,
mutation: () => ({ active: true }),
};
});
}
getInactiveMutations(props, point) {
const { childName, continuous } = point;
const { activateData, activateLabels, labels } = props;
if (!activateData && !activateLabels) {
return [];
}
const defaultTarget = activateData ? ["data"] : [];
const targets =
labels && !activateLabels
? defaultTarget
: defaultTarget.concat("labels");
if (isEmpty(targets)) {
return [];
}
return targets.map((target) => {
const eventKey = continuous && target === "data" ? "all" : point.eventKey;
return {
childName,
eventKey,
target,
mutation: () => null,
};
});
}
// eslint-disable-next-line max-params
getParentMutation(activePoints, mousePosition?, parentSVG?, vIndex?) {
return [
{
target: "parent",
eventKey: "parent",
mutation: () => ({ activePoints, mousePosition, parentSVG, vIndex }),
},
];
}
onActivated(props, points) {
if (Helpers.isFunction(props.onActivated)) {
props.onActivated(points, props);
}
}
onDeactivated(props, points) {
if (Helpers.isFunction(props.onDeactivated)) {
props.onDeactivated(points, props);
}
}
onMouseLeave = (evt, targetProps) => {
this.onMouseMove.cancel();
const activePoints = targetProps.activePoints || [];
this.onDeactivated(targetProps, activePoints);
const inactiveMutations = activePoints.length
? activePoints.map((point) =>
this.getInactiveMutations(targetProps, point),
)
: [];
return this.getParentMutation([]).concat(...inactiveMutations);
};
private handleMouseMove = (evt, targetProps) => {
const activePoints = targetProps.activePoints || [];
const parentSVG = targetProps.parentSVG || Selection.getParentSVG(evt);
const mousePosition = Selection.getSVGEventCoordinates(evt, parentSVG);
if (!this.withinBounds(targetProps, mousePosition)) {
this.onDeactivated(targetProps, activePoints);
const inactiveMutations = activePoints.length
? activePoints.map((point) =>
this.getInactiveMutations(targetProps, point),
)
: [];
return this.getParentMutation([], mousePosition, parentSVG).concat(
...inactiveMutations,
);
}
const { points = [], index } = this.getVoronoiPoints(
targetProps,
mousePosition,
);
const parentMutations = this.getParentMutation(
points,
mousePosition,
parentSVG,
index,
);
if (activePoints.length && isEqual(points, activePoints)) {
return parentMutations;
}
this.onActivated(targetProps, points);
this.onDeactivated(targetProps, activePoints);
const activeMutations = points.length
? points.map((point) => this.getActiveMutations(targetProps, point))
: [];
const inactiveMutations = activePoints.length
? activePoints.map((point) =>
this.getInactiveMutations(targetProps, point),
)
: [];
return parentMutations.concat(...inactiveMutations, ...activeMutations);
};
onMouseMove = throttle(this.handleMouseMove, ON_MOUSE_MOVE_THROTTLE_MS, {
leading: true,
trailing: false,
});
}
export const VoronoiHelpers = new VoronoiHelpersClass();