Skip to content

Commit d70fd8b

Browse files
Filmbostock
andauthored
curve: "projection" (#1156)
* curve: "geodesic" for Plot.line closes #1146 * Line handles the "sphere" curve to follow great circles on spherical projections. * fix filtering, input normalization * Update README * curve: "projection" Co-authored-by: Mike Bostock <[email protected]>
1 parent daffeb2 commit d70fd8b

File tree

7 files changed

+702
-11
lines changed

7 files changed

+702
-11
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2836,8 +2836,9 @@ The following named curve methods are supported:
28362836
* *step* - a piecewise constant function where *y* changes at the midpoint of *x*
28372837
* *step-after* - a piecewise constant function where *y* changes after *x*
28382838
* *step-before* - a piecewise constant function where *x* changes after *y*
2839+
* *projection* - use the (possibly spherical) [projection](#projection-options)
28392840
2840-
If *curve* is a function, it will be invoked with a given *context* in the same fashion as a [D3 curve factory](https://github.com/d3/d3-shape/blob/main/README.md#custom-curves).
2841+
If *curve* is a function, it will be invoked with a given *context* in the same fashion as a [D3 curve factory](https://github.com/d3/d3-shape/blob/main/README.md#custom-curves). The *projection* curve is only available for the [line mark](#line) and is typically used in conjunction with a spherical [projection](#projection-options) to interpolate along [geodesics](https://en.wikipedia.org/wiki/Geodesic).
28412842
28422843
The tension option only has an effect on bundle, cardinal and Catmull–Rom splines (*bundle*, *cardinal*, *cardinal-open*, *cardinal-closed*, *catmull-rom*, *catmull-rom-open*, and *catmull-rom-closed*). For bundle splines, it corresponds to [beta](https://github.com/d3/d3-shape/blob/main/README.md#curveBundle_beta); for cardinal splines, [tension](https://github.com/d3/d3-shape/blob/main/README.md#curveCardinal_tension); for Catmull–Rom splines, [alpha](https://github.com/d3/d3-shape/blob/main/README.md#curveCatmullRom_alpha).
28432844

src/marks/line.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {line as shapeLine} from "d3";
1+
import {geoPath, line as shapeLine} from "d3";
22
import {create} from "../context.js";
33
import {Curve} from "../curve.js";
44
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
55
import {Mark} from "../plot.js";
6+
import {coerceNumbers} from "../scales.js";
67
import {
78
applyDirectStyles,
89
applyIndirectStyles,
@@ -23,28 +24,40 @@ const defaults = {
2324
strokeMiterlimit: 1
2425
};
2526

27+
const curveProjection = Symbol("projection");
28+
29+
// For the “projection” curve, return a symbol instead of a curve
30+
// implementation; we’ll use d3.geoPath instead of d3.line to render.
31+
function LineCurve({curve, tension}) {
32+
return typeof curve !== "function" && `${curve}`.toLowerCase() === "projection"
33+
? curveProjection
34+
: Curve(curve, tension);
35+
}
36+
2637
export class Line extends Mark {
2738
constructor(data, options = {}) {
28-
const {x, y, z, curve, tension} = options;
39+
const {x, y, z} = options;
40+
const curve = LineCurve(options);
2941
super(
3042
data,
3143
{
32-
x: {value: x, scale: "x"},
33-
y: {value: y, scale: "y"},
44+
x: {value: x, scale: curve === curveProjection ? undefined : "x"}, // unscaled if projected
45+
y: {value: y, scale: curve === curveProjection ? undefined : "y"}, // unscaled if projected
3446
z: {value: maybeZ(options), optional: true}
3547
},
3648
options,
3749
defaults
3850
);
3951
this.z = z;
40-
this.curve = Curve(curve, tension);
52+
this.curve = curve;
4153
markers(this, options);
4254
}
4355
filter(index) {
4456
return index;
4557
}
4658
render(index, scales, channels, dimensions, context) {
4759
const {x: X, y: Y} = channels;
60+
const {curve} = this;
4861
return create("svg:g", context)
4962
.call(applyIndirectStyles, this, scales, dimensions, context)
5063
.call(applyTransform, this, scales)
@@ -59,17 +72,39 @@ export class Line extends Mark {
5972
.call(applyGroupedMarkers, this, channels)
6073
.attr(
6174
"d",
62-
shapeLine()
63-
.curve(this.curve)
64-
.defined((i) => i >= 0)
65-
.x((i) => X[i])
66-
.y((i) => Y[i])
75+
curve === curveProjection
76+
? sphereLine(context.projection, X, Y)
77+
: shapeLine()
78+
.curve(curve)
79+
.defined((i) => i >= 0)
80+
.x((i) => X[i])
81+
.y((i) => Y[i])
6782
)
6883
)
6984
.node();
7085
}
7186
}
7287

88+
function sphereLine(projection, X, Y) {
89+
const path = geoPath(projection);
90+
X = coerceNumbers(X);
91+
Y = coerceNumbers(Y);
92+
return (I) => {
93+
let line = [];
94+
const lines = [line];
95+
for (const i of I) {
96+
// Check for undefined value; see groupIndex.
97+
if (i === -1) {
98+
line = [];
99+
lines.push(line);
100+
} else {
101+
line.push([X[i], Y[i]]);
102+
}
103+
}
104+
return path({type: "MultiLineString", coordinates: lines});
105+
};
106+
}
107+
73108
/** @jsdoc line */
74109
export function line(data, options = {}) {
75110
let {x, y, ...remainingOptions} = options;

test/data/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ https://www.flother.is/2017/olympic-games-data/
1919
## barley.csv
2020
http://search.r-project.org/R/library/lattice/html/barley.html
2121

22+
## beagle.csv
23+
https://observablehq.com/@bmschmidt/data-driven-projections-darwins-world
24+
2225
## bls-metro-unemployment.csv
2326
Bureau of Labor Statistics
2427
https://www.bls.gov/

0 commit comments

Comments
 (0)