Skip to content

Commit da5c99c

Browse files
mbostockFil
andauthored
Add d3.pathRound. (#12)
* pathFixed * expose Path; template literal * minimize diff * minimize diff * Path(digits); pathFixed(digits = 3) * Update README * simpler iteration * pathRound, not pathFixed * shorter Co-authored-by: Philippe Rivière <fil@rezo.net>
1 parent 6230b3c commit da5c99c

4 files changed

Lines changed: 166 additions & 53 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ Now code you write once can be used with both Canvas (for performance) and SVG (
2121

2222
## Installing
2323

24-
If you use npm, `npm install d3-path`. You can also download the [latest release on GitHub](https://github.com/d3/d3-path/releases/latest). In modern browsers, you can import d3-path from Skypack:
24+
If you use npm, `npm install d3-path`. You can also download the [latest release on GitHub](https://github.com/d3/d3-path/releases/latest). In modern browsers, you can import d3-path from jsDelivr:
2525

2626
```html
2727
<script type="module">
2828
29-
import {path} from "https://cdn.skypack.dev/d3-path@3";
29+
import {path} from "https://cdn.jsdelivr.net/npm/d3-path@3/+esm";
3030
3131
const p = path();
3232
p.moveTo(1, 2);
@@ -88,3 +88,7 @@ Creates a new subpath containing just the four points ⟨*x*, *y*⟩, ⟨*x* + *
8888
<a name="path_toString" href="#path_toString">#</a> <i>path</i>.<b>toString</b>()
8989

9090
Returns the string representation of this *path* according to SVG’s [path data specification](http://www.w3.org/TR/SVG/paths.html#PathData).
91+
92+
<a name="pathRound" href="#pathRound">#</a> d3.<b>pathRound</b>(*digits* = 3) · [Source](https://github.com/d3/d3-path/blob/master/src/path.js), [Examples](https://observablehq.com/@d3/d3-path)
93+
94+
Like [d3.path](#path), except limits the digits after the decimal to the specified number of *digits*.

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export {default as path} from "./path.js";
1+
export {Path, path, pathRound} from "./path.js";

src/path.js

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,68 @@ const pi = Math.PI,
33
epsilon = 1e-6,
44
tauEpsilon = tau - epsilon;
55

6-
function Path() {
7-
this._x0 = this._y0 = // start of current subpath
8-
this._x1 = this._y1 = null; // end of current subpath
9-
this._ = "";
6+
function append(strings) {
7+
this._ += strings[0];
8+
for (let i = 1, n = strings.length; i < n; ++i) {
9+
this._ += arguments[i] + strings[i];
10+
}
1011
}
1112

12-
function path() {
13-
return new Path;
13+
function appendRound(digits) {
14+
let d = Math.floor(digits);
15+
if (!(d >= 0)) throw new Error(`invalid digits: ${digits}`);
16+
if (d > 15) return append;
17+
const k = 10 ** d;
18+
return function(strings) {
19+
this._ += strings[0];
20+
for (let i = 1, n = strings.length; i < n; ++i) {
21+
this._ += Math.round(arguments[i] * k) / k + strings[i];
22+
}
23+
};
1424
}
1525

16-
Path.prototype = path.prototype = {
17-
constructor: Path,
18-
moveTo: function(x, y) {
19-
this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
20-
},
21-
closePath: function() {
26+
export class Path {
27+
constructor(digits) {
28+
this._x0 = this._y0 = // start of current subpath
29+
this._x1 = this._y1 = null; // end of current subpath
30+
this._ = "";
31+
this._append = digits == null ? append : appendRound(digits);
32+
}
33+
moveTo(x, y) {
34+
this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`;
35+
}
36+
closePath() {
2237
if (this._x1 !== null) {
2338
this._x1 = this._x0, this._y1 = this._y0;
24-
this._ += "Z";
39+
this._append`Z`;
2540
}
26-
},
27-
lineTo: function(x, y) {
28-
this._ += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
29-
},
30-
quadraticCurveTo: function(x1, y1, x, y) {
31-
this._ += "Q" + (+x1) + "," + (+y1) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
32-
},
33-
bezierCurveTo: function(x1, y1, x2, y2, x, y) {
34-
this._ += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
35-
},
36-
arcTo: function(x1, y1, x2, y2, r) {
41+
}
42+
lineTo(x, y) {
43+
this._append`L${this._x1 = +x},${this._y1 = +y}`;
44+
}
45+
quadraticCurveTo(x1, y1, x, y) {
46+
this._append`Q${+x1},${+y1},${this._x1 = +x},${this._y1 = +y}`;
47+
}
48+
bezierCurveTo(x1, y1, x2, y2, x, y) {
49+
this._append`C${+x1},${+y1},${+x2},${+y2},${this._x1 = +x},${this._y1 = +y}`;
50+
}
51+
arcTo(x1, y1, x2, y2, r) {
3752
x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;
38-
var x0 = this._x1,
53+
54+
// Is the radius negative? Error.
55+
if (r < 0) throw new Error(`negative radius: ${r}`);
56+
57+
let x0 = this._x1,
3958
y0 = this._y1,
4059
x21 = x2 - x1,
4160
y21 = y2 - y1,
4261
x01 = x0 - x1,
4362
y01 = y0 - y1,
4463
l01_2 = x01 * x01 + y01 * y01;
4564

46-
// Is the radius negative? Error.
47-
if (r < 0) throw new Error("negative radius: " + r);
48-
4965
// Is this path empty? Move to (x1,y1).
5066
if (this._x1 === null) {
51-
this._ += "M" + (this._x1 = x1) + "," + (this._y1 = y1);
67+
this._append`M${this._x1 = x1},${this._y1 = y1}`;
5268
}
5369

5470
// Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
@@ -58,12 +74,12 @@ Path.prototype = path.prototype = {
5874
// Equivalently, is (x1,y1) coincident with (x2,y2)?
5975
// Or, is the radius zero? Line to (x1,y1).
6076
else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon) || !r) {
61-
this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1);
77+
this._append`L${this._x1 = x1},${this._y1 = y1}`;
6278
}
6379

6480
// Otherwise, draw an arc!
6581
else {
66-
var x20 = x2 - x0,
82+
let x20 = x2 - x0,
6783
y20 = y2 - y0,
6884
l21_2 = x21 * x21 + y21 * y21,
6985
l20_2 = x20 * x20 + y20 * y20,
@@ -75,32 +91,33 @@ Path.prototype = path.prototype = {
7591

7692
// If the start tangent is not coincident with (x0,y0), line to.
7793
if (Math.abs(t01 - 1) > epsilon) {
78-
this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01);
94+
this._append`L${x1 + t01 * x01},${y1 + t01 * y01}`;
7995
}
8096

81-
this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21);
97+
this._append`A${r},${r},0,0,${+(y01 * x20 > x01 * y20)},${this._x1 = x1 + t21 * x21},${this._y1 = y1 + t21 * y21}`;
8298
}
83-
},
84-
arc: function(x, y, r, a0, a1, ccw) {
99+
}
100+
arc(x, y, r, a0, a1, ccw) {
85101
x = +x, y = +y, r = +r, ccw = !!ccw;
86-
var dx = r * Math.cos(a0),
102+
103+
// Is the radius negative? Error.
104+
if (r < 0) throw new Error(`negative radius: ${r}`);
105+
106+
let dx = r * Math.cos(a0),
87107
dy = r * Math.sin(a0),
88108
x0 = x + dx,
89109
y0 = y + dy,
90110
cw = 1 ^ ccw,
91111
da = ccw ? a0 - a1 : a1 - a0;
92112

93-
// Is the radius negative? Error.
94-
if (r < 0) throw new Error("negative radius: " + r);
95-
96113
// Is this path empty? Move to (x0,y0).
97114
if (this._x1 === null) {
98-
this._ += "M" + x0 + "," + y0;
115+
this._append`M${x0},${y0}`;
99116
}
100117

101118
// Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0).
102119
else if (Math.abs(this._x1 - x0) > epsilon || Math.abs(this._y1 - y0) > epsilon) {
103-
this._ += "L" + x0 + "," + y0;
120+
this._append`L${x0},${y0}`;
104121
}
105122

106123
// Is this arc empty? We’re done.
@@ -111,20 +128,29 @@ Path.prototype = path.prototype = {
111128

112129
// Is this a complete circle? Draw two arcs to complete the circle.
113130
if (da > tauEpsilon) {
114-
this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0);
131+
this._append`A${r},${r},0,1,${cw},${x - dx},${y - dy}A${r},${r},0,1,${cw},${this._x1 = x0},${this._y1 = y0}`;
115132
}
116133

117134
// Is this arc non-empty? Draw an arc!
118135
else if (da > epsilon) {
119-
this._ += "A" + r + "," + r + ",0," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1));
136+
this._append`A${r},${r},0,${+(da >= pi)},${cw},${this._x1 = x + r * Math.cos(a1)},${this._y1 = y + r * Math.sin(a1)}`;
120137
}
121-
},
122-
rect: function(x, y, w, h) {
123-
this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + (+w) + "v" + (+h) + "h" + (-w) + "Z";
124-
},
125-
toString: function() {
138+
}
139+
rect(x, y, w, h) {
140+
this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}h${w = +w}v${+h}h${-w}Z`;
141+
}
142+
toString() {
126143
return this._;
127144
}
128-
};
145+
}
129146

130-
export default path;
147+
export function path() {
148+
return new Path;
149+
}
150+
151+
// Allow instanceof d3.path
152+
path.prototype = Path.prototype;
153+
154+
export function pathRound(digits = 3) {
155+
return new Path(+digits);
156+
}

test/pathRound-test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import assert from "assert";
2+
import {path, pathRound} from "../src/index.js";
3+
4+
it("pathRound() defaults to three digits of precision", () => {
5+
const p = pathRound();
6+
p.moveTo(Math.PI, Math.E);
7+
assert.strictEqual(p + "", "M3.142,2.718");
8+
});
9+
10+
it("pathRound(null) is equivalent to pathRound(0)", () => {
11+
const p = pathRound(null);
12+
p.moveTo(Math.PI, Math.E);
13+
assert.strictEqual(p + "", "M3,3");
14+
});
15+
16+
it("pathRound(digits) validates the specified digits", () => {
17+
assert.throws(() => pathRound(NaN), /invalid digits/);
18+
assert.throws(() => pathRound(-1), /invalid digits/);
19+
});
20+
21+
it("pathRound(digits) ignores digits if greater than 15", () => {
22+
const p = pathRound(40);
23+
p.moveTo(Math.PI, Math.E);
24+
assert.strictEqual(p + "", "M3.141592653589793,2.718281828459045");
25+
});
26+
27+
it("pathRound.moveTo(x, y) limits the precision", () => {
28+
const p = pathRound(1);
29+
p.moveTo(123.456, 789.012);
30+
assert.strictEqual(p + "", "M123.5,789");
31+
});
32+
33+
it("pathRound.lineTo(x, y) limits the precision", () => {
34+
const p = pathRound(1);
35+
p.moveTo(0, 0);
36+
p.lineTo(123.456, 789.012);
37+
assert.strictEqual(p + "", "M0,0L123.5,789");
38+
});
39+
40+
it("pathRound.arc(x, y, r, a0, a1, ccw) limits the precision", () => {
41+
const p0 = path(), p = pathRound(1);
42+
p0.arc(10.0001, 10.0001, 123.456, 0, Math.PI+0.0001);
43+
p.arc(10.0001, 10.0001, 123.456, 0, Math.PI+0.0001);
44+
assert.strictEqual(p + "", precision(p0 + "", 1));
45+
p0.arc(10.0001, 10.0001, 123.456, 0, Math.PI-0.0001);
46+
p.arc(10.0001, 10.0001, 123.456, 0, Math.PI-0.0001);
47+
assert.strictEqual(p + "", precision(p0 + "", 1));
48+
p0.arc(10.0001, 10.0001, 123.456, 0, Math.PI / 2, true);
49+
p.arc(10.0001, 10.0001, 123.456, 0, Math.PI / 2, true);
50+
assert.strictEqual(p + "", precision(p0 + "", 1));
51+
});
52+
53+
it("pathRound.arcTo(x1, y1, x2, y2, r) limits the precision", () => {
54+
const p0 = path(), p = pathRound(1);
55+
p0.arcTo(10.0001, 10.0001, 123.456, 456.789, 12345.6789);
56+
p.arcTo(10.0001, 10.0001, 123.456, 456.789, 12345.6789);
57+
assert.strictEqual(p + "", precision(p0 + "", 1));
58+
});
59+
60+
it("pathRound.quadraticCurveTo(x1, y1, x, y) limits the precision", () => {
61+
const p0 = path(), p = pathRound(1);
62+
p0.quadraticCurveTo(10.0001, 10.0001, 123.456, 456.789);
63+
p.quadraticCurveTo(10.0001, 10.0001, 123.456, 456.789);
64+
assert.strictEqual(p + "", precision(p0 + "", 1));
65+
});
66+
67+
it("pathRound.bezierCurveTo(x1, y1, x2, y2, x, y) limits the precision", () => {
68+
const p0 = path(), p = pathRound(1);
69+
p0.bezierCurveTo(10.0001, 10.0001, 123.456, 456.789, 0.007, 0.006);
70+
p.bezierCurveTo(10.0001, 10.0001, 123.456, 456.789, 0.007, 0.006);
71+
assert.strictEqual(p + "", precision(p0 + "", 1));
72+
});
73+
74+
it("pathRound.rect(x, y, w, h) limits the precision", () => {
75+
const p0 = path(), p = pathRound(1);
76+
p0.rect(10.0001, 10.0001, 123.456, 456.789);
77+
p.rect(10.0001, 10.0001, 123.456, 456.789);
78+
assert.strictEqual(p + "", precision(p0 + "", 1));
79+
});
80+
81+
function precision(str, precision) {
82+
return str.replace(/\d+\.\d+/g, s => +parseFloat(s).toFixed(precision));
83+
}

0 commit comments

Comments
 (0)