From 51d85c472182e9390e6401ead932535e00c55546 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 1 Nov 2023 18:31:17 -0700 Subject: [PATCH] rect support for band scales (#1909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rect support for band scales * band hint if only one value * more band scale support * band rect docs * Update docs/marks/rect.md Co-authored-by: Philippe Rivière --------- Co-authored-by: Philippe Rivière --- docs/marks/bar.md | 2 +- docs/marks/cell.md | 2 +- docs/marks/rect.md | 6 +- src/marks/rect.js | 32 ++++++--- src/transforms/interval.js | 2 +- test/output/groupedRects.svg | 64 ++++++++--------- test/output/rectBandX.svg | 136 +++++++++++++++++++++++++++++++++++ test/output/rectBandX1.svg | 65 +++++++++++++++++ test/output/rectBandY.svg | 124 ++++++++++++++++++++++++++++++++ test/output/rectPointX1.svg | 65 +++++++++++++++++ test/plots/rect-band.ts | 59 +++++++++++++++ 11 files changed, 506 insertions(+), 51 deletions(-) create mode 100644 test/output/rectBandX.svg create mode 100644 test/output/rectBandX1.svg create mode 100644 test/output/rectBandY.svg create mode 100644 test/output/rectPointX1.svg 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 @@ + + + + + + + + + + + + + + + + + + + 0.00 + 0.01 + 0.02 + 0.03 + 0.04 + 0.05 + 0.06 + 0.07 + 0.08 + 0.09 + 0.10 + 0.11 + 0.12 + + + ↑ frequency + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + + + letter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + A + B + C + D + E + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + + + letter + + + + + + + + + + + + 0.00 + 0.02 + 0.04 + 0.06 + 0.08 + 0.10 + 0.12 + + + frequency → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + A + B + C + D + E + + + + + + + + + + + + + \ 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} + ) + ] + }); +}