diff --git a/README.md b/README.md
index af78d9a24f..c6b3c66233 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.
@@ -252,12 +252,16 @@ 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:
* *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*
diff --git a/src/scales.js b/src/scales.js
index 3c314d3535..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, ScaleLog, ScaleSymlog, 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";
@@ -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..dfa5feb1bd 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} 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,
@@ -196,11 +21,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,
+ 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 e49592e7f9..430f2c9c16 100644
--- a/src/scales/quantitative.js
+++ b/src/scales/quantitative.js
@@ -1,53 +1,17 @@
import {
- min,
- max,
- quantile,
- reverse as reverseof,
- piecewise,
+ ascending,
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,
+ pairs,
+ piecewise,
scaleDiverging,
scaleDivergingLog,
scaleDivergingPow,
@@ -57,16 +21,18 @@ import {
scaleLog,
scalePow,
scaleSqrt,
+ scaleQuantile,
scaleSymlog,
+ scaleThreshold,
scaleIdentity
} from "d3";
+import {ordinalRange, quantitativeScheme} from "./schemes.js";
import {registry, radius, opacity, color} from "./index.js";
import {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 +44,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,
@@ -151,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 : scheme !== undefined ? Scheme(scheme) : type === "cyclical" ? interpolateRainbow : interpolateTurbo) : round ? interpolateRound : undefined,
+ scheme = type === "cyclical" ? "rainbow" : "turbo",
+ interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : round ? interpolateRound : undefined,
reverse,
inset
}) {
@@ -198,10 +105,34 @@ 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 = "rdylbu",
+ domain = inferQuantileDomain(channels),
+ range = registry.get(key) === color ? ordinalRange(scheme, quantiles) : undefined,
+ reverse,
+ percent
+}) {
+ if (reverse = !!reverse) range = reverseof(range); // domain unordered, so reverse range
+ return {type: "quantile", scale: scaleQuantile(domain, range), reverse, domain, range, percent};
+}
+
export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
}
+export function ScaleThreshold(key, channels, {
+ domain = [0], // explicit thresholds in ascending order
+ scheme = "rdylbu",
+ range = registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined,
+ reverse,
+ percent
+}) {
+ if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error("non-ascending domain");
+ 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() {
return {type: "identity", scale: scaleIdentity()};
}
@@ -212,8 +143,8 @@ function ScaleD(key, scale, channels, {
domain = inferDomain(channels),
pivot = 0,
range,
- scheme,
- interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? Scheme(scheme) : interpolateRdBu) : undefined,
+ scheme = "rdbu",
+ interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : undefined,
reverse
}) {
domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)];
@@ -284,3 +215,12 @@ function inferLogDomain(channels) {
}
return [1, 10];
}
+
+function inferQuantileDomain(channels) {
+ const domain = [];
+ for (const {value} of channels) {
+ if (value === undefined) continue;
+ for (const v of value) domain.push(v);
+ }
+ return domain;
+}
diff --git a/src/scales/schemes.js b/src/scales/schemes.js
new file mode 100644
index 0000000000..0766192b8e
--- /dev/null
+++ b/src/scales/schemes.js
@@ -0,0 +1,241 @@
+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";
+
+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}) => {
+ 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];
+ };
+}
+
+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();
+ };
+}
+
+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 function ordinalRange(scheme, length) {
+ const s = ordinalScheme(scheme);
+ const r = typeof s === "function" ? s({length}) : s;
+ return r.length !== length ? r.slice(length) : r;
+}
+
+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);
+}
diff --git a/test/output/usPresidentialForecast2016.svg b/test/output/usPresidentialForecast2016.svg
index c83fdeedf4..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 f9477471a2..a97d98442d 100644
--- a/test/plots/us-presidential-forecast-2016.js
+++ b/test/plots/us-presidential-forecast-2016.js
@@ -11,6 +11,10 @@ export default async function() {
ticks: 5,
percent: true
},
+ color: {
+ type: "threshold",
+ domain: [270]
+ },
marks: [
Plot.ruleX(data, {x: "dem_electoral_votes", y: "probability", shapeRendering: "crispEdges", stroke: "dem_electoral_votes", strokeWidth: 1.5}),
Plot.ruleY([0]),