From 4a48828da759fd70b664f1b5a5e6aa7b84c41ffc Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 22 Jun 2023 11:35:53 -0700 Subject: [PATCH] easier piecewise range --- src/scales/quantitative.js | 28 +++--- ...e.html => colorPiecewiseLinearDomain.html} | 43 ++++----- .../colorPiecewiseLinearDomainReverse.html | 93 +++++++++++++++++++ ...in.html => colorPiecewiseLinearRange.html} | 23 +++-- ...html => colorPiecewiseLinearRangeHcl.html} | 23 +++-- ... => colorPiecewiseLinearRangeReverse.html} | 35 ++++--- ...color-misaligned.ts => color-piecewise.ts} | 20 ++-- test/plots/index.ts | 3 +- test/plots/warn-misaligned-diverging.ts | 8 ++ test/scales/scales-test.js | 90 +++++++++--------- 10 files changed, 227 insertions(+), 139 deletions(-) rename test/output/{warnMisalignedLinearRangeReverse.html => colorPiecewiseLinearDomain.html} (70%) create mode 100644 test/output/colorPiecewiseLinearDomainReverse.html rename test/output/{warnMisalignedLinearDomain.html => colorPiecewiseLinearRange.html} (80%) rename test/output/{warnMisalignedLinearRange.html => colorPiecewiseLinearRangeHcl.html} (78%) rename test/output/{warnMisalignedLinearDomainReverse.html => colorPiecewiseLinearRangeReverse.html} (80%) rename test/plots/{color-misaligned.ts => color-piecewise.ts} (64%) create mode 100644 test/plots/warn-misaligned-diverging.ts diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index dbbc4a6d75..71e031387f 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -10,6 +10,7 @@ import { max, median, min, + piecewise, quantile, quantize, reverse as reverseof, @@ -24,7 +25,6 @@ import { } from "d3"; 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"; @@ -81,30 +81,24 @@ export function createScaleQ( ) { interval = maybeRangeInterval(interval, type); if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes + if (typeof interpolate !== "function") interpolate = maybeInterpolator(interpolate); // named interpolator 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 an explicit range is specified, and it has a different length than the + // domain, then redistribute the range using a piecewise interpolator. 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.`); + if (n !== m) { + if (interpolate.length === 1) throw new Error("invalid piecewise interpolator"); // e.g., turbo + interpolate = piecewise(interpolate, range); + range = undefined; } } - // 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 - // function is a “fixed” interpolator on the [0, 1] interval, as when a - // color scheme such as interpolateRdBu is used. - if (typeof interpolate !== "function") { - interpolate = maybeInterpolator(interpolate); - } + // Disambiguate between a two-argument interpolator that is used in + // conjunction with the range, and a one-argument “fixed” interpolator on the + // [0, 1] interval as with the RdBu color scheme. if (interpolate.length === 1) { if (reverse) { interpolate = flip(interpolate); diff --git a/test/output/warnMisalignedLinearRangeReverse.html b/test/output/colorPiecewiseLinearDomain.html similarity index 70% rename from test/output/warnMisalignedLinearRangeReverse.html rename to test/output/colorPiecewiseLinearDomain.html index b4308ac587..ec82379196 100644 --- a/test/output/warnMisalignedLinearRangeReverse.html +++ b/test/output/colorPiecewiseLinearDomain.html @@ -17,27 +17,23 @@ - 10 - - - - 8 + 0 - + - 6 + 5 - + - 4 + 10 - + - 2 + 15 - 0 + 20 @@ -82,17 +78,16 @@ 10 - - - - - - - - - - - + + + + + + + + + + + - ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/colorPiecewiseLinearDomainReverse.html b/test/output/colorPiecewiseLinearDomainReverse.html new file mode 100644 index 0000000000..45a4f248db --- /dev/null +++ b/test/output/colorPiecewiseLinearDomainReverse.html @@ -0,0 +1,93 @@ +
+ + + + + + 0 + + + + 5 + + + + 10 + + + + 15 + + + + 20 + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/warnMisalignedLinearDomain.html b/test/output/colorPiecewiseLinearRange.html similarity index 80% rename from test/output/warnMisalignedLinearDomain.html rename to test/output/colorPiecewiseLinearRange.html index 5209ae62a4..d18b383ebe 100644 --- a/test/output/warnMisalignedLinearDomain.html +++ b/test/output/colorPiecewiseLinearRange.html @@ -13,7 +13,7 @@ white-space: pre; } - + @@ -83,16 +83,15 @@ - - - - - - - - - - + + + + + + + + + + - ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/warnMisalignedLinearRange.html b/test/output/colorPiecewiseLinearRangeHcl.html similarity index 78% rename from test/output/warnMisalignedLinearRange.html rename to test/output/colorPiecewiseLinearRangeHcl.html index 5209ae62a4..10fdc5a120 100644 --- a/test/output/warnMisalignedLinearRange.html +++ b/test/output/colorPiecewiseLinearRangeHcl.html @@ -13,7 +13,7 @@ white-space: pre; } - + @@ -83,16 +83,15 @@ - - - - - - - - - - + + + + + + + + + + - ⚠️1 warning. Please check the console. \ No newline at end of file diff --git a/test/output/warnMisalignedLinearDomainReverse.html b/test/output/colorPiecewiseLinearRangeReverse.html similarity index 80% rename from test/output/warnMisalignedLinearDomainReverse.html rename to test/output/colorPiecewiseLinearRangeReverse.html index b4308ac587..97a46b7fba 100644 --- a/test/output/warnMisalignedLinearDomainReverse.html +++ b/test/output/colorPiecewiseLinearRangeReverse.html @@ -13,31 +13,31 @@ white-space: pre; } - + - 10 + 0 - 8 + 2 - 6 + 4 - 4 + 6 - 2 + 8 - 0 + 10 @@ -82,17 +82,16 @@ 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-piecewise.ts similarity index 64% rename from test/plots/color-misaligned.ts rename to test/plots/color-piecewise.ts index c3ea3e27c7..bb78efa33f 100644 --- a/test/plots/color-misaligned.ts +++ b/test/plots/color-piecewise.ts @@ -1,31 +1,31 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -export function warnMisalignedDivergingDomain() { - 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 warnMisalignedLinearDomain() { +export function colorPiecewiseLinearDomain() { return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ color: {legend: true, type: "linear", domain: [0, 10, 20], range: ["red", "blue"]} }); } -export function warnMisalignedLinearDomainReverse() { +export function colorPiecewiseLinearDomainReverse() { 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 warnMisalignedLinearRange() { +export function colorPiecewiseLinearRange() { return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ color: {legend: true, type: "linear", domain: [0, 10], range: ["red", "blue", "green"]} }); } -export function warnMisalignedLinearRangeReverse() { +export function colorPiecewiseLinearRangeHcl() { + return Plot.cellX(d3.range(11), {fill: Plot.identity}).plot({ + color: {legend: true, type: "linear", domain: [0, 10], range: ["red", "blue", "green"], interpolate: "hcl"} + }); +} + +export function colorPiecewiseLinearRangeReverse() { 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 fcfc9f075b..5bfd5b464f 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -48,7 +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 "./color-piecewise.js"; export * from "./country-centroids.js"; export * from "./covid-ihme-projected-deaths.js"; export * from "./crimean-war-arrow.js"; @@ -313,6 +313,7 @@ export * from "./walmarts-decades.js"; export * from "./walmarts-density-unprojected.js"; export * from "./walmarts-density.js"; export * from "./walmarts.js"; +export * from "./warn-misaligned-diverging.js"; export * from "./wealth-britain-bar.js"; export * from "./wealth-britain-proportion-plot.js"; export * from "./word-cloud.js"; diff --git a/test/plots/warn-misaligned-diverging.ts b/test/plots/warn-misaligned-diverging.ts new file mode 100644 index 0000000000..903f3c1f4d --- /dev/null +++ b/test/plots/warn-misaligned-diverging.ts @@ -0,0 +1,8 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export function warnMisalignedDivergingDomain() { + return Plot.cellX(d3.range(-5, 6), {x: Plot.identity, fill: Plot.identity}).plot({ + color: {legend: true, type: "diverging", domain: [-5, 5, 10]} + }); +} diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 736ac984c9..2303cfc2ef 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -724,72 +724,72 @@ 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 = assert.warns( - () => - Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ - color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"]} - }), - /domain contains extra/ - ); - scaleEqual(plot.scale("color"), { +it("plot(…).scale('color') distributes an explicit range equally across more domain elements", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"]} + }); + const color = plot.scale("color"); + scaleEqual(color, { type: "linear", - domain: [0, 100], - range: ["red", "blue"], - interpolate: d3.interpolateRgb, + domain: [0, 100, 200], + range: [0, 0.5, 1], + interpolate: color.interpolate, clamp: false }); + assert.strictEqual(color.interpolate(0), "rgb(255, 0, 0)"); + assert.strictEqual(color.interpolate(0.5), "rgb(128, 0, 128)"); + assert.strictEqual(color.interpolate(1), "rgb(0, 0, 255)"); }); -it("plot(…).scale('color') ignores extra range elements with an explicit range", () => { - const plot = assert.warns( - () => - Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ - color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"]} - }), - /range contains extra/ - ); - scaleEqual(plot.scale("color"), { +it("plot(…).scale('color') distributes an explicit range equally across fewer domain elements", () => { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"]} + }); + const color = plot.scale("color"); + scaleEqual(color, { type: "linear", domain: [0, 100], - range: ["red", "blue"], - interpolate: d3.interpolateRgb, + range: [0, 1], + interpolate: color.interpolate, clamp: false }); + assert.strictEqual(color.interpolate(0), "rgb(255, 0, 0)"); + assert.strictEqual(color.interpolate(0.5), "rgb(0, 0, 255)"); + assert.strictEqual(color.interpolate(1), "rgb(0, 128, 0)"); }); it("plot(…).scale('color') ignores extra domain elements with an explicit range when reversed", () => { - const plot = assert.warns( - () => - Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ - color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"], reverse: true} - }), - /domain contains extra/ - ); - scaleEqual(plot.scale("color"), { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100, 200], range: ["red", "blue"], reverse: true} + }); + const color = plot.scale("color"); + scaleEqual(color, { type: "linear", - domain: [100, 0], - range: ["red", "blue"], - interpolate: d3.interpolateRgb, + domain: [0, 100, 200], + range: [0, 0.5, 1], + interpolate: color.interpolate, clamp: false }); + assert.strictEqual(color.interpolate(0), "rgb(0, 0, 255)"); + assert.strictEqual(color.interpolate(0.5), "rgb(128, 0, 128)"); + assert.strictEqual(color.interpolate(1), "rgb(255, 0, 0)"); }); it("plot(…).scale('color') ignores extra range elements with an explicit range when reversed", () => { - const plot = assert.warns( - () => - Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ - color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"], reverse: true} - }), - /range contains extra/ - ); - scaleEqual(plot.scale("color"), { + const plot = Plot.cellX([100, 200, 300, 400], {fill: Plot.identity}).plot({ + color: {type: "linear", domain: [0, 100], range: ["red", "blue", "green"], reverse: true} + }); + const color = plot.scale("color"); + scaleEqual(color, { type: "linear", - domain: [100, 0], - range: ["red", "blue"], - interpolate: d3.interpolateRgb, + domain: [0, 100], + range: [0, 1], + interpolate: color.interpolate, clamp: false }); + assert.strictEqual(color.interpolate(0), "rgb(0, 128, 0)"); + assert.strictEqual(color.interpolate(0.5), "rgb(0, 0, 255)"); + assert.strictEqual(color.interpolate(1), "rgb(255, 0, 0)"); }); it("plot(…).scale('color') can return a polylinear piecewise linear scale with an explicit scheme", () => {