Skip to content

Instantly share code, notes, and snippets.

@valtism
Last active October 28, 2022 19:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save valtism/5a6265b503aa7a7ae2a765f0d11163cf to your computer and use it in GitHub Desktop.
Save valtism/5a6265b503aa7a7ae2a765f0d11163cf to your computer and use it in GitHub Desktop.
import type { LinePathConfig } from "@visx/shape";
import { line } from "@visx/shape";
import clsx from "clsx";
import { getClosest } from "migrated/shared/helpers/getClosest";
import * as React from "react";
type PrySplitLinePathRenderer = (renderProps: {
index: number;
path: string;
styles: Omit<React.SVGProps<SVGPathElement>, "x" | "y" | "children">;
}) => React.ReactNode;
type PrySplitLinePathProps<Datum> = {
/** Array of data segments, where each segment will be a separate path in the rendered line. */
segments: Datum[][];
/** Styles to apply to each segment. If fewer styles are specified than the number of segments, they will be re-used. */
styles: Omit<React.SVGProps<SVGPathElement>, "x" | "y" | "children">[];
/** Override render function which is passed the configured path generator as input. */
children?: PrySplitLinePathRenderer;
/** className applied to path element. */
className?: string;
} & LinePathConfig<Datum>;
// Matches digit with decimal, e.g. 12.9374462
const digitRegex = /(\d+.\d+)/g;
// A more performant version of Visx's <SplitLinePath> (500x faster)
// See: https://github.com/airbnb/visx/issues/1591
// Tradeoffs:
// Only splits via `x` segmentation. Can be extended to split via `y` if needed
// Does not support `length` segmentation
// Does not support closed curves, e.g. `curveBasisClosed`
export default function PrySplitLinePath<Datum>({
children,
className,
curve,
defined,
segments,
x,
y,
styles,
}: PrySplitLinePathProps<Datum>) {
// Convert data in all segments to points.
const startPointsInSegments = React.useMemo(() => {
const xFn = typeof x === "number" || typeof x === "undefined" ? () => x : x;
const yFn = typeof y === "number" || typeof y === "undefined" ? () => y : y;
return segments.map((s) => ({ x: xFn(s[0], 0, s), y: yFn(s[0], 0, s) }));
}, [x, y, segments]);
// Calculate path for entire line
const pathString = React.useMemo(() => {
const path = line<Datum>({ x, y, defined, curve });
return path(segments.flat()) || "";
}, [x, y, defined, curve, segments]);
// Split path into component points
const pathParts = React.useMemo(
() => pathString.split(digitRegex),
[pathString],
);
const xParts = React.useMemo(
() => pathParts.filter((_, i) => (i + 3) % 4 === 0).map(Number),
[pathParts],
);
const paths = React.useMemo(
() =>
startPointsInSegments.map((startPoint, index, startPoints) => {
const isLastLoop = index === startPoints.length - 1;
// Not all curves produce paths that touch datum points. Match with closest.
const closestStartPoint = getClosest(xParts, startPoint.x!);
const startIndex = pathParts.indexOf(String(closestStartPoint));
const closestEndPoint = isLastLoop
? pathParts[pathParts.length - 4]
: getClosest(xParts, startPoints[index + 1].x!);
const endIndex = pathParts.indexOf(String(closestEndPoint)) + 3;
return "M" + pathParts.slice(startIndex, endIndex).join("");
}),
[pathParts, startPointsInSegments, xParts],
);
return (
<g>
{paths.map((path, index) =>
children ? (
<React.Fragment key={index}>
{children({
index,
path,
styles: styles[index] || styles[index % styles.length],
})}
</React.Fragment>
) : (
<path
key={index}
className={clsx("visx-linepath", className)}
d={path}
fill="transparent"
strokeWidth={2}
// without this a datum surrounded by nulls will not be visible
// https://github.com/d3/d3-shape#line_defined
strokeLinecap="round"
{...(styles[index] || styles[index % styles.length])}
/>
),
)}
</g>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment