diff --git a/src/plot.js b/src/plot.js index a659b2a924..45cdcfce2e 100644 --- a/src/plot.js +++ b/src/plot.js @@ -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; @@ -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: newIntantiatedScales, ...newScales} = createScaleFunctions(newScaleDescriptors); Object.assign(scaleDescriptors, newScaleDescriptors); Object.assign(scales, newScales); + Object.assign(scales.scales, newIntantiatedScales); } // Sort and filter the facets to match the fx and fy domains; this is needed @@ -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(); diff --git a/src/scales.d.ts b/src/scales.d.ts index 3ed4d489c8..8dd27fc66a 100644 --- a/src/scales.d.ts +++ b/src/scales.d.ts @@ -161,9 +161,11 @@ 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: diff --git a/src/scales.js b/src/scales.js index 5b993c18e7..8a6ca78d4f 100644 --- a/src/scales.js +++ b/src/scales.js @@ -97,17 +97,21 @@ 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); + if (scale) { + scaleFunctions[key] = scale; // drop identity scales + // 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! @@ -513,10 +517,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]; }; } diff --git a/test/output/renderInstantiatedScales.svg b/test/output/renderInstantiatedScales.svg new file mode 100644 index 0000000000..ec7b3e066a --- /dev/null +++ b/test/output/renderInstantiatedScales.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + + + ↑ culmen_depth_mm + + + + + + + + + + 35 + 40 + 45 + 50 + 55 + + + culmen_length_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + x: {"type":"linear","domain":[32.1,59.6],"range":[40,680],"clamp":false} + y: {"type":"linear","domain":[13.1,21.5],"range":[370,20],"clamp":false} + color: {"type":"linear","domain":[1,4],"range":[0,0.2],"clamp":false} + r: {"type":"pow","domain":[0,4],"range":[0,10],"clamp":false,"exponent":0.5} + + + \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index 65c6cfb5c6..f50407b08f 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -240,6 +240,7 @@ export * from "./raster-vapor.js"; export * from "./raster-walmart.js"; export * from "./rect-band.js"; export * from "./reducer-scale-override.js"; +export * from "./render.js"; export * from "./seattle-precipitation-density.js"; export * from "./seattle-precipitation-rule.js"; export * from "./seattle-precipitation-sum.js"; diff --git a/test/plots/render.ts b/test/plots/render.ts new file mode 100644 index 0000000000..2e173775ae --- /dev/null +++ b/test/plots/render.ts @@ -0,0 +1,37 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import * as htl from "htl"; + +export async function renderInstantiatedScales() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.dot( + penguins, + Plot.hexbin( + {fill: "count", r: "count"}, + { + stroke: "white", + x: "culmen_length_mm", + y: "culmen_depth_mm", + render(index, scales, values, dimensions, context, next) { + const w = (dimensions.width + dimensions.marginLeft - dimensions.marginRight) / 2; + const h = (dimensions.height + dimensions.marginTop - dimensions.marginBottom) / 2; + return htl.svg` + ${next(index, scales, values, dimensions, context)} + ${d3 + .select(context.ownerSVGElement) + .append("g") + .call((g) => + g + .selectAll() + .data(Object.entries(scales.scales)) + .join("text") + .attr("x", w) + .attr("y", (d, i) => h + 16 * i) + .text(([key, scale]) => `${key}: ${JSON.stringify(scale)}`) + ) + .node()}`; + } + } + ) + ).plot({width: 700, color: {scheme: "Blues", range: [0, 0.2]}}); +}