Skip to content

Commit a6e4c43

Browse files
authored
scale unknown (#559)
* scale unknown * tests for unknown * simplify test * Update README * Update README * fix test
1 parent c92b022 commit a6e4c43

File tree

11 files changed

+148
-9
lines changed

11 files changed

+148
-9
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,15 @@ A scale’s domain (the extent of its inputs, abstract values) and range (the ex
171171

172172
* *scale*.**domain** - typically [*min*, *max*], or an array of ordinal or categorical values
173173
* *scale*.**range** - typically [*min*, *max*], or an array of ordinal or categorical values
174-
* *scale*.**reverse** - reverses the domain, say to flip the chart along *x* or *y*
174+
* *scale*.**unknown** - the desired output value (defaults to undefined) for invalid input values
175+
* *scale*.**reverse** - reverses the domain (or in somes cases, the range), say to flip the chart along *x* or *y*
175176

176177
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-options) 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*].
177178

178179
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.
179180

181+
The behavior of the *scale*.unknown option depends on the scale type. For quantitative and temporal scales, the unknown value is used whenever the input value is undefined, null, or NaN. For ordinal or categorical scales, the unknown value is returned for any input value outside the domain. For band or point scales, the unknown option has no effect; it is effectively always equal to undefined. If the unknown option is set to undefined (the default), or null or NaN, then the affected input values will be considered undefined and filtered from the output.
182+
180183
Quantitative scales can be further customized with additional options:
181184

182185
* *scale*.**clamp** - if true, clamp input values to the scale’s domain

src/scales.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,15 @@ function exposeScale({
293293
}) {
294294
if (type === "identity") return {type: "identity"};
295295
const domain = scale.domain();
296+
const unknown = scale.unknown ? scale.unknown() : undefined;
296297
return {
297298
type,
298299
domain,
299300
...range !== undefined && {range: Array.from(range)}, // defensive copy
300301
...transform !== undefined && {transform},
301302
...percent && {percent}, // only exposed if truthy
302303
...label !== undefined && {label},
304+
...unknown !== undefined && {unknown},
303305

304306
// quantitative
305307
...interpolate !== undefined && {interpolate},

src/scales/diverging.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function ScaleD(key, scale, transform, channels, {
1515
nice,
1616
clamp,
1717
domain = inferDomain(channels),
18+
unknown,
1819
pivot = 0,
1920
range,
2021
scheme = "rdbu",
@@ -46,7 +47,7 @@ function ScaleD(key, scale, transform, channels, {
4647
}
4748

4849
if (reverse) interpolate = flip(interpolate);
49-
scale.domain([min, pivot, max]).interpolator(interpolate);
50+
scale.domain([min, pivot, max]).unknown(unknown).interpolator(interpolate);
5051
if (clamp) scale.clamp(clamp);
5152
if (nice) scale.nice(nice);
5253
return {type, interpolate, scale};

src/scales/ordinal.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {InternSet, reverse as reverseof, sort} from "d3";
2-
import {scaleBand, scaleOrdinal, scalePoint} from "d3";
2+
import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3";
33
import {ordinalScheme} from "./schemes.js";
44
import {ascendingDefined} from "../defined.js";
55
import {registry, color} from "./index.js";
@@ -27,9 +27,11 @@ export function ScaleOrdinal(key, channels, {
2727
type,
2828
scheme = type === "ordinal" ? "turbo" : "tableau10", // ignored if not color
2929
range = registry.get(key) === color ? ordinalScheme(scheme) : undefined,
30+
unknown,
3031
...options
3132
}) {
32-
return ScaleO(scaleOrdinal().unknown(undefined), channels, {type, range, ...options});
33+
if (unknown === scaleImplicit) throw new Error("implicit unknown is not supported");
34+
return ScaleO(scaleOrdinal().unknown(unknown), channels, {type, range, ...options});
3335
}
3436

3537
export function ScalePoint(key, channels, {

src/scales/quantitative.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function ScaleQ(key, scale, channels, {
5252
clamp,
5353
zero,
5454
domain = (registry.get(key) === radius || registry.get(key) === opacity ? inferZeroDomain : inferDomain)(channels),
55+
unknown,
5556
round,
5657
range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? unit : undefined,
5758
type,
@@ -105,7 +106,7 @@ export function ScaleQ(key, scale, channels, {
105106
}
106107

107108
if (reverse) domain = reverseof(domain);
108-
scale.domain(domain);
109+
scale.domain(domain).unknown(unknown);
109110
if (nice) scale.nice(nice === true ? undefined : nice);
110111
if (range !== undefined) scale.range(range);
111112
if (clamp) scale.clamp(clamp);
@@ -149,14 +150,15 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
149150

150151
export function ScaleThreshold(key, channels, {
151152
domain = [0], // explicit thresholds in ascending order
153+
unknown,
152154
scheme = "rdylbu",
153155
interpolate,
154156
range = interpolate !== undefined ? quantize(interpolate, domain.length + 1) : registry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined,
155157
reverse
156158
}) {
157159
if (!pairs(domain).every(([a, b]) => ascending(a, b) <= 0)) throw new Error("non-ascending domain");
158160
if (reverse) range = reverseof(range); // domain ascending, so reverse range
159-
return {type: "threshold", scale: scaleThreshold(domain, range), domain, range};
161+
return {type: "threshold", scale: scaleThreshold(domain, range).unknown(unknown), domain, range};
160162
}
161163

162164
export function ScaleIdentity() {

test/output/penguinIslandUnknown.svg

Lines changed: 56 additions & 0 deletions
Loading

test/output/simpsonsRatings.svg

Lines changed: 4 additions & 1 deletion
Loading

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export {default as musicRevenue} from "./music-revenue.js";
7676
export {default as ordinalBar} from "./ordinal-bar.js";
7777
export {default as penguinCulmen} from "./penguin-culmen.js";
7878
export {default as penguinCulmenArray} from "./penguin-culmen-array.js";
79+
export {default as penguinIslandUnknown} from "./penguin-island-unknown.js";
7980
export {default as penguinMass} from "./penguin-mass.js";
8081
export {default as penguinMassSex} from "./penguin-mass-sex.js";
8182
export {default as penguinMassSexSpecies} from "./penguin-mass-sex-species.js";

test/plots/penguin-island-unknown.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
6+
return Plot.plot({
7+
color: {
8+
domain: ["Dream"],
9+
unknown: "#ccc"
10+
},
11+
marks: [
12+
Plot.barY(penguins, Plot.groupX({y: "count"}, {x: "sex", fill: "island"})),
13+
Plot.ruleY([0])
14+
]
15+
});
16+
}

test/plots/simpsons-ratings.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export default async function() {
1414
label: "Season"
1515
},
1616
color: {
17-
scheme: "PiYG"
17+
scheme: "PiYG",
18+
unknown: "#ddd"
1819
},
1920
height: 640,
2021
marks: [
@@ -26,7 +27,7 @@ export default async function() {
2627
Plot.text(data, {
2728
x: "number_in_season",
2829
y: "season",
29-
text: d => d.imdb_rating == null ? null : d.imdb_rating.toFixed(1),
30+
text: d => d.imdb_rating == null ? "-" : d.imdb_rating.toFixed(1),
3031
title: "title"
3132
})
3233
]

0 commit comments

Comments
 (0)