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

geometries and projections #1111

Merged
merged 43 commits into from
Nov 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
bf05165
geometry mark
mbostock Nov 23, 2022
79d4538
pRetTIEr
mbostock Nov 23, 2022
1c2fd49
project x & y; re-fix #1043
mbostock Nov 23, 2022
ca62bc2
more projections, and options
mbostock Nov 23, 2022
5945fa3
add missing test
mbostock Nov 23, 2022
8027a85
PretTier
mbostock Nov 23, 2022
587420d
projection + hexbin
mbostock Nov 24, 2022
f63a8eb
projection.stream for x and y
mbostock Nov 24, 2022
d320bb7
test null projection
mbostock Nov 24, 2022
a85bc7a
another comment
mbostock Nov 24, 2022
8191c5a
remove TODO’s
mbostock Nov 24, 2022
8d63435
simplify walmarts
mbostock Nov 24, 2022
854b4ac
Update README
mbostock Nov 24, 2022
7c0711c
sphere, graticule
mbostock Nov 24, 2022
9c8a785
update graticule snapshot
mbostock Nov 24, 2022
119e3e8
Walmart data provenance
Fil Nov 25, 2022
4bc985e
r channel for geometries; sort by R.
Fil Nov 25, 2022
06ac9a9
Use the facets dimensions, and respect margins. Note that this change…
Fil Nov 25, 2022
6b6c05d
clip: "sphere"
mbostock Nov 25, 2022
fe09ac7
allow projection configuration function
mbostock Nov 25, 2022
355467a
revert dimensions changes
mbostock Nov 25, 2022
5d0e16c
withDefaultSort
mbostock Nov 25, 2022
ce63d6d
Merge branch 'fil/geo' into mbostock/geo
mbostock Nov 25, 2022
2bb4ff0
tweak faceted walmarts test
mbostock Nov 25, 2022
d93b812
fix auto height for pre-projected geometry
mbostock Nov 25, 2022
15c6880
more projections
mbostock Nov 26, 2022
d973dbb
projection + density
mbostock Nov 26, 2022
e2b1691
all marks can clip to sphere
Fil Nov 26, 2022
2e4ddf3
test voronoi clip to sphere
Fil Nov 26, 2022
4b561a4
error if sphere clip lacks projection
mbostock Nov 26, 2022
d0e41cb
error if projection and band-requiring channels
mbostock Nov 26, 2022
c0cd002
Update README.md
mbostock Nov 26, 2022
1114a5b
Update README.md
mbostock Nov 26, 2022
ed1b7e5
pReTTIER
mbostock Nov 26, 2022
cf8476e
projection requires x and y channels
mbostock Nov 26, 2022
11a8e83
Update README
mbostock Nov 26, 2022
23d3c83
Update README
mbostock Nov 26, 2022
c2c4d49
Update README
mbostock Nov 26, 2022
30b1acd
Update README
mbostock Nov 26, 2022
dce3b72
a more interesting voronoi map
Fil Nov 26, 2022
48da147
geo
mbostock Nov 26, 2022
3efd94d
use fitExtent for armadillo
mbostock Nov 26, 2022
4690c5e
more data provenance
mbostock Nov 26, 2022
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
71 changes: 68 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,37 @@ Plot automatically generates axes for position scales. You can configure these a

Top-level options are also supported as shorthand: **grid** (for *x* and *y* only; see [facet.grid](#facet-options)), **label**, **axis**, **inset**, **round**, **align**, and **padding**.

### Projection options

The top-level **projection** option applies a two-dimensional (often geographic) projection in place of *x* and *y* scales. It is typically used in conjunction with a [geo mark](#geo) to produce a map, but can be used with any mark that supports *x* and *y* channels, such as [dot](#dot) and [text](#text). The following built-in named projections are supported:

* *equirectangular* - the equirectangular, or *plate carrée*, projection
* *orthographic* - the orthographic projection
* *stereographic* - the stereographic projection
* *mercator* - the Mercator projection
* *equal-earth* - the [Equal Earth projection](https://en.wikipedia.org/wiki/Equal_Earth_projection) by Šavrič *et al.*
* *natural-earth* - the [Natural Earth projection](https://en.wikipedia.org/wiki/Natural_Earth_projection) by Patterson
* *azimuthal-equal-area* - the azimuthal equal-area projection
* *azimuthal-equidistant* - the azimuthal equidistant projection
* *conic-conformal* - the conic conformal projection
* *conic-equal-area* - the conic equal-area projection
* *conic-equidistant* - the conic equidistant projection
* *gnomonic* - the gnomonic projection
* *transverse-mercator* - the transverse Mercator projection
* *albers* - the Albers’ conic equal-area projection
* *albers-usa* - a composite Albers conic equal-area projection suitable for the United States
* *identity* or null (default) - the identity projection for pre-projected geometry

In addition to these named projections, the **projection** option may be specified as a [D3 projection](https://github.com/d3/d3-geo/blob/main/README.md#projections), or any custom projection that implements [*projection*.stream](https://github.com/d3/d3-geo/blob/main/README.md#projection_stream), or a function that receives a configuration object ({width, height, marginTop, marginRight, marginBottom, marginLeft, ...options}) and returns such a projection.

If the **projection** option is specified as an object, the following additional projection options are supported:

* projection.**type** - one of the projection names above
* projection.**center** - the [projection’s center of reference](https://github.com/d3/d3-geo/blob/main/README.md#projection_center)
* projection.**parallels** - the [standard parallels](https://github.com/d3/d3-geo/blob/main/README.md#conic_parallels) (for conic projections only)
* projection.**precision** - the [sampling threshold](https://github.com/d3/d3-geo/blob/main/README.md#projection_precision)
* projection.**rotate** - a two- or three- element array of Euler angles to rotate the sphere

### Color options

The normal scale types—*linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordinal*—can be used to encode color. In addition, Plot supports special scale types for color:
Expand Down Expand Up @@ -1209,12 +1240,46 @@ Equivalent to [Plot.dot](#plotdotdata-options) except that the **symbol** option

<!-- jsdocEnd hexagon -->

### Geo

[Source](./src/marks/geo.js) · [Examples](https://observablehq.com/@observablehq/plot-geo) · Draws polygons, lines, points, and other GeoJSON geometry, often in conjunction with a [geographic projection](#projection-options) to produce a thematic map. The **geometry** channel specifies the geometry (GeoJSON object) to draw; if not specified, the mark’s *data* is assumed to be GeoJSON.

#### Plot.geo(*data*, *options*)

```js
Plot.geo(counties, {fill: d => d.properties.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.

In addition to the [standard mark options](#marks), the **r** option controls the size of Point and MultiPoint geometries. It can be specified as either a channel or constant. When **r** is specified as a number, it is interpreted as a constant radius in pixels; otherwise it is interpreted as a channel and the effective radius is controlled by the *r* scale. (As with [dots](#dot), the *r* scale defaults to a *sqrt* scale such that the visual area of a point is proportional to its associated value.) If the **r** option is not specified it defaults to 3 pixels. Geometries with a nonpositive radius are not drawn. If **r** is a channel, geometries will be sorted by descending radius by default.

#### Plot.sphere(*options*)

```js
Plot.sphere()
```

Returns a new geo mark with a *Sphere* geometry object and the given *options*.

#### Plot.graticule(*options*)

```js
Plot.graticule()
```

Returns a new geo mark with a [default 10° global graticule](https://github.com/d3/d3-geo/blob/main/README.md#geoGraticule10) geometry object and the given *options*.

### Hexgrid

The hexgrid mark can be used to support marks using the [hexbin](#hexbin) layout.

#### Plot.hexgrid(*options*)

```js
Plot.hexgrid()
```

<!-- jsdoc hexgrid -->

The **binWidth** option specifies the distance between the centers of neighboring hexagons, in pixels (defaults to 20). The **clip** option defaults to true, clipping the mark to the frame’s dimensions.
Expand Down Expand Up @@ -2355,7 +2420,7 @@ The supported stack options are:

The following **order** methods are supported:

- null - input order (default)
- null (default) - input order
- *value* - ascending value order (or descending with **reverse**)
- *sum* - order series by their total value
- *appearance* - order series by the position of their maximum value
Expand All @@ -2369,7 +2434,7 @@ The stack transform supports diverging stacks: negative values are stacked below

After all values have been stacked from zero, an optional **offset** can be applied to translate or scale the stacks. The following **offset** methods are supported:

- null - a zero baseline (default)
- null (default) - a zero baseline
- *expand* (or *normalize*) - rescale each stack to fill [0, 1]
- *center* (or *silhouette*) - align the centers of all stacks
- *wiggle* - translate stacks to minimize apparent movement
Expand Down Expand Up @@ -2769,7 +2834,7 @@ A [marker](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) defi

The following named markers are supported:

* *none* - no marker (default)
* *none* (default) - no marker
* *arrow* - an arrowhead
* *dot* - a filled *circle* without a stroke and 2.5px radius
* *circle*, equivalent to *circle-fill* - a filled circle with a white stroke and 3px radius
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"canvas": "2",
"d3-geo-projection": "^4.0.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.5.0",
"get-tsconfig": "^4.1.0",
Expand All @@ -65,6 +66,7 @@
"prettier": "^2.7.1",
"rollup": "2",
"rollup-plugin-terser": "7",
"topojson-client": "^3.1.0",
"tsx": "^3.8.0",
"typescript": "^4.6.4",
"typescript-module-alias": "^1.0.2",
Expand Down
31 changes: 28 additions & 3 deletions src/channel.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {ascending, descending, rollup, sort} from "d3";
import {first, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
import {applyProjection} from "./projection.js";
import {registry} from "./scales/index.js";
import {maybeReduce} from "./transforms/group.js";

Expand All @@ -24,13 +25,37 @@ export function Channels(descriptors, data) {
}

// TODO Use Float64Array for scales with numeric ranges, e.g. position?
export function valueObject(channels, scales) {
return Object.fromEntries(
export function valueObject(channels, scales, {projection}) {
let x, y; // names of channels bound to x and y scale

const values = Object.fromEntries(
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
const scale = scales[scaleName];
let scale;
if (scaleName !== undefined) {
if (scaleName === "x") x = x === undefined ? name : "*";
else if (scaleName === "y") y = y === undefined ? name : "*";
scale = scales[scaleName];
}
return [name, scale === undefined ? value : map(value, scale)];
})
);

// If there is a projection, and there are both x and y channels, and those x
// and y channels are associated with the x and y scale respectively (and not
// already in screen coordinates as with an initializer), then apply the
// projection, replacing the x and y values. Note that the x and y scales
// themselves don’t exist if there is a projection, but whether the channels
// are associated with scales still determines whether the projection should
// apply; think of the projection as a combination xy-scale.
if (projection) {
if (x === "x" && y === "y") {
applyProjection(values, projection);
} else if (x || y) {
throw new Error("projection requires x and y channels");
}
}

return values;
}

// Note: mutates channel.domain! This is set to a function so that it is lazily
Expand Down
8 changes: 6 additions & 2 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {creator, select} from "d3";
import {maybeProjection} from "./projection.js";

export function Context({document = window.document} = {}) {
return {document};
export function Context(
{document = typeof window !== "undefined" ? window.document : undefined, projection} = {},
dimensions
) {
return {document, projection: maybeProjection(projection, dimensions)};
}

export function create(name, {document}) {
Expand Down
11 changes: 7 additions & 4 deletions src/dimensions.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {isProjection} from "./projection.js";
import {isOrdinalScale} from "./scales.js";
import {offset} from "./style.js";

export function Dimensions(
scales,
geometry,
{x: {axis: xAxis} = {}, y: {axis: yAxis} = {}, fx: {axis: fxAxis} = {}, fy: {axis: fyAxis} = {}},
{
projection,
width = 640,
height = autoHeight(scales),
height = autoHeight(scales, geometry || isProjection(projection)),
facet: {
margin: facetMargin,
marginTop: facetMarginTop = facetMargin !== undefined ? facetMargin : fxAxis === "top" ? 30 : 0,
Expand Down Expand Up @@ -43,8 +46,8 @@ export function Dimensions(
};
}

function autoHeight({y, fy, fx}) {
function autoHeight({y, fy, fx}, geometry) {
const nfy = fy ? fy.scale.domain().length : 1;
const ny = y ? (isOrdinalScale(y) ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1;
return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
const ny = y ? (isOrdinalScale(y) ? y.scale.domain().length : Math.max(7, 17 / nfy)) : geometry ? 17 : 1;
return !!(y || fy || geometry) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/de
export {Density, density} from "./marks/density.js";
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
export {Frame, frame} from "./marks/frame.js";
export {Geo, geo, sphere, graticule} from "./marks/geo.js";
export {Hexgrid, hexgrid} from "./marks/hexgrid.js";
export {Image, image} from "./marks/image.js";
export {Line, line, lineX, lineY} from "./marks/line.js";
Expand Down
2 changes: 1 addition & 1 deletion src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Area extends Mark {
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, scales, 0, 0)
.call((g) =>
g
Expand Down
2 changes: 1 addition & 1 deletion src/marks/arrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class Arrow extends Mark {
const wingScale = headLength / 1.5;

return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, scales)
.call((g) =>
g
Expand Down
2 changes: 1 addition & 1 deletion src/marks/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class AbstractBar extends Mark {
render(index, scales, channels, dimensions, context) {
const {rx, ry} = this;
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(this._transform, this, scales)
.call((g) =>
g
Expand Down
15 changes: 9 additions & 6 deletions src/marks/delaunay.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class DelaunayLink extends Mark {
markers(this, options);
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x: X, y: Y, z: Z} = channels;
const {curve} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
Expand Down Expand Up @@ -123,8 +124,8 @@ class DelaunayLink extends Mark {
}

return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, {x: X && x, y: Y && y})
.call(
Z
? (g) =>
Expand Down Expand Up @@ -155,6 +156,7 @@ class AbstractDelaunayMark extends Mark {
);
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x: X, y: Y, z: Z} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
const xi = X ? (i) => X[i] : constant(cx);
Expand All @@ -172,8 +174,8 @@ class AbstractDelaunayMark extends Mark {
}

return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, {x: X && x, y: Y && y})
.call(
Z
? (g) =>
Expand Down Expand Up @@ -223,6 +225,7 @@ class Voronoi extends Mark {
);
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x: X, y: Y, z: Z} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
const xi = X ? (i) => X[i] : constant(cx);
Expand All @@ -242,8 +245,8 @@ class Voronoi extends Mark {
}

return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, {x: X && x, y: Y && y})
.call(
Z
? (g) =>
Expand Down
26 changes: 20 additions & 6 deletions src/marks/density.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {contourDensity, create, geoPath} from "d3";
import {identity, isTypedArray, maybeTuple, maybeZ, valueof} from "../options.js";
import {valueObject} from "../channel.js";
import {isTypedArray, maybeTuple, maybeZ} from "../options.js";
import {Mark} from "../plot.js";
import {coerceNumbers} from "../scales.js";
import {
Expand Down Expand Up @@ -49,8 +50,8 @@ export class Density extends Mark {
const {contours} = channels;
const path = geoPath();
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyTransform, this, {})
.call((g) =>
g
.selectAll()
Expand Down Expand Up @@ -84,15 +85,28 @@ function densityInitializer(options, fillDensity, strokeDensity) {
: typeof thresholds?.[Symbol.iterator] === "function"
? coerceNumbers(thresholds)
: +thresholds;
return initializer(options, function (data, facets, channels, scales, dimensions) {
const X = channels.x ? coerceNumbers(valueof(channels.x.value, scales[channels.x.scale] || identity)) : null;
const Y = channels.y ? coerceNumbers(valueof(channels.y.value, scales[channels.y.scale] || identity)) : null;
return initializer(options, function (data, facets, channels, scales, dimensions, context) {
const W = channels.weight ? coerceNumbers(channels.weight.value) : null;
const Z = channels.z?.value;
const {z} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
const {width, height} = dimensions;

// Extract the scaled (or projected!) values for the x and y channels.
let {x: X, y: Y} = valueObject(
{
...(channels.x && {x: channels.x}),
...(channels.y && {y: channels.y})
},
scales,
context
);

// Coerce the x and y channels to numbers (so that null is properly treated
// as an undefined value rather than being coerced to zero).
if (X) X = coerceNumbers(X);
if (Y) Y = coerceNumbers(Y);

// Group any of the input channels according to the first index associated
// with each z-series or facet. Drop any channels not be needed for
// rendering after the contours are computed.
Expand Down
Loading