diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3000818eeb..1a35534a6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,32 +1,40 @@
 # Observable Plot - Changelog
 
-## 0.2.1
+## 0.2.3
+
+*Not yet released.* These notes are a work in progress.
+
+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.
 
-### Marks
+Fix a crash with the axis.tickRotate option when there are no ticks to rotate.
 
-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.
+## 0.2.1
 
-### Scales
+Released September 19, 2021.
 
-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).
+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.
 
-### Transforms
+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).
 
-#### Plot.bin
+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:
 
-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()
 ```
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/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/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/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/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
new file mode 100644
index 0000000000..1dac682957
--- /dev/null
+++ b/src/transforms/interval.js
@@ -0,0 +1,47 @@
+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
+// 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;
+}
+
+// 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, 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 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", maybeInsetX, options);
+}
+
+export function maybeIntervalY(options = {}) {
+  return maybeIntervalK("y", maybeInsetY, options);
+}
diff --git a/test/output/aaplVolumeRect.svg b/test/output/aaplVolumeRect.svg
new file mode 100644
index 0000000000..fee5740556
--- /dev/null
+++ b/test/output/aaplVolumeRect.svg
@@ -0,0 +1,173 @@
+<svg class="plot" fill="currentColor" text-anchor="middle" width="640" height="400" viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g transform="translate(40,0)" fill="none" text-anchor="end">
+    <g class="tick" opacity="1" transform="translate(0,370.5)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">0</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,344.199792904655)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">5</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,317.89958580931)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">10</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,291.59937871396494)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">15</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,265.29917161861994)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">20</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,238.99896452327494)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">25</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,212.6987574279299)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">30</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,186.3985503325849)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">35</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,160.09834323723987)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">40</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,133.7981361418949)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">45</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,107.49792904654987)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">50</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,81.19772195120487)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">55</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,54.89751485585984)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">60</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(0,28.59730776051481)">
+      <line stroke="currentColor" x2="-6"></line>
+      <line stroke="currentColor" x2="580" stroke-opacity="0.1"></line><text fill="currentColor" x="-9" dy="0.32em">65</text>
+    </g><text fill="currentColor" transform="translate(-40,20)" dy="-1em" text-anchor="start">↑ Daily trade volume (millions)</text>
+  </g>
+  <g transform="translate(0,370)" fill="none" text-anchor="middle">
+    <g class="tick" opacity="1" transform="translate(60.85087719298246,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Mar 18</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(132.07894736842104,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Mar 25</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(203.30701754385964,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">April</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(274.53508771929825,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Apr 08</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(345.7631578947369,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Apr 15</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(416.99122807017545,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Apr 22</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(488.21929824561397,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">Apr 29</text>
+    </g>
+    <g class="tick" opacity="1" transform="translate(559.4473684210526,0)">
+      <line stroke="currentColor" y2="6"></line><text fill="currentColor" y="9" dy="0.71em">May 06</text>
+    </g>
+  </g>
+  <g fill="#ccc">
+    <rect x="41" y="162.72964589401167" width="9.175438596491219" height="207.27035410598833"></rect>
+    <rect x="71.52631578947368" y="194.06844666468288" width="9.175438596491219" height="175.93155333531712"></rect>
+    <rect x="81.7017543859649" y="266.64334214014553" width="9.175438596491233" height="103.35665785985447"></rect>
+    <rect x="91.87719298245614" y="175.08969122054" width="9.175438596491233" height="194.91030877946"></rect>
+    <rect x="102.05263157894737" y="151.7566734896918" width="9.175438596491219" height="218.2433265103082"></rect>
+    <rect x="112.22807017543859" y="154.1868126253017" width="9.175438596491233" height="215.8131873746983"></rect>
+    <rect x="142.75438596491227" y="172.53173307844673" width="9.175438596491233" height="197.46826692155327"></rect>
+    <rect x="152.9298245614035" y="154.74542902400682" width="9.175438596491233" height="215.25457097599318"></rect>
+    <rect x="163.10526315789474" y="150.82196412952325" width="9.175438596491205" height="219.17803587047675"></rect>
+    <rect x="173.28070175438594" y="168.0222995698789" width="9.175438596491233" height="201.9777004301211"></rect>
+    <rect x="213.98245614035088" y="172.29187518973723" width="9.175438596491233" height="197.70812481026277"></rect>
+    <rect x="224.1578947368421" y="210.73646591342873" width="9.175438596491205" height="159.26353408657127"></rect>
+    <rect x="234.33333333333331" y="187.97363667240762" width="9.175438596491233" height="182.02636332759238"></rect>
+    <rect x="244.50877192982455" y="228.3302524519308" width="9.175438596491233" height="141.6697475480692"></rect>
+    <rect x="254.68421052631578" y="185.87067211306385" width="9.175438596491233" height="184.12932788693615"></rect>
+    <rect x="285.2105263157894" y="217.3656961138814" width="9.17543859649129" height="152.6343038861186"></rect>
+    <rect x="295.3859649122807" y="220.56958734223633" width="9.175438596491176" height="149.43041265776367"></rect>
+    <rect x="305.5614035087719" y="252.00885490401177" width="9.175438596491233" height="117.99114509598823"></rect>
+    <rect x="315.7368421052631" y="249.60133394650387" width="9.175438596491233" height="120.39866605349613"></rect>
+    <rect x="325.91228070175436" y="237.84514137488463" width="9.17543859649129" height="132.15485862511537"></rect>
+    <rect x="356.4385964912281" y="256.49672224276145" width="9.175438596491176" height="113.50327775723855"></rect>
+    <rect x="366.6140350877193" y="230.05449402910156" width="9.17543859649129" height="139.94550597089844"></rect>
+    <rect x="376.7894736842106" y="260.83047036793243" width="9.175438596491176" height="109.16952963206757"></rect>
+    <rect x="386.96491228070175" y="186.9042702519109" width="9.175438596491233" height="183.0957297480891"></rect>
+    <rect x="397.140350877193" y="25.514101419610007" width="9.175438596491233" height="344.48589858038997"></rect>
+    <rect x="427.66666666666663" y="177.9269575619858" width="9.17543859649129" height="192.0730424380142"></rect>
+    <rect x="437.8421052631579" y="192.77868450872714" width="9.175438596491176" height="177.22131549127286"></rect>
+    <rect x="448.0175438596491" y="220.70897843984167" width="9.175438596491233" height="149.29102156015833"></rect>
+    <rect x="458.1929824561403" y="222.91346179857345" width="9.175438596491233" height="147.08653820142655"></rect>
+    <rect x="468.36842105263156" y="182.44901516995947" width="9.175438596491233" height="187.55098483004053"></rect>
+    <rect x="498.89473684210526" y="146.83011869659182" width="9.175438596491176" height="223.16988130340818"></rect>
+    <rect x="509.07017543859644" y="88.22273720532496" width="9.175438596491233" height="281.77726279467504"></rect>
+    <rect x="519.2456140350877" y="20" width="9.175438596491176" height="350"></rect>
+    <rect x="529.4210526315788" y="190.79985692687342" width="9.175438596491404" height="179.20014307312658"></rect>
+    <rect x="539.5964912280702" y="74.37883419447726" width="9.175438596491176" height="295.62116580552276"></rect>
+    <rect x="570.1228070175438" y="146.70387770253413" width="9.17543859649129" height="223.29612229746587"></rect>
+    <rect x="580.2982456140351" y="220.60009558246693" width="9.175438596491176" height="149.39990441753307"></rect>
+    <rect x="590.4736842105262" y="247.90812661370552" width="9.17543859649129" height="122.09187338629448"></rect>
+    <rect x="600.6491228070175" y="222.77512270925197" width="9.175438596491176" height="147.22487729074803"></rect>
+    <rect x="610.8245614035087" y="234.25621511465388" width="9.17543859649129" height="135.74378488534612"></rect>
+  </g>
+  <g stroke="currentColor" transform="translate(0,0.5)">
+    <line x1="41" x2="50.17543859649122" y1="162.72964589401167" y2="162.72964589401167"></line>
+    <line x1="71.52631578947368" x2="80.7017543859649" y1="194.06844666468288" y2="194.06844666468288"></line>
+    <line x1="81.7017543859649" x2="90.87719298245614" y1="266.64334214014553" y2="266.64334214014553"></line>
+    <line x1="91.87719298245614" x2="101.05263157894737" y1="175.08969122054" y2="175.08969122054"></line>
+    <line x1="102.05263157894737" x2="111.22807017543859" y1="151.7566734896918" y2="151.7566734896918"></line>
+    <line x1="112.22807017543859" x2="121.40350877192982" y1="154.1868126253017" y2="154.1868126253017"></line>
+    <line x1="142.75438596491227" x2="151.9298245614035" y1="172.53173307844673" y2="172.53173307844673"></line>
+    <line x1="152.9298245614035" x2="162.10526315789474" y1="154.74542902400682" y2="154.74542902400682"></line>
+    <line x1="163.10526315789474" x2="172.28070175438594" y1="150.82196412952325" y2="150.82196412952325"></line>
+    <line x1="173.28070175438594" x2="182.45614035087718" y1="168.0222995698789" y2="168.0222995698789"></line>
+    <line x1="213.98245614035088" x2="223.1578947368421" y1="172.29187518973723" y2="172.29187518973723"></line>
+    <line x1="224.1578947368421" x2="233.33333333333331" y1="210.73646591342873" y2="210.73646591342873"></line>
+    <line x1="234.33333333333331" x2="243.50877192982455" y1="187.97363667240762" y2="187.97363667240762"></line>
+    <line x1="244.50877192982455" x2="253.68421052631578" y1="228.3302524519308" y2="228.3302524519308"></line>
+    <line x1="254.68421052631578" x2="263.859649122807" y1="185.87067211306385" y2="185.87067211306385"></line>
+    <line x1="285.2105263157894" x2="294.3859649122807" y1="217.3656961138814" y2="217.3656961138814"></line>
+    <line x1="295.3859649122807" x2="304.5614035087719" y1="220.56958734223633" y2="220.56958734223633"></line>
+    <line x1="305.5614035087719" x2="314.7368421052631" y1="252.00885490401177" y2="252.00885490401177"></line>
+    <line x1="315.7368421052631" x2="324.91228070175436" y1="249.60133394650387" y2="249.60133394650387"></line>
+    <line x1="325.91228070175436" x2="335.08771929824564" y1="237.84514137488463" y2="237.84514137488463"></line>
+    <line x1="356.4385964912281" x2="365.6140350877193" y1="256.49672224276145" y2="256.49672224276145"></line>
+    <line x1="366.6140350877193" x2="375.7894736842106" y1="230.05449402910156" y2="230.05449402910156"></line>
+    <line x1="376.7894736842106" x2="385.96491228070175" y1="260.83047036793243" y2="260.83047036793243"></line>
+    <line x1="386.96491228070175" x2="396.140350877193" y1="186.9042702519109" y2="186.9042702519109"></line>
+    <line x1="397.140350877193" x2="406.3157894736842" y1="25.514101419610007" y2="25.514101419610007"></line>
+    <line x1="427.66666666666663" x2="436.8421052631579" y1="177.9269575619858" y2="177.9269575619858"></line>
+    <line x1="437.8421052631579" x2="447.0175438596491" y1="192.77868450872714" y2="192.77868450872714"></line>
+    <line x1="448.0175438596491" x2="457.1929824561403" y1="220.70897843984167" y2="220.70897843984167"></line>
+    <line x1="458.1929824561403" x2="467.36842105263156" y1="222.91346179857345" y2="222.91346179857345"></line>
+    <line x1="468.36842105263156" x2="477.5438596491228" y1="182.44901516995947" y2="182.44901516995947"></line>
+    <line x1="498.89473684210526" x2="508.07017543859644" y1="146.83011869659182" y2="146.83011869659182"></line>
+    <line x1="509.07017543859644" x2="518.2456140350877" y1="88.22273720532496" y2="88.22273720532496"></line>
+    <line x1="519.2456140350877" x2="528.4210526315788" y1="20" y2="20"></line>
+    <line x1="529.4210526315788" x2="538.5964912280702" y1="190.79985692687342" y2="190.79985692687342"></line>
+    <line x1="539.5964912280702" x2="548.7719298245614" y1="74.37883419447726" y2="74.37883419447726"></line>
+    <line x1="570.1228070175438" x2="579.2982456140351" y1="146.70387770253413" y2="146.70387770253413"></line>
+    <line x1="580.2982456140351" x2="589.4736842105262" y1="220.60009558246693" y2="220.60009558246693"></line>
+    <line x1="590.4736842105262" x2="599.6491228070175" y1="247.90812661370552" y2="247.90812661370552"></line>
+    <line x1="600.6491228070175" x2="609.8245614035087" y1="222.77512270925197" y2="222.77512270925197"></line>
+    <line x1="610.8245614035087" x2="620" y1="234.25621511465388" y2="234.25621511465388"></line>
+  </g>
+  <g stroke="currentColor" transform="translate(0,0.5)">
+    <line x1="40" x2="620" y1="370" y2="370"></line>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/test/plots/aapl-volume-rect.js b/test/plots/aapl-volume-rect.js
new file mode 100644
index 0000000000..26296d5a5e
--- /dev/null
+++ b/test/plots/aapl-volume-rect.js
@@ -0,0 +1,18 @@
+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(-40);
+  return Plot.plot({
+    y: {
+      grid: true,
+      transform: d => d / 1e6,
+      label: "↑ Daily trade volume (millions)"
+    },
+    marks: [
+      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])
+    ]
+  });
+}
diff --git a/test/plots/index.js b/test/plots/index.js
index 33b7797717..9d4da351a8 100644
--- a/test/plots/index.js
+++ b/test/plots/index.js
@@ -5,6 +5,7 @@ export {default as aaplClose} from "./aapl-close.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";