diff --git a/src/marks/line.js b/src/marks/line.js index 35038ab7ca..74e4b19fe5 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,4 +1,4 @@ -import {geoPath, line as shapeLine} from "d3"; +import {line as shapeLine} from "d3"; import {create} from "../context.js"; import {curveAuto, maybeCurveAuto} from "../curve.js"; import {Mark} from "../mark.js"; @@ -67,7 +67,7 @@ export class Line extends Mark { .attr( "d", curve === curveAuto && context.projection - ? sphereLine(context.projection, X, Y) + ? sphereLine(context.path(), X, Y) : shapeLine() .curve(curve) .defined((i) => i >= 0) @@ -79,8 +79,7 @@ export class Line extends Mark { } } -function sphereLine(projection, X, Y) { - const path = geoPath(projection); +function sphereLine(path, X, Y) { X = coerceNumbers(X); Y = coerceNumbers(Y); return (I) => { diff --git a/src/marks/link.js b/src/marks/link.js index 9bac4f0a4c..602727df3f 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -1,4 +1,4 @@ -import {geoPath, pathRound as path} from "d3"; +import {pathRound as path} from "d3"; import {create} from "../context.js"; import {curveAuto, maybeCurveAuto} from "../curve.js"; import {Mark} from "../mark.js"; @@ -52,7 +52,7 @@ export class Link extends Mark { .attr( "d", curve === curveAuto && context.projection - ? sphereLink(context.projection, X1, Y1, X2, Y2) + ? sphereLink(context.path(), X1, Y1, X2, Y2) : (i) => { const p = path(); const c = curve(p); @@ -70,8 +70,7 @@ export class Link extends Mark { } } -function sphereLink(projection, X1, Y1, X2, Y2) { - const path = geoPath(projection); +function sphereLink(path, X1, Y1, X2, Y2) { X1 = coerceNumbers(X1); Y1 = coerceNumbers(Y1); X2 = coerceNumbers(X2); diff --git a/src/plot.js b/src/plot.js index d92aca0587..16976c2585 100644 --- a/src/plot.js +++ b/src/plot.js @@ -159,6 +159,11 @@ export function plot(options = {}) { context.className = className; context.projection = createProjection(options, subdimensions); + // A path generator for marks that want to draw GeoJSON. + context.path = function () { + return geoPath(this.projection ?? xyProjection(scales)); + }; + // Allows e.g. the axis mark to determine faceting lazily. context.filterFacets = (data, channels) => { return facetFilter(facets, {channels, groups: facetGroups(data, channels)}); @@ -236,11 +241,6 @@ export function plot(options = {}) { facetTranslate = facetTranslator(fx, fy, dimensions); } - // A path generator for marks that want to draw GeoJSON. - context.path = function () { - return geoPath(this.projection ?? xyProjection(scales)); - }; - // Compute value objects, applying scales and projection as needed. for (const [mark, state] of stateByMark) { state.values = mark.scale(state.channels, scales, context); diff --git a/src/transforms/centroid.js b/src/transforms/centroid.js index a7d745e64f..6ad36c8c65 100644 --- a/src/transforms/centroid.js +++ b/src/transforms/centroid.js @@ -1,4 +1,4 @@ -import {geoCentroid as GeoCentroid, geoPath} from "d3"; +import {geoCentroid as GeoCentroid} from "d3"; import {memoize1} from "../memoize.js"; import {identity, valueof} from "../options.js"; import {initializer} from "./basic.js"; @@ -9,20 +9,17 @@ export function centroid({geometry = identity, ...options} = {}) { // Suppress defaults for x and y since they will be computed by the initializer. // Propagate the (memoized) geometry channel in case it’s still needed. {...options, x: null, y: null, geometry: {transform: getG}}, - (data, facets, channels, scales, dimensions, {projection}) => { + (data, facets, channels, scales, dimensions, context) => { const G = getG(data); const n = G.length; const X = new Float64Array(n); const Y = new Float64Array(n); - const path = geoPath(projection); - for (let i = 0; i < n; ++i) [X[i], Y[i]] = path.centroid(G[i]); + const {centroid} = context.path(); + for (let i = 0; i < n; ++i) [X[i], Y[i]] = centroid(G[i]); return { data, facets, - channels: { - x: {value: X, scale: projection == null ? "x" : null, source: null}, - y: {value: Y, scale: projection == null ? "y" : null, source: null} - } + channels: {x: {value: X, scale: null, source: null}, y: {value: Y, scale: null, source: null}} }; } ); diff --git a/test/output/geoTipScaled.svg b/test/output/geoTipScaled.svg new file mode 100644 index 0000000000..87587ffce6 --- /dev/null +++ b/test/output/geoTipScaled.svg @@ -0,0 +1,206 @@ + + + + + 2001 + + + 2011 + + + 2021 + + + + year + + + + + 51.30 + 51.35 + 51.40 + 51.45 + 51.50 + 51.55 + 51.60 + 51.65 + + + + + + −0.4 + −0.2 + 0.0 + 0.2 + + + −0.4 + −0.2 + 0.0 + 0.2 + + + −0.4 + −0.2 + 0.0 + 0.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/geo-tip.ts b/test/plots/geo-tip.ts index 3079418444..9ba1043222 100644 --- a/test/plots/geo-tip.ts +++ b/test/plots/geo-tip.ts @@ -91,7 +91,7 @@ function getFirstPoint(feature) { : feature.geometry.coordinates[0][0][0]; } -/** The geo mark with the tip option and x and y channels. */ +/** The geo mark with the tip option, x and y channels and a projection. */ export async function geoTipXY() { const [london, boroughs] = await getLondonBoroughs(); const access = await getLondonAccess(); @@ -115,6 +115,31 @@ export async function geoTipXY() { }); } +/** The geo mark with the tip option, and scaled x and y channels. */ +export async function geoTipScaled() { + const [, boroughs] = await getLondonBoroughs(); + const access = await getLondonAccess(); + return Plot.plot({ + width: 900, + height: 265, + color: {scheme: "RdYlBu", pivot: 0.5}, + marks: [ + Plot.geo( + access, + Plot.centroid({ + fx: "year", + geometry: (d) => boroughs.get(d.borough), + fill: "access", + stroke: "var(--plot-background)", + strokeWidth: 0.75, + channels: {borough: "borough"}, + tip: true + }) + ) + ] + }); +} + async function getLondonBoroughs() { const london = feature(await d3.json("data/london.json"), "boroughs"); const boroughs = new Map(london.features.map((d) => [d.id, d]));