From c67e7c273e84930c22e95aaba52be3d1e455cd3e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 9 Feb 2022 21:21:18 -0800 Subject: [PATCH 1/3] warnings! --- src/options.js | 12 ++ src/plot.js | 16 +- src/scales.js | 17 ++- src/warnings.js | 12 ++ test/output/crimeanWarOverlapped.svg | 213 +++++++++++---------------- test/output/crimeanWarStacked.svg | 213 +++++++++++---------------- test/plots/crimean-war-overlapped.js | 2 +- test/plots/crimean-war-stacked.js | 2 +- test/plots/fruit-sales-date.js | 3 + 9 files changed, 227 insertions(+), 263 deletions(-) create mode 100644 src/warnings.js diff --git a/src/options.js b/src/options.js index a655888ed7..d3ca40e45c 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,17 @@ 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); + } +} + 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..a078aaf804 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} 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,14 @@ export function normalizeScale(key, scale, hint) { function Scale(key, channels = [], options = {}) { const type = inferScaleType(key, channels, options); + + // Warn for common misuse of implicit band scales. + if (options.type === undefined && type === "band") { + 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 "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "band".`); + 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 and convert them to Date objects. Dates are typically associated with a "utc" or "time" scale rather than a "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`); + } + options.type = type; // Mutates input! // Once the scale type is known, coerce the associated channel values and any @@ -246,7 +255,11 @@ function inferScaleType(key, channels, {type, domain, range, scheme}) { // If any channel is ordinal or temporal, it takes priority. const values = channels.map(({value}) => value).filter(value => value !== undefined); - if (values.some(isOrdinal)) return asOrdinalType(kind); + if (values.some(isOrdinal)) { + const type = asOrdinalType(kind); + 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 and convert them to Date objects. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`); + return type; + } if (values.some(isTemporal)) return "utc"; return "linear"; } 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" })) From cb22d75d0fd4f182a2174d34b0aba57d050dc90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 10 Feb 2022 09:10:31 +0100 Subject: [PATCH 2/3] uplift an old warning --- src/transforms/window.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transforms/window.js b/src/transforms/window.js index cb53440f6f..e300d91140 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; @@ -29,7 +30,7 @@ function maybeAnchor(anchor = "middle", k) { function maybeShift(shift) { if (shift === undefined) return; - console.warn("shift is deprecated; please use anchor instead"); + warn("Warning: shift is deprecated; please use anchor instead"); switch (`${shift}`.toLowerCase()) { case "centered": return "middle"; case "leading": return "start"; From 6bf93c2d0450203a08b25d0b40e08e8e2ee6ba9e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 10 Feb 2022 22:07:22 -0800 Subject: [PATCH 3/3] numeric string warning --- src/options.js | 9 ++++ src/scales.js | 32 ++++++++---- src/transforms/window.js | 8 +-- test/plots/software-versions.js | 1 + test/plots/stargazers-hourly-group.js | 1 + test/scales/scales-test.js | 72 +++++++++++++-------------- 6 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/options.js b/src/options.js index d3ca40e45c..5eeb0aefe2 100644 --- a/src/options.js +++ b/src/options.js @@ -222,6 +222,15 @@ export function isTemporalString(values) { } } +// 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/scales.js b/src/scales.js index a078aaf804..91bef69edf 100644 --- a/src/scales.js +++ b/src/scales.js @@ -1,5 +1,5 @@ import {parse as isoParse} from "isoformat"; -import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order, isTemporalString} 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"; @@ -135,11 +135,21 @@ export function normalizeScale(key, scale, hint) { function Scale(key, channels = [], options = {}) { const type = inferScaleType(key, channels, options); - // Warn for common misuse of implicit band scales. - if (options.type === undefined && type === "band") { + // 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 "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "band".`); - 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 and convert them to Date objects. Dates are typically associated with a "utc" or "time" scale rather than a "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`); + 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! @@ -199,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"; @@ -255,11 +269,7 @@ function inferScaleType(key, channels, {type, domain, range, scheme}) { // If any channel is ordinal or temporal, it takes priority. const values = channels.map(({value}) => value).filter(value => value !== undefined); - if (values.some(isOrdinal)) { - const type = asOrdinalType(kind); - 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 and convert them to Date objects. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`); - return type; - } + if (values.some(isOrdinal)) return asOrdinalType(kind); if (values.some(isTemporal)) return "utc"; return "linear"; } @@ -285,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 e300d91140..e386dbb91f 100644 --- a/src/transforms/window.js +++ b/src/transforms/window.js @@ -14,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)); } @@ -29,8 +33,6 @@ function maybeAnchor(anchor = "middle", k) { } function maybeShift(shift) { - if (shift === undefined) return; - warn("Warning: shift is deprecated; please use anchor instead"); switch (`${shift}`.toLowerCase()) { case "centered": return "middle"; case "leading": return "start"; 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", () => {