From 072698919835e4b782014013ec3f4f92be9dcd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 12:44:10 +0200 Subject: [PATCH 01/12] expose instantiated scales descriptors in the render API --- src/plot.js | 7 +- src/scales.d.ts | 6 +- src/scales.js | 30 +-- test/output/renderInstantiatedScales.svg | 270 +++++++++++++++++++++++ test/plots/index.ts | 1 + test/plots/render.ts | 37 ++++ 6 files changed, 333 insertions(+), 18 deletions(-) create mode 100644 test/output/renderInstantiatedScales.svg create mode 100644 test/plots/render.ts 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]}}); +} From 9803451f97109d65f89f4df2db83e599c5394e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 16:47:28 +0200 Subject: [PATCH 02/12] =?UTF-8?q?not=20bugs=20per=20se,=20but=20since=20in?= =?UTF-8?q?=20the=20future=20we'll=20want=20to=20use=20the=20descriptor?= =?UTF-8?q?=E2=80=99s=20range=20and=20domain,=20better=20align=20it=20with?= =?UTF-8?q?=20what=20the=20D3=20scales=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scales.js | 5 +++-- test/scales/scales-test.js | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/scales.js b/src/scales.js index 8a6ca78d4f..5c8b89a4e1 100644 --- a/src/scales.js +++ b/src/scales.js @@ -527,11 +527,12 @@ export function exposeScales(scales) { // 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}; + if (type === "identity") + return {type: "identity", apply: (d) => d, invert: (d) => d, ...(range !== undefined && {range})}; const unknown = scale.unknown ? scale.unknown() : undefined; return { type, - domain: slice(domain), // defensive copy + domain: [...new Set(domain)], // defensive copy, ensure uniqueness ...(range !== undefined && {range: slice(range)}), // defensive copy ...(transform !== undefined && {transform}), ...(percent && {percent}), // only exposed if truthy diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 2303cfc2ef..108b6c208b 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -2080,7 +2080,7 @@ it("plot(…).scale(name) reflects the given transform", async () => { it("plot(…).scale(name) can return an identity scale, ignoring all other options", () => { const plot = Plot.dot([1, 2], {x: (d) => d, fill: (d) => d}).plot({x: {type: "identity"}, color: {type: "identity"}}); - scaleEqual(plot.scale("x"), {type: "identity"}); + scaleEqual(plot.scale("x"), {range: [20, 620], type: "identity"}); scaleEqual(plot.scale("color"), {type: "identity"}); }); @@ -2102,6 +2102,21 @@ it("plot(…).scale(name).apply and invert return the expected functions", () => ]); }); +it("plot(…).scale(name) returns a deduplicated domain", () => { + const letters = "abbbcaabbcc"; + const plot = Plot.dotX(letters).plot({x: {domain: letters}}); + scaleEqual(plot.scale("x"), { + align: 0.5, + bandwidth: 0, + domain: ["a", "b", "c"], + padding: 0.5, + range: [20, 620], + round: true, + step: 200, + type: "point" + }); +}); + // Given a plot specification (or, as shorthand, an array of marks or a single // mark), asserts that the given named scales, when materialized from the first // plot and used to produce a second plot, produce the same output and the same From 604c6e0ac17c241de18d9ff0e336e97da089f182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 17:04:30 +0200 Subject: [PATCH 03/12] naming --- src/plot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plot.js b/src/plot.js index 45cdcfce2e..0411bbf65a 100644 --- a/src/plot.js +++ b/src/plot.js @@ -221,10 +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 {scales: newIntantiatedScales, ...newScales} = createScaleFunctions(newScaleDescriptors); + const {scales: newExposedScales, ...newScales} = createScaleFunctions(newScaleDescriptors); Object.assign(scaleDescriptors, newScaleDescriptors); Object.assign(scales, newScales); - Object.assign(scales.scales, newIntantiatedScales); + Object.assign(scales.scales, newExposedScales); } // Sort and filter the facets to match the fx and fy domains; this is needed From 8cdc66e282a12d9797d9d90a33bf5a51015c2648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 17:04:43 +0200 Subject: [PATCH 04/12] whitespace --- src/scales.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/scales.d.ts b/src/scales.d.ts index 8dd27fc66a..3d548745f1 100644 --- a/src/scales.d.ts +++ b/src/scales.d.ts @@ -163,9 +163,7 @@ export type ScaleName = "x" | "y" | "fx" | "fy" | "r" | "color" | "opacity" | "s * The instantiated scales’ apply functions; passed to marks and initializers * for rendering. The scales property exposes all the scale definitions. */ -export type ScaleFunctions = { - [key in ScaleName]?: (value: any) => any; -} & {scales: {[key in ScaleName]?: Scale}}; +export type ScaleFunctions = {[key in ScaleName]?: (value: any) => any} & {scales: {[key in ScaleName]?: Scale}}; /** * The supported scale types. For quantitative data, one of: From a52a16fef9fa34338e4ffb5157aad018d1d26af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 17:05:02 +0200 Subject: [PATCH 05/12] reverting this, will be tracked as #1812 --- src/scales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales.js b/src/scales.js index 5c8b89a4e1..ac190b905d 100644 --- a/src/scales.js +++ b/src/scales.js @@ -532,7 +532,7 @@ function exposeScale({scale, type, domain, range, interpolate, interval, transfo const unknown = scale.unknown ? scale.unknown() : undefined; return { type, - domain: [...new Set(domain)], // defensive copy, ensure uniqueness + domain: slice(domain), // defensive copy ...(range !== undefined && {range: slice(range)}), // defensive copy ...(transform !== undefined && {transform}), ...(percent && {percent}), // only exposed if truthy From ebc160488eb415db8926c4c4b283b456f0014932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 16 Aug 2023 17:11:36 +0200 Subject: [PATCH 06/12] revert --- test/scales/scales-test.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 108b6c208b..fd6e260ab8 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -2102,21 +2102,6 @@ it("plot(…).scale(name).apply and invert return the expected functions", () => ]); }); -it("plot(…).scale(name) returns a deduplicated domain", () => { - const letters = "abbbcaabbcc"; - const plot = Plot.dotX(letters).plot({x: {domain: letters}}); - scaleEqual(plot.scale("x"), { - align: 0.5, - bandwidth: 0, - domain: ["a", "b", "c"], - padding: 0.5, - range: [20, 620], - round: true, - step: 200, - type: "point" - }); -}); - // Given a plot specification (or, as shorthand, an array of marks or a single // mark), asserts that the given named scales, when materialized from the first // plot and used to produce a second plot, produce the same output and the same From 4282d7a64ab75bb5d8cae835d61317d6e7e1bd07 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 10:37:03 -0700 Subject: [PATCH 07/12] more uniform handling of identity scales --- src/scales.js | 24 ++++++++++++------------ src/scales/index.js | 4 ++++ src/scales/quantitative.js | 13 ++++++++++--- test/scales/scales-test.js | 2 +- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/scales.js b/src/scales.js index ac190b905d..c066b925f3 100644 --- a/src/scales.js +++ b/src/scales.js @@ -103,13 +103,11 @@ export function createScaleFunctions(descriptors) { 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; - } + 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; } @@ -366,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: @@ -526,13 +524,15 @@ export function exposeScales(scales) { // 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, ...(range !== undefined && {range})}; +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 diff --git a/src/scales/index.js b/src/scales/index.js index 2e624a709a..ee2f2b2688 100644 --- a/src/scales/index.js +++ b/src/scales/index.js @@ -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; +} diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 71e031387f..02a21dfbde 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -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); @@ -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) { diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index fd6e260ab8..94f8d7c1d8 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -2080,7 +2080,7 @@ it("plot(…).scale(name) reflects the given transform", async () => { it("plot(…).scale(name) can return an identity scale, ignoring all other options", () => { const plot = Plot.dot([1, 2], {x: (d) => d, fill: (d) => d}).plot({x: {type: "identity"}, color: {type: "identity"}}); - scaleEqual(plot.scale("x"), {range: [20, 620], type: "identity"}); + scaleEqual(plot.scale("x"), {domain: [20, 620], range: [20, 620], type: "identity"}); scaleEqual(plot.scale("color"), {type: "identity"}); }); From ad43e422698113469f5d0bc0c9d37714106fc790 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 10:48:28 -0700 Subject: [PATCH 08/12] =?UTF-8?q?don=E2=80=99t=20expose=20domain=20and=20r?= =?UTF-8?q?ange=20for=20identity=20(yet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scales.js | 4 ++-- src/scales/quantitative.js | 5 +---- test/scales/scales-test.js | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/scales.js b/src/scales.js index c066b925f3..cbac3d4aa6 100644 --- a/src/scales.js +++ b/src/scales.js @@ -528,11 +528,11 @@ function exposeScale({scale, type, range, domain, interpolate, interval, transfo // 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; + if (type === "identity") return {type: "identity", apply: (d) => d, invert: (d) => d}; const unknown = scale.unknown ? scale.unknown() : undefined; return { type, - ...(domain !== undefined && {domain: slice(domain)}), // defensive copy + domain: slice(domain), // defensive copy ...(range !== undefined && {range: slice(range)}), // defensive copy ...(transform !== undefined && {transform}), ...(percent && {percent}), // only exposed if truthy diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 02a21dfbde..0625ffcf01 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -261,11 +261,8 @@ function isOrdered(domain, sign) { // 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}; + return {type: "identity", scale: hasNumericRange(registry.get(key)) ? scaleIdentity() : (d) => d}; } export function inferDomain(channels, f = finite) { diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 94f8d7c1d8..2303cfc2ef 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -2080,7 +2080,7 @@ it("plot(…).scale(name) reflects the given transform", async () => { it("plot(…).scale(name) can return an identity scale, ignoring all other options", () => { const plot = Plot.dot([1, 2], {x: (d) => d, fill: (d) => d}).plot({x: {type: "identity"}, color: {type: "identity"}}); - scaleEqual(plot.scale("x"), {domain: [20, 620], range: [20, 620], type: "identity"}); + scaleEqual(plot.scale("x"), {type: "identity"}); scaleEqual(plot.scale("color"), {type: "identity"}); }); From ded5734b506e5d6b99d458167e183b8e56f826cc Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 10:50:05 -0700 Subject: [PATCH 09/12] remove obsolete comment --- src/scales.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/scales.js b/src/scales.js index cbac3d4aa6..377761e392 100644 --- a/src/scales.js +++ b/src/scales.js @@ -525,9 +525,6 @@ export function exposeScales(scales) { // 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, 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") return {type: "identity", apply: (d) => d, invert: (d) => d}; const unknown = scale.unknown ? scale.unknown() : undefined; return { From 422c6ba08481a6ba0f1d0470424254b4bad1c778 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 10:50:28 -0700 Subject: [PATCH 10/12] minimize diff --- src/scales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales.js b/src/scales.js index 377761e392..18d858fb99 100644 --- a/src/scales.js +++ b/src/scales.js @@ -524,7 +524,7 @@ export function exposeScales(scales) { // 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, range, domain, interpolate, interval, transform, percent, pivot}) { +function exposeScale({scale, type, domain, range, interpolate, interval, transform, percent, pivot}) { if (type === "identity") return {type: "identity", apply: (d) => d, invert: (d) => d}; const unknown = scale.unknown ? scale.unknown() : undefined; return { From a77c0a2500fd5a48a748d85072cec9ca862103b7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 11:28:57 -0700 Subject: [PATCH 11/12] rewrite as unit tests --- test/output/renderInstantiatedScales.svg | 270 ----------------------- test/plots/index.ts | 1 - test/plots/render.ts | 37 ---- test/scales/scales-test.js | 60 +++++ 4 files changed, 60 insertions(+), 308 deletions(-) delete mode 100644 test/output/renderInstantiatedScales.svg delete mode 100644 test/plots/render.ts diff --git a/test/output/renderInstantiatedScales.svg b/test/output/renderInstantiatedScales.svg deleted file mode 100644 index ec7b3e066a..0000000000 --- a/test/output/renderInstantiatedScales.svg +++ /dev/null @@ -1,270 +0,0 @@ - - - - - - - - - - - - - - 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 f50407b08f..65c6cfb5c6 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -240,7 +240,6 @@ 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 deleted file mode 100644 index 2e173775ae..0000000000 --- a/test/plots/render.ts +++ /dev/null @@ -1,37 +0,0 @@ -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]}}); -} diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 2303cfc2ef..6e5c6125a2 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -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; + } + ] + }); +}); + // Given a plot specification (or, as shorthand, an array of marks or a single // mark), asserts that the given named scales, when materialized from the first // plot and used to produce a second plot, produce the same output and the same From 63f9af58c5ab143dc1ab83cd17e390a9643d3efa Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 16 Aug 2023 11:30:05 -0700 Subject: [PATCH 12/12] whole word style --- src/scales.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scales.js b/src/scales.js index 18d858fb99..5bc4865f04 100644 --- a/src/scales.js +++ b/src/scales.js @@ -100,9 +100,9 @@ export function createScales( 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); + 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;