From 4a83dd9982c5ea2e2629e668e2f6f3cc0346a3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 17 Jan 2023 18:56:03 +0100 Subject: [PATCH] ensure thresholds consistency across facets (#1226) * ensure thresholds consistency across facets * arrayify Co-authored-by: Mike Bostock * nice ticks; use contour * polish Co-authored-by: Mike Bostock --- src/marks/contour.js | 38 +++--- test/output/functionContourFaceted2.svg | 160 ++++++++++++++++++++++++ test/plots/function-contour.js | 25 ++++ 3 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 test/output/functionContourFaceted2.svg diff --git a/src/marks/contour.js b/src/marks/contour.js index 337eb2e353..4b0e93c653 100644 --- a/src/marks/contour.js +++ b/src/marks/contour.js @@ -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"; @@ -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) { @@ -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)); } diff --git a/test/output/functionContourFaceted2.svg b/test/output/functionContourFaceted2.svg new file mode 100644 index 0000000000..722b661a44 --- /dev/null +++ b/test/output/functionContourFaceted2.svg @@ -0,0 +1,160 @@ + + + + + cos + + + lin + + + + + lin + + + sin + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + + + 12 + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + + + 12 + + + + + 0 + + + 5 + + + 10 + + + + + 0 + + + 5 + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/function-contour.js b/test/plots/function-contour.js index 14fc1f3e7e..6724f5ad70 100644 --- a/test/plots/function-contour.js +++ b/test/plots/function-contour.js @@ -42,3 +42,28 @@ export async function functionContourFaceted() { ] }); } + +export async function functionContourFaceted2() { + function lin(x) { + return x / (4 * Math.PI); + } + return Plot.plot({ + height: 580, + color: {type: "diverging"}, + fx: {tickFormat: (f) => f?.name}, + fy: {tickFormat: (f) => f?.name}, + marks: [ + Plot.contour({ + fill: (x, y, {fx, fy}) => fx(x) * fy(y), + fx: [Math.sin, Math.sin, lin, lin], + fy: [Math.cos, lin, lin, Math.cos], + x1: 0, + y1: 0, + x2: 4 * Math.PI, + y2: 4 * Math.PI, + thresholds: 10 + }), + Plot.frame() + ] + }); +}