Skip to content
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