Skip to content

Commit

Permalink
more GeoJSON awareness (#2092)
Browse files Browse the repository at this point in the history
* more geojson-aware

* use GeoJSON collections
  • Loading branch information
mbostock authored Jun 16, 2024
1 parent 437e15f commit 0e5c684
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 33 deletions.
20 changes: 10 additions & 10 deletions docs/marks/geo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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
})
Expand All @@ -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. <VersionBadge pr="2092" />

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.

Expand All @@ -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"
})
]
Expand Down Expand Up @@ -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)"}))
]
Expand All @@ -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`})
]
})
Expand All @@ -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.
Expand Down
18 changes: 0 additions & 18 deletions src/marks/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 19 additions & 3 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/plots/country-centroids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {feature} from "topojson-client";
export async function countryCentroids() {
const world = await d3.json<any>("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: [
Expand Down
2 changes: 1 addition & 1 deletion test/plots/us-county-choropleth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
]
});
Expand Down

0 comments on commit 0e5c684

Please sign in to comment.