From 662a3c791eff5328eb840a9605df7afab14b93cb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 9 Jul 2023 09:05:21 -0400 Subject: [PATCH 1/2] functional sweep --- src/marks/arrow.js | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/marks/arrow.js b/src/marks/arrow.js index 43ba2dee5f..ddb87c2373 100644 --- a/src/marks/arrow.js +++ b/src/marks/arrow.js @@ -2,7 +2,7 @@ import {descending} from "d3"; import {create} from "../context.js"; import {Mark} from "../mark.js"; import {radians} from "../math.js"; -import {constant, keyword} from "../options.js"; +import {constant, maybeKeyword} from "../options.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; import {maybeSameValue} from "./link.js"; @@ -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,36 @@ export class Arrow extends Mark { } } +// Maybe flip the bend angle, depending on the arrow orientation. +function maybeSweep(sweep) { + switch (maybeKeyword(sweep, "sweep", ["order-x", "order-y", "order"])) { + case "order-x": + return sweepOrderX; + case "order-y": + return sweepOrderY; + case "order": + return sweepOrder; + case undefined: + return sweepNone; + } +} + +function sweepNone() { + return 1; +} + +function sweepOrderX(x1, y1, x2) { + return descending(x1, x2); +} + +function sweepOrderY(x1, y1, x2, y2) { + return descending(y1, y2); +} + +function sweepOrder(x1, y1, x2, y2) { + return descending(x1, x2) || 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. From 0869f72e563ef280c028906533d129a09194f7e4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 9 Jul 2023 09:29:54 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=C2=B1[xy]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/marks/arrow.d.ts | 15 +++++++------- src/marks/arrow.js | 44 ++++++++++++++-------------------------- test/plots/collatz.ts | 2 +- test/plots/miserables.ts | 2 +- 4 files changed, 25 insertions(+), 38 deletions(-) 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 ddb87c2373..7d1bdaf217 100644 --- a/src/marks/arrow.js +++ b/src/marks/arrow.js @@ -1,8 +1,8 @@ -import {descending} from "d3"; +import {ascending, descending} from "d3"; import {create} from "../context.js"; import {Mark} from "../mark.js"; import {radians} from "../math.js"; -import {constant, maybeKeyword} from "../options.js"; +import {constant, keyword} from "../options.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; import {maybeSameValue} from "./link.js"; @@ -28,7 +28,7 @@ export class Arrow extends Mark { inset = 0, insetStart = inset, insetEnd = inset, - sweep = null + sweep } = options; super( data, @@ -155,35 +155,21 @@ export class Arrow extends Mark { } // Maybe flip the bend angle, depending on the arrow orientation. -function maybeSweep(sweep) { - switch (maybeKeyword(sweep, "sweep", ["order-x", "order-y", "order"])) { - case "order-x": - return sweepOrderX; - case "order-y": - return sweepOrderY; - case "order": - return sweepOrder; - case undefined: - return sweepNone; +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); } } -function sweepNone() { - return 1; -} - -function sweepOrderX(x1, y1, x2) { - return descending(x1, x2); -} - -function sweepOrderY(x1, y1, x2, y2) { - return descending(y1, y2); -} - -function sweepOrder(x1, y1, x2, y2) { - return descending(x1, x2) || 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,