Skip to content

Commit

Permalink
ensure thresholds consistency across facets (observablehq#1226)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
2 people authored and chaichontat committed Jan 14, 2024
1 parent 721df8e commit 4a83dd9
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 15 deletions.
38 changes: 23 additions & 15 deletions src/marks/contour.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {blur2, contours, geoPath, map, max, min, range, thresholdSturges} from "d3";
import {blur2, contours, geoPath, map, max, min, nice, range, ticks, thresholdSturges} from "d3";
import {Channels} from "../channel.js";
import {create} from "../context.js";
import {labelof, identity} from "../options.js";
import {labelof, identity, arrayify} from "../options.js";
import {Position} from "../projection.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, styles} from "../style.js";
import {initializer} from "../transforms/basic.js";
Expand Down Expand Up @@ -148,23 +148,15 @@ function contourGeometry({thresholds, interval, ...options}) {
// 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 contour thresholds.
const T = maybeTicks(thresholds, V, ...finiteExtent(VV));
if (T === null) throw new Error(`unsupported thresholds: ${thresholds}`);

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

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

// Apply the thresholds interval, function, or count, and return an array of
// ticks. 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.
function maybeTicks(thresholds, V, min, max) {
if (typeof thresholds?.range === "function") return thresholds.range(thresholds.floor(min), max);
if (typeof thresholds === "function") thresholds = thresholds(V, min, max);
if (typeof thresholds !== "number") return arrayify(thresholds, Array);
const tz = ticks(...nice(min, max, thresholds), thresholds);
while (tz[tz.length - 1] >= max) tz.pop();
while (tz[1] < min) tz.shift();
return tz;
}

export function contour() {
return new Contour(...maybeTuples("value", ...arguments));
}
Expand Down
Loading

0 comments on commit 4a83dd9

Please sign in to comment.