diff --git a/src/options.js b/src/options.js index a655888ed7..5eeb0aefe2 100644 --- a/src/options.js +++ b/src/options.js @@ -1,3 +1,4 @@ +import {parse as isoParse} from "isoformat"; import {color, descending} from "d3"; import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3"; import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3"; @@ -210,6 +211,26 @@ export function isTemporal(values) { } } +// Are these strings that might represent dates? This is stricter than ISO 8601 +// because we want to ignore false positives on numbers; for example, the string +// "1192" is more likely to represent a number than a date even though it is +// valid ISO 8601 representing 1192-01-01. +export function isTemporalString(values) { + for (const value of values) { + if (value == null) continue; + return typeof value === "string" && isNaN(value) && isoParse(value); + } +} + +// Are these strings that might represent numbers? This is stricter than +// coercion because we want to ignore false positives on e.g. empty strings. +export function isNumericString(values) { + for (const value of values) { + if (value == null || value === "") continue; + return typeof value === "string" && !isNaN(value); + } +} + export function isNumeric(values) { for (const value of values) { if (value == null) continue; diff --git a/src/plot.js b/src/plot.js index e1980d8378..5299d73fb2 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,4 +1,4 @@ -import {create, cross, difference, groups, InternMap} from "d3"; +import {create, cross, difference, groups, InternMap, select} from "d3"; import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js"; import {Channel, channelSort} from "./channel.js"; import {defined} from "./defined.js"; @@ -8,6 +8,7 @@ import {arrayify, isOptions, keyword, range, first, second, where} from "./optio import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js"; import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic} from "./transforms/basic.js"; +import {consumeWarnings} from "./warnings.js"; export function plot(options = {}) { const {facet, style, caption, ariaLabel, ariaDescription} = options; @@ -119,6 +120,19 @@ export function plot(options = {}) { figure.scale = exposeScales(scaleDescriptors); figure.legend = exposeLegends(scaleDescriptors, options); + + const w = consumeWarnings(); + if (w > 0) { + select(svg).append("text") + .attr("x", width) + .attr("y", 20) + .attr("dy", "-1em") + .attr("text-anchor", "end") + .text("⚠️") + .append("title") + .text(`${w.toLocaleString("en-US")} warning${w === 1 ? "" : "s"}. Please check the console.`); + } + return figure; } diff --git a/src/scales.js b/src/scales.js index 9dffed4168..91bef69edf 100644 --- a/src/scales.js +++ b/src/scales.js @@ -1,10 +1,11 @@ import {parse as isoParse} from "isoformat"; -import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order} from "./options.js"; +import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order, isTemporalString, isNumericString} from "./options.js"; import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js"; import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js"; import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js"; import {ScaleTime, ScaleUtc} from "./scales/temporal.js"; import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js"; +import {warn} from "./warnings.js"; export function Scales(channels, { inset: globalInset = 0, @@ -133,6 +134,24 @@ export function normalizeScale(key, scale, hint) { function Scale(key, channels = [], options = {}) { const type = inferScaleType(key, channels, options); + + // Warn for common misuses of implicit ordinal scales. We disable this test if + // you set the domain or range explicitly, since setting the domain or range + // (typically with a cardinality of more than two) is another indication that + // you intended for the scale to be ordinal; we also disable it for facet + // scales since these are always band scales. + if (options.type === undefined + && options.domain === undefined + && options.range === undefined + && key !== "fx" + && key !== "fy" + && isOrdinalScale({type})) { + const values = channels.map(({value}) => value).filter(value => value !== undefined); + if (values.some(isTemporal)) warn(`Warning: some data associated with the ${key} scale are dates. Dates are typically associated with a "utc" or "time" scale rather than a "${formatScaleType(type)}" scale. If you are using a bar mark, you probably want a rect mark with the interval option instead; if you are using a group transform, you probably want a bin transform instead. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`); + else if (values.some(isTemporalString)) warn(`Warning: some data associated with the ${key} scale are strings that appear to be dates (e.g., YYYY-MM-DD). If these strings represent dates, you should parse them to Date objects. Dates are typically associated with a "utc" or "time" scale rather than a "${formatScaleType(type)}" scale. If you are using a bar mark, you probably want a rect mark with the interval option instead; if you are using a group transform, you probably want a bin transform instead. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`); + else if (values.some(isNumericString)) warn(`Warning: some data associated with the ${key} scale are strings that appear to be numbers. If these strings represent numbers, you should parse or coerce them to numbers. Numbers are typically associated with a "linear" scale rather than a "${formatScaleType(type)}" scale. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`); + } + options.type = type; // Mutates input! // Once the scale type is known, coerce the associated channel values and any @@ -190,6 +209,10 @@ function Scale(key, channels = [], options = {}) { } } +function formatScaleType(type) { + return typeof type === "symbol" ? type.description : type; +} + function inferScaleType(key, channels, {type, domain, range, scheme}) { // The facet scales are always band scales; this cannot be changed. if (key === "fx" || key === "fy") return "band"; @@ -272,7 +295,7 @@ export function isTemporalScale({type}) { } export function isOrdinalScale({type}) { - return type === "ordinal" || type === "point" || type === "band"; + return type === "ordinal" || type === "point" || type === "band" || type === ordinalImplicit; } function isThresholdScale({type}) { diff --git a/src/transforms/window.js b/src/transforms/window.js index cb53440f6f..e386dbb91f 100644 --- a/src/transforms/window.js +++ b/src/transforms/window.js @@ -1,5 +1,6 @@ import {mapX, mapY} from "./map.js"; import {deviation, max, min, median, mode, variance} from "d3"; +import {warn} from "../warnings.js"; export function windowX(windowOptions = {}, options) { if (arguments.length === 1) options = windowOptions; @@ -13,7 +14,11 @@ export function windowY(windowOptions = {}, options) { export function window(options = {}) { if (typeof options === "number") options = {k: options}; - let {k, reduce, shift, anchor = maybeShift(shift)} = options; + let {k, reduce, shift, anchor} = options; + if (anchor === undefined && shift !== undefined) { + anchor = maybeShift(shift); + warn(`Warning: the shift option is deprecated; please use anchor "${anchor}" instead.`); + } if (!((k = Math.floor(k)) > 0)) throw new Error("invalid k"); return maybeReduce(reduce)(k, maybeAnchor(anchor, k)); } @@ -28,8 +33,6 @@ function maybeAnchor(anchor = "middle", k) { } function maybeShift(shift) { - if (shift === undefined) return; - console.warn("shift is deprecated; please use anchor instead"); switch (`${shift}`.toLowerCase()) { case "centered": return "middle"; case "leading": return "start"; diff --git a/src/warnings.js b/src/warnings.js new file mode 100644 index 0000000000..6c13e8db1e --- /dev/null +++ b/src/warnings.js @@ -0,0 +1,12 @@ +let warnings = 0; + +export function consumeWarnings() { + const w = warnings; + warnings = 0; + return w; +} + +export function warn(message) { + console.warn(message); + ++warnings; +} diff --git a/test/output/crimeanWarOverlapped.svg b/test/output/crimeanWarOverlapped.svg index e4487a7f97..32b60e552f 100644 --- a/test/output/crimeanWarOverlapped.svg +++ b/test/output/crimeanWarOverlapped.svg @@ -57,153 +57,108 @@ 2,600 ↑ deaths - - + + Apr - - May - - - Jun - - + Jul - - Aug - - - Sep - - + Oct - - Nov - - - Dec - - + Jan - - Feb - - - Mar - - + Apr - - May - - - Jun - - + Jul - - Aug - - - Sep - - + Oct - - Nov - - - Dec - - + Jan - - Feb - - - Mar + + Apr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/output/crimeanWarStacked.svg b/test/output/crimeanWarStacked.svg index a3d1cbc610..f069d566be 100644 --- a/test/output/crimeanWarStacked.svg +++ b/test/output/crimeanWarStacked.svg @@ -36,153 +36,108 @@ 3,000 ↑ deaths - - + + Apr - - May - - - Jun - - + Jul - - Aug - - - Sep - - + Oct - - Nov - - - Dec - - + Jan - - Feb - - - Mar - - + Apr - - May - - - Jun - - + Jul - - Aug - - - Sep - - + Oct - - Nov - - - Dec - - + Jan - - Feb - - - Mar + + Apr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plots/crimean-war-overlapped.js b/test/plots/crimean-war-overlapped.js index 05b2dec665..071209a38a 100644 --- a/test/plots/crimean-war-overlapped.js +++ b/test/plots/crimean-war-overlapped.js @@ -11,7 +11,7 @@ export default async function() { label: null }, marks: [ - Plot.barY(data, {x: "date", y2: "deaths", sort: d => -d.deaths, fill: "cause"}), + Plot.rectY(data, {x: "date", interval: d3.utcMonth, y2: "deaths", fill: "cause", mixBlendMode: "multiply"}), Plot.ruleY([0]) ] }); diff --git a/test/plots/crimean-war-stacked.js b/test/plots/crimean-war-stacked.js index d442a02d17..5387fa8a33 100644 --- a/test/plots/crimean-war-stacked.js +++ b/test/plots/crimean-war-stacked.js @@ -11,7 +11,7 @@ export default async function() { label: null }, marks: [ - Plot.barY(data, {x: "date", y: "deaths", fill: "cause", reverse: true}), + Plot.rectY(data, {x: "date", interval: d3.utcMonth, y: "deaths", fill: "cause", reverse: true}), Plot.ruleY([0]) ] }); diff --git a/test/plots/fruit-sales-date.js b/test/plots/fruit-sales-date.js index c5a2de3b64..8116e9b3b2 100644 --- a/test/plots/fruit-sales-date.js +++ b/test/plots/fruit-sales-date.js @@ -4,6 +4,9 @@ import * as d3 from "d3"; export default async function() { const sales = await d3.csv("data/fruit-sales.csv", d3.autoType); return Plot.plot({ + x: { + type: "band" // treat dates as ordinal, not temporal + }, marks: [ Plot.barY(sales, Plot.stackY({x: "date", y: "units", fill: "fruit"})), Plot.text(sales, Plot.stackY({x: "date", y: "units", text: "fruit" })) diff --git a/test/plots/software-versions.js b/test/plots/software-versions.js index b691e7a069..31ef4c0a0e 100644 --- a/test/plots/software-versions.js +++ b/test/plots/software-versions.js @@ -30,6 +30,7 @@ export default async function() { percent: true }, color: { + type: "ordinal", scheme: "blues" }, marks: [ diff --git a/test/plots/stargazers-hourly-group.js b/test/plots/stargazers-hourly-group.js index bea5ff6044..66e3ddbeae 100644 --- a/test/plots/stargazers-hourly-group.js +++ b/test/plots/stargazers-hourly-group.js @@ -5,6 +5,7 @@ export default async function() { const stargazers = await d3.csv("data/stargazers.csv", d3.autoType); return Plot.plot({ x: { + type: "band", label: "New stargazers per hour →" }, y: { diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 6165f4c895..8613e2abc0 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -1137,60 +1137,60 @@ it("plot({clamp, …}).scale('x').clamp reflects the given clamp option", () => }); it("plot({align, …}).scale('x').align reflects the given align option for point scales", () => { - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {align: 0}}).scale("x").align, 0); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {align: 0.7}}).scale("x").align, 0.7); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {align: "0.7"}}).scale("x").align, 0.7); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {align: 1}}).scale("x").align, 1); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {align: 0}}).scale("x").align, 0); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {align: 0.7}}).scale("x").align, 0.7); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {align: "0.7"}}).scale("x").align, 0.7); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {align: 1}}).scale("x").align, 1); }); it("plot({align, …}).scale('x').align reflects the given align option for band scales", () => { - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {align: 0}}).scale("x").align, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {align: 0.7}}).scale("x").align, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {align: "0.7"}}).scale("x").align, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {align: 1}}).scale("x").align, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {align: 0}}).scale("x").align, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {align: 0.7}}).scale("x").align, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {align: "0.7"}}).scale("x").align, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {align: 1}}).scale("x").align, 1); }); it("plot({paddingInner, …}).scale('x').paddingInner reflects the given paddingInner option for band scales", () => { - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingInner: 0}}).scale("x").paddingInner, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingInner: 0.7}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingInner: "0.7"}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingInner: 1}}).scale("x").paddingInner, 1); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingInner: 0}}).scale("x").paddingInner, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingInner: 0.7}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingInner: "0.7"}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingInner: 1}}).scale("x").paddingInner, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingInner: 0}}).scale("x").paddingInner, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingInner: 0.7}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingInner: "0.7"}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingInner: 1}}).scale("x").paddingInner, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingInner: 0}}).scale("x").paddingInner, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingInner: 0.7}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingInner: "0.7"}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingInner: 1}}).scale("x").paddingInner, 1); }); it("plot({paddingOuter, …}).scale('x').paddingOuter reflects the given paddingOuter option for band scales", () => { - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingOuter: 0}}).scale("x").paddingOuter, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingOuter: 0.7}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingOuter: "0.7"}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {paddingOuter: 1}}).scale("x").paddingOuter, 1); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingOuter: 0}}).scale("x").paddingOuter, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingOuter: 0.7}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingOuter: "0.7"}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0, paddingOuter: 1}}).scale("x").paddingOuter, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingOuter: 0}}).scale("x").paddingOuter, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingOuter: 0.7}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingOuter: "0.7"}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {paddingOuter: 1}}).scale("x").paddingOuter, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingOuter: 0}}).scale("x").paddingOuter, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingOuter: 0.7}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingOuter: "0.7"}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0, paddingOuter: 1}}).scale("x").paddingOuter, 1); }); it("plot({padding, …}).scale('x').paddingInner reflects the given padding option for band scales", () => { - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0}}).scale("x").paddingInner, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0.7}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").paddingInner, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 1}}).scale("x").paddingInner, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0}}).scale("x").paddingInner, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0.7}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").paddingInner, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 1}}).scale("x").paddingInner, 1); }); it("plot({padding, …}).scale('x').paddingOuter reflects the given padding option for band scales", () => { - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0}}).scale("x").paddingOuter, 0); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0.7}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").paddingOuter, 0.7); - assert.strictEqual(Plot.cell(["1", "2", "3"], {x: d => d}).plot({x: {padding: 1}}).scale("x").paddingOuter, 1); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0}}).scale("x").paddingOuter, 0); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 0.7}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").paddingOuter, 0.7); + assert.strictEqual(Plot.cell("abc", {x: d => d}).plot({x: {padding: 1}}).scale("x").paddingOuter, 1); }); it("plot({padding, …}).scale('x').padding reflects the given padding option for point scales", () => { - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0}}).scale("x").padding, 0); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {padding: 0.7}}).scale("x").padding, 0.7); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").padding, 0.7); - assert.strictEqual(Plot.dot(["1", "2", "3"], {x: d => d}).plot({x: {padding: 1}}).scale("x").padding, 1); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {padding: 0}}).scale("x").padding, 0); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {padding: 0.7}}).scale("x").padding, 0.7); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {padding: "0.7"}}).scale("x").padding, 0.7); + assert.strictEqual(Plot.dot("abc", {x: d => d}).plot({x: {padding: 1}}).scale("x").padding, 1); }); it("plot(…).scale('x').label reflects the default label for named fields, possibly reversed", () => {