Skip to content

Commit adf8278

Browse files
Filmbostock
andauthored
ensure thresholds consistency across facets (#1226)
* ensure thresholds consistency across facets * arrayify Co-authored-by: Mike Bostock <[email protected]> * nice ticks; use contour * polish Co-authored-by: Mike Bostock <[email protected]>
1 parent 5e6341d commit adf8278

File tree

3 files changed

+208
-15
lines changed

3 files changed

+208
-15
lines changed

src/marks/contour.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {blur2, contours, geoPath, map, max, min, range, thresholdSturges} from "d3";
1+
import {blur2, contours, geoPath, map, max, min, nice, range, ticks, thresholdSturges} from "d3";
22
import {Channels} from "../channel.js";
33
import {create} from "../context.js";
4-
import {labelof, identity} from "../options.js";
4+
import {labelof, identity, arrayify} from "../options.js";
55
import {Position} from "../projection.js";
66
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, styles} from "../style.js";
77
import {initializer} from "../transforms/basic.js";
@@ -148,23 +148,15 @@ function contourGeometry({thresholds, interval, ...options}) {
148148
// Blur the raster grid, if desired.
149149
if (this.blur > 0) for (const V of VV) blur2({data: V, width: w, height: h}, this.blur);
150150

151-
// Compute the contour thresholds; d3-contour unlike d3-array doesn’t pass
152-
// the min and max automatically, so we do that here to normalize, and also
153-
// so we can share consistent thresholds across facets. When an interval is
154-
// used, note that the lowest threshold should be below (or equal) to the
155-
// lowest value, or else some data will be missing.
156-
const T =
157-
typeof thresholds?.range === "function"
158-
? thresholds.range(...(([min, max]) => [thresholds.floor(min), max])(finiteExtent(VV)))
159-
: typeof thresholds === "function"
160-
? thresholds(V, ...finiteExtent(VV))
161-
: thresholds;
151+
// Compute the contour thresholds.
152+
const T = maybeTicks(thresholds, V, ...finiteExtent(VV));
153+
if (T === null) throw new Error(`unsupported thresholds: ${thresholds}`);
162154

163155
// Compute the (maybe faceted) contours.
164-
const contour = contours().thresholds(T).size([w, h]).smooth(this.smooth);
156+
const {contour} = contours().size([w, h]).smooth(this.smooth);
165157
const contourData = [];
166158
const contourFacets = [];
167-
for (const V of VV) contourFacets.push(range(contourData.length, contourData.push(...contour(V))));
159+
for (const V of VV) contourFacets.push(range(contourData.length, contourData.push(...T.map((t) => contour(V, t)))));
168160

169161
// Rescale the contour multipolygon from grid to screen coordinates.
170162
for (const {coordinates} of contourData) {
@@ -187,6 +179,22 @@ function contourGeometry({thresholds, interval, ...options}) {
187179
});
188180
}
189181

182+
// Apply the thresholds interval, function, or count, and return an array of
183+
// ticks. d3-contour unlike d3-array doesn’t pass the min and max automatically,
184+
// so we do that here to normalize, and also so we can share consistent
185+
// thresholds across facets. When an interval is used, note that the lowest
186+
// threshold should be below (or equal) to the lowest value, or else some data
187+
// will be missing.
188+
function maybeTicks(thresholds, V, min, max) {
189+
if (typeof thresholds?.range === "function") return thresholds.range(thresholds.floor(min), max);
190+
if (typeof thresholds === "function") thresholds = thresholds(V, min, max);
191+
if (typeof thresholds !== "number") return arrayify(thresholds, Array);
192+
const tz = ticks(...nice(min, max, thresholds), thresholds);
193+
while (tz[tz.length - 1] >= max) tz.pop();
194+
while (tz[1] < min) tz.shift();
195+
return tz;
196+
}
197+
190198
export function contour() {
191199
return new Contour(...maybeTuples("value", ...arguments));
192200
}

0 commit comments

Comments
 (0)