diff --git a/README.md b/README.md
index a571838..57af7c2 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,10 @@ Returns the projected planar bounding box (typically in pixels) for the specifie
Returns the projected planar centroid (typically in pixels) for the specified GeoJSON *object*. This is handy for, say, labeling state or county boundaries, or displaying a symbol map. For example, a [noncontiguous cartogram](https://bl.ocks.org/mbostock/4055908) might scale each state around its centroid. This method observes any clipping performed by the [projection](#path_projection); see [*projection*.clipAngle](#projection_clipAngle) and [*projection*.clipExtent](#projection_clipExtent). This is the planar equivalent of [d3.geoCentroid](#geoCentroid).
+# path.digits([digits]) [<>](https://github.com/d3/d3-geo/blob/main/src/path/index.js "Source")
+
+If *digits* is specified (as a non-negative number), sets the number of fractional digits for coordinates generated in SVG path strings. If *projection* is not specified, returns the current number of digits, which defaults to 3.
+
# path.measure(object) [<>](https://github.com/d3/d3-geo/blob/main/src/path/measure.js "Source")
Returns the projected planar length (typically in pixels) for the specified GeoJSON *object*. Point and MultiPoint geometries have zero length. For Polygon and MultiPolygon geometries, this method computes the summed length of all rings. This method observes any clipping performed by the [projection](#path_projection); see [*projection*.clipAngle](#projection_clipAngle) and [*projection*.clipExtent](#projection_clipExtent). This is the planar equivalent of [d3.geoLength](#geoLength).
diff --git a/src/path/index.js b/src/path/index.js
index 45da305..e8292bc 100644
--- a/src/path/index.js
+++ b/src/path/index.js
@@ -8,7 +8,8 @@ import pathMeasure from "./measure.js";
import PathString from "./string.js";
export default function(projection, context) {
- var pointRadius = 4.5,
+ let digits = 3,
+ pointRadius = 4.5,
projectionStream,
contextStream;
@@ -41,12 +42,14 @@ export default function(projection, context) {
};
path.projection = function(_) {
- return arguments.length ? (projectionStream = _ == null ? (projection = null, identity) : (projection = _).stream, path) : projection;
+ if (!arguments.length) return projection;
+ projectionStream = _ == null ? (projection = null, identity) : (projection = _).stream;
+ return path;
};
path.context = function(_) {
if (!arguments.length) return context;
- contextStream = _ == null ? (context = null, new PathString) : new PathContext(context = _);
+ contextStream = _ == null ? (context = null, new PathString(digits)) : new PathContext(context = _);
if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
return path;
};
@@ -57,5 +60,17 @@ export default function(projection, context) {
return path;
};
- return path.projection(projection).context(context);
+ path.digits = function(_) {
+ if (!arguments.length) return digits;
+ if (_ == null) digits = null;
+ else {
+ const d = Math.floor(_);
+ if (!(d >= 0)) throw new RangeError(`invalid digits: ${_}`);
+ digits = d;
+ }
+ if (context === null) contextStream = new PathString(digits);
+ return path;
+ };
+
+ return path.projection(projection).digits(digits).context(context);
}
diff --git a/src/path/string.js b/src/path/string.js
index 02e57b0..d9efbfc 100644
--- a/src/path/string.js
+++ b/src/path/string.js
@@ -1,59 +1,86 @@
-export default function PathString() {
- this._string = [];
-}
+// Simple caching for constant-radius points.
+let cacheDigits, cacheAppend, cacheRadius, cacheCircle;
-PathString.prototype = {
- _radius: 4.5,
- _circle: circle(4.5),
- pointRadius: function(_) {
- if ((_ = +_) !== this._radius) this._radius = _, this._circle = null;
+export default class PathString {
+ constructor(digits) {
+ this._append = digits == null ? append : appendRound(digits);
+ this._radius = 4.5;
+ this._ = "";
+ }
+ pointRadius(_) {
+ this._radius = +_;
return this;
- },
- polygonStart: function() {
+ }
+ polygonStart() {
this._line = 0;
- },
- polygonEnd: function() {
+ }
+ polygonEnd() {
this._line = NaN;
- },
- lineStart: function() {
+ }
+ lineStart() {
this._point = 0;
- },
- lineEnd: function() {
- if (this._line === 0) this._string.push("Z");
+ }
+ lineEnd() {
+ if (this._line === 0) this._ += "Z";
this._point = NaN;
- },
- point: function(x, y) {
+ }
+ point(x, y) {
switch (this._point) {
case 0: {
- this._string.push("M", x, ",", y);
+ this._append`M${x},${y}`;
this._point = 1;
break;
}
case 1: {
- this._string.push("L", x, ",", y);
+ this._append`L${x},${y}`;
break;
}
default: {
- if (this._circle == null) this._circle = circle(this._radius);
- this._string.push("M", x, ",", y, this._circle);
+ this._append`M${x},${y}`;
+ if (this._radius !== cacheRadius || this._append !== cacheAppend) {
+ const r = this._radius;
+ const s = this._;
+ this._ = ""; // stash the old string so we can cache the circle path fragment
+ this._append`m0,${r}a${r},${r} 0 1,1 0,${-2 * r}a${r},${r} 0 1,1 0,${2 * r}z`;
+ cacheRadius = r;
+ cacheAppend = this._append;
+ cacheCircle = this._;
+ this._ = s;
+ }
+ this._ += cacheCircle;
break;
}
}
- },
- result: function() {
- if (this._string.length) {
- var result = this._string.join("");
- this._string = [];
- return result;
- } else {
- return null;
- }
}
-};
+ result() {
+ const result = this._;
+ this._ = "";
+ return result.length ? result : null;
+ }
+}
-function circle(radius) {
- return "m0," + radius
- + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius
- + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius
- + "z";
+function append(strings) {
+ let i = 1;
+ this._ += strings[0];
+ for (const j = strings.length; i < j; ++i) {
+ this._ += arguments[i] + strings[i];
+ }
+}
+
+function appendRound(digits) {
+ const d = Math.floor(digits);
+ if (!(d >= 0)) throw new RangeError(`invalid digits: ${digits}`);
+ if (d > 15) return append;
+ if (d !== cacheDigits) {
+ const k = 10 ** d;
+ cacheDigits = d;
+ cacheAppend = function append(strings) {
+ let i = 1;
+ this._ += strings[0];
+ for (const j = strings.length; i < j; ++i) {
+ this._ += Math.round(arguments[i] * k) / k + strings[i];
+ }
+ };
+ }
+ return cacheAppend;
}
diff --git a/test/asserts.js b/test/asserts.js
index 1720a72..b24d767 100644
--- a/test/asserts.js
+++ b/test/asserts.js
@@ -11,7 +11,7 @@ function normalizePath(path) {
}
function formatNumber(s) {
- return Math.abs((s = +s) - Math.round(s)) < 1e-6 ? Math.round(s) : s.toFixed(6);
+ return Math.abs((s = +s) - Math.round(s)) < 1e-6 ? Math.round(s) : s.toFixed(3);
}
export function assertInDelta(actual, expected, delta = 1e-6) {
diff --git a/test/path/index-test.js b/test/path/index-test.js
index ab0e263..5bba4ac 100644
--- a/test/path/index-test.js
+++ b/test/path/index-test.js
@@ -203,3 +203,67 @@ it("geoPath(LineString) then geoPath(Point) does not treat the point as part of
{type: "arc", x: 165, y: 160, r: 4.5}
]);
});
+
+it("geoPath.digits() defaults to three", () => {
+ const path = geoPath();
+ assert.strictEqual(path.digits(), 3);
+});
+
+it("geoPath.digits(digits) returns the current path after setting the digits option", () => {
+ const path = geoPath();
+ assert.strictEqual(path.digits(4), path);
+ assert.strictEqual(path.digits(), 4);
+ assert.strictEqual(path.digits(0).digits(), 0);
+ assert.strictEqual(geoPath().digits(), 3); // doesn’t affect default
+});
+
+it("geoPath.digits(nullish) sets digits to null", () => {
+ const path = geoPath();
+ assert.strictEqual(path.digits(null).digits(), null);
+ assert.strictEqual(path.digits(undefined).digits(), null);
+});
+
+it("geoPath.digits(digits) floors and coerces digits if not nullish", () => {
+ const path = geoPath();
+ assert.strictEqual(path.digits(3.5).digits(), 3);
+ assert.strictEqual(path.digits(3.9).digits(), 3);
+ assert.strictEqual(path.digits("3").digits(), 3);
+ assert.strictEqual(path.digits(" 3").digits(), 3);
+ assert.strictEqual(path.digits("").digits(), 0);
+});
+
+it("geoPath.digits(digits) throws if digits is not valid", () => {
+ const path = geoPath();
+ assert.throws(() => path.digits(NaN).digits(), RangeError);
+ assert.throws(() => path.digits(-1).digits(), RangeError);
+ assert.throws(() => path.digits(-0.1).digits(), RangeError);
+});
+
+it("path(object) respects the specified digits", () => {
+ const line = {type: "LineString", coordinates: [[Math.PI, Math.E], [Math.E, Math.PI]]};
+ assert.strictEqual(geoPath().digits(0)(line), "M3,3L3,3");
+ assert.strictEqual(geoPath().digits(1)(line), "M3.1,2.7L2.7,3.1");
+ assert.strictEqual(geoPath().digits(2)(line), "M3.14,2.72L2.72,3.14");
+ assert.strictEqual(geoPath().digits(3)(line), "M3.142,2.718L2.718,3.142");
+ assert.strictEqual(geoPath().digits(4)(line), "M3.1416,2.7183L2.7183,3.1416");
+ assert.strictEqual(geoPath().digits(5)(line), "M3.14159,2.71828L2.71828,3.14159");
+ assert.strictEqual(geoPath().digits(6)(line), "M3.141593,2.718282L2.718282,3.141593");
+ assert.strictEqual(geoPath().digits(40)(line), "M3.141592653589793,2.718281828459045L2.718281828459045,3.141592653589793");
+ assert.strictEqual(geoPath().digits(null)(line), "M3.141592653589793,2.718281828459045L2.718281828459045,3.141592653589793");
+});
+
+it("path(object) handles variable-radius points with different digits", () => {
+ const p1 = geoPath().digits(1);
+ const p2 = geoPath().digits(2);
+ const point = {type: "Point", coordinates: [Math.PI, Math.E]};
+ assert.strictEqual(p1.pointRadius(1)(point), "M3.1,2.7m0,1a1,1 0 1,1 0,-2a1,1 0 1,1 0,2z");
+ assert.strictEqual(p1(point), "M3.1,2.7m0,1a1,1 0 1,1 0,-2a1,1 0 1,1 0,2z");
+ assert.strictEqual(p1.pointRadius(2)(point), "M3.1,2.7m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+ assert.strictEqual(p1(point), "M3.1,2.7m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+ assert.strictEqual(p2.pointRadius(1)(point), "M3.14,2.72m0,1a1,1 0 1,1 0,-2a1,1 0 1,1 0,2z");
+ assert.strictEqual(p2(point), "M3.14,2.72m0,1a1,1 0 1,1 0,-2a1,1 0 1,1 0,2z");
+ assert.strictEqual(p1(point), "M3.1,2.7m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+ assert.strictEqual(p2.pointRadius(2)(point), "M3.14,2.72m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+ assert.strictEqual(p2(point), "M3.14,2.72m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+ assert.strictEqual(p1(point), "M3.1,2.7m0,2a2,2 0 1,1 0,-4a2,2 0 1,1 0,4z");
+});