From 06688db49ef09537f26ea62d931e4d2578049fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 24 May 2021 22:02:48 +0200 Subject: [PATCH 01/15] quantile and threshold color scales `{type: "threshold", domain: [30, 50], range: ["red", "green", "blue"]}` closes #372 `color: {type: "quantile", scheme: "blues", quantiles: 7}` closes #373 --- README.md | 2 + src/scales.js | 4 +- src/scales/ordinal.js | 183 +---------------------------- src/scales/quantitative.js | 146 +++++++---------------- src/scales/schemes.js | 233 +++++++++++++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 287 deletions(-) create mode 100644 src/scales/schemes.js diff --git a/README.md b/README.md index af78d9a24f..4f5144e800 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,8 @@ The normal scale types — *linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordina * *diverging-pow* - like *pow*, but with a pivot; defaults to the *rdbu* scheme * *diverging-sqrt* - like *sqrt*, but with a pivot; defaults to the *rdbu* scheme * *diverging-symlog* - like *symlog*, but with a pivot; defaults to the *rdbu* scheme +* *threshold* - given a *domain* of *n* = 1 or more values and a *range* of *n* + 1 colors, returns the *i*th color of the *range* for values that are smaller than the *i*th element of the domain. Returns the *n*th color for values above the highest threshold +* *quantile* - given a number of *quantiles*, bins the channels into ordered *quantiles* having roughly the same number of values, then returns a threshold scale based on the bins’ limits Color scales support two additional options: diff --git a/src/scales.js b/src/scales.js index 3c314d3535..7007dc9f49 100644 --- a/src/scales.js +++ b/src/scales.js @@ -1,5 +1,5 @@ import {registry, position, radius, opacity} from "./scales/index.js"; -import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleIdentity} from "./scales/quantitative.js"; +import {ScaleLinear, ScaleSqrt, ScalePow, ScaleQuantile, ScaleLog, ScaleSymlog, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js"; import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/quantitative.js"; import {ScaleTime, ScaleUtc} from "./scales/temporal.js"; import {ScaleOrdinal, ScalePoint, ScaleBand} from "./scales/ordinal.js"; @@ -67,6 +67,8 @@ function Scale(key, channels = [], options = {}) { case "categorical": case "ordinal": return ScaleOrdinal(key, channels, options); case "cyclical": case "sequential": case "linear": return ScaleLinear(key, channels, options); case "sqrt": return ScaleSqrt(key, channels, options); + case "threshold": return ScaleThreshold(key, channels, options); + case "quantile": return ScaleQuantile(key, channels, options); case "pow": return ScalePow(key, channels, options); case "log": return ScaleLog(key, channels, options); case "symlog": return ScaleSymlog(key, channels, options); diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index f1432791dd..654a3bf69d 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -1,184 +1,9 @@ import {InternSet, reverse as reverseof, sort} from "d3"; -import {quantize} from "d3"; import {scaleBand, scaleOrdinal, scalePoint} from "d3"; -import { - interpolateBlues, - interpolateBrBG, - interpolateBuGn, - interpolateBuPu, - interpolateGnBu, - interpolateGreens, - interpolateGreys, - interpolateOranges, - interpolateOrRd, - interpolatePiYG, - interpolatePRGn, - interpolatePuBu, - interpolatePuBuGn, - interpolatePuOr, - interpolatePuRd, - interpolatePurples, - interpolateRdBu, - interpolateRdGy, - interpolateRdPu, - interpolateRdYlBu, - interpolateRdYlGn, - interpolateReds, - interpolateSpectral, - interpolateYlGn, - interpolateYlGnBu, - interpolateYlOrBr, - interpolateYlOrRd, - interpolateTurbo, - interpolateViridis, - interpolateMagma, - interpolateInferno, - interpolatePlasma, - interpolateCividis, - interpolateCubehelixDefault, - interpolateWarm, - interpolateCool, - interpolateRainbow, - interpolateSinebow, - schemeAccent, - schemeBlues, - schemeBrBG, - schemeBuGn, - schemeBuPu, - schemeCategory10, - schemeDark2, - schemeGnBu, - schemeGreens, - schemeGreys, - schemeOranges, - schemeOrRd, - schemePaired, - schemePastel1, - schemePastel2, - schemePiYG, - schemePRGn, - schemePuBu, - schemePuBuGn, - schemePuOr, - schemePuRd, - schemePurples, - schemeRdBu, - schemeRdGy, - schemeRdPu, - schemeRdYlBu, - schemeRdYlGn, - schemeReds, - schemeSet1, - schemeSet2, - schemeSet3, - schemeSpectral, - schemeTableau10, - schemeYlGn, - schemeYlGnBu, - schemeYlOrBr, - schemeYlOrRd -} from "d3"; +import {ordinalScheme, ordinalSchemes} from "./schemes.js"; import {ascendingDefined} from "../defined.js"; import {registry, color} from "./index.js"; -// TODO Allow this to be extended. -const schemes = new Map([ - // categorical - ["accent", schemeAccent], - ["category10", schemeCategory10], - ["dark2", schemeDark2], - ["paired", schemePaired], - ["pastel1", schemePastel1], - ["pastel2", schemePastel2], - ["set1", schemeSet1], - ["set2", schemeSet2], - ["set3", schemeSet3], - ["tableau10", schemeTableau10], - - // diverging - ["brbg", scheme11(schemeBrBG, interpolateBrBG)], - ["prgn", scheme11(schemePRGn, interpolatePRGn)], - ["piyg", scheme11(schemePiYG, interpolatePiYG)], - ["puor", scheme11(schemePuOr, interpolatePuOr)], - ["rdbu", scheme11(schemeRdBu, interpolateRdBu)], - ["rdgy", scheme11(schemeRdGy, interpolateRdGy)], - ["rdylbu", scheme11(schemeRdYlBu, interpolateRdYlBu)], - ["rdylgn", scheme11(schemeRdYlGn, interpolateRdYlGn)], - ["spectral", scheme11(schemeSpectral, interpolateSpectral)], - - // reversed diverging (for temperature data) - ["burd", scheme11r(schemeRdBu, interpolateRdBu)], - ["buylrd", scheme11r(schemeRdGy, interpolateRdGy)], - - // sequential (single-hue) - ["blues", scheme9(schemeBlues, interpolateBlues)], - ["greens", scheme9(schemeGreens, interpolateGreens)], - ["greys", scheme9(schemeGreys, interpolateGreys)], - ["oranges", scheme9(schemeOranges, interpolateOranges)], - ["purples", scheme9(schemePurples, interpolatePurples)], - ["reds", scheme9(schemeReds, interpolateReds)], - - // sequential (multi-hue) - ["turbo", schemei(interpolateTurbo)], - ["viridis", schemei(interpolateViridis)], - ["magma", schemei(interpolateMagma)], - ["inferno", schemei(interpolateInferno)], - ["plasma", schemei(interpolatePlasma)], - ["cividis", schemei(interpolateCividis)], - ["cubehelix", schemei(interpolateCubehelixDefault)], - ["warm", schemei(interpolateWarm)], - ["cool", schemei(interpolateCool)], - ["bugn", scheme9(schemeBuGn, interpolateBuGn)], - ["bupu", scheme9(schemeBuPu, interpolateBuPu)], - ["gnbu", scheme9(schemeGnBu, interpolateGnBu)], - ["orrd", scheme9(schemeOrRd, interpolateOrRd)], - ["pubu", scheme9(schemePuBu, interpolatePuBu)], - ["pubugn", scheme9(schemePuBuGn, interpolatePuBuGn)], - ["purd", scheme9(schemePuRd, interpolatePuRd)], - ["rdpu", scheme9(schemeRdPu, interpolateRdPu)], - ["ylgn", scheme9(schemeYlGn, interpolateYlGn)], - ["ylgnbu", scheme9(schemeYlGnBu, interpolateYlGnBu)], - ["ylorbr", scheme9(schemeYlOrBr, interpolateYlOrBr)], - ["ylorrd", scheme9(schemeYlOrRd, interpolateYlOrRd)], - - // cyclical - ["rainbow", schemei(interpolateRainbow)], - ["sinebow", schemei(interpolateSinebow)] -]); - -function scheme9(scheme, interpolate) { - return ({length: n}) => { - n = n > 3 ? Math.floor(n) : 3; - return n > 9 ? quantize(interpolate, n) : scheme[n]; - }; -} - -function scheme11(scheme, interpolate) { - return ({length: n}) => { - n = n > 3 ? Math.floor(n) : 3; - return n > 11 ? quantize(interpolate, n) : scheme[n]; - }; -} - -function scheme11r(scheme, interpolate) { - return ({length: n}) => { - n = n > 3 ? Math.floor(n) : 3; - return n > 11 ? quantize(t => interpolate(1 - t), n) : scheme[n].slice().reverse(); - }; -} - -function schemei(interpolate) { - return ({length: n}) => { - return quantize(interpolate, n > 0 ? Math.floor(n) : 0); - }; -} - -function Scheme(scheme) { - const s = (scheme + "").toLowerCase(); - if (!schemes.has(s)) throw new Error(`unknown scheme: ${s}`); - return schemes.get(s); -} - export function ScaleO(scale, channels, { domain = inferDomain(channels), range, @@ -198,9 +23,9 @@ export function ScaleO(scale, channels, { export function ScaleOrdinal(key, channels, { scheme, type, - range = registry.get(key) === color ? (scheme !== undefined ? Scheme(scheme) - : type === "ordinal" ? schemes.get("turbo") - : schemeTableau10) : undefined, + range = registry.get(key) === color ? (scheme !== undefined ? ordinalScheme(scheme) + : ordinalSchemes.get(type === "ordinal" ? "turbo" : "tableau10")) + : undefined, ...options }) { return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options}); diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index e49592e7f9..490b4fe84e 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -1,53 +1,15 @@ import { - min, - max, - quantile, - reverse as reverseof, - piecewise, interpolateHcl, interpolateHsl, interpolateLab, interpolateNumber, interpolateRgb, interpolateRound, - interpolateBlues, - interpolateBrBG, - interpolateBuGn, - interpolateBuPu, - interpolateCividis, - interpolateCool, - interpolateCubehelixDefault, - interpolateGnBu, - interpolateGreens, - interpolateGreys, - interpolateInferno, - interpolateMagma, - interpolateOranges, - interpolateOrRd, - interpolatePiYG, - interpolatePlasma, - interpolatePRGn, - interpolatePuBu, - interpolatePuBuGn, - interpolatePuOr, - interpolatePuRd, - interpolatePurples, - interpolateRainbow, - interpolateRdBu, - interpolateRdGy, - interpolateRdPu, - interpolateRdYlBu, - interpolateRdYlGn, - interpolateReds, - interpolateSinebow, - interpolateSpectral, - interpolateTurbo, - interpolateViridis, - interpolateWarm, - interpolateYlGn, - interpolateYlGnBu, - interpolateYlOrBr, - interpolateYlOrRd, + min, + max, + quantile, + reverse as reverseof, + piecewise, scaleDiverging, scaleDivergingLog, scaleDivergingPow, @@ -57,16 +19,18 @@ import { scaleLog, scalePow, scaleSqrt, + scaleQuantile, scaleSymlog, + scaleThreshold, scaleIdentity } from "d3"; +import {ordinalScheme, quantitativeScheme, quantitativeSchemes} from "./schemes.js"; import {registry, radius, opacity, color} from "./index.js"; -import {positive, negative} from "../defined.js"; +import {defined, positive, negative} from "../defined.js"; import {constant} from "../mark.js"; const flip = i => t => i(1 - t); -// TODO Allow this to be extended. const interpolators = new Map([ // numbers ["number", interpolateNumber], @@ -78,71 +42,12 @@ const interpolators = new Map([ ["lab", interpolateLab] ]); -// TODO Allow this to be extended. -const schemes = new Map([ - // diverging - ["brbg", interpolateBrBG], - ["prgn", interpolatePRGn], - ["piyg", interpolatePiYG], - ["puor", interpolatePuOr], - ["rdbu", interpolateRdBu], - ["rdgy", interpolateRdGy], - ["rdylbu", interpolateRdYlBu], - ["rdylgn", interpolateRdYlGn], - ["spectral", interpolateSpectral], - - // reversed diverging (for temperature data) - ["burd", t => interpolateRdBu(1 - t)], - ["buylrd", t => interpolateRdYlBu(1 - t)], - - // sequential (single-hue) - ["blues", interpolateBlues], - ["greens", interpolateGreens], - ["greys", interpolateGreys], - ["purples", interpolatePurples], - ["reds", interpolateReds], - ["oranges", interpolateOranges], - - // sequential (multi-hue) - ["turbo", interpolateTurbo], - ["viridis", interpolateViridis], - ["magma", interpolateMagma], - ["inferno", interpolateInferno], - ["plasma", interpolatePlasma], - ["cividis", interpolateCividis], - ["cubehelix", interpolateCubehelixDefault], - ["warm", interpolateWarm], - ["cool", interpolateCool], - ["bugn", interpolateBuGn], - ["bupu", interpolateBuPu], - ["gnbu", interpolateGnBu], - ["orrd", interpolateOrRd], - ["pubugn", interpolatePuBuGn], - ["pubu", interpolatePuBu], - ["purd", interpolatePuRd], - ["rdpu", interpolateRdPu], - ["ylgnbu", interpolateYlGnBu], - ["ylgn", interpolateYlGn], - ["ylorbr", interpolateYlOrBr], - ["ylorrd", interpolateYlOrRd], - - // cyclical - ["rainbow", interpolateRainbow], - ["sinebow", interpolateSinebow] -]); - function Interpolator(interpolate) { const i = (interpolate + "").toLowerCase(); if (!interpolators.has(i)) throw new Error(`unknown interpolator: ${i}`); return interpolators.get(i); } -function Scheme(scheme) { - const s = (scheme + "").toLowerCase(); - if (!schemes.has(s)) throw new Error(`unknown scheme: ${s}`); - return schemes.get(s); -} - export function ScaleQ(key, scale, channels, { nice, clamp, @@ -153,7 +58,7 @@ export function ScaleQ(key, scale, channels, { range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined, scheme, type, - interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? Scheme(scheme) : type === "cyclical" ? interpolateRainbow : interpolateTurbo) : round ? interpolateRound : undefined, + interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? quantitativeScheme(scheme) : quantitativeSchemes.get(type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : undefined, reverse, inset }) { @@ -167,7 +72,7 @@ export function ScaleQ(key, scale, channels, { // is used in conjunction with the range. And other times the interpolate // function is a “fixed” interpolator independent of the range, as when a // color scheme such as interpolateRdBu is used. - if (interpolate !== undefined) { + if (scale.interpolate && interpolate !== undefined) { if (typeof interpolate !== "function") { interpolate = Interpolator(interpolate); } else if (interpolate.length === 1) { @@ -198,10 +103,29 @@ export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(chan return ScaleQ(key, scaleLog().base(base), channels, {domain, ...options}); } +export function ScaleQuantile(key, channels, { + quantiles = 5, + scheme, + domain = inferFullDomain(channels), + range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: quantiles}).slice(0, quantiles), + ...options +}) { + return ScaleQ(key, scaleQuantile(), [], {domain, range, ...options}); +} + export function ScaleSymlog(key, channels, {constant = 1, ...options}) { return ScaleQ(key, scaleSymlog().constant(constant), channels, options); } +export function ScaleThreshold(key, channels, { + domain = inferDomain(channels), + scheme, + range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: domain.length + 1}), + ...options +}) { + return ScaleQ(key, scaleThreshold(), channels, {domain, range, ...options}); +} + export function ScaleIdentity() { return {type: "identity", scale: scaleIdentity()}; } @@ -213,7 +137,7 @@ function ScaleD(key, scale, channels, { pivot = 0, range, scheme, - interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? Scheme(scheme) : interpolateRdBu) : undefined, + interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : "rdbu")) : undefined, reverse }) { domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)]; @@ -284,3 +208,11 @@ function inferLogDomain(channels) { } return [1, 10]; } + +function inferFullDomain(channels) { + let domain = []; + for (const {value} of channels) { + if (value !== undefined) domain = domain.concat(value); + } + return domain.filter(defined); +} diff --git a/src/scales/schemes.js b/src/scales/schemes.js new file mode 100644 index 0000000000..3642798e38 --- /dev/null +++ b/src/scales/schemes.js @@ -0,0 +1,233 @@ +import { + interpolateBlues, + interpolateBrBG, + interpolateBuGn, + interpolateBuPu, + interpolateGnBu, + interpolateGreens, + interpolateGreys, + interpolateOranges, + interpolateOrRd, + interpolatePiYG, + interpolatePRGn, + interpolatePuBu, + interpolatePuBuGn, + interpolatePuOr, + interpolatePuRd, + interpolatePurples, + interpolateRdBu, + interpolateRdGy, + interpolateRdPu, + interpolateRdYlBu, + interpolateRdYlGn, + interpolateReds, + interpolateSpectral, + interpolateYlGn, + interpolateYlGnBu, + interpolateYlOrBr, + interpolateYlOrRd, + interpolateTurbo, + interpolateViridis, + interpolateMagma, + interpolateInferno, + interpolatePlasma, + interpolateCividis, + interpolateCubehelixDefault, + interpolateWarm, + interpolateCool, + interpolateRainbow, + interpolateSinebow, + quantize, + schemeAccent, + schemeBlues, + schemeBrBG, + schemeBuGn, + schemeBuPu, + schemeCategory10, + schemeDark2, + schemeGnBu, + schemeGreens, + schemeGreys, + schemeOranges, + schemeOrRd, + schemePaired, + schemePastel1, + schemePastel2, + schemePiYG, + schemePRGn, + schemePuBu, + schemePuBuGn, + schemePuOr, + schemePuRd, + schemePurples, + schemeRdBu, + schemeRdGy, + schemeRdPu, + schemeRdYlBu, + schemeRdYlGn, + schemeReds, + schemeSet1, + schemeSet2, + schemeSet3, + schemeSpectral, + schemeTableau10, + schemeYlGn, + schemeYlGnBu, + schemeYlOrBr, + schemeYlOrRd +} from "d3"; + +export const ordinalSchemes = new Map([ + // categorical + ["accent", schemeAccent], + ["category10", schemeCategory10], + ["dark2", schemeDark2], + ["paired", schemePaired], + ["pastel1", schemePastel1], + ["pastel2", schemePastel2], + ["set1", schemeSet1], + ["set2", schemeSet2], + ["set3", schemeSet3], + ["tableau10", schemeTableau10], + + // diverging + ["brbg", scheme11(schemeBrBG, interpolateBrBG)], + ["prgn", scheme11(schemePRGn, interpolatePRGn)], + ["piyg", scheme11(schemePiYG, interpolatePiYG)], + ["puor", scheme11(schemePuOr, interpolatePuOr)], + ["rdbu", scheme11(schemeRdBu, interpolateRdBu)], + ["rdgy", scheme11(schemeRdGy, interpolateRdGy)], + ["rdylbu", scheme11(schemeRdYlBu, interpolateRdYlBu)], + ["rdylgn", scheme11(schemeRdYlGn, interpolateRdYlGn)], + ["spectral", scheme11(schemeSpectral, interpolateSpectral)], + + // reversed diverging (for temperature data) + ["burd", scheme11r(schemeRdBu, interpolateRdBu)], + ["buylrd", scheme11r(schemeRdGy, interpolateRdGy)], + + // sequential (single-hue) + ["blues", scheme9(schemeBlues, interpolateBlues)], + ["greens", scheme9(schemeGreens, interpolateGreens)], + ["greys", scheme9(schemeGreys, interpolateGreys)], + ["oranges", scheme9(schemeOranges, interpolateOranges)], + ["purples", scheme9(schemePurples, interpolatePurples)], + ["reds", scheme9(schemeReds, interpolateReds)], + + // sequential (multi-hue) + ["turbo", schemei(interpolateTurbo)], + ["viridis", schemei(interpolateViridis)], + ["magma", schemei(interpolateMagma)], + ["inferno", schemei(interpolateInferno)], + ["plasma", schemei(interpolatePlasma)], + ["cividis", schemei(interpolateCividis)], + ["cubehelix", schemei(interpolateCubehelixDefault)], + ["warm", schemei(interpolateWarm)], + ["cool", schemei(interpolateCool)], + ["bugn", scheme9(schemeBuGn, interpolateBuGn)], + ["bupu", scheme9(schemeBuPu, interpolateBuPu)], + ["gnbu", scheme9(schemeGnBu, interpolateGnBu)], + ["orrd", scheme9(schemeOrRd, interpolateOrRd)], + ["pubu", scheme9(schemePuBu, interpolatePuBu)], + ["pubugn", scheme9(schemePuBuGn, interpolatePuBuGn)], + ["purd", scheme9(schemePuRd, interpolatePuRd)], + ["rdpu", scheme9(schemeRdPu, interpolateRdPu)], + ["ylgn", scheme9(schemeYlGn, interpolateYlGn)], + ["ylgnbu", scheme9(schemeYlGnBu, interpolateYlGnBu)], + ["ylorbr", scheme9(schemeYlOrBr, interpolateYlOrBr)], + ["ylorrd", scheme9(schemeYlOrRd, interpolateYlOrRd)], + + // cyclical + ["rainbow", schemei(interpolateRainbow)], + ["sinebow", schemei(interpolateSinebow)] +]); + +function scheme9(scheme, interpolate) { + return ({length: n}) => { + n = n > 3 ? Math.floor(n) : 3; + return n > 9 ? quantize(interpolate, n) : scheme[n]; + }; +} + +function scheme11(scheme, interpolate) { + return ({length: n}) => { + n = n > 3 ? Math.floor(n) : 3; + return n > 11 ? quantize(interpolate, n) : scheme[n]; + }; +} + +function scheme11r(scheme, interpolate) { + return ({length: n}) => { + n = n > 3 ? Math.floor(n) : 3; + return n > 11 ? quantize(t => interpolate(1 - t), n) : scheme[n].slice().reverse(); + }; +} + +function schemei(interpolate) { + return ({length: n}) => { + return quantize(interpolate, n > 0 ? Math.floor(n) : 0); + }; +} + +export function ordinalScheme(scheme) { + const s = (scheme + "").toLowerCase(); + if (!ordinalSchemes.has(s)) throw new Error(`unknown scheme: ${s}`); + return ordinalSchemes.get(s); +} + +export const quantitativeSchemes = new Map([ + // diverging + ["brbg", interpolateBrBG], + ["prgn", interpolatePRGn], + ["piyg", interpolatePiYG], + ["puor", interpolatePuOr], + ["rdbu", interpolateRdBu], + ["rdgy", interpolateRdGy], + ["rdylbu", interpolateRdYlBu], + ["rdylgn", interpolateRdYlGn], + ["spectral", interpolateSpectral], + + // reversed diverging (for temperature data) + ["burd", t => interpolateRdBu(1 - t)], + ["buylrd", t => interpolateRdYlBu(1 - t)], + + // sequential (single-hue) + ["blues", interpolateBlues], + ["greens", interpolateGreens], + ["greys", interpolateGreys], + ["purples", interpolatePurples], + ["reds", interpolateReds], + ["oranges", interpolateOranges], + + // sequential (multi-hue) + ["turbo", interpolateTurbo], + ["viridis", interpolateViridis], + ["magma", interpolateMagma], + ["inferno", interpolateInferno], + ["plasma", interpolatePlasma], + ["cividis", interpolateCividis], + ["cubehelix", interpolateCubehelixDefault], + ["warm", interpolateWarm], + ["cool", interpolateCool], + ["bugn", interpolateBuGn], + ["bupu", interpolateBuPu], + ["gnbu", interpolateGnBu], + ["orrd", interpolateOrRd], + ["pubugn", interpolatePuBuGn], + ["pubu", interpolatePuBu], + ["purd", interpolatePuRd], + ["rdpu", interpolateRdPu], + ["ylgnbu", interpolateYlGnBu], + ["ylgn", interpolateYlGn], + ["ylorbr", interpolateYlOrBr], + ["ylorrd", interpolateYlOrRd], + + // cyclical + ["rainbow", interpolateRainbow], + ["sinebow", interpolateSinebow] +]); + +export function quantitativeScheme(scheme) { + const s = (scheme + "").toLowerCase(); + if (!quantitativeSchemes.has(s)) throw new Error(`unknown scheme: ${s}`); + return quantitativeSchemes.get(s); +} From 3e8b4c0eaf0f7b0a8f14baf90b1f03fb7d9c0ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 5 Jul 2021 12:58:19 +0200 Subject: [PATCH 02/15] fix {reverse} for "threshold" and "quantile" scales. It did not work, because: - scaleThreshold relies on an ascending domain (and breaks if it's not ascending); let's check for this situation and reverse the scale if the domain is descending - scaleQuantile ignores the domain order --- src/scales/quantitative.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 490b4fe84e..f7916afb71 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -1,4 +1,5 @@ import { + ascending, interpolateHcl, interpolateHsl, interpolateLab, @@ -108,8 +109,10 @@ export function ScaleQuantile(key, channels, { scheme, domain = inferFullDomain(channels), range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: quantiles}).slice(0, quantiles), + reverse, ...options }) { + if (reverse) range = reverseof(range); return ScaleQ(key, scaleQuantile(), [], {domain, range, ...options}); } @@ -121,8 +124,11 @@ export function ScaleThreshold(key, channels, { domain = inferDomain(channels), scheme, range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: domain.length + 1}), + reverse, ...options }) { + if (ascending(domain[domain.length-1], domain[0]) < 0) domain.sort(ascending), reverse = !reverse; + if (reverse) range = reverseof(range); return ScaleQ(key, scaleThreshold(), channels, {domain, range, ...options}); } From a596be53f697ff624505610cc67b9be56273d749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 10 Jul 2021 09:22:30 +0200 Subject: [PATCH 03/15] re-add ScaleQuantile, ScaleThreshold --- src/scales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales.js b/src/scales.js index 7007dc9f49..6a2b328260 100644 --- a/src/scales.js +++ b/src/scales.js @@ -1,5 +1,5 @@ import {registry, position, radius, opacity} from "./scales/index.js"; -import {ScaleLinear, ScaleSqrt, ScalePow, ScaleQuantile, ScaleLog, ScaleSymlog, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js"; +import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js"; import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/quantitative.js"; import {ScaleTime, ScaleUtc} from "./scales/temporal.js"; import {ScaleOrdinal, ScalePoint, ScaleBand} from "./scales/ordinal.js"; From 4286100d658ebef38f8d5fb4e0d213cfc0556685 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 15:18:59 -0700 Subject: [PATCH 04/15] test threshold scale --- test/output/usPresidentialForecast2016.svg | 1078 +++++++++---------- test/plots/us-presidential-forecast-2016.js | 5 + 2 files changed, 544 insertions(+), 539 deletions(-) diff --git a/test/output/usPresidentialForecast2016.svg b/test/output/usPresidentialForecast2016.svg index c83fdeedf4..783ec43146 100644 --- a/test/output/usPresidentialForecast2016.svg +++ b/test/output/usPresidentialForecast2016.svg @@ -40,545 +40,545 @@ Electoral votes for Hillary Clinton → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plots/us-presidential-forecast-2016.js b/test/plots/us-presidential-forecast-2016.js index f9477471a2..11c613ae6b 100644 --- a/test/plots/us-presidential-forecast-2016.js +++ b/test/plots/us-presidential-forecast-2016.js @@ -11,6 +11,11 @@ export default async function() { ticks: 5, percent: true }, + color: { + type: "threshold", + domain: [270], + range: ["red", "blue"] // TODO rdbu? + }, marks: [ Plot.ruleX(data, {x: "dem_electoral_votes", y: "probability", shapeRendering: "crispEdges", stroke: "dem_electoral_votes", strokeWidth: 1.5}), Plot.ruleY([0]), From 96d2f3f3d5318a80f9fe2d27ce6dcf721b9f8951 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 15:27:46 -0700 Subject: [PATCH 05/15] favor diverging extrema --- src/scales/schemes.js | 2 + test/output/usPresidentialForecast2016.svg | 1078 +++++++++---------- test/plots/us-presidential-forecast-2016.js | 3 +- 3 files changed, 542 insertions(+), 541 deletions(-) diff --git a/src/scales/schemes.js b/src/scales/schemes.js index 3642798e38..4f55f50a9e 100644 --- a/src/scales/schemes.js +++ b/src/scales/schemes.js @@ -150,6 +150,7 @@ function scheme9(scheme, interpolate) { function scheme11(scheme, interpolate) { return ({length: n}) => { + if (n === 2) return [scheme[3][0], scheme[3][2]]; // favor diverging extrema n = n > 3 ? Math.floor(n) : 3; return n > 11 ? quantize(interpolate, n) : scheme[n]; }; @@ -157,6 +158,7 @@ function scheme11(scheme, interpolate) { function scheme11r(scheme, interpolate) { return ({length: n}) => { + if (n === 2) return [scheme[3][0], scheme[3][2]]; // favor diverging extrema n = n > 3 ? Math.floor(n) : 3; return n > 11 ? quantize(t => interpolate(1 - t), n) : scheme[n].slice().reverse(); }; diff --git a/test/output/usPresidentialForecast2016.svg b/test/output/usPresidentialForecast2016.svg index 783ec43146..e0ebf273a3 100644 --- a/test/output/usPresidentialForecast2016.svg +++ b/test/output/usPresidentialForecast2016.svg @@ -40,545 +40,545 @@ Electoral votes for Hillary Clinton → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plots/us-presidential-forecast-2016.js b/test/plots/us-presidential-forecast-2016.js index 11c613ae6b..a97d98442d 100644 --- a/test/plots/us-presidential-forecast-2016.js +++ b/test/plots/us-presidential-forecast-2016.js @@ -13,8 +13,7 @@ export default async function() { }, color: { type: "threshold", - domain: [270], - range: ["red", "blue"] // TODO rdbu? + domain: [270] }, marks: [ Plot.ruleX(data, {x: "dem_electoral_votes", y: "probability", shapeRendering: "crispEdges", stroke: "dem_electoral_votes", strokeWidth: 1.5}), From c4a537d1ec237eeabb70823b0e34ef7bccf36fb1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 16:36:45 -0700 Subject: [PATCH 06/15] ordinal schemes; default thresholds of [0] --- src/scales/ordinal.js | 2 +- src/scales/quantitative.js | 24 ++++++++++++------------ src/scales/schemes.js | 6 ++++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index 654a3bf69d..c23a6f6c62 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -74,7 +74,7 @@ function inferDomain(channels) { const domain = new InternSet(); for (const {value} of channels) { if (value === undefined) continue; - for (const v of value) domain.add(v); + for (const v of value) domain.add(v); // TODO skip nullish? } return sort(domain, ascendingDefined); } diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index f7916afb71..39321d8162 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -25,7 +25,7 @@ import { scaleThreshold, scaleIdentity } from "d3"; -import {ordinalScheme, quantitativeScheme, quantitativeSchemes} from "./schemes.js"; +import {ordinalRange, quantitativeScheme, quantitativeSchemes} from "./schemes.js"; import {registry, radius, opacity, color} from "./index.js"; import {defined, positive, negative} from "../defined.js"; import {constant} from "../mark.js"; @@ -106,9 +106,9 @@ export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(chan export function ScaleQuantile(key, channels, { quantiles = 5, - scheme, - domain = inferFullDomain(channels), - range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: quantiles}).slice(0, quantiles), + scheme = "rdylbu", + domain = inferQuantileDomain(channels), + range = ordinalRange(scheme, quantiles), reverse, ...options }) { @@ -121,13 +121,12 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) { } export function ScaleThreshold(key, channels, { - domain = inferDomain(channels), - scheme, - range = ordinalScheme(scheme === undefined ? "rdylbu" : scheme)({length: domain.length + 1}), + domain = [0], // you must specify the thresholds explicitly, and in ascending order! + scheme = "rdylbu", + range = ordinalRange(scheme, domain.length + 1), reverse, ...options }) { - if (ascending(domain[domain.length-1], domain[0]) < 0) domain.sort(ascending), reverse = !reverse; if (reverse) range = reverseof(range); return ScaleQ(key, scaleThreshold(), channels, {domain, range, ...options}); } @@ -215,10 +214,11 @@ function inferLogDomain(channels) { return [1, 10]; } -function inferFullDomain(channels) { - let domain = []; +function inferQuantileDomain(channels) { + const domain = []; for (const {value} of channels) { - if (value !== undefined) domain = domain.concat(value); + if (value === undefined) continue; + for (const v of value) if (defined(v)) domain.push(v); } - return domain.filter(defined); + return domain; } diff --git a/src/scales/schemes.js b/src/scales/schemes.js index 4f55f50a9e..d741f0dfcb 100644 --- a/src/scales/schemes.js +++ b/src/scales/schemes.js @@ -176,6 +176,12 @@ export function ordinalScheme(scheme) { return ordinalSchemes.get(s); } +export function ordinalRange(scheme, length) { + const s = ordinalScheme(scheme); + const r = typeof s === "function" ? s({length}) : s; + return r.length !== length ? r.slice(length) : r; +} + export const quantitativeSchemes = new Map([ // diverging ["brbg", interpolateBrBG], From f25b67b52a298943ad01f153a112f412e12f8189 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 16:38:33 -0700 Subject: [PATCH 07/15] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f5144e800..0bf9fe3a45 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ A scale’s domain (the extent of its inputs, abstract values) and range (the ex * *scale*.**range** - typically [*min*, *max*], or an array of ordinal or categorical values * *scale*.**reverse** - reverses the domain, say to flip the chart along *x* or *y* -For most quantitative scales, the default domain is the [*min*, *max*] of all values associated with the scale. For the *radius* and *opacity* scales, the default domain is [0, *max*] to ensure a meaningful value encoding. For ordinal scales, the default domain is the set of all distinct values associated with the scale in natural ascending order; set the domain explicitly for a different order. If a scale is reversed, it is equivalent to setting the domain as [*max*, *min*] instead of [*min*, *max*]. +For most quantitative scales, the default domain is the [*min*, *max*] of all values associated with the scale. For the *radius* and *opacity* scales, the default domain is [0, *max*] to ensure a meaningful value encoding. For ordinal scales, the default domain is the set of all distinct values associated with the scale in natural ascending order; set the domain explicitly for a different order. For threshold scales, the default domain is [0] to separate negative and non-negative values. For quantile scales, the default domain is the set of all defined values associated with the scale. If a scale is reversed, it is equivalent to setting the domain as [*max*, *min*] instead of [*min*, *max*]. The default range depends on the scale: for [position scales](#position-options) (*x*, *y*, *fx*, and *fy*), the default range depends on the plot’s [size and margins](#layout-options). For [color scales](#color-options), there are default color schemes for quantitative, ordinal, and categorical data. For opacity, the default range is [0, 1]. And for radius, the default range is designed to produce dots of “reasonable” size assuming a *sqrt* scale type for accurate area representation: zero maps to zero, the first quartile maps to a radius of three pixels, and other values are extrapolated. This convention for radius ensures that if the scale’s data values are all equal, dots have the default constant radius of three pixels, while if the data varies, dots will tend to be larger. From f681af157ea2eaaf1faf6274281569a5332fd41c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 16:42:12 -0700 Subject: [PATCH 08/15] =?UTF-8?q?don=E2=80=99t=20export=20schemes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scales/ordinal.js | 6 ++---- src/scales/quantitative.js | 5 ++--- src/scales/schemes.js | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index c23a6f6c62..56b63f202d 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -1,6 +1,6 @@ import {InternSet, reverse as reverseof, sort} from "d3"; import {scaleBand, scaleOrdinal, scalePoint} from "d3"; -import {ordinalScheme, ordinalSchemes} from "./schemes.js"; +import {ordinalScheme} from "./schemes.js"; import {ascendingDefined} from "../defined.js"; import {registry, color} from "./index.js"; @@ -23,9 +23,7 @@ export function ScaleO(scale, channels, { export function ScaleOrdinal(key, channels, { scheme, type, - range = registry.get(key) === color ? (scheme !== undefined ? ordinalScheme(scheme) - : ordinalSchemes.get(type === "ordinal" ? "turbo" : "tableau10")) - : undefined, + range = registry.get(key) === color ? (ordinalScheme(scheme !== undefined ? scheme : type === "ordinal" ? "turbo" : "tableau10")) : undefined, ...options }) { return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options}); diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 39321d8162..50a4709bc2 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -1,5 +1,4 @@ import { - ascending, interpolateHcl, interpolateHsl, interpolateLab, @@ -25,7 +24,7 @@ import { scaleThreshold, scaleIdentity } from "d3"; -import {ordinalRange, quantitativeScheme, quantitativeSchemes} from "./schemes.js"; +import {ordinalRange, quantitativeScheme} from "./schemes.js"; import {registry, radius, opacity, color} from "./index.js"; import {defined, positive, negative} from "../defined.js"; import {constant} from "../mark.js"; @@ -59,7 +58,7 @@ export function ScaleQ(key, scale, channels, { range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined, scheme, type, - interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? quantitativeScheme(scheme) : quantitativeSchemes.get(type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : undefined, + interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : undefined, reverse, inset }) { diff --git a/src/scales/schemes.js b/src/scales/schemes.js index d741f0dfcb..0766192b8e 100644 --- a/src/scales/schemes.js +++ b/src/scales/schemes.js @@ -77,7 +77,7 @@ import { schemeYlOrRd } from "d3"; -export const ordinalSchemes = new Map([ +const ordinalSchemes = new Map([ // categorical ["accent", schemeAccent], ["category10", schemeCategory10], @@ -182,7 +182,7 @@ export function ordinalRange(scheme, length) { return r.length !== length ? r.slice(length) : r; } -export const quantitativeSchemes = new Map([ +const quantitativeSchemes = new Map([ // diverging ["brbg", interpolateBrBG], ["prgn", interpolatePRGn], From 1531953f49e7ebfc89c00dc111630b9270b06edb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 16:45:49 -0700 Subject: [PATCH 09/15] require ascending thresholds --- src/scales/quantitative.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 50a4709bc2..21c30bb6a4 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -1,4 +1,5 @@ import { + ascending, interpolateHcl, interpolateHsl, interpolateLab, @@ -9,6 +10,7 @@ import { max, quantile, reverse as reverseof, + pairs, piecewise, scaleDiverging, scaleDivergingLog, @@ -126,6 +128,7 @@ export function ScaleThreshold(key, channels, { reverse, ...options }) { + if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error("non-ascending domain"); if (reverse) range = reverseof(range); return ScaleQ(key, scaleThreshold(), channels, {domain, range, ...options}); } From e3ce85ad5cc39176dd20c7cd7327a1efdb924d10 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 16:57:04 -0700 Subject: [PATCH 10/15] =?UTF-8?q?threshold=20scales=20don=E2=80=99t=20supp?= =?UTF-8?q?ort=20continuous=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scales/quantitative.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 21c30bb6a4..d51fc0c5f6 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -74,7 +74,7 @@ export function ScaleQ(key, scale, channels, { // is used in conjunction with the range. And other times the interpolate // function is a “fixed” interpolator independent of the range, as when a // color scheme such as interpolateRdBu is used. - if (scale.interpolate && interpolate !== undefined) { + if (interpolate !== undefined) { if (typeof interpolate !== "function") { interpolate = Interpolator(interpolate); } else if (interpolate.length === 1) { @@ -122,15 +122,15 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) { } export function ScaleThreshold(key, channels, { - domain = [0], // you must specify the thresholds explicitly, and in ascending order! + domain = [0], // explicit thresholds in ascending order scheme = "rdylbu", range = ordinalRange(scheme, domain.length + 1), reverse, - ...options + percent }) { if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error("non-ascending domain"); - if (reverse) range = reverseof(range); - return ScaleQ(key, scaleThreshold(), channels, {domain, range, ...options}); + if (reverse = !!reverse) range = reverseof(range); // domain ascending, so reverse range + return {type: "threshold", scale: scaleThreshold(domain, range), reverse, domain, range, percent}; } export function ScaleIdentity() { From 2cdf1e3cd1e76bcf09a22172ad7c6d48eb341289 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 17:07:11 -0700 Subject: [PATCH 11/15] =?UTF-8?q?quantile=20scales=20don=E2=80=99t=20suppo?= =?UTF-8?q?rt=20continuous=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scales/ordinal.js | 4 ++-- src/scales/quantitative.js | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index 56b63f202d..d5ad6d9a5b 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -21,9 +21,9 @@ export function ScaleO(scale, channels, { } export function ScaleOrdinal(key, channels, { - scheme, type, - range = registry.get(key) === color ? (ordinalScheme(scheme !== undefined ? scheme : type === "ordinal" ? "turbo" : "tableau10")) : undefined, + scheme = type === "ordinal" ? "turbo" : "tableau10", // ignored if not color + range = registry.get(key) === color ? ordinalScheme(scheme) : undefined, ...options }) { return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options}); diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index d51fc0c5f6..c33eada4a9 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -28,7 +28,7 @@ import { } from "d3"; import {ordinalRange, quantitativeScheme} from "./schemes.js"; import {registry, radius, opacity, color} from "./index.js"; -import {defined, positive, negative} from "../defined.js"; +import {positive, negative} from "../defined.js"; import {constant} from "../mark.js"; const flip = i => t => i(1 - t); @@ -58,9 +58,9 @@ export function ScaleQ(key, scale, channels, { percent, round, range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined, - scheme, type, - interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : undefined, + scheme = type === "cyclical" ? "rainbow" : "turbo", // ignored if not color + interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : round ? interpolateRound : undefined, reverse, inset }) { @@ -109,12 +109,12 @@ export function ScaleQuantile(key, channels, { quantiles = 5, scheme = "rdylbu", domain = inferQuantileDomain(channels), - range = ordinalRange(scheme, quantiles), + range = registry.get(key) === color ? ordinalRange(scheme, quantiles) : undefined, reverse, - ...options + percent }) { - if (reverse) range = reverseof(range); - return ScaleQ(key, scaleQuantile(), [], {domain, range, ...options}); + if (reverse = !!reverse) range = reverseof(range); // domain unordered, so reverse range + return {type: "quantitative", scale: scaleQuantile(domain, range), reverse, domain, range, percent}; } export function ScaleSymlog(key, channels, {constant = 1, ...options}) { @@ -123,8 +123,8 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) { export function ScaleThreshold(key, channels, { domain = [0], // explicit thresholds in ascending order - scheme = "rdylbu", - range = ordinalRange(scheme, domain.length + 1), + scheme = "rdylbu", // ignored if not color + range = registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined, reverse, percent }) { @@ -143,8 +143,8 @@ function ScaleD(key, scale, channels, { domain = inferDomain(channels), pivot = 0, range, - scheme, - interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : "rdbu")) : undefined, + scheme = "rdbu", // ignored if not color + interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : undefined, reverse }) { domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)]; @@ -220,7 +220,7 @@ function inferQuantileDomain(channels) { const domain = []; for (const {value} of channels) { if (value === undefined) continue; - for (const v of value) if (defined(v)) domain.push(v); + for (const v of value) domain.push(v); } return domain; } From 51f44ef32b82337df69c1b0703149ee381b6785e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 17:14:41 -0700 Subject: [PATCH 12/15] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0bf9fe3a45..c6b3c66233 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,8 @@ Color scales support two additional options: * *scale*.**scheme** - a named color scheme in lieu of a range, such as *reds* * *scale*.**interpolate** - in conjunction with a range, how to interpolate colors +For quantile color scales, the *scale*.scheme option is used in conjunction with *scale*.**quantiles**, which determines how many quantiles to compute, and thus the number of elements in the scale’s range; it defaults to 5 for quintiles. + The following sequential scale schemes are supported for both quantitative and ordinal data: * blues *blues* From fa2806630e4f5df8a0b13accc4e9384d4111a334 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 17:15:55 -0700 Subject: [PATCH 13/15] remove comment --- src/scales/ordinal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index d5ad6d9a5b..dfa5feb1bd 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -72,7 +72,7 @@ function inferDomain(channels) { const domain = new InternSet(); for (const {value} of channels) { if (value === undefined) continue; - for (const v of value) domain.add(v); // TODO skip nullish? + for (const v of value) domain.add(v); } return sort(domain, ascendingDefined); } From ddd6cc6e916b6c6a252b48afd51f8754e3ebaeb4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 17:16:37 -0700 Subject: [PATCH 14/15] type = quantile --- src/scales/quantitative.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index c33eada4a9..2486aad661 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -114,7 +114,7 @@ export function ScaleQuantile(key, channels, { percent }) { if (reverse = !!reverse) range = reverseof(range); // domain unordered, so reverse range - return {type: "quantitative", scale: scaleQuantile(domain, range), reverse, domain, range, percent}; + return {type: "quantile", scale: scaleQuantile(domain, range), reverse, domain, range, percent}; } export function ScaleSymlog(key, channels, {constant = 1, ...options}) { From ceec1d934c78eaeafd43be2b229c76775cdd5fe4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 2 Aug 2021 17:19:19 -0700 Subject: [PATCH 15/15] remove comment --- src/scales/quantitative.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 2486aad661..430f2c9c16 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -59,7 +59,7 @@ export function ScaleQ(key, scale, channels, { round, range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined, type, - scheme = type === "cyclical" ? "rainbow" : "turbo", // ignored if not color + scheme = type === "cyclical" ? "rainbow" : "turbo", interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : round ? interpolateRound : undefined, reverse, inset @@ -123,7 +123,7 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) { export function ScaleThreshold(key, channels, { domain = [0], // explicit thresholds in ascending order - scheme = "rdylbu", // ignored if not color + scheme = "rdylbu", range = registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined, reverse, percent @@ -143,7 +143,7 @@ function ScaleD(key, scale, channels, { domain = inferDomain(channels), pivot = 0, range, - scheme = "rdbu", // ignored if not color + scheme = "rdbu", interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : undefined, reverse }) {