From a86edf893e1ec53a180bc639eb31f79e74ac5de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 29 May 2021 14:59:36 +0200 Subject: [PATCH 01/20] checkpoint --- README.md | 16 ++++++++++++++++ src/plot.js | 18 ++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e992997f7..be5609e9b8 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,22 @@ Plot.plot({ }) ``` +All the scales are exposed as the *scales* property of the plot. We recommend using *scale*.copy if making any use of these scales that might mutate them. + +```js +color = Plot.plot({…}).scales.color; +color.range() // ["red", "blue"] +``` + +And, to reuse the scale in another plot: + +```js +Plot.plot({ + color: { domain: color.copy().domain(), range: color.copy().range() } +}) +``` + + ### Position options The position scales (*x*, *y*, *fx*, and *fy*) support additional options: diff --git a/src/plot.js b/src/plot.js index 8350bb4347..f421b35639 100644 --- a/src/plot.js +++ b/src/plot.js @@ -88,12 +88,8 @@ export function plot(options = {}) { if (node != null) svg.appendChild(node); } - // Wrap the plot in a figure with a caption, if desired. - if (caption == null) return svg; - const figure = document.createElement("figure"); - figure.appendChild(svg); - const figcaption = figure.appendChild(document.createElement("figcaption")); - figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption)); + const figure = wrap(svg, {caption}); + figure.scales = scales; return figure; } @@ -143,3 +139,13 @@ function autoHeight({y, fy, fx}) { const ny = y ? (y.type === "ordinal" ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1; return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60; } + +// Wrap the plot in a figure with a caption, if desired. +function wrap(svg, {caption}) { + if (caption == null) return svg; + const figure = document.createElement("figure"); + figure.appendChild(svg); + const figcaption = figure.appendChild(document.createElement("figcaption")); + figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption)); + return figure; +} From d3dc27da0b2a86279d34b36c1ea2ed04f0483ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 31 May 2021 17:13:09 +0200 Subject: [PATCH 02/20] expose scales as a reusable Plot scale options object --- src/axes.js | 11 +++ src/plot.js | 9 ++- src/scales.js | 57 ++++++++++---- test/scales/scales-test.js | 151 +++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 test/scales/scales-test.js diff --git a/src/axes.js b/src/axes.js index 8eb04c6917..255a50661d 100644 --- a/src/axes.js +++ b/src/axes.js @@ -37,6 +37,7 @@ function autoAxisTicksK(scale, axis, k) { } // Mutates axis.{label,labelAnchor,labelOffset}! +// Mutates scale.label! export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) { if (fx) { autoAxisLabelsX(fx, scales.fx, channels.get("fx")); @@ -77,6 +78,7 @@ function autoAxisLabelsX(axis, scale, channels) { if (axis.label === undefined) { axis.label = inferLabel(channels, scale, axis, "x"); } + scale.label = axis.label; } function autoAxisLabelsY(axis, opposite, scale, channels) { @@ -88,6 +90,15 @@ function autoAxisLabelsY(axis, opposite, scale, channels) { if (axis.label === undefined) { axis.label = inferLabel(channels, scale, axis, "y"); } + scale.label = axis.label; +} + +export function autoScaleLabel(scale, channels, {color} = {}) { + if (scale === undefined) return; + if (color !== undefined) scale.label = color.label; + if (scale.label === undefined) { + scale.label = inferLabel(channels, scale, {}); + } } // Channels can have labels; if all the channels for a given scale are diff --git a/src/plot.js b/src/plot.js index f421b35639..d38c1f5a87 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,8 +1,8 @@ import {create} from "d3"; -import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; +import {Axes, autoAxisTicks, autoAxisLabels, autoScaleLabel} from "./axes.js"; import {facets} from "./facet.js"; import {values} from "./mark.js"; -import {Scales, autoScaleRange} from "./scales.js"; +import {Scales, autoScaleRange, exposeScales} from "./scales.js"; import {offset} from "./style.js"; export function plot(options = {}) { @@ -52,6 +52,9 @@ export function plot(options = {}) { autoScaleRange(scaleDescriptors, dimensions); autoAxisTicks(scaleDescriptors, axes); autoAxisLabels(scaleChannels, scaleDescriptors, axes, dimensions); + for (const key of ["color", "r", "opacity"]) { + autoScaleLabel(scaleDescriptors[key], scaleChannels.get(key), options); + } // Normalize the options. options = {...scaleDescriptors, ...dimensions}; @@ -89,7 +92,7 @@ export function plot(options = {}) { } const figure = wrap(svg, {caption}); - figure.scales = scales; + figure.scales = (key) => exposeScales(scaleDescriptors, key); return figure; } diff --git a/src/scales.js b/src/scales.js index 6fe7349945..6da233976c 100644 --- a/src/scales.js +++ b/src/scales.js @@ -57,22 +57,26 @@ function autoScaleRound(scale) { } function Scale(key, channels = [], options = {}) { - switch (inferScaleType(key, channels, options)) { - case "diverging": return ScaleDiverging(key, channels, options); - case "categorical": case "ordinal": return ScaleOrdinal(key, channels, options); - case "cyclical": case "sequential": case "linear": return ScaleLinear(key, channels, options); - case "sqrt": return ScalePow(key, channels, {...options, exponent: 0.5}); - case "pow": return ScalePow(key, channels, options); - case "log": return ScaleLog(key, channels, options); - case "symlog": return ScaleSymlog(key, channels, options); - case "utc": return ScaleUtc(key, channels, options); - case "time": return ScaleTime(key, channels, options); - case "point": return ScalePoint(key, channels, options); - case "band": return ScaleBand(key, channels, options); - case "identity": return registry.get(key) === position ? ScaleIdentity(key, channels, options) : undefined; - case undefined: return; + const type = inferScaleType(key, channels, options); + let scale; + switch (type) { + case "diverging": scale = ScaleDiverging(key, channels, options); break; + case "categorical": case "ordinal": scale = ScaleOrdinal(key, channels, options); break; + case "cyclical": case "sequential": case "linear": scale = ScaleLinear(key, channels, options); break; + case "sqrt": scale = ScalePow(key, channels, {...options, exponent: 0.5}); break; + case "pow": scale = ScalePow(key, channels, options); break; + case "log": scale = ScaleLog(key, channels, options); break; + case "symlog": scale = ScaleSymlog(key, channels, options); break; + case "utc": scale = ScaleUtc(key, channels, options); break; + case "time": scale = ScaleTime(key, channels, options); break; + case "point": scale = ScalePoint(key, channels, options); break; + case "band": scale = ScaleBand(key, channels, options); break; + case "identity": scale = registry.get(key) === position ? ScaleIdentity(key, channels, options) : undefined; break; + case undefined: break; default: throw new Error(`unknown scale type: ${options.type}`); } + if (scale) scale.scale.type = type; + return scale; } function inferScaleType(key, channels, {type, domain, range}) { @@ -105,3 +109,28 @@ function inferScaleType(key, channels, {type, domain, range}) { function asOrdinalType(key) { return registry.get(key) === position ? "point" : "ordinal"; } + +// prepare scales for exposure through the plot's scales() function +export function exposeScales(scaleDescriptors, key) { + if (key === undefined) { + return Object.fromEntries( + Object.entries(scaleDescriptors) + .map(([key, descriptor]) => [key, exposeScale(descriptor)]) + ); + } + if (key in scaleDescriptors) { + return exposeScale(scaleDescriptors[key]); + } +} + +function exposeScale({scale, label}) { + return { + domain: scale.domain(), + range: scale.range(), + ...scale.interpolate && {interpolate: scale.interpolate()}, + ...label !== undefined && {label}, + ...scale.type && {type: scale.type}, + ...scale.clamp && scale.clamp() && {clamp: true}, + scale + }; +} diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js new file mode 100644 index 0000000000..f878d4b568 --- /dev/null +++ b/test/scales/scales-test.js @@ -0,0 +1,151 @@ +import * as Plot from "@observablehq/plot"; +import tape from "tape-await"; +import {JSDOM} from "jsdom"; +const {window} = new JSDOM(""); +global.document = window.document; + +tape("plot(…).scales() exposes the plot’s scales", test => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + test.equal(typeof plot.scales, "function"); + const scales = plot.scales(); + test.equal(Object.entries(scales).length, 2); + test.assert("x" in scales); + test.assert("y" in scales); +}); + +tape("plot(…).scales('x') exposes the plot’s x scale", test => { + const x = Plot.dot([1, 2], {x: d => d}).plot().scales('x'); + test.deepEqual(x.domain, [1, 2]); + test.deepEqual(x.range, [20, 620]); + test.equal(typeof x.interpolate, "function"); + test.equal(x.type, "linear"); + test.equal(x.clamp, undefined); + test.equal(typeof x.scale, "function"); +}); + +tape("plot(…).scales('y') exposes the plot’s y scale", test => { + const y0 = Plot.dot([1, 2], {x: d => d}).plot().scales('y'); + test.equal(y0, undefined); + const y = Plot.dot([1, 2], {y: d => d}).plot().scales('y'); + test.deepEqual(y.domain, [1, 2]); + test.deepEqual(y.range, [380, 20]); + test.equal(typeof y.interpolate, "function"); + test.equal(y.type, "linear"); + test.equal(y.clamp, undefined); + test.equal(typeof y.scale, "function"); +}); + +tape("plot(…).scales('fx') exposes the plot’s fx scale", test => { + const fx0 = Plot.dot([1, 2], {x: d => d}).plot().scales('fx'); + test.equal(fx0, undefined); + const data = [1, 2]; + const fx = Plot.dot(data, {y: d => d}).plot({facet: {data, x: data}}).scales('fx'); + test.deepEqual(fx.domain, [1, 2]); + test.deepEqual(fx.range, [40, 620]); + test.equal(typeof fx.interpolate, "undefined"); + test.equal(fx.type, "band"); + test.equal(fx.clamp, undefined); + test.equal(typeof fx.scale, "function"); +}); + +tape("plot(…).scales('fy') exposes the plot’s fy scale", test => { + const fy0 = Plot.dot([1, 2], {x: d => d}).plot().scales('fy'); + test.equal(fy0, undefined); + const data = [1, 2]; + const fy = Plot.dot(data, {y: d => d}).plot({facet: {data, y: data}}).scales('fy'); + test.deepEqual(fy.domain, [1, 2]); + test.deepEqual(fy.range, [20, 380]); + test.equal(typeof fy.interpolate, "undefined"); + test.equal(fy.type, "band"); + test.equal(fy.clamp, undefined); + test.equal(typeof fy.scale, "function"); +}); + +tape("plot(…).scales('color') exposes a continuous color scale", test => { + const color0 = Plot.dot([1, 2], {x: d => d}).plot().scales('color'); + test.equal(color0, undefined); + const data = [1, 2, 3, 4, 5]; + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales('color'); + test.deepEqual(color.domain, [1, 5]); + test.deepEqual(color.range, [0, 1]); + test.equal(typeof color.interpolate, "function"); + test.equal(color.type, "linear"); + test.equal(color.clamp, undefined); + test.equal(typeof color.scale, "function"); +}); + +tape("plot(…).scales('color') exposes an ordinal color scale", test => { + const data = ["a", "b", "c", "d"]; + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales('color'); + test.deepEqual(color.domain, data); + test.deepEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); + test.equal(typeof color.interpolate, "undefined"); + test.equal(color.type, "ordinal"); + test.equal(color.clamp, undefined); + test.equal(typeof color.scale, "function"); +}); + +tape("plot(…).scales('r') exposes a radius scale", test => { + const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales('r'); + test.equal(r0, undefined); + const data = [1, 2, 3, 4, 9]; + const r = Plot.dot(data, {r: d => d}).plot().scales('r'); + test.deepEqual(r.domain, [0, 9]); + test.deepEqual(r.range, [0, Math.sqrt(40.5)]); + test.equal(typeof r.interpolate, "function"); + test.equal(r.type, "sqrt"); + test.equal(r.clamp, undefined); + test.equal(typeof r.scale, "function"); +}); + +tape("plot(…).scales('opacity') exposes a linear scale", test => { + const opacity0 = Plot.dot([1, 2], {x: d => d}).plot().scales('opacity'); + test.equal(opacity0, undefined); + const data = [1, 2, 3, 4, 9]; + const opacity = Plot.dot(data, {fillOpacity: d => d}).plot().scales('opacity'); + test.deepEqual(opacity.domain, [0, 9]); + test.deepEqual(opacity.range, [0, 1]); + test.equal(typeof opacity.interpolate, "function"); + test.equal(opacity.type, "linear"); + test.equal(opacity.clamp, undefined); + test.equal(typeof opacity.scale, "function"); +}); + +tape("plot(…).scales expose inset domain", test => { + test.deepEqual(scaleOpt({inset: null}).range, [20, 620]); + test.deepEqual(scaleOpt({inset: 7}).range, [27, 613]); +}); + +tape("plot(…).scales expose clamp", test => { + test.equal(scaleOpt({clamp: false}).clamp, undefined); + test.equal(scaleOpt({clamp: true}).clamp, true); +}); + +tape("plot(…).scales expose rounded scales", test => { + test.equal(scaleOpt({round: false}).scale(Math.SQRT2), 144.26406871192853); + test.equal(scaleOpt({round: true}).scale(Math.SQRT2), 144); + test.equal(scaleOpt({round: true}).interpolate(0, 100)(Math.SQRT1_2), 71); +}); + +tape("plot(…).scales expose label", test => { + test.equal(scaleOpt({}).label, "x →"); + test.equal(scaleOpt({label: "value"}).label, "value"); +}); + +tape("plot(…).scales expose color label", test => { + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot().scales("color"); + test.equal(x.label, "x"); + const y = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot({color: {label: "y"}}).scales("color"); + test.equal(y.label, "y"); +}); + +tape("plot(…).scales expose radius label", test => { + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot().scales("r"); + test.equal(x.label, "x"); + const r = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot({color: {label: "radius"}}).scales("color"); + test.equal(r.label, "radius"); +}); + +function scaleOpt(x) { + return Plot.dot([{x: 1}, {x: 2}, {x: 3}], {x: "x"}).plot({x}).scales("x"); +} From 85ae2c23a5b4c29e3b1be5d73f21ca854b22dbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 31 May 2021 21:38:30 +0200 Subject: [PATCH 03/20] fix 'r' label --- src/axes.js | 4 ++-- src/plot.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/axes.js b/src/axes.js index 255a50661d..13410d23ff 100644 --- a/src/axes.js +++ b/src/axes.js @@ -93,9 +93,9 @@ function autoAxisLabelsY(axis, opposite, scale, channels) { scale.label = axis.label; } -export function autoScaleLabel(scale, channels, {color} = {}) { +export function autoScaleLabel(scale, channels, options) { if (scale === undefined) return; - if (color !== undefined) scale.label = color.label; + if (options !== undefined) scale.label = options.label; if (scale.label === undefined) { scale.label = inferLabel(channels, scale, {}); } diff --git a/src/plot.js b/src/plot.js index d38c1f5a87..3f879be9d9 100644 --- a/src/plot.js +++ b/src/plot.js @@ -53,7 +53,7 @@ export function plot(options = {}) { autoAxisTicks(scaleDescriptors, axes); autoAxisLabels(scaleChannels, scaleDescriptors, axes, dimensions); for (const key of ["color", "r", "opacity"]) { - autoScaleLabel(scaleDescriptors[key], scaleChannels.get(key), options); + autoScaleLabel(scaleDescriptors[key], scaleChannels.get(key), options[key]); } // Normalize the options. From 295942e94e58b725e59da1712ac1687ecfa0b4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 1 Jun 2021 05:44:21 +0200 Subject: [PATCH 04/20] scale:legend option accepts a function that receives the scale; if that function returns a DOM node, adds it to the figure. --- src/plot.js | 32 ++++++++++++++++++++++---------- src/scales.js | 5 ++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/plot.js b/src/plot.js index 3f879be9d9..1d177db123 100644 --- a/src/plot.js +++ b/src/plot.js @@ -91,9 +91,7 @@ export function plot(options = {}) { if (node != null) svg.appendChild(node); } - const figure = wrap(svg, {caption}); - figure.scales = (key) => exposeScales(scaleDescriptors, key); - return figure; + return wrap(svg, scaleDescriptors, {caption}); } function Dimensions( @@ -144,11 +142,25 @@ function autoHeight({y, fy, fx}) { } // Wrap the plot in a figure with a caption, if desired. -function wrap(svg, {caption}) { - if (caption == null) return svg; - const figure = document.createElement("figure"); - figure.appendChild(svg); - const figcaption = figure.appendChild(document.createElement("figcaption")); - figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption)); - return figure; +function wrap(svg, scaleDescriptors, {caption} = {}) { + const scales = (key) => exposeScales(scaleDescriptors, key); + const legends = []; + for (let key in scaleDescriptors) { + const {legend} = scaleDescriptors[key]; + if (typeof legend === "function") { + const l = legend(scales(key)); + if (l instanceof Node) legends.push(l); + } + } + if (caption == null && legends.length === 0) { + return svg.scales = scales, svg; + } + const figure = create("figure"); + for (const legend of legends) figure.append(() => legend); + figure.append(() => svg); + if (caption != null) { + figure.append("figcaption") + .append(() => caption instanceof Node ? caption : document.createTextNode(caption)); + } + return Object.assign(figure.node(), {scales}); } diff --git a/src/scales.js b/src/scales.js index 6da233976c..4e9d60803c 100644 --- a/src/scales.js +++ b/src/scales.js @@ -75,7 +75,10 @@ function Scale(key, channels = [], options = {}) { case undefined: break; default: throw new Error(`unknown scale type: ${options.type}`); } - if (scale) scale.scale.type = type; + if (scale) { + if (options.legend) scale.legend = options.legend; + scale.scale.type = type; + } return scale; } From 4169fcfec7ba145ec2f68418395b725ff3d3b9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 1 Jun 2021 05:52:41 +0200 Subject: [PATCH 05/20] simplify: always compute scales --- src/plot.js | 4 +-- src/scales.js | 15 ++++------ test/scales/scales-test.js | 60 +++++++++++++++++++------------------- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/plot.js b/src/plot.js index 1d177db123..4bc999ff8f 100644 --- a/src/plot.js +++ b/src/plot.js @@ -143,12 +143,12 @@ function autoHeight({y, fy, fx}) { // Wrap the plot in a figure with a caption, if desired. function wrap(svg, scaleDescriptors, {caption} = {}) { - const scales = (key) => exposeScales(scaleDescriptors, key); + const scales = exposeScales(scaleDescriptors); const legends = []; for (let key in scaleDescriptors) { const {legend} = scaleDescriptors[key]; if (typeof legend === "function") { - const l = legend(scales(key)); + const l = legend(scales[key]); if (l instanceof Node) legends.push(l); } } diff --git a/src/scales.js b/src/scales.js index 4e9d60803c..0847859094 100644 --- a/src/scales.js +++ b/src/scales.js @@ -114,16 +114,11 @@ function asOrdinalType(key) { } // prepare scales for exposure through the plot's scales() function -export function exposeScales(scaleDescriptors, key) { - if (key === undefined) { - return Object.fromEntries( - Object.entries(scaleDescriptors) - .map(([key, descriptor]) => [key, exposeScale(descriptor)]) - ); - } - if (key in scaleDescriptors) { - return exposeScale(scaleDescriptors[key]); - } +export function exposeScales(scaleDescriptors) { + return Object.fromEntries( + Object.entries(scaleDescriptors) + .map(([key, descriptor]) => [key, exposeScale(descriptor)]) + ); } function exposeScale({scale, label}) { diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index f878d4b568..f437b75172 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -4,17 +4,17 @@ import {JSDOM} from "jsdom"; const {window} = new JSDOM(""); global.document = window.document; -tape("plot(…).scales() exposes the plot’s scales", test => { +tape("plot(…).scales exposes the plot’s scales", test => { const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); - test.equal(typeof plot.scales, "function"); - const scales = plot.scales(); + test.equal(typeof plot.scales, "object"); + const scales = plot.scales; test.equal(Object.entries(scales).length, 2); test.assert("x" in scales); test.assert("y" in scales); }); -tape("plot(…).scales('x') exposes the plot’s x scale", test => { - const x = Plot.dot([1, 2], {x: d => d}).plot().scales('x'); +tape("plot(…).scales.x exposes the plot’s x scale", test => { + const x = Plot.dot([1, 2], {x: d => d}).plot().scales.x; test.deepEqual(x.domain, [1, 2]); test.deepEqual(x.range, [20, 620]); test.equal(typeof x.interpolate, "function"); @@ -23,10 +23,10 @@ tape("plot(…).scales('x') exposes the plot’s x scale", test => { test.equal(typeof x.scale, "function"); }); -tape("plot(…).scales('y') exposes the plot’s y scale", test => { - const y0 = Plot.dot([1, 2], {x: d => d}).plot().scales('y'); +tape("plot(…).scales.y exposes the plot’s y scale", test => { + const y0 = Plot.dot([1, 2], {x: d => d}).plot().scales.y; test.equal(y0, undefined); - const y = Plot.dot([1, 2], {y: d => d}).plot().scales('y'); + const y = Plot.dot([1, 2], {y: d => d}).plot().scales.y; test.deepEqual(y.domain, [1, 2]); test.deepEqual(y.range, [380, 20]); test.equal(typeof y.interpolate, "function"); @@ -35,11 +35,11 @@ tape("plot(…).scales('y') exposes the plot’s y scale", test => { test.equal(typeof y.scale, "function"); }); -tape("plot(…).scales('fx') exposes the plot’s fx scale", test => { - const fx0 = Plot.dot([1, 2], {x: d => d}).plot().scales('fx'); +tape("plot(…).scales.fx exposes the plot’s fx scale", test => { + const fx0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fx; test.equal(fx0, undefined); const data = [1, 2]; - const fx = Plot.dot(data, {y: d => d}).plot({facet: {data, x: data}}).scales('fx'); + const fx = Plot.dot(data, {y: d => d}).plot({facet: {data, x: data}}).scales.fx; test.deepEqual(fx.domain, [1, 2]); test.deepEqual(fx.range, [40, 620]); test.equal(typeof fx.interpolate, "undefined"); @@ -48,11 +48,11 @@ tape("plot(…).scales('fx') exposes the plot’s fx scale", test => { test.equal(typeof fx.scale, "function"); }); -tape("plot(…).scales('fy') exposes the plot’s fy scale", test => { - const fy0 = Plot.dot([1, 2], {x: d => d}).plot().scales('fy'); +tape("plot(…).scales.fy exposes the plot’s fy scale", test => { + const fy0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fy; test.equal(fy0, undefined); const data = [1, 2]; - const fy = Plot.dot(data, {y: d => d}).plot({facet: {data, y: data}}).scales('fy'); + const fy = Plot.dot(data, {y: d => d}).plot({facet: {data, y: data}}).scales.fy; test.deepEqual(fy.domain, [1, 2]); test.deepEqual(fy.range, [20, 380]); test.equal(typeof fy.interpolate, "undefined"); @@ -61,11 +61,11 @@ tape("plot(…).scales('fy') exposes the plot’s fy scale", test => { test.equal(typeof fy.scale, "function"); }); -tape("plot(…).scales('color') exposes a continuous color scale", test => { - const color0 = Plot.dot([1, 2], {x: d => d}).plot().scales('color'); +tape("plot(…).scales.color exposes a continuous color scale", test => { + const color0 = Plot.dot([1, 2], {x: d => d}).plot().scales.color; test.equal(color0, undefined); const data = [1, 2, 3, 4, 5]; - const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales('color'); + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales.color; test.deepEqual(color.domain, [1, 5]); test.deepEqual(color.range, [0, 1]); test.equal(typeof color.interpolate, "function"); @@ -74,9 +74,9 @@ tape("plot(…).scales('color') exposes a continuous color scale", test => { test.equal(typeof color.scale, "function"); }); -tape("plot(…).scales('color') exposes an ordinal color scale", test => { +tape("plot(…).scales.color exposes an ordinal color scale", test => { const data = ["a", "b", "c", "d"]; - const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales('color'); + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales.color; test.deepEqual(color.domain, data); test.deepEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); test.equal(typeof color.interpolate, "undefined"); @@ -85,11 +85,11 @@ tape("plot(…).scales('color') exposes an ordinal color scale", test => { test.equal(typeof color.scale, "function"); }); -tape("plot(…).scales('r') exposes a radius scale", test => { - const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales('r'); +tape("plot(…).scales.r exposes a radius scale", test => { + const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales.r; test.equal(r0, undefined); const data = [1, 2, 3, 4, 9]; - const r = Plot.dot(data, {r: d => d}).plot().scales('r'); + const r = Plot.dot(data, {r: d => d}).plot().scales.r; test.deepEqual(r.domain, [0, 9]); test.deepEqual(r.range, [0, Math.sqrt(40.5)]); test.equal(typeof r.interpolate, "function"); @@ -98,11 +98,11 @@ tape("plot(…).scales('r') exposes a radius scale", test => { test.equal(typeof r.scale, "function"); }); -tape("plot(…).scales('opacity') exposes a linear scale", test => { - const opacity0 = Plot.dot([1, 2], {x: d => d}).plot().scales('opacity'); +tape("plot(…).scales.opacity exposes a linear scale", test => { + const opacity0 = Plot.dot([1, 2], {x: d => d}).plot().scales.opacity; test.equal(opacity0, undefined); const data = [1, 2, 3, 4, 9]; - const opacity = Plot.dot(data, {fillOpacity: d => d}).plot().scales('opacity'); + const opacity = Plot.dot(data, {fillOpacity: d => d}).plot().scales.opacity; test.deepEqual(opacity.domain, [0, 9]); test.deepEqual(opacity.range, [0, 1]); test.equal(typeof opacity.interpolate, "function"); @@ -133,19 +133,19 @@ tape("plot(…).scales expose label", test => { }); tape("plot(…).scales expose color label", test => { - const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot().scales("color"); + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot().scales.color; test.equal(x.label, "x"); - const y = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot({color: {label: "y"}}).scales("color"); + const y = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot({color: {label: "y"}}).scales.color; test.equal(y.label, "y"); }); tape("plot(…).scales expose radius label", test => { - const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot().scales("r"); + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot().scales.r; test.equal(x.label, "x"); - const r = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot({color: {label: "radius"}}).scales("color"); + const r = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot({r: {label: "radius"}}).scales.r; test.equal(r.label, "radius"); }); function scaleOpt(x) { - return Plot.dot([{x: 1}, {x: 2}, {x: 3}], {x: "x"}).plot({x}).scales("x"); + return Plot.dot([{x: 1}, {x: 2}, {x: 3}], {x: "x"}).plot({x}).scales.x; } From 228565b251e26ba4ced4f47604e4b885fca7c8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Jun 2021 16:35:43 +0200 Subject: [PATCH 06/20] The plot's scales object contains the minimum set of options that can be used to recreate the scale in another plot. In particular it doesn't show the (D3) scale anymore. Instead, a convenience function Plot.scale(options) is provided, that will turn a scale *options* object into a D3 scale, ready to use in another context. (This also clears the question of mutability.) --- README.md | 9 ++++++--- src/index.js | 1 + src/scales.js | 7 +++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index be5609e9b8..3ef2f8fee1 100644 --- a/README.md +++ b/README.md @@ -200,21 +200,24 @@ Plot.plot({ }) ``` -All the scales are exposed as the *scales* property of the plot. We recommend using *scale*.copy if making any use of these scales that might mutate them. +All the scale definitions are exposed as the *scales* property of the plot. ```js color = Plot.plot({…}).scales.color; -color.range() // ["red", "blue"] +color.range // ["red", "blue"] ``` And, to reuse the scale in another plot: ```js +const plot1 = Plot.plot(…); + Plot.plot({ - color: { domain: color.copy().domain(), range: color.copy().range() } + color: plot:.scales.color }) ``` +Plot.*scale*(*options*) returns a [D3 scale](https://github.com/d3/d3-scale) that matches the Plot *options* object. ### Position options diff --git a/src/index.js b/src/index.js index 1583d4a352..8c5ce92e40 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ export {plot} from "./plot.js"; export {Mark, valueof} from "./mark.js"; +export {scale} from "./scales.js"; export {Area, area, areaX, areaY} from "./marks/area.js"; export {BarX, BarY, barX, barY} from "./marks/bar.js"; export {Cell, cell, cellX, cellY} from "./marks/cell.js"; diff --git a/src/scales.js b/src/scales.js index 0847859094..418a76d8d4 100644 --- a/src/scales.js +++ b/src/scales.js @@ -82,6 +82,10 @@ function Scale(key, channels = [], options = {}) { return scale; } +export function scale(options) { + return Scale(undefined, undefined, options).scale; +} + function inferScaleType(key, channels, {type, domain, range}) { if (key === "fx" || key === "fy") return "band"; if (type !== undefined) { @@ -128,7 +132,6 @@ function exposeScale({scale, label}) { ...scale.interpolate && {interpolate: scale.interpolate()}, ...label !== undefined && {label}, ...scale.type && {type: scale.type}, - ...scale.clamp && scale.clamp() && {clamp: true}, - scale + ...scale.clamp && scale.clamp() && {clamp: true} }; } From 0c1905ceb3b6593c56b10f369df5861f4297407f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Jun 2021 16:41:05 +0200 Subject: [PATCH 07/20] fix tests --- test/scales/scales-test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index f437b75172..8cae775b5c 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -20,7 +20,7 @@ tape("plot(…).scales.x exposes the plot’s x scale", test => { test.equal(typeof x.interpolate, "function"); test.equal(x.type, "linear"); test.equal(x.clamp, undefined); - test.equal(typeof x.scale, "function"); + test.equal(typeof Plot.scale(x), "function"); }); tape("plot(…).scales.y exposes the plot’s y scale", test => { @@ -32,7 +32,7 @@ tape("plot(…).scales.y exposes the plot’s y scale", test => { test.equal(typeof y.interpolate, "function"); test.equal(y.type, "linear"); test.equal(y.clamp, undefined); - test.equal(typeof y.scale, "function"); + test.equal(typeof Plot.scale(y), "function"); }); tape("plot(…).scales.fx exposes the plot’s fx scale", test => { @@ -45,7 +45,7 @@ tape("plot(…).scales.fx exposes the plot’s fx scale", test => { test.equal(typeof fx.interpolate, "undefined"); test.equal(fx.type, "band"); test.equal(fx.clamp, undefined); - test.equal(typeof fx.scale, "function"); + test.equal(typeof Plot.scale(fx), "function"); }); tape("plot(…).scales.fy exposes the plot’s fy scale", test => { @@ -58,7 +58,7 @@ tape("plot(…).scales.fy exposes the plot’s fy scale", test => { test.equal(typeof fy.interpolate, "undefined"); test.equal(fy.type, "band"); test.equal(fy.clamp, undefined); - test.equal(typeof fy.scale, "function"); + test.equal(typeof Plot.scale(fy), "function"); }); tape("plot(…).scales.color exposes a continuous color scale", test => { @@ -71,7 +71,7 @@ tape("plot(…).scales.color exposes a continuous color scale", test => { test.equal(typeof color.interpolate, "function"); test.equal(color.type, "linear"); test.equal(color.clamp, undefined); - test.equal(typeof color.scale, "function"); + test.equal(typeof Plot.scale(color), "function"); }); tape("plot(…).scales.color exposes an ordinal color scale", test => { @@ -82,7 +82,7 @@ tape("plot(…).scales.color exposes an ordinal color scale", test => { test.equal(typeof color.interpolate, "undefined"); test.equal(color.type, "ordinal"); test.equal(color.clamp, undefined); - test.equal(typeof color.scale, "function"); + test.equal(typeof Plot.scale(color), "function"); }); tape("plot(…).scales.r exposes a radius scale", test => { @@ -95,7 +95,7 @@ tape("plot(…).scales.r exposes a radius scale", test => { test.equal(typeof r.interpolate, "function"); test.equal(r.type, "sqrt"); test.equal(r.clamp, undefined); - test.equal(typeof r.scale, "function"); + test.equal(typeof Plot.scale(r), "function"); }); tape("plot(…).scales.opacity exposes a linear scale", test => { @@ -108,7 +108,7 @@ tape("plot(…).scales.opacity exposes a linear scale", test => { test.equal(typeof opacity.interpolate, "function"); test.equal(opacity.type, "linear"); test.equal(opacity.clamp, undefined); - test.equal(typeof opacity.scale, "function"); + test.equal(typeof Plot.scale(opacity), "function"); }); tape("plot(…).scales expose inset domain", test => { @@ -122,8 +122,8 @@ tape("plot(…).scales expose clamp", test => { }); tape("plot(…).scales expose rounded scales", test => { - test.equal(scaleOpt({round: false}).scale(Math.SQRT2), 144.26406871192853); - test.equal(scaleOpt({round: true}).scale(Math.SQRT2), 144); + test.equal(Plot.scale(scaleOpt({round: false}))(Math.SQRT2), 144.26406871192853); + test.equal(Plot.scale(scaleOpt({round: true}))(Math.SQRT2), 144); test.equal(scaleOpt({round: true}).interpolate(0, 100)(Math.SQRT1_2), 71); }); From ac2137606e0b9680f0c91d6fc306815a20869aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Jun 2021 16:57:16 +0200 Subject: [PATCH 08/20] Update README.md Co-authored-by: Mike Bostock --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ef2f8fee1..714e912274 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ And, to reuse the scale in another plot: const plot1 = Plot.plot(…); Plot.plot({ - color: plot:.scales.color + color: plot1.scales.color }) ``` From 7600f6565a159870e109e17107f7e1679bd4772d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Jun 2021 20:07:18 +0200 Subject: [PATCH 09/20] Update src/scales.js Co-authored-by: Mike Bostock --- src/scales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales.js b/src/scales.js index 418a76d8d4..052ec7853b 100644 --- a/src/scales.js +++ b/src/scales.js @@ -132,6 +132,6 @@ function exposeScale({scale, label}) { ...scale.interpolate && {interpolate: scale.interpolate()}, ...label !== undefined && {label}, ...scale.type && {type: scale.type}, - ...scale.clamp && scale.clamp() && {clamp: true} + ...scale.clamp && {clamp: scale.clamp()} }; } From c15934ed611f2c3d84235f65ff66fe50cdd74b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Jun 2021 20:15:50 +0200 Subject: [PATCH 10/20] fix tests (clamp option) --- test/scales/scales-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 8cae775b5c..6b67c8e58b 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -19,7 +19,7 @@ tape("plot(…).scales.x exposes the plot’s x scale", test => { test.deepEqual(x.range, [20, 620]); test.equal(typeof x.interpolate, "function"); test.equal(x.type, "linear"); - test.equal(x.clamp, undefined); + test.equal(x.clamp, false); test.equal(typeof Plot.scale(x), "function"); }); @@ -31,7 +31,7 @@ tape("plot(…).scales.y exposes the plot’s y scale", test => { test.deepEqual(y.range, [380, 20]); test.equal(typeof y.interpolate, "function"); test.equal(y.type, "linear"); - test.equal(y.clamp, undefined); + test.equal(y.clamp, false); test.equal(typeof Plot.scale(y), "function"); }); @@ -70,7 +70,7 @@ tape("plot(…).scales.color exposes a continuous color scale", test => { test.deepEqual(color.range, [0, 1]); test.equal(typeof color.interpolate, "function"); test.equal(color.type, "linear"); - test.equal(color.clamp, undefined); + test.equal(color.clamp, false); test.equal(typeof Plot.scale(color), "function"); }); @@ -94,7 +94,7 @@ tape("plot(…).scales.r exposes a radius scale", test => { test.deepEqual(r.range, [0, Math.sqrt(40.5)]); test.equal(typeof r.interpolate, "function"); test.equal(r.type, "sqrt"); - test.equal(r.clamp, undefined); + test.equal(r.clamp, false); test.equal(typeof Plot.scale(r), "function"); }); @@ -107,7 +107,7 @@ tape("plot(…).scales.opacity exposes a linear scale", test => { test.deepEqual(opacity.range, [0, 1]); test.equal(typeof opacity.interpolate, "function"); test.equal(opacity.type, "linear"); - test.equal(opacity.clamp, undefined); + test.equal(opacity.clamp, false); test.equal(typeof Plot.scale(opacity), "function"); }); @@ -117,7 +117,7 @@ tape("plot(…).scales expose inset domain", test => { }); tape("plot(…).scales expose clamp", test => { - test.equal(scaleOpt({clamp: false}).clamp, undefined); + test.equal(scaleOpt({clamp: false}).clamp, false); test.equal(scaleOpt({clamp: true}).clamp, true); }); From 0c0d701f2ae480910f578d85ba57ae2b952c6caf Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 9 Jun 2021 10:22:12 -0700 Subject: [PATCH 11/20] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 714e912274..74e5165cfe 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,13 @@ Plot.plot({ }) ``` -Plot.*scale*(*options*) returns a [D3 scale](https://github.com/d3/d3-scale) that matches the Plot *options* object. +#### Plot.scale(*scaleOptions*) + +```js +Plot.scale(plot1.scales.color) +``` + +Returns a [D3 scale](https://github.com/d3/d3-scale) that matches the given Plot scale *options* object. ### Position options From 8ddd39a0be3a1bbb82e6d27f26b7649f915e1b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 9 Jun 2021 22:44:31 +0200 Subject: [PATCH 12/20] remove scale.legend for now --- src/plot.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/plot.js b/src/plot.js index 4bc999ff8f..1e261b0a5f 100644 --- a/src/plot.js +++ b/src/plot.js @@ -144,19 +144,10 @@ function autoHeight({y, fy, fx}) { // Wrap the plot in a figure with a caption, if desired. function wrap(svg, scaleDescriptors, {caption} = {}) { const scales = exposeScales(scaleDescriptors); - const legends = []; - for (let key in scaleDescriptors) { - const {legend} = scaleDescriptors[key]; - if (typeof legend === "function") { - const l = legend(scales[key]); - if (l instanceof Node) legends.push(l); - } - } - if (caption == null && legends.length === 0) { + if (caption == null) { return svg.scales = scales, svg; } const figure = create("figure"); - for (const legend of legends) figure.append(() => legend); figure.append(() => svg); if (caption != null) { figure.append("figcaption") From 5a04ff18b2a83d758e71e3cade944e346e4c0a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 10 Jun 2021 11:20:55 +0200 Subject: [PATCH 13/20] use a lazy getter on each scale ensure we don't compute the scale options twice always return label, clamp, type --- src/plot.js | 22 +++++++++------------- src/scales.js | 20 ++++++++++++-------- test/scales/scales-test.js | 10 ++++++++++ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/plot.js b/src/plot.js index 1e261b0a5f..f984799952 100644 --- a/src/plot.js +++ b/src/plot.js @@ -91,7 +91,7 @@ export function plot(options = {}) { if (node != null) svg.appendChild(node); } - return wrap(svg, scaleDescriptors, {caption}); + return exposeScales(wrap(svg, {caption}), scaleDescriptors); } function Dimensions( @@ -142,16 +142,12 @@ function autoHeight({y, fy, fx}) { } // Wrap the plot in a figure with a caption, if desired. -function wrap(svg, scaleDescriptors, {caption} = {}) { - const scales = exposeScales(scaleDescriptors); - if (caption == null) { - return svg.scales = scales, svg; - } - const figure = create("figure"); - figure.append(() => svg); - if (caption != null) { - figure.append("figcaption") - .append(() => caption instanceof Node ? caption : document.createTextNode(caption)); - } - return Object.assign(figure.node(), {scales}); +function wrap(svg, {caption} = {}) { + if (caption == null) return svg; + const figure = document.createElement("figure"); + figure.appendChild(svg); + const figcaption = document.createElement("figcaption"); + figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption)); + figure.appendChild(figcaption); + return figure; } diff --git a/src/scales.js b/src/scales.js index 052ec7853b..bbe805489f 100644 --- a/src/scales.js +++ b/src/scales.js @@ -76,7 +76,6 @@ function Scale(key, channels = [], options = {}) { default: throw new Error(`unknown scale type: ${options.type}`); } if (scale) { - if (options.legend) scale.legend = options.legend; scale.scale.type = type; } return scale; @@ -118,11 +117,16 @@ function asOrdinalType(key) { } // prepare scales for exposure through the plot's scales() function -export function exposeScales(scaleDescriptors) { - return Object.fromEntries( - Object.entries(scaleDescriptors) - .map(([key, descriptor]) => [key, exposeScale(descriptor)]) - ); +export function exposeScales(figure, scaleDescriptors) { + const scales = figure.scales = {}; + for (const key in scaleDescriptors) { + let cache; + Object.defineProperty(scales, key, { + enumerable: true, + get: () => cache = cache || exposeScale(scaleDescriptors[key]) + }); + } + return figure; } function exposeScale({scale, label}) { @@ -130,8 +134,8 @@ function exposeScale({scale, label}) { domain: scale.domain(), range: scale.range(), ...scale.interpolate && {interpolate: scale.interpolate()}, - ...label !== undefined && {label}, - ...scale.type && {type: scale.type}, + label, + type: scale.type, ...scale.clamp && {clamp: scale.clamp()} }; } diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 6b67c8e58b..da6ba78297 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -13,6 +13,16 @@ tape("plot(…).scales exposes the plot’s scales", test => { test.assert("y" in scales); }); +tape("plot(…).scales[key] is computed once", test => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + test.assert(plot.scales.x === plot.scales.x); +}); + +tape("plot(…).scales[key] is computed lazily", test => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + test.assert(plot.scales.x === plot.scales.x); +}); + tape("plot(…).scales.x exposes the plot’s x scale", test => { const x = Plot.dot([1, 2], {x: d => d}).plot().scales.x; test.deepEqual(x.domain, [1, 2]); From 3cdb63129729125b80a398b75ca8dc55339f855c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 10 Jun 2021 13:06:19 +0200 Subject: [PATCH 14/20] remove comment --- src/scales.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scales.js b/src/scales.js index bbe805489f..32bd216d23 100644 --- a/src/scales.js +++ b/src/scales.js @@ -116,7 +116,6 @@ function asOrdinalType(key) { return registry.get(key) === position ? "point" : "ordinal"; } -// prepare scales for exposure through the plot's scales() function export function exposeScales(figure, scaleDescriptors) { const scales = figure.scales = {}; for (const key in scaleDescriptors) { From 291f6c71d4e8bb7ae800d76dd5ede5cf75dd2e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 11 Jun 2021 19:17:13 +0200 Subject: [PATCH 15/20] =?UTF-8?q?disambiguate=20the=20scale's=20family=20(?= =?UTF-8?q?"temporal",=20"quantitative",=20"ordinal",=20"identity")=20and?= =?UTF-8?q?=20type=20("utc",=20"time"=20/=20"linear",=20"sqrt",=20"log"?= =?UTF-8?q?=E2=80=A6=20/=20"categorical"=20/=20"identity")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/axes.js | 8 ++++---- src/plot.js | 2 +- src/scales.js | 4 ++-- src/scales/ordinal.js | 2 +- src/scales/quantitative.js | 6 +++--- src/scales/temporal.js | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/axes.js b/src/axes.js index 13410d23ff..6cbc62a3cf 100644 --- a/src/axes.js +++ b/src/axes.js @@ -71,7 +71,7 @@ export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) { function autoAxisLabelsX(axis, scale, channels) { if (axis.labelAnchor === undefined) { - axis.labelAnchor = scale.type === "ordinal" ? "center" + axis.labelAnchor = scale.family === "ordinal" ? "center" : scale.reverse ? "left" : "right"; } @@ -83,7 +83,7 @@ function autoAxisLabelsX(axis, scale, channels) { function autoAxisLabelsY(axis, opposite, scale, channels) { if (axis.labelAnchor === undefined) { - axis.labelAnchor = scale.type === "ordinal" ? "center" + axis.labelAnchor = scale.family === "ordinal" ? "center" : opposite && opposite.axis === "top" ? "bottom" // TODO scale.reverse? : "top"; } @@ -115,8 +115,8 @@ function inferLabel(channels = [], scale, axis, key) { if (candidate !== undefined) { const {percent, reverse} = scale; // Ignore the implicit label for temporal scales if it’s simply “date”. - if (scale.type === "temporal" && /^(date|time|year)$/i.test(candidate)) return; - if (scale.type !== "ordinal" && (key === "x" || key === "y")) { + if (scale.family === "temporal" && /^(date|time|year)$/i.test(candidate)) return; + if (scale.family !== "ordinal" && (key === "x" || key === "y")) { if (percent) candidate = `${candidate} (%)`; if (axis.labelAnchor === "center") { candidate = `${candidate} →`; diff --git a/src/plot.js b/src/plot.js index f984799952..3e6e70c26c 100644 --- a/src/plot.js +++ b/src/plot.js @@ -137,7 +137,7 @@ function ScaleFunctions(scales) { function autoHeight({y, fy, fx}) { const nfy = fy ? fy.scale.domain().length : 1; - const ny = y ? (y.type === "ordinal" ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1; + const ny = y ? (y.family === "ordinal" ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1; return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60; } diff --git a/src/scales.js b/src/scales.js index 32bd216d23..c2f85a730f 100644 --- a/src/scales.js +++ b/src/scales.js @@ -44,14 +44,14 @@ function autoScaleRangeY(scale, dimensions) { const {inset = 0} = scale; const {height, marginTop = 0, marginBottom = 0} = dimensions; const range = [height - marginBottom - inset, marginTop + inset]; - if (scale.type === "ordinal") range.reverse(); + if (scale.family === "ordinal") range.reverse(); scale.scale.range(range); } autoScaleRound(scale); } function autoScaleRound(scale) { - if (scale.round === undefined && scale.type === "ordinal" && scale.scale.step() >= 5) { + if (scale.round === undefined && scale.family === "ordinal" && scale.scale.step() >= 5) { scale.scale.round(true); } } diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index f1432791dd..ed43611520 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -192,7 +192,7 @@ export function ScaleO(scale, channels, { if (typeof range === "function") range = range(domain); scale.range(range); } - return {type: "ordinal", reverse, domain, range, scale, inset}; + return {family: "ordinal", reverse, domain, range, scale, inset}; } export function ScaleOrdinal(key, channels, { diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 540022acfc..0e793e8d0b 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -174,7 +174,7 @@ export function ScaleQ(key, scale, channels, { if (range !== undefined) scale.range(range); if (clamp) scale.clamp(clamp); - return {type: "quantitative", reverse, domain, range, scale, inset, percent}; + return {family: "quantitative", reverse, domain, range, scale, inset, percent}; } export function ScaleLinear(key, channels, options) { @@ -194,7 +194,7 @@ export function ScaleSymlog(key, channels, {constant = 1, ...options}) { } export function ScaleIdentity() { - return {type: "identity", scale: scaleIdentity()}; + return {family: "identity", scale: scaleIdentity(), type: "identity"}; } export function ScaleDiverging(key, channels, { @@ -222,7 +222,7 @@ export function ScaleDiverging(key, channels, { const scale = scaleDiverging(domain, interpolate); if (clamp) scale.clamp(clamp); if (nice) scale.nice(nice); - return {type: "quantitative", reverse, domain, scale}; + return {family: "quantitative", reverse, domain, scale}; } function inferDomain(channels, f) { diff --git a/src/scales/temporal.js b/src/scales/temporal.js index 71034b2221..165d8108b8 100644 --- a/src/scales/temporal.js +++ b/src/scales/temporal.js @@ -3,7 +3,7 @@ import {ScaleQ} from "./quantitative.js"; function ScaleT(key, scale, channels, options) { const s = ScaleQ(key, scale, channels, options); - s.type = "temporal"; + s.family = "temporal"; return s; } From d3c252c033b6c6e8397698374ea13f540f03f3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 11 Jun 2021 20:00:31 +0200 Subject: [PATCH 16/20] expose all scale options except the following which are part of the definition of: scale.domain() // domain, nice, scale.range() // range, inset, reverse, scale.interpolate() // interpolate, clamp, round paddingInner & paddingOuter // padding --- src/scales.js | 50 ++++++++++++++++++-------------------- src/scales/ordinal.js | 13 +++++----- src/scales/quantitative.js | 16 ++++++------ test/scales/scales-test.js | 50 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 44 deletions(-) diff --git a/src/scales.js b/src/scales.js index c2f85a730f..5c0343f9ce 100644 --- a/src/scales.js +++ b/src/scales.js @@ -57,28 +57,24 @@ function autoScaleRound(scale) { } function Scale(key, channels = [], options = {}) { - const type = inferScaleType(key, channels, options); - let scale; - switch (type) { - case "diverging": scale = ScaleDiverging(key, channels, options); break; - case "categorical": case "ordinal": scale = ScaleOrdinal(key, channels, options); break; - case "cyclical": case "sequential": case "linear": scale = ScaleLinear(key, channels, options); break; - case "sqrt": scale = ScalePow(key, channels, {...options, exponent: 0.5}); break; - case "pow": scale = ScalePow(key, channels, options); break; - case "log": scale = ScaleLog(key, channels, options); break; - case "symlog": scale = ScaleSymlog(key, channels, options); break; - case "utc": scale = ScaleUtc(key, channels, options); break; - case "time": scale = ScaleTime(key, channels, options); break; - case "point": scale = ScalePoint(key, channels, options); break; - case "band": scale = ScaleBand(key, channels, options); break; - case "identity": scale = registry.get(key) === position ? ScaleIdentity(key, channels, options) : undefined; break; + const type = options.type; + options.type = inferScaleType(key, channels, options); + switch (options.type) { + case "diverging": return ScaleDiverging(key, channels, options); + case "categorical": case "ordinal": return ScaleOrdinal(key, channels, options); + case "cyclical": case "sequential": case "linear": return ScaleLinear(key, channels, options); + case "sqrt": return ScalePow(key, channels, {...options, exponent: 0.5}); + case "pow": return ScalePow(key, channels, options); + case "log": return ScaleLog(key, channels, options); + case "symlog": return ScaleSymlog(key, channels, options); + case "utc": return ScaleUtc(key, channels, options); + case "time": return ScaleTime(key, channels, options); + case "point": return ScalePoint(key, channels, options); + case "band": return ScaleBand(key, channels, options); + case "identity": return registry.get(key) === position ? ScaleIdentity(key, channels, options) : undefined; case undefined: break; - default: throw new Error(`unknown scale type: ${options.type}`); + default: throw new Error(`unknown scale type: ${type}`); } - if (scale) { - scale.scale.type = type; - } - return scale; } export function scale(options) { @@ -100,7 +96,7 @@ function inferScaleType(key, channels, {type, domain, range}) { for (const {type} of channels) if (type !== undefined) return type; if ((domain || range || []).length > 2) return asOrdinalType(key); if (domain !== undefined) { - if (isOrdinal(domain)) return asOrdinalType(key); + if (isOrdinal(domain)) return asOrdinalType(key, type); if (isTemporal(domain)) return "utc"; return "linear"; } @@ -112,8 +108,8 @@ function inferScaleType(key, channels, {type, domain, range}) { } // Positional scales default to a point scale instead of an ordinal scale. -function asOrdinalType(key) { - return registry.get(key) === position ? "point" : "ordinal"; +function asOrdinalType(key, type = "categorical") { + return registry.get(key) === position ? "point" : type; } export function exposeScales(figure, scaleDescriptors) { @@ -128,13 +124,13 @@ export function exposeScales(figure, scaleDescriptors) { return figure; } -function exposeScale({scale, label}) { +function exposeScale({scale, ...options}) { + for (const remove of ["domain", "range", "interpolate", "clamp", "round", "nice", "padding", "inset", "reverse"]) delete options[remove]; return { domain: scale.domain(), range: scale.range(), ...scale.interpolate && {interpolate: scale.interpolate()}, - label, - type: scale.type, - ...scale.clamp && {clamp: scale.clamp()} + ...scale.clamp && {clamp: scale.clamp()}, + ...options }; } diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index ed43611520..aaa7c47193 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -183,7 +183,7 @@ export function ScaleO(scale, channels, { domain = inferDomain(channels), range, reverse, - inset + ...options }) { if (reverse = !!reverse) domain = reverseof(domain); scale.domain(domain); @@ -192,7 +192,7 @@ export function ScaleO(scale, channels, { if (typeof range === "function") range = range(domain); scale.range(range); } - return {family: "ordinal", reverse, domain, range, scale, inset}; + return {family: "ordinal", domain, range, scale, ...options}; } export function ScaleOrdinal(key, channels, { @@ -203,7 +203,7 @@ export function ScaleOrdinal(key, channels, { : schemeTableau10) : undefined, ...options }) { - return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options}); + return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, type, ...options}); } export function ScalePoint(key, channels, { @@ -216,7 +216,7 @@ export function ScalePoint(key, channels, { .align(align) .padding(padding), channels, - options + {align, padding, ...options} ); } @@ -233,12 +233,11 @@ export function ScaleBand(key, channels, { .paddingInner(paddingInner) .paddingOuter(paddingOuter), channels, - options + {align, paddingInner, paddingOuter, ...options} ); } -function maybeRound(scale, channels, options = {}) { - const {round} = options; +function maybeRound(scale, channels, {round, ...options} = {}) { if (round !== undefined) scale.round(round); scale = ScaleO(scale, channels, options); scale.round = round; diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 0e793e8d0b..6728d1841d 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -143,14 +143,13 @@ export function ScaleQ(key, scale, channels, { clamp, zero, domain = (registry.get(key) === radius || registry.get(key) === opacity ? inferZeroDomain : inferDomain)(channels), - percent, round, range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined, scheme, type, interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? Scheme(scheme) : type === "cyclical" ? interpolateRainbow : interpolateTurbo) : round ? interpolateRound : undefined, reverse, - inset + ...rest }) { if (zero) domain = domain[1] < 0 ? [domain[0], 0] : domain[0] > 0 ? [0, domain[1]] : domain; if (reverse = !!reverse) domain = reverseof(domain); @@ -174,7 +173,7 @@ export function ScaleQ(key, scale, channels, { if (range !== undefined) scale.range(range); if (clamp) scale.clamp(clamp); - return {family: "quantitative", reverse, domain, range, scale, inset, percent}; + return {family: "quantitative", reverse, domain, range, scale, type, ...rest}; } export function ScaleLinear(key, channels, options) { @@ -182,15 +181,15 @@ export function ScaleLinear(key, channels, options) { } export function ScalePow(key, channels, {exponent = 1, ...options}) { - return ScaleQ(key, scalePow().exponent(exponent), channels, options); + return ScaleQ(key, scalePow().exponent(exponent), channels, options.type === "sqrt" ? options : {exponent, ...options}); } export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(channels), ...options}) { - return ScaleQ(key, scaleLog().base(base), channels, {domain, ...options}); + return ScaleQ(key, scaleLog().base(base), channels, {base, domain, ...options}); } export function ScaleSymlog(key, channels, {constant = 1, ...options}) { - return ScaleQ(key, scaleSymlog().constant(constant), channels, options); + return ScaleQ(key, scaleSymlog().constant(constant), channels, {constant, ...options}); } export function ScaleIdentity() { @@ -205,7 +204,8 @@ export function ScaleDiverging(key, channels, { range, scheme, interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : scheme !== undefined ? Scheme(scheme) : interpolateRdBu) : undefined, - reverse + reverse, + ...rest }) { domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)]; if (reverse = !!reverse) domain = reverseof(domain); @@ -222,7 +222,7 @@ export function ScaleDiverging(key, channels, { const scale = scaleDiverging(domain, interpolate); if (clamp) scale.clamp(clamp); if (nice) scale.nice(nice); - return {family: "quantitative", reverse, domain, scale}; + return {family: "quantitative", scale, ...rest}; } function inferDomain(channels, f) { diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index da6ba78297..9fc16d8612 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -86,15 +86,26 @@ tape("plot(…).scales.color exposes a continuous color scale", test => { tape("plot(…).scales.color exposes an ordinal color scale", test => { const data = ["a", "b", "c", "d"]; - const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales.color; + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot({ color: { type: "ordinal" }}).scales.color; test.deepEqual(color.domain, data); - test.deepEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); + test.deepEqual(color.range, ['rgb(35, 23, 27)', 'rgb(46, 229, 174)', 'rgb(254, 185, 39)', 'rgb(144, 12, 0)']); test.equal(typeof color.interpolate, "undefined"); test.equal(color.type, "ordinal"); test.equal(color.clamp, undefined); test.equal(typeof Plot.scale(color), "function"); }); +tape("plot(…).scales.color exposes a categorical color scale", test => { + const data = ["a", "b", "c", "d"]; + const color = Plot.dot(data, {y: d => d, fill: d => d}).plot({ color: { type: "categorical" }}).scales.color; + test.deepEqual(color.domain, data); + test.deepEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); + test.equal(typeof color.interpolate, "undefined"); + test.equal(color.type, "categorical"); + test.equal(color.clamp, undefined); + test.equal(typeof Plot.scale(color), "function"); +}); + tape("plot(…).scales.r exposes a radius scale", test => { const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales.r; test.equal(r0, undefined); @@ -156,6 +167,41 @@ tape("plot(…).scales expose radius label", test => { test.equal(r.label, "radius"); }); +tape("plot(…).scales expose pow exponent", test => { + const x = Plot.dotX([]).plot({x: { type: "pow", exponent: 0.3 }}).scales.x; + test.equal(x.type, "pow"); + test.equal(x.exponent, 0.3); + const y = Plot.dotX([]).plot({x: { type: "sqrt" }}).scales.x; + test.equal(y.type, "sqrt"); + test.equal(y.exponent, undefined); +}); + +tape("plot(…).scales expose log base", test => { + const x = Plot.dotX([]).plot({x: { type: "log", base: 2 }}).scales.x; + test.equal(x.type, "log"); + test.equal(x.base, 2); +}); + +tape("plot(…).scales expose symlog constant", test => { + const x = Plot.dotX([]).plot({x: { type: "symlog", constant: 42 }}).scales.x; + test.equal(x.type, "symlog"); + test.equal(x.constant, 42); +}); + +tape("plot(…).scales expose align, paddingInner and paddingOuter", test => { + const x = Plot.cellX(["A", "B"]).plot({x: { paddingOuter: -0.2, align: 1 }}).scales.x; + test.equal(x.type, "band"); + test.equal(x.align, 1); + test.equal(x.paddingInner, 0.1); + test.equal(x.paddingOuter, -0.2); +}); + +tape("plot(…).scales expose unexpected scale options", test => { + const x = Plot.dotX([]).plot({x: { lala: 42, width: 420 }}).scales.x; + test.equal(x.lala, 42); + test.equal(x.width, 420); +}); + function scaleOpt(x) { return Plot.dot([{x: 1}, {x: 2}, {x: 3}], {x: "x"}).plot({x}).scales.x; } From b7146f0214582d5facc7e8fa9d5927434830450b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 13 Jun 2021 10:15:54 +0200 Subject: [PATCH 17/20] if the domain of a diverging scale already has 3 values, ignore the pivot --- src/scales/quantitative.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 6728d1841d..cb38d84760 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -207,7 +207,7 @@ export function ScaleDiverging(key, channels, { reverse, ...rest }) { - domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)]; + if (domain.length === 2) domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)]; if (reverse = !!reverse) domain = reverseof(domain); // Sometimes interpolator is named interpolator, such as "lab" for Lab color From 712606f1a0c7907e95fedbe0870231d8c10f4009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 13 Jun 2021 14:52:32 +0200 Subject: [PATCH 18/20] don't expose the range if it's been subsumed in the interpolator --- src/scales.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scales.js b/src/scales.js index 5c0343f9ce..4122aa456f 100644 --- a/src/scales.js +++ b/src/scales.js @@ -130,6 +130,7 @@ function exposeScale({scale, ...options}) { domain: scale.domain(), range: scale.range(), ...scale.interpolate && {interpolate: scale.interpolate()}, + ...scale.interpolator && {interpolate: scale.interpolator(), range: undefined}, ...scale.clamp && {clamp: scale.clamp()}, ...options }; From 2ad2ccea8b5f9155c2b45e1a84484a90199f0157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 5 Jul 2021 16:37:51 +0200 Subject: [PATCH 19/20] fix reverse domain (#448) --- src/scales/quantitative.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index cb38d84760..36ca6ae2b1 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -152,25 +152,27 @@ export function ScaleQ(key, scale, channels, { ...rest }) { if (zero) domain = domain[1] < 0 ? [domain[0], 0] : domain[0] > 0 ? [0, domain[1]] : domain; - if (reverse = !!reverse) domain = reverseof(domain); - scale.domain(domain); - if (nice) scale.nice(nice === true ? undefined : nice); + reverse = !!reverse; // Sometimes interpolator is named interpolator, such as "lab" for Lab color // space. Other times interpolate is a function that takes two arguments and // is used in conjunction with the range. And other times the interpolate // function is a “fixed” interpolator independent of the range, as when a // color scheme such as interpolateRdBu is used. - if (interpolate !== undefined) { + if (scale.interpolate && interpolate !== undefined) { if (typeof interpolate !== "function") { interpolate = Interpolator(interpolate); } else if (interpolate.length === 1) { - if (reverse) interpolate = flip(interpolate); + if (reverse) interpolate = flip(interpolate), reverse = null; interpolate = constant(interpolate); } scale.interpolate(interpolate); } + if (reverse) domain = reverseof(domain); + scale.domain(domain); + if (nice) scale.nice(nice === true ? undefined : nice); + if (range !== undefined) scale.range(range); if (clamp) scale.clamp(clamp); return {family: "quantitative", reverse, domain, range, scale, type, ...rest}; From 2c71d54f6a5e9b31fcdb09b93a97a5f70eeceff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 4 Aug 2021 10:19:47 +0200 Subject: [PATCH 20/20] upgrade tests --- test/scales/scales-test.js | 234 ++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 9fc16d8612..23021410f1 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -1,205 +1,205 @@ import * as Plot from "@observablehq/plot"; -import tape from "tape-await"; +import assert from "assert"; import {JSDOM} from "jsdom"; const {window} = new JSDOM(""); global.document = window.document; -tape("plot(…).scales exposes the plot’s scales", test => { +it("plot(…).scales exposes the plot’s scales", () => { const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); - test.equal(typeof plot.scales, "object"); + assert.strictEqual(typeof plot.scales, "object"); const scales = plot.scales; - test.equal(Object.entries(scales).length, 2); - test.assert("x" in scales); - test.assert("y" in scales); + assert.strictEqual(Object.entries(scales).length, 2); + assert("x" in scales); + assert("y" in scales); }); -tape("plot(…).scales[key] is computed once", test => { +it("plot(…).scales[key] is computed once", () => { const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); - test.assert(plot.scales.x === plot.scales.x); + assert(plot.scales.x === plot.scales.x); }); -tape("plot(…).scales[key] is computed lazily", test => { +it("plot(…).scales[key] is computed lazily", () => { const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); - test.assert(plot.scales.x === plot.scales.x); + assert(plot.scales.x === plot.scales.x); }); -tape("plot(…).scales.x exposes the plot’s x scale", test => { +it("plot(…).scales.x exposes the plot’s x scale", () => { const x = Plot.dot([1, 2], {x: d => d}).plot().scales.x; - test.deepEqual(x.domain, [1, 2]); - test.deepEqual(x.range, [20, 620]); - test.equal(typeof x.interpolate, "function"); - test.equal(x.type, "linear"); - test.equal(x.clamp, false); - test.equal(typeof Plot.scale(x), "function"); + assert.deepStrictEqual(x.domain, [1, 2]); + assert.deepStrictEqual(x.range, [20, 620]); + assert.strictEqual(typeof x.interpolate, "function"); + assert.strictEqual(x.type, "linear"); + assert.strictEqual(x.clamp, false); + assert.strictEqual(typeof Plot.scale(x), "function"); }); -tape("plot(…).scales.y exposes the plot’s y scale", test => { +it("plot(…).scales.y exposes the plot’s y scale", () => { const y0 = Plot.dot([1, 2], {x: d => d}).plot().scales.y; - test.equal(y0, undefined); + assert.strictEqual(y0, undefined); const y = Plot.dot([1, 2], {y: d => d}).plot().scales.y; - test.deepEqual(y.domain, [1, 2]); - test.deepEqual(y.range, [380, 20]); - test.equal(typeof y.interpolate, "function"); - test.equal(y.type, "linear"); - test.equal(y.clamp, false); - test.equal(typeof Plot.scale(y), "function"); + assert.deepStrictEqual(y.domain, [1, 2]); + assert.deepStrictEqual(y.range, [380, 20]); + assert.strictEqual(typeof y.interpolate, "function"); + assert.strictEqual(y.type, "linear"); + assert.strictEqual(y.clamp, false); + assert.strictEqual(typeof Plot.scale(y), "function"); }); -tape("plot(…).scales.fx exposes the plot’s fx scale", test => { +it("plot(…).scales.fx exposes the plot’s fx scale", () => { const fx0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fx; - test.equal(fx0, undefined); + assert.strictEqual(fx0, undefined); const data = [1, 2]; const fx = Plot.dot(data, {y: d => d}).plot({facet: {data, x: data}}).scales.fx; - test.deepEqual(fx.domain, [1, 2]); - test.deepEqual(fx.range, [40, 620]); - test.equal(typeof fx.interpolate, "undefined"); - test.equal(fx.type, "band"); - test.equal(fx.clamp, undefined); - test.equal(typeof Plot.scale(fx), "function"); + assert.deepStrictEqual(fx.domain, [1, 2]); + assert.deepStrictEqual(fx.range, [40, 620]); + assert.strictEqual(typeof fx.interpolate, "undefined"); + assert.strictEqual(fx.type, "band"); + assert.strictEqual(fx.clamp, undefined); + assert.strictEqual(typeof Plot.scale(fx), "function"); }); -tape("plot(…).scales.fy exposes the plot’s fy scale", test => { +it("plot(…).scales.fy exposes the plot’s fy scale", () => { const fy0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fy; - test.equal(fy0, undefined); + assert.strictEqual(fy0, undefined); const data = [1, 2]; const fy = Plot.dot(data, {y: d => d}).plot({facet: {data, y: data}}).scales.fy; - test.deepEqual(fy.domain, [1, 2]); - test.deepEqual(fy.range, [20, 380]); - test.equal(typeof fy.interpolate, "undefined"); - test.equal(fy.type, "band"); - test.equal(fy.clamp, undefined); - test.equal(typeof Plot.scale(fy), "function"); + assert.deepStrictEqual(fy.domain, [1, 2]); + assert.deepStrictEqual(fy.range, [20, 380]); + assert.strictEqual(typeof fy.interpolate, "undefined"); + assert.strictEqual(fy.type, "band"); + assert.strictEqual(fy.clamp, undefined); + assert.strictEqual(typeof Plot.scale(fy), "function"); }); -tape("plot(…).scales.color exposes a continuous color scale", test => { +it("plot(…).scales.color exposes a continuous color scale", () => { const color0 = Plot.dot([1, 2], {x: d => d}).plot().scales.color; - test.equal(color0, undefined); + assert.strictEqual(color0, undefined); const data = [1, 2, 3, 4, 5]; const color = Plot.dot(data, {y: d => d, fill: d => d}).plot().scales.color; - test.deepEqual(color.domain, [1, 5]); - test.deepEqual(color.range, [0, 1]); - test.equal(typeof color.interpolate, "function"); - test.equal(color.type, "linear"); - test.equal(color.clamp, false); - test.equal(typeof Plot.scale(color), "function"); + assert.deepStrictEqual(color.domain, [1, 5]); + assert.deepStrictEqual(color.range, [0, 1]); + assert.strictEqual(typeof color.interpolate, "function"); + assert.strictEqual(color.type, "linear"); + assert.strictEqual(color.clamp, false); + assert.strictEqual(typeof Plot.scale(color), "function"); }); -tape("plot(…).scales.color exposes an ordinal color scale", test => { +it("plot(…).scales.color exposes an ordinal color scale", () => { const data = ["a", "b", "c", "d"]; const color = Plot.dot(data, {y: d => d, fill: d => d}).plot({ color: { type: "ordinal" }}).scales.color; - test.deepEqual(color.domain, data); - test.deepEqual(color.range, ['rgb(35, 23, 27)', 'rgb(46, 229, 174)', 'rgb(254, 185, 39)', 'rgb(144, 12, 0)']); - test.equal(typeof color.interpolate, "undefined"); - test.equal(color.type, "ordinal"); - test.equal(color.clamp, undefined); - test.equal(typeof Plot.scale(color), "function"); + assert.deepStrictEqual(color.domain, data); + assert.deepStrictEqual(color.range, ['rgb(35, 23, 27)', 'rgb(46, 229, 174)', 'rgb(254, 185, 39)', 'rgb(144, 12, 0)']); + assert.strictEqual(typeof color.interpolate, "undefined"); + assert.strictEqual(color.type, "ordinal"); + assert.strictEqual(color.clamp, undefined); + assert.strictEqual(typeof Plot.scale(color), "function"); }); -tape("plot(…).scales.color exposes a categorical color scale", test => { +it("plot(…).scales.color exposes a categorical color scale", () => { const data = ["a", "b", "c", "d"]; const color = Plot.dot(data, {y: d => d, fill: d => d}).plot({ color: { type: "categorical" }}).scales.color; - test.deepEqual(color.domain, data); - test.deepEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); - test.equal(typeof color.interpolate, "undefined"); - test.equal(color.type, "categorical"); - test.equal(color.clamp, undefined); - test.equal(typeof Plot.scale(color), "function"); + assert.deepStrictEqual(color.domain, data); + assert.deepStrictEqual(color.range, ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']); + assert.strictEqual(typeof color.interpolate, "undefined"); + assert.strictEqual(color.type, "categorical"); + assert.strictEqual(color.clamp, undefined); + assert.strictEqual(typeof Plot.scale(color), "function"); }); -tape("plot(…).scales.r exposes a radius scale", test => { +it("plot(…).scales.r exposes a radius scale", () => { const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales.r; - test.equal(r0, undefined); + assert.strictEqual(r0, undefined); const data = [1, 2, 3, 4, 9]; const r = Plot.dot(data, {r: d => d}).plot().scales.r; - test.deepEqual(r.domain, [0, 9]); - test.deepEqual(r.range, [0, Math.sqrt(40.5)]); - test.equal(typeof r.interpolate, "function"); - test.equal(r.type, "sqrt"); - test.equal(r.clamp, false); - test.equal(typeof Plot.scale(r), "function"); + assert.deepStrictEqual(r.domain, [0, 9]); + assert.deepStrictEqual(r.range, [0, Math.sqrt(40.5)]); + assert.strictEqual(typeof r.interpolate, "function"); + assert.strictEqual(r.type, "sqrt"); + assert.strictEqual(r.clamp, false); + assert.strictEqual(typeof Plot.scale(r), "function"); }); -tape("plot(…).scales.opacity exposes a linear scale", test => { +it("plot(…).scales.opacity exposes a linear scale", () => { const opacity0 = Plot.dot([1, 2], {x: d => d}).plot().scales.opacity; - test.equal(opacity0, undefined); + assert.strictEqual(opacity0, undefined); const data = [1, 2, 3, 4, 9]; const opacity = Plot.dot(data, {fillOpacity: d => d}).plot().scales.opacity; - test.deepEqual(opacity.domain, [0, 9]); - test.deepEqual(opacity.range, [0, 1]); - test.equal(typeof opacity.interpolate, "function"); - test.equal(opacity.type, "linear"); - test.equal(opacity.clamp, false); - test.equal(typeof Plot.scale(opacity), "function"); + assert.deepStrictEqual(opacity.domain, [0, 9]); + assert.deepStrictEqual(opacity.range, [0, 1]); + assert.strictEqual(typeof opacity.interpolate, "function"); + assert.strictEqual(opacity.type, "linear"); + assert.strictEqual(opacity.clamp, false); + assert.strictEqual(typeof Plot.scale(opacity), "function"); }); -tape("plot(…).scales expose inset domain", test => { - test.deepEqual(scaleOpt({inset: null}).range, [20, 620]); - test.deepEqual(scaleOpt({inset: 7}).range, [27, 613]); +it("plot(…).scales expose inset domain", () => { + assert.deepStrictEqual(scaleOpt({inset: null}).range, [20, 620]); + assert.deepStrictEqual(scaleOpt({inset: 7}).range, [27, 613]); }); -tape("plot(…).scales expose clamp", test => { - test.equal(scaleOpt({clamp: false}).clamp, false); - test.equal(scaleOpt({clamp: true}).clamp, true); +it("plot(…).scales expose clamp", () => { + assert.strictEqual(scaleOpt({clamp: false}).clamp, false); + assert.strictEqual(scaleOpt({clamp: true}).clamp, true); }); -tape("plot(…).scales expose rounded scales", test => { - test.equal(Plot.scale(scaleOpt({round: false}))(Math.SQRT2), 144.26406871192853); - test.equal(Plot.scale(scaleOpt({round: true}))(Math.SQRT2), 144); - test.equal(scaleOpt({round: true}).interpolate(0, 100)(Math.SQRT1_2), 71); +it("plot(…).scales expose rounded scales", () => { + assert.strictEqual(Plot.scale(scaleOpt({round: false}))(Math.SQRT2), 144.26406871192853); + assert.strictEqual(Plot.scale(scaleOpt({round: true}))(Math.SQRT2), 144); + assert.strictEqual(scaleOpt({round: true}).interpolate(0, 100)(Math.SQRT1_2), 71); }); -tape("plot(…).scales expose label", test => { - test.equal(scaleOpt({}).label, "x →"); - test.equal(scaleOpt({label: "value"}).label, "value"); +it("plot(…).scales expose label", () => { + assert.strictEqual(scaleOpt({}).label, "x →"); + assert.strictEqual(scaleOpt({label: "value"}).label, "value"); }); -tape("plot(…).scales expose color label", test => { +it("plot(…).scales expose color label", () => { const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot().scales.color; - test.equal(x.label, "x"); + assert.strictEqual(x.label, "x"); const y = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot({color: {label: "y"}}).scales.color; - test.equal(y.label, "y"); + assert.strictEqual(y.label, "y"); }); -tape("plot(…).scales expose radius label", test => { +it("plot(…).scales expose radius label", () => { const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot().scales.r; - test.equal(x.label, "x"); + assert.strictEqual(x.label, "x"); const r = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot({r: {label: "radius"}}).scales.r; - test.equal(r.label, "radius"); + assert.strictEqual(r.label, "radius"); }); -tape("plot(…).scales expose pow exponent", test => { +it("plot(…).scales expose pow exponent", () => { const x = Plot.dotX([]).plot({x: { type: "pow", exponent: 0.3 }}).scales.x; - test.equal(x.type, "pow"); - test.equal(x.exponent, 0.3); + assert.strictEqual(x.type, "pow"); + assert.strictEqual(x.exponent, 0.3); const y = Plot.dotX([]).plot({x: { type: "sqrt" }}).scales.x; - test.equal(y.type, "sqrt"); - test.equal(y.exponent, undefined); + assert.strictEqual(y.type, "sqrt"); + assert.strictEqual(y.exponent, undefined); }); -tape("plot(…).scales expose log base", test => { +it("plot(…).scales expose log base", () => { const x = Plot.dotX([]).plot({x: { type: "log", base: 2 }}).scales.x; - test.equal(x.type, "log"); - test.equal(x.base, 2); + assert.strictEqual(x.type, "log"); + assert.strictEqual(x.base, 2); }); -tape("plot(…).scales expose symlog constant", test => { +it("plot(…).scales expose symlog constant", () => { const x = Plot.dotX([]).plot({x: { type: "symlog", constant: 42 }}).scales.x; - test.equal(x.type, "symlog"); - test.equal(x.constant, 42); + assert.strictEqual(x.type, "symlog"); + assert.strictEqual(x.constant, 42); }); -tape("plot(…).scales expose align, paddingInner and paddingOuter", test => { +it("plot(…).scales expose align, paddingInner and paddingOuter", () => { const x = Plot.cellX(["A", "B"]).plot({x: { paddingOuter: -0.2, align: 1 }}).scales.x; - test.equal(x.type, "band"); - test.equal(x.align, 1); - test.equal(x.paddingInner, 0.1); - test.equal(x.paddingOuter, -0.2); + assert.strictEqual(x.type, "band"); + assert.strictEqual(x.align, 1); + assert.strictEqual(x.paddingInner, 0.1); + assert.strictEqual(x.paddingOuter, -0.2); }); -tape("plot(…).scales expose unexpected scale options", test => { +it("plot(…).scales expose unexpected scale options", () => { const x = Plot.dotX([]).plot({x: { lala: 42, width: 420 }}).scales.x; - test.equal(x.lala, 42); - test.equal(x.width, 420); + assert.strictEqual(x.lala, 42); + assert.strictEqual(x.width, 420); }); function scaleOpt(x) {