From 30f621654cfc60f7cac700ac6b86ab3bc065c42c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Sep 2021 11:16:27 -0700 Subject: [PATCH 1/6] interval for rect --- src/marks/rect.js | 7 +- src/transforms/interval.js | 41 +++++++++ test/output/aaplCloseRect.svg | 153 ++++++++++++++++++++++++++++++++++ test/plots/aapl-close-rect.js | 15 ++++ test/plots/index.js | 1 + 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/transforms/interval.js create mode 100644 test/output/aaplCloseRect.svg create mode 100644 test/plots/aapl-close-rect.js diff --git a/src/marks/rect.js b/src/marks/rect.js index f303751771..ba8b625e24 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -3,6 +3,7 @@ import {filter} from "../defined.js"; import {Mark, number} from "../mark.js"; import {isCollapsed} from "../scales.js"; import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js"; +import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; const defaults = {}; @@ -64,13 +65,13 @@ export class Rect extends Mark { } export function rect(data, options) { - return new Rect(data, options); + return new Rect(data, maybeIntervalX(maybeIntervalY(options))); } export function rectX(data, options) { - return new Rect(data, maybeStackX(options)); + return new Rect(data, maybeStackX(maybeIntervalY(options))); } export function rectY(data, options) { - return new Rect(data, maybeStackY(options)); + return new Rect(data, maybeStackY(maybeIntervalX(options))); } diff --git a/src/transforms/interval.js b/src/transforms/interval.js new file mode 100644 index 0000000000..a7675244fe --- /dev/null +++ b/src/transforms/interval.js @@ -0,0 +1,41 @@ +import {labelof, maybeValue, valueof} from "../mark.js"; + +// TODO Allow the interval to be specified as a string, e.g. “day” or “hour”? +// This will require the interval knowing the type of the associated scale to +// chose between UTC and local time (or better, an explicit timeZone option). +function maybeInterval(interval) { + if (interval == null) return; + if (typeof interval.floor !== "function" || typeof interval.offset !== "function") throw new Error("invalid interval"); + return interval; +} + +// The interval may be specified either as x: {value, interval} or as {x, +// interval}. The former is used, for example, for Plot.rect. +function maybeIntervalValue(value, {interval} = {}) { + (value = {...maybeValue(value)}); + value.interval = maybeInterval(value.interval === undefined ? interval : value.interval); + return value; +} + +function maybeIntervalK(k, options = {}) { + const {[k]: v, [`${k}1`]: v1, [`${k}2`]: v2} = options; + const {value, interval} = maybeIntervalValue(v, options); + if (interval == null) return options; + let V1; + const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v))); + const label = labelof(v); + return { + ...options, + [k]: undefined, + [`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1, + [`${k}2`]: v2 === undefined ? {transform: () => tv1().map(v => interval.offset(v)), label} : v2 + }; +} + +export function maybeIntervalX(options) { + return maybeIntervalK("x", options); +} + +export function maybeIntervalY(options = {}) { + return maybeIntervalK("y", options); +} diff --git a/test/output/aaplCloseRect.svg b/test/output/aaplCloseRect.svg new file mode 100644 index 0000000000..e736e4e50a --- /dev/null +++ b/test/output/aaplCloseRect.svg @@ -0,0 +1,153 @@ + + + + + 0 + + + + 20 + + + + 40 + + + + 60 + + + + 80 + + + + 100 + + + + 120 + + + + 140 + + + + 160 + + + + 180 + ↑ Close + + + + February + + + March + + + April + + + May + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/aapl-close-rect.js b/test/plots/aapl-close-rect.js new file mode 100644 index 0000000000..6f1e55543c --- /dev/null +++ b/test/plots/aapl-close-rect.js @@ -0,0 +1,15 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const AAPL = (await d3.csv("data/aapl.csv", d3.autoType)).slice(-90); + return Plot.plot({ + y: { + grid: true + }, + marks: [ + Plot.rectY(AAPL, {x: "Date", interval: d3.utcDay, y: "Close"}), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 33b7797717..c29db8f99c 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -2,6 +2,7 @@ export {default as aaplBollinger} from "./aapl-bollinger.js"; export {default as aaplCandlestick} from "./aapl-candlestick.js"; export {default as aaplChangeVolume} from "./aapl-change-volume.js"; export {default as aaplClose} from "./aapl-close.js"; +export {default as aaplCloseRect} from "./aapl-close-rect.js"; export {default as aaplCloseUntyped} from "./aapl-close-untyped.js"; export {default as aaplMonthly} from "./aapl-monthly.js"; export {default as aaplVolume} from "./aapl-volume.js"; From 827000c900c3cbc467a4c4b382f3d99028262473 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Sep 2021 13:48:15 -0700 Subject: [PATCH 2/6] default insets for intervals --- src/transforms/bin.js | 30 ++-- src/transforms/inset.js | 17 ++ src/transforms/interval.js | 11 +- test/output/aaplCloseRect.svg | 153 ------------------ test/output/aaplVolumeRect.svg | 131 +++++++++++++++ ...aapl-close-rect.js => aapl-volume-rect.js} | 8 +- test/plots/index.js | 2 +- 7 files changed, 169 insertions(+), 183 deletions(-) create mode 100644 src/transforms/inset.js delete mode 100644 test/output/aaplCloseRect.svg create mode 100644 test/output/aaplVolumeRect.svg rename test/plots/{aapl-close-rect.js => aapl-volume-rect.js} (59%) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 1a9397bf1b..09d96b6469 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,31 +1,25 @@ import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3"; import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js"; -import {offset} from "../style.js"; import {basic} from "./basic.js"; import {maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js"; +import {maybeInsetX, maybeInsetY} from "./inset.js"; // Group on {z, fill, stroke}, then optionally on y, then bin x. -export function binX(outputs = {y: "count"}, {inset, insetLeft, insetRight, ...options} = {}) { - let {x, y} = options; - x = maybeBinValue(x, options, identity); - ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - return binn(x, null, null, y, outputs, {inset, insetLeft, insetRight, ...options}); +export function binX(outputs = {y: "count"}, options = {}) { + const {x, y} = options; + return binn(maybeBinValue(x, options, identity), null, null, y, outputs, maybeInsetX(options)); } // Group on {z, fill, stroke}, then optionally on x, then bin y. -export function binY(outputs = {x: "count"}, {inset, insetTop, insetBottom, ...options} = {}) { - let {x, y} = options; - y = maybeBinValue(y, options, identity); - ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); - return binn(null, y, x, null, outputs, {inset, insetTop, insetBottom, ...options}); +export function binY(outputs = {x: "count"}, options = {}) { + const {x, y} = options; + return binn(null, maybeBinValue(y, options, identity), x, null, outputs, maybeInsetY(options)); } // Group on {z, fill, stroke}, then bin on x and y. -export function bin(outputs = {fill: "count"}, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { +export function bin(outputs = {fill: "count"}, options = {}) { const {x, y} = maybeBinValueTuple(options); - ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); - ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options}); + return binn(x, y, null, null, outputs, maybeInsetX(maybeInsetY(options))); } function binn( @@ -252,9 +246,3 @@ function binfilter([{x0, x1}, set]) { function binempty() { return new Uint32Array(0); } - -function maybeInset(inset, inset1, inset2) { - return inset === undefined && inset1 === undefined && inset2 === undefined - ? (offset ? [1, 0] : [0.5, 0.5]) - : [inset1, inset2]; -} diff --git a/src/transforms/inset.js b/src/transforms/inset.js new file mode 100644 index 0000000000..46cd750d44 --- /dev/null +++ b/src/transforms/inset.js @@ -0,0 +1,17 @@ +import {offset} from "../style.js"; + +export function maybeInsetX({inset, insetLeft, insetRight, ...options} = {}) { + ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); + return {inset, insetLeft, insetRight, ...options}; +} + +export function maybeInsetY({inset, insetTop, insetBottom, ...options} = {}) { + ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); + return {inset, insetTop, insetBottom, ...options}; +} + +function maybeInset(inset, inset1, inset2) { + return inset === undefined && inset1 === undefined && inset2 === undefined + ? (offset ? [1, 0] : [0.5, 0.5]) + : [inset1, inset2]; +} diff --git a/src/transforms/interval.js b/src/transforms/interval.js index a7675244fe..a84e061713 100644 --- a/src/transforms/interval.js +++ b/src/transforms/interval.js @@ -1,4 +1,5 @@ import {labelof, maybeValue, valueof} from "../mark.js"; +import {maybeInsetX, maybeInsetY} from "./inset.js"; // TODO Allow the interval to be specified as a string, e.g. “day” or “hour”? // This will require the interval knowing the type of the associated scale to @@ -17,25 +18,25 @@ function maybeIntervalValue(value, {interval} = {}) { return value; } -function maybeIntervalK(k, options = {}) { +function maybeIntervalK(k, maybeInsetK, options = {}) { const {[k]: v, [`${k}1`]: v1, [`${k}2`]: v2} = options; const {value, interval} = maybeIntervalValue(v, options); if (interval == null) return options; let V1; const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v))); const label = labelof(v); - return { + return maybeInsetK({ ...options, [k]: undefined, [`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1, [`${k}2`]: v2 === undefined ? {transform: () => tv1().map(v => interval.offset(v)), label} : v2 - }; + }); } export function maybeIntervalX(options) { - return maybeIntervalK("x", options); + return maybeIntervalK("x", maybeInsetX, options); } export function maybeIntervalY(options = {}) { - return maybeIntervalK("y", options); + return maybeIntervalK("y", maybeInsetY, options); } diff --git a/test/output/aaplCloseRect.svg b/test/output/aaplCloseRect.svg deleted file mode 100644 index e736e4e50a..0000000000 --- a/test/output/aaplCloseRect.svg +++ /dev/null @@ -1,153 +0,0 @@ - - - - - 0 - - - - 20 - - - - 40 - - - - 60 - - - - 80 - - - - 100 - - - - 120 - - - - 140 - - - - 160 - - - - 180 - ↑ Close - - - - February - - - March - - - April - - - May - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/output/aaplVolumeRect.svg b/test/output/aaplVolumeRect.svg new file mode 100644 index 0000000000..8b767e80ae --- /dev/null +++ b/test/output/aaplVolumeRect.svg @@ -0,0 +1,131 @@ + + + + + 0 + + + + 5 + + + + 10 + + + + 15 + + + + 20 + + + + 25 + + + + 30 + + + + 35 + + + + 40 + + + + 45 + + + + 50 + + + + 55 + + + + 60 + + + + 65 + ↑ Daily trade volume (millions) + + + + Mar 18 + + + Mar 25 + + + April + + + Apr 08 + + + Apr 15 + + + Apr 22 + + + Apr 29 + + + May 06 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/aapl-close-rect.js b/test/plots/aapl-volume-rect.js similarity index 59% rename from test/plots/aapl-close-rect.js rename to test/plots/aapl-volume-rect.js index 6f1e55543c..60f5a14254 100644 --- a/test/plots/aapl-close-rect.js +++ b/test/plots/aapl-volume-rect.js @@ -2,13 +2,15 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; export default async function() { - const AAPL = (await d3.csv("data/aapl.csv", d3.autoType)).slice(-90); + const AAPL = (await d3.csv("data/aapl.csv", d3.autoType)).slice(-40); return Plot.plot({ y: { - grid: true + grid: true, + transform: d => d / 1e6, + label: "↑ Daily trade volume (millions)" }, marks: [ - Plot.rectY(AAPL, {x: "Date", interval: d3.utcDay, y: "Close"}), + Plot.rectY(AAPL, {x: "Date", interval: d3.utcDay, y: "Volume"}), Plot.ruleY([0]) ] }); diff --git a/test/plots/index.js b/test/plots/index.js index c29db8f99c..9d4da351a8 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -2,10 +2,10 @@ export {default as aaplBollinger} from "./aapl-bollinger.js"; export {default as aaplCandlestick} from "./aapl-candlestick.js"; export {default as aaplChangeVolume} from "./aapl-change-volume.js"; export {default as aaplClose} from "./aapl-close.js"; -export {default as aaplCloseRect} from "./aapl-close-rect.js"; export {default as aaplCloseUntyped} from "./aapl-close-untyped.js"; export {default as aaplMonthly} from "./aapl-monthly.js"; export {default as aaplVolume} from "./aapl-volume.js"; +export {default as aaplVolumeRect} from "./aapl-volume-rect.js"; export {default as anscombeQuartet} from "./anscombe-quartet.js"; export {default as athletesHeightWeight} from "./athletes-height-weight.js"; export {default as athletesHeightWeightBin} from "./athletes-height-weight-bin.js"; From f7c29ad954eddef2698932a63a59df6d815b6b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 24 Sep 2021 15:31:05 +0200 Subject: [PATCH 3/6] Update src/transforms/interval.js --- src/transforms/interval.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/interval.js b/src/transforms/interval.js index a84e061713..0701e584ca 100644 --- a/src/transforms/interval.js +++ b/src/transforms/interval.js @@ -13,7 +13,7 @@ function maybeInterval(interval) { // The interval may be specified either as x: {value, interval} or as {x, // interval}. The former is used, for example, for Plot.rect. function maybeIntervalValue(value, {interval} = {}) { - (value = {...maybeValue(value)}); + value = {...maybeValue(value)}; value.interval = maybeInterval(value.interval === undefined ? interval : value.interval); return value; } From cb26d3fff9cf85843fbce3c1fe72499d02b21a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 24 Sep 2021 15:57:36 +0200 Subject: [PATCH 4/6] numeric interval & documentation (#552) --- CHANGELOG.md | 8 ++++++++ README.md | 4 +++- src/transforms/interval.js | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3000818eeb..54422483cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Observable Plot - Changelog +## 0.3.0 + +*Not yet released.* These notes are a work in progress. + +### Marks + +The rect marks now accept an *interval* option that allows to derive *x1* and *x2* from *x*. A typical use case is an interval: d3.utcDay which creates a rect spanning the whole day that contains a certain date-time. The interval can be specified as an object with *floor** method that returns *x1* from *x* and an **offset** method that returns *x2* from *x1*. If the interval is specified as a (non-null) number, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. + ## 0.2.1 Released September 19, 2021. diff --git a/README.md b/README.md index 13dd900356..0a650d7d28 100644 --- a/README.md +++ b/README.md @@ -801,7 +801,9 @@ The following channels are optional: * **x2** - the ending horizontal position; bound to the *x* scale * **y2** - the ending vertical position; bound to the *y* scale -Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both. The rect mark supports the [standard mark options](#marks), including insets and rounded corners. The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise. +Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both. **x1** and **x2** can be derived from **x** and an **interval** object (such as d3.utcDay) with a **floor** method that returns *x1* from *x* and an **offset** method that returns *x2* from *x1*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. The interval may be specified either as as {x, interval} or x: {value, interval}—typically to apply different intervals to x and y. + +The rect mark supports the [standard mark options](#marks), including insets and rounded corners. The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise. #### Plot.rect(*data*, *options*) diff --git a/src/transforms/interval.js b/src/transforms/interval.js index 0701e584ca..1dac682957 100644 --- a/src/transforms/interval.js +++ b/src/transforms/interval.js @@ -6,6 +6,11 @@ import {maybeInsetX, maybeInsetY} from "./inset.js"; // chose between UTC and local time (or better, an explicit timeZone option). function maybeInterval(interval) { if (interval == null) return; + if (typeof interval === "number") { + const n = interval; + // Note: this offset doesn’t support the optional step argument for simplicity. + interval = {floor: d => n * Math.floor(d / n), offset: d => d + n}; + } if (typeof interval.floor !== "function" || typeof interval.offset !== "function") throw new Error("invalid interval"); return interval; } From e45860155a1fc215b1501fb54cf1ed1f34d02bb6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 24 Sep 2021 10:25:06 -0700 Subject: [PATCH 5/6] interval for rule and bar --- src/marks/bar.js | 5 ++-- src/marks/rule.js | 11 +++++---- test/output/aaplVolumeRect.svg | 44 +++++++++++++++++++++++++++++++++- test/plots/aapl-volume-rect.js | 3 ++- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/marks/bar.js b/src/marks/bar.js index 9b223edadc..d814ceb4ea 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -3,6 +3,7 @@ import {filter} from "../defined.js"; import {Mark, number} from "../mark.js"; import {isCollapsed} from "../scales.js"; import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js"; +import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; const defaults = {}; @@ -116,9 +117,9 @@ export class BarY extends AbstractBar { } export function barX(data, options) { - return new BarX(data, maybeStackX(options)); + return new BarX(data, maybeStackX(maybeIntervalX(options))); } export function barY(data, options) { - return new BarY(data, maybeStackY(options)); + return new BarY(data, maybeStackY(maybeIntervalY(options))); } diff --git a/src/marks/rule.js b/src/marks/rule.js index bfad5a0067..cab950245d 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -3,6 +3,7 @@ import {filter} from "../defined.js"; import {Mark, identity, number} from "../mark.js"; import {isCollapsed} from "../scales.js"; import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js"; +import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; const defaults = { fill: null, @@ -97,14 +98,16 @@ export class RuleY extends Mark { } } -export function ruleX(data, {x = identity, y, y1, y2, ...options} = {}) { +export function ruleX(data, options) { + let {x = identity, y, y1, y2, ...rest} = maybeIntervalY(options); ([y1, y2] = maybeOptionalZero(y, y1, y2)); - return new RuleX(data, {...options, x, y1, y2}); + return new RuleX(data, {...rest, x, y1, y2}); } -export function ruleY(data, {y = identity, x, x1, x2, ...options} = {}) { +export function ruleY(data, options) { + let {y = identity, x, x1, x2, ...rest} = maybeIntervalX(options); ([x1, x2] = maybeOptionalZero(x, x1, x2)); - return new RuleY(data, {...options, y, x1, x2}); + return new RuleY(data, {...rest, y, x1, x2}); } // For marks specified either as [0, x] or [x1, x2], or nothing. diff --git a/test/output/aaplVolumeRect.svg b/test/output/aaplVolumeRect.svg index 8b767e80ae..fee5740556 100644 --- a/test/output/aaplVolumeRect.svg +++ b/test/output/aaplVolumeRect.svg @@ -83,7 +83,7 @@ May 06 - + @@ -125,6 +125,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plots/aapl-volume-rect.js b/test/plots/aapl-volume-rect.js index 60f5a14254..26296d5a5e 100644 --- a/test/plots/aapl-volume-rect.js +++ b/test/plots/aapl-volume-rect.js @@ -10,7 +10,8 @@ export default async function() { label: "↑ Daily trade volume (millions)" }, marks: [ - Plot.rectY(AAPL, {x: "Date", interval: d3.utcDay, y: "Volume"}), + Plot.rectY(AAPL, {x: "Date", interval: d3.utcDay, y: "Volume", fill: "#ccc"}), + Plot.ruleY(AAPL, {x: "Date", interval: d3.utcDay, y: "Volume"}), Plot.ruleY([0]) ] }); From 17115d53f97bc33fa2c6d710ec69e37ee98200fb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 24 Sep 2021 10:51:26 -0700 Subject: [PATCH 6/6] Update CHANGELOG --- CHANGELOG.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54422483cd..1a35534a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,40 +1,40 @@ # Observable Plot - Changelog -## 0.3.0 +## 0.2.3 *Not yet released.* These notes are a work in progress. -### Marks +Rect, bar, and rule marks now accept an *interval* option that allows to derive *x1* and *x2* from *x*, or *y1* and *y2* from *y*, where appropriate. A typical use case is for data that represents a fixed time interval; for example, using d3.utcDay as the interval creates rects that span a whole day, from UTC midnight to UTC midnight, that contains the associated time instant. The interval must be specifed as an object with two methods: **floor**(*x*) returns the start of the interval *x1* for the given *x*, while **offset**(*x*) returns the end of the interval *x2* for the given interval start *x*. If the interval is specified as a number, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. + +## 0.2.2 + +Released September 19, 2021. -The rect marks now accept an *interval* option that allows to derive *x1* and *x2* from *x*. A typical use case is an interval: d3.utcDay which creates a rect spanning the whole day that contains a certain date-time. The interval can be specified as an object with *floor** method that returns *x1* from *x* and an **offset** method that returns *x2* from *x1*. If the interval is specified as a (non-null) number, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. +Fix a crash with the axis.tickRotate option when there are no ticks to rotate. ## 0.2.1 Released September 19, 2021. -### Marks - The constant *dx* and *dy* options have been extended to all marks, allowing to shift the mark by *dx* pixels horizontally and *dy* pixels vertically. Since only text elements accept the dx and dy properties, in all the other marks these are rendered as a transform (2D transformation) property of the mark’s parent, possibly including a 0.5px offset on low-density screens. -### Scales - Quantitative scales, as well as identity position scales, now coerce channel values to numbers; both null and undefined are coerced to NaN. Similarly, time scales coerce channel values to dates; numbers are assumed to be milliseconds since UNIX epoch, while strings are assumed to be in [ISO 8601 format](https://github.com/mbostock/isoformat/blob/main/README.md#parsedate-fallback). -### Transforms +Bin transform reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles: -#### Plot.bin - -The reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles: ```js Plot.rect( athletes, Plot.bin( { fill: "count", - title: (bin, { x1, x2, y1, y2 }) => - `${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}` + title: (bin, {x1, x2, y1, y2}) => `${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}` }, - { x: "weight", y: "height", inset: 0 } + { + x: "weight", + y: "height", + inset: 0 + } ) ).plot() ```