diff --git a/README.md b/README.md
index aa991f1f2fe..6916ec05596 100644
--- a/README.md
+++ b/README.md
@@ -185,12 +185,15 @@ A scale’s domain (the extent of its inputs, abstract values) and range (the ex
* *scale*.**range** - typically [*min*, *max*], or an array of ordinal or categorical values
* *scale*.**unknown** - the desired output value (defaults to undefined) for invalid input values
* *scale*.**reverse** - reverses the domain (or in somes cases, the range), say to flip the chart along *x* or *y*
+* *scale*.**interval** - an interval to create an array of ordinal values
For most quantitative scales, the default domain is the [*min*, *max*] of all values associated with the scale. For the *radius* and *opacity* scales, the default domain is [0, *max*] to ensure a meaningful value encoding. For ordinal scales, the default domain is the set of all distinct values associated with the scale in natural ascending order; for a different order, set the domain explicitly or add a [sort option](#sort-options) to an associated mark. For threshold scales, the default domain is [0] to separate negative and non-negative values. For quantile scales, the default domain is the set of all defined values associated with the scale. If a scale is reversed, it is equivalent to setting the domain as [*max*, *min*] instead of [*min*, *max*].
The default range depends on the scale: for [position scales](#position-options) (*x*, *y*, *fx*, and *fy*), the default range depends on the plot’s [size and margins](#layout-options). For [color scales](#color-options), there are default color schemes for quantitative, ordinal, and categorical data. For opacity, the default range is [0, 1]. And for radius, the default range is designed to produce dots of “reasonable” size assuming a *sqrt* scale type for accurate area representation: zero maps to zero, the first quartile maps to a radius of three pixels, and other values are extrapolated. This convention for radius ensures that if the scale’s data values are all equal, dots have the default constant radius of three pixels, while if the data varies, dots will tend to be larger.
-The behavior of the *scale*.unknown option depends on the scale type. For quantitative and temporal scales, the unknown value is used whenever the input value is undefined, null, or NaN. For ordinal or categorical scales, the unknown value is returned for any input value outside the domain. For band or point scales, the unknown option has no effect; it is effectively always equal to undefined. If the unknown option is set to undefined (the default), or null or NaN, then the affected input values will be considered undefined and filtered from the output.
+The behavior of the *scale*.**unknown** option depends on the scale type. For quantitative and temporal scales, the unknown value is used whenever the input value is undefined, null, or NaN. For ordinal or categorical scales, the unknown value is returned for any input value outside the domain. For band or point scales, the unknown option has no effect; it is effectively always equal to undefined. If the unknown option is set to undefined (the default), or null or NaN, then the affected input values will be considered undefined and filtered from the output.
+
+For data at regular intervals, such as integer values or daily samples, the *scale*.**interval** option can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—must expose an *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) functions. The option can also be specified as a number, in which case it will be promoted to a numeric interval with the given step. This option sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, the default *scale*.domain is an array of uniformly-spaced values spanning the extent of the values associated with the scale.
Quantitative scales can be further customized with additional options:
diff --git a/src/axes.js b/src/axes.js
index 70343a8047a..7a32af7268a 100644
--- a/src/axes.js
+++ b/src/axes.js
@@ -2,6 +2,7 @@ import {extent} from "d3";
import {AxisX, AxisY} from "./axis.js";
import {isOrdinalScale, isTemporalScale, scaleOrder} from "./scales.js";
import {position, registry} from "./scales/index.js";
+import {formatDefault} from "./format.js";
export function Axes(
{x: xScale, y: yScale, fx: fxScale, fy: fyScale},
@@ -18,8 +19,8 @@ export function Axes(
return {
...xAxis && {x: new AxisX({grid, line, label, fontVariant: inferFontVariant(xScale), ...x, axis: xAxis})},
...yAxis && {y: new AxisY({grid, line, label, fontVariant: inferFontVariant(yScale), ...y, axis: yAxis})},
- ...fxAxis && {fx: new AxisX({name: "fx", grid: facetGrid, label: facetLabel, ...fx, axis: fxAxis})},
- ...fyAxis && {fy: new AxisY({name: "fy", grid: facetGrid, label: facetLabel, ...fy, axis: fyAxis})}
+ ...fxAxis && {fx: new AxisX({name: "fx", grid: facetGrid, label: facetLabel, fontVariant: inferFontVariant(fxScale), ...fx, axis: fxAxis})},
+ ...fyAxis && {fy: new AxisY({name: "fy", grid: facetGrid, label: facetLabel, fontVariant: inferFontVariant(fyScale), ...fy, axis: fyAxis})}
};
}
@@ -34,8 +35,20 @@ export function autoAxisTicks({x, y, fx, fy}, {x: xAxis, y: yAxis, fx: fxAxis, f
function autoAxisTicksK(scale, axis, k) {
if (axis.ticks === undefined) {
- const [min, max] = extent(scale.scale.range());
- axis.ticks = (max - min) / k;
+ const interval = scale.interval;
+ if (interval !== undefined) {
+ const [min, max] = extent(scale.scale.domain());
+ axis.ticks = interval.range(interval.floor(min), interval.offset(interval.floor(max)));
+ } else {
+ const [min, max] = extent(scale.scale.range());
+ axis.ticks = (max - min) / k;
+ }
+ }
+ // D3’s ordinal scales simply use toString by default, but if the ordinal
+ // scale domain (or ticks) are numbers or dates (say because we’re applying a
+ // time interval to the ordinal scale), we want Plot’s default formatter.
+ if (axis.tickFormat === undefined && isOrdinalScale(scale)) {
+ axis.tickFormat = formatDefault;
}
}
@@ -144,5 +157,5 @@ function inferLabel(channels = [], scale, axis, key) {
}
export function inferFontVariant(scale) {
- return isOrdinalScale(scale) ? undefined : "tabular-nums";
+ return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums";
}
diff --git a/src/scales.js b/src/scales.js
index 41586bd62a5..3d8b60ce6b6 100644
--- a/src/scales.js
+++ b/src/scales.js
@@ -137,20 +137,21 @@ 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.
+ // you specify a scale interval or 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
+ && options.interval === 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)}".`);
+ 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 specify the interval of the ${key} scale (e.g., d3.utcDay), or 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)}".`);
+ 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 specify the interval of the ${key} scale (e.g., 1 for integers), or you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`);
}
options.type = type; // Mutates input!
@@ -409,6 +410,7 @@ function exposeScale({
range,
label,
interpolate,
+ interval,
transform,
percent,
pivot
@@ -423,6 +425,7 @@ function exposeScale({
...percent && {percent}, // only exposed if truthy
...label !== undefined && {label},
...unknown !== undefined && {unknown},
+ ...interval !== undefined && {interval},
// quantitative
...interpolate !== undefined && {interpolate},
diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js
index beb53a2b03e..634b578d9aa 100644
--- a/src/scales/ordinal.js
+++ b/src/scales/ordinal.js
@@ -16,11 +16,13 @@ export const ordinalImplicit = Symbol("ordinal");
export function ScaleO(scale, channels, {
type,
interval,
- domain = inferDomain(channels, interval),
+ domain,
range,
reverse,
hint
}) {
+ interval = maybeInterval(interval);
+ if (domain === undefined) domain = inferDomain(channels, interval);
if (type === "categorical" || type === ordinalImplicit) type = "ordinal"; // shorthand for color schemes
if (reverse) domain = reverseof(domain);
scale.domain(domain);
@@ -29,18 +31,20 @@ export function ScaleO(scale, channels, {
if (typeof range === "function") range = range(domain);
scale.range(range);
}
- return {type, domain, range, scale, hint};
+ return {type, domain, range, scale, hint, interval};
}
export function ScaleOrdinal(key, channels, {
type,
interval,
- domain = inferDomain(channels, interval),
+ domain,
range,
scheme,
unknown,
...options
}) {
+ interval = maybeInterval(interval);
+ if (domain === undefined) domain = inferDomain(channels, interval);
let hint;
if (registry.get(key) === symbol) {
hint = inferSymbolHint(channels);
@@ -113,7 +117,7 @@ function inferDomain(channels, interval) {
if (value === undefined) continue;
for (const v of value) values.add(v);
}
- if ((interval = maybeInterval(interval)) != null) {
+ if (interval !== undefined) {
const [min, max] = extent(values).map(interval.floor, interval);
return interval.range(min, interval.offset(max));
}
diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js
index f966a152c77..0e208cf947c 100644
--- a/src/scales/quantitative.js
+++ b/src/scales/quantitative.js
@@ -26,6 +26,7 @@ import {
import {positive, negative, finite} from "../defined.js";
import {arrayify, constant, order, slice} from "../options.js";
import {ordinalRange, quantitativeScheme} from "./schemes.js";
+import {maybeInterval} from "../transforms/interval.js";
import {registry, radius, opacity, color, length} from "./index.js";
export const flip = i => t => i(1 - t);
@@ -57,10 +58,12 @@ export function ScaleQ(key, scale, channels, {
unknown,
round,
scheme,
+ interval,
range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === length ? inferLengthRange(channels, domain) : registry.get(key) === opacity ? unit : undefined,
interpolate = registry.get(key) === color ? (scheme == null && range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : interpolateNumber,
reverse
}) {
+ interval = maybeInterval(interval);
if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes
reverse = !!reverse;
@@ -105,7 +108,7 @@ export function ScaleQ(key, scale, channels, {
if (nice) scale.nice(nice === true ? undefined : nice), domain = scale.domain();
if (range !== undefined) scale.range(range);
if (clamp) scale.clamp(clamp);
- return {type, domain, range, scale, interpolate};
+ return {type, domain, range, scale, interpolate, interval};
}
export function ScaleLinear(key, channels, options) {
diff --git a/test/output/downloadsOrdinal.svg b/test/output/downloadsOrdinal.svg
new file mode 100644
index 00000000000..9c45a66d42b
--- /dev/null
+++ b/test/output/downloadsOrdinal.svg
@@ -0,0 +1,255 @@
+
\ No newline at end of file
diff --git a/test/output/ibmTrading.svg b/test/output/ibmTrading.svg
new file mode 100644
index 00000000000..d94be4617e9
--- /dev/null
+++ b/test/output/ibmTrading.svg
@@ -0,0 +1,164 @@
+
\ No newline at end of file
diff --git a/test/output/integerInterval.svg b/test/output/integerInterval.svg
new file mode 100644
index 00000000000..b551bd339e7
--- /dev/null
+++ b/test/output/integerInterval.svg
@@ -0,0 +1,62 @@
+
\ No newline at end of file
diff --git a/test/output/sparseCell.svg b/test/output/sparseCell.svg
index 7c6a23bc3b8..d936c08ff2e 100644
--- a/test/output/sparseCell.svg
+++ b/test/output/sparseCell.svg
@@ -13,7 +13,7 @@
white-space: pre;
}
-
+ 1
@@ -127,7 +127,7 @@
28Season
-
+ 1
diff --git a/test/output/yearlyRequests.svg b/test/output/yearlyRequests.svg
new file mode 100644
index 00000000000..7d90b7d8932
--- /dev/null
+++ b/test/output/yearlyRequests.svg
@@ -0,0 +1,121 @@
+
\ No newline at end of file
diff --git a/test/output/yearlyRequestsDot.svg b/test/output/yearlyRequestsDot.svg
new file mode 100644
index 00000000000..b10b3d18169
--- /dev/null
+++ b/test/output/yearlyRequestsDot.svg
@@ -0,0 +1,71 @@
+
\ No newline at end of file
diff --git a/test/output/yearlyRequestsLine.svg b/test/output/yearlyRequestsLine.svg
new file mode 100644
index 00000000000..6d2b0fe7c54
--- /dev/null
+++ b/test/output/yearlyRequestsLine.svg
@@ -0,0 +1,110 @@
+
\ No newline at end of file
diff --git a/test/plots/downloads-ordinal.js b/test/plots/downloads-ordinal.js
new file mode 100644
index 00000000000..f2491f51a04
--- /dev/null
+++ b/test/plots/downloads-ordinal.js
@@ -0,0 +1,21 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function() {
+ const downloads = (await d3.csv("data/downloads.csv", d3.autoType))
+ .filter(d => d.date.getUTCFullYear() === 2019 && d.date.getUTCMonth() <= 1 && d.downloads > 0);
+ return Plot.plot({
+ width: 960,
+ marginBottom: 55,
+ x: {
+ interval: d3.utcDay,
+ tickRotate: -90,
+ tickFormat: "%b %d"
+ },
+ marks: [
+ Plot.barY(downloads, {x: "date", y: "downloads", fill: "#ccc"}),
+ Plot.tickY(downloads, {x: "date", y: "downloads"}),
+ Plot.ruleY([0])
+ ]
+ });
+}
diff --git a/test/plots/ibm-trading.js b/test/plots/ibm-trading.js
new file mode 100644
index 00000000000..6d1cffc6ddd
--- /dev/null
+++ b/test/plots/ibm-trading.js
@@ -0,0 +1,27 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+// This example uses both an interval (to define an ordinal x-scale) and a
+// custom transform to parse the dates from their string representation. This is
+// not a recommended pattern: you should instead parse strings to dates when
+// loading the data, say by applying d3.autoType or calling array.map.
+export default async function() {
+ const ibm = await d3.csv("data/ibm.csv").then(data => data.slice(-20));
+ return Plot.plot({
+ marginBottom: 65,
+ x: {
+ interval: d3.utcDay,
+ transform: d => d3.utcDay.floor(d3.isoParse(d)),
+ tickRotate: -40,
+ label: null
+ },
+ y: {
+ transform: d => d / 1e6,
+ label: "↑ Volume (USD, millions)",
+ grid: true
+ },
+ marks: [
+ Plot.barY(ibm, {x: "Date", y: "Volume"})
+ ]
+ });
+}
diff --git a/test/plots/index.js b/test/plots/index.js
index 990a10e3b1d..436b34e49bf 100644
--- a/test/plots/index.js
+++ b/test/plots/index.js
@@ -53,6 +53,7 @@ export {default as documentationLinks} from "./documentation-links.js";
export {default as dodgeTextRadius} from "./dodge-text-radius.js";
export {default as dotSort} from "./dot-sort.js";
export {default as downloads} from "./downloads.js";
+export {default as downloadsOrdinal} from "./downloads-ordinal.js";
export {default as driving} from "./driving.js";
export {default as empty} from "./empty.js";
export {default as emptyLegend} from "./empty-legend.js";
@@ -84,6 +85,8 @@ export {default as hexbinZ} from "./hexbin-z.js";
export {default as hexbinZNull} from "./hexbin-z-null.js";
export {default as highCardinalityOrdinal} from "./high-cardinality-ordinal.js";
export {default as identityScale} from "./identity-scale.js";
+export {default as integerInterval} from "./integer-interval.js";
+export {default as ibmTrading} from "./ibm-trading.js";
export {default as industryUnemployment} from "./industry-unemployment.js";
export {default as industryUnemploymentShare} from "./industry-unemployment-share.js";
export {default as industryUnemploymentStream} from "./industry-unemployment-stream.js";
@@ -211,6 +214,9 @@ export {default as wealthBritainBar} from "./wealth-britain-bar.js";
export {default as wealthBritainProportionPlot} from "./wealth-britain-proportion-plot.js";
export {default as wordCloud} from "./word-cloud.js";
export {default as wordLengthMobyDick} from "./word-length-moby-dick.js";
+export {default as yearlyRequests} from "./yearly-requests.js";
+export {default as yearlyRequestsDot} from "./yearly-requests-dot.js";
+export {default as yearlyRequestsLine} from "./yearly-requests-line.js";
export * from "./legend-color.js";
export * from "./legend-opacity.js";
diff --git a/test/plots/integer-interval.js b/test/plots/integer-interval.js
new file mode 100644
index 00000000000..e53b1a3744d
--- /dev/null
+++ b/test/plots/integer-interval.js
@@ -0,0 +1,16 @@
+import * as Plot from "@observablehq/plot";
+
+export default async function() {
+ const requests = [[2, 9], [3, 17], [3.5, 10], [5, 12]];
+ return Plot.plot({
+ x: {
+ interval: 1
+ },
+ y: {
+ zero: true
+ },
+ marks: [
+ Plot.line(requests)
+ ]
+ });
+}
diff --git a/test/plots/yearly-requests-dot.js b/test/plots/yearly-requests-dot.js
new file mode 100644
index 00000000000..02dadf3a93c
--- /dev/null
+++ b/test/plots/yearly-requests-dot.js
@@ -0,0 +1,26 @@
+import * as d3 from "d3";
+import * as Plot from "@observablehq/plot";
+
+export default async function() {
+ const requests = [
+ [new Date("2002-1-01"), 9],
+ [new Date("2003-1-01"), 17],
+ [new Date("2005-1-01"), 5]
+ ];
+ return Plot.plot({
+ label: null,
+ x: {
+ type: "utc",
+ interval: d3.utcYear,
+ inset: 40,
+ grid: true
+ },
+ y: {
+ zero: true
+ },
+ marks: [
+ Plot.ruleY([0]),
+ Plot.dot(requests)
+ ]
+ });
+}
diff --git a/test/plots/yearly-requests-line.js b/test/plots/yearly-requests-line.js
new file mode 100644
index 00000000000..f59a501baf2
--- /dev/null
+++ b/test/plots/yearly-requests-line.js
@@ -0,0 +1,32 @@
+import * as Plot from "@observablehq/plot";
+
+export default async function() {
+ const requests = [
+ [2002, 9],
+ [2003, 17],
+ [2004, 12],
+ [2005, 5],
+ [2006, 12],
+ [2007, 18],
+ [2008, 16],
+ [2009, 11],
+ [2010, 9],
+ [2011, 8],
+ [2012, 9],
+ [2019, 20]
+ ];
+ return Plot.plot({
+ label: null,
+ x: {
+ interval: 1,
+ tickFormat: "", // TODO https://github.com/observablehq/plot/issues/768
+ inset: 20
+ },
+ y: {
+ zero: true
+ },
+ marks: [
+ Plot.line(requests)
+ ]
+ });
+}
diff --git a/test/plots/yearly-requests.js b/test/plots/yearly-requests.js
new file mode 100644
index 00000000000..a3c5ed60842
--- /dev/null
+++ b/test/plots/yearly-requests.js
@@ -0,0 +1,28 @@
+import * as Plot from "@observablehq/plot";
+
+export default async function() {
+ const requests = [
+ [2002, 9],
+ [2003, 17],
+ [2004, 12],
+ [2005, 5],
+ [2006, 12],
+ [2007, 18],
+ [2008, 16],
+ [2009, 11],
+ [2010, 9],
+ [2011, 8],
+ [2012, 9],
+ [2019, 20]
+ ];
+ return Plot.plot({
+ label: null,
+ x: {
+ interval: 1,
+ tickFormat: "" // TODO https://github.com/observablehq/plot/issues/768
+ },
+ marks: [
+ Plot.barY(requests, {x: "0", y: "1"})
+ ]
+ });
+}
diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js
index 755274137bc..72332f2319b 100644
--- a/test/scales/scales-test.js
+++ b/test/scales/scales-test.js
@@ -1408,6 +1408,52 @@ it("plot(…).scale(name) reflects the given custom interpolator", async () => {
});
});
+it("plot(…).scale(name).interval changes the domain and sets the transform option for ordinal scales", async () => {
+ const requests = [[2002,9],[2003,17],[2004,12],[2005,5],[2006,12],[2007,18],[2008,16],[2009,11],[2010,9],[2011,8],[2012,9],[2019,20]];
+ const plot = Plot.barY(requests, {x: "0", y: "1"}).plot({x: {interval: 1}});
+ scaleEqual(plot.scale("x"), {
+ align: 0.5,
+ bandwidth: 29,
+ domain: d3.range(2002, 2020),
+ interval: ["floor", "offset", "range"],
+ label: "0",
+ paddingInner: 0.1,
+ paddingOuter: 0.1,
+ range: [40, 620],
+ round: true,
+ step: 32,
+ type: "band"
+ });
+});
+
+it("plot(…).scale(name).interval reflects the interval option for quantitative scales", async () => {
+ const penguins = await d3.csv("data/penguins.csv", d3.autoType);
+ const plot = Plot.dotX(penguins, {x: "body_mass_g"}).plot({x: {interval: 50}});
+ scaleEqual(plot.scale("x"), {
+ clamp: false,
+ domain: [2700, 6300],
+ interpolate: d3.interpolateNumber,
+ interval: ["floor", "offset", "range"],
+ label: "body_mass_g →",
+ range: [20, 620],
+ type: "linear"
+ });
+});
+
+it("The interval option is reusable for ordinal scales", async () => {
+ const requests = [[2002,9],[2003.5,17],[2005.9,5]];
+ const plot1 = Plot.barY(requests, {x: "0", y: "1"}).plot({x: {interval: 1}, className: "a"});
+ const plot2 = Plot.barY(requests, {x: "0", y: "1"}).plot({x: plot1.scale("x"), className: "a"});
+ assert.strictEqual(plot1.innerHTML, plot2.innerHTML);
+});
+
+it("The interval option is reusable for quantitative scales", async () => {
+ const requests = [[2002,9],[2003.5,17],[2005.9,5]];
+ const plot1 = Plot.dot(requests, {x: "0", y: "1"}).plot({x: {interval: 1}, className: "a"});
+ const plot2 = Plot.dot(requests, {x: "0", y: "1"}).plot({x: plot1.scale("x"), className: "a"});
+ assert.strictEqual(plot1.innerHTML, plot2.innerHTML);
+});
+
it("plot(…).scale('color') allows a range to be specified in conjunction with a scheme", async () => {
const gistemp = await d3.csv("data/gistemp.csv", d3.autoType);
const plot = Plot.dot(gistemp, {x: "Date", fill: "Anomaly"}).plot({color: {range: [0, 0.5], scheme: "cool"}});
@@ -1500,6 +1546,7 @@ function scaleEqual({...scale}, spec) {
} else {
delete scale.apply;
}
+ if (scale.interval) scale.interval = Object.keys(scale.interval);
if (typeof scale.invert !== "function" && !(["band", "point", "threshold", "ordinal", "diverging", "diverging-log", "diverging-symlog", "diverging-pow" ].includes(scale.type))) {
scale.invert = typeof scale.invert;
} else {