Skip to content

Commit 952147c

Browse files
authored
projected links (#1296)
1 parent 5d86dc6 commit 952147c

File tree

6 files changed

+111
-30
lines changed

6 files changed

+111
-30
lines changed

src/curve.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ import {
2020
curveStepAfter,
2121
curveStepBefore
2222
} from "d3";
23-
import type {CurveFactory, CurveBundleFactory, CurveCardinalFactory, CurveCatmullRomFactory} from "d3";
23+
import type {
24+
CurveFactory,
25+
CurveBundleFactory,
26+
CurveCardinalFactory,
27+
CurveCatmullRomFactory,
28+
CurveGenerator,
29+
Path
30+
} from "d3";
2431

2532
type CurveFunction = CurveFactory | CurveBundleFactory | CurveCardinalFactory | CurveCatmullRomFactory;
2633
type CurveName =
@@ -83,3 +90,16 @@ export function Curve(curve: CurveName | CurveFunction = curveLinear, tension?:
8390
}
8491
return c;
8592
}
93+
94+
// For the “auto” curve, return a symbol instead of a curve implementation;
95+
// we’ll use d3.geoPath to render if there’s a projection.
96+
export function PathCurve(curve: CurveName | CurveFunction = curveAuto, tension?: number): CurveFunction {
97+
return typeof curve !== "function" && `${curve}`.toLowerCase() === "auto" ? curveAuto : Curve(curve, tension);
98+
}
99+
100+
// This is a special built-in curve that will use d3.geoPath when there is a
101+
// projection, and the linear curve when there is not. You can explicitly
102+
// opt-out of d3.geoPath and instead use d3.line with the "linear" curve.
103+
export function curveAuto(context: CanvasRenderingContext2D | Path): CurveGenerator {
104+
return curveLinear(context);
105+
}

src/marks/line.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {curveLinear, geoPath, line as shapeLine} from "d3";
1+
import {geoPath, line as shapeLine} from "d3";
22
import {create} from "../context.js";
3-
import {Curve} from "../curve.js";
3+
import {curveAuto, PathCurve} from "../curve.js";
44
import {Mark} from "../mark.js";
55
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
66
import {coerceNumbers} from "../scales.js";
@@ -24,22 +24,9 @@ const defaults = {
2424
strokeMiterlimit: 1
2525
};
2626

27-
// This is a special built-in curve that will use d3.geoPath when there is a
28-
// projection, and the linear curve when there is not. You can explicitly
29-
// opt-out of d3.geoPath and instead use d3.line with the "linear" curve.
30-
function curveAuto(context) {
31-
return curveLinear(context);
32-
}
33-
34-
// For the “auto” curve, return a symbol instead of a curve implementation;
35-
// we’ll use d3.geoPath instead of d3.line to render if there’s a projection.
36-
function LineCurve({curve = curveAuto, tension}) {
37-
return typeof curve !== "function" && `${curve}`.toLowerCase() === "auto" ? curveAuto : Curve(curve, tension);
38-
}
39-
4027
export class Line extends Mark {
4128
constructor(data, options = {}) {
42-
const {x, y, z} = options;
29+
const {x, y, z, curve, tension} = options;
4330
super(
4431
data,
4532
{
@@ -51,7 +38,7 @@ export class Line extends Mark {
5138
defaults
5239
);
5340
this.z = z;
54-
this.curve = LineCurve(options);
41+
this.curve = PathCurve(curve, tension);
5542
markers(this, options);
5643
}
5744
filter(index) {

src/marks/link.js

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {pathRound as path} from "d3";
1+
import {geoPath, pathRound as path} from "d3";
22
import {create} from "../context.js";
3-
import {Curve} from "../curve.js";
3+
import {curveAuto, PathCurve} from "../curve.js";
44
import {Mark} from "../mark.js";
5+
import {coerceNumbers} from "../scales.js";
56
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
67
import {markers, applyMarkers} from "./marker.js";
78

@@ -26,9 +27,15 @@ export class Link extends Mark {
2627
options,
2728
defaults
2829
);
29-
this.curve = Curve(curve, tension);
30+
this.curve = PathCurve(curve, tension);
3031
markers(this, options);
3132
}
33+
project(channels, values, context) {
34+
// For the auto curve, projection is handled at render.
35+
if (this.curve !== curveAuto) {
36+
super.project(channels, values, context);
37+
}
38+
}
3239
render(index, scales, channels, dimensions, context) {
3340
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
3441
const {curve} = this;
@@ -42,22 +49,43 @@ export class Link extends Mark {
4249
.enter()
4350
.append("path")
4451
.call(applyDirectStyles, this)
45-
.attr("d", (i) => {
46-
const p = path();
47-
const c = curve(p);
48-
c.lineStart();
49-
c.point(X1[i], Y1[i]);
50-
c.point(X2[i], Y2[i]);
51-
c.lineEnd();
52-
return p;
53-
})
52+
.attr(
53+
"d",
54+
curve === curveAuto && context.projection
55+
? sphereLink(context.projection, X1, Y1, X2, Y2)
56+
: (i) => {
57+
const p = path();
58+
const c = curve(p);
59+
c.lineStart();
60+
c.point(X1[i], Y1[i]);
61+
c.point(X2[i], Y2[i]);
62+
c.lineEnd();
63+
return p;
64+
}
65+
)
5466
.call(applyChannelStyles, this, channels)
5567
.call(applyMarkers, this, channels)
5668
)
5769
.node();
5870
}
5971
}
6072

73+
function sphereLink(projection, X1, Y1, X2, Y2) {
74+
const path = geoPath(projection);
75+
X1 = coerceNumbers(X1);
76+
Y1 = coerceNumbers(Y1);
77+
X2 = coerceNumbers(X2);
78+
Y2 = coerceNumbers(Y2);
79+
return (i) =>
80+
path({
81+
type: "LineString",
82+
coordinates: [
83+
[X1[i], Y1[i]],
84+
[X2[i], Y2[i]]
85+
]
86+
});
87+
}
88+
6189
/** @jsdoc link */
6290
export function link(data, options = {}) {
6391
let {x, x1, x2, y, y1, y2, ...remainingOptions} = options;

test/output/geoLink.svg

Lines changed: 31 additions & 0 deletions
Loading

test/plots/geo-link.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export async function geoLink() {
4+
const xy = {x1: [-122.4194], y1: [37.7749], x2: [2.3522], y2: [48.8566]};
5+
return Plot.plot({
6+
projection: "equal-earth",
7+
marks: [
8+
Plot.sphere(),
9+
Plot.graticule(),
10+
Plot.link({length: 1}, {curve: "linear", strokeOpacity: 0.3, ...xy}),
11+
Plot.link({length: 1}, {markerEnd: "arrow", ...xy})
12+
]
13+
});
14+
}

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ export * from "./electricity-demand.js";
289289
export * from "./federal-funds.js";
290290
export * from "./frame.js";
291291
export * from "./function-contour.js";
292+
export * from "./geo-link.js";
292293
export * from "./heatmap.js";
293294
export * from "./image-rendering.js";
294295
export * from "./legend-color.js";

0 commit comments

Comments
 (0)