Skip to content

Commit

Permalink
projected links (observablehq#1296)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock authored Feb 28, 2023
1 parent 5d86dc6 commit 952147c
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 30 deletions.
22 changes: 21 additions & 1 deletion src/curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ import {
curveStepAfter,
curveStepBefore
} from "d3";
import type {CurveFactory, CurveBundleFactory, CurveCardinalFactory, CurveCatmullRomFactory} from "d3";
import type {
CurveFactory,
CurveBundleFactory,
CurveCardinalFactory,
CurveCatmullRomFactory,
CurveGenerator,
Path
} from "d3";

type CurveFunction = CurveFactory | CurveBundleFactory | CurveCardinalFactory | CurveCatmullRomFactory;
type CurveName =
Expand Down Expand Up @@ -83,3 +90,16 @@ export function Curve(curve: CurveName | CurveFunction = curveLinear, tension?:
}
return c;
}

// For the “auto” curve, return a symbol instead of a curve implementation;
// we’ll use d3.geoPath to render if there’s a projection.
export function PathCurve(curve: CurveName | CurveFunction = curveAuto, tension?: number): CurveFunction {
return typeof curve !== "function" && `${curve}`.toLowerCase() === "auto" ? curveAuto : Curve(curve, tension);
}

// This is a special built-in curve that will use d3.geoPath when there is a
// projection, and the linear curve when there is not. You can explicitly
// opt-out of d3.geoPath and instead use d3.line with the "linear" curve.
export function curveAuto(context: CanvasRenderingContext2D | Path): CurveGenerator {
return curveLinear(context);
}
21 changes: 4 additions & 17 deletions src/marks/line.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {curveLinear, geoPath, line as shapeLine} from "d3";
import {geoPath, line as shapeLine} from "d3";
import {create} from "../context.js";
import {Curve} from "../curve.js";
import {curveAuto, PathCurve} from "../curve.js";
import {Mark} from "../mark.js";
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {coerceNumbers} from "../scales.js";
Expand All @@ -24,22 +24,9 @@ const defaults = {
strokeMiterlimit: 1
};

// This is a special built-in curve that will use d3.geoPath when there is a
// projection, and the linear curve when there is not. You can explicitly
// opt-out of d3.geoPath and instead use d3.line with the "linear" curve.
function curveAuto(context) {
return curveLinear(context);
}

// For the “auto” curve, return a symbol instead of a curve implementation;
// we’ll use d3.geoPath instead of d3.line to render if there’s a projection.
function LineCurve({curve = curveAuto, tension}) {
return typeof curve !== "function" && `${curve}`.toLowerCase() === "auto" ? curveAuto : Curve(curve, tension);
}

export class Line extends Mark {
constructor(data, options = {}) {
const {x, y, z} = options;
const {x, y, z, curve, tension} = options;
super(
data,
{
Expand All @@ -51,7 +38,7 @@ export class Line extends Mark {
defaults
);
this.z = z;
this.curve = LineCurve(options);
this.curve = PathCurve(curve, tension);
markers(this, options);
}
filter(index) {
Expand Down
52 changes: 40 additions & 12 deletions src/marks/link.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {pathRound as path} from "d3";
import {geoPath, pathRound as path} from "d3";
import {create} from "../context.js";
import {Curve} from "../curve.js";
import {curveAuto, PathCurve} from "../curve.js";
import {Mark} from "../mark.js";
import {coerceNumbers} from "../scales.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
import {markers, applyMarkers} from "./marker.js";

Expand All @@ -26,9 +27,15 @@ export class Link extends Mark {
options,
defaults
);
this.curve = Curve(curve, tension);
this.curve = PathCurve(curve, tension);
markers(this, options);
}
project(channels, values, context) {
// For the auto curve, projection is handled at render.
if (this.curve !== curveAuto) {
super.project(channels, values, context);
}
}
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
const {curve} = this;
Expand All @@ -42,22 +49,43 @@ export class Link extends Mark {
.enter()
.append("path")
.call(applyDirectStyles, this)
.attr("d", (i) => {
const p = path();
const c = curve(p);
c.lineStart();
c.point(X1[i], Y1[i]);
c.point(X2[i], Y2[i]);
c.lineEnd();
return p;
})
.attr(
"d",
curve === curveAuto && context.projection
? sphereLink(context.projection, X1, Y1, X2, Y2)
: (i) => {
const p = path();
const c = curve(p);
c.lineStart();
c.point(X1[i], Y1[i]);
c.point(X2[i], Y2[i]);
c.lineEnd();
return p;
}
)
.call(applyChannelStyles, this, channels)
.call(applyMarkers, this, channels)
)
.node();
}
}

function sphereLink(projection, X1, Y1, X2, Y2) {
const path = geoPath(projection);
X1 = coerceNumbers(X1);
Y1 = coerceNumbers(Y1);
X2 = coerceNumbers(X2);
Y2 = coerceNumbers(Y2);
return (i) =>
path({
type: "LineString",
coordinates: [
[X1[i], Y1[i]],
[X2[i], Y2[i]]
]
});
}

/** @jsdoc link */
export function link(data, options = {}) {
let {x, x1, x2, y, y1, y2, ...remainingOptions} = options;
Expand Down
31 changes: 31 additions & 0 deletions test/output/geoLink.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions test/plots/geo-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Plot from "@observablehq/plot";

export async function geoLink() {
const xy = {x1: [-122.4194], y1: [37.7749], x2: [2.3522], y2: [48.8566]};
return Plot.plot({
projection: "equal-earth",
marks: [
Plot.sphere(),
Plot.graticule(),
Plot.link({length: 1}, {curve: "linear", strokeOpacity: 0.3, ...xy}),
Plot.link({length: 1}, {markerEnd: "arrow", ...xy})
]
});
}
1 change: 1 addition & 0 deletions test/plots/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export * from "./electricity-demand.js";
export * from "./federal-funds.js";
export * from "./frame.js";
export * from "./function-contour.js";
export * from "./geo-link.js";
export * from "./heatmap.js";
export * from "./image-rendering.js";
export * from "./legend-color.js";
Expand Down

0 comments on commit 952147c

Please sign in to comment.