Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/victory-area/src/area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
VictoryCommonPrimitiveProps,
LineHelpers,
VictoryCommonThemeProps,
roundPathString,
} from "victory-core";

const defined = (d) => {
Expand Down Expand Up @@ -147,7 +148,7 @@ export const Area: React.FC<AreaProps> = (initialProps) => {
{
key: `${id}-area`,
style: Object.assign({}, style, { stroke: areaStroke }),
d: areaFunction(data),
d: roundPathString(areaFunction(data)),
desc,
tabIndex,
},
Expand All @@ -163,7 +164,7 @@ export const Area: React.FC<AreaProps> = (initialProps) => {
{
key: `${id}-area-stroke`,
style: Object.assign({}, style, { fill: "none" }),
d: lineFunction(data),
d: roundPathString(lineFunction(data)),
},
sharedProps,
),
Expand Down
1 change: 1 addition & 0 deletions packages/victory-core/src/victory-util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * as Hooks from "./hooks";
export * as Immutable from "./immutable";
export * as LabelHelpers from "./label-helpers";
export * as LineHelpers from "./line-helpers";
export * from "./round-path-string";
export * as Log from "./log";
export * as PointPathHelpers from "./point-path-helpers";
export * as Scale from "./scale";
Expand Down
28 changes: 28 additions & 0 deletions packages/victory-core/src/victory-util/round-path-string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from "vitest";

import { roundPathString } from "./round-path-string";

describe("roundPathString", () => {
it("normalizes floating point precision differences", () => {
expect(roundPathString("M60,240L252.50000000000003,67.2")).toBe(
"M60,240L252.5,67.2",
);
});

it("leaves integer coordinates untouched", () => {
expect(roundPathString("M0,0L100,200Z")).toBe("M0,0L100,200Z");
});

it("rounds to 3 decimals by default", () => {
expect(roundPathString("M1.123456,2.987654")).toBe("M1.123,2.988");
});

it("honors a custom precision", () => {
expect(roundPathString("M1.123456,2.987654", 1)).toBe("M1.1,3");
});

it("handles negative numbers and scientific notation", () => {
// -1.5e2 = -150 -> rounds to -150 (integer, untouched); 3.14159265 -> 3.142
expect(roundPathString("M-1.5e2,0L3.14159265,0")).toBe("M-150,0L3.142,0");
});
});
29 changes: 29 additions & 0 deletions packages/victory-core/src/victory-util/round-path-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Rounds the numeric coordinates in an SVG path `d` string to a fixed number
* of decimal places.
*
* d3-shape can emit values like `252.50000000000003` on one platform/build and
* `252.5` on another (e.g. server vs. client in a Next.js app), which causes
* React hydration mismatches because the serialized `d` attribute differs.
* Rounding every coordinate to `precision` decimals makes the output
* deterministic across environments while being visually indistinguishable at
* rendered scale.
*
* @example
* roundPathString("M60,240L252.50000000000003,67.2")
* // => "M60,240L252.5,67.2"
*/
export function roundPathString(d: string, precision = 3): string {
return d.replace(
/(-?\d*\.?\d+(?:[eE][-+]?\d+)?)/g,
(num) => {
const parsed = Number(num);
// Leave integers untouched to keep the string minimal.
if (Number.isInteger(parsed)) {
return String(parsed);
}
const rounded = parseFloat(parsed.toFixed(precision));
return String(rounded);
},
);
}
3 changes: 2 additions & 1 deletion packages/victory-line/src/curve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
StringOrCallback,
NumberOrCallback,
VictoryCommonPrimitiveProps,
roundPathString,
} from "victory-core";

const evaluateProps = (props) => {
Expand Down Expand Up @@ -47,7 +48,7 @@ export const Curve: React.FC<CurveProps> = (initialProps) => {
const lineFunction = LineHelpers.getLineFunction(props);
const defaultTransform =
polar && origin ? `translate(${origin.x}, ${origin.y})` : undefined;
const d = lineFunction(props.data);
const d = roundPathString(lineFunction(props.data));

return React.cloneElement(props.pathComponent!, {
...props.events,
Expand Down