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
39 changes: 22 additions & 17 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, desc] of Object.entries(descriptors)) {
const {scale, type, interval, label} = desc;
scales[key] = exposeScale(desc);
Copy link
Member

Choose a reason for hiding this comment

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

This involves a bunch of copying and these exposed scales are rarely used. I think we should investigate whether we can make this lazy using a getter (but probably a caching getter so that if you access the same scale multiple times it returns the same instance).

Copy link
Member

Choose a reason for hiding this comment

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

Fil’s comment: We’ll need these exposed scales in the near future anyway when we adopt them internally for axes etc., and therefore it’s premature optimization (or even slower) to make these lazy.

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,21 +515,24 @@ 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];
};
}

// Note: axis- and legend-related properties (such as label, ticks and
// tickFormat) are not included here as they do not affect the scale’s behavior.
function exposeScale({scale, type, domain, range, interpolate, interval, transform, percent, pivot}) {
if (type === "identity") return {type: "identity", apply: (d) => d, invert: (d) => d};
function exposeScale({scale, type, range, domain, interpolate, interval, transform, percent, pivot}) {
// The domain and range may be missing for non-position identity scales (e.g.,
// color), and for position identity scales, only the range is computed
// internally (by autoScaleRange) and then promoted to the domain here.
if (type === "identity") domain = range;
const unknown = scale.unknown ? scale.unknown() : undefined;
return {
type,
domain: slice(domain), // defensive copy
...(domain !== undefined && {domain: slice(domain)}), // defensive copy
...(range !== undefined && {range: slice(range)}), // defensive copy
...(transform !== undefined && {transform}),
...(percent && {percent}), // only exposed if truthy
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;
}
13 changes: 10 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,15 @@ 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.
const identityScale = (d) => d;
identityScale.invert = identityScale;

export function createScaleIdentity(key) {
return {type: "identity", scale: hasNumericRange(registry.get(key)) ? scaleIdentity() : identityScale};
}

export function inferDomain(channels, f = finite) {
Expand Down
Loading