Skip to content

Commit

Permalink
expose instantiated scales descriptors in the render API (#1810)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Fil and mbostock authored Aug 16, 2023
1 parent 2511ef3 commit f360557
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 22 deletions.
7 changes: 4 additions & 3 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ export function plot(options = {}) {

// Initalize the scales and dimensions.
const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark, options), options);
const scales = createScaleFunctions(scaleDescriptors);
const dimensions = createDimensions(scaleDescriptors, marks, options);

autoScaleRange(scaleDescriptors, dimensions);

const scales = createScaleFunctions(scaleDescriptors);
const {fx, fy} = scales;
const subdimensions = fx || fy ? innerDimensions(scaleDescriptors, dimensions) : dimensions;
const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions;
Expand Down Expand Up @@ -221,9 +221,10 @@ export function plot(options = {}) {
addScaleChannels(newChannelsByScale, stateByMark, options, (key) => newByScale.has(key));
addScaleChannels(channelsByScale, stateByMark, options, (key) => newByScale.has(key));
const newScaleDescriptors = inheritScaleLabels(createScales(newChannelsByScale, options), scaleDescriptors);
const newScales = createScaleFunctions(newScaleDescriptors);
const {scales: newExposedScales, ...newScales} = createScaleFunctions(newScaleDescriptors);
Object.assign(scaleDescriptors, newScaleDescriptors);
Object.assign(scales, newScales);
Object.assign(scales.scales, newExposedScales);
}

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

figure.scale = exposeScales(scaleDescriptors);
figure.scale = exposeScales(scales.scales);
figure.legend = exposeLegends(scaleDescriptors, context, options);

const w = consumeWarnings();
Expand Down
4 changes: 2 additions & 2 deletions src/scales.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ export type ScaleName = "x" | "y" | "fx" | "fy" | "r" | "color" | "opacity" | "s

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

/**
* The supported scale types. For quantitative data, one of:
Expand Down
30 changes: 16 additions & 14 deletions src/scales.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,19 @@ export function createScales(
return scales;
}

export function createScaleFunctions(scales) {
return Object.fromEntries(
Object.entries(scales)
.filter(([, {scale}]) => scale) // drop identity scales
.map(([name, {scale, type, interval, label}]) => {
scale.type = type; // for axis
if (interval != null) scale.interval = interval; // for axis
if (label != null) scale.label = label; // for axis
return [name, scale];
})
);
export function createScaleFunctions(descriptors) {
const scales = {};
const scaleFunctions = {scales};
for (const [key, descriptor] of Object.entries(descriptors)) {
const {scale, type, interval, label} = descriptor;
scales[key] = exposeScale(descriptor);
scaleFunctions[key] = scale;
// TODO: pass these properties, which are needed for axes, in the descriptor.
scale.type = type;
if (interval != null) scale.interval = interval;
if (label != null) scale.label = label;
}
return scaleFunctions;
}

// Mutates scale.range!
Expand Down Expand Up @@ -362,7 +364,7 @@ function createScale(key, channels = [], options = {}) {
case "band":
return createScaleBand(key, channels, options);
case "identity":
return registry.get(key) === position ? createScaleIdentity() : {type: "identity"};
return createScaleIdentity(key);
case undefined:
return;
default:
Expand Down Expand Up @@ -513,10 +515,10 @@ export function scale(options = {}) {
return scale;
}

export function exposeScales(scaleDescriptors) {
export function exposeScales(scales) {
return (key) => {
if (!registry.has((key = `${key}`))) throw new Error(`unknown scale: ${key}`);
return key in scaleDescriptors ? exposeScale(scaleDescriptors[key]) : undefined;
return scales[key];
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/scales/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ export const registry = new Map([
export function isPosition(kind) {
return kind === position || kind === projection;
}

export function hasNumericRange(kind) {
return kind === position || kind === radius || kind === length || kind === opacity;
}
10 changes: 7 additions & 3 deletions src/scales/quantitative.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "d3";
import {finite, negative, positive} from "../defined.js";
import {arrayify, constant, maybeNiceInterval, maybeRangeInterval, orderof, slice} from "../options.js";
import {color, length, opacity, radius, registry} from "./index.js";
import {color, length, opacity, radius, registry, hasNumericRange} from "./index.js";
import {ordinalRange, quantitativeScheme} from "./schemes.js";

export const flip = (i) => (t) => i(1 - t);
Expand Down Expand Up @@ -257,8 +257,12 @@ function isOrdered(domain, sign) {
return true;
}

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

export function inferDomain(channels, f = finite) {
Expand Down
60 changes: 60 additions & 0 deletions test/scales/scales-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,66 @@ it("plot(…).scale(name).apply and invert return the expected functions", () =>
]);
});

it("Plot.plot passes render functions scale descriptors", async () => {
const seed = d3.randomLcg(42);
const x = d3.randomNormal.source(seed)();
Plot.plot({
marks: [
Plot.dotX({length: 10001}, {x, fill: seed}),
(index, {x, color, scales}) => {
assert.deepStrictEqual(Object.keys(scales), ["color", "x"]);
assert.strictEqual(x(0), 314.6324357568407);
assert.strictEqual(x(1), 400.26512486789505);
assert.strictEqual(color(0), "rgb(35, 23, 27)");
assert.strictEqual(color(1), "rgb(144, 12, 0)");
scaleEqual(scales.color, {
type: "linear",
domain: [0.0003394410014152527, 0.999856373295188],
range: [0, 1],
clamp: false,
interpolate: d3.interpolateTurbo
});
scaleEqual(scales.x, {
type: "linear",
domain: [-3.440653783215207, 3.5660162890264693],
range: [20, 620],
clamp: false,
interpolate: d3.interpolateNumber
});
return null;
}
]
});
});

it("Plot.plot passes render functions re-initialized scale descriptors and functions", async () => {
const seed = d3.randomLcg(42);
const x = d3.randomNormal.source(seed)();
const y = d3.randomNormal.source(seed)();
Plot.plot({
marks: [
Plot.dot({length: 10001}, Plot.hexbin({fill: "count"}, {x, y})),
(index, {x, y, color, scales}) => {
assert.deepStrictEqual(Object.keys(scales), ["x", "y", "color"]);
assert.ok(Math.abs(x(0) - 351) < 1);
assert.ok(Math.abs(x(1) - 426) < 1);
assert.ok(Math.abs(y(0) - 196) < 1);
assert.ok(Math.abs(y(1) - 148) < 1);
assert.strictEqual(color(1), "rgb(35, 23, 27)");
assert.strictEqual(color(10), "rgb(72, 58, 164)");
scaleEqual(scales.color, {
type: "linear",
domain: [1, 161],
range: [0, 1],
clamp: false,
interpolate: d3.interpolateTurbo
});
return null;
}
]
});
});

it("plot(…).scale(name) returns a deduplicated ordinal domain", () => {
const letters = "abbbcaabbcc";
const plot = Plot.dotX(letters).plot({x: {domain: letters}});
Expand Down

0 comments on commit f360557

Please sign in to comment.