Skip to content

Commit f360557

Browse files
Filmbostock
andauthored
expose instantiated scales descriptors in the render API (#1810)
* expose instantiated scales descriptors in the render API * more uniform handling of identity scales * don’t expose domain and range for identity (yet) --------- Co-authored-by: Mike Bostock <mbostock@gmail.com>
1 parent 2511ef3 commit f360557

File tree

6 files changed

+93
-22
lines changed

6 files changed

+93
-22
lines changed

src/plot.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ export function plot(options = {}) {
141141

142142
// Initalize the scales and dimensions.
143143
const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark, options), options);
144-
const scales = createScaleFunctions(scaleDescriptors);
145144
const dimensions = createDimensions(scaleDescriptors, marks, options);
146145

147146
autoScaleRange(scaleDescriptors, dimensions);
148147

148+
const scales = createScaleFunctions(scaleDescriptors);
149149
const {fx, fy} = scales;
150150
const subdimensions = fx || fy ? innerDimensions(scaleDescriptors, dimensions) : dimensions;
151151
const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions;
@@ -221,9 +221,10 @@ export function plot(options = {}) {
221221
addScaleChannels(newChannelsByScale, stateByMark, options, (key) => newByScale.has(key));
222222
addScaleChannels(channelsByScale, stateByMark, options, (key) => newByScale.has(key));
223223
const newScaleDescriptors = inheritScaleLabels(createScales(newChannelsByScale, options), scaleDescriptors);
224-
const newScales = createScaleFunctions(newScaleDescriptors);
224+
const {scales: newExposedScales, ...newScales} = createScaleFunctions(newScaleDescriptors);
225225
Object.assign(scaleDescriptors, newScaleDescriptors);
226226
Object.assign(scales, newScales);
227+
Object.assign(scales.scales, newExposedScales);
227228
}
228229

229230
// Sort and filter the facets to match the fx and fy domains; this is needed
@@ -333,7 +334,7 @@ export function plot(options = {}) {
333334
if (caption != null) figure.append(createFigcaption(document, caption));
334335
}
335336

336-
figure.scale = exposeScales(scaleDescriptors);
337+
figure.scale = exposeScales(scales.scales);
337338
figure.legend = exposeLegends(scaleDescriptors, context, options);
338339

339340
const w = consumeWarnings();

src/scales.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ export type ScaleName = "x" | "y" | "fx" | "fy" | "r" | "color" | "opacity" | "s
161161

162162
/**
163163
* The instantiated scales’ apply functions; passed to marks and initializers
164-
* for rendering.
164+
* for rendering. The scales property exposes all the scale definitions.
165165
*/
166-
export type ScaleFunctions = {[key in ScaleName]?: (value: any) => any};
166+
export type ScaleFunctions = {[key in ScaleName]?: (value: any) => any} & {scales: {[key in ScaleName]?: Scale}};
167167

168168
/**
169169
* The supported scale types. For quantitative data, one of:

src/scales.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,19 @@ export function createScales(
9797
return scales;
9898
}
9999

100-
export function createScaleFunctions(scales) {
101-
return Object.fromEntries(
102-
Object.entries(scales)
103-
.filter(([, {scale}]) => scale) // drop identity scales
104-
.map(([name, {scale, type, interval, label}]) => {
105-
scale.type = type; // for axis
106-
if (interval != null) scale.interval = interval; // for axis
107-
if (label != null) scale.label = label; // for axis
108-
return [name, scale];
109-
})
110-
);
100+
export function createScaleFunctions(descriptors) {
101+
const scales = {};
102+
const scaleFunctions = {scales};
103+
for (const [key, descriptor] of Object.entries(descriptors)) {
104+
const {scale, type, interval, label} = descriptor;
105+
scales[key] = exposeScale(descriptor);
106+
scaleFunctions[key] = scale;
107+
// TODO: pass these properties, which are needed for axes, in the descriptor.
108+
scale.type = type;
109+
if (interval != null) scale.interval = interval;
110+
if (label != null) scale.label = label;
111+
}
112+
return scaleFunctions;
111113
}
112114

113115
// Mutates scale.range!
@@ -362,7 +364,7 @@ function createScale(key, channels = [], options = {}) {
362364
case "band":
363365
return createScaleBand(key, channels, options);
364366
case "identity":
365-
return registry.get(key) === position ? createScaleIdentity() : {type: "identity"};
367+
return createScaleIdentity(key);
366368
case undefined:
367369
return;
368370
default:
@@ -513,10 +515,10 @@ export function scale(options = {}) {
513515
return scale;
514516
}
515517

516-
export function exposeScales(scaleDescriptors) {
518+
export function exposeScales(scales) {
517519
return (key) => {
518520
if (!registry.has((key = `${key}`))) throw new Error(`unknown scale: ${key}`);
519-
return key in scaleDescriptors ? exposeScale(scaleDescriptors[key]) : undefined;
521+
return scales[key];
520522
};
521523
}
522524

src/scales/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ export const registry = new Map([
4545
export function isPosition(kind) {
4646
return kind === position || kind === projection;
4747
}
48+
49+
export function hasNumericRange(kind) {
50+
return kind === position || kind === radius || kind === length || kind === opacity;
51+
}

src/scales/quantitative.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from "d3";
2626
import {finite, negative, positive} from "../defined.js";
2727
import {arrayify, constant, maybeNiceInterval, maybeRangeInterval, orderof, slice} from "../options.js";
28-
import {color, length, opacity, radius, registry} from "./index.js";
28+
import {color, length, opacity, radius, registry, hasNumericRange} from "./index.js";
2929
import {ordinalRange, quantitativeScheme} from "./schemes.js";
3030

3131
export const flip = (i) => (t) => i(1 - t);
@@ -257,8 +257,12 @@ function isOrdered(domain, sign) {
257257
return true;
258258
}
259259

260-
export function createScaleIdentity() {
261-
return {type: "identity", scale: scaleIdentity()};
260+
// For non-numeric identity scales such as color and symbol, we can’t use D3’s
261+
// identity scale because it coerces to number; and we can’t compute the domain
262+
// (and equivalently range) since we can’t know whether the values are
263+
// continuous or discrete.
264+
export function createScaleIdentity(key) {
265+
return {type: "identity", scale: hasNumericRange(registry.get(key)) ? scaleIdentity() : (d) => d};
262266
}
263267

264268
export function inferDomain(channels, f = finite) {

test/scales/scales-test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,66 @@ it("plot(…).scale(name).apply and invert return the expected functions", () =>
21022102
]);
21032103
});
21042104

2105+
it("Plot.plot passes render functions scale descriptors", async () => {
2106+
const seed = d3.randomLcg(42);
2107+
const x = d3.randomNormal.source(seed)();
2108+
Plot.plot({
2109+
marks: [
2110+
Plot.dotX({length: 10001}, {x, fill: seed}),
2111+
(index, {x, color, scales}) => {
2112+
assert.deepStrictEqual(Object.keys(scales), ["color", "x"]);
2113+
assert.strictEqual(x(0), 314.6324357568407);
2114+
assert.strictEqual(x(1), 400.26512486789505);
2115+
assert.strictEqual(color(0), "rgb(35, 23, 27)");
2116+
assert.strictEqual(color(1), "rgb(144, 12, 0)");
2117+
scaleEqual(scales.color, {
2118+
type: "linear",
2119+
domain: [0.0003394410014152527, 0.999856373295188],
2120+
range: [0, 1],
2121+
clamp: false,
2122+
interpolate: d3.interpolateTurbo
2123+
});
2124+
scaleEqual(scales.x, {
2125+
type: "linear",
2126+
domain: [-3.440653783215207, 3.5660162890264693],
2127+
range: [20, 620],
2128+
clamp: false,
2129+
interpolate: d3.interpolateNumber
2130+
});
2131+
return null;
2132+
}
2133+
]
2134+
});
2135+
});
2136+
2137+
it("Plot.plot passes render functions re-initialized scale descriptors and functions", async () => {
2138+
const seed = d3.randomLcg(42);
2139+
const x = d3.randomNormal.source(seed)();
2140+
const y = d3.randomNormal.source(seed)();
2141+
Plot.plot({
2142+
marks: [
2143+
Plot.dot({length: 10001}, Plot.hexbin({fill: "count"}, {x, y})),
2144+
(index, {x, y, color, scales}) => {
2145+
assert.deepStrictEqual(Object.keys(scales), ["x", "y", "color"]);
2146+
assert.ok(Math.abs(x(0) - 351) < 1);
2147+
assert.ok(Math.abs(x(1) - 426) < 1);
2148+
assert.ok(Math.abs(y(0) - 196) < 1);
2149+
assert.ok(Math.abs(y(1) - 148) < 1);
2150+
assert.strictEqual(color(1), "rgb(35, 23, 27)");
2151+
assert.strictEqual(color(10), "rgb(72, 58, 164)");
2152+
scaleEqual(scales.color, {
2153+
type: "linear",
2154+
domain: [1, 161],
2155+
range: [0, 1],
2156+
clamp: false,
2157+
interpolate: d3.interpolateTurbo
2158+
});
2159+
return null;
2160+
}
2161+
]
2162+
});
2163+
});
2164+
21052165
it("plot(…).scale(name) returns a deduplicated ordinal domain", () => {
21062166
const letters = "abbbcaabbcc";
21072167
const plot = Plot.dotX(letters).plot({x: {domain: letters}});

0 commit comments

Comments
 (0)