Skip to content

Commit

Permalink
Add d3.contourDensity.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Apr 10, 2017
1 parent 14d09a6 commit 5d5b241
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 0 deletions.
58 changes: 58 additions & 0 deletions README.md
Expand Up @@ -24,6 +24,10 @@ And here is a trippy animation of a simple trigonometric function:

[<img alt="Contour Plot III" src="https://raw.githubusercontent.com/d3/d3-contour/master/img/sin-cos.png" width="420" height="219">](https://bl.ocks.org/mbostock/bf2f5f02b62b5b3bb92ae1b59b53da36)

Contour plots can visualize the [estimated density](#density-estimation) of point clouds, and avoid overplotting in larger datasets. This library also implements fast two-dimensional kernel density estimation for contour generation; see [d3.contourDensity](#contourDensity). For example, here is a scatterplot showing the relationship between the idle duration and active duration for Old Faithful’s eruptions:

[<img alt="Density Contours" src="https://raw.githubusercontent.com/d3/d3-contour/master/img/faithful.png" width="420" height="219">](https://bl.ocks.org/mbostock/e3f4376d54e02d5d43ae32a7cf0e6aa9)

## Installing

If you use NPM, `npm install d3-contour`. Otherwise, download the [latest release](https://github.com/d3/d3-contour/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-contour.v1.min.js) or as part of [D3 4.0](https://github.com/d3/d3). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3` global is exported:
Expand Down Expand Up @@ -100,3 +104,57 @@ If *thresholds* is specified, sets the threshold generator to the specified func
Thresholds are defined as an array of values [*x0*, *x1*, …]. The first [generated contour](#_contour) corresponds to the area where the input values are greater than or equal to *x0*; the second contour corresponds to the area where the input values are greater than or equal to *x1*, and so on. Thus, there is exactly one generated MultiPolygon geometry object for each specified threshold value; the threshold value is exposed as <i>geometry</i>.value.

If a *count* is specified instead of an array of *thresholds*, then the input values’ [extent](https://github.com/d3/d3-array/blob/master/README.md#extent) will be uniformly divided into approximately *count* bins; see [d3.ticks](https://github.com/d3/d3-array/blob/master/README.md#ticks).

## Density Estimation

<a name="contourDensity" href="#contourDensity">#</a> d3.<b>contourDensity</b>() [<>](https://github.com/d3/d3-contour/blob/master/src/density.js "Source")

Constructs a new density estimator with the default settings.

<a name="_density" href="#_density">#</a> <i>density</i>(<i>data</i>) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L27 "Source")

Estimates the density contours for the given array of *data*, returning an array of [GeoJSON](http://geojson.org/geojson-spec.html) [MultiPolygon](http://geojson.org/geojson-spec.html#multipolygon) [geometry objects](http://geojson.org/geojson-spec.html#geometry-objects). Each geometry object represents the area where the estimated number of points per square pixel is greater than or equal to the corresponding [threshold value](#density_thresholds); the threshold value for each geometry object is exposed as <i>geometry</i>.value.

The *x*- and *y*-coordinate for each data point are computed using [*density*.x](#density_x) and [*density*.y](#density_y). The generated contours are only accurate within the estimator’s [defined size](#density_size).

The returned geometry objects are typically passed to [d3.geoPath](https://github.com/d3/d3-geo/blob/master/README.md#geoPath) to display, using null or [d3.geoIdentity](https://github.com/d3/d3-geo/blob/master/README.md#geoIdentity) as the associated projection.

<a name="density_x" href="#density_x">#</a> <i>density</i>.<b>x</b>([<i>x</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L91 "Source")

If *x* is specified, sets the *x*-coordinate accessor. If *x* is not specified, returns the current *x*-coordinate accessor, which defaults to:

```js
function x(d) {
return d[0];
}
```

<a name="density_y" href="#density_y">#</a> <i>density</i>.<b>y</b>([<i>y</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L95 "Source")

If *y* is specified, sets the *y*-coordinate accessor. If *y* is not specified, returns the current *y*-coordinate accessor, which defaults to:

```js
function y(d) {
return d[1];
}
```

<a name="density_size" href="#density_size">#</a> <i>density</i>.<b>size</b>([<i>size</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L99 "Source")

If *size* is specified, sets the size of the density estimator to the specified bounds and returns the estimator. The *size* is specified as an array \[<i>width</i>, <i>height</i>\], where <i>width</i> is the maximum *x*-value and <i>height</i> is the maximum *y*-value. If *size* is not specified, returns the current size which defaults to [960, 500]. The [estimated density contours](#_density) are only accurate within the defined size.

<a name="density_cellSize" href="#density_cellSize">#</a> <i>density</i>.<b>cellSize</b>([<i>cellSize</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L106 "Source")

If *cellSize* is specified, sets the size of individual cells in the underlying bin grid to the specified positive integer and returns the estimator. If *cellSize* is not specified, returns the current cell size, which defaults to 4. The cell size is rounded down to the nearest power of two. Smaller cells produce more detailed contour polygons, but are more expensive to compute.

<a name="density_thresholds" href="#density_thresholds">#</a> <i>density</i>.<b>thresholds</b>([<i>thresholds</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L112 "Source")

If *thresholds* is specified, sets the threshold generator to the specified function or array and returns this contour generator. If *thresholds* is not specified, returns the current threshold generator, which by default generates about twenty nicely-rounded density thresholds.

Thresholds are defined as an array of values [*x0*, *x1*, …]. The first [generated density contour](#_density) corresponds to the area where the estimated density is greater than or equal to *x0*; the second contour corresponds to the area where the estimated density is greater than or equal to *x1*, and so on. Thus, there is exactly one generated MultiPolygon geometry object for each specified threshold value; the threshold value is exposed as <i>geometry</i>.value. The first value *x0* should typically be greater than zero.

If a *count* is specified instead of an array of *thresholds*, then approximately *count* uniformly-spaced nicely-rounded thresholds will be generated; see [d3.ticks](https://github.com/d3/d3-array/blob/master/README.md#ticks).

<a name="density_bandwidth" href="#density_bandwidth">#</a> <i>density</i>.<b>bandwidth</b>([<i>bandwidth</i>]) [<>](https://github.com/d3/d3-contour/blob/master/src/density.js#L116 "Source")

If *bandwidth* is specified, sets the bandwidth (the standard deviation) of the Gaussian kernel and returns the estimate. If *bandwidth* is not specified, returns the current bandwidth, which defaults to 20.4939…. The specified *bandwidth* is currently rounded to the nearest supported value by this implementation, and must be nonnegative.
Binary file added img/faithful.png
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 index.js
@@ -1 +1,2 @@
export {default as contours} from "./src/contours";
export {default as contourDensity} from "./src/density";
43 changes: 43 additions & 0 deletions src/blur.js
@@ -0,0 +1,43 @@
// TODO Optimize edge cases.
// TODO Optimize index calculation.
// TODO Optimize arguments.
export function blurX(source, target, r) {
var n = source.width,
m = source.height,
w = (r << 1) + 1;
for (var j = 0; j < m; ++j) {
for (var i = 0, sr = 0; i < n + r; ++i) {
if (i < n) {
sr += source.data[i + j * n];
}
if (i >= r) {
if (i >= w) {
sr -= source.data[i - w + j * n];
}
target.data[i - r + j * n] = sr / Math.min(i + 1, n - 1 + w - i, w);
}
}
}
}

// TODO Optimize edge cases.
// TODO Optimize index calculation.
// TODO Optimize arguments.
export function blurY(source, target, r) {
var n = source.width,
m = source.height,
w = (r << 1) + 1;
for (var i = 0; i < n; ++i) {
for (var j = 0, sr = 0; j < m + r; ++j) {
if (j < m) {
sr += source.data[i + j * n];
}
if (j >= r) {
if (j >= w) {
sr -= source.data[i + (j - w) * n];
}
target.data[i + (j - r) * n] = sr / Math.min(j + 1, m - 1 + w - j, w);
}
}
}
}
123 changes: 123 additions & 0 deletions src/density.js
@@ -0,0 +1,123 @@
import {max, range, tickStep} from "d3-array";
import {slice} from "./array";
import {blurX, blurY} from "./blur";
import constant from "./constant";
import contours from "./contours";

function defaultX(d) {
return d[0];
}

function defaultY(d) {
return d[1];
}

export default function() {
var x = defaultX,
y = defaultY,
dx = 960,
dy = 500,
r = 20, // blur radius
k = 2, // log2(grid cell size)
o = r * 3, // grid offset, to pad for blur
n = (dx + o * 2) >> k, // grid width
m = (dy + o * 2) >> k, // grid height
threshold = constant(20);

function density(data) {
var values0 = new Float32Array(n * m),
values1 = new Float32Array(n * m);

data.forEach(function(d, i, data) {
var xi = (x(d, i, data) + o) >> k,
yi = (y(d, i, data) + o) >> k;
if (xi >= 0 && xi < n && yi >= 0 && yi < m) {
++values0[xi + yi * n];
}
});

// TODO Optimize.
blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);

var tz = threshold(values0);

// Convert number of thresholds into uniform thresholds.
if (!Array.isArray(tz)) {
var stop = max(values0);
tz = tickStep(0, stop, tz);
tz = range(0, Math.floor(stop / tz) * tz, tz);
tz.shift();
}

return contours()
.thresholds(tz)
.size([n, m])
(values0)
.map(transform);
}

function transform(geometry) {
geometry.value *= Math.pow(2, -2 * k); // Density in points per square pixel.
geometry.coordinates.forEach(transformPolygon);
return geometry;
}

function transformPolygon(coordinates) {
coordinates.forEach(transformRing);
}

function transformRing(coordinates) {
coordinates.forEach(transformPoint);
}

// TODO Optimize.
function transformPoint(coordinates) {
coordinates[0] = coordinates[0] * Math.pow(2, k) - o;
coordinates[1] = coordinates[1] * Math.pow(2, k) - o;
}

function resize() {
o = r * 3;
n = (dx + o * 2) >> k;
m = (dy + o * 2) >> k;
return density;
}

density.x = function(_) {
return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), density) : x;
};

density.y = function(_) {
return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), density) : y;
};

density.size = function(_) {
if (!arguments.length) return [dx, dy];
var _0 = Math.ceil(_[0]), _1 = Math.ceil(_[1]);
if (!(_0 >= 0) && !(_0 >= 0)) throw new Error("invalid size");
return dx = _0, dy = _1, resize();
};

density.cellSize = function(_) {
if (!arguments.length) return 1 << k;
if (!((_ = +_) >= 1)) throw new Error("invalid cell size");
return k = Math.floor(Math.log(_) / Math.LN2), resize();
};

density.thresholds = function(_) {
return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), density) : threshold;
};

density.bandwidth = function(_) {
if (!arguments.length) return Math.sqrt(r * (r + 1));
if (!((_ = +_) >= 0)) throw new Error("invalid bandwidth");
return r = Math.round((Math.sqrt(4 * _ * _ + 1) - 1) / 2), resize();
};

return density;
}

0 comments on commit 5d5b241

Please sign in to comment.