import React from "react";
import defaults from "lodash/defaults";
import pick from "lodash/pick";
import { VictoryTooltip } from "victory-tooltip";
import {
Helpers,
VictoryContainerProps,
PaddingProps,
VictoryContainer,
VictoryEventHandler,
} from "victory-core";
import { VoronoiHelpers } from "./voronoi-helpers";
export interface VictoryVoronoiContainerProps extends VictoryContainerProps {
activateData?: boolean;
activateLabels?: boolean;
disable?: boolean;
labels?: (point: any, index?: number, points?: any[]) => string;
labelComponent?: React.ReactElement;
mouseFollowTooltips?: boolean;
onActivated?: (points: any[], props: VictoryVoronoiContainerProps) => void;
onDeactivated?: (points: any[], props: VictoryVoronoiContainerProps) => void;
radius?: number;
voronoiBlacklist?: (string | RegExp)[];
voronoiDimension?: "x" | "y";
voronoiPadding?: PaddingProps;
horizontal?: boolean;
}
interface VictoryVoronoiContainerMutatedProps
extends VictoryVoronoiContainerProps {
mousePosition: { x: number; y: number };
activePoints: any[];
}
export const VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS = {
activateData: true,
activateLabels: true,
labelComponent: ,
voronoiPadding: 5,
};
const getPoint = (point) => {
const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"];
return pick(point, whitelist);
};
export const useVictoryVoronoiContainer = (
initialProps: VictoryVoronoiContainerProps,
) => {
const props = {
...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS,
...(initialProps as VictoryVoronoiContainerMutatedProps),
};
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 ? React.cloneElement(labelComponent, labelProps) : null;
}
return null;
};
return {
props,
children: [
...React.Children.toArray(children),
getTooltip(),
] as React.ReactElement[],
};
};
export const VictoryVoronoiContainer = (
initialProps: VictoryVoronoiContainerProps,
) => {
const { props, children } = useVictoryVoronoiContainer(initialProps);
return {children};
};
VictoryVoronoiContainer.role = "container";
VictoryVoronoiContainer.defaultEvents = (
initialProps: VictoryVoronoiContainerProps,
) => {
const props = { ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, ...initialProps };
const createEventHandler =
(handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler =>
// 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,
},
},
];
};