Skip to content

Commit 81f17ea

Browse files
mbostockFil
andauthored
quantize scale (#829)
* quantize scale * tests * document quantize scales * handle descending domain for quantize scales (#830) * reverse and descending * tests for quantize scales * remove dead code * update test snapshot * handle non-array domain Co-authored-by: Mike Bostock <[email protected]> * Update README * exact thresholds when range is specified * rename quantiles option to n * Update README Co-authored-by: Philippe Rivière <[email protected]>
1 parent 5b8b227 commit 81f17ea

13 files changed

+2354
-1934
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,16 @@ The normal scale types—*linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordinal*
305305
* *categorical* - equivalent to *ordinal*, but defaults to the *tableau10* scheme
306306
* *sequential* - equivalent to *linear*
307307
* *cyclical* - equivalent to *linear*, but defaults to the *rainbow* scheme
308-
* *threshold* - encodes based on the specified discrete thresholds
309-
* *quantile* - encodes based on the computed quantile thresholds
308+
* *threshold* - encodes based on the specified discrete thresholds; defaults to the *rdylbu* scheme
309+
* *quantile* - encodes based on the computed quantile thresholds; defaults to the *rdylbu* scheme
310+
* *quantize* - transforms a continuous domain into quantized thresholds; defaults to the *rdylbu* scheme
310311
* *diverging* - like *linear*, but with a pivot; defaults to the *rdbu* scheme
311312
* *diverging-log* - like *log*, but with a pivot that defaults to 1; defaults to the *rdbu* scheme
312313
* *diverging-pow* - like *pow*, but with a pivot; defaults to the *rdbu* scheme
313314
* *diverging-sqrt* - like *sqrt*, but with a pivot; defaults to the *rdbu* scheme
314315
* *diverging-symlog* - like *symlog*, but with a pivot; defaults to the *rdbu* scheme
315316

316-
For a *threshold* scale, the *domain* represents *n* (typically numeric) thresholds which will produce a *range* of *n* + 1 output colors; the *i*th color of the *range* applies to values that are smaller than the *i*th element of the domain and larger or equal to the *i* - 1th element of the domain. For a *quantile* scale, the *domain* represents all input values to the scale, and the *quantiles* option specifies how many quantiles to compute from the *domain*; *n* quantiles will produce *n* - 1 thresholds, and an output range of *n* colors.
317+
For a *threshold* scale, the *domain* represents *n* (typically numeric) thresholds which will produce a *range* of *n* + 1 output colors; the *i*th color of the *range* applies to values that are smaller than the *i*th element of the domain and larger or equal to the *i* - 1th element of the domain. For a *quantile* scale, the *domain* represents all input values to the scale, and the *n* option specifies how many quantiles to compute from the *domain*; *n* quantiles will produce *n* - 1 thresholds, and an output range of *n* colors. For a *quantize* scale, the domain will be transformed into approximately *n* quantized values, where *n* is an option that defaults to 5.
317318

318319
By default, all diverging color scales are symmetric around the pivot; set *symmetric* to false if you want to cover the whole extent on both sides.
319320

@@ -322,7 +323,7 @@ Color scales support two additional options:
322323
* *scale*.**scheme** - a named color scheme in lieu of a range, such as *reds*
323324
* *scale*.**interpolate** - in conjunction with a range, how to interpolate colors
324325

325-
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.
326+
For quantile and quantize color scales, the *scale*.scheme option is used in conjunction with *scale*.**n**, which determines how many quantiles or quantized values to compute, and thus the number of elements in the scale’s range; it defaults to 5 (for quintiles in the case of a quantile scale).
326327

327328
The following sequential scale schemes are supported for both quantitative and ordinal data:
328329

src/scales.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {parse as isoParse} from "isoformat";
22
import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order, isTemporalString, isNumericString, isScaleOptions} from "./options.js";
33
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
4-
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
4+
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleQuantize, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
55
import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js";
66
import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
77
import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js";
@@ -196,6 +196,7 @@ function Scale(key, channels = [], options = {}) {
196196
case "sqrt": return ScaleSqrt(key, channels, options);
197197
case "threshold": return ScaleThreshold(key, channels, options);
198198
case "quantile": return ScaleQuantile(key, channels, options);
199+
case "quantize": return ScaleQuantize(key, channels, options);
199200
case "pow": return ScalePow(key, channels, options);
200201
case "log": return ScaleLog(key, channels, options);
201202
case "symlog": return ScaleSymlog(key, channels, options);

src/scales/quantitative.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
ascending,
2+
descending,
33
extent,
44
interpolateHcl,
55
interpolateHsl,
@@ -20,10 +20,11 @@ import {
2020
scaleQuantile,
2121
scaleSymlog,
2222
scaleThreshold,
23-
scaleIdentity
23+
scaleIdentity,
24+
ticks
2425
} from "d3";
2526
import {positive, negative, finite} from "../defined.js";
26-
import {constant, order} from "../options.js";
27+
import {arrayify, constant, order} from "../options.js";
2728
import {ordinalRange, quantitativeScheme} from "./schemes.js";
2829
import {registry, radius, opacity, color, length} from "./index.js";
2930

@@ -52,7 +53,7 @@ export function ScaleQ(key, scale, channels, {
5253
nice,
5354
clamp,
5455
zero,
55-
domain = (registry.get(key) === radius || registry.get(key) === opacity || registry.get(key) === length ? inferZeroDomain : inferDomain)(channels),
56+
domain = inferAutoDomain(key, channels),
5657
unknown,
5758
round,
5859
scheme,
@@ -123,23 +124,49 @@ export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(chan
123124
return ScaleQ(key, scaleLog().base(base), channels, {...options, domain});
124125
}
125126

127+
export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
128+
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
129+
}
130+
126131
export function ScaleQuantile(key, channels, {
127-
quantiles = 5,
132+
range,
133+
quantiles = range === undefined ? 5 : (range = [...range]).length, // deprecated; use n instead
134+
n = quantiles,
128135
scheme = "rdylbu",
129136
domain = inferQuantileDomain(channels),
130137
interpolate,
131-
range = interpolate !== undefined ? quantize(interpolate, quantiles) : registry.get(key) === color ? ordinalRange(scheme, quantiles) : undefined,
132138
reverse
133139
}) {
140+
if (range === undefined) range = interpolate !== undefined ? quantize(interpolate, n) : registry.get(key) === color ? ordinalRange(scheme, n) : undefined;
134141
return ScaleThreshold(key, channels, {
135-
domain: scaleQuantile(domain, range === undefined ? {length: quantiles} : range).quantiles(),
142+
domain: scaleQuantile(domain, range === undefined ? {length: n} : range).quantiles(),
136143
range,
137144
reverse
138145
});
139146
}
140147

141-
export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
142-
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
148+
export function ScaleQuantize(key, channels, {
149+
range,
150+
n = range === undefined ? 5 : (range = [...range]).length,
151+
scheme = "rdylbu",
152+
domain = inferAutoDomain(key, channels),
153+
interpolate,
154+
reverse
155+
}) {
156+
const [min, max] = extent(domain);
157+
let thresholds;
158+
if (range === undefined) {
159+
thresholds = ticks(min, max, n); // approximate number of nice, round thresholds
160+
if (thresholds[0] <= min) thresholds.splice(0, 1); // drop exact lower bound
161+
if (thresholds[thresholds.length - 1] >= max) thresholds.pop(); // drop exact upper bound
162+
n = thresholds.length + 1;
163+
range = interpolate !== undefined ? quantize(interpolate, n) : registry.get(key) === color ? ordinalRange(scheme, n) : undefined;
164+
} else {
165+
thresholds = quantize(interpolateNumber(min, max), n + 1).slice(1, -1); // exactly n - 1 thresholds to match range
166+
if (min instanceof Date) thresholds = thresholds.map(x => new Date(x)); // preserve date types
167+
}
168+
if (order(arrayify(domain)) < 0) thresholds.reverse(); // preserve descending domain
169+
return ScaleThreshold(key, channels, {domain: thresholds, range, reverse});
143170
}
144171

145172
export function ScaleThreshold(key, channels, {
@@ -150,9 +177,20 @@ export function ScaleThreshold(key, channels, {
150177
range = interpolate !== undefined ? quantize(interpolate, domain.length + 1) : registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined,
151178
reverse
152179
}) {
153-
if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error(`the ${key} scale has a non-ascending domain`);
180+
const sign = order(arrayify(domain)); // preserve descending domain
181+
if (!pairs(domain).every(([a, b]) => isOrdered(a, b, sign))) throw new Error(`the ${key} scale has a non-monotonic domain`);
154182
if (reverse) range = reverseof(range); // domain ascending, so reverse range
155-
return {type: "threshold", scale: scaleThreshold(domain, range === undefined ? [] : range).unknown(unknown), domain, range};
183+
return {
184+
type: "threshold",
185+
scale: scaleThreshold(sign < 0 ? reverseof(domain) : domain, range === undefined ? [] : range).unknown(unknown),
186+
domain,
187+
range
188+
};
189+
}
190+
191+
function isOrdered(a, b, sign) {
192+
const s = descending(a, b);
193+
return s === 0 || s === sign;
156194
}
157195

158196
export function ScaleIdentity() {
@@ -166,6 +204,11 @@ export function inferDomain(channels, f = finite) {
166204
] : [0, 1];
167205
}
168206

207+
function inferAutoDomain(key, channels) {
208+
const type = registry.get(key);
209+
return (type === radius || type === opacity || type === length ? inferZeroDomain : inferDomain)(channels);
210+
}
211+
169212
function inferZeroDomain(channels) {
170213
return [0, channels.length ? max(channels, ({value}) => value === undefined ? value : max(value, finite)) : 1];
171214
}

test/output/colorLegendQuantize.svg

Lines changed: 49 additions & 0 deletions
Loading
Lines changed: 49 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)