Skip to content

Commit cd579a3

Browse files
authored
allow thresholds array (#958)
* allow thresholds array * tweak scaling logic
1 parent 8e53224 commit cd579a3

File tree

2 files changed

+22
-13
lines changed

2 files changed

+22
-13
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ If a **z** channel is specified, the input points are grouped by *z*, and separa
10321032
10331033
Draws contours representing the estimated density of the two-dimensional points given by the **x** and **y** channels, and possibly weighted by the **weight** channel. If either of the **x** or **y** channels are not specified, the corresponding position is controlled by the **frameAnchor** option.
10341034
1035-
The **thresholds** option, which defaults to 20, specifies one more than the number of contours that will be computed at uniformly-spaced intervals between 0 (exclusive) and the maximum density (exclusive). The **bandwidth** option, which defaults to 20, specifies the standard deviation of the Gaussian kernel used for estimation in pixels.
1035+
The **thresholds** option, which defaults to 20, specifies one more than the number of contours that will be computed at uniformly-spaced intervals between 0 (exclusive) and the maximum density (exclusive). The **thresholds** option may also be specified as an array or iterable of explicit density values. The **bandwidth** option, which defaults to 20, specifies the standard deviation of the Gaussian kernel used for estimation in pixels.
10361036
10371037
If a **z**, **stroke** or **fill** channel is specified, the input points are grouped by series, and separate sets of contours are generated for each series. If the **stroke** or **fill** is specified as *density*, a color channel is constructed with values representing the density threshold value of each contour.
10381038

src/marks/density.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {contourDensity, create, geoPath} from "d3";
2-
import {identity, maybeTuple, maybeZ, valueof} from "../options.js";
2+
import {identity, isTypedArray, maybeTuple, maybeZ, valueof} from "../options.js";
33
import {Mark} from "../plot.js";
44
import {coerceNumbers} from "../scales.js";
55
import {applyFrameAnchor, applyDirectStyles, applyIndirectStyles, applyChannelStyles, applyTransform, groupZ} from "../style.js";
@@ -63,9 +63,10 @@ export function density(data, {x, y, ...options} = {}) {
6363
const dropChannels = new Set(["x", "y", "z", "weight"]);
6464

6565
function densityInitializer(options, fillDensity, strokeDensity) {
66+
const k = 100; // arbitrary scale factor for readability
6667
let {bandwidth, thresholds} = options;
6768
bandwidth = bandwidth === undefined ? 20 : +bandwidth;
68-
thresholds = thresholds === undefined ? 20 : +thresholds; // TODO Allow an array of thresholds?
69+
thresholds = thresholds === undefined ? 20 : typeof thresholds?.[Symbol.iterator] === "function" ? coerceNumbers(thresholds) : +thresholds;
6970
return initializer(options, function(data, facets, channels, scales, dimensions) {
7071
const X = channels.x ? coerceNumbers(valueof(channels.x.value, scales[channels.x.scale] || identity)) : null;
7172
const Y = channels.y ? coerceNumbers(valueof(channels.y.value, scales[channels.y.scale] || identity)) : null;
@@ -85,7 +86,6 @@ function densityInitializer(options, fillDensity, strokeDensity) {
8586
// If the fill or stroke encodes density, construct new output channels.
8687
const FD = fillDensity && [];
8788
const SD = strokeDensity && [];
88-
const k = 100; // arbitrary scale factor for readability
8989

9090
const density = contourDensity()
9191
.x(X ? i => X[i] : cx)
@@ -94,23 +94,32 @@ function densityInitializer(options, fillDensity, strokeDensity) {
9494
.size([width, height])
9595
.bandwidth(bandwidth);
9696

97-
// Compute the grid for each facet-series; find the maximum density of all
98-
// grids and use this to compute contour thresholds.
99-
let maxValue = 0;
97+
// Compute the grid for each facet-series.
10098
const facetsContours = [];
10199
for (const facet of facets) {
102100
const facetContours = [];
103101
facetsContours.push(facetContours);
104102
for (const index of Z ? groupZ(facet, Z, z) : [facet]) {
105103
const contour = density.contours(index);
106-
const max = contour.max;
107-
if (max > maxValue) maxValue = max;
108104
facetContours.push([index, contour]);
109105
}
110106
}
111107

108+
// If explicit thresholds were not specified, find the maximum density of
109+
// all grids and use this to compute thresholds.
110+
let T = thresholds;
111+
if (!isTypedArray(T)) {
112+
let maxValue = 0;
113+
for (const facetContours of facetsContours) {
114+
for (const [, contour] of facetContours) {
115+
const max = contour.max;
116+
if (max > maxValue) maxValue = max;
117+
}
118+
}
119+
T = Float64Array.from({length: thresholds - 1}, (_, i) => maxValue * k * (i + 1) / thresholds);
120+
}
121+
112122
// Generate contours for each facet-series.
113-
const T = Array.from({length: thresholds - 1}, (_, i) => maxValue * (i + 1) / thresholds);
114123
const newFacets = [];
115124
const contours = [];
116125
for (const facetContours of facetsContours) {
@@ -119,9 +128,9 @@ function densityInitializer(options, fillDensity, strokeDensity) {
119128
for (const [index, contour] of facetContours) {
120129
for (const t of T) {
121130
newFacet.push(contours.length);
122-
contours.push(contour(t));
123-
if (FD) FD.push(t * k);
124-
if (SD) SD.push(t * k);
131+
contours.push(contour(t / k));
132+
if (FD) FD.push(t);
133+
if (SD) SD.push(t);
125134
for (const key in newChannels) {
126135
newChannels[key].value.push(channels[key].value[index[0]]);
127136
}

0 commit comments

Comments
 (0)