Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The geometry channels accepts a channelSpec #1831

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/marks/geo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface GeoOptions extends MarkOptions {
* A required channel for the geometry to render; defaults to identity,
* assuming *data* is a GeoJSON object or an iterable of GeoJSON objects.
*/
geometry?: ChannelValue;
geometry?: ChannelValueSpec | {value: ChannelValue; scale?: null | boolean | "auto" | "projection"};
Fil marked this conversation as resolved.
Show resolved Hide resolved

/**
* The size of Point and MultiPoint geometries, defaulting to a constant 3
Expand Down
25 changes: 20 additions & 5 deletions src/marks/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,17 @@ export class Geo extends Mark {
const [vr, cr] = maybeNumberChannel(options.r, 3);
super(
data,
{
geometry: {value: options.geometry, scale: "projection"},
r: {value: vr, scale: "r", filter: positive, optional: true}
},
{geometry: maybeGeometry(options.geometry), r: {value: vr, scale: "r", filter: positive, optional: true}},
Fil marked this conversation as resolved.
Show resolved Hide resolved
withDefaultSort(options),
defaults
);
this.r = cr;
}
render(index, scales, channels, dimensions, context) {
const {geometry: G, r: R} = channels;
const path = geoPath(context.projection ?? scaleProjection(scales));
const path = geoPath(
channels.channels.geometry.scale === "projection" ? context.projection ?? scaleProjection(scales) : null
);
const {r} = this;
if (negative(r)) index = [];
else if (r !== undefined) path.pointRadius(r);
Expand Down Expand Up @@ -95,3 +94,19 @@ export function sphere({strokeWidth = 1.5, ...options} = {}) {
export function graticule({strokeOpacity = 0.1, ...options} = {}) {
return geo(geoGraticule10(), {strokeOpacity, ...options});
}

export function maybeGeometry(geometry) {
const {value, scale} = geometry?.value ? geometry : {value: geometry};
Fil marked this conversation as resolved.
Show resolved Hide resolved
switch (scale) {
case false:
case null:
return {value, scale: null};
case undefined:
case true:
case "auto":
case "projection":
return {value, scale: "projection"};
default:
throw new Error(`invalid projection scale: ${scale}`);
}
}
5 changes: 3 additions & 2 deletions src/transforms/centroid.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type {ChannelTransform, ChannelValue} from "../channel.js";
import type {ChannelTransform} from "../channel.js";
import type {Initialized} from "./basic.js";
import type {GeoOptions} from "../marks/geo.js";

/** Options for the centroid and geoCentroid transforms. */
export interface CentroidOptions {
/**
* A channel supplying GeoJSON geometry; defaults to the identity transform,
* assuming that the data is GeoJSON geometry.
*/
geometry?: ChannelValue;
geometry?: GeoOptions["geometry"];
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/transforms/centroid.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {geoCentroid as GeoCentroid, geoPath} from "d3";
import {identity, valueof} from "../options.js";
import {initializer} from "./basic.js";
import {maybeGeometry} from "../marks/geo.js";

export function centroid({geometry = identity, ...options} = {}) {
// Suppress defaults for x and y since they will be computed by the initializer.
return initializer({...options, x: null, y: null}, (data, facets, channels, scales, dimensions, {projection}) => {
const G = valueof(data, geometry);
const {value, scale} = maybeGeometry(geometry);
const G = valueof(data, value);
const n = G.length;
const X = new Float64Array(n);
const Y = new Float64Array(n);
const path = geoPath(projection);
const path = geoPath(scale === "projection" ? projection : null);
for (let i = 0; i < n; ++i) [X[i], Y[i]] = path.centroid(G[i]);
return {
data,
Expand Down
1 change: 1 addition & 0 deletions test/data/counties-albers-10m.json

Large diffs are not rendered by default.

3,166 changes: 3,166 additions & 0 deletions test/output/usPreprojected.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/plots/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export * from "./us-county-choropleth.js";
export * from "./us-county-spikes.js";
export * from "./us-population-state-age-dots.js";
export * from "./us-population-state-age.js";
export * from "./us-preprojected.js";
export * from "./us-president-favorability-dots.js";
export * from "./us-president-gallery.js";
export * from "./us-presidential-election-2020.js";
Expand Down
22 changes: 22 additions & 0 deletions test/plots/us-preprojected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {feature, mesh} from "topojson-client";

export async function usPreprojected() {
const [[pr_nation, {features: pr_counties}], statemesh] = await Promise.all([
d3
.json<any>("data/counties-albers-10m.json")
.then((us) => [feature(us, us.objects.nation), feature(us, us.objects.counties)]),
d3.json<any>("data/us-counties-10m.json").then((us) => mesh(us, us.objects.states))
]);
return Plot.plot({
width: 975,
height: 610,
projection: "albers-usa",
marks: [
Plot.geo(pr_nation, {fill: "#dedede", geometry: {value: Plot.identity, scale: null}}),
Plot.geo(statemesh, {stroke: "#fff", geometry: {value: Plot.identity, scale: "projection"}}),
Plot.dot(pr_counties, Plot.centroid({geometry: {value: Plot.identity, scale: null}}))
]
});
}
36 changes: 35 additions & 1 deletion test/scales/scales-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2235,11 +2235,45 @@ it("mark(data, {channels}) respects a scale set to true or false", () => {

it("mark(data, {channels}) rejects unknown scales", () => {
assert.throws(
() => Plot.dot([], {channels: {fill: {value: (d) => d, scale: "neo"}}}).initialize().channels.fill.scale,
() => Plot.dot([], {channels: {fill: {value: (d) => d, scale: "neo"}}}).initialize(),
/^Error: unknown scale: neo$/
);
});

it("geo(data, {geometry: {scale}}) rejects invalid scales", () => {
assert.ok(Plot.geo([], {geometry: {value: Plot.identity, scale: "projection"}}));
assert.ok(Plot.geo([], {geometry: {value: Plot.identity, scale: "auto"}}));
assert.ok(Plot.geo([], {geometry: {value: Plot.identity, scale: true}}));
assert.ok(Plot.geo([], {geometry: {value: Plot.identity, scale: null}}));
assert.ok(Plot.geo([], {geometry: {value: Plot.identity}}));
assert.ok(Plot.geo([], {geometry: {value: Plot.identity, scale: false}}));
assert.throws(
() => Plot.geo([], {geometry: {value: Plot.identity, scale: "x"}}),
/^Error: invalid projection scale: x$/
);
assert.throws(
() => Plot.geo([], {geometry: {value: Plot.identity, scale: "neo"}}),
/^Error: invalid projection scale: neo$/
);
});

it("centroid(data, {geometry: {scale}}) rejects invalid scales", () => {
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: "projection"}})).plot());
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: "auto"}})).plot());
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: true}})).plot());
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: null}})).plot());
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity}})).plot());
assert.ok(Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: false}})).plot());
assert.throws(
() => Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: "x"}})).plot(),
/^Error: invalid projection scale: x$/
);
assert.throws(
() => Plot.text([], Plot.centroid({geometry: {value: Plot.identity, scale: "neo"}})).plot(),
/^Error: invalid projection scale: neo$/
);
});

// Given a plot specification (or, as shorthand, an array of marks or a single
// mark), asserts that the given named scales, when materialized from the first
// plot and used to produce a second plot, produce the same output and the same
Expand Down