Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

path.digits #272

Merged
merged 8 commits into from
Dec 19, 2022
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<a href="#path_digits" name="path_digits">#</a> <i>path</i>.<b>digits</b>([<i>digits</i>]) [<>](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.

<a href="#path_measure" name="path_measure">#</a> <i>path</i>.<b>measure</b>(<i>object</i>) [<>](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).
Expand Down
23 changes: 19 additions & 4 deletions src/path/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
};
Expand All @@ -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);
}
103 changes: 65 additions & 38 deletions src/path/string.js
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion test/asserts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
64 changes: 64 additions & 0 deletions test/path/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});