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.

+ +

+ +:::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}) + ] + }); +}