diff --git a/docs/marks/bar.md b/docs/marks/bar.md
index 9516c3b13b..2dcb68cec7 100644
--- a/docs/marks/bar.md
+++ b/docs/marks/bar.md
@@ -26,7 +26,7 @@ const timeseries = [
# Bar mark
:::tip
-The bar mark is one of several marks in Plot for drawing rectangles; it should be used when one dimension is ordinal and the other is quantitative. See also [rect](./rect.md) and [cell](./cell.md).
+The bar mark is a variant of the [rect mark](./rect.md) for use when one dimension is ordinal and the other is quantitative. See also the [cell mark](./cell.md).
:::
The **bar mark** comes in two orientations: [barY](#barY) extends vertically↑ as in a vertical bar chart or column chart, while [barX](#barX) extends horizontally→. For example, the bar chart below shows the frequency of letters in the English language.
diff --git a/docs/marks/cell.md b/docs/marks/cell.md
index 8a8d19b0dd..6bea26c6ee 100644
--- a/docs/marks/cell.md
+++ b/docs/marks/cell.md
@@ -21,7 +21,7 @@ onMounted(() => {
# Cell mark
:::tip
-The cell mark is one of several marks in Plot for drawing rectangles; it should be used when both dimensions are ordinal. See also [bar](./bar.md) and [rect](./rect.md).
+The cell mark is a variant of the [rect mark](./rect.md) for use when both dimensions are ordinal. See also the [bar mark](./bar.md).
:::
The **cell mark** draws rectangles positioned in two ordinal dimensions. Hence, the plot’s *x* and *y* scales are [band scales](../features/scales.md). Cells typically also have a **fill** color encoding.
diff --git a/docs/marks/rect.md b/docs/marks/rect.md
index a0431fb19f..ef87e6d665 100644
--- a/docs/marks/rect.md
+++ b/docs/marks/rect.md
@@ -26,10 +26,6 @@ onMounted(() => {
# Rect mark
-:::tip
-The rect mark is one of several marks in Plot for drawing rectangles; it should be used when both dimensions are quantitative. See also [bar](./bar.md) and [cell](./cell.md).
-:::
-
The **rect mark** draws axis-aligned rectangles defined by **x1**, **y1**, **x2**, and **y2**. For example, here we display geographic bounding boxes of U.S. counties represented as [*x1*, *y1*, *x2*, *y2*] tuples, where *x1* & *x2* are degrees longitude and *y1* & *y2* are degrees latitude.
:::plot defer https://observablehq.com/@observablehq/plot-county-boxes
@@ -199,7 +195,7 @@ 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.
+If **x1** is specified but **x2** is not specified, then *x* must be a *band* scale; if **y1** is specified but **y2** is not specified, then *y* must be a *band* scale.
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each **x** to produce **x1**, and *interval*.offset(*x1*) is invoked for each **x1** to produce **x2**. The same is true for **y**, **y1**, and **y2**, respectively. If the interval is specified as a number *n*, **x1** and **x2** are taken as the two consecutive multiples of *n* that bracket **x**. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
diff --git a/src/marks/rect.js b/src/marks/rect.js
index 6cc2a1ac48..7dee9fbcf1 100644
--- a/src/marks/rect.js
+++ b/src/marks/rect.js
@@ -30,8 +30,8 @@ export class Rect extends Mark {
super(
data,
{
- x1: {value: x1, scale: "x", optional: true},
- y1: {value: y1, scale: "y", optional: true},
+ x1: {value: x1, scale: "x", type: x1 != null && x2 == null ? "band" : undefined, optional: true},
+ y1: {value: y1, scale: "y", type: y1 != null && y2 == null ? "band" : undefined, optional: true},
x2: {value: x2, scale: "x", optional: true},
y2: {value: y2, scale: "y", optional: true}
},
@@ -51,9 +51,11 @@ export class Rect extends Mark {
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
const {projection} = context;
const {insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this;
+ const bx = (x?.bandwidth ? x.bandwidth() : 0) - insetLeft - insetRight;
+ const by = (y?.bandwidth ? y.bandwidth() : 0) - insetTop - insetBottom;
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
- .call(applyTransform, this, {x: X1 && X2 && x, y: Y1 && Y2 && y}, 0, 0)
+ .call(applyTransform, this, {}, 0, 0)
.call((g) =>
g
.selectAll()
@@ -63,26 +65,34 @@ export class Rect extends Mark {
.call(applyDirectStyles, this)
.attr(
"x",
- X1 && X2 && (projection || !isCollapsed(x))
- ? (i) => Math.min(X1[i], X2[i]) + insetLeft
+ X1 && (projection || !isCollapsed(x))
+ ? X2
+ ? (i) => Math.min(X1[i], X2[i]) + insetLeft
+ : (i) => X1[i] + insetLeft
: marginLeft + insetLeft
)
.attr(
"y",
- Y1 && Y2 && (projection || !isCollapsed(y))
- ? (i) => Math.min(Y1[i], Y2[i]) + insetTop
+ Y1 && (projection || !isCollapsed(y))
+ ? Y2
+ ? (i) => Math.min(Y1[i], Y2[i]) + insetTop
+ : (i) => Y1[i] + insetTop
: marginTop + insetTop
)
.attr(
"width",
- X1 && X2 && (projection || !isCollapsed(x))
- ? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight)
+ X1 && (projection || !isCollapsed(x))
+ ? X2
+ ? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) + bx)
+ : bx
: width - marginRight - marginLeft - insetRight - insetLeft
)
.attr(
"height",
- Y1 && Y2 && (projection || !isCollapsed(y))
- ? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) - insetTop - insetBottom)
+ Y1 && (projection || !isCollapsed(y))
+ ? Y2
+ ? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) + by)
+ : by
: height - marginTop - marginBottom - insetTop - insetBottom
)
.call(applyAttr, "rx", rx)
diff --git a/src/transforms/interval.js b/src/transforms/interval.js
index a417e83618..48742a5ed3 100644
--- a/src/transforms/interval.js
+++ b/src/transforms/interval.js
@@ -22,7 +22,7 @@ function maybeIntervalK(k, maybeInsetK, options, trivial) {
...options,
[k]: undefined,
[`${k}1`]: v1 === undefined ? kv : v1,
- [`${k}2`]: v2 === undefined ? kv : v2
+ [`${k}2`]: v2 === undefined && !(v1 === v2 && trivial) ? kv : v2
};
}
let D1, V1;
diff --git a/test/output/groupedRects.svg b/test/output/groupedRects.svg
index 30569c3116..5e3f210209 100644
--- a/test/output/groupedRects.svg
+++ b/test/output/groupedRects.svg
@@ -42,40 +42,40 @@
↑ Frequency
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
- A
- B
- C
- D
- E
- F
- G
- H
- I
- J
+
+ A
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/rectBandX.svg b/test/output/rectBandX.svg
new file mode 100644
index 0000000000..0dbc0ff909
--- /dev/null
+++ b/test/output/rectBandX.svg
@@ -0,0 +1,136 @@
+
\ No newline at end of file
diff --git a/test/output/rectBandX1.svg b/test/output/rectBandX1.svg
new file mode 100644
index 0000000000..1352af63ee
--- /dev/null
+++ b/test/output/rectBandX1.svg
@@ -0,0 +1,65 @@
+
\ No newline at end of file
diff --git a/test/output/rectBandY.svg b/test/output/rectBandY.svg
new file mode 100644
index 0000000000..f8af281c9a
--- /dev/null
+++ b/test/output/rectBandY.svg
@@ -0,0 +1,124 @@
+
\ No newline at end of file
diff --git a/test/output/rectPointX1.svg b/test/output/rectPointX1.svg
new file mode 100644
index 0000000000..070ba05d46
--- /dev/null
+++ b/test/output/rectPointX1.svg
@@ -0,0 +1,65 @@
+
\ No newline at end of file
diff --git a/test/plots/rect-band.ts b/test/plots/rect-band.ts
index 23fd6bfb44..5c759ed4f9 100644
--- a/test/plots/rect-band.ts
+++ b/test/plots/rect-band.ts
@@ -1,4 +1,5 @@
import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
export async function rectBand() {
return Plot.plot({
@@ -14,3 +15,61 @@ export async function rectBand() {
]
});
}
+
+export async function rectBandX() {
+ const alphabet = await d3.csv("data/alphabet.csv", d3.autoType);
+ return Plot.rectY(alphabet, {x: "letter", y: "frequency"}).plot();
+}
+
+export async function rectBandY() {
+ const alphabet = await d3.csv("data/alphabet.csv", d3.autoType);
+ return Plot.rectX(alphabet, {y: "letter", x: "frequency"}).plot();
+}
+
+export async function rectBandX1() {
+ return Plot.plot({
+ round: true,
+ x: {type: "band", domain: "ABCDE"},
+ y: {type: "linear", domain: [0, 9]},
+ marks: [
+ Plot.rect(
+ [
+ ["A", 0, "A", 1],
+ ["A", 1, "B", 2],
+ ["A", 2, "C", 3],
+ ["A", 3, "D", 4],
+ ["A", 4, "E", 5],
+ ["B", 5, "E", 6],
+ ["C", 6, "E", 7],
+ ["D", 7, "E", 8],
+ ["E", 8, "E", 9]
+ ],
+ {x1: "0", y1: "1", x2: "2", y2: "3", inset: 0.5}
+ )
+ ]
+ });
+}
+
+export async function rectPointX1() {
+ return Plot.plot({
+ round: true,
+ x: {type: "point", domain: "ABCDE"},
+ y: {type: "linear", domain: [0, 9]},
+ marks: [
+ Plot.rect(
+ [
+ ["A", 0, "A", 1],
+ ["A", 1, "B", 2],
+ ["A", 2, "C", 3],
+ ["A", 3, "D", 4],
+ ["A", 4, "E", 5],
+ ["B", 5, "E", 6],
+ ["C", 6, "E", 7],
+ ["D", 7, "E", 8],
+ ["E", 8, "E", 9]
+ ],
+ {x1: "0", y1: "1", x2: "2", y2: "3", inset: 0.5, insetLeft: -0.5, insetRight: -0.5}
+ )
+ ]
+ });
+}