diff --git a/README.md b/README.md index c6b3c66233..60e672a502 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,31 @@ Plot.plot({ }) ``` +All the scale definitions are exposed as the *scales* property of the plot. + +```js +color = Plot.plot({…}).scales.color; +color.range // ["red", "blue"] +``` + +And, to reuse the scale in another plot: + +```js +const plot1 = Plot.plot(…); + +Plot.plot({ + color: plot1.scales.color +}) +``` + +#### 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 The position scales (*x*, *y*, *fx*, and *fy*) support additional options: diff --git a/src/axes.js b/src/axes.js index 8eb04c6917..6cbc62a3cf 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")); @@ -70,24 +71,34 @@ 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"; } if (axis.label === undefined) { axis.label = inferLabel(channels, scale, axis, "x"); } + scale.label = axis.label; } 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"; } if (axis.label === undefined) { axis.label = inferLabel(channels, scale, axis, "y"); } + scale.label = axis.label; +} + +export function autoScaleLabel(scale, channels, options) { + if (scale === undefined) return; + if (options !== undefined) scale.label = options.label; + if (scale.label === undefined) { + scale.label = inferLabel(channels, scale, {}); + } } // Channels can have labels; if all the channels for a given scale are @@ -104,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/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/plot.js b/src/plot.js index 06bda82abf..2001146d9f 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[key]); + } // Normalize the options. options = {...scaleDescriptors, ...dimensions}; @@ -88,13 +91,7 @@ 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)); - return figure; + return exposeScales(wrap(svg, {caption}), scaleDescriptors); } function Dimensions( @@ -140,6 +137,17 @@ 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; } + +// 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 = 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 6a2b328260..4e77eb580e 100644 --- a/src/scales.js +++ b/src/scales.js @@ -45,20 +45,22 @@ 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); } } function Scale(key, channels = [], options = {}) { - switch (inferScaleType(key, channels, options)) { + const type = options.type; + options.type = inferScaleType(key, channels, options); + switch (options.type) { case "diverging": return ScaleDiverging(key, channels, options); case "diverging-sqrt": return ScaleDivergingSqrt(key, channels, options); case "diverging-pow": return ScaleDivergingPow(key, channels, options); @@ -77,11 +79,15 @@ function Scale(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; - default: throw new Error(`unknown scale type: ${options.type}`); + case undefined: break; + default: throw new Error(`unknown scale type: ${type}`); } } +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) { @@ -97,7 +103,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"; } @@ -109,6 +115,30 @@ 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) { + 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, ...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()}, + ...scale.interpolator && {interpolate: scale.interpolator(), range: undefined}, + ...scale.clamp && {clamp: scale.clamp()}, + ...options + }; } diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index dfa5feb1bd..0afcf26a0b 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -8,7 +8,7 @@ export function ScaleO(scale, channels, { domain = inferDomain(channels), range, reverse, - inset + ...options }) { if (reverse = !!reverse) domain = reverseof(domain); scale.domain(domain); @@ -17,7 +17,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", domain, range, scale, ...options}; } export function ScaleOrdinal(key, channels, { @@ -26,7 +26,7 @@ export function ScaleOrdinal(key, channels, { range = registry.get(key) === color ? ordinalScheme(scheme) : undefined, ...options }) { - return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options}); + return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, type, ...options}); } export function ScalePoint(key, channels, { @@ -39,7 +39,7 @@ export function ScalePoint(key, channels, { .align(align) .padding(padding), channels, - options + {align, padding, ...options} ); } @@ -56,12 +56,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 430f2c9c16..b832a1c36a 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -55,38 +55,39 @@ 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, type, scheme = type === "cyclical" ? "rainbow" : "turbo", interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : 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); - 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 {type: "quantitative", reverse, domain, range, scale, inset, percent}; + return {family: "quantitative", reverse, domain, range, scale, type, ...rest}; } export function ScaleLinear(key, channels, options) { @@ -98,11 +99,11 @@ export function ScaleSqrt(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 ScaleQuantile(key, channels, { @@ -118,7 +119,7 @@ export function ScaleQuantile(key, channels, { } 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 ScaleThreshold(key, channels, { @@ -134,7 +135,7 @@ export function ScaleThreshold(key, channels, { } export function ScaleIdentity() { - return {type: "identity", scale: scaleIdentity()}; + return {family: "identity", scale: scaleIdentity(), type: "identity"}; } function ScaleD(key, scale, channels, { @@ -145,9 +146,10 @@ function ScaleD(key, scale, channels, { range, scheme = "rdbu", interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : undefined, - reverse + 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 @@ -162,7 +164,7 @@ function ScaleD(key, scale, channels, { scale.domain(domain).interpolator(interpolate); if (clamp) scale.clamp(clamp); if (nice) scale.nice(nice); - return {type: "quantitative", reverse, domain, scale}; + return {family: "quantitative", scale, ...rest}; } export function ScaleDiverging(key, channels, options) { 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; } diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js new file mode 100644 index 0000000000..23021410f1 --- /dev/null +++ b/test/scales/scales-test.js @@ -0,0 +1,207 @@ +import * as Plot from "@observablehq/plot"; +import assert from "assert"; +import {JSDOM} from "jsdom"; +const {window} = new JSDOM(""); +global.document = window.document; + +it("plot(…).scales exposes the plot’s scales", () => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + assert.strictEqual(typeof plot.scales, "object"); + const scales = plot.scales; + assert.strictEqual(Object.entries(scales).length, 2); + assert("x" in scales); + assert("y" in scales); +}); + +it("plot(…).scales[key] is computed once", () => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + assert(plot.scales.x === plot.scales.x); +}); + +it("plot(…).scales[key] is computed lazily", () => { + const plot = Plot.dot([1, 2], {x: d => d, y: d => d}).plot(); + assert(plot.scales.x === plot.scales.x); +}); + +it("plot(…).scales.x exposes the plot’s x scale", () => { + const x = Plot.dot([1, 2], {x: d => d}).plot().scales.x; + 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"); +}); + +it("plot(…).scales.y exposes the plot’s y scale", () => { + const y0 = Plot.dot([1, 2], {x: d => d}).plot().scales.y; + assert.strictEqual(y0, undefined); + const y = Plot.dot([1, 2], {y: d => d}).plot().scales.y; + 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"); +}); + +it("plot(…).scales.fx exposes the plot’s fx scale", () => { + const fx0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fx; + assert.strictEqual(fx0, undefined); + const data = [1, 2]; + const fx = Plot.dot(data, {y: d => d}).plot({facet: {data, x: data}}).scales.fx; + 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"); +}); + +it("plot(…).scales.fy exposes the plot’s fy scale", () => { + const fy0 = Plot.dot([1, 2], {x: d => d}).plot().scales.fy; + assert.strictEqual(fy0, undefined); + const data = [1, 2]; + const fy = Plot.dot(data, {y: d => d}).plot({facet: {data, y: data}}).scales.fy; + 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"); +}); + +it("plot(…).scales.color exposes a continuous color scale", () => { + const color0 = Plot.dot([1, 2], {x: d => d}).plot().scales.color; + 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; + 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"); +}); + +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; + 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"); +}); + +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; + 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"); +}); + +it("plot(…).scales.r exposes a radius scale", () => { + const r0 = Plot.dot([1, 2], {x: d => d}).plot().scales.r; + assert.strictEqual(r0, undefined); + const data = [1, 2, 3, 4, 9]; + const r = Plot.dot(data, {r: d => d}).plot().scales.r; + 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"); +}); + +it("plot(…).scales.opacity exposes a linear scale", () => { + const opacity0 = Plot.dot([1, 2], {x: d => d}).plot().scales.opacity; + assert.strictEqual(opacity0, undefined); + const data = [1, 2, 3, 4, 9]; + const opacity = Plot.dot(data, {fillOpacity: d => d}).plot().scales.opacity; + 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"); +}); + +it("plot(…).scales expose inset domain", () => { + assert.deepStrictEqual(scaleOpt({inset: null}).range, [20, 620]); + assert.deepStrictEqual(scaleOpt({inset: 7}).range, [27, 613]); +}); + +it("plot(…).scales expose clamp", () => { + assert.strictEqual(scaleOpt({clamp: false}).clamp, false); + assert.strictEqual(scaleOpt({clamp: true}).clamp, true); +}); + +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); +}); + +it("plot(…).scales expose label", () => { + assert.strictEqual(scaleOpt({}).label, "x →"); + assert.strictEqual(scaleOpt({label: "value"}).label, "value"); +}); + +it("plot(…).scales expose color label", () => { + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot().scales.color; + assert.strictEqual(x.label, "x"); + const y = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {fill: "x"}).plot({color: {label: "y"}}).scales.color; + assert.strictEqual(y.label, "y"); +}); + +it("plot(…).scales expose radius label", () => { + const x = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot().scales.r; + assert.strictEqual(x.label, "x"); + const r = Plot.dot([{x: 1}, {x: 2}, {x: 3}], {r: "x"}).plot({r: {label: "radius"}}).scales.r; + assert.strictEqual(r.label, "radius"); +}); + +it("plot(…).scales expose pow exponent", () => { + const x = Plot.dotX([]).plot({x: { type: "pow", exponent: 0.3 }}).scales.x; + assert.strictEqual(x.type, "pow"); + assert.strictEqual(x.exponent, 0.3); + const y = Plot.dotX([]).plot({x: { type: "sqrt" }}).scales.x; + assert.strictEqual(y.type, "sqrt"); + assert.strictEqual(y.exponent, undefined); +}); + +it("plot(…).scales expose log base", () => { + const x = Plot.dotX([]).plot({x: { type: "log", base: 2 }}).scales.x; + assert.strictEqual(x.type, "log"); + assert.strictEqual(x.base, 2); +}); + +it("plot(…).scales expose symlog constant", () => { + const x = Plot.dotX([]).plot({x: { type: "symlog", constant: 42 }}).scales.x; + assert.strictEqual(x.type, "symlog"); + assert.strictEqual(x.constant, 42); +}); + +it("plot(…).scales expose align, paddingInner and paddingOuter", () => { + const x = Plot.cellX(["A", "B"]).plot({x: { paddingOuter: -0.2, align: 1 }}).scales.x; + assert.strictEqual(x.type, "band"); + assert.strictEqual(x.align, 1); + assert.strictEqual(x.paddingInner, 0.1); + assert.strictEqual(x.paddingOuter, -0.2); +}); + +it("plot(…).scales expose unexpected scale options", () => { + const x = Plot.dotX([]).plot({x: { lala: 42, width: 420 }}).scales.x; + assert.strictEqual(x.lala, 42); + assert.strictEqual(x.width, 420); +}); + +function scaleOpt(x) { + return Plot.dot([{x: 1}, {x: 2}, {x: 3}], {x: "x"}).plot({x}).scales.x; +}