From 0e5c6847e752b05e444ffd15661009f59ff9240c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 16 Jun 2024 09:41:05 -0700 Subject: [PATCH] more GeoJSON awareness (#2092) * more geojson-aware * use GeoJSON collections --- docs/marks/geo.md | 20 ++++++++++---------- src/marks/geo.js | 18 ------------------ src/options.js | 22 +++++++++++++++++++--- test/plots/country-centroids.ts | 2 +- test/plots/us-county-choropleth.ts | 2 +- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/docs/marks/geo.md b/docs/marks/geo.md index 56748f0027..eeea8d1875 100644 --- a/docs/marks/geo.md +++ b/docs/marks/geo.md @@ -11,8 +11,8 @@ const walmarts = shallowRef({type: "FeatureCollection", features: []}); const world = shallowRef(null); const statemesh = computed(() => us.value ? topojson.mesh(us.value, us.value.objects.states, (a, b) => a !== b) : {type: null}); const nation = computed(() => us.value ? topojson.feature(us.value, us.value.objects.nation) : {type: null}); -const states = computed(() => us.value ? topojson.feature(us.value, us.value.objects.states).features : []); -const counties = computed(() => us.value ? topojson.feature(us.value, us.value.objects.counties).features : []); +const states = computed(() => us.value ? topojson.feature(us.value, us.value.objects.states) : {type: null}); +const counties = computed(() => us.value ? topojson.feature(us.value, us.value.objects.counties) : {type: null}); const land = computed(() => world.value ? topojson.feature(world.value, world.value.objects.land) : {type: null}); onMounted(() => { @@ -48,7 +48,7 @@ Plot.plot({ }, marks: [ Plot.geo(counties, { - fill: (d) => d.properties.unemployment, + fill: "unemployment", title: (d) => `${d.properties.name} ${d.properties.unemployment}%`, tip: true }) @@ -57,7 +57,7 @@ Plot.plot({ ``` ::: -A geo mark’s data is typically [GeoJSON](https://geojson.org/). You can pass a single GeoJSON object, a feature or geometry collection, or an array or iterable of GeoJSON objects. +A geo mark’s data is typically [GeoJSON](https://geojson.org/). You can pass a single GeoJSON object, a feature or geometry collection, or an array or iterable of GeoJSON objects; Plot automatically normalizes these into an array of features or geometries. When a mark’s data is GeoJSON, Plot will look for the specified field name (such as _unemployment_ above, for **fill**) in the GeoJSON object’s `properties` if the object does not have this property directly. The size of Point and MultiPoint geometries is controlled by the **r** option. For example, below we show earthquakes in the last seven days with a magnitude of 2.5 or higher as reported by the [USGS](https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php). As with the [dot mark](./dot.md), the effective radius is controlled by the *r* scale, which is by default a *sqrt* scale such that the area of a point is proportional to its value. And likewise point geometries are by default sorted by descending radius to reduce occlusion, drawing the smallest circles on top. Set the **sort** option to null to use input order instead. @@ -70,12 +70,12 @@ Plot.plot({ Plot.geo(land, {fill: "currentColor", fillOpacity: 0.2}), Plot.sphere(), Plot.geo(earthquakes, { - r: (d) => d.properties.mag, + r: "mag", fill: "red", fillOpacity: 0.2, stroke: "red", - title: (d) => d.properties.title, - href: (d) => d.properties.url, + title: "title", + href: "url", target: "_blank" }) ] @@ -137,7 +137,7 @@ By default, the geo mark doesn’t have **x** and **y** channels; when you use t Plot.plot({ projection: "albers-usa", marks: [ - Plot.geo(states, {strokeOpacity: 0.1, tip: true, title: (d) => d.properties.name}), + Plot.geo(states, {strokeOpacity: 0.1, tip: true, title: "name"}), Plot.geo(nation), Plot.dot(states, Plot.centroid({fill: "red", stroke: "var(--vp-c-bg-alt)"})) ] @@ -157,7 +157,7 @@ Plot.plot({ marks: [ Plot.geo(statemesh, {strokeOpacity: 0.2}), Plot.geo(nation), - Plot.geo(walmarts, {fy: (d) => d.properties.date, r: 1.5, fill: "blue", tip: true, title: (d) => d.properties.date}), + Plot.geo(walmarts, {fy: "date", r: 1.5, fill: "blue", tip: true, title: "date"}), Plot.axisFy({frameAnchor: "top", dy: 30, tickFormat: (d) => `${d.getUTCFullYear()}’s`}) ] }) @@ -181,7 +181,7 @@ The **x** and **y** position channels may also be specified in conjunction with ## geo(*data*, *options*) {#geo} ```js -Plot.geo(counties, {fill: (d) => d.properties.rate}) +Plot.geo(counties, {fill: "rate"}) ``` Returns a new geo mark with the given *data* and *options*. If *data* is a GeoJSON feature collection, then the mark’s data is *data*.features; if *data* is a GeoJSON geometry collection, then the mark’s data is *data*.geometries; if *data* is some other GeoJSON object, then the mark’s data is the single-element array [*data*]. If the **geometry** option is not specified, *data* is assumed to be a GeoJSON object or an iterable of GeoJSON objects. diff --git a/src/marks/geo.js b/src/marks/geo.js index 1c33f0a269..854e3dcef5 100644 --- a/src/marks/geo.js +++ b/src/marks/geo.js @@ -70,24 +70,6 @@ function scaleProjection({x: X, y: Y}) { } export function geo(data, options = {}) { - switch (data?.type) { - case "FeatureCollection": - data = data.features; - break; - case "GeometryCollection": - data = data.geometries; - break; - case "Feature": - case "LineString": - case "MultiLineString": - case "MultiPoint": - case "MultiPolygon": - case "Point": - case "Polygon": - case "Sphere": - data = [data]; - break; - } if (options.tip && options.x === undefined && options.y === undefined) options = centroid(options); else if (options.geometry === undefined) options = {...options, geometry: identity}; return new Geo(data, options); diff --git a/src/options.js b/src/options.js index a170e4c003..d7abbadf0e 100644 --- a/src/options.js +++ b/src/options.js @@ -47,7 +47,7 @@ function floater(f) { } export const singleton = [null]; // for data-less decoration marks, e.g. frame -export const field = (name) => (d) => d[name]; +export const field = (name) => (d) => { const v = d[name]; return v === undefined && d.type === "Feature" ? d.properties?.[name] : v; }; // prettier-ignore export const indexOf = {transform: range}; export const identity = {transform: (d) => d}; export const zero = () => 0; @@ -131,8 +131,24 @@ export function keyword(input, name, allowed) { } // Promotes the specified data to an array as needed. -export function arrayify(data) { - return data == null || data instanceof Array || data instanceof TypedArray ? data : Array.from(data); +export function arrayify(values) { + if (values == null || values instanceof Array || values instanceof TypedArray) return values; + switch (values.type) { + case "FeatureCollection": + return values.features; + case "GeometryCollection": + return values.geometries; + case "Feature": + case "LineString": + case "MultiLineString": + case "MultiPoint": + case "MultiPolygon": + case "Point": + case "Polygon": + case "Sphere": + return [values]; + } + return Array.from(values); } // An optimization of type.from(values, f): if the given values are already an diff --git a/test/plots/country-centroids.ts b/test/plots/country-centroids.ts index 2d61f236cd..55dd28a407 100644 --- a/test/plots/country-centroids.ts +++ b/test/plots/country-centroids.ts @@ -5,7 +5,7 @@ import {feature} from "topojson-client"; export async function countryCentroids() { const world = await d3.json("data/countries-110m.json"); const land = feature(world, world.objects.land); - const countries = feature(world, world.objects.countries).features; + const countries = feature(world, world.objects.countries); return Plot.plot({ projection: "mercator", marks: [ diff --git a/test/plots/us-county-choropleth.ts b/test/plots/us-county-choropleth.ts index a8d2ac2ce6..73dfb256d6 100644 --- a/test/plots/us-county-choropleth.ts +++ b/test/plots/us-county-choropleth.ts @@ -23,7 +23,7 @@ export async function usCountyChoropleth() { label: "Unemployment (%)" }, marks: [ - Plot.geo(counties, {fill: (d) => unemployment.get(d.id), title: (d) => d.properties.name}), + Plot.geo(counties, {fill: (d) => unemployment.get(d.id), title: "name"}), Plot.geo(statemesh, {stroke: "white"}) ] });