Skip to content

expose instantiated scales descriptors in the render API #1810

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

Merged
merged 13 commits into from
Aug 16, 2023
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