Skip to content

Commit e2ed033

Browse files
Filmbostock
andcommitted
arrow sweep (#1740)
* arrow sweep option; note that I also removed the arrow head if headLength is zero. * miserables.json * miserables arc diagram * the arrow head and insets computations depend on the flipped bend angle * darker initializer * functional sweep (#1741) * ±[xy] --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent 8da176f commit e2ed033

File tree

10 files changed

+1061
-12
lines changed

10 files changed

+1061
-12
lines changed

docs/marks/arrow.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,12 @@ The arrow mark supports the [standard mark options](../features/marks.md#mark-op
111111
* **insetEnd** - inset at the end of the arrow (useful if the arrow points to a dot)
112112
* **insetStart** - inset at the start of the arrow
113113
* **inset** - shorthand for the two insets
114+
* **sweep** - the sweep order
114115

115116
The **bend** option sets the angle between the straight line connecting the two points and the outgoing direction of the arrow from the start point. It must be within ±90°. A positive angle will produce a clockwise curve; a negative angle will produce a counterclockwise curve; zero will produce a straight line. The **headAngle** determines how pointy the arrowhead is; it is typically between 0° and 180°. The **headLength** determines the scale of the arrowhead relative to the stroke width. Assuming the default of stroke width 1.5px, the **headLength** is the length of the arrowhead’s side in pixels.
116117

118+
The **sweep** option can be used to make arrows bend in the same direction, independently of the relative positions of the starting and ending points. It defaults to 1 indicating a positive (clockwise) bend angle; -1 indicates a negative (anticlockwise) bend angle. 0 effectively clears the bend angle. If set to *-x*, the bend angle is flipped when the ending point is to the left of the starting point — ensuring all arrows bulge up (down if bend is negative); if set to *-y*, the bend angle is flipped when the ending point is above the starting point — ensuring all arrows bulge right (left if bend is negative); the sign is negated for *+x* and *+y*.
119+
117120
## arrow(*data*, *options*)
118121

119122
```js

src/marks/arrow.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ export interface ArrowOptions extends MarkOptions {
8484
* points to a dot.
8585
*/
8686
insetEnd?: number;
87+
88+
/**
89+
* The sweep order; defaults to 1 indicating a positive (clockwise) bend
90+
* angle; -1 indicates a negative (anticlockwise) bend angle; 0 effectively
91+
* clears the bend angle. If set to *-x*, the bend angle is flipped when the
92+
* ending point is to the left of the starting point — ensuring all arrows
93+
* bulge up (down if bend is negative); if set to *-y*, the bend angle is
94+
* flipped when the ending point is above the starting point — ensuring all
95+
* arrows bulge right (left if bend is negative); the sign is negated for *+x*
96+
* and *+y*.
97+
*/
98+
sweep?: number | "+x" | "-x" | "+y" | "-y" | ((x1: number, y1: number, x2: number, y2: number) => number);
8799
}
88100

89101
/**

src/marks/arrow.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import {ascending, descending} from "d3";
12
import {create} from "../context.js";
23
import {Mark} from "../mark.js";
34
import {radians} from "../math.js";
4-
import {constant} from "../options.js";
5+
import {constant, keyword} from "../options.js";
56
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
67
import {maybeSameValue} from "./link.js";
78

@@ -26,7 +27,8 @@ export class Arrow extends Mark {
2627
headLength = 8, // Disable the arrow with headLength = 0; or, use Plot.link.
2728
inset = 0,
2829
insetStart = inset,
29-
insetEnd = inset
30+
insetEnd = inset,
31+
sweep
3032
} = options;
3133
super(
3234
data,
@@ -44,19 +46,13 @@ export class Arrow extends Mark {
4446
this.headLength = +headLength;
4547
this.insetStart = +insetStart;
4648
this.insetEnd = +insetEnd;
49+
this.sweep = maybeSweep(sweep);
4750
}
4851
render(index, scales, channels, dimensions, context) {
4952
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
5053
const {strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
5154
const sw = SW ? (i) => SW[i] : constant(strokeWidth === undefined ? 1 : strokeWidth);
5255

53-
// When bending, the offset between the straight line between the two points
54-
// and the outgoing tangent from the start point. (Also the negative
55-
// incoming tangent to the end point.) This must be within ±π/2. A positive
56-
// angle will produce a clockwise curve; a negative angle will produce a
57-
// counterclockwise curve; zero will produce a straight line.
58-
const bendAngle = bend * radians;
59-
6056
// The angle between the arrow’s shaft and one of the wings; the “head”
6157
// angle between the wings is twice this value.
6258
const wingAngle = (headAngle * radians) / 2;
@@ -91,6 +87,13 @@ export class Arrow extends Mark {
9187
// wings, but that’s okay since vectors are usually small.)
9288
const headLength = Math.min(wingScale * sw(i), lineLength / 3);
9389

90+
// When bending, the offset between the straight line between the two points
91+
// and the outgoing tangent from the start point. (Also the negative
92+
// incoming tangent to the end point.) This must be within ±π/2. A positive
93+
// angle will produce a clockwise curve; a negative angle will produce a
94+
// counterclockwise curve; zero will produce a straight line.
95+
const bendAngle = this.sweep(x1, y1, x2, y2) * bend * radians;
96+
9497
// The radius of the circle that intersects with the two endpoints
9598
// and has the specified bend angle.
9699
const r = Math.hypot(lineLength / Math.tan(bendAngle), lineLength) / 2;
@@ -141,16 +144,32 @@ export class Arrow extends Mark {
141144

142145
// If the radius is very large (or even infinite, as when the bend
143146
// angle is zero), then render a straight line.
144-
return `M${x1},${y1}${
145-
r < 1e5 ? `A${r},${r} 0,0,${bendAngle > 0 ? 1 : 0} ` : `L`
146-
}${x2},${y2}M${x3},${y3}L${x2},${y2}L${x4},${y4}`;
147+
const a = r < 1e5 ? `A${r},${r} 0,0,${bendAngle > 0 ? 1 : 0} ` : `L`;
148+
const h = headLength ? `M${x3},${y3}L${x2},${y2}L${x4},${y4}` : "";
149+
return `M${x1},${y1}${a}${x2},${y2}${h}`;
147150
})
148151
.call(applyChannelStyles, this, channels)
149152
)
150153
.node();
151154
}
152155
}
153156

157+
// Maybe flip the bend angle, depending on the arrow orientation.
158+
function maybeSweep(sweep = 1) {
159+
if (typeof sweep === "number") return constant(Math.sign(sweep));
160+
if (typeof sweep === "function") return (x1, y1, x2, y2) => Math.sign(sweep(x1, y1, x2, y2));
161+
switch (keyword(sweep, "sweep", ["+x", "-x", "+y", "-y"])) {
162+
case "+x":
163+
return (x1, y1, x2) => ascending(x1, x2);
164+
case "-x":
165+
return (x1, y1, x2) => descending(x1, x2);
166+
case "+y":
167+
return (x1, y1, x2, y2) => ascending(y1, y2);
168+
case "-y":
169+
return (x1, y1, x2, y2) => descending(y1, y2);
170+
}
171+
}
172+
154173
// Returns the center of a circle that goes through the two given points ⟨ax,ay⟩
155174
// and ⟨bx,by⟩ and has radius r. There are two such points; use the sign +1 or
156175
// -1 to choose between them. Returns [NaN, NaN] if r is too small.

test/data/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ https://observablehq.com/@tophtucker/examples-of-bitemporal-charts
115115
The New York Times
116116
https://www.nytimes.com/2019/12/02/upshot/wealth-poverty-divide-american-cities.html
117117

118+
## miserables.json
119+
Character interactions in the chapters of “Les Miserables”, Donald Knuth, Stanford Graph Base
120+
https://www-cs-faculty.stanford.edu/~knuth/sgb.html
121+
118122
## mtcars.csv
119123
1974 *Motor Trend* US magazine
120124
https://www.rdocumentation.org/packages/datasets/versions/3.6.2/topics/mtcars

0 commit comments

Comments
 (0)