Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/scales/quantitative.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ascending,
descending,
extent,
interpolateHcl,
interpolateHsl,
Expand All @@ -20,13 +20,13 @@ import {
scaleQuantile,
scaleSymlog,
scaleThreshold,
scaleIdentity
scaleIdentity,
ticks
} from "d3";
import {positive, negative, finite} from "../defined.js";
import {constant, order} from "../options.js";
import {arrayify, constant, order} from "../options.js";
import {ordinalRange, quantitativeScheme} from "./schemes.js";
import {registry, radius, opacity, color, length} from "./index.js";
import {ticks} from "d3";

export const flip = i => t => i(1 - t);
const unit = [0, 1];
Expand Down Expand Up @@ -151,10 +151,11 @@ export function ScaleQuantize(key, channels, {
range,
reverse
}) {
domain = extent(domain); // TODO preserve descending domains?
const thresholds = ticks(...domain, n);
if (thresholds[0] <= domain[0]) thresholds.splice(0, 1); // drop exact lower bound
if (thresholds[thresholds.length - 1] >= domain[1]) thresholds.pop(); // drop exact upper bound
const [min, max] = extent(domain);
const thresholds = ticks(min, max, n);
if (order(arrayify(domain)) < 0) thresholds.reverse(); // preserve descending domain
if (thresholds[0] <= min) thresholds.splice(0, 1); // drop exact lower bound
if (thresholds[thresholds.length - 1] >= max) thresholds.pop(); // drop exact upper bound
n = thresholds.length + 1;
if (range === undefined) range = interpolate !== undefined ? quantize(interpolate, n) : registry.get(key) === color ? ordinalRange(scheme, n) : undefined;
return ScaleThreshold(key, channels, {domain: thresholds, range, reverse});
Expand All @@ -168,9 +169,20 @@ export function ScaleThreshold(key, channels, {
range = interpolate !== undefined ? quantize(interpolate, domain.length + 1) : registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined,
reverse
}) {
if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error(`the ${key} scale has a non-ascending domain`);
const sign = order(arrayify(domain)); // preserve descending domain
if (!pairs(domain).every(([a, b]) => isOrdered(a, b, sign))) throw new Error(`the ${key} scale has a non-monotonic domain`);
if (reverse) range = reverseof(range); // domain ascending, so reverse range
return {type: "threshold", scale: scaleThreshold(domain, range === undefined ? [] : range).unknown(unknown), domain, range};
return {
type: "threshold",
scale: scaleThreshold(sign < 0 ? reverseof(domain) : domain, range === undefined ? [] : range).unknown(unknown),
domain,
range
};
}

function isOrdered(a, b, sign) {
const s = descending(a, b);
return s === 0 || s === sign;
}

export function ScaleIdentity() {
Expand Down
49 changes: 49 additions & 0 deletions test/output/colorLegendQuantize.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions test/output/colorLegendQuantizeDescending.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions test/output/colorLegendQuantizeDescendingReversed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions test/output/colorLegendQuantizeReverse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions test/plots/legend-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,50 @@ export function colorLegendQuantileImplicit() {
}).legend("color");
}

// Quantize scales are implicitly converted to a threshold scale
export function colorLegendQuantize() {
return Plot.legend({
color: {
type: "quantize",
n: 7,
domain: d3.range(1, 120).map(i => i ** 2 / 100),
label: "quantize scale"
}
});
}

export function colorLegendQuantizeDescending() {
return Plot.legend({
color: {
type: "quantize",
domain: d3.range(1, 220).reverse().map(i => i ** 2 / 100),
label: "quantize descending"
}
});
}

export function colorLegendQuantizeDescendingReversed() {
return Plot.legend({
color: {
type: "quantize",
reverse: true,
domain: d3.range(1, 11).reverse().map(i => i ** 2 / 10),
label: "quantize descending reversed"
}
});
}

export function colorLegendQuantizeReverse() {
return Plot.legend({
color: {
type: "quantize",
reverse: true,
domain: d3.range(1, 120).map(i => i ** 2 / 100 - 50),
label: "quantize reversed"
}
});
}

export function colorLegendImplicitLabel() {
return Plot.plot({
color: {scheme: "viridis"},
Expand Down
22 changes: 22 additions & 0 deletions test/scales/scales-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,28 @@ it("plot(…).scale('color') can promote a reversed quantized scale to a thresho
});
});

it("plot(…).scale('color') can promote a descending quantized scale to a threshold scale", async () => {
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
const plot = Plot.dot(penguins, {x: "body_mass_g", fill: "body_mass_g"}).plot({color: {domain: [6500, 2500], type: "quantize"}});
scaleEqual(plot.scale("color"), {
type: "threshold",
domain: [6000, 5000, 4000, 3000],
range: d3.schemeRdYlBu[5],
label: "body_mass_g"
});
});

it("plot(…).scale('color') can promote a reverse and descending quantized scale to a threshold scale", async () => {
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
const plot = Plot.dot(penguins, {x: "body_mass_g", fill: "body_mass_g"}).plot({color: {domain: [6500, 2500], type: "quantize", reverse: true}});
scaleEqual(plot.scale("color"), {
type: "threshold",
domain: [6000, 5000, 4000, 3000],
range: d3.reverse(d3.schemeRdYlBu[5]),
label: "body_mass_g"
});
});

it("plot(…).scale('color') promotes a cyclical scale to a linear scale", () => {
const plot = Plot.dot([1, 2, 3, 4, 5], {y: d => d, fill: d => d}).plot({color: {type: "cyclical"}});
scaleEqual(plot.scale("color"), {
Expand Down