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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features/scales.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ A scale’s domain (the extent of its inputs, abstract values) and range (the ex
* **reverse** - reverses the domain (or the range), say to flip the chart along *x* or *y*
* **interval** - an interval or time interval (for interval data; see below)

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; for a different order, set the domain explicitly or add a [**sort** option](#sort-mark-option) to an associated mark. 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*].
For most quantitative scales, the default domain is the [*min*, *max*] of all values associated with the scale. For linear scales, the default domain is extended to include zero if within 7% of the domain extent; use **zero**: false to disable this, or **zero**: true to always include zero. For the *radius*, *opacity*, and *length* 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; for a different order, set the domain explicitly or add a [**sort** option](#sort-mark-option) to an associated mark. 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 (*x*, *y*, *fx*, and *fy*), the default range depends on the [plot’s size and margins](./plots.md). For color scales, 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.

Expand Down
102 changes: 51 additions & 51 deletions src/scales/quantitative.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,38 +53,34 @@ export function createScaleQ(
key,
scale,
channels,
{
type,
nice,
clamp,
zero,
domain = inferAutoDomain(key, channels),
unknown,
round,
scheme,
interval,
range = registry.get(key) === radius
? inferRadialRange(channels, domain)
: registry.get(key) === length
? inferLengthRange(channels, domain)
: registry.get(key) === opacity
? unit
: undefined,
interpolate = registry.get(key) === color
? scheme == null && range !== undefined
? interpolateRgb
: quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")
: round
? interpolateRound
: interpolateNumber,
reverse
}
{type, nice, clamp, zero, domain, unknown, round, scheme, interval, range, interpolate, reverse}
) {
domain = maybeRepeat(domain);
domain = maybeRepeat(deriveDomain(key, channels, domain, zero, type));
interval = maybeRangeInterval(interval, type);
if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes
if (interpolate === undefined) {
interpolate =
registry.get(key) === color
? scheme == null && range !== undefined
? interpolateRgb
: quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")
: round
? interpolateRound
: interpolateNumber;
}
if (typeof interpolate !== "function") interpolate = maybeInterpolator(interpolate); // named interpolator
if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes
reverse = !!reverse;
if (range === undefined) {
const s = registry.get(key);
range =
s === radius
? inferRadialRange(channels, domain)
: s === length
? inferLengthRange(channels, domain)
: s === opacity
? unit
: undefined;
}

// If an explicit range is specified, and it has a different length than the
// domain, then redistribute the range using a piecewise interpolator.
Expand Down Expand Up @@ -115,21 +111,6 @@ export function createScaleQ(
scale.interpolate(interpolate);
}

// If a zero option is specified, we assume that the domain is numeric, and we
// want to ensure that the domain crosses zero. However, note that the domain
// may be reversed (descending) so we shouldn’t assume that the first value is
// smaller than the last; and also it’s possible that the domain has more than
// two values for a “poly” scale. And lastly be careful not to mutate input!
if (zero) {
const [min, max] = extent(domain);
if (min > 0 || max < 0) {
domain = slice(domain);
const o = orderof(domain) || 1; // treat degenerate as ascending
if (o === Math.sign(min)) domain[0] = 0; // [1, 2] or [-1, -2]
else domain[domain.length - 1] = 0; // [2, 1] or [-2, -1]
}
}

if (reverse) domain = reverseof(domain);
scale.domain(domain).unknown(unknown);
if (nice) scale.nice(maybeNice(nice, type)), (domain = scale.domain());
Expand Down Expand Up @@ -202,12 +183,13 @@ export function createScaleQuantize(
range,
n = range === undefined ? 5 : (range = [...range]).length,
scheme = "rdylbu",
domain = inferAutoDomain(key, channels),
domain,
unknown,
interpolate,
reverse
}
) {
domain = deriveDomain(key, channels, domain);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note : this is equivalent to what was before (because quantile scales do not support the zero option), but it feels better to use the same pattern as we do for linear scales. If that makes sense we could also decide to support the zero option for quantize scales, but this PR is not about adding this as a new feature.

const [min, max] = extent(domain);
let thresholds;
if (range === undefined) {
Expand Down Expand Up @@ -282,13 +264,31 @@ 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];
// Infer the domain from channels if not explicitly provided, then extend it
// to cross zero if requested, or implicitly for radius, opacity, and length
// scales, and for linear scales when the origin is within 7% of the spread.
// The domain may be reversed (descending) or have more than two values (poly).
function deriveDomain(key, channels, domain, zero, type) {
const isInferredDomain = domain === undefined;
if (isInferredDomain) domain = inferDomain(channels);
if (zero || (zero === undefined && isInferredDomain)) {
const s = registry.get(key);
const [min, max] = extent(domain);
if (
(min > 0 || max < 0) &&
(zero ||
s === radius ||
s === opacity ||
s === length ||
(type === "linear" && (min > 0 ? min < 0.07 * (max - min) : max > 0.07 * (min - max))))
) {
domain = slice(domain); // don't mutate input!
const o = orderof(domain) || 1; // treat degenerate as ascending
if (o === Math.sign(min)) domain[0] = 0; // [1, 2] or [-1, -2]
else domain[domain.length - 1] = 0; // [2, 1] or [-2, -1]
}
}
return domain;
}

// We don’t want the upper bound of the radial domain to be zero, as this would
Expand Down
54 changes: 27 additions & 27 deletions test/output/arcCollatz.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading