Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rounded rect #2099

Merged
merged 22 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions docs/data/api.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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}`);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/features/facets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<a id="facetAnchor" class="header-anchor" href="#facetAnchor" aria-label="Permalink to &quot;facetAnchor&quot;"></a> <VersionBadge version="0.6.3" /> controls the placement of the mark with respect to the facets. Based on the value, the mark will be displayed on:
The **facetAnchor** option<a id="facetAnchor" href="#facetAnchor" aria-label="Permalink to &quot;facetAnchor&quot;"></a> <VersionBadge version="0.6.3" /> 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
Expand Down
6 changes: 2 additions & 4 deletions docs/features/marks.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,16 +531,14 @@ 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:
Marks with horizontal or vertical extents ([rects](../marks/rect.md), [bars](../marks/bar.md), [cells](../marks/cell.md), [frames](../marks/frame.md), [rules](../marks/rule.md), and [ticks](../marks/tick.md)), 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:

* **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.
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.)

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.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/plots.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<a id="aspectRatio" class="header-anchor" href="#aspectRatio" aria-label="Permalink to &quot;aspectRatio&quot;"></a> <VersionBadge version="0.6.4" />, 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<a id="aspectRatio" href="#aspectRatio" aria-label="Permalink to &quot;aspectRatio&quot;"></a> <VersionBadge version="0.6.4" />, 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.

<p>
<label class="label-input">
Expand Down
2 changes: 1 addition & 1 deletion docs/features/scales.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<a id="interval" class="header-anchor" href="#interval" aria-label="Permalink to &quot;interval&quot;"></a> <VersionBadge version="0.5.1" /> 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<a id="interval" href="#interval" aria-label="Permalink to &quot;interval&quot;"></a> <VersionBadge version="0.5.1" /> 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.

<p>
<label class="label-input">
Expand Down
1 change: 1 addition & 0 deletions docs/marks/box.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
88 changes: 80 additions & 8 deletions docs/marks/rect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]);
Expand Down Expand Up @@ -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
Expand All @@ -61,15 +62,15 @@ 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
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
Expand Down Expand Up @@ -161,16 +162,15 @@ 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
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,
Expand All @@ -186,6 +186,62 @@ 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.<a id="r" href="#r" aria-label="Permalink to &quot;r&quot;"></a> If the combined corner radii excede 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.
mbostock marked this conversation as resolved.
Show resolved Hide resolved

<label class="label-input" style="display: flex;">
<span style="display: inline-block; width: 7em;">r:</span>
<input type="range" v-model.number="r" min="0" max="25" step="0.2">
<span style="font-variant-numeric: tabular-nums;">{{r}}</span>
</label>

:::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.

:::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:
Expand All @@ -199,7 +255,23 @@ 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 rounded corners. Each corner (or side) is individually addressable using the following options <VersionBadge pr="2099" />:

* **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. TODO The rounded corner options also apply to the [bar](./bar.md), [cell](./cell.md), and [frame](./frame.md) marks.

The rect mark supports the [standard mark options](../features/marks.md#mark-options). The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.

## rect(*data*, *options*) {#rect}

Expand Down
Loading