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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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: {