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 35 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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,35 @@ 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 (often geographic) projection in place of *x* and *y* scales. It is typically used in conjunction with [geometry marks](#geometry) to produce a map, but can be used with any mark that supports *x* and *y*, 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’ equal-area conic projection
* *albers-usa* - a composite Albers conic equal-area projection suitable for the United States
* *identity* - the identity projection (for pre-projected geometry)

In addition to the named projections above, the **projection** option may also 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, ...options}) and returns 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.**rotate** - a two- or three- element array of Euler angles to rotate the sphere
* projection.**precision** - the [sampling threshold](https://github.com/d3/d3-geo/blob/main/README.md#projection_precision)

### 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,6 +1238,28 @@ Equivalent to [Plot.dot](#plotdotdata-options) except that the **symbol** option

<!-- jsdocEnd hexagon -->

### Geometry

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

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

```js
Plot.geometry(counties, {fill: d => d.properties.rate})
```

Returns a new geometry 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 a single-element array of [*data*]. If the **geometry** option is not specified, *data* is assumed to be a GeoJSON object or an array of GeoJSON objects.

In addition to the [standard mark options](#marks), the optional **r** channel can be specified to indicate the effective radius (in pixels) of Point and MultiPoint geometries. When the radius is specified as a number, it is interpreted as a constant, which defaults to 3 pixels; otherwise it is interpreted as a channel, and uses the *r* scale. Features (or geometries) with a nonpositive radius are not drawn.

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

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

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

Returns a new geometry 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.
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 {Geometry, geometry, sphere, graticule} from "./marks/geometry.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
15 changes: 10 additions & 5 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const defaults = {
strokeWidth: 1.5
};

export function withDefaultSort(options) {
return options.sort === undefined && options.reverse === undefined
? sort({channel: "r", order: "descending"}, options)
: options;
}

export class Dot extends Mark {
constructor(data, options = {}) {
const {x, y, r, rotate, symbol = symbolCircle, frameAnchor} = options;
Expand All @@ -36,9 +42,7 @@ export class Dot extends Mark {
rotate: {value: vrotate, optional: true},
symbol: {value: vsymbol, scale: "symbol", optional: true}
},
options.sort === undefined && options.reverse === undefined
? sort({channel: "r", order: "descending"}, options)
: options,
withDefaultSort(options),
defaults
);
this.r = cr;
Expand All @@ -60,12 +64,13 @@ export class Dot extends Mark {
}
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x: X, y: Y, r: R, rotate: A, symbol: S} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
const circle = this.symbol === symbolCircle;
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((g) =>
g
.selectAll()
Expand Down
2 changes: 1 addition & 1 deletion src/marks/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class Frame extends Mark {
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
const {insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this;
return create("svg:rect", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyIndirectStyles, this, scales, dimensions, context)
.call(applyDirectStyles, this)
.call(applyTransform, this, {})
.attr("x", marginLeft + insetLeft)
Expand Down
Loading