From 692e5d1acb77510e5c83f6306533a1b6ac436869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 24 Nov 2024 12:05:41 +0100 Subject: [PATCH 1/6] Set context.path early --- src/context.js | 13 ++++++++++--- src/plot.js | 11 +++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/context.js b/src/context.js index 3e3e55d705..8661a0d242 100644 --- a/src/context.js +++ b/src/context.js @@ -1,9 +1,16 @@ -import {creator, select} from "d3"; +import {creator, geoPath, select} from "d3"; import {maybeClip} from "./options.js"; +import {xyProjection} from "./projection.js"; -export function createContext(options = {}) { +export function createContext(options = {}, scales) { const {document = typeof window !== "undefined" ? window.document : undefined, clip} = options; - return {document, clip: maybeClip(clip)}; + return { + document, + clip: maybeClip(clip), + path() { + return geoPath(this.projection ?? (scales && xyProjection(scales))); + } + }; } export function create(name, {document}) { diff --git a/src/plot.js b/src/plot.js index d92aca0587..8f82d6acc9 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,4 +1,4 @@ -import {creator, geoPath, select} from "d3"; +import {creator, select} from "d3"; import {createChannel, inferChannelScale} from "./channel.js"; import {createContext} from "./context.js"; import {createDimensions} from "./dimensions.js"; @@ -11,7 +11,7 @@ import {frame} from "./marks/frame.js"; import {tip} from "./marks/tip.js"; import {isColor, isIterable, isNone, isScaleOptions} from "./options.js"; import {dataify, lengthof, map, yes, maybeIntervalTransform, subarray} from "./options.js"; -import {createProjection, getGeometryChannels, hasProjection, xyProjection} from "./projection.js"; +import {createProjection, getGeometryChannels, hasProjection} from "./projection.js"; import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; import {innerDimensions, outerDimensions} from "./scales.js"; import {isPosition, registry as scaleRegistry} from "./scales/index.js"; @@ -151,7 +151,7 @@ export function plot(options = {}) { const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions; // Initialize the context. - const context = createContext(options); + const context = createContext(options, scales); const document = context.document; const svg = creator("svg").call(document.documentElement); let figure = svg; // replaced with the figure element, if any @@ -236,11 +236,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); From dab0655a7c7082f0e20b62751737f25f760b698d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 24 Nov 2024 12:24:29 +0100 Subject: [PATCH 2/6] adopt context.path --- src/marks/line.js | 7 +++---- src/marks/link.js | 7 +++---- src/transforms/centroid.js | 12 ++++++------ 3 files changed, 12 insertions(+), 14 deletions(-) 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/transforms/centroid.js b/src/transforms/centroid.js index a7d745e64f..7f56a57049 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,19 +9,19 @@ 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} + x: {value: X, scale: context.projection == null ? "x" : null, source: null}, + y: {value: Y, scale: context.projection == null ? "y" : null, source: null} } }; } From 49091405702fc0269bfd48ee56ab03a2b3565767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 24 Nov 2024 12:40:39 +0100 Subject: [PATCH 3/6] don't scale twice! adds a unit test --- src/transforms/centroid.js | 4 +- test/output/geoTipXYScaled.svg | 206 +++++++++++++++++++++++++++++++++ test/plots/geo-tip.ts | 29 ++++- 3 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 test/output/geoTipXYScaled.svg diff --git a/src/transforms/centroid.js b/src/transforms/centroid.js index 7f56a57049..8336713ce2 100644 --- a/src/transforms/centroid.js +++ b/src/transforms/centroid.js @@ -20,8 +20,8 @@ export function centroid({geometry = identity, ...options} = {}) { data, facets, channels: { - x: {value: X, scale: context.projection == null ? "x" : null, source: null}, - y: {value: Y, scale: context.projection == null ? "y" : null, source: null} + x: {value: X, scale: null, source: null}, + y: {value: Y, scale: null, source: null} } }; } diff --git a/test/output/geoTipXYScaled.svg b/test/output/geoTipXYScaled.svg new file mode 100644 index 0000000000..87587ffce6 --- /dev/null +++ b/test/output/geoTipXYScaled.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..651e65b184 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,33 @@ export async function geoTipXY() { }); } +/** The geo mark with the tip option, and scaled x and y channels. */ +export async function geoTipXYScaled() { + 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({ + x: (d) => getFirstPoint(boroughs.get(d.borough))[0], + y: (d) => getFirstPoint(boroughs.get(d.borough))[1], + 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])); From 6cb6c686aa4da6687cd19c4197b48c3ba97b4b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 25 Nov 2024 09:08:23 +0100 Subject: [PATCH 4/6] define path with projection --- src/context.js | 13 +++---------- src/plot.js | 11 ++++++++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/context.js b/src/context.js index 8661a0d242..3e3e55d705 100644 --- a/src/context.js +++ b/src/context.js @@ -1,16 +1,9 @@ -import {creator, geoPath, select} from "d3"; +import {creator, select} from "d3"; import {maybeClip} from "./options.js"; -import {xyProjection} from "./projection.js"; -export function createContext(options = {}, scales) { +export function createContext(options = {}) { const {document = typeof window !== "undefined" ? window.document : undefined, clip} = options; - return { - document, - clip: maybeClip(clip), - path() { - return geoPath(this.projection ?? (scales && xyProjection(scales))); - } - }; + return {document, clip: maybeClip(clip)}; } export function create(name, {document}) { diff --git a/src/plot.js b/src/plot.js index 8f82d6acc9..16976c2585 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,4 +1,4 @@ -import {creator, select} from "d3"; +import {creator, geoPath, select} from "d3"; import {createChannel, inferChannelScale} from "./channel.js"; import {createContext} from "./context.js"; import {createDimensions} from "./dimensions.js"; @@ -11,7 +11,7 @@ import {frame} from "./marks/frame.js"; import {tip} from "./marks/tip.js"; import {isColor, isIterable, isNone, isScaleOptions} from "./options.js"; import {dataify, lengthof, map, yes, maybeIntervalTransform, subarray} from "./options.js"; -import {createProjection, getGeometryChannels, hasProjection} from "./projection.js"; +import {createProjection, getGeometryChannels, hasProjection, xyProjection} from "./projection.js"; import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; import {innerDimensions, outerDimensions} from "./scales.js"; import {isPosition, registry as scaleRegistry} from "./scales/index.js"; @@ -151,7 +151,7 @@ export function plot(options = {}) { const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions; // Initialize the context. - const context = createContext(options, scales); + const context = createContext(options); const document = context.document; const svg = creator("svg").call(document.documentElement); let figure = svg; // replaced with the figure element, if any @@ -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)}); From b58ce2a9a145842352c9bc670fd57240d97012d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 25 Nov 2024 09:08:34 +0100 Subject: [PATCH 5/6] pretty --- src/transforms/centroid.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/transforms/centroid.js b/src/transforms/centroid.js index 8336713ce2..6ad36c8c65 100644 --- a/src/transforms/centroid.js +++ b/src/transforms/centroid.js @@ -19,10 +19,7 @@ export function centroid({geometry = identity, ...options} = {}) { return { data, facets, - channels: { - x: {value: X, scale: null, source: null}, - y: {value: Y, scale: null, source: null} - } + channels: {x: {value: X, scale: null, source: null}, y: {value: Y, scale: null, source: null}} }; } ); From 22f82a758c84e53fd9fae342bf38ab2f09f03fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 25 Nov 2024 09:12:03 +0100 Subject: [PATCH 6/6] better test --- test/output/{geoTipXYScaled.svg => geoTipScaled.svg} | 0 test/plots/geo-tip.ts | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) rename test/output/{geoTipXYScaled.svg => geoTipScaled.svg} (100%) diff --git a/test/output/geoTipXYScaled.svg b/test/output/geoTipScaled.svg similarity index 100% rename from test/output/geoTipXYScaled.svg rename to test/output/geoTipScaled.svg diff --git a/test/plots/geo-tip.ts b/test/plots/geo-tip.ts index 651e65b184..9ba1043222 100644 --- a/test/plots/geo-tip.ts +++ b/test/plots/geo-tip.ts @@ -116,7 +116,7 @@ export async function geoTipXY() { } /** The geo mark with the tip option, and scaled x and y channels. */ -export async function geoTipXYScaled() { +export async function geoTipScaled() { const [, boroughs] = await getLondonBoroughs(); const access = await getLondonAccess(); return Plot.plot({ @@ -127,8 +127,6 @@ export async function geoTipXYScaled() { Plot.geo( access, Plot.centroid({ - x: (d) => getFirstPoint(boroughs.get(d.borough))[0], - y: (d) => getFirstPoint(boroughs.get(d.borough))[1], fx: "year", geometry: (d) => boroughs.get(d.borough), fill: "access",