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 47188f86c0..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]; @@ -82,6 +83,20 @@ 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); + 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 // 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 +128,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..00123dc7a9 --- /dev/null +++ b/test/output/colorMisalignedDivergingDomain.html @@ -0,0 +1,94 @@ +
+ + + + + + −4 + + + + −2 + + + + 0 + + + + 2 + + + + 4 + + + + + + + + + + + + + + + + + + + -5 + -4 + -3 + -2 + -1 + 0 + 1 + 2 + 3 + 4 + 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 new file mode 100644 index 0000000000..5209ae62a4 --- /dev/null +++ b/test/output/colorMisalignedLinearDomain.html @@ -0,0 +1,98 @@ +
+ + + + + + 0 + + + + 2 + + + + 4 + + + + 6 + + + + 8 + + + + 10 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + + ⚠️1 warning. Please check the console. +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearDomainReverse.html b/test/output/colorMisalignedLinearDomainReverse.html new file mode 100644 index 0000000000..b4308ac587 --- /dev/null +++ b/test/output/colorMisalignedLinearDomainReverse.html @@ -0,0 +1,98 @@ +
+ + + + + + 10 + + + + 8 + + + + 6 + + + + 4 + + + + 2 + + + + 0 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + + ⚠️1 warning. Please check the console. +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearRange.html b/test/output/colorMisalignedLinearRange.html new file mode 100644 index 0000000000..5209ae62a4 --- /dev/null +++ b/test/output/colorMisalignedLinearRange.html @@ -0,0 +1,98 @@ +
+ + + + + + 0 + + + + 2 + + + + 4 + + + + 6 + + + + 8 + + + + 10 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + + ⚠️1 warning. Please check the console. +
\ No newline at end of file diff --git a/test/output/colorMisalignedLinearRangeReverse.html b/test/output/colorMisalignedLinearRangeReverse.html new file mode 100644 index 0000000000..b4308ac587 --- /dev/null +++ b/test/output/colorMisalignedLinearRangeReverse.html @@ -0,0 +1,98 @@ +
+ + + + + + 10 + + + + 8 + + + + 6 + + + + 4 + + + + 2 + + + + 0 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + + ⚠️1 warning. Please check the console. +
\ 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: {