diff --git a/src/marks/arrow.d.ts b/src/marks/arrow.d.ts index 2cbd0f3a58..4ecff4b87c 100644 --- a/src/marks/arrow.d.ts +++ b/src/marks/arrow.d.ts @@ -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); } /** diff --git a/src/marks/arrow.js b/src/marks/arrow.js index 43ba2dee5f..7d1bdaf217 100644 --- a/src/marks/arrow.js +++ b/src/marks/arrow.js @@ -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"; @@ -28,7 +28,7 @@ export class Arrow extends Mark { inset = 0, insetStart = inset, insetEnd = inset, - sweep = null + sweep } = options; super( data, @@ -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; @@ -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. @@ -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. diff --git a/test/plots/collatz.ts b/test/plots/collatz.ts index 59c0f6c02b..277cf2ed77 100644 --- a/test/plots/collatz.ts +++ b/test/plots/collatz.ts @@ -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)})` }), () => diff --git a/test/plots/miserables.ts b/test/plots/miserables.ts index 72629f1a72..59e012bee0 100644 --- a/test/plots/miserables.ts +++ b/test/plots/miserables.ts @@ -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,