Skip to content

functional sweep #1741

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

Merged
merged 2 commits into from
Jul 9, 2023
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
15 changes: 8 additions & 7 deletions src/marks/arrow.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ export interface ArrowOptions extends MarkOptions {
insetEnd?: number;

/**
* The sweep order; defaults to null. If set to order-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 order-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). If set to order, applies
* an order-x sweep, and breaks ties with order-y.
* The sweep order; 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.
*/
sweep?: null | "order-x" | "order-y" | "order";
sweep?: number | "+x" | "-x" | "+y" | "-y" | ((x1: number, y1: number, x2: number, y2: number) => number);
}

/**
Expand Down
34 changes: 20 additions & 14 deletions src/marks/arrow.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {descending} from "d3";
import {ascending, descending} from "d3";
import {create} from "../context.js";
import {Mark} from "../mark.js";
import {radians} from "../math.js";
Expand Down Expand Up @@ -28,7 +28,7 @@ export class Arrow extends Mark {
inset = 0,
insetStart = inset,
insetEnd = inset,
sweep = null
sweep
} = options;
super(
data,
Expand All @@ -46,7 +46,7 @@ export class Arrow extends Mark {
this.headLength = +headLength;
this.insetStart = +insetStart;
this.insetEnd = +insetEnd;
this.sweep = sweep == null ? sweep : keyword(sweep, "sweep", ["order-x", "order-y", "order"]);
this.sweep = maybeSweep(sweep);
}
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
Expand Down Expand Up @@ -87,22 +87,12 @@ export class Arrow extends Mark {
// wings, but that’s okay since vectors are usually small.)
const headLength = Math.min(wingScale * sw(i), lineLength / 3);

// Maybe flip the bending, if a sweep order is applied.
const flip =
this.sweep == null
? 1
: this.sweep === "order-x"
? descending(x1, x2)
: this.sweep === "order-y"
? descending(y1, y2)
: descending(x1, x2) || descending(y1, y2); // "order"

// When bending, the offset between the straight line between the two points
// and the outgoing tangent from the start point. (Also the negative
// incoming tangent to the end point.) This must be within ±π/2. A positive
// angle will produce a clockwise curve; a negative angle will produce a
// counterclockwise curve; zero will produce a straight line.
const bendAngle = flip * bend * radians;
const bendAngle = this.sweep(x1, y1, x2, y2) * bend * radians;

// The radius of the circle that intersects with the two endpoints
// and has the specified bend angle.
Expand Down Expand Up @@ -164,6 +154,22 @@ export class Arrow extends Mark {
}
}

// Maybe flip the bend angle, depending on the arrow orientation.
function maybeSweep(sweep = 1) {
if (typeof sweep === "number") return constant(Math.sign(sweep));
if (typeof sweep === "function") return (x1, y1, x2, y2) => Math.sign(sweep(x1, y1, x2, y2));
switch (keyword(sweep, "sweep", ["+x", "-x", "+y", "-y"])) {
case "+x":
return (x1, y1, x2) => ascending(x1, x2);
case "-x":
return (x1, y1, x2) => descending(x1, x2);
case "+y":
return (x1, y1, x2, y2) => ascending(y1, y2);
case "-y":
return (x1, y1, x2, y2) => descending(y1, y2);
}
}

// Returns the center of a circle that goes through the two given points ⟨ax,ay⟩
// and ⟨bx,by⟩ and has radius r. There are two such points; use the sign +1 or
// -1 to choose between them. Returns [NaN, NaN] if r is too small.
Expand Down
2 changes: 1 addition & 1 deletion test/plots/collatz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function collatzArcDiagramUp() {
dy: -3,
bend: 70,
inset: 4,
sweep: "order",
sweep: "-x",
stroke: ([a, b]) => `url(#gradient${+(a > b)})`
}),
() =>
Expand Down
2 changes: 1 addition & 1 deletion test/plots/miserables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function miserablesArcDiagram() {
x: 0,
y1: "source",
y2: "target",
sweep: "order-y",
sweep: "-y",
bend: 90,
stroke: samegroup,
sort: samegroup,
Expand Down