Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quantize scale #829

Merged
merged 8 commits into from
Mar 26, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,16 @@ The normal scale types—*linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordinal*
* *categorical* - equivalent to *ordinal*, but defaults to the *tableau10* scheme
* *sequential* - equivalent to *linear*
* *cyclical* - equivalent to *linear*, but defaults to the *rainbow* scheme
* *threshold* - encodes based on the specified discrete thresholds
* *quantile* - encodes based on the computed quantile thresholds
* *threshold* - encodes based on the specified discrete thresholds; defaults to the *rdylbu* scheme
* *quantile* - encodes based on the computed quantile thresholds; defaults to the *rdylbu* scheme
* *quantize* - transforms a continuous domain into quantized thresholds; defaults to the *rdylbu* scheme
* *diverging* - like *linear*, but with a pivot; defaults to the *rdbu* scheme
* *diverging-log* - like *log*, but with a pivot that defaults to 1; defaults to the *rdbu* scheme
* *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

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.
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. For a *quantize* scale, the domain will be transformed into approximately *n* quantized values, where *n* is an option that defaults to 5.

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.

Expand Down
3 changes: 2 additions & 1 deletion src/scales.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {parse as isoParse} from "isoformat";
import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order, isTemporalString, isNumericString, isScaleOptions} from "./options.js";
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleQuantize, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js";
import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js";
Expand Down Expand Up @@ -196,6 +196,7 @@ function Scale(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 "quantize": return ScaleQuantize(key, channels, options);
case "pow": return ScalePow(key, channels, options);
case "log": return ScaleLog(key, channels, options);
case "symlog": return ScaleSymlog(key, channels, options);
Expand Down
64 changes: 54 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,10 +20,11 @@ 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";

Expand Down Expand Up @@ -52,7 +53,7 @@ export function ScaleQ(key, scale, channels, {
nice,
clamp,
zero,
domain = (registry.get(key) === radius || registry.get(key) === opacity || registry.get(key) === length ? inferZeroDomain : inferDomain)(channels),
domain = inferAutoDomain(key, channels),
unknown,
round,
scheme,
Expand Down Expand Up @@ -123,23 +124,50 @@ export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(chan
return ScaleQ(key, scaleLog().base(base), channels, {...options, domain});
}

export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
}

export function ScaleQuantile(key, channels, {
quantiles = 5,
quantiles,
mbostock marked this conversation as resolved.
Show resolved Hide resolved
scheme = "rdylbu",
domain = inferQuantileDomain(channels),
interpolate,
range = interpolate !== undefined ? quantize(interpolate, quantiles) : registry.get(key) === color ? ordinalRange(scheme, quantiles) : undefined,
range,
reverse
}) {
if (quantiles === undefined) quantiles = range === undefined ? 5 : (range = [...range]).length;
if (range === undefined) range = interpolate !== undefined ? quantize(interpolate, quantiles) : registry.get(key) === color ? ordinalRange(scheme, quantiles) : undefined;
return ScaleThreshold(key, channels, {
domain: scaleQuantile(domain, range === undefined ? {length: quantiles} : range).quantiles(),
range,
reverse
});
}

export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
export function ScaleQuantize(key, channels, {
n,
scheme = "rdylbu",
domain = inferAutoDomain(key, channels),
interpolate,
range,
reverse
}) {
if (n === undefined) n = range === undefined ? 5 : (range = [...range]).length;
const [min, max] = extent(domain);
let thresholds;
if (range === undefined) {
thresholds = ticks(min, max, n); // approximate number of nice, round thresholds
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;
} else {
thresholds = quantize(interpolateNumber(min, max), n + 1).slice(1, -1); // exactly n - 1 thresholds to match range
if (min instanceof Date) thresholds = thresholds.map(x => new Date(x)); // preserve date types
}
if (order(arrayify(domain)) < 0) thresholds.reverse(); // preserve descending domain
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});
}

export function ScaleThreshold(key, channels, {
Expand All @@ -150,9 +178,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 All @@ -166,6 +205,11 @@ export function inferDomain(channels, f = finite) {
] : [0, 1];
}

function inferAutoDomain(key, channels) {
const type = registry.get(key);
return (type === radius || type === opacity || type === length ? inferZeroDomain : inferDomain)(channels);
}

function inferZeroDomain(channels) {
return [0, channels.length ? max(channels, ({value}) => value === undefined ? value : max(value, finite)) : 1];
}
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.
49 changes: 49 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.
37 changes: 37 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.
37 changes: 37 additions & 0 deletions test/output/colorLegendQuantizeRange.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.
Loading