From 0b84095d711bce8959bae51d4aa403aca17fa8ba Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 24 May 2023 16:22:57 -0700 Subject: [PATCH 1/2] fix misaligned range --- src/scales/quantitative.js | 12 ++- .../colorMisalignedDivergingDomain.html | 93 ++++++++++++++++++ test/output/colorMisalignedLinearDomain.html | 97 +++++++++++++++++++ .../colorMisalignedLinearDomainReverse.html | 97 +++++++++++++++++++ test/output/colorMisalignedLinearRange.html | 97 +++++++++++++++++++ .../colorMisalignedLinearRangeReverse.html | 97 +++++++++++++++++++ test/plots/color-misaligned.ts | 32 ++++++ test/plots/index.ts | 1 + test/scales/scales-test.js | 66 +++++++++++++ 9 files changed, 590 insertions(+), 2 deletions(-) create mode 100644 test/output/colorMisalignedDivergingDomain.html create mode 100644 test/output/colorMisalignedLinearDomain.html create mode 100644 test/output/colorMisalignedLinearDomainReverse.html create mode 100644 test/output/colorMisalignedLinearRange.html create mode 100644 test/output/colorMisalignedLinearRangeReverse.html create mode 100644 test/plots/color-misaligned.ts diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 47188f86c0..9acc0f0dfa 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -82,6 +82,15 @@ export function createScaleQ( if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes reverse = !!reverse; + // If an explicit range is specified, ensure that the domain and range have + // the same length; truncate to whichever one is shorter. + if (range !== undefined) { + const n = (domain = arrayify(domain)).length; + const m = (range = arrayify(range)).length; + if (n > m) domain = domain.slice(0, m); + else if (m > n) range = range.slice(0, n); + } + // Sometimes interpolate is a named interpolator, such as "lab" for Lab color // space. Other times interpolate is a function that takes two arguments and // is used in conjunction with the range. And other times the interpolate @@ -113,8 +122,7 @@ export function createScaleQ( const [min, max] = extent(domain); if (min > 0 || max < 0) { domain = slice(domain); - if (orderof(domain) !== Math.sign(min)) domain[domain.length - 1] = 0; - // [2, 1] or [-2, -1] + if (orderof(domain) !== Math.sign(min)) domain[domain.length - 1] = 0; // [2, 1] or [-2, -1] else domain[0] = 0; // [1, 2] or [-1, -2] } } diff --git a/test/output/colorMisalignedDivergingDomain.html b/test/output/colorMisalignedDivergingDomain.html new file mode 100644 index 0000000000..d7fd524a5a --- /dev/null +++ b/test/output/colorMisalignedDivergingDomain.html @@ -0,0 +1,93 @@ +
+ + + + + + −4 + + + + −2 + + + + 0 + + + + 2 + + + + 4 + + + + + + + + + + + + + + + + + + + -5 + -4 + -3 + -2 + -1 + 0 + 1 + 2 + 3 + 4 + 5 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearDomain.html b/test/output/colorMisalignedLinearDomain.html new file mode 100644 index 0000000000..a7c1e5e732 --- /dev/null +++ b/test/output/colorMisalignedLinearDomain.html @@ -0,0 +1,97 @@ +
+ + + + + + 0 + + + + 2 + + + + 4 + + + + 6 + + + + 8 + + + + 10 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearDomainReverse.html b/test/output/colorMisalignedLinearDomainReverse.html new file mode 100644 index 0000000000..92dcff82b9 --- /dev/null +++ b/test/output/colorMisalignedLinearDomainReverse.html @@ -0,0 +1,97 @@ +
+ + + + + + 10 + + + + 8 + + + + 6 + + + + 4 + + + + 2 + + + + 0 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearRange.html b/test/output/colorMisalignedLinearRange.html new file mode 100644 index 0000000000..a7c1e5e732 --- /dev/null +++ b/test/output/colorMisalignedLinearRange.html @@ -0,0 +1,97 @@ +
+ + + + + + 0 + + + + 2 + + + + 4 + + + + 6 + + + + 8 + + + + 10 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearRangeReverse.html b/test/output/colorMisalignedLinearRangeReverse.html new file mode 100644 index 0000000000..92dcff82b9 --- /dev/null +++ b/test/output/colorMisalignedLinearRangeReverse.html @@ -0,0 +1,97 @@ +
+ + + + + + 10 + + + + 8 + + + + 6 + + + + 4 + + + + 2 + + + + 0 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/plots/color-misaligned.ts b/test/plots/color-misaligned.ts new file mode 100644 index 0000000000..765c974e60 --- /dev/null +++ b/test/plots/color-misaligned.ts @@ -0,0 +1,32 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export function colorMisalignedDivergingDomain() { + return Plot.cellX(d3.range(-5, 6), {x: Plot.identity, fill: Plot.identity}).plot({ + color: {legend: true, type: "diverging", domain: [-5, 5, 10]} + }); +} + +export function colorMisalignedLinearDomain() { + return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ + color: {legend: true, type: "linear", domain: [0, 10, 20], range: ["red", "blue"]} + }); +} + +export function colorMisalignedLinearDomainReverse() { + return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ + color: {legend: true, type: "linear", domain: [0, 10, 20], reverse: true, range: ["red", "blue"]} + }); +} + +export function colorMisalignedLinearRange() { + return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ + color: {legend: true, type: "linear", domain: [0, 10], range: ["red", "blue", "green"]} + }); +} + +export function colorMisalignedLinearRangeReverse() { + return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ + color: {legend: true, type: "linear", domain: [0, 10], reverse: true, range: ["red", "blue", "green"]} + }); +} diff --git a/test/plots/index.ts b/test/plots/index.ts index 0178aa9eee..b6886b388d 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -48,6 +48,7 @@ export * from "./cars-parcoords.js"; export * from "./channel-domain.js"; export * from "./clamp.js"; export * from "./collapsed-histogram.js"; +export * from "./color-misaligned.js"; export * from "./country-centroids.js"; export * from "./covid-ihme-projected-deaths.js"; export * from "./crimean-war-arrow.js"; diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 85b4ee8e97..662d8ef29f 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -410,6 +410,20 @@ it("plot(…).scale(name) handles a reversed diverging scale with a descending d }); }); +it("plot(…).scale(name) ignores extra domain elements with a diverging scale", async () => { + const plot = Plot.plot({color: {type: "diverging", domain: [-5, 5, 10]}}); + const {interpolate, ...color} = plot.scale("color"); + scaleEqual(color, { + type: "diverging", + symmetric: false, + domain: [-5, 5], + pivot: 0, + clamp: false + }); + const expected = d3.scaleDiverging([-5, 0, 5], d3.interpolateRdBu); + for (const t of d3.range(-5, 6)) assert.strictEqual(color.apply(t), expected(t), t); +}); + it("plot(…).scale(name) promotes the given zero option to the domain", async () => { const penguins = await d3.csv("data/penguins.csv", d3.autoType); const plot = Plot.dotX(penguins, {x: "body_mass_g"}).plot({x: {zero: true}}); @@ -707,6 +721,58 @@ it("plot(…).scale('color') can return a “polylinear” piecewise linear scal }); }); +it("plot(…).scale('color') ignores extra domain elements with an explicit range", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"]} + }); + scaleEqual(plot.scale("color"), { + type: "linear", + domain: [0, 100], + range: ["red", "blue"], + interpolate: d3.interpolateRgb, + clamp: false + }); +}); + +it("plot(…).scale('color') ignores extra range elements with an explicit range", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"]} + }); + scaleEqual(plot.scale("color"), { + type: "linear", + domain: [0, 100], + range: ["red", "blue"], + interpolate: d3.interpolateRgb, + clamp: false + }); +}); + +it("plot(…).scale('color') ignores extra domain elements with an explicit range when reversed", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"], reverse: true} + }); + scaleEqual(plot.scale("color"), { + type: "linear", + domain: [100, 0], + range: ["red", "blue"], + interpolate: d3.interpolateRgb, + clamp: false + }); +}); + +it("plot(…).scale('color') ignores extra range elements with an explicit range when reversed", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"], reverse: true} + }); + scaleEqual(plot.scale("color"), { + type: "linear", + domain: [100, 0], + range: ["red", "blue"], + interpolate: d3.interpolateRgb, + clamp: false + }); +}); + it("plot(…).scale('color') can return a polylinear piecewise linear scale with an explicit scheme", () => { const plot = Plot.ruleX([100, 200, 300, 400], {stroke: (d) => d}).plot({ color: { From cc47ce563c369d92c7f83815e56a2916a7cb6aa9 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 May 2023 09:00:33 -0700 Subject: [PATCH 2/2] warn on extra elements --- src/scales/diverging.js | 11 +++++++--- src/scales/quantitative.js | 20 ++++++++++++------- .../colorMisalignedDivergingDomain.html | 1 + test/output/colorMisalignedLinearDomain.html | 1 + .../colorMisalignedLinearDomainReverse.html | 1 + test/output/colorMisalignedLinearRange.html | 1 + .../colorMisalignedLinearRangeReverse.html | 1 + 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/scales/diverging.js b/src/scales/diverging.js index 83c40c8606..c2ae3028b7 100644 --- a/src/scales/diverging.js +++ b/src/scales/diverging.js @@ -8,10 +8,12 @@ import { scaleDivergingPow, scaleDivergingSymlog } from "d3"; -import {positive, negative} from "../defined.js"; +import {negative, positive} from "../defined.js"; +import {arrayify} from "../options.js"; +import {warn} from "../warnings.js"; +import {color, registry} from "./index.js"; +import {flip, inferDomain, interpolatePiecewise, maybeInterpolator} from "./quantitative.js"; import {quantitativeScheme} from "./schemes.js"; -import {registry, color} from "./index.js"; -import {inferDomain, maybeInterpolator, flip, interpolatePiecewise} from "./quantitative.js"; function createScaleD( key, @@ -37,7 +39,10 @@ function createScaleD( } ) { pivot = +pivot; + domain = arrayify(domain); let [min, max] = domain; + if (domain.length > 2) warn(`Warning: the diverging ${key} scale domain contains extra elements.`); + if (descending(min, max) < 0) ([min, max] = [max, min]), (reverse = !reverse); min = Math.min(min, pivot); max = Math.max(max, pivot); diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 9acc0f0dfa..dbbc4a6d75 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -7,25 +7,26 @@ import { interpolateNumber, interpolateRgb, interpolateRound, - min, max, median, + min, quantile, quantize, reverse as reverseof, + scaleIdentity, scaleLinear, scaleLog, scalePow, scaleQuantile, scaleSymlog, scaleThreshold, - scaleIdentity, ticks } from "d3"; -import {positive, negative, finite} from "../defined.js"; -import {arrayify, constant, orderof, slice, maybeNiceInterval, maybeRangeInterval} from "../options.js"; +import {finite, negative, positive} from "../defined.js"; +import {arrayify, constant, maybeNiceInterval, maybeRangeInterval, orderof, slice} from "../options.js"; +import {warn} from "../warnings.js"; +import {color, length, opacity, radius, registry} from "./index.js"; import {ordinalRange, quantitativeScheme} from "./schemes.js"; -import {registry, radius, opacity, color, length} from "./index.js"; export const flip = (i) => (t) => i(1 - t); const unit = [0, 1]; @@ -87,8 +88,13 @@ export function createScaleQ( if (range !== undefined) { const n = (domain = arrayify(domain)).length; const m = (range = arrayify(range)).length; - if (n > m) domain = domain.slice(0, m); - else if (m > n) range = range.slice(0, n); + if (n > m) { + domain = domain.slice(0, m); + warn(`Warning: the ${key} scale domain contains extra elements.`); + } else if (m > n) { + range = range.slice(0, n); + warn(`Warning: the ${key} scale range contains extra elements.`); + } } // Sometimes interpolate is a named interpolator, such as "lab" for Lab color diff --git a/test/output/colorMisalignedDivergingDomain.html b/test/output/colorMisalignedDivergingDomain.html index d7fd524a5a..00123dc7a9 100644 --- a/test/output/colorMisalignedDivergingDomain.html +++ b/test/output/colorMisalignedDivergingDomain.html @@ -90,4 +90,5 @@ + ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/colorMisalignedLinearDomain.html b/test/output/colorMisalignedLinearDomain.html index a7c1e5e732..5209ae62a4 100644 --- a/test/output/colorMisalignedLinearDomain.html +++ b/test/output/colorMisalignedLinearDomain.html @@ -94,4 +94,5 @@ + ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/colorMisalignedLinearDomainReverse.html b/test/output/colorMisalignedLinearDomainReverse.html index 92dcff82b9..b4308ac587 100644 --- a/test/output/colorMisalignedLinearDomainReverse.html +++ b/test/output/colorMisalignedLinearDomainReverse.html @@ -94,4 +94,5 @@ + ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/colorMisalignedLinearRange.html b/test/output/colorMisalignedLinearRange.html index a7c1e5e732..5209ae62a4 100644 --- a/test/output/colorMisalignedLinearRange.html +++ b/test/output/colorMisalignedLinearRange.html @@ -94,4 +94,5 @@ + ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/colorMisalignedLinearRangeReverse.html b/test/output/colorMisalignedLinearRangeReverse.html index 92dcff82b9..b4308ac587 100644 --- a/test/output/colorMisalignedLinearRangeReverse.html +++ b/test/output/colorMisalignedLinearRangeReverse.html @@ -94,4 +94,5 @@ + ⚠️1 warning. Please check the console. \ No newline at end of file