Skip to content

raster mark #1196

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

Merged
merged 137 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
067e3d4
image data mark
mbostock Dec 21, 2022
8d74b41
PreTtiER
mbostock Dec 21, 2022
f7963ad
handle invalid data; stride, offset
mbostock Dec 21, 2022
1829c6a
handle flipped images
mbostock Dec 21, 2022
37726fb
archive test failure artifacts
mbostock Dec 21, 2022
b899270
skip image data tests, for now
mbostock Dec 22, 2022
6723675
PreTtiER
mbostock Dec 22, 2022
9ca0583
only ignore generated images in CI
mbostock Dec 22, 2022
04eacc1
only ignore large generated images
mbostock Dec 22, 2022
cbeb965
fillOpacity
mbostock Dec 22, 2022
568e0c0
tweak
mbostock Dec 22, 2022
fb7b938
fix formula
mbostock Dec 22, 2022
32d04f2
PreTtiER
mbostock Dec 22, 2022
dea0da5
volcano
mbostock Dec 22, 2022
30c4bd3
more idiomatic heatmap
mbostock Dec 22, 2022
ce258ba
fill as f(x, y)
mbostock Dec 22, 2022
afcf8d0
pixel midpoints
mbostock Dec 22, 2022
1c29502
PreTtiER
mbostock Dec 22, 2022
65f2884
not pixelated, again
mbostock Dec 22, 2022
d517606
PreTtiER
mbostock Dec 22, 2022
83826bb
raster
mbostock Dec 23, 2022
be771df
pixelRatio
mbostock Dec 23, 2022
72b327c
fix aria-label; comments
mbostock Dec 23, 2022
47f11c5
Goldstein–Price
mbostock Dec 23, 2022
4606198
tentative documentation for Plot.raster
Fil Jan 2, 2023
3626c31
fix partial coverage of sample fill
mbostock Jan 2, 2023
07c26f2
raster fillOpacity
mbostock Jan 2, 2023
9514cba
require x1, y1, x2, y2
mbostock Jan 2, 2023
15a371e
validate width, height
mbostock Jan 2, 2023
0bb39a5
fix for sparse samples
mbostock Jan 2, 2023
c7c580c
better error on missing scales
mbostock Jan 3, 2023
44a0f96
document
Fil Jan 4, 2023
ea5ef99
floor rounded (or floored?)
Fil Jan 4, 2023
f8ae682
exploration for a "nearest" raster interpolate method
Fil Jan 4, 2023
bb01d6c
barycentric interpolation
Fil Jan 4, 2023
badb88d
raster tuple shorthand
mbostock Jan 4, 2023
eae7db6
barycentric interpolate and extrapolate
Fil Jan 4, 2023
ffb6b0b
only maybeTuple if isTuples
mbostock Jan 4, 2023
4c5904a
allow marks to apply scales selectively (like we do with projections)
Fil Jan 4, 2023
f028a08
interpolate on values
Fil Jan 4, 2023
117fd5a
3 interpolation methods for the nearest neighbor: voronoi renderCell,…
Fil Jan 4, 2023
5aef679
barycentric walmart
Fil Jan 4, 2023
2d2d128
fold mark.project into mark.scale
mbostock Jan 4, 2023
7148071
fix barycentric extrapolation
mbostock Jan 4, 2023
c3e3d6b
materialize fewer arrays
mbostock Jan 5, 2023
1b44758
use channel names
mbostock Jan 5, 2023
42e93be
don’t pass {r, g, b, a}
mbostock Jan 5, 2023
8f807ec
don’t overload x & y channels
mbostock Jan 5, 2023
f2fed57
fix inverted x or y; simplify example
mbostock Jan 5, 2023
1f5f9a7
simpler
mbostock Jan 5, 2023
13cd422
Merge branch 'fil/raster-interpolate' into mbostock/image-data
mbostock Jan 5, 2023
2b9e251
fix grid orientation
mbostock Jan 5, 2023
79aa62e
only stroke if opaque
mbostock Jan 5, 2023
a526cab
optional x1, y1, x2, y2
mbostock Jan 5, 2023
d6fb66e
shorten
mbostock Jan 5, 2023
e11440d
fix order
mbostock Jan 5, 2023
3b01241
const
mbostock Jan 5, 2023
5543dde
rasterize
mbostock Jan 5, 2023
11ae0e1
The performance measurements I had done were just rubbish (I forgot t…
Fil Jan 5, 2023
97a583c
rasterize
Fil Jan 5, 2023
e91d1a9
tolerance for points that are on a triangle's edge
Fil Jan 5, 2023
e4f4064
use a symbol for values that need extrapolation, simplify and fix a f…
Fil Jan 5, 2023
40c5869
rasterize with walk on spheres
Fil Jan 5, 2023
a29af5b
document rasterize
Fil Jan 5, 2023
94d5373
pixelSize
mbostock Jan 5, 2023
b4c2a5f
default to full frame
mbostock Jan 5, 2023
83d42ed
remove ignored options
mbostock Jan 5, 2023
15261c9
reformat options
mbostock Jan 5, 2023
e19cea8
fix the ca55 tests (the coordinates represent a planar projection)
Fil Jan 6, 2023
bc98ca2
caveat about webkit/safari
Fil Jan 6, 2023
ce4c412
remove console.log
Fil Jan 6, 2023
960b751
more built-in rasterizers
mbostock Jan 6, 2023
f7eac2a
fix walk-on-spheres implementation; remove blur
Fil Jan 6, 2023
043f31e
port fixes to wos
mbostock Jan 6, 2023
d8016eb
Merge branch 'mbostock/more-rasterizers' into mbostock/image-data
mbostock Jan 6, 2023
c60ce63
adaptive extrapolation
mbostock Jan 6, 2023
9cb18c8
fillOpacity fixes
Fil Jan 7, 2023
8351cab
renames walk-on-spheres to random-walk; documents the rasterize option
Fil Jan 7, 2023
9ef6f48
a constant fillOpacity informs the opacity property on the g element,…
Fil Jan 7, 2023
a4bd54d
fix bug with projection clip in indirectStyles
Fil Jan 7, 2023
199bd6f
performance optimizations for randow-walk:
Fil Jan 7, 2023
302b308
sample pixel centroids
mbostock Jan 7, 2023
37f55e5
fix handling of undefined values
mbostock Jan 7, 2023
a44efcf
use transform for equirectangular coordinates
mbostock Jan 7, 2023
8318583
don’t delete
mbostock Jan 7, 2023
23195c1
stroke if constant fillOpacity
mbostock Jan 7, 2023
8d813d7
fix test snapshots
mbostock Jan 7, 2023
9899836
fix typo in test name
mbostock Jan 7, 2023
b2b5870
note potential bias caused by stroke
mbostock Jan 7, 2023
5fc5997
rename tests
mbostock Jan 7, 2023
860c9d9
don’t bootstrap random-walk with none
mbostock Jan 7, 2023
55ca148
terminate walk when minimum distance is reached
mbostock Jan 7, 2023
4edb533
comment re. opacity
mbostock Jan 7, 2023
2d9ced2
comment re. none order bias
mbostock Jan 8, 2023
aa572fd
contour mark
mbostock Jan 8, 2023
80d5d4d
dense grid contours
mbostock Jan 8, 2023
6653543
consolidate code
mbostock Jan 8, 2023
65035db
more code consolidation
mbostock Jan 8, 2023
9c5d9f2
cleaner
mbostock Jan 8, 2023
7d87f11
cleaner deferred channels
mbostock Jan 8, 2023
bd4d29c
interpolate, not rasterize
mbostock Jan 8, 2023
c607e7b
blur
mbostock Jan 8, 2023
14eea58
cleaner
mbostock Jan 8, 2023
01e7cd1
use typed array when possible
mbostock Jan 8, 2023
ec4ce85
optimize barycentric interpolation
mbostock Jan 8, 2023
dd06ea0
nicer contours for ca55 with barycentric+blur 3; support raster blur
Fil Jan 8, 2023
773c575
ignore negative blur
mbostock Jan 8, 2023
8b7140f
cleaner tests
mbostock Jan 8, 2023
afe7fc7
for contours, filter points with missing X and Y before calling the i…
Fil Jan 9, 2023
80560b4
fix barycentric interpolate for filtered points
Fil Jan 9, 2023
fcb9413
contour shorthands
mbostock Jan 9, 2023
fdb9d34
fix contour filtering
mbostock Jan 9, 2023
11dbb07
filter value, too
mbostock Jan 9, 2023
784db88
materialize x and y when needed
mbostock Jan 9, 2023
d7806cc
default to nearest
mbostock Jan 9, 2023
0fd19dd
comment
mbostock Jan 9, 2023
f747c77
remove obsolete opacity trick
mbostock Jan 9, 2023
99e7586
better contour thresholds; fix test
mbostock Jan 10, 2023
7d6572c
nullish instead of undefined
mbostock Jan 10, 2023
de26f60
renderBounds
mbostock Jan 10, 2023
af13e6f
fix circular import
mbostock Jan 10, 2023
7cceb64
a hand-written Peters projection seemed more fun than the sqrt scale;…
Fil Jan 10, 2023
4dd319f
update raster documentation with interpolate; document contour
Fil Jan 10, 2023
4bb1716
document Plot.identity
Fil Jan 10, 2023
307aae6
peters axes
Fil Jan 10, 2023
9eb341c
symmetric Peters
Fil Jan 10, 2023
1e22d3d
style tweak
mbostock Jan 10, 2023
21c9705
NaN instead of null
mbostock Jan 10, 2023
9a2e357
avoid error when empty quantile domain
mbostock Jan 10, 2023
85d81ab
faceted sampler raster
mbostock Jan 10, 2023
820a44c
fix test snapshot
mbostock Jan 10, 2023
a745cc3
faceted contour; fix dense faceted raster
mbostock Jan 11, 2023
d191ed0
Merge branch 'main' into mbostock/image-data
mbostock Jan 11, 2023
756050a
expose spatial interpolators
mbostock Jan 11, 2023
83a1983
pass x, y, step
mbostock Jan 11, 2023
e122bc2
error when data undefined, but not null
mbostock Jan 11, 2023
3f694e8
d3 7.8.1
mbostock Jan 11, 2023
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
6 changes: 6 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:
echo ::add-matcher::.github/eslint.json
yarn run eslint . --format=compact
- run: yarn test
- name: Test artifacts
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-output-changes
path: test/output/*-changed.*
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,66 @@ Equivalent to [Plot.cell](#plotcelldata-options), except that if the **y** optio

<!-- jsdocEnd cellY -->


### Contour

[Source](./src/marks/contour.js) · [Examples](https://observablehq.com/@observablehq/plot-contour) · Renders contour polygons from two-dimensional samples.

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

<!-- jsdoc contour -->

Returns a new contour mark with the given *data* and *options*. The *data* represents a discrete set of samples in abstract coordinates, bound to the scales *x* and *y*, and a **value** channel.

Most of the options are identical to the [raster](#raster) mark’s options, which is used internally to compute a rectangular grid of numeric values. Marching squares are then applied to derive the contour polygons for each threshold value.

The following options define the value channel and the aesthetics of the contours:
* **value** - the sample’s value (a channel); as a shorthand notation, it can be defined by setting either fill, fillOpacity or stroke
* **fill** - the contour’s fill color; if a channel, bound to the *color* scale
* **fillOpacity** - the contour’s opacity; if a channel, bound to the *opacity* scale
* **stroke** - the contour’s stroke color; if a channel, bound to the *color* scale; defaults to currentColor
* **strokeOpacity** - the (constant or variable) contour’s stroke opacity; if a channel, bound to the *opacity* scale; defaults to 1
* **strokeWidth** - the (constant or variable) contour’s stroke width; defaults to 1
* **thresholds** - the thresholds — an array of threshold values; if a *count* is specified instead of an array of thresholds, then the input values’ extent will be uniformly divided into approximately *count* bins. Defaults to [Sturges’s formula](https://github.com/d3/d3-contour/blob/main/README.md#contours_thresholds).
* **x** and **y** - the sample’s coordinates.
* **interpolate** - the interpolate method (see [raster](#raster) for details).
* **blur** - the blur radius, a non-negative number of pixels, that defaults to 0.

Each sample is projected onto the coordinate system of a rectangle with dimensions that may be specified directly with the following options:

* **width** - the number of pixels on each horizontal line
* **height** - the number of lines; a positive integer

Alternatively, the width and height of the raster can be imputed from the starting and ending positions for x and y, and a pixel size:

* **x1** - the starting horizontal position; bound to the *x* scale
* **x2** - the ending horizontal position; bound to the *x* scale
* **y1** - the starting vertical position; bound to the *y* scale
* **y2** - the ending vertical position; bound to the *y* scale
* **pixelSize** - the density of the raster image; defaults to 1

If a width has been specified, x1 defaults to 0 and x2 defaults to width; similarly, if a height has been specified, y1 defaults to 0 and y2 defaults to height. Otherwise, if data has been specified, x1, y1, x2, and y2 respectively default to the frame’s left, top, right, and bottom, coordinates. Lastly, if no data has been specified, and fill is a function of x and y, you must specify all of x1, x2, y1 and y2 to define the domain (see below).

The defaults for this mark make it convenient to draw thresholds from a flat array of values representing a rectangular matrix:

```js
Plot.contour(volcano.values, {width: volcano.width, height: volcano.height, fill: volcano.values, thresholds: 5})
```

When *data* is not specified and *value* is a function, a sample is taken for every pixel of the raster, which allows to draw contours from a function and a two-dimensional domain:

```js
Plot.contour({
fill: (x, y) => x * y * Math.sin(x) * Math.sin(y),
x1: 0,
x2: 2 * Math.PI,
y1: 0,
y2: 2 * Math.PI
})
```

<!-- jsdocEnd contour -->

### Delaunay

[<img src="./img/voronoi.png" width="320" height="198" alt="a Voronoi diagram of penguin culmens, showing the length and depth of several species">](https://observablehq.com/@observablehq/plot-delaunay)
Expand Down Expand Up @@ -1504,6 +1564,63 @@ Returns a new link with the given *data* and *options*.

<!-- jsdocEnd link -->

### Raster

[Source](./src/marks/raster.js) · [Examples](https://observablehq.com/@observablehq/plot-raster) · Fills a raster image with color samples.

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

<!-- jsdoc raster -->

Returns a new raster mark with the given *data* and *options*. The *data* represents a discrete set of samples in abstract coordinates, bound to the scales *x* and *y*, a **fill** channel bound to the *color* scale, and a **fillOpacity** channel bound to the *opacity* scale.

Each sample is drawn on a rectangular raster image with dimensions that may be specified directly with the following options:

* **width** - the number of pixels on each horizontal line
* **height** - the number of lines; a positive integer

Alternatively, the width and height of the raster can be imputed from the starting and ending positions for x and y, and a pixel size:

* **x1** - the starting horizontal position; bound to the *x* scale
* **x2** - the ending horizontal position; bound to the *x* scale
* **y1** - the starting vertical position; bound to the *y* scale
* **y2** - the ending vertical position; bound to the *y* scale
* **pixelSize** - the density of the raster image; defaults to 1

If a width has been specified, x1 defaults to 0 and x2 defaults to width; similarly, if a height has been specified, y1 defaults to 0 and y2 defaults to height. Otherwise, if data has been specified, x1, y1, x2, and y2 respectively default to the frame’s left, top, right, and bottom, coordinates. Lastly, if no data has been specified, and fill is a function of x and y, you must specify all of x1, x2, y1 and y2 to define the domain (see below).


The following options are supported:

* **fill** - the sample’s color; if a channel, bound to the *color* scale
* **fillOpacity** - the sample’s opacity; if a channel, bound to the *opacity* scale
* **x** and **y** - the sample’s coordinates
* **imageRendering** - the [image-rendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/image-rendering) attribute of the image; defaults to auto, which blends neighboring samples with bilinear interpolation. A typical setting is pixelated, that asks the browser to render each pixel as a solid rectangle (unfortunately not supported by Webkit).
* **interpolate** - the interpolate method.
* **blur** - the blur radius, a non-negative number of pixels, that defaults to 0.

The interpolate option supports the following settings:
* none - default if the *x* and *y* options are not null: assigns the value to the pixel under the (floor rounded) coordinates of each sample—if inside the raster
* dense - default otherwise; assumes that the data describes every pixel on the raster of dimensions width × height, starting from the top left, in row-major order
* nearest - evaluates each pixel with the closest sample, resulting in Voronoi cells
* barycentric - does a Delaunay triangulation of the samples, then evaluates each triangle’s interior with a mix of the values of its vertices, weighted by the distance to each of the vertices; points outside the convex hull are extrapolated
* random-walk - evaluates a pixel by simulating a random walk, and picking the value of the first sample reached
* a function that receives a sample index, width and height of the raster, the *x* and *y* positions of the samples (in the coordinate system of the raster), and an array of (unscaled) values, and must return a dense array of width * height values, organized in row-major order.

The defaults for this mark make it convenient to draw an image from a flat array of values representing a rectangular matrix:

```js
Plot.raster(volcano.values, {width: volcano.width, height: volcano.height, fill: volcano.values})
```

When *data* is not specified and *fill* or *fillOpacity* is a function, a sample is taken for every pixel of the raster, which allows to fill an image from a function and a two-dimensional domain:

```js
Plot.raster({x1: -1, x2: 1, y1: -1, y2: 1, fill: (x, y) => Math.atan2(y, x)})
```

<!-- jsdocEnd raster -->

### Rect

[<img src="./img/rect.png" width="320" height="198" alt="a histogram">](https://observablehq.com/@observablehq/plot-rect)
Expand Down Expand Up @@ -2771,6 +2888,18 @@ Plot.column is typically used by options transforms to define new channels; the

<!-- jsdocEnd column -->

#### Plot.identity

<!-- jsdoc identity -->

This channel helper returns a source array as-is, avoiding an extra copy when defining a channel as being equal to the data:

```js
Plot.raster(await readValues(), {width: 300, height: 200, fill: Plot.identity})
```

<!-- jsdocEnd identity -->

## Initializers

Initializers can be used to transform and derive new channels prior to rendering. Unlike transforms which operate in abstract data space, initializers can operate in screen space such as pixel coordinates and colors. For example, initializers can modify a marks’ positions to avoid occlusion. Initializers are invoked *after* the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or may not, as desired) be passed to scales.
Expand Down
5 changes: 4 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {boxX, boxY} from "./marks/box.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
export {Contour, contour} from "./marks/contour.js";
export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/delaunay.js";
export {Density, density} from "./marks/density.js";
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
Expand All @@ -14,13 +15,15 @@ export {Image, image} from "./marks/image.js";
export {Line, line, lineX, lineY} from "./marks/line.js";
export {linearRegressionX, linearRegressionY} from "./marks/linearRegression.js";
export {Link, link} from "./marks/link.js";
export {Raster, raster} from "./marks/raster.js";
export {interpolateNone, interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk} from "./marks/raster.js";
export {Rect, rect, rectX, rectY} from "./marks/rect.js";
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
export {Text, text, textX, textY} from "./marks/text.js";
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
export {tree, cluster} from "./marks/tree.js";
export {Vector, vector, vectorX, vectorY, spike} from "./marks/vector.js";
export {valueof, column} from "./options.js";
export {valueof, column, identity} from "./options.js";
export {filter, reverse, sort, shuffle, basic as transform, initializer} from "./transforms/basic.js";
export {bin, binX, binY} from "./transforms/bin.js";
export {centroid, geoCentroid} from "./transforms/centroid.js";
Expand Down
199 changes: 199 additions & 0 deletions src/marks/contour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {blur2, contours, geoPath, map, max, min, range, thresholdSturges} from "d3";
import {Channels} from "../channel.js";
import {create} from "../context.js";
import {labelof, identity} from "../options.js";
import {Position} from "../projection.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, styles} from "../style.js";
import {initializer} from "../transforms/basic.js";
import {maybeThresholds} from "../transforms/bin.js";
import {AbstractRaster, maybeTuples, rasterBounds, sampler} from "./raster.js";

const defaults = {
ariaLabel: "contour",
fill: "none",
stroke: "currentColor",
strokeMiterlimit: 1,
pixelSize: 2
};

export class Contour extends AbstractRaster {
constructor(data, {value, ...options} = {}) {
const channels = styles({}, options, defaults);

// If value is not specified explicitly, look for a channel to promote. If
// more than one channel is present, throw an error. (To disambiguate,
// specify the value option explicitly.)
if (value === undefined) {
for (const key in channels) {
if (channels[key].value != null) {
if (value !== undefined) throw new Error("ambiguous contour value");
value = options[key];
options[key] = "value";
}
}
}

// For any channel specified as the literal (contour threshold) "value"
// (maybe because of the promotion above), propagate the label from the
// original value definition.
if (value != null) {
const v = {transform: (D) => D.map((d) => d.value), label: labelof(value)};
for (const key in channels) {
if (options[key] === "value") {
options[key] = v;
}
}
}

// If the data is null, then we’ll construct the raster grid by evaluating a
// function for each point in a dense grid. The value channel is populated
// by the sampler initializer, and hence is not passed to super to avoid
// computing it before there’s data.
if (data == null) {
if (typeof value !== "function") throw new Error("invalid contour value");
options = sampler("value", {value, ...options});
value = null;
}

// Otherwise if data was provided, it represents a discrete set of spatial
// samples (often a grid, but not necessarily). If no interpolation method
// was specified, default to nearest.
else {
let {interpolate} = options;
if (value === undefined) value = identity;
if (interpolate === undefined) options.interpolate = "nearest";
}

// Wrap the options in our initializer that computes the contour geometries;
// this runs after any other initializers (and transforms).
super(data, {value: {value, optional: true}}, contourGeometry(options), defaults);

// With the exception of the x, y, x1, y1, x2, y2, and value channels, this
// mark’s channels are not evaluated on the initial data but rather on the
// contour multipolygons generated in the initializer.
const contourChannels = {geometry: {value: identity}};
for (const key in this.channels) {
const channel = this.channels[key];
const {scale} = channel;
if (scale === "x" || scale === "y" || key === "value") continue;
contourChannels[key] = channel;
delete this.channels[key];
}
this.contourChannels = contourChannels;
}
filter(index, {x, y, value, ...channels}, values) {
// Only filter channels constructed by the contourGeometry initializer; the
// x, y, and value channels must be filtered by the initializer itself.
return super.filter(index, channels, values);
}
render(index, scales, channels, dimensions, context) {
const {geometry: G} = channels;
const path = geoPath();
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyTransform, this, scales)
.call((g) => {
g.selectAll()
.data(index)
.enter()
.append("path")
.call(applyDirectStyles, this)
.attr("d", (i) => path(G[i]))
.call(applyChannelStyles, this, channels);
})
.node();
}
}

function contourGeometry({thresholds, interval, ...options}) {
thresholds = maybeThresholds(thresholds, interval, thresholdSturges);
return initializer(options, function (data, facets, channels, scales, dimensions, context) {
const [x1, y1, x2, y2] = rasterBounds(channels, scales, dimensions, context);
const dx = x2 - x1;
const dy = y2 - y1;
const {pixelSize: k, width: w = Math.round(Math.abs(dx) / k), height: h = Math.round(Math.abs(dy) / k)} = this;
const kx = w / dx;
const ky = h / dy;
const V = channels.value.value;
const VV = []; // V per facet

// Interpolate the raster grid, as needed.
if (this.interpolate) {
const {x: X, y: Y} = Position(channels, scales, context);
// Convert scaled (screen) coordinates to grid (canvas) coordinates.
const IX = map(X, (x) => (x - x1) * kx, Float64Array);
const IY = map(Y, (y) => (y - y1) * ky, Float64Array);
// The contour mark normally skips filtering on x, y, and value, so here
// we’re careful to use different names (0, 1, 2) when filtering.
const ichannels = [channels.x, channels.y, channels.value];
const ivalues = [IX, IY, V];
for (const facet of facets) {
const index = this.filter(facet, ichannels, ivalues);
VV.push(this.interpolate(index, w, h, IX, IY, V));
}
}

// Otherwise, chop up the existing dense raster grid into facets, if needed.
// V must be a dense grid in projected coordinates; if there are multiple
// facets, then V must be laid out vertically as facet 0, 1, 2… etc.
else if (facets) {
const n = w * h;
const m = facets.length;
for (let i = 0; i < m; ++i) VV.push(V.slice(i * n, i * n + n));
} else {
VV.push(V);
}

// Blur the raster grid, if desired.
if (this.blur > 0) for (const V of VV) blur2({data: V, width: w, height: h}, this.blur);

// Compute the contour thresholds; d3-contour unlike d3-array doesn’t pass
// the min and max automatically, so we do that here to normalize, and also
// so we can share consistent thresholds across facets. When an interval is
// used, note that the lowest threshold should be below (or equal) to the
// lowest value, or else some data will be missing.
const T =
typeof thresholds?.range === "function"
? thresholds.range(...(([min, max]) => [thresholds.floor(min), max])(finiteExtent(VV)))
: typeof thresholds === "function"
? thresholds(V, ...finiteExtent(VV))
: thresholds;

// Compute the (maybe faceted) contours.
const contour = contours().thresholds(T).size([w, h]);
const contourData = [];
const contourFacets = [];
for (const V of VV) contourFacets.push(range(contourData.length, contourData.push(...contour(V))));

// Rescale the contour multipolygon from grid to screen coordinates.
for (const {coordinates} of contourData) {
for (const rings of coordinates) {
for (const ring of rings) {
for (const point of ring) {
point[0] = point[0] / kx + x1;
point[1] = point[1] / ky + y1;
}
}
}
}

// Compute the deferred channels.
return {
data: contourData,
facets: contourFacets,
channels: Channels(this.contourChannels, contourData)
};
});
}

export function contour() {
return new Contour(...maybeTuples(...arguments));
}

function finiteExtent(VV) {
return [min(VV, (V) => min(V, finite)), max(VV, (V) => max(V, finite))];
}

function finite(x) {
return isFinite(x) ? x : NaN;
}
Loading