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"})
]
});