import React from "react"; import defaults from "lodash/defaults"; import { Helpers, Path, UserProps, VictoryCommonProps, OrientationTypes, VictoryStyleObject, StringOrNumberOrCallback, } from "victory-core"; export interface FlyoutProps extends VictoryCommonProps { active?: boolean; center?: { x: number; y: number; }; className?: string; clipPath?: string; cornerRadius?: number; data?: any[]; datum?: object; dx?: number; dy?: number; events?: object; id?: StringOrNumberOrCallback; index?: number; orientation?: OrientationTypes; pathComponent?: React.ReactElement; pointerLength?: number; pointerWidth?: number; role?: string; shapeRendering?: string; style?: VictoryStyleObject; transform?: string; x?: number; y?: number; } interface FlyoutPathProps { center: { x: number; y: number; }; cornerRadius: number; dx?: number; dy?: number; height: number; orientation: OrientationTypes; pointerLength: number; pointerWidth: number; width: number; x: number; y: number; } const getVerticalPath = (props: FlyoutPathProps) => { const { pointerWidth, cornerRadius, orientation, width, height, center } = props; const sign = orientation === "bottom" ? 1 : -1; const x = props.x + (props.dx || 0); const y = props.y + (props.dy || 0); const centerX = center.x; const centerY = center.y; const pointerEdge = centerY + sign * (height / 2); const oppositeEdge = centerY - sign * (height / 2); const rightEdge = centerX + width / 2; const leftEdge = centerX - width / 2; const pointerLength = sign * (y - pointerEdge) < 0 ? 0 : props.pointerLength; const direction = orientation === "bottom" ? "0 0 0" : "0 0 1"; const arc = `${cornerRadius} ${cornerRadius} ${direction}`; return `M ${centerX - pointerWidth / 2}, ${pointerEdge} L ${pointerLength ? x : centerX + pointerWidth / 2}, ${ pointerLength ? y : pointerEdge } L ${centerX + pointerWidth / 2}, ${pointerEdge} L ${rightEdge - cornerRadius}, ${pointerEdge} A ${arc} ${rightEdge}, ${pointerEdge - sign * cornerRadius} L ${rightEdge}, ${oppositeEdge + sign * cornerRadius} A ${arc} ${rightEdge - cornerRadius}, ${oppositeEdge} L ${leftEdge + cornerRadius}, ${oppositeEdge} A ${arc} ${leftEdge}, ${oppositeEdge + sign * cornerRadius} L ${leftEdge}, ${pointerEdge - sign * cornerRadius} A ${arc} ${leftEdge + cornerRadius}, ${pointerEdge} z`; }; const getHorizontalPath = (props: FlyoutPathProps) => { const { pointerWidth, cornerRadius, orientation, width, height, center } = props; const sign = orientation === "left" ? 1 : -1; const x = props.x + (props.dx || 0); const y = props.y + (props.dy || 0); const centerX = center.x; const centerY = center.y; const pointerEdge = centerX - sign * (width / 2); const oppositeEdge = centerX + sign * (width / 2); const bottomEdge = centerY + height / 2; const topEdge = centerY - height / 2; const pointerLength = sign * (x - pointerEdge) > 0 ? 0 : props.pointerLength; const direction = orientation === "left" ? "0 0 0" : "0 0 1"; const arc = `${cornerRadius} ${cornerRadius} ${direction}`; return `M ${pointerEdge}, ${centerY - pointerWidth / 2} L ${pointerLength ? x : pointerEdge}, ${ pointerLength ? y : centerY + pointerWidth / 2 } L ${pointerEdge}, ${centerY + pointerWidth / 2} L ${pointerEdge}, ${bottomEdge - cornerRadius} A ${arc} ${pointerEdge + sign * cornerRadius}, ${bottomEdge} L ${oppositeEdge - sign * cornerRadius}, ${bottomEdge} A ${arc} ${oppositeEdge}, ${bottomEdge - cornerRadius} L ${oppositeEdge}, ${topEdge + cornerRadius} A ${arc} ${oppositeEdge - sign * cornerRadius}, ${topEdge} L ${pointerEdge + sign * cornerRadius}, ${topEdge} A ${arc} ${pointerEdge}, ${topEdge + cornerRadius} z`; }; const getFlyoutPath = (props: FlyoutPathProps) => { const orientation = props.orientation || "top"; return orientation === "left" || orientation === "right" ? getHorizontalPath(props) : getVerticalPath(props); }; const evaluateProps = (props: FlyoutProps) => { /** * Potential evaluated props are: * `id` * `style` */ const id = Helpers.evaluateProp(props.id, props); const style = Helpers.evaluateStyle(props.style, props); return { ...props, id, style }; }; const defaultProps = { pathComponent: , role: "presentation", shapeRendering: "auto", }; export const Flyout: React.FC = (initialProps) => { const props = evaluateProps(defaults({}, initialProps, defaultProps)); const userProps = UserProps.getSafeUserProps(props); // check for required props for this subcomponent // they should be passed in from the wrapper UserProps.assert(props.height, "Flyout props[height] is undefined"); UserProps.assert(props.width, "Flyout props[width] is undefined"); UserProps.assert(props.x, "Flyout props[x] is undefined"); UserProps.assert(props.y, "Flyout props[y] is undefined"); const flyoutPathProps: FlyoutPathProps = { center: props.center || { x: 0, y: 0 }, cornerRadius: props.cornerRadius || 0, dx: props.dx, dy: props.dy, height: props.height, orientation: props.orientation || "top", pointerLength: props.pointerLength || 0, pointerWidth: props.pointerWidth || 0, width: props.width, x: props.x, y: props.y, }; return React.cloneElement(props.pathComponent!, { ...props.events, ...userProps, style: props.style, d: getFlyoutPath(flyoutPathProps), className: props.className, shapeRendering: props.shapeRendering, role: props.role, transform: props.transform, clipPath: props.clipPath, }); };