diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts
index bbabdd8cce..8c038a869e 100644
--- a/docs/data/api.data.ts
+++ b/docs/data/api.data.ts
@@ -49,8 +49,6 @@ function getHref(name: string, path: string): string {
case "features/plot":
case "features/projection":
return `${path}s`;
- case "features/inset":
- return "features/scales";
case "features/options":
return "features/transforms";
case "marks/axis": {
@@ -85,8 +83,8 @@ function getInterfaceName(name: string, path: string): string {
name = name.replace(/([a-z0-9])([A-Z])/, (_, a, b) => `${a} ${b}`); // camel case conversion
name = name.toLowerCase();
if (name === "curve auto") name = "curve";
- if (name === "plot facet") name = "plot";
- if (name === "bollinger window") name = "bollinger map method";
+ else if (name === "plot facet") name = "plot";
+ else if (name === "bollinger window") name = "bollinger map method";
else if (path.startsWith("marks/")) name += " mark";
else if (path.startsWith("transforms/")) name += " transform";
return name;
@@ -105,10 +103,15 @@ export default {
if (Node.isInterfaceDeclaration(declaration)) {
if (isInternalInterface(name)) continue;
for (const property of declaration.getProperties()) {
- const path = index.getRelativePathTo(declaration.getSourceFile());
- const href = getHref(name, path);
if (property.getJsDocs().some((d) => d.getTags().some((d) => Node.isJSDocDeprecatedTag(d)))) continue;
- allOptions.push({name: property.getName(), context: {name: getInterfaceName(name, path), href}});
+ if (name === "InsetOptions") {
+ allOptions.push({name: property.getName(), context: {name: "mark", href: "features/marks"}});
+ allOptions.push({name: property.getName(), context: {name: "scale", href: "features/scales"}});
+ } else {
+ const path = index.getRelativePathTo(declaration.getSourceFile());
+ const href = getHref(name, path);
+ allOptions.push({name: property.getName(), context: {name: getInterfaceName(name, path), href}});
+ }
}
} else if (Node.isFunctionDeclaration(declaration)) {
const comment = getDescription(declaration);
@@ -141,7 +144,9 @@ export default {
throw new Error(`anchor not found: ${href}#${name}`);
}
}
- for (const {context: {href}} of allOptions) {
+ for (const {
+ context: {href}
+ } of allOptions) {
if (!anchors.has(`/${href}.md`)) {
throw new Error(`file not found: ${href}`);
}
diff --git a/docs/features/facets.md b/docs/features/facets.md
index a7215e4ba6..2a7ec21a00 100644
--- a/docs/features/facets.md
+++ b/docs/features/facets.md
@@ -246,7 +246,7 @@ Faceting can be explicitly enabled or disabled on a mark with the **facet** opti
When mark-level faceting is used, the default *auto* setting is equivalent to *include*: the mark will be faceted if either the **fx** or **fy** channel option (or both) is specified. The null or false option will disable faceting, while *exclude* draws the subset of the mark’s data *not* in the current facet. When a mark uses *super* faceting, it is not allowed to use position scales (*x*, *y*, *fx*, or *fy*); *super* faceting is intended for decorations, such as labels and legends.
-The **facetAnchor** option controls the placement of the mark with respect to the facets. Based on the value, the mark will be displayed on:
+The **facetAnchor** option controls the placement of the mark with respect to the facets. Based on the value, the mark will be displayed on:
* null - non-empty facets
* *top*, *right*, *bottom*, or *left* - the given side
diff --git a/docs/features/marks.md b/docs/features/marks.md
index da319afdb0..4ffb071655 100644
--- a/docs/features/marks.md
+++ b/docs/features/marks.md
@@ -531,17 +531,6 @@ Plot.dot(numbers, {x: {transform: (data) => data}})
The **title**, **href**, and **ariaLabel** options can *only* be specified as channels. When these options are specified as a string, the string refers to the name of a column in the mark’s associated data. If you’d like every instance of a particular mark to have the same value, specify the option as a function that returns the desired value, *e.g.* `() => "Hello, world!"`.
-The rectangular marks ([bar](../marks/bar.md), [cell](../marks/cell.md), [frame](../marks/frame.md), and [rect](../marks/rect.md)) support insets and rounded corner constant options:
-
-* **insetTop** - inset the top edge
-* **insetRight** - inset the right edge
-* **insetBottom** - inset the bottom edge
-* **insetLeft** - inset the left edge
-* **rx** - the [*x* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) for rounded corners
-* **ry** - the [*y* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners
-
-Insets are specified in pixels. Corner radii are specified in either pixels or percentages (strings). Both default to zero. Insets are typically used to ensure a one-pixel gap between adjacent bars; note that the [bin transform](../transforms/bin.md) provides default insets, and that the [band scale padding](./scales.md#position-scale-options) defaults to 0.1, which also provides separation.
-
For marks that support the **frameAnchor** option, it may be specified as one of the four sides (*top*, *right*, *bottom*, *left*), one of the four corners (*top-left*, *top-right*, *bottom-right*, *bottom-left*), or the *middle* of the frame.
All marks support the following [transform](./transforms.md) options:
@@ -554,6 +543,36 @@ All marks support the following [transform](./transforms.md) options:
The **sort** option, when not specified as a channel value (such as a field name or an accessor function), can also be used to [impute ordinal scale domains](./scales.md#sort-mark-option).
+### Insets
+
+Rect-like marks support insets: a positive inset moves the respective side in (towards the opposing side), whereas a negative inset moves the respective side out (away from the opposing side). Insets are specified in pixels using the following options:
+
+* **inset** - shorthand for all four insets
+* **insetTop** - inset the top edge
+* **insetRight** - inset the right edge
+* **insetBottom** - inset the bottom edge
+* **insetLeft** - inset the left edge
+
+Insets default to zero. Insets are commonly used to create a one-pixel gap between adjacent bars in histograms; the [bin transform](../transforms/bin.md) provides default insets. (Note that the [band scale padding](./scales.md#position-scale-options) defaults to 0.1 as an alternative to insets.)
+
+### Rounded corners
+
+Rect-like marks support rounded corners. Each corner (or side) is individually addressable using the following options:
+
+* **r** - the radius for all four corners
+* **rx1** - the radius for the **x1**-**y1** and **x1**-**y2** corners
+* **rx2** - the radius for the **x2**-**y1** and **x2**-**y2** corners
+* **ry1** - the radius for the **x1**-**y1** and **x2**-**y1** corners
+* **ry2** - the radius for the **x1**-**y2** and **x2**-**y2** corners
+* **rx1y1** - the radius for the **x1**-**y1** corner
+* **rx1y2** - the radius for the **x1**-**y2** corner
+* **rx2y1** - the radius for the **x2**-**y1** corner
+* **rx2y2** - the radius for the **x2**-**y2** corner
+* **rx** - the [*x*-radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) for elliptical corners
+* **ry** - the [*y*-radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for elliptical corners
+
+Corner radii are specified in either pixels or, for **rx** and **ry**, as percentages (strings) or the keyword *auto*. If the corner radii are too big, they are reduced proportionally.
+
## marks(...*marks*) {#marks}
```js
diff --git a/docs/features/plots.md b/docs/features/plots.md
index d4191244d3..34548522e7 100644
--- a/docs/features/plots.md
+++ b/docs/features/plots.md
@@ -218,7 +218,7 @@ The default **width** is 640. On Observable, the width can be set to the [standa
Plot does not adjust margins automatically to make room for long tick labels. If your *y* axis labels are too long, you can increase the **marginLeft** to make more room. Also consider using a different **tickFormat** for short labels (*e.g.*, `s` for SI prefix notation), or a scale **transform** (say to convert units to millions or billions).
:::
-The **aspectRatio** option , if not null, computes a default **height** such that a variation of one unit in the *x* dimension is represented by the corresponding number of pixels as a variation in the *y* dimension of one unit.
+The **aspectRatio** option , if not null, computes a default **height** such that a variation of one unit in the *x* dimension is represented by the corresponding number of pixels as a variation in the *y* dimension of one unit.
diff --git a/docs/features/scales.md b/docs/features/scales.md
index 1f964bc140..c932cebe52 100644
--- a/docs/features/scales.md
+++ b/docs/features/scales.md
@@ -601,7 +601,7 @@ Plot.plot({
[Mark transforms](./transforms.md) typically consume values *before* they are passed through scales (_e.g._, when binning). In this case the mark transforms will see the values prior to the scale transform as input, and the scale transform will apply to the *output* of the mark transform.
:::
-The **interval** scale option sets an ordinal scale’s **domain** to the start of every interval within the extent of the data. In addition, it implicitly sets the **transform** of the scale to *interval*.floor, rounding values down to the start of each interval. For example, below we generate a time-series bar chart; when an **interval** is specified, missing days are visible.
+The **interval** scale option sets an ordinal scale’s **domain** to the start of every interval within the extent of the data. In addition, it implicitly sets the **transform** of the scale to *interval*.floor, rounding values down to the start of each interval. For example, below we generate a time-series bar chart; when an **interval** is specified, missing days are visible.
diff --git a/docs/marks/bar.md b/docs/marks/bar.md
index 2dcb68cec7..fb252bd071 100644
--- a/docs/marks/bar.md
+++ b/docs/marks/bar.md
@@ -213,7 +213,7 @@ Plot.plot({
## Bar options
-For required channels, see [barX](#barX) and [barY](#barY). The bar mark supports the [standard mark options](../features/marks.md), including insets and rounded corners. The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
+For required channels, see [barX](#barX) and [barY](#barY). The bar mark supports the [standard mark options](../features/marks.md), including [insets](../features/marks.md#insets) and [rounded corners](../features/marks.md#rounded-corners). The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
## barX(*data*, *options*) {#barX}
diff --git a/docs/marks/box.md b/docs/marks/box.md
index a4bdfa5970..2ea4b49278 100644
--- a/docs/marks/box.md
+++ b/docs/marks/box.md
@@ -121,6 +121,7 @@ The given *options* are passed through to these underlying marks, with the excep
* **stroke** - the stroke color of the rule, tick, and dot; defaults to *currentColor*
* **strokeOpacity** - the stroke opacity of the rule, tick, and dot; defaults to 1
* **strokeWidth** - the stroke width of the tick; defaults to 1
+* **r** - the radius of the dot; defaults to 3
## boxX(*data*, *options*) {#boxX}
diff --git a/docs/marks/cell.md b/docs/marks/cell.md
index 6bea26c6ee..38c2e9c92e 100644
--- a/docs/marks/cell.md
+++ b/docs/marks/cell.md
@@ -149,7 +149,7 @@ When an ordinal scale domain has high cardinality, the **ticks** scale option ca
## Cell options
-In addition to the [standard mark options](../features/marks.md#mark-options), including insets and rounded corners, the following optional channels are supported:
+In addition to the [standard mark options](../features/marks.md#mark-options), including [insets](../features/marks.md#insets) and [rounded corners](../features/marks.md#rounded-corners), the following optional channels are supported:
* **x** - the horizontal position; bound to the *x* scale, which must be *band*
* **y** - the vertical position; bound to the *y* scale, which must be *band*
diff --git a/docs/marks/frame.md b/docs/marks/frame.md
index ffc6449ecc..20fa51fb78 100644
--- a/docs/marks/frame.md
+++ b/docs/marks/frame.md
@@ -111,7 +111,7 @@ Plot.plot({
## Frame options
-The frame mark supports the [standard mark options](../features/marks.md#mark-options), and the **rx** and **ry** options to set the [*x* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) and [*y* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners. It does not accept any data. The default **stroke** is *currentColor*, and the default **fill** is *none*.
+The frame mark supports the [standard mark options](../features/marks.md#mark-options), including [insets](../features/marks.md#insets) and [rounded corners](../features/marks.md#rounded-corners). It does not accept any data. The default **stroke** is *currentColor*, and the default **fill** is *none*.
If the **anchor** option is specified as one of *left*, *right*, *top*, or *bottom*, that side is rendered as a single line (and the **fill**, **fillOpacity**, **rx**, and **ry** options are ignored).
diff --git a/docs/marks/rect.md b/docs/marks/rect.md
index ef87e6d665..676e8a781c 100644
--- a/docs/marks/rect.md
+++ b/docs/marks/rect.md
@@ -3,8 +3,9 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import * as topojson from "topojson-client";
-import {computed, shallowRef, onMounted} from "vue";
+import {computed, ref, shallowRef, onMounted} from "vue";
+const r = ref(4);
const diamonds = shallowRef([]);
const seattle = shallowRef([]);
const olympians = shallowRef([{weight: 31, height: 1.21, sex: "female"}, {weight: 170, height: 2.21, sex: "male"}]);
@@ -45,7 +46,7 @@ Plot.plot({
```
:::
-More commonly, the rect mark is used to produce histograms or heatmaps of quantitative data. For example, given some binned observations computed by [d3.bin](https://d3js.org/d3-array/bin), we can produce a basic histogram with [rectY](#rectY) as follows:
+The rect mark is often used to produce histograms or heatmaps of quantitative data. For example, given some binned observations computed by [d3.bin](https://d3js.org/d3-array/bin), we can produce a basic histogram with [rectY](#rectY) as follows:
:::plot https://observablehq.com/@observablehq/plot-rects-and-bins
```js
@@ -61,7 +62,7 @@ bins = d3.bin()(d3.range(1000).map(d3.randomNormal()))
d3.bin uses *x0* and *x1* to represent the lower and upper bound of each bin, whereas the rect mark uses **x1** and **x2**. The *length* field is the count of values in each bin, which is encoded as **y**.
:::
-Most often, the rect mark is paired with the [bin transform](../transforms/bin.md) to bin quantitative values as part of the plot itself. As an added bonus, this sets default [inset options](../features/marks.md#mark-options) for a 1px gap separating adjacent rects, improving readability.
+More commonly, the rect mark is paired with the [bin transform](../transforms/bin.md) to bin quantitative values automatically. As an added bonus, this sets default [inset options](../features/marks.md#mark-options) for a 1px gap separating adjacent rects, improving readability.
:::plot https://observablehq.com/@observablehq/plot-rects-and-bins
```js
@@ -69,7 +70,7 @@ Plot.rectY(d3.range(1000).map(d3.randomNormal()), Plot.binX()).plot()
```
:::
-Like the [bar mark](./bar.md), the rect mark has two convenience constructors for common orientations: [rectX](#rectX) is for horizontal→ rects and applies an implicit [stackX transform](../transforms/stack.md#stackX), while [rectY](#rectY) is for vertical↑ rects and applies an implicit [stackY transform](../transforms/stack.md#stackY).
+Like the [bar mark](./bar.md), the rect mark has two convenience constructors for common orientations: [rectX](#rectX) is for horizontal→ rects with an implicit [stackX transform](../transforms/stack.md#stackX), while [rectY](#rectY) is for vertical↑ rects with an implicit [stackY transform](../transforms/stack.md#stackY).
:::plot defer https://observablehq.com/@observablehq/plot-vertical-histogram
```js
@@ -161,7 +162,7 @@ Plot.plot({
```
:::
-The [interval transform](../transforms/interval.md) may be used to convert a single value in **x** or **y** (or both) into an extent. For example, the chart below shows the observed daily maximum temperature in Seattle for the year 2015. The day-in-month and month-in-year numbers are expanded to unit intervals by setting the **interval** option to 1.
+The [interval transform](../transforms/interval.md) may be used to convert a single value in **x** or **y** (or both) into an extent. (Unlike the bin transform, the interval transform will produce overlapping rects if multiple points have the same position.) The chart below shows the observed daily maximum temperature in Seattle for the year 2015. The day-in-month and month-in-year numbers are expanded to unit intervals by setting the **interval** option to 1.
:::plot defer https://observablehq.com/@observablehq/plot-seattle-heatmap-quantitative
```js
@@ -169,8 +170,7 @@ Plot.plot({
aspectRatio: 1,
y: {ticks: 12, tickFormat: Plot.formatMonth("en", "narrow")},
marks: [
- Plot.rect(seattle, {
- filter: (d) => d.date.getUTCFullYear() === 2015,
+ Plot.rect(seattle.filter((d) => d.date.getUTCFullYear() === 2015), {
x: (d) => d.date.getUTCDate(),
y: (d) => d.date.getUTCMonth(),
interval: 1,
@@ -186,6 +186,64 @@ Plot.plot({
A similar chart could be made with the [cell mark](./cell.md) using ordinal *x* and *y* scales instead, or with the [dot mark](./dot.md) as a scatterplot.
:::
+To round corners, use the **r** option. If the combined corner radii exceed the width or height of the rect, the radii are proportionally reduced to produce a pill shape with circular caps. Try increasing the radii below.
+
+
+
+ r:
+
+ {{r}}
+
+
+
+:::plot hidden defer
+```js
+Plot.plot({
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", r, thresholds: 10})),
+ Plot.ruleY([0])
+ ]
+})
+```
+:::
+
+```js-vue
+Plot.plot({
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", r: {{r}}, thresholds: 10})),
+ Plot.ruleY([0])
+ ]
+})
+```
+
+To round corners on a specific side, use the **rx1**, **ry1**, **rx2**, or **ry2** options. When stacking rounded rects vertically, use a positive **ry2** and a corresponding negative **ry1**; likewise for stacking rounded rects horizontally, use a positive **rx2** and a negative **rx1**. Use the **clip** option to hide the “wings” below zero.
+
+:::plot defer
+```js
+Plot.plot({
+ color: {legend: true},
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", fill: "sex", ry2: 4, ry1: -4, clip: "frame"})),
+ Plot.ruleY([0])
+ ]
+})
+```
+:::
+
+You can even round specific corners using the **rx1y1**, **rx2y1**, **rx2y2**, and **rx1y2** options.
+
+:::plot defer
+```js
+Plot.plot({
+ color: {legend: true},
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", fill: "sex", rx1y2: 10, rx1y1: -10, clip: "frame"})),
+ Plot.ruleY([0])
+ ]
+})
+```
+:::
+
## Rect options
The following channels are optional:
@@ -199,7 +257,7 @@ If **x1** is specified but **x2** is not specified, then *x* must be a *band* sc
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).
-The rect mark supports the [standard mark options](../features/marks.md#mark-options), including insets and rounded corners. The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
+The rect mark supports the [standard mark options](../features/marks.md#mark-options), including [insets](../features/marks.md#insets) and [rounded corners](../features/marks.md#rounded-corners). The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
## rect(*data*, *options*) {#rect}
diff --git a/src/marks/bar.js b/src/marks/bar.js
index b0ddd21b2c..a17453730a 100644
--- a/src/marks/bar.js
+++ b/src/marks/bar.js
@@ -1,26 +1,25 @@
import {create} from "../context.js";
import {Mark} from "../mark.js";
-import {hasXY, identity, indexOf, number} from "../options.js";
+import {hasXY, identity, indexOf} from "../options.js";
import {isCollapsed} from "../scales.js";
import {applyAttr, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
-import {impliedString} from "../style.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
+import {applyRoundedRect, rectInsets, rectRadii} from "./rect.js";
export class AbstractBar extends Mark {
constructor(data, channels, options = {}, defaults) {
super(data, channels, options, defaults);
- const {inset = 0, insetTop = inset, insetRight = inset, insetBottom = inset, insetLeft = inset, rx, ry} = options;
- this.insetTop = number(insetTop);
- this.insetRight = number(insetRight);
- this.insetBottom = number(insetBottom);
- this.insetLeft = number(insetLeft);
- this.rx = impliedString(rx, "auto"); // number or percentage
- this.ry = impliedString(ry, "auto");
+ rectInsets(this, options);
+ rectRadii(this, options);
}
render(index, scales, channels, dimensions, context) {
- const {rx, ry} = this;
+ const {rx, ry, rx1y1, rx1y2, rx2y1, rx2y2} = this;
+ const x = this._x(scales, channels, dimensions);
+ const y = this._y(scales, channels, dimensions);
+ const w = this._width(scales, channels, dimensions);
+ const h = this._height(scales, channels, dimensions);
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(this._transform, this, scales)
@@ -29,15 +28,26 @@ export class AbstractBar extends Mark {
.selectAll()
.data(index)
.enter()
- .append("rect")
- .call(applyDirectStyles, this)
- .attr("x", this._x(scales, channels, dimensions))
- .attr("width", this._width(scales, channels, dimensions))
- .attr("y", this._y(scales, channels, dimensions))
- .attr("height", this._height(scales, channels, dimensions))
- .call(applyAttr, "rx", rx)
- .call(applyAttr, "ry", ry)
- .call(applyChannelStyles, this, channels)
+ .call(
+ rx1y1 || rx1y2 || rx2y1 || rx2y2
+ ? (g) =>
+ g
+ .append("path")
+ .call(applyDirectStyles, this)
+ .call(applyRoundedRect, x, y, add(x, w), add(y, h), this)
+ .call(applyChannelStyles, this, channels)
+ : (g) =>
+ g
+ .append("rect")
+ .call(applyDirectStyles, this)
+ .attr("x", x)
+ .attr("width", w)
+ .attr("y", y)
+ .attr("height", h)
+ .call(applyAttr, "rx", rx)
+ .call(applyAttr, "ry", ry)
+ .call(applyChannelStyles, this, channels)
+ )
)
.node();
}
@@ -61,6 +71,16 @@ export class AbstractBar extends Mark {
}
}
+function add(a, b) {
+ return typeof a === "function" && typeof b === "function"
+ ? (i) => a(i) + b(i)
+ : typeof a === "function"
+ ? (i) => a(i) + b
+ : typeof b === "function"
+ ? (i) => a + b(i)
+ : a + b;
+}
+
const defaults = {
ariaLabel: "bar"
};
diff --git a/src/marks/box.js b/src/marks/box.js
index 7d74db6972..27d8fa6ee5 100644
--- a/src/marks/box.js
+++ b/src/marks/box.js
@@ -15,6 +15,7 @@ export function boxX(
{
x = identity,
y = null,
+ r,
fill = "#ccc",
fillOpacity,
stroke = "currentColor",
@@ -29,7 +30,7 @@ export function boxX(
ruleY(data, group({x1: loqr1, x2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
barX(data, group({x1: "p25", x2: "p75"}, {x, y, fill, fillOpacity, ...options})),
tickX(data, group({x: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, sort, ...options})),
- dot(data, map({x: oqr}, {x, y, z: y, stroke, strokeOpacity, ...options}))
+ dot(data, map({x: oqr}, {x, y, z: y, r, stroke, strokeOpacity, ...options}))
);
}
@@ -40,6 +41,7 @@ export function boxY(
{
y = identity,
x = null,
+ r,
fill = "#ccc",
fillOpacity,
stroke = "currentColor",
@@ -54,7 +56,7 @@ export function boxY(
ruleX(data, group({y1: loqr1, y2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
barY(data, group({y1: "p25", y2: "p75"}, {x, y, fill, fillOpacity, ...options})),
tickY(data, group({y: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, sort, ...options})),
- dot(data, map({y: oqr}, {x, y, z: x, stroke, strokeOpacity, ...options}))
+ dot(data, map({y: oqr}, {x, y, z: x, r, stroke, strokeOpacity, ...options}))
);
}
diff --git a/src/marks/frame.d.ts b/src/marks/frame.d.ts
index 5d0e456823..da2cb6a147 100644
--- a/src/marks/frame.d.ts
+++ b/src/marks/frame.d.ts
@@ -6,8 +6,8 @@ import type {RectCornerOptions} from "./rect.js";
export interface FrameOptions extends MarkOptions, InsetOptions, RectCornerOptions {
/**
* If null (default), the rectangular outline of the frame is drawn; otherwise
- * the frame is drawn as a line only on the given side, and the **rx**,
- * **ry**, **fill**, and **fillOpacity** options are ignored.
+ * the frame is drawn as a line only on the given side, and the corner radii
+ * (**r** *etc.*) and fill (**fill** and **fillOpacity**) options are ignored.
*/
anchor?: "top" | "right" | "bottom" | "left" | null;
}
diff --git a/src/marks/frame.js b/src/marks/frame.js
index 372678cf33..eca4084d26 100644
--- a/src/marks/frame.js
+++ b/src/marks/frame.js
@@ -1,7 +1,8 @@
import {create} from "../context.js";
import {Mark} from "../mark.js";
-import {maybeKeyword, number, singleton} from "../options.js";
+import {maybeKeyword, singleton} from "../options.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
+import {applyRoundedRect, rectInsets, rectRadii} from "./rect.js";
const defaults = {
ariaLabel: "frame",
@@ -20,33 +21,21 @@ const lineDefaults = {
export class Frame extends Mark {
constructor(options = {}) {
- const {
- anchor = null,
- inset = 0,
- insetTop = inset,
- insetRight = inset,
- insetBottom = inset,
- insetLeft = inset,
- rx,
- ry
- } = options;
+ const {anchor = null} = options;
super(singleton, undefined, options, anchor == null ? defaults : lineDefaults);
this.anchor = maybeKeyword(anchor, "anchor", ["top", "right", "bottom", "left"]);
- this.insetTop = number(insetTop);
- this.insetRight = number(insetRight);
- this.insetBottom = number(insetBottom);
- this.insetLeft = number(insetLeft);
- this.rx = number(rx);
- this.ry = number(ry);
+ rectInsets(this, options);
+ if (!anchor) rectRadii(this, options);
}
render(index, scales, channels, dimensions, context) {
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
- const {anchor, insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this;
+ const {anchor, insetTop, insetRight, insetBottom, insetLeft} = this;
+ const {rx, ry, rx1y1, rx1y2, rx2y1, rx2y2} = this;
const x1 = marginLeft + insetLeft;
const x2 = width - marginRight - insetRight;
const y1 = marginTop + insetTop;
const y2 = height - marginBottom - insetBottom;
- return create(anchor ? "svg:line" : "svg:rect", context)
+ return create(anchor ? "svg:line" : rx1y1 || rx1y2 || rx2y1 || rx2y2 ? "svg:path" : "svg:rect", context)
.datum(0)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyDirectStyles, this)
@@ -61,6 +50,8 @@ export class Frame extends Mark {
? (line) => line.attr("x1", x1).attr("x2", x2).attr("y1", y1).attr("y2", y1)
: anchor === "bottom"
? (line) => line.attr("x1", x1).attr("x2", x2).attr("y1", y2).attr("y2", y2)
+ : rx1y1 || rx1y2 || rx2y1 || rx2y2
+ ? (path) => path.call(applyRoundedRect, x1, y1, x2, y2, this)
: (rect) =>
rect
.attr("x", x1)
diff --git a/src/marks/rect.d.ts b/src/marks/rect.d.ts
index 3133b87b2c..52a5b1a9a1 100644
--- a/src/marks/rect.d.ts
+++ b/src/marks/rect.d.ts
@@ -6,10 +6,82 @@ import type {StackOptions} from "../transforms/stack.js";
/** Options for marks that render rectangles, including bar, cell, and rect. */
export interface RectCornerOptions {
+ /**
+ * The rounded radius for all corners, in pixels; shorthand for **rx1y1**,
+ * **rx2y1**, **rx2y2**, and **rx1y2**. If the combined corner radii for each
+ * side is greater than the corresponding length (width or height) of the
+ * rect, the corner radii will be shrunk proportionally to maintain circular
+ * corners. For elliptic corners, or to specify the corner radius as a
+ * proportion of the width or height, use **rx** and **ry** instead.
+ */
+ r?: number;
+
+ /**
+ * The rounded radius for the **x1** corners (typically left for positive
+ * *x*-values), in pixels; shorthand for **rx1y1** and **rx1y2**.
+ */
+ rx1?: number;
+
+ /**
+ * The rounded radius for the **y1** corners (typically bottom for positive
+ * *y*-values), in pixels; shorthand for **rx1y1** and **rx2y1**.
+ */
+ ry1?: number;
+
+ /**
+ * The rounded radius for the **x2** corners (typically right for positive
+ * *x*-values), in pixels; shorthand for **rx2y1** and **rx2y2**.
+ */
+ rx2?: number;
+
+ /**
+ * The rounded radius for the **y2** corners (typically top for positive
+ * *y*-values), in pixels; shorthand for **rx1y2** and **rx2y2**.
+ */
+ ry2?: number;
+
+ /**
+ * The rounded radius for the **x1y1** corner (typically bottom-left for
+ * positive values), in pixels. If **rx1y1** + **rx2y1** is greater than the
+ * width of the rect, or if **rx1y1** + **rx1y2** is greater than the height
+ * of the rect, the corner radii will be shrunk proportionally to maintain
+ * circular corners.
+ */
+ rx1y1?: number;
+
+ /**
+ * The rounded radius for the **x1y2** corner (typically top-left for positive
+ * values), in pixels. If **rx1y2** + **rx2y2** is greater than the
+ * width of the rect, or if **rx1y1** + **rx1y2** is greater than the height
+ * of the rect, the corner radii will be shrunk proportionally to maintain
+ * circular corners.
+ */
+ rx1y2?: number;
+
+ /**
+ * The rounded radius for the **x2y1** corner (typically bottom-right for
+ * positive values), in pixels. If **rx1y1** + **rx2y1** is greater than the
+ * width of the rect, or if **rx2y1** + **rx2y2** is greater than the height
+ * of the rect, the corner radii will be shrunk proportionally to maintain
+ * circular corners.
+ */
+ rx2y1?: number;
+
+ /**
+ * The rounded radius for the **x2y2** corner (typically top-right for
+ * positive values), in pixels. If **rx1y2** + **rx2y2** is greater than the
+ * width of the rect, or if **rx2y1** + **rx2y2** is greater than the height
+ * of the rect, the corner radii will be shrunk proportionally to maintain
+ * circular corners.
+ */
+ rx2y2?: number;
+
/**
* The rounded corner [*x*-radius][1], either in pixels or as a percentage of
* the rect width. If **rx** is not specified, it defaults to **ry** if
- * present, and otherwise draws square corners.
+ * present, and otherwise draws square corners. This option is ignored if a
+ * more specific corner radius (one of **rx1y1**, **rx2y1**, **rx1y2**, or
+ * **rx2y2**) is specified.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx
*/
@@ -18,7 +90,9 @@ export interface RectCornerOptions {
/**
* The rounded corner [*y*-radius][1], either in pixels or as a percentage of
* the rect height. If **ry** is not specified, it defaults to **rx** if
- * present, and otherwise draws square corners.
+ * present, and otherwise draws square corners. This option is ignored if a
+ * more specific corner radius (one of **rx1y1**, **rx2y1**, **rx1y2**, or
+ * **rx2y2**) is specified.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry
*/
diff --git a/src/marks/rect.js b/src/marks/rect.js
index 7dee9fbcf1..aea82c42aa 100644
--- a/src/marks/rect.js
+++ b/src/marks/rect.js
@@ -1,6 +1,6 @@
import {create} from "../context.js";
import {Mark} from "../mark.js";
-import {hasXY, identity, indexOf, number} from "../options.js";
+import {constant, hasXY, identity, indexOf, number} from "../options.js";
import {isCollapsed} from "../scales.js";
import {applyAttr, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
import {impliedString} from "../style.js";
@@ -14,19 +14,7 @@ const defaults = {
export class Rect extends Mark {
constructor(data, options = {}) {
- const {
- x1,
- y1,
- x2,
- y2,
- inset = 0,
- insetTop = inset,
- insetRight = inset,
- insetBottom = inset,
- insetLeft = inset,
- rx,
- ry
- } = options;
+ const {x1, y1, x2, y2} = options;
super(
data,
{
@@ -38,21 +26,20 @@ export class Rect extends Mark {
options,
defaults
);
- this.insetTop = number(insetTop);
- this.insetRight = number(insetRight);
- this.insetBottom = number(insetBottom);
- this.insetLeft = number(insetLeft);
- this.rx = impliedString(rx, "auto"); // number or percentage
- this.ry = impliedString(ry, "auto");
+ rectInsets(this, options);
+ rectRadii(this, options);
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
- const {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels;
+ let {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels;
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;
+ const {insetTop, insetRight, insetBottom, insetLeft} = this;
+ const {rx, ry, rx1y1, rx1y2, rx2y1, rx2y2} = this;
+ if ((X1 || X2) && !projection && isCollapsed(x)) X1 = X2 = null; // ignore if collapsed
+ if ((Y1 || Y2) && !projection && isCollapsed(y)) Y1 = Y2 = null; // ignore if collapsed
+ const bx = x?.bandwidth ? x.bandwidth() : 0;
+ const by = y?.bandwidth ? y.bandwidth() : 0;
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyTransform, this, {}, 0, 0)
@@ -61,48 +48,171 @@ export class Rect extends Mark {
.selectAll()
.data(index)
.enter()
- .append("rect")
- .call(applyDirectStyles, this)
- .attr(
- "x",
- X1 && (projection || !isCollapsed(x))
- ? X2
- ? (i) => Math.min(X1[i], X2[i]) + insetLeft
- : (i) => X1[i] + insetLeft
- : marginLeft + insetLeft
+ .call(
+ rx1y1 || rx1y2 || rx2y1 || rx2y2
+ ? (g) =>
+ g
+ .append("path")
+ .call(applyDirectStyles, this)
+ .call(
+ applyRoundedRect,
+ X1 && X2
+ ? (i) => X1[i] + (X2[i] < X1[i] ? -insetRight : insetLeft)
+ : X1
+ ? (i) => X1[i] + insetLeft
+ : marginLeft + insetLeft,
+ Y1 && Y2
+ ? (i) => Y1[i] + (Y2[i] < Y1[i] ? -insetBottom : insetTop)
+ : Y1
+ ? (i) => Y1[i] + insetTop
+ : marginTop + insetTop,
+ X1 && X2
+ ? (i) => X2[i] - (X2[i] < X1[i] ? -insetLeft : insetRight)
+ : X1
+ ? (i) => X1[i] + bx - insetRight
+ : width - marginRight - insetRight,
+ Y1 && Y2
+ ? (i) => Y2[i] - (Y2[i] < Y1[i] ? -insetTop : insetBottom)
+ : Y1
+ ? (i) => Y1[i] + by - insetBottom
+ : height - marginBottom - insetBottom,
+ this
+ )
+ .call(applyChannelStyles, this, channels)
+ : (g) =>
+ g
+ .append("rect")
+ .call(applyDirectStyles, this)
+ .attr(
+ "x",
+ X1
+ ? X2
+ ? (i) => Math.min(X1[i], X2[i]) + insetLeft
+ : (i) => X1[i] + insetLeft
+ : marginLeft + insetLeft
+ )
+ .attr(
+ "y",
+ Y1
+ ? Y2
+ ? (i) => Math.min(Y1[i], Y2[i]) + insetTop
+ : (i) => Y1[i] + insetTop
+ : marginTop + insetTop
+ )
+ .attr(
+ "width",
+ X1
+ ? X2
+ ? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) + bx - insetLeft - insetRight)
+ : bx - insetLeft - insetRight
+ : width - marginRight - marginLeft - insetRight - insetLeft
+ )
+ .attr(
+ "height",
+ Y1
+ ? Y2
+ ? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) + by - insetTop - insetBottom)
+ : by - insetTop - insetBottom
+ : height - marginTop - marginBottom - insetTop - insetBottom
+ )
+ .call(applyAttr, "rx", rx)
+ .call(applyAttr, "ry", ry)
+ .call(applyChannelStyles, this, channels)
)
- .attr(
- "y",
- Y1 && (projection || !isCollapsed(y))
- ? Y2
- ? (i) => Math.min(Y1[i], Y2[i]) + insetTop
- : (i) => Y1[i] + insetTop
- : marginTop + insetTop
- )
- .attr(
- "width",
- 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 && (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)
- .call(applyAttr, "ry", ry)
- .call(applyChannelStyles, this, channels)
)
.node();
}
}
+export function rectInsets(
+ mark,
+ {inset = 0, insetTop = inset, insetRight = inset, insetBottom = inset, insetLeft = inset} = {}
+) {
+ mark.insetTop = number(insetTop);
+ mark.insetRight = number(insetRight);
+ mark.insetBottom = number(insetBottom);
+ mark.insetLeft = number(insetLeft);
+}
+
+export function rectRadii(
+ mark,
+ {
+ r,
+ rx, // for elliptic corners
+ ry, // for elliptic corners
+ rx1 = r,
+ ry1 = r,
+ rx2 = r,
+ ry2 = r,
+ rx1y1 = rx1 !== undefined ? +rx1 : ry1 !== undefined ? +ry1 : 0,
+ rx1y2 = rx1 !== undefined ? +rx1 : ry2 !== undefined ? +ry2 : 0,
+ rx2y1 = rx2 !== undefined ? +rx2 : ry1 !== undefined ? +ry1 : 0,
+ rx2y2 = rx2 !== undefined ? +rx2 : ry2 !== undefined ? +ry2 : 0
+ } = {}
+) {
+ if (rx1y1 || rx1y2 || rx2y1 || rx2y2) {
+ mark.rx1y1 = rx1y1;
+ mark.rx1y2 = rx1y2;
+ mark.rx2y1 = rx2y1;
+ mark.rx2y2 = rx2y2;
+ } else {
+ mark.rx = impliedString(rx, "auto"); // number or percentage
+ mark.ry = impliedString(ry, "auto");
+ }
+}
+
+export function applyRoundedRect(selection, X1, Y1, X2, Y2, mark) {
+ const {rx1y1: r11, rx1y2: r12, rx2y1: r21, rx2y2: r22} = mark;
+ if (typeof X1 !== "function") X1 = constant(X1);
+ if (typeof Y1 !== "function") Y1 = constant(Y1);
+ if (typeof X2 !== "function") X2 = constant(X2);
+ if (typeof Y2 !== "function") Y2 = constant(Y2);
+ const rx = Math.max(Math.abs(r11 + r21), Math.abs(r12 + r22));
+ const ry = Math.max(Math.abs(r11 + r12), Math.abs(r21 + r22));
+ selection.attr("d", (i) => {
+ const x1 = X1(i);
+ const y1 = Y1(i);
+ const x2 = X2(i);
+ const y2 = Y2(i);
+ const ix = x1 > x2;
+ const iy = y1 > y2;
+ const l = ix ? x2 : x1;
+ const r = ix ? x1 : x2;
+ const t = iy ? y2 : y1;
+ const b = iy ? y1 : y2;
+ const k = Math.min(1, (r - l) / rx, (b - t) / ry);
+ const tl = k * (ix ? (iy ? r22 : r21) : iy ? r12 : r11);
+ const tr = k * (ix ? (iy ? r12 : r11) : iy ? r22 : r21);
+ const br = k * (ix ? (iy ? r11 : r12) : iy ? r21 : r22);
+ const bl = k * (ix ? (iy ? r21 : r22) : iy ? r11 : r12);
+ return (
+ `M${l},${t + biasY(tl, bl)}A${tl},${tl} 0 0 ${tl < 0 ? 0 : 1} ${l + biasX(tl, bl)},${t}` +
+ `H${r - biasX(tr, br)}A${tr},${tr} 0 0 ${tr < 0 ? 0 : 1} ${r},${t + biasY(tr, br)}` +
+ `V${b - biasY(br, tr)}A${br},${br} 0 0 ${br < 0 ? 0 : 1} ${r - biasX(br, tr)},${b}` +
+ `H${l + biasX(bl, tl)}A${bl},${bl} 0 0 ${bl < 0 ? 0 : 1} ${l},${b - biasY(bl, tl)}` +
+ `Z`
+ );
+ });
+}
+
+/**
+ * If the opposing corner has a negative radius r2, if this corner has a
+ * negative radius r1, this corner’s “wing” will extend horizontally rather than
+ * vertically.
+ */
+function biasX(r1, r2) {
+ return r2 < 0 ? r1 : Math.abs(r1);
+}
+
+/**
+ * If the opposing corner has a negative radius r2, if this corner has a
+ * negative radius r1, this corner’s “wing” will extend horizontally rather than
+ * vertically.
+ */
+function biasY(r1, r2) {
+ return r2 < 0 ? Math.abs(r1) : r1;
+}
+
export function rect(data, options) {
return new Rect(data, maybeTrivialIntervalX(maybeTrivialIntervalY(options)));
}
diff --git a/test/output/roundedBarYR.svg b/test/output/roundedBarYR.svg
new file mode 100644
index 0000000000..1bc8edbd3b
--- /dev/null
+++ b/test/output/roundedBarYR.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedBarYRx.svg b/test/output/roundedBarYRx.svg
new file mode 100644
index 0000000000..494b7a90a5
--- /dev/null
+++ b/test/output/roundedBarYRx.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedBarYRy.svg b/test/output/roundedBarYRy.svg
new file mode 100644
index 0000000000..83b67acfd1
--- /dev/null
+++ b/test/output/roundedBarYRy.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectAsymmetricX.svg b/test/output/roundedRectAsymmetricX.svg
new file mode 100644
index 0000000000..a1d5c3aeb1
--- /dev/null
+++ b/test/output/roundedRectAsymmetricX.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectAsymmetricY.svg b/test/output/roundedRectAsymmetricY.svg
new file mode 100644
index 0000000000..2e13c85a60
--- /dev/null
+++ b/test/output/roundedRectAsymmetricY.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+ 0.1
+ 0.2
+ 0.3
+ 0.4
+ 0.5
+ 0.6
+ 0.7
+ 0.8
+ 0.9
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectBand.svg b/test/output/roundedRectBand.svg
new file mode 100644
index 0000000000..aa076b27de
--- /dev/null
+++ b/test/output/roundedRectBand.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectCollapsedX.svg b/test/output/roundedRectCollapsedX.svg
new file mode 100644
index 0000000000..52a8315c5c
--- /dev/null
+++ b/test/output/roundedRectCollapsedX.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+ 1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectCollapsedY.svg b/test/output/roundedRectCollapsedY.svg
new file mode 100644
index 0000000000..f275bc8415
--- /dev/null
+++ b/test/output/roundedRectCollapsedY.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+ 0.1
+ 0.2
+ 0.3
+ 0.4
+ 0.5
+ 0.6
+ 0.7
+ 0.8
+ 0.9
+ 1.0
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectCorners.svg b/test/output/roundedRectCorners.svg
new file mode 100644
index 0000000000..3ad86a954b
--- /dev/null
+++ b/test/output/roundedRectCorners.svg
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+ 3.5
+ 3.0
+ 2.5
+ 2.0
+ 1.5
+ 1.0
+ 0.5
+ 0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+ 0.5
+ 1.0
+ 1.5
+ 2.0
+ 2.5
+ 3.0
+ 3.5
+ 4.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectNegativeX.html b/test/output/roundedRectNegativeX.html
new file mode 100644
index 0000000000..04bfce0cab
--- /dev/null
+++ b/test/output/roundedRectNegativeX.html
@@ -0,0 +1,232 @@
+
+
+
+
+ female
+
+ male
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30
+ 40
+ 50
+ 60
+ 70
+ 80
+ 90
+ 100
+ 110
+ 120
+ 130
+ 140
+ 150
+ 160
+ 170
+
+
+ ↑ weight
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 100
+ 200
+ 300
+ 400
+ 500
+ 600
+
+
+ Frequency →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectNegativeY.html b/test/output/roundedRectNegativeY.html
new file mode 100644
index 0000000000..bacbed2611
--- /dev/null
+++ b/test/output/roundedRectNegativeY.html
@@ -0,0 +1,228 @@
+
+
+
+
+ female
+
+ male
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 50
+ 100
+ 150
+ 200
+ 250
+ 300
+ 350
+ 400
+ 450
+ 500
+ 550
+ 600
+
+
+ ↑ Frequency
+
+
+
+
+
+
+
+
+
+
+
+ 40
+ 60
+ 80
+ 100
+ 120
+ 140
+ 160
+
+
+ weight →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectNegativeY1.html b/test/output/roundedRectNegativeY1.html
new file mode 100644
index 0000000000..3962f3291c
--- /dev/null
+++ b/test/output/roundedRectNegativeY1.html
@@ -0,0 +1,228 @@
+
+
+
+
+ female
+
+ male
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 50
+ 100
+ 150
+ 200
+ 250
+ 300
+ 350
+ 400
+ 450
+ 500
+ 550
+ 600
+
+
+ ↑ Frequency
+
+
+
+
+
+
+
+
+
+
+
+ 40
+ 60
+ 80
+ 100
+ 120
+ 140
+ 160
+
+
+ weight →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectOpposing.svg b/test/output/roundedRectOpposing.svg
new file mode 100644
index 0000000000..056554f39c
--- /dev/null
+++ b/test/output/roundedRectOpposing.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectR.svg b/test/output/roundedRectR.svg
new file mode 100644
index 0000000000..8b0c2c4a28
--- /dev/null
+++ b/test/output/roundedRectR.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectRx.svg b/test/output/roundedRectRx.svg
new file mode 100644
index 0000000000..d7c63747fd
--- /dev/null
+++ b/test/output/roundedRectRx.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectRy.svg b/test/output/roundedRectRy.svg
new file mode 100644
index 0000000000..13225cf88d
--- /dev/null
+++ b/test/output/roundedRectRy.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+ 0.9
+ 0.8
+ 0.7
+ 0.6
+ 0.5
+ 0.4
+ 0.3
+ 0.2
+ 0.1
+ 0.0
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/roundedRectSides.svg b/test/output/roundedRectSides.svg
new file mode 100644
index 0000000000..eac354c73b
--- /dev/null
+++ b/test/output/roundedRectSides.svg
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+ 3.5
+ 3.0
+ 2.5
+ 2.0
+ 1.5
+ 1.0
+ 0.5
+ 0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+ 0.5
+ 1.0
+ 1.5
+ 2.0
+ 2.5
+ 3.0
+ 3.5
+ 4.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 7b99e1ada1..b7af3ca8cd 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -253,6 +253,7 @@ export * from "./raster-vapor.js";
export * from "./raster-walmart.js";
export * from "./rect-band.js";
export * from "./reducer-scale-override.js";
+export * from "./rounded-rect.js";
export * from "./seattle-precipitation-density.js";
export * from "./seattle-precipitation-rule.js";
export * from "./seattle-precipitation-sum.js";
diff --git a/test/plots/rounded-rect.ts b/test/plots/rounded-rect.ts
new file mode 100644
index 0000000000..6ae49029bd
--- /dev/null
+++ b/test/plots/rounded-rect.ts
@@ -0,0 +1,271 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export function roundedBarYR() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.barY({length: 1}, {x: 0, ...xy, r: 25}),
+ Plot.barY({length: 1}, {x: 1, ...xy, r: 50}),
+ Plot.barY({length: 1}, {x: 2, ...xy, r: 75}),
+ Plot.barY({length: 1}, {x: 3, ...xy, r: 100})
+ ]
+ });
+}
+
+export function roundedBarYRx() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.barY({length: 1}, {x: 0, ...xy, rx: 25}),
+ Plot.barY({length: 1}, {x: 1, ...xy, rx: 50}),
+ Plot.barY({length: 1}, {x: 2, ...xy, rx: 75}),
+ Plot.barY({length: 1}, {x: 3, ...xy, rx: 100})
+ ]
+ });
+}
+
+export function roundedBarYRy() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.barY({length: 1}, {x: 0, ...xy, ry: 25}),
+ Plot.barY({length: 1}, {x: 1, ...xy, ry: 50}),
+ Plot.barY({length: 1}, {x: 2, ...xy, ry: 75}),
+ Plot.barY({length: 1}, {x: 3, ...xy, ry: 100})
+ ]
+ });
+}
+
+export function roundedRectR() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x: 0, ...xy, r: 25}),
+ Plot.rect({length: 1}, {x: 1, ...xy, r: 50}),
+ Plot.rect({length: 1}, {x: 2, ...xy, r: 75}),
+ Plot.rect({length: 1}, {x: 3, ...xy, r: 100})
+ ]
+ });
+}
+
+export function roundedRectRx() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x: 0, ...xy, rx: 25}),
+ Plot.rect({length: 1}, {x: 1, ...xy, rx: 50}),
+ Plot.rect({length: 1}, {x: 2, ...xy, rx: 75}),
+ Plot.rect({length: 1}, {x: 3, ...xy, rx: 100})
+ ]
+ });
+}
+
+export function roundedRectRy() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x: 0, ...xy, ry: 25}),
+ Plot.rect({length: 1}, {x: 1, ...xy, ry: 50}),
+ Plot.rect({length: 1}, {x: 2, ...xy, ry: 75}),
+ Plot.rect({length: 1}, {x: 3, ...xy, ry: 100})
+ ]
+ });
+}
+
+export function roundedRectAsymmetricX() {
+ const xy = {y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2};
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x: 0, ...xy, rx1y1: 500, rx2y1: 50}),
+ Plot.rect({length: 1}, {x: 1, ...xy, rx2y1: 500, rx1y1: 50}),
+ Plot.rect({length: 1}, {x: 2, ...xy, rx2y2: 500, rx1y2: 50}),
+ Plot.rect({length: 1}, {x: 3, ...xy, rx1y2: 500, rx2y2: 50})
+ ]
+ });
+}
+
+export function roundedRectAsymmetricY() {
+ const xy = {x1: 0, x2: 1, inset: 4, insetTop: 2, insetBottom: 2};
+ return Plot.plot({
+ y: {inset: 2},
+ height: 400,
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {y: 0, ...xy, rx1y1: 500, rx1y2: 50}),
+ Plot.rect({length: 1}, {y: 1, ...xy, rx2y1: 500, rx2y2: 50}),
+ Plot.rect({length: 1}, {y: 2, ...xy, rx2y2: 500, rx2y1: 50}),
+ Plot.rect({length: 1}, {y: 3, ...xy, rx1y2: 500, rx1y1: 50})
+ ]
+ });
+}
+
+export function roundedRectCorners() {
+ return Plot.plot({
+ y: {reverse: true},
+ inset: 4,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x1: 0, x2: 1, y1: 0, y2: 1, inset: 4, rx1y1: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 2, y1: 0, y2: 1, inset: 4, rx2y1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 3, y1: 0, y2: 1, inset: 4, rx2y2: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 4, y1: 0, y2: 1, inset: 4, rx1y2: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 0, y1: 1, y2: 2, inset: 4, rx1y1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 1, y1: 1, y2: 2, inset: 4, rx2y1: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 2, y1: 1, y2: 2, inset: 4, rx2y2: 20}),
+ Plot.rect({length: 1}, {x1: 4, x2: 3, y1: 1, y2: 2, inset: 4, rx1y2: 20}),
+ Plot.rect({length: 1}, {x1: 0, x2: 1, y1: 3, y2: 2, inset: 4, rx1y1: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 2, y1: 3, y2: 2, inset: 4, rx2y1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 3, y1: 3, y2: 2, inset: 4, rx2y2: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 4, y1: 3, y2: 2, inset: 4, rx1y2: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 0, y1: 4, y2: 3, inset: 4, rx1y1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 1, y1: 4, y2: 3, inset: 4, rx2y1: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 2, y1: 4, y2: 3, inset: 4, rx2y2: 20}),
+ Plot.rect({length: 1}, {x1: 4, x2: 3, y1: 4, y2: 3, inset: 4, rx1y2: 20})
+ ]
+ });
+}
+
+export function roundedRectBand() {
+ return Plot.plot({
+ x: {inset: 2},
+ y: {reverse: true},
+ padding: 0,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x: 1, y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2, ry1: 20}),
+ Plot.rect({length: 1}, {x: 2, y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2, ry1: 20}),
+ Plot.rect({length: 1}, {x: 3, y1: 0, y2: 1, inset: 4, insetLeft: 2, insetRight: 2, ry1: 20})
+ ]
+ });
+}
+
+export function roundedRectCollapsedX() {
+ return Plot.plot({
+ y: {reverse: true},
+ marks: [Plot.frame(), Plot.rect({length: 1}, {x2: 1, y1: 0, y2: 1, inset: 4, ry1: 20})]
+ });
+}
+
+export function roundedRectCollapsedY() {
+ return Plot.plot({
+ marks: [Plot.frame(), Plot.rect({length: 1}, {x1: 0, x2: 1, y2: 1, inset: 4, ry1: 20})]
+ });
+}
+
+export function roundedRectSides() {
+ return Plot.plot({
+ y: {reverse: true},
+ inset: 4,
+ marks: [
+ Plot.frame(),
+ Plot.rect({length: 1}, {x1: 0, x2: 1, y1: 0, y2: 1, inset: 4, rx1: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 2, y1: 0, y2: 1, inset: 4, ry1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 3, y1: 0, y2: 1, inset: 4, rx2: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 4, y1: 0, y2: 1, inset: 4, ry2: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 0, y1: 1, y2: 2, inset: 4, rx1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 1, y1: 1, y2: 2, inset: 4, ry1: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 2, y1: 1, y2: 2, inset: 4, rx2: 20}),
+ Plot.rect({length: 1}, {x1: 4, x2: 3, y1: 1, y2: 2, inset: 4, ry2: 20}),
+ Plot.rect({length: 1}, {x1: 0, x2: 1, y1: 3, y2: 2, inset: 4, rx1: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 2, y1: 3, y2: 2, inset: 4, ry1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 3, y1: 3, y2: 2, inset: 4, rx2: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 4, y1: 3, y2: 2, inset: 4, ry2: 20}),
+ Plot.rect({length: 1}, {x1: 1, x2: 0, y1: 4, y2: 3, inset: 4, rx1: 20}),
+ Plot.rect({length: 1}, {x1: 2, x2: 1, y1: 4, y2: 3, inset: 4, ry1: 20}),
+ Plot.rect({length: 1}, {x1: 3, x2: 2, y1: 4, y2: 3, inset: 4, rx2: 20}),
+ Plot.rect({length: 1}, {x1: 4, x2: 3, y1: 4, y2: 3, inset: 4, ry2: 20})
+ ]
+ });
+}
+
+export async function roundedRectNegativeX() {
+ const olympians = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.plot({
+ color: {legend: true},
+ height: 640,
+ marks: [
+ Plot.rectX(olympians, Plot.binY({x: "count"}, {rx2: 4, rx1: -4, clip: "frame", y: "weight", fill: "sex"})),
+ Plot.ruleX([0])
+ ]
+ });
+}
+
+export async function roundedRectNegativeY() {
+ const olympians = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.plot({
+ color: {legend: true},
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {ry2: 4, ry1: -4, clip: "frame", x: "weight", fill: "sex"})),
+ Plot.ruleY([0])
+ ]
+ });
+}
+
+export async function roundedRectNegativeY1() {
+ const olympians = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.plot({
+ color: {legend: true},
+ marks: [
+ Plot.rectY(olympians, Plot.binX({y: "count"}, {rx1y2: 4, rx1y1: -4, clip: "frame", x: "weight", fill: "sex"})),
+ Plot.ruleY([0])
+ ]
+ });
+}
+
+export function roundedRectOpposing() {
+ return Plot.plot({
+ height: 400,
+ padding: 0,
+ inset: 10,
+ round: false,
+ marks: [
+ Plot.frame(),
+ Plot.cell({length: 1}, {x: 0, y: 0, inset: 10, rx1y1: 20, rx2y1: 20, rx2y2: 20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 1, y: 0, inset: 10, rx1y1: -20, rx2y1: 20, rx2y2: 20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 2, y: 0, inset: 10, rx1y1: 20, rx2y1: -20, rx2y2: 20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 3, y: 0, inset: 10, rx1y1: -20, rx2y1: -20, rx2y2: 20, rx1y2: 20, fill: "#5ca75b"}),
+ Plot.cell({length: 1}, {x: 0, y: 1, inset: 10, rx1y1: 20, rx2y1: 20, rx2y2: -20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 1, y: 1, inset: 10, rx1y1: -20, rx2y1: 20, rx2y2: -20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 2, y: 1, inset: 10, rx1y1: 20, rx2y1: -20, rx2y2: -20, rx1y2: 20, fill: "#5ca75b"}),
+ Plot.cell({length: 1}, {x: 3, y: 1, inset: 10, rx1y1: -20, rx2y1: -20, rx2y2: -20, rx1y2: 20}),
+ Plot.cell({length: 1}, {x: 0, y: 2, inset: 10, rx1y1: 20, rx2y1: 20, rx2y2: 20, rx1y2: -20}),
+ Plot.cell({length: 1}, {x: 1, y: 2, inset: 10, rx1y1: -20, rx2y1: 20, rx2y2: 20, rx1y2: -20, fill: "#5ca75b"}),
+ Plot.cell({length: 1}, {x: 2, y: 2, inset: 10, rx1y1: 20, rx2y1: -20, rx2y2: 20, rx1y2: -20}),
+ Plot.cell({length: 1}, {x: 3, y: 2, inset: 10, rx1y1: -20, rx2y1: -20, rx2y2: 20, rx1y2: -20}),
+ Plot.cell({length: 1}, {x: 0, y: 3, inset: 10, rx1y1: 20, rx2y1: 20, rx2y2: -20, rx1y2: -20, fill: "#5ca75b"}),
+ Plot.cell({length: 1}, {x: 1, y: 3, inset: 10, rx1y1: -20, rx2y1: 20, rx2y2: -20, rx1y2: -20}),
+ Plot.cell({length: 1}, {x: 2, y: 3, inset: 10, rx1y1: 20, rx2y1: -20, rx2y2: -20, rx1y2: -20}),
+ Plot.cell({length: 1}, {x: 3, y: 3, inset: 10, rx1y1: -20, rx2y1: -20, rx2y2: -20, rx1y2: -20})
+ ]
+ });
+}