From 340ee3fcff900da93c0f3213854323aa6ece12c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 5 Dec 2022 14:21:35 +0100 Subject: [PATCH 1/5] curve: "geodesic" for Plot.line closes #1146 --- README.md | 1 + src/marks/line.js | 55 ++++++- test/data/README.md | 3 + test/data/beagle.csv | 303 ++++++++++++++++++++++++++++++++++++++ test/output/beagle.svg | 326 +++++++++++++++++++++++++++++++++++++++++ test/plots/beagle.js | 22 +++ test/plots/index.js | 1 + 7 files changed, 708 insertions(+), 3 deletions(-) create mode 100644 test/data/beagle.csv create mode 100644 test/output/beagle.svg create mode 100644 test/plots/beagle.js diff --git a/README.md b/README.md index 955a6ec8ac..2061379f06 100644 --- a/README.md +++ b/README.md @@ -2836,6 +2836,7 @@ The following named curve methods are supported: * *step* - a piecewise constant function where *y* changes at the midpoint of *x* * *step-after* - a piecewise constant function where *y* changes after *x* * *step-before* - a piecewise constant function where *x* changes after *y* +* *geodesic* - a curve for spherical data along the geodesic (available only for the line mark, associated with a spherical projection) 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). diff --git a/src/marks/line.js b/src/marks/line.js index bb4c7fffaa..7dad945b0d 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,4 +1,4 @@ -import {line as shapeLine} from "d3"; +import {geoPath, line as shapeLine} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; import {indexOf, identity, maybeTuple, maybeZ} from "../options.js"; @@ -72,9 +72,11 @@ export class Line extends Mark { /** @jsdoc line */ export function line(data, options = {}) { - let {x, y, ...remainingOptions} = options; + let {x, y, curve, ...remainingOptions} = options; [x, y] = maybeTuple(x, y); - return new Line(data, {...remainingOptions, x, y}); + return curve === "geodesic" + ? new LineGeodesic(data, {...remainingOptions, x, y}) + : new Line(data, {...remainingOptions, curve, x, y}); } /** @jsdoc lineX */ @@ -88,3 +90,50 @@ export function lineY(data, options = {}) { const {x = indexOf, y = identity, ...remainingOptions} = options; return new Line(data, maybeDenseIntervalX({...remainingOptions, x, y})); } + +export class LineGeodesic extends Mark { + constructor(data, options = {}) { + const {x, y, z} = options; + super( + data, + { + x: {value: x}, + y: {value: y}, + z: {value: maybeZ(options), optional: true} + }, + options, + defaults + ); + this.z = z; + markers(this, options); + } + filter(index) { + return index; + } + render(index, scales, channels, dimensions, context) { + const {x: X, y: Y} = channels; + const {projection} = context; + if (projection === undefined) throw new Error("A projection is needed for geodesic curves"); + const path = geoPath(projection); + return create("svg:g", context) + .call(applyIndirectStyles, this, scales, dimensions, context) + .call(applyTransform, this, scales) + .call((g) => + g + .selectAll() + .data(groupIndex(index, [X, Y], this, channels)) + .enter() + .append("path") + .call(applyDirectStyles, this) + .call(applyGroupedChannelStyles, this, channels) + .call(applyGroupedMarkers, this, channels) + .attr("d", (I) => + path({ + type: "LineString", + coordinates: Array.from(I, (i) => [+X[i], +Y[i]]).filter(([x, y]) => !isNaN(x + y)) + }) + ) + ) + .node(); + } +} diff --git a/test/data/README.md b/test/data/README.md index 37c82c5ba7..5cc6a5ee29 100644 --- a/test/data/README.md +++ b/test/data/README.md @@ -19,6 +19,9 @@ https://www.flother.is/2017/olympic-games-data/ ## barley.csv http://search.r-project.org/R/library/lattice/html/barley.html +## beagle.csv +https://observablehq.com/@bmschmidt/data-driven-projections-darwins-world + ## bls-metro-unemployment.csv Bureau of Labor Statistics https://www.bls.gov/ diff --git a/test/data/beagle.csv b/test/data/beagle.csv new file mode 100644 index 0000000000..c7a6c873c2 --- /dev/null +++ b/test/data/beagle.csv @@ -0,0 +1,303 @@ +-4.17,50.37 +-7.01,47.88 +-8.75,46.24 +-9.69,45.34 +-10.75,44.27 +-11.6,43.42 +-15.05,38.4 +-15.53,37.48 +-16.54,34.36 +-21.27,20.48 +-23.4,15.05 +-23.5,14.9 +-24.29,14.22 +-25.08,13.55 +-25.32,13.28 +-26.57,11.87 +-38.42,-13.48 +-36.32,-17.2 +-38.67,-17.96 +-43.13,-22.9 +-38.75,-16.92 +-38.52,-20.17 +-40.52,-23.07 +-42.68,-23.08 +-43.13,-22.9 +-43.13,-22.9 +-45.73,-27.13 +-48.09,-30.17 +-50.3,-33.35 +-53.32,-34.98 +-58.36,-34.59 +-56.6,-36.38 +-56.64,-36.51 +-57.96,-38.6 +-58.79,-38.77 +-61.06,-39.23 +-60.91,-39.27 +-60.81,-39.29 +-60.67,-39.32 +-60.6,-39.34 +-60.43,-39.38 +-60.39,-39.39 +-60.29,-39.41 +-60.28,-39.41 +-60.21,-39.43 +-60.07,-39.46 +-59.94,-39.5 +-59.87,-39.51 +-59.52,-39.53 +-58.92,-39.41 +-57.17,-38.85 +-57.75,-34.68 +-56.58,-35.13 +-56.18,-34.87 +-58.01,-39.19 +-58.17,-39.33 +-59.72,-40.05 +-62.1,-40.8 +-61.49,-42.15 +-61.37,-43.57 +-63.37,-46.28 +-68.16,-56.48 +-69.31,-57.01 +-69.4,-56.26 +-69.38,-56.25 +-71.15,-56.73 +-69.4,-56.12 +-69.21,-56.29 +-68.74,-55.98 +-63.33,-53.3 +-62.88,-53.3 +-59.36,-50.12 +-59.92,-49.07 +-61.6,-47.2 +-62.33,-43.02 +-62.62,-41.13 +-62.66,-41.15 +-60.17,-40.48 +-55.54,-37.01 +-55.5,-37.06 +-54.95,-34.9 +-55.78,-34.9 +-54.87,-34.9 +-53.28,-35.23 +-58.07,-38.15 +-59.29,-40.13 +-59.14,-38.31 +-58.88,-38.09 +-61.97,-41.67 +-56.2,-34.9 +-54.95,-34.9 +-56.24,-35.18 +-56.97,-37.77 +-55.81,-34.9 +-56.45,-37.18 +-57.02,-37.82 +-58.4,-41.25 +-58.82,-42.53 +-60.19,-43.53 +-60.77,-44.2 +-64.43,-46.79 +-65.48,-47.63 +-66.37,-48.77 +-67.4,-49 +-66.01,-49.31 +-70.55,-53.11 +-70.71,-53.34 +-70.79,-53.43 +-70.91,-53.63 +-70.53,-53.59 +-70.51,-53.58 +-70.11,-53.54 +-68.93,-53.41 +-68.36,-53.34 +-68.23,-53.33 +-69.78,-52.81 +-70.11,-54.02 +-72.78,-52.38 +-72.6,-52.51 +-67.01,-55.24 +-66.64,-55.15 +-65.81,-54.83 +-60.8,-49.44 +-64.15,-50.17 +-65.08,-49.77 +-66.63,-50.95 +-66.76,-52.47 +-66.71,-52.28 +-66.71,-52.46 +-70.69,-53.3 +-70.93,-53.63 +-72.06,-54.13 +-72.91,-54.5 +-74.99,-54.78 +-76.23,-44.45 +-74.14,-43.29 +-73.87,-43.18 +-75.08,-42.69 +-74.96,-42.23 +-75.01,-42.19 +-75.44,-41.83 +-73.75,-35.95 +-73.18,-34.6 +-72.89,-34.17 +-71.95,-33.27 +-71.85,-33.19 +-78.03,-39.85 +-76.35,-41.48 +-76.28,-41.53 +-77.34,-43.72 +-76.75,-44.3 +-76.3,-44.44 +-74.54,-44.44 +-73.9,-43.54 +-74.09,-44 +-74.34,-45.03 +-74.6,-45.3 +-75.05,-45.9 +-75.56,-46.58 +-74.33,-44.57 +-74.05,-43.81 +-73.5,-39.83 +-73.53,-39.72 +-73.6,-38.95 +-73.77,-38.95 +-73.91,-38.3 +-73.08,-36.83 +-72.86,-35.55 +-72.82,-35.28 +-72.43,-33.75 +-72.27,-33.61 +-79.05,-34.18 +-81.16,-35.03 +-76.25,-35.63 +-73.04,-36.64 +-71.6,-33 +-71.59,-32.69 +-71.54,-32.39 +-71.28,-31.58 +-71.68,-30.22 +-71.43,-29.97 +-74.41,-31.44 +-71.65,-27.68 +-71.48,-25.53 +-70.32,-20.29 +-72.32,-18.78 +-72.78,-18.47 +-83.32,-6.87 +-89.95,-0.84 +-91.2,-1.09 +-91.33,-0.55 +-91.24,-0.36 +-90.66,0.52 +-90.67,0.26 +-91.29,-0.57 +-90.85,0.16 +-90.84,0.22 +-92.41,1.38 +-99.48,-0.92 +-104.57,-4.9 +-105.44,-5.48 +-107,-6.53 +-126.11,-11.71 +-142.07,-15.39 +-146.09,-16.09 +-147.83,-16.79 +-148.52,-17.08 +-148.67,-17.15 +-149.26,-17.4 +-152.63,-17.33 +-153.93,-17.58 +-155,-17.9 +-160.64,-19.3 +-165.21,-21.14 +-167,-22.03 +-169.98,-23.1 +-172.81,-24.23 +-179.87,-28.13 +179.93,-28.36 +179.67,-28.66 +174.28,-35.28 +174,-35.98 +164.88,-34.33 +163.97,-34.35 +159.9,-35.1 +150.44,-39.11 +149.93,-42.8 +148.24,-42.86 +147.96,-42.87 +145.23,-44.12 +133.38,-41.66 +132.35,-41.43 +129.02,-40.57 +125.48,-39.63 +123.3,-38.02 +123.2,-37.92 +122.76,-37.58 +117.29,-35.03 +113.89,-34.87 +112.97,-32.72 +109.52,-28.13 +105.73,-22.96 +104.26,-20.76 +97.54,-12.42 +97.38,-12.34 +97.36,-12.33 +97.07,-12.17 +44.99,-26.58 +42.67,-27.52 +42.55,-27.52 +42.13,-27.51 +37.87,-27.83 +36.97,-27.97 +22.76,-34.79 +21.92,-35.46 +21.05,-35.2 +20.37,-35.08 +19.97,-34.87 +20.03,-35.18 +19.85,-35.19 +17.13,-34.95 +17.06,-34.95 +16.92,-34.95 +16.91,-31.68 +15.48,-30.48 +5.22,-23.07 +3.27,-21 +-0.53,-18.87 +-3.72,-17.88 +-6.49,-15.29 +-8.21,-13.94 +-22.72,-11.28 +-23.87,-11.44 +-38.51,-12.98 +-38.25,-13.03 +-36.28,-11.5 +-35.31,-9.68 +-35.1,-7.97 +-34.86,-8.06 +-34.83,-7.96 +-34.42,-6.67 +-29.5,2.3 +-24.3,9.95 +-23.42,12.86 +-23.27,13.9 +-25.35,14.4 +-30.61,19.05 +-32.42,23.12 +-32.6,23.33 +-34.57,25.68 +-35.78,27.87 +-34.5,32.68 +-32.37,35.25 +-28.86,37.25 +-27.55,38.18 +-27.09,38.76 +-26.14,38.07 +-24.5,39.33 +-23.7,39.92 +-13.93,45.63 +-8.97,48.25 +-5.05,50.14 +-4.17,50.37 \ No newline at end of file diff --git a/test/output/beagle.svg b/test/output/beagle.svg new file mode 100644 index 0000000000..899ca78c26 --- /dev/null +++ b/test/output/beagle.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/beagle.js b/test/plots/beagle.js new file mode 100644 index 0000000000..34bb28f49c --- /dev/null +++ b/test/plots/beagle.js @@ -0,0 +1,22 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {feature} from "topojson-client"; + +export default async function () { + const world = await d3.json("data/countries-50m.json"); + // note: this returns strings; we should clean it up to make a better example, + // but we keep it to help test the projection’s robustness + const beagle = await d3.text("data/beagle.csv").then(d3.csvParseRows); + const land = feature(world, world.objects.land); + return Plot.plot({ + width: 960, + height: 480, + projection: {type: "equal-earth", rotate: [-10, 0]}, + marks: [ + Plot.geo(land, {fill: "currentColor"}), + Plot.graticule(), + Plot.line(beagle, {stroke: (d, i) => i, z: null, curve: "geodesic"}), + Plot.sphere() + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 6ed2454083..d3e5284ea7 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -25,6 +25,7 @@ export {default as athletesWeightCumulative} from "./athletes-weight-cumulative. export {default as availability} from "./availability.js"; export {default as ballotStatusRace} from "./ballot-status-race.js"; export {default as bandClip} from "./band-clip.js"; +export {default as beagle} from "./beagle.js"; export {default as beckerBarley} from "./becker-barley.js"; export {default as binStrings} from "./bin-strings.js"; export {default as binTimestamps} from "./bin-timestamps.js"; From 88dadd4b8e18e9c4899922559264029f2cfd1993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 5 Dec 2022 21:51:40 +0100 Subject: [PATCH 2/5] Line handles the "sphere" curve to follow great circles on spherical projections. --- src/marks/line.js | 92 +++++++++++++++----------------------------- test/plots/beagle.js | 2 +- 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/src/marks/line.js b/src/marks/line.js index 7dad945b0d..bf22cff4d5 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -26,18 +26,23 @@ const defaults = { export class Line extends Mark { constructor(data, options = {}) { const {x, y, z, curve, tension} = options; + + // When the line mark is used with a "sphere" curve and a projection + // (supposedly spherical), keep x and y as numbers. + const sphere = curve === "sphere"; super( data, { - x: {value: x, scale: "x"}, - y: {value: y, scale: "y"}, + x: {value: x, scale: sphere ? undefined : "x"}, + y: {value: y, scale: sphere ? undefined : "y"}, z: {value: maybeZ(options), optional: true} }, options, defaults ); this.z = z; - this.curve = Curve(curve, tension); + if (sphere) this.sphere = true; + else this.curve = Curve(curve, tension); markers(this, options); } filter(index) { @@ -45,6 +50,25 @@ export class Line extends Mark { } render(index, scales, channels, dimensions, context) { const {x: X, y: Y} = channels; + + let shape; + if (this.sphere) { + const {projection} = context; + if (projection === undefined) throw new Error("the sphere curve requires a projection"); + const path = geoPath(projection); + shape = (I) => + path({ + type: "LineString", + coordinates: Array.from(I, (i) => [+X[i], +Y[i]]).filter(([x, y]) => !isNaN(x + y)) + }); + } else { + shape = shapeLine() + .curve(this.curve) + .defined((i) => i >= 0) + .x((i) => X[i]) + .y((i) => Y[i]); + } + return create("svg:g", context) .call(applyIndirectStyles, this, scales, dimensions, context) .call(applyTransform, this, scales) @@ -57,14 +81,7 @@ export class Line extends Mark { .call(applyDirectStyles, this) .call(applyGroupedChannelStyles, this, channels) .call(applyGroupedMarkers, this, channels) - .attr( - "d", - shapeLine() - .curve(this.curve) - .defined((i) => i >= 0) - .x((i) => X[i]) - .y((i) => Y[i]) - ) + .attr("d", shape) ) .node(); } @@ -72,11 +89,9 @@ export class Line extends Mark { /** @jsdoc line */ export function line(data, options = {}) { - let {x, y, curve, ...remainingOptions} = options; + let {x, y, ...remainingOptions} = options; [x, y] = maybeTuple(x, y); - return curve === "geodesic" - ? new LineGeodesic(data, {...remainingOptions, x, y}) - : new Line(data, {...remainingOptions, curve, x, y}); + return new Line(data, {...remainingOptions, x, y}); } /** @jsdoc lineX */ @@ -90,50 +105,3 @@ export function lineY(data, options = {}) { const {x = indexOf, y = identity, ...remainingOptions} = options; return new Line(data, maybeDenseIntervalX({...remainingOptions, x, y})); } - -export class LineGeodesic extends Mark { - constructor(data, options = {}) { - const {x, y, z} = options; - super( - data, - { - x: {value: x}, - y: {value: y}, - z: {value: maybeZ(options), optional: true} - }, - options, - defaults - ); - this.z = z; - markers(this, options); - } - filter(index) { - return index; - } - render(index, scales, channels, dimensions, context) { - const {x: X, y: Y} = channels; - const {projection} = context; - if (projection === undefined) throw new Error("A projection is needed for geodesic curves"); - const path = geoPath(projection); - return create("svg:g", context) - .call(applyIndirectStyles, this, scales, dimensions, context) - .call(applyTransform, this, scales) - .call((g) => - g - .selectAll() - .data(groupIndex(index, [X, Y], this, channels)) - .enter() - .append("path") - .call(applyDirectStyles, this) - .call(applyGroupedChannelStyles, this, channels) - .call(applyGroupedMarkers, this, channels) - .attr("d", (I) => - path({ - type: "LineString", - coordinates: Array.from(I, (i) => [+X[i], +Y[i]]).filter(([x, y]) => !isNaN(x + y)) - }) - ) - ) - .node(); - } -} diff --git a/test/plots/beagle.js b/test/plots/beagle.js index 34bb28f49c..06aee8b248 100644 --- a/test/plots/beagle.js +++ b/test/plots/beagle.js @@ -15,7 +15,7 @@ export default async function () { marks: [ Plot.geo(land, {fill: "currentColor"}), Plot.graticule(), - Plot.line(beagle, {stroke: (d, i) => i, z: null, curve: "geodesic"}), + Plot.line(beagle, {stroke: (d, i) => i, z: null, curve: "sphere"}), Plot.sphere() ] }); From a095526bc269f44265ac2c867b4b2c2e37b4f220 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 5 Dec 2022 18:12:58 -0800 Subject: [PATCH 3/5] fix filtering, input normalization --- src/marks/line.js | 75 +++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/marks/line.js b/src/marks/line.js index bf22cff4d5..9f8874d9a1 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -3,6 +3,7 @@ import {create} from "../context.js"; import {Curve} from "../curve.js"; import {indexOf, identity, maybeTuple, maybeZ} from "../options.js"; import {Mark} from "../plot.js"; +import {coerceNumbers} from "../scales.js"; import { applyDirectStyles, applyIndirectStyles, @@ -23,26 +24,30 @@ const defaults = { strokeMiterlimit: 1 }; +const curveSphere = Symbol("sphere"); + +// For the “sphere” curve, return a symbol instead of a curve implementation; +// we’ll use d3.geoPath with a projection instead of d3.line to render. +function LineCurve({curve, tension}) { + return typeof curve !== "function" && `${curve}`.toLowerCase() === "sphere" ? curveSphere : Curve(curve, tension); +} + export class Line extends Mark { constructor(data, options = {}) { - const {x, y, z, curve, tension} = options; - - // When the line mark is used with a "sphere" curve and a projection - // (supposedly spherical), keep x and y as numbers. - const sphere = curve === "sphere"; + const {x, y, z} = options; + const curve = LineCurve(options); super( data, { - x: {value: x, scale: sphere ? undefined : "x"}, - y: {value: y, scale: sphere ? undefined : "y"}, + x: {value: x, scale: curve === curveSphere ? undefined : "x"}, // unscaled if projected + y: {value: y, scale: curve === curveSphere ? undefined : "y"}, // unscaled if projected z: {value: maybeZ(options), optional: true} }, options, defaults ); this.z = z; - if (sphere) this.sphere = true; - else this.curve = Curve(curve, tension); + this.curve = curve; markers(this, options); } filter(index) { @@ -50,25 +55,7 @@ export class Line extends Mark { } render(index, scales, channels, dimensions, context) { const {x: X, y: Y} = channels; - - let shape; - if (this.sphere) { - const {projection} = context; - if (projection === undefined) throw new Error("the sphere curve requires a projection"); - const path = geoPath(projection); - shape = (I) => - path({ - type: "LineString", - coordinates: Array.from(I, (i) => [+X[i], +Y[i]]).filter(([x, y]) => !isNaN(x + y)) - }); - } else { - shape = shapeLine() - .curve(this.curve) - .defined((i) => i >= 0) - .x((i) => X[i]) - .y((i) => Y[i]); - } - + const {curve} = this; return create("svg:g", context) .call(applyIndirectStyles, this, scales, dimensions, context) .call(applyTransform, this, scales) @@ -81,12 +68,42 @@ export class Line extends Mark { .call(applyDirectStyles, this) .call(applyGroupedChannelStyles, this, channels) .call(applyGroupedMarkers, this, channels) - .attr("d", shape) + .attr( + "d", + curve === curveSphere + ? sphereLine(context.projection, X, Y) + : shapeLine() + .curve(curve) + .defined((i) => i >= 0) + .x((i) => X[i]) + .y((i) => Y[i]) + ) ) .node(); } } +function sphereLine(projection, X, Y) { + if (!projection) throw new Error("sphere curve requires a projection"); + const path = geoPath(projection); + X = coerceNumbers(X); + Y = coerceNumbers(Y); + return (I) => { + let line = []; + const lines = [line]; + for (const i of I) { + // Check for undefined value; see groupIndex. + if (i === -1) { + line = []; + lines.push(line); + } else { + line.push([X[i], Y[i]]); + } + } + return path({type: "MultiLineString", coordinates: lines}); + }; +} + /** @jsdoc line */ export function line(data, options = {}) { let {x, y, ...remainingOptions} = options; From 7d7310ecb746eb090b01913a358fd73bc91c0727 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 5 Dec 2022 18:16:31 -0800 Subject: [PATCH 4/5] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2061379f06..3e006fc9e1 100644 --- a/README.md +++ b/README.md @@ -2836,9 +2836,9 @@ The following named curve methods are supported: * *step* - a piecewise constant function where *y* changes at the midpoint of *x* * *step-after* - a piecewise constant function where *y* changes after *x* * *step-before* - a piecewise constant function where *x* changes after *y* -* *geodesic* - a curve for spherical data along the geodesic (available only for the line mark, associated with a spherical projection) +* *sphere* - for spherical data along the geodesic -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). +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 *sphere* curve is only available for the [line mark](#line) and requires an associated (spherical) [projection](#projection-options). 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). From 12bb0f16b4896618ef076aa0a1aa6479f40b623a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 5 Dec 2022 18:21:26 -0800 Subject: [PATCH 5/5] curve: "projection" --- README.md | 4 ++-- src/marks/line.js | 17 +++++++++-------- test/plots/beagle.js | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3e006fc9e1..163cc70b05 100644 --- a/README.md +++ b/README.md @@ -2836,9 +2836,9 @@ The following named curve methods are supported: * *step* - a piecewise constant function where *y* changes at the midpoint of *x* * *step-after* - a piecewise constant function where *y* changes after *x* * *step-before* - a piecewise constant function where *x* changes after *y* -* *sphere* - for spherical data along the geodesic +* *projection* - use the (possibly spherical) [projection](#projection-options) -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 *sphere* curve is only available for the [line mark](#line) and requires an associated (spherical) [projection](#projection-options). +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). 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). diff --git a/src/marks/line.js b/src/marks/line.js index 9f8874d9a1..80a21f0564 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -24,12 +24,14 @@ const defaults = { strokeMiterlimit: 1 }; -const curveSphere = Symbol("sphere"); +const curveProjection = Symbol("projection"); -// For the “sphere” curve, return a symbol instead of a curve implementation; -// we’ll use d3.geoPath with a projection instead of d3.line to render. +// For the “projection” curve, return a symbol instead of a curve +// implementation; we’ll use d3.geoPath instead of d3.line to render. function LineCurve({curve, tension}) { - return typeof curve !== "function" && `${curve}`.toLowerCase() === "sphere" ? curveSphere : Curve(curve, tension); + return typeof curve !== "function" && `${curve}`.toLowerCase() === "projection" + ? curveProjection + : Curve(curve, tension); } export class Line extends Mark { @@ -39,8 +41,8 @@ export class Line extends Mark { super( data, { - x: {value: x, scale: curve === curveSphere ? undefined : "x"}, // unscaled if projected - y: {value: y, scale: curve === curveSphere ? undefined : "y"}, // unscaled if projected + x: {value: x, scale: curve === curveProjection ? undefined : "x"}, // unscaled if projected + y: {value: y, scale: curve === curveProjection ? undefined : "y"}, // unscaled if projected z: {value: maybeZ(options), optional: true} }, options, @@ -70,7 +72,7 @@ export class Line extends Mark { .call(applyGroupedMarkers, this, channels) .attr( "d", - curve === curveSphere + curve === curveProjection ? sphereLine(context.projection, X, Y) : shapeLine() .curve(curve) @@ -84,7 +86,6 @@ export class Line extends Mark { } function sphereLine(projection, X, Y) { - if (!projection) throw new Error("sphere curve requires a projection"); const path = geoPath(projection); X = coerceNumbers(X); Y = coerceNumbers(Y); diff --git a/test/plots/beagle.js b/test/plots/beagle.js index 06aee8b248..79a1302ab5 100644 --- a/test/plots/beagle.js +++ b/test/plots/beagle.js @@ -15,7 +15,7 @@ export default async function () { marks: [ Plot.geo(land, {fill: "currentColor"}), Plot.graticule(), - Plot.line(beagle, {stroke: (d, i) => i, z: null, curve: "sphere"}), + Plot.line(beagle, {stroke: (d, i) => i, z: null, curve: "projection"}), Plot.sphere() ] });