From c4e04097907dafcb82b7b8253a86663546f9ac65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 16 Jan 2023 13:07:45 +0100 Subject: [PATCH 1/4] ensure thresholds consistency across facets --- src/marks/contour.js | 11 +- test/output/functionContourFaceted2.svg | 164 ++++++++++++++++++++++++ test/plots/function-contour.js | 25 ++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 test/output/functionContourFaceted2.svg diff --git a/src/marks/contour.js b/src/marks/contour.js index 337eb2e353..c4253c734d 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, isIterable} from "../options.js"; import {Position} from "../projection.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, styles} from "../style.js"; import {initializer} from "../transforms/basic.js"; @@ -158,7 +158,12 @@ function contourGeometry({thresholds, interval, ...options}) { ? thresholds.range(...(([min, max]) => [thresholds.floor(min), max])(finiteExtent(VV))) : typeof thresholds === "function" ? thresholds(V, ...finiteExtent(VV)) - : thresholds; + : typeof thresholds === "number" + ? ticks(...nice(...finiteExtent(VV), thresholds), thresholds) + : isIterable(thresholds) + ? [...thresholds] + : null; + 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); diff --git a/test/output/functionContourFaceted2.svg b/test/output/functionContourFaceted2.svg new file mode 100644 index 0000000000..ef04dfd536 --- /dev/null +++ b/test/output/functionContourFaceted2.svg @@ -0,0 +1,164 @@ + + + + + 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() + ] + }); +} From a602c4213141d50c268157d3678a42d3b5a85a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 16 Jan 2023 22:22:49 +0100 Subject: [PATCH 2/4] arrayify Co-authored-by: Mike Bostock --- src/marks/contour.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/marks/contour.js b/src/marks/contour.js index c4253c734d..8f503ed8cc 100644 --- a/src/marks/contour.js +++ b/src/marks/contour.js @@ -160,9 +160,7 @@ function contourGeometry({thresholds, interval, ...options}) { ? thresholds(V, ...finiteExtent(VV)) : typeof thresholds === "number" ? ticks(...nice(...finiteExtent(VV), thresholds), thresholds) - : isIterable(thresholds) - ? [...thresholds] - : null; + : arrayify(thresholds, Array); if (T === null) throw new Error(`Unsupported thresholds: ${thresholds}`); // Compute the (maybe faceted) contours. From 67b6e4fbdcd0f6a66d28990e81dca35dec43d825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 16 Jan 2023 23:14:01 +0100 Subject: [PATCH 3/4] nice ticks; use contour --- src/marks/contour.js | 24 +++++++++++++++++++----- test/output/functionContourFaceted2.svg | 4 ---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/marks/contour.js b/src/marks/contour.js index 8f503ed8cc..0cfcfa7567 100644 --- a/src/marks/contour.js +++ b/src/marks/contour.js @@ -1,7 +1,7 @@ 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, isIterable} 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"; @@ -157,17 +157,17 @@ function contourGeometry({thresholds, interval, ...options}) { typeof thresholds?.range === "function" ? thresholds.range(...(([min, max]) => [thresholds.floor(min), max])(finiteExtent(VV))) : typeof thresholds === "function" - ? thresholds(V, ...finiteExtent(VV)) + ? maybeNiceTicks(thresholds, finiteExtent(VV), V) : typeof thresholds === "number" - ? ticks(...nice(...finiteExtent(VV), thresholds), thresholds) + ? niceTicks(thresholds, finiteExtent(VV)) : arrayify(thresholds, Array); 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) { @@ -190,6 +190,20 @@ function contourGeometry({thresholds, interval, ...options}) { }); } +// Convert number of thresholds into uniform thresholds. +function niceTicks(thresholds, e) { + const tz = ticks(...nice(...e, thresholds), thresholds); + while (tz[tz.length - 1] >= e[1]) tz.pop(); + while (tz[1] < e[0]) tz.shift(); + return tz; +} + +// Apply the thresholds function and return an array of ticks. +function maybeNiceTicks(thresholds, e, V) { + thresholds = thresholds(V, ...e); + return Array.isArray(thresholds) ? thresholds : niceTicks(thresholds, e); +} + export function contour() { return new Contour(...maybeTuples("value", ...arguments)); } diff --git a/test/output/functionContourFaceted2.svg b/test/output/functionContourFaceted2.svg index ef04dfd536..722b661a44 100644 --- a/test/output/functionContourFaceted2.svg +++ b/test/output/functionContourFaceted2.svg @@ -109,7 +109,6 @@ - @@ -125,7 +124,6 @@ - @@ -141,7 +139,6 @@ - @@ -157,7 +154,6 @@ - From 3aa3848e23ad73fd0122dcf2c5bbc381346f7329 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 17 Jan 2023 09:53:15 -0800 Subject: [PATCH 4/4] polish --- src/marks/contour.js | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/marks/contour.js b/src/marks/contour.js index 0cfcfa7567..4b0e93c653 100644 --- a/src/marks/contour.js +++ b/src/marks/contour.js @@ -148,20 +148,9 @@ 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" - ? maybeNiceTicks(thresholds, finiteExtent(VV), V) - : typeof thresholds === "number" - ? niceTicks(thresholds, finiteExtent(VV)) - : arrayify(thresholds, Array); - if (T === null) throw new Error(`Unsupported thresholds: ${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().size([w, h]).smooth(this.smooth); @@ -190,20 +179,22 @@ function contourGeometry({thresholds, interval, ...options}) { }); } -// Convert number of thresholds into uniform thresholds. -function niceTicks(thresholds, e) { - const tz = ticks(...nice(...e, thresholds), thresholds); - while (tz[tz.length - 1] >= e[1]) tz.pop(); - while (tz[1] < e[0]) tz.shift(); +// 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; } -// Apply the thresholds function and return an array of ticks. -function maybeNiceTicks(thresholds, e, V) { - thresholds = thresholds(V, ...e); - return Array.isArray(thresholds) ? thresholds : niceTicks(thresholds, e); -} - export function contour() { return new Contour(...maybeTuples("value", ...arguments)); }