diff --git a/CHANGELOG-2021.md b/CHANGELOG-2021.md
index eee5066d1c..581acbc615 100644
--- a/CHANGELOG-2021.md
+++ b/CHANGELOG-2021.md
@@ -24,9 +24,9 @@ Update D3 to 7.2.1.
[Released December 4, 2021.](https://github.com/observablehq/plot/releases/tag/v0.3.0)
-Plot can now produce [legends for *color* and *opacity* scales](./README.md#legends)!
+Plot can now produce [legends for *color* and *opacity* scales](https://observablehq.com/plot/features/legends)!
-[ ](https://observablehq.com/@observablehq/plot-legends)
+[ ](https://observablehq.com/plot/features/legends)
```js
Plot.plot({
@@ -41,9 +41,9 @@ Plot.plot({
The top-level plot *scale*.**legend** option generates an inline legend for the given *scale* (*color* or *opacity*). Alternatively, the new *plot*.legend(*name*) function returns a legend for the scale with the given *name*. The new standalone Plot.**legend**(*options*) function also allows you to create a legend independently of a chart. Two forms of color legend are provided: *swatches* for ordinal or discrete scales (*e.g.*, threshold color scales), and *ramp* for continuous scales.
-The new [Plot.image](./README.md#image) mark centers an image on the given *xy* position.
+The new [Plot.image](https://observablehq.com/plot/marks/image) mark centers an image on the given *xy* position.
-[ ](https://observablehq.com/@observablehq/plot-image)
+[ ](https://observablehq.com/plot/marks/image)
```js
Plot.plot({
@@ -223,7 +223,7 @@ The *x1* and *x2* outputs now default to undefined if *x* is explicitly defined;
### Marks
-The [*marks* option](./README.md#mark-options) now accepts render functions, null, and undefined as shorthand mark definitions. Nullish marks produce no output and are useful for conditional display (equivalent to the empty array). Render functions are invoked when plotting and may return an SVG element to insert into the plot, such as a legend or annotation.
+The [*marks* option](https://observablehq.com/plot/features/plots#marks-option) now accepts render functions, null, and undefined as shorthand mark definitions. Nullish marks produce no output and are useful for conditional display (equivalent to the empty array). Render functions are invoked when plotting and may return an SVG element to insert into the plot, such as a legend or annotation.
@@ -234,7 +234,7 @@ Plot.marks(
).plot()
```
-The [Plot.marks(...*marks*)](./README.md#plotmarksmarks) function provides [*mark*.plot](./README.md#plotplotoptions) shorthand for array marks. This is useful for composite marks, such as [boxes](https://github.com/observablehq/plot/blob/8fef4fa52a4cca4135f5f964e3c328ef8f18f672/test/plots/morley-boxplot.js#L18-L23).
+The [Plot.marks(...*marks*)](https://observablehq.com/plot/features/marks#marks) function provides [*mark*.plot](https://observablehq.com/plot/features/plots#mark_plot) shorthand for array marks. This is useful for composite marks, such as [boxes](https://github.com/observablehq/plot/blob/8fef4fa52a4cca4135f5f964e3c328ef8f18f672/test/plots/morley-boxplot.js#L18-L23).
All marks now support the [shapeRendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) option. (This is a constant; it may not vary across marks.) All marks now allow strokeWidth to be specified as a channel. (The strokeWidth channel is unscaled; values are specified in literal pixels.) Text marks now also allow stroke and strokeOpacity to be specified as channels. If its fill is not *none*, a line’s default stroke is now *none* rather than *currentColor*, making it consistent with dot and other marks. When a fill or fillOpacity channel is used with a link, or when a stroke or strokeOpacity channel is used with a rule, undefined values will now be filtered. The text mark now uses attributes instead of styles for font rendering properties, improving compatibility with Firefox.
@@ -256,7 +256,7 @@ The link mark now supports *x* or *y* shorthand for one-dimensional links, equiv
### Scales
-The new [*sort* options](./README.md#sort-options) allow convenient control over the order of ordinal domains, including the *fx* and *fy* facet domains. The aggregation method can be controlled via the *reduce* option, which defaults to *max*. The *reverse* and *limit* options are also supported. For example, a bar chart can be sorted by descending value like so:
+The new [*sort* mark option](https://observablehq.com/plot/features/scales#sort-mark-option) allows convenient control over the order of ordinal domains, including the *fx* and *fy* facet domains. The aggregation method can be controlled via the *reduce* option, which defaults to *max*. The *reverse* and *limit* options are also supported. For example, a bar chart can be sorted by descending value like so:
diff --git a/CHANGELOG-2022.md b/CHANGELOG-2022.md
index dd9e9502fa..76976e0590 100644
--- a/CHANGELOG-2022.md
+++ b/CHANGELOG-2022.md
@@ -6,9 +6,9 @@ Year: [Current (2023)](./CHANGELOG.md) · **2022** · [2021](./CHANGELOG-2021.md
[Released December 12, 2022.](https://github.com/observablehq/plot/releases/tag/v0.6.1)
-The new [geo mark](./README.md#geo) renders GeoJSON geometries such as polygons, lines, and points. Together with Plot’s new [projection system](https://observablehq.com/@observablehq/plot-projections), Plot can now produce [thematic maps](https://observablehq.com/@observablehq/plot-mapping). For example, the choropleth map below shows unemployment rates by U.S. county.
+The new [geo mark](https://observablehq.com/plot/marks/geo) renders GeoJSON geometries such as polygons, lines, and points. Together with Plot’s new [projection system](https://observablehq.com/plot/features/projections), Plot can now produce [thematic maps](https://observablehq.com/@observablehq/plot-mapping). For example, the choropleth map below shows unemployment rates by U.S. county.
-[ ](https://observablehq.com/@observablehq/plot-geo)
+[ ](https://observablehq.com/plot/marks/geo)
```js
Plot.geo(counties, {fill: (d) => d.properties.unemployment}).plot({
@@ -23,9 +23,9 @@ Plot.geo(counties, {fill: (d) => d.properties.unemployment}).plot({
})
```
-The new top-level [**projection** option](./README.md#projection-options) controls how geometric coordinates are transformed to the screen and supports a variety of common geographic projections, including the composite U.S. Albers projection shown above, the Equal Earth projection, the Mercator projection, the orthographic and stereographic projections, several conic and azimuthal projections, among others. Projections can be fit to geometry using the projection.**domain** option, and rotated to an arbitrary aspect using the projection.**rotate** option.
+The new top-level [**projection** option](https://observablehq.com/plot/features/projections) controls how geometric coordinates are transformed to the screen and supports a variety of common geographic projections, including the composite U.S. Albers projection shown above, the Equal Earth projection, the Mercator projection, the orthographic and stereographic projections, several conic and azimuthal projections, among others. Projections can be fit to geometry using the projection.**domain** option, and rotated to an arbitrary aspect using the projection.**rotate** option.
-[ ](https://observablehq.com/@observablehq/plot-projections)
+[ ](https://observablehq.com/plot/features/projections)
```js
Plot.plot({
@@ -67,9 +67,9 @@ Plot.plot({
})
```
-For the [line mark](./README.md#line), the specified projection doesn’t simply project control points; the projection has full control over how geometry is transformed from its native coordinate system (often spherical) to the screen. This allows line geometry to be represented as [geodesics](https://en.wikipedia.org/wiki/Geodesic), which are sampled and clipped during projection. For example, the map below shows the route of Charles Darwin’s voyage on the HMS *Beagle*; note that the line is cut when it crosses the antimeridian in the Pacific ocean. (Also note the use of the *stroke* channel to vary color.)
+For the [line mark](https://observablehq.com/plot/marks/line), the specified projection doesn’t simply project control points; the projection has full control over how geometry is transformed from its native coordinate system (often spherical) to the screen. This allows line geometry to be represented as [geodesics](https://en.wikipedia.org/wiki/Geodesic), which are sampled and clipped during projection. For example, the map below shows the route of Charles Darwin’s voyage on the HMS *Beagle*; note that the line is cut when it crosses the antimeridian in the Pacific ocean. (Also note the use of the *stroke* channel to vary color.)
-[ ](https://observablehq.com/@observablehq/plot-geo)
+[ ](https://observablehq.com/plot/marks/geo)
```js
Plot.plot({
@@ -121,7 +121,7 @@ Plot.plot({
})
```
-In addition to the included basic projections, Plot’s projection system can be extended using any projection implementation compatible with D3’s [projection stream interface](https://github.com/d3/d3-geo/blob/main/README.md#streams). This includes all the projections provided by the [d3-geo-projection](https://github.com/d3/d3-geo-projection) and [d3-geo-polygon](https://github.com/d3/d3-geo-polygon) libraries! For example, here is a world map using Goode’s interrupted homolosine projection.
+In addition to the included basic projections, Plot’s projection system can be extended using any projection implementation compatible with D3’s [projection stream interface](https://d3js.org/d3-geo/stream). This includes all the projections provided by the [d3-geo-projection](https://github.com/d3/d3-geo-projection) and [d3-geo-polygon](https://github.com/d3/d3-geo-polygon) libraries! For example, here is a world map using Goode’s interrupted homolosine projection.
[ ](https://observablehq.com/@observablehq/plot-extended-projections)
@@ -142,9 +142,9 @@ Plot.plot({
})
```
-Plot now supports [mark-level faceting](./README.md#facet-options) via the new *mark*.**fx** and *mark*.**fy** options. Mark-level faceting makes it easier to control which marks are faceted (versus repeated across facets), especially when combining multiple datasets or specifying faceted annotations.
+Plot now supports [mark-level faceting](https://observablehq.com/plot/features/facets#mark-facet-options) via the new *mark*.**fx** and *mark*.**fy** options. Mark-level faceting makes it easier to control which marks are faceted (versus repeated across facets), especially when combining multiple datasets or specifying faceted annotations.
-[ ](https://observablehq.com/@observablehq/plot-facets)
+[ ](https://observablehq.com/plot/features/facets)
```js
Plot.plot({
@@ -162,7 +162,7 @@ In addition to the above new features, this release also includes a variety of b
[Released September 7, 2022.](https://github.com/observablehq/plot/releases/tag/v0.6.0)
-[ ](https://observablehq.com/@observablehq/plot-window)
+[ ](https://observablehq.com/plot/transforms/window)
```js
Plot.plot({
@@ -174,7 +174,7 @@ Plot.plot({
})
```
-[breaking] [Plot.window](./README.md#plotwindowk), [Plot.windowX](./README.md#plotwindowxk-options) and [Plot.windowY](./README.md#plotwindowyk-options) now return an aggregate value even when the window contains undefined values, for example at the beginning or end of a series. Set the new **strict** option to true to instead return undefined if the window contains any undefined values.
+[breaking] [Plot.window](https://observablehq.com/plot/transforms/window#window), [Plot.windowX](https://observablehq.com/plot/transforms/window#windowX) and [Plot.windowY](https://observablehq.com/plot/transforms/window#windowY) now return an aggregate value even when the window contains undefined values, for example at the beginning or end of a series. Set the new **strict** option to true to instead return undefined if the window contains any undefined values.
Parts of the README have been incorporated throughout the codebase as JSDoc comments. This allows IDEs to display the documentation as tooltips.
@@ -192,9 +192,9 @@ Plot now uses D3 7.6.1, using [d3.blur2](https://observablehq.com/@d3/d3-blur) f
[Released June 27, 2022.](https://github.com/observablehq/plot/releases/tag/v0.5.1)
-The new [density mark](./README.md#density) creates contours representing the [estimated density](https://en.wikipedia.org/wiki/Multivariate_kernel_density_estimation) of two-dimensional point clouds. The **bandwidth** and number of **thresholds** are configurable.
+The new [density mark](https://observablehq.com/plot/marks/density) creates contours representing the [estimated density](https://en.wikipedia.org/wiki/Multivariate_kernel_density_estimation) of two-dimensional point clouds. The **bandwidth** and number of **thresholds** are configurable.
-[ ](https://observablehq.com/@observablehq/plot-density)
+[ ](https://observablehq.com/plot/marks/density)
```js
Plot.plot({
@@ -209,7 +209,7 @@ Plot.plot({
By default, as shown above, the density is represented by contour lines. By setting the **fill** option to *density*, you can draw filled regions with a sequential color encoding instead.
-[ ](https://observablehq.com/@observablehq/plot-density)
+[ ](https://observablehq.com/plot/marks/density)
```js
Plot.density(diamonds, {x: "carat", y: "price", fill: "density"}).plot({
@@ -221,9 +221,9 @@ Plot.density(diamonds, {x: "carat", y: "price", fill: "density"}).plot({
})
```
-The new [linear regression marks](./README.md#linear-regression) produce [linear regressions](https://en.wikipedia.org/wiki/Linear_regression) with [confidence interval](https://en.wikipedia.org/wiki/Confidence_interval) bands, representing the estimated relation of a dependent variable (typically *y*) on an independent variable (typically *x*).
+The new [linear regression marks](https://observablehq.com/plot/marks/linear-regression) produce [linear regressions](https://en.wikipedia.org/wiki/Linear_regression) with [confidence interval](https://en.wikipedia.org/wiki/Confidence_interval) bands, representing the estimated relation of a dependent variable (typically *y*) on an independent variable (typically *x*).
-[ ](https://observablehq.com/@observablehq/plot-linear-regression)
+[ ](https://observablehq.com/plot/marks/linear-regression)
```js
Plot.plot({
@@ -236,9 +236,9 @@ Plot.plot({
})
```
-The new [Delaunay and Voronoi marks](./README.md#delaunay) produce Delaunay triangulations and Voronoi tesselations: [Plot.delaunayLink](./README.md#plotdelaunaylinkdata-options) draws links for each edge of the Delaunay triangulation of the given points, [Plot.delaunayMesh](./README.md#plotdelaunaymeshdata-options) draws a mesh of the Delaunay triangulation of the given points, [Plot.hull](./README.md#plothulldata-options) draws a convex hull around the given points, [Plot.voronoi](./README.md#plotvoronoidata-options) draws polygons for each cell of the Voronoi tesselation of the given points, and [Plot.voronoiMesh](./README.md#plotvoronoimeshdata-options) draws a mesh for the cell boundaries of the Voronoi tesselation of the given points.
+The new [Delaunay and Voronoi marks](https://observablehq.com/plot/marks/delaunay) produce Delaunay triangulations and Voronoi tesselations: [Plot.delaunayLink](https://observablehq.com/plot/marks/delaunay#delaunayLink) draws links for each edge of the Delaunay triangulation of the given points, [Plot.delaunayMesh](https://observablehq.com/plot/marks/delaunay#delaunayMesh) draws a mesh of the Delaunay triangulation of the given points, [Plot.hull](https://observablehq.com/plot/marks/delaunay#hull) draws a convex hull around the given points, [Plot.voronoi](https://observablehq.com/plot/marks/delaunay#voronoi) draws polygons for each cell of the Voronoi tesselation of the given points, and [Plot.voronoiMesh](https://observablehq.com/plot/marks/delaunay#voronoiMesh) draws a mesh for the cell boundaries of the Voronoi tesselation of the given points.
-[ ](https://observablehq.com/@observablehq/plot-delaunay)
+[ ](https://observablehq.com/plot/marks/delaunay)
```js
Plot.plot({
@@ -249,7 +249,7 @@ Plot.plot({
})
```
-For data at regular intervals, such as integer values or daily samples, the new [*scale*.**interval** option](./README.md#scale-options) can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, for ordinal scales the default *scale*.**domain** is an array of uniformly-spaced values spanning the extent of the values associated with the scale.
+For data at regular intervals, such as integer values or daily samples, the new [*scale*.**interval** option](https://observablehq.com/plot/features/scales#interval) can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, for ordinal scales the default *scale*.**domain** is an array of uniformly-spaced values spanning the extent of the values associated with the scale.
All marks now support the **pointerEvents** option to set the [pointer-events attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events). The frame decoration mark now supports the **rx** and **ry** options. The cell mark now respects the **dx** and **dy** options.
@@ -263,11 +263,11 @@ Improve the error message when the **facet** option is used without **data**. Th
[Released June 7, 2022.](https://github.com/observablehq/plot/releases/tag/v0.5.0)
-Plot now supports [mark initializers](./README.md#initializers) via the **initializer** option. Initializers can transform data, channels, and indexes. Unlike [data transforms](./README.md#transforms) which operate in abstract data space, initializers can operate in screen space such as pixel coordinates and colors. For example, initializers can modify a marks’ positions to avoid occlusion. The new hexbin and dodge transforms are implemented as mark initializers.
+Plot now supports [mark initializers](https://observablehq.com/plot/features/transforms#custom-initializers) via the **initializer** option. Initializers can transform data, channels, and indexes. Unlike [data transforms](https://observablehq.com/plot/features/transforms) which operate in abstract data space, initializers can operate in screen space such as pixel coordinates and colors. For example, initializers can modify a marks’ positions to avoid occlusion. The new hexbin and dodge transforms are implemented as mark initializers.
-The new [hexbin transform](./README.md#hexbin) functions similarly to the bin transform, except it aggregates both *x* and *y* into hexagonal bins before reducing. The size of the hexagons can be specified with the **binWidth** option, which controls the width of the (pointy-topped) hexagons.
+The new [hexbin transform](https://observablehq.com/plot/transforms/hexbin) functions similarly to the bin transform, except it aggregates both *x* and *y* into hexagonal bins before reducing. The size of the hexagons can be specified with the **binWidth** option, which controls the width of the (pointy-topped) hexagons.
-[ ](https://observablehq.com/@observablehq/plot-hexbin)
+[ ](https://observablehq.com/plot/transforms/hexbin)
```js
Plot.plot({
@@ -286,9 +286,9 @@ Plot.plot({
})
```
-The new [dodge transform](./README.md#dodge) can be used to produce beeswarm plots. Given an *x* channel representing the desired horizontal position of circles, the dodgeY transform derives a new *y* (vertical position) channel such that the circles do not overlap; the dodgeX transform similarly derives a new *x* channel given a *y* channel.
+The new [dodge transform](https://observablehq.com/plot/transforms/dodge) can be used to produce beeswarm plots. Given an *x* channel representing the desired horizontal position of circles, the dodgeY transform derives a new *y* (vertical position) channel such that the circles do not overlap; the dodgeX transform similarly derives a new *x* channel given a *y* channel.
-[ ](https://observablehq.com/@observablehq/plot-dodge)
+[ ](https://observablehq.com/plot/transforms/dodge)
```js
Plot.plot({
@@ -304,7 +304,7 @@ Plot.plot({
If an *r* channel is specified, the circles may have varying radius. By default, the dodge transform sorts the input data by descending radius, such that the largest circles are placed first. The order of placement greatly affects the resulting layout; to change the placement order, use the standard mark **sort** option.
-[ ](https://observablehq.com/@observablehq/plot-dodge)
+[ ](https://observablehq.com/plot/transforms/dodge)
```js
Plot.plot({
@@ -331,13 +331,13 @@ When using the dodgeY transform, you should set the height of your plot explicit
[breaking] Color scales with diverging color schemes now default to the *diverging* scale type instead of the *linear* scale type. This includes the *brbg*, *prgn*, *piyg*, *puor*, *rdbu*, *rdgy*, *rdylbu*, *rdylgn*, *spectral*, *burd*, and *buylrd* schemes. If you want to use a diverging color scheme with a linear color scale, set the scale **type** option to *linear*. Color scales will also default to diverging if the scale **pivot** option is set. (For diverging scales, the pivot defaults to zero.)
-The [sort transform](./README.md#plotsortorder-options) now supports sorting on an existing channel, avoiding the need to duplicate the channel definition. For example, to sort dots by ascending radius:
+The [sort transform](https://observablehq.com/plot/transforms/sort) now supports sorting on an existing channel, avoiding the need to duplicate the channel definition. For example, to sort dots by ascending radius:
~~~js
Plot.dot(earthquakes, {x: "longitude", y: "latitude", r: "intensity", sort: {channel: "r"}})
~~~
-The [dot mark](./README.md#dot) now sorts by descending radius by default to reduce occlusion. The dot mark now supports the *hexagon* symbol type for pointy-topped hexagons. The new [circle](./README.md#plotcircledata-options) and [hexagon](./README.md#plothexagondata-options) marks are convenience shorthand for dot marks with the *circle* and *hexagon* symbol, respectively. The dotX, dotY, textX, and textY marks now support the **interval** option. The rule mark now correctly respects the **dx** and **dy** options. The new [hexgrid decoration mark](./README.md#hexgrid) draws a hexagonal grid; it is intended to be used with the hexbin transform as an alternative to the default horizontal and vertical axis grid.
+The [dot mark](https://observablehq.com/plot/marks/dot) now sorts by descending radius by default to reduce occlusion. The dot mark now supports the *hexagon* symbol type for pointy-topped hexagons. The new [circle](https://observablehq.com/plot/marks/dot#circle) and [hexagon](https://observablehq.com/plot/marks/dot#hexagon) marks are convenience shorthand for dot marks with the *circle* and *hexagon* symbol, respectively. The dotX, dotY, textX, and textY marks now support the **interval** option. The rule mark now correctly respects the **dx** and **dy** options. The new [hexgrid decoration mark](https://observablehq.com/plot/marks/hexgrid) draws a hexagonal grid; it is intended to be used with the hexbin transform as an alternative to the default horizontal and vertical axis grid.
The **zero** scale option (like the **nice** and **clamp** scale options) may now be specified as a top-level option, applying to all quantitative scales.
@@ -345,7 +345,7 @@ Marks can now define a channel hint to set the default range of the *r* scale. T
Improve the performance of internal array operations, including type coercion. Thanks, @yurivish!
-Fix a crash when using the [area mark](./README.md#area) shorthand.
+Fix a crash when using the [area mark](https://observablehq.com/plot/marks/area) shorthand.
[breaking] The return signature of the internal *mark*.initialize method has changed. It now returns a {data, facets, channels} object instead of {index, channels}, and *channels* is now represented as an object with named properties representing channels rather than an iterable of [*name*, *channel*].
@@ -353,7 +353,7 @@ Fix a crash when using the [area mark](./README.md#area) shorthand.
[Released April 12, 2022.](https://github.com/observablehq/plot/releases/tag/v0.4.3)
-The new [tree mark and transforms](./README.md#tree) can generate hierarchical node-link diagrams using D3’s [“tidy” tree](https://observablehq.com/@d3/tree) or [cluster (dendrogram)](https://observablehq.com/@d3/cluster) layout. The tree transform uses [d3.stratify](https://observablehq.com/@d3/d3-stratify) to convert tabular data into a hierarchy by parsing a slash-separated **path** for each row.
+The new [tree mark and transforms](https://observablehq.com/plot/marks/tree) can generate hierarchical node-link diagrams using D3’s [“tidy” tree](https://observablehq.com/@d3/tree) or [cluster (dendrogram)](https://observablehq.com/@d3/cluster) layout. The tree transform uses [d3.stratify](https://observablehq.com/@d3/d3-stratify) to convert tabular data into a hierarchy by parsing a slash-separated **path** for each row.
@@ -367,7 +367,7 @@ Plot.plot({
})
```
-The [line](./README.md#line) and [area](./README.md#area) marks (specifically lineX, lineY, areaX, and areaY) now support an implicit [bin transform](./README.md#bin) with the **interval** option. This can be used to “regularize” time series data, say to show gaps or default to zero when data is missing, rather than interpolating across missing data. This is also useful for stacking time series data that is sampled at irregular intervals or with missing samples.
+The [line](https://observablehq.com/plot/marks/line) and [area](https://observablehq.com/plot/marks/area) marks (specifically lineX, lineY, areaX, and areaY) now support an implicit [bin transform](https://observablehq.com/plot/transforms/bin) with the **interval** option. This can be used to “regularize” time series data, say to show gaps or default to zero when data is missing, rather than interpolating across missing data. This is also useful for stacking time series data that is sampled at irregular intervals or with missing samples.
@@ -395,7 +395,7 @@ Plot.plot({
})
```
-The [stack transform](./README.md#stack) now allows the **offset** option to be specified as a function. For example, this can be used to visualize Likert survey results with a neutral category as a [diverging stacked bar chart](https://observablehq.com/@observablehq/plot-diverging-stacked-bar).
+The [stack transform](https://observablehq.com/plot/transforms/stack) now allows the **offset** option to be specified as a function. For example, this can be used to visualize Likert survey results with a neutral category as a [diverging stacked bar chart](https://observablehq.com/@observablehq/plot-diverging-stacked-bar).
@@ -427,7 +427,7 @@ function Likert(
}
```
-The new [_quantize_ scale type](./README.md#color-options) transforms a continuous domain into discrete, evenly-spaced thresholds. The _threshold_ scale type now supports domains in descending order (in addition to ascending order), such as [20, 10, 5, 0] instead of [0, 5, 10, 20].
+The new [_quantize_ scale type](https://observablehq.com/plot/features/scales#color-scale-options) transforms a continuous domain into discrete, evenly-spaced thresholds. The _threshold_ scale type now supports domains in descending order (in addition to ascending order), such as [20, 10, 5, 0] instead of [0, 5, 10, 20].
@@ -445,17 +445,17 @@ Plot.plot({
})
```
-The [bin transform](./README.md#bin) now coerces the input channel (the quantity being binned) to numbers as necessary. In addition, the bin transform now correctly handles typed array input channels representing temporal data. The [rect mark](./README.md#rect) now promotes the _x_ channel to _x1_ and _x2_ if the latter two are not specified, and likewise the _y_ channel to _y1_ and _y2_.
+The [bin transform](https://observablehq.com/plot/transforms/bin) now coerces the input channel (the quantity being binned) to numbers as necessary. In addition, the bin transform now correctly handles typed array input channels representing temporal data. The [rect mark](https://observablehq.com/plot/marks/rect) now promotes the _x_ channel to _x1_ and _x2_ if the latter two are not specified, and likewise the _y_ channel to _y1_ and _y2_.
Fix a crash when **text** or **title** channels contain heterogenous types; each value is now independently formatted in a type-appropriate default formatter. Fix a rendering bug with one-dimensional rects whose opposite dimension is a band scale. Fix a rendering bug with swoopy arrows. Improve error messages to give more context.
-New helpers make it easier to implement custom transforms. [Plot.column](./README.md#plotcolumnsource) constructs lazily-evaluated columns for derived channels, and [Plot.transform](./README.md#plottransformoptions-transform) composes a [custom data transform](./README.md#custom-transforms) with any of Plot’s built-in [basic transforms](./README.md#transforms).
+New helpers make it easier to implement custom transforms. [Plot.column](https://observablehq.com/plot/features/transforms#column) constructs lazily-evaluated columns for derived channels, and [Plot.transform](https://observablehq.com/plot/features/transforms#transform) composes a [custom data transform](https://observablehq.com/plot/features/transforms#custom-transforms) with any of Plot’s built-in [basic transforms](https://observablehq.com/plot/features/transforms).
## 0.4.2
[Released February 26, 2022.](https://github.com/observablehq/plot/releases/tag/v0.4.2)
-The new [box mark](./README.md#box) generates a horizontal or vertical boxplot suitable for visualizing one-dimensional distributions. It is a convenience mark that composites a rule, bar, tick, and dot.
+The new [box mark](https://observablehq.com/plot/marks/box) generates a horizontal or vertical boxplot suitable for visualizing one-dimensional distributions. It is a convenience mark that composites a rule, bar, tick, and dot.
@@ -463,7 +463,7 @@ The new [box mark](./README.md#box) generates a horizontal or vertical boxplot s
Plot.boxX(morley, {x: "Speed", y: "Expt"}).plot({x: {grid: true, inset: 6}})
```
-[Plot’s shorthand syntax](https://observablehq.com/@observablehq/plot-shorthand) has been expanded. The [bar mark](./README.md#bar) now supports one-dimensional shorthand: if no *options* are specified, then Plot.barX and Plot.barY can be used to visualize an array of numbers. This shorthand also now applies to the [rect mark](./README.md#rect) and the [vector mark](./README.md#vector). The [area mark](./README.md#area) now supports two-dimensional shorthand: if no *options* are specified, then Plot.area can be used to visualize an array of *xy*-tuples, similar to Plot.line.
+[Plot’s shorthand syntax](https://observablehq.com/plot/features/shorthand) has been expanded. The [bar mark](https://observablehq.com/plot/marks/bar) now supports one-dimensional shorthand: if no *options* are specified, then Plot.barX and Plot.barY can be used to visualize an array of numbers. This shorthand also now applies to the [rect mark](https://observablehq.com/plot/marks/rect) and the [vector mark](https://observablehq.com/plot/marks/vector). The [area mark](https://observablehq.com/plot/marks/area) now supports two-dimensional shorthand: if no *options* are specified, then Plot.area can be used to visualize an array of *xy*-tuples, similar to Plot.line.
@@ -471,7 +471,7 @@ Plot.boxX(morley, {x: "Speed", y: "Expt"}).plot({x: {grid: true, inset: 6}})
Plot.barY(d3.range(20).map(Math.random)).plot()
```
-The mark [sort options](./README.md#sort-options) now support implicit “width” and “height” channels, defined as |*x2* - *x1*| and |*y2* - *y1*| respectively. These channels are useful for sorting rects and bars by length. The *reverse* option defaults to true when sorting by these channels. When sorting by *y* and no *y* channel is available, sorting will now fallback to *y2* if available; the same fallback logic applies to *x* and *x2*. (This behavior was previously supported on marks that support implicit stacking but now applies universally to all marks.)
+The mark [sort option](https://observablehq.com/plot/features/scales#sort-mark-option) now supports implicit “width” and “height” channels, defined as |*x2* - *x1*| and |*y2* - *y1*| respectively. These channels are useful for sorting rects and bars by length. The *reverse* option defaults to true when sorting by these channels. When sorting by *y* and no *y* channel is available, sorting will now fallback to *y2* if available; the same fallback logic applies to *x* and *x2*. (This behavior was previously supported on marks that support implicit stacking but now applies universally to all marks.)
@@ -479,7 +479,7 @@ The mark [sort options](./README.md#sort-options) now support implicit “width
Plot.rectY(energy, {x: "Year", interval: 1, y: "Value", fill: "Description", sort: {color: "height"}})
```
-The [bin transform](./README.md#bin) now supports *x* and *y* reducers which represent the midpoint of the bin: (*x1* + *x2*) / 2 and (*y1* + *y2*) / 2 respectively. The [bin](./README.md#bin), [group](./README.md#group), and [window](./README.md#window) transforms now support percentile reducers of the form *pXX* where *XX* is a number in [00, 99]; for example *p25* represents the first quartile and *p75* represents the third quartile.
+The [bin transform](https://observablehq.com/plot/transforms/bin) now supports *x* and *y* reducers which represent the midpoint of the bin: (*x1* + *x2*) / 2 and (*y1* + *y2*) / 2 respectively. The [bin](https://observablehq.com/plot/transforms/bin), [group](https://observablehq.com/plot/transforms/group), and [window](https://observablehq.com/plot/transforms/window) transforms now support percentile reducers of the form *pXX* where *XX* is a number in [00, 99]; for example *p25* represents the first quartile and *p75* represents the third quartile.
The error message when attempting to create a standalone legend without a valid scale definition has been improved. The high cardinality warning for the implicit *z* channel has been relaxed; it is now only triggered if more than half of the values are distinct. When the axis *ticks* option is specified as null, no ticks are generated. When the axis *tickFormat* option is specified as null, no tick labels are generated.
@@ -487,7 +487,7 @@ The error message when attempting to create a standalone legend without a valid
[Released February 17, 2022.](https://github.com/observablehq/plot/releases/tag/v0.4.1)
-The [area](./README.md#area) and [line marks](./README.md#line) now support varying fill, stroke, title, and other channels within series. For example, this chart of unemployment rates by metro area highlights increases in red and decreases in blue using a window transform with the *difference* reducer.
+The [area](https://observablehq.com/plot/marks/area) and [line marks](https://observablehq.com/plot/marks/line) now support varying fill, stroke, title, and other channels within series. For example, this chart of unemployment rates by metro area highlights increases in red and decreases in blue using a window transform with the *difference* reducer.
@@ -515,7 +515,7 @@ Plot.line(aapl, {x: "Date", y: "Close"}) // 🌶 Oops, Date is a string!
We will add [more warnings](https://github.com/observablehq/plot/issues/755) in the future. If Plot did something you didn’t expect, please [let us know](https://github.com/observablehq/plot/discussions); perhaps it will inspire a new warning that will help other users.
-The [text mark](./README.md#text) now supports automatic wrapping for easier annotation. The new **lineWidth** option specifies the desired length of a line in ems. The line breaking, wrapping, and text metrics implementations are all rudimentary, but they should be acceptable for text that is mostly ASCII. (For more control, you can hard-wrap text manually.) The **monospace** option now provides convenient defaults for monospaced text.
+The [text mark](https://observablehq.com/plot/marks/text) now supports automatic wrapping for easier annotation. The new **lineWidth** option specifies the desired length of a line in ems. The line breaking, wrapping, and text metrics implementations are all rudimentary, but they should be acceptable for text that is mostly ASCII. (For more control, you can hard-wrap text manually.) The **monospace** option now provides convenient defaults for monospaced text.
@@ -523,7 +523,7 @@ The [text mark](./README.md#text) now supports automatic wrapping for easier ann
Plot.text([mobydick], {dx: 6, dy: 6, fontSize: 12, lineWidth: 80, lineHeight: 1.2, frameAnchor: "top-left", monospace: true})
```
-The line and link marks now support [marker options](./README.md#markers) for drawing a shape such as a dot or arrowhead on each vertex. Circle and arrow markers are provided, or you can implement a custom marker function that returns an SVG marker element. Markers automatically inherit the stroke color of the associated mark.
+The line and link marks now support [marker options](https://observablehq.com/plot/features/markers) for drawing a shape such as a dot or arrowhead on each vertex. Circle and arrow markers are provided, or you can implement a custom marker function that returns an SVG marker element. Markers automatically inherit the stroke color of the associated mark.
@@ -557,9 +557,9 @@ Fix a crash in default tuple accessors for *x* and *y* when data is undefined. F
[Released January 20, 2022.](https://github.com/observablehq/plot/releases/tag/v0.4.0)
-The new [arrow mark](./README.md#arrow) draws arrows between pairs of points. It is similar to the [link mark](./README.md#link), except it is suitable for directed edges (say for representing change over time) and supports a configurable arrowhead. It also supports “swoopy” arrows with the *bend* option, and insets for arrows to shorten the arrow’s start or end.
+The new [arrow mark](https://observablehq.com/plot/marks/arrow) draws arrows between pairs of points. It is similar to the [link mark](https://observablehq.com/plot/marks/link), except it is suitable for directed edges (say for representing change over time) and supports a configurable arrowhead. It also supports “swoopy” arrows with the *bend* option, and insets for arrows to shorten the arrow’s start or end.
-[ ](https://observablehq.com/@observablehq/plot-arrow)
+[ ](https://observablehq.com/plot/marks/arrow)
```js
Plot.arrow(data, {
@@ -572,9 +572,9 @@ Plot.arrow(data, {
})
```
-The new [vector mark](./README.md#vector) similarly draws arrows at the given position (*x* and *y*) with the given magnitude (*length*) and direction (*rotate*). It is intended to visualize vector fields, such as a map of wind speed and direction.
+The new [vector mark](https://observablehq.com/plot/marks/vector) similarly draws arrows at the given position (*x* and *y*) with the given magnitude (*length*) and direction (*rotate*). It is intended to visualize vector fields, such as a map of wind speed and direction.
-[ ](https://observablehq.com/@observablehq/plot-vector)
+[ ](https://observablehq.com/plot/marks/vector)
```js
Plot.vector((T => d3.cross(T, T))(d3.ticks(0, 2 * Math.PI, 20)), {
@@ -583,17 +583,17 @@ Plot.vector((T => d3.cross(T, T))(d3.ticks(0, 2 * Math.PI, 20)), {
})
```
-The [dot mark](./README.md#dot) now supports a *symbol* option to control the displayed shape, which defaults to *circle*. The *symbol* channel (and associated *symbol* scale) can also be used as an categorical encoding. The default symbol set is based on whether symbols are stroked or filled, improving differentiability and giving uniform weight. Plot supports all of D3’s built-in symbol types: *circle*, *cross*, *diamond*, *square*, *star*, *triangle*, and *wye* (for fill) and *circle*, *plus*, *times*, *triangle2*, *asterisk*, *square2*, and *diamond2* (for stroke, based on [Heman Robinson’s research](https://www.tandfonline.com/doi/abs/10.1080/10618600.2019.1637746)); you can also implement a [custom symbol type](https://github.com/d3/d3-shape/blob/main/README.md#custom-symbol-types).
+The [dot mark](https://observablehq.com/plot/marks/dot) now supports a *symbol* option to control the displayed shape, which defaults to *circle*. The *symbol* channel (and associated *symbol* scale) can also be used as an categorical encoding. The default symbol set is based on whether symbols are stroked or filled, improving differentiability and giving uniform weight. Plot supports all of D3’s built-in symbol types: *circle*, *cross*, *diamond*, *square*, *star*, *triangle*, and *wye* (for fill) and *circle*, *plus*, *times*, *triangle2*, *asterisk*, *square2*, and *diamond2* (for stroke, based on [Heman Robinson’s research](https://www.tandfonline.com/doi/abs/10.1080/10618600.2019.1637746)); you can also implement a [custom symbol type](https://d3js.org/d3-shape/symbol#custom-symbols).
-[ ](https://observablehq.com/@observablehq/plot-dot)
+[ ](https://observablehq.com/plot/marks/dot)
```js
Plot.dot(penguins, {x: "body_mass_g", y: "flipper_length_mm", stroke: "species", symbol: "species"})
```
-The [text mark](./README.md#text) now supports multiline text! When a text value contains `\r`, `\r\n`, or `\n`, it will be split into multiple lines using tspan elements. The new *lineAnchor* and *lineHeight* options control how the lines are positioned relative to the given *xy* position. The text, dot, and image marks now also support a *frameAnchor* option for positioning relative to the frame rather than according to data. This is particularly useful for annotations.
+The [text mark](https://observablehq.com/plot/marks/text) now supports multiline text! When a text value contains `\r`, `\r\n`, or `\n`, it will be split into multiple lines using tspan elements. The new *lineAnchor* and *lineHeight* options control how the lines are positioned relative to the given *xy* position. The text, dot, and image marks now also support a *frameAnchor* option for positioning relative to the frame rather than according to data. This is particularly useful for annotations.
-[ ](https://observablehq.com/@observablehq/plot-text)
+[ ](https://observablehq.com/plot/marks/text)
```js
Plot.plot({
@@ -613,9 +613,9 @@ All marks now support the new standard *href* channel and *target* option, turni
Plot.barY(alphabet, {x: "letter", y: "frequency", href: d => `https://en.wikipedia.org/wiki/${d.letter}`})
```
-The [bin](./README.md#bin) and [group](./README.md#group) transforms now propagate the *title* and *href* channels, if present, by default. The default reducer for the *title* channel automatically selects the top five distinct title values by count, making it easier to inspect the contents of a given bin or group.
+The [bin](https://observablehq.com/plot/transforms/bin) and [group](https://observablehq.com/plot/transforms/group) transforms now propagate the *title* and *href* channels, if present, by default. The default reducer for the *title* channel automatically selects the top five distinct title values by count, making it easier to inspect the contents of a given bin or group.
-[ ](https://observablehq.com/@observablehq/plot-bin)
+[ ](https://observablehq.com/plot/transforms/bin)
```js
Plot.rectY(data, Plot.binX({y: "count"}, {x: "body_mass_g", fill: "species", title: d => `${d.species} ${d.sex}`}))
@@ -623,7 +623,7 @@ Plot.rectY(data, Plot.binX({y: "count"}, {x: "body_mass_g", fill: "species", tit
The bin transform now supports shorthand reducers for the bin extent: *x1*, *x2*, *y1*, and *y2*. The window transform now supports the *first* and *last* reducers to select the first or last element of the window, respectively.
-The new generalized [select transform](./README.md#select) can now call a custom selector function, or the shorthand *min* and *max*, to select the points to display. The selector function is passed two arguments: the index of the current group (*e.g.*, [0, 1, 2, …]) and the given channel’s values. For example, to select the dot with the greatest *fill* value:
+The new generalized [select transform](https://observablehq.com/plot/transforms/select) can now call a custom selector function, or the shorthand *min* and *max*, to select the points to display. The selector function is passed two arguments: the index of the current group (*e.g.*, [0, 1, 2, …]) and the given channel’s values. For example, to select the dot with the greatest *fill* value:
```js
Plot.dotX(data, Plot.select({fill: "max"}, {x: "letter", fill: "frequency", stroke: "black"})
@@ -631,7 +631,7 @@ Plot.dotX(data, Plot.select({fill: "max"}, {x: "letter", fill: "frequency", stro
The *color* scale now defaults to an *identity* scale if all associated defined values are valid CSS colors, rather than defaulting to the tableau10 categorical color scheme. The new *symbol* scale similarly defaults to *identity* if all associated defined values are valid symbol names (or symbol type objects).
-[ ](https://observablehq.com/@observablehq/plot-bar)
+[ ](https://observablehq.com/plot/marks/bar)
```js
Plot.barY(alphabet, {x: "letter", y: "frequency", fill: d => /[AEIOU]/.test(d.letter) ? "red" : "black"})
@@ -639,13 +639,13 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", fill: d => /[AEIOU]/.test(d.le
The *color* scale now has a special default range for boolean data, encoding false as light gray and true as dark gray. If you’d prefer more color, specify a sequential scheme such as *reds* or *blues*. (You can opt-out of the special boolean range by setting the scale type to *categorical* or by specifying an explicit *range*.)
-[ ](https://observablehq.com/@observablehq/plot-bar)
+[ ](https://observablehq.com/plot/marks/bar)
```js
Plot.barY(alphabet, {x: "letter", y: "frequency", fill: d => /[AEIOU]/.test(d.letter)})
```
-The new [Plot.scale](./README.md#scale-options) method allows you to construct a standalone scale for use independent of any chart, or across charts. The returned object has the same form as *plot*.scale(*name*), allowing you to inspect the scale options and invoke the scale programmatically with *scale*.apply (and *scale*.invert, where applicable).
+The new [Plot.scale](https://observablehq.com/plot/features/scales#scale) method allows you to construct a standalone scale for use independent of any chart, or across charts. The returned object has the same form as *plot*.scale(*name*), allowing you to inspect the scale options and invoke the scale programmatically with *scale*.apply (and *scale*.invert, where applicable).
```js
const scale = Plot.scale({color: {type: "linear"}});
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27b971440d..e7de2c7826 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,145 @@
Year: **Current (2023)** · [2022](./CHANGELOG-2022.md) · [2021](./CHANGELOG-2021.md)
+## 0.6.10
+
+[Released August 14, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.10)
+
+The new **title** and **subtitle** [plot options](https://observablehq.com/plot/features/plots#other-options) specify a primary and secondary heading. Headings are implemented as h2 and h3 elements by default, but you can provide existing elements instead of text for greater control. Like the existing **caption** option, headings add context and assist interpretation.
+
+
+
+```js
+Plot.plot({
+ title: "For charts, an informative title",
+ subtitle: "Subtitle to follow with additional context",
+ caption: "Figure 1. A chart with a title, subtitle, and caption.",
+ marks: [
+ Plot.frame(),
+ Plot.text(["Titles, subtitles, captions, and annotations assist interpretation by telling the reader what’s interesting. Don’t make the reader work to find what you already know."], {lineWidth: 30, frameAnchor: "middle"})
+ ]
+})
+```
+
+When a chart has a title, subtitle, caption, or legend, Plot automatically wraps the chart’s SVG element with an HTML figure element. The new **figure** plot option, if true, wraps the chart in a figure even if it doesn’t have these other elements; likewise, if false, Plot ignores these other elements and returns a bare SVG element. The figure element now has an associated class (`plot-d6a7b5-figure`).
+
+The new **clip** plot option determines the default clipping behavior if the [**clip** mark option](https://observablehq.com/plot/features/marks#mark-options) is not specified; set it to true to enable clipping. This option does not affect axis, grid, and frame marks, whose **clip** option defaults to false.
+
+
+
+```js
+Plot.plot({
+ clip: true,
+ x: {domain: [new Date(2015, 0, 1), new Date(2015, 3, 1)]},
+ y: {grid: true},
+ marks: [
+ Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.1}),
+ Plot.lineY(aapl, {x: "Date", y: "Close"}),
+ Plot.ruleY([0], {clip: false})
+ ]
+});
+```
+
+The new [bollinger mark](https://observablehq.com/plot/marks/bollinger) composes a line representing a moving average and an area representing volatility as a band; the band thickness is proportional to the deviation of nearby values. The bollinger mark is built on the new [bollinger map method](https://observablehq.com/plot/marks/bollinger#bollinger), and is often used to analyze the price of financial instruments such as stocks.
+
+
+
+```js
+Plot.bollingerY(aapl, {x: "Date", y: "Close", n: 20, k: 2}).plot()
+```
+
+The [arrow mark](https://observablehq.com/plot/marks/arrow) supports a new **sweep** option to control the bend orientation. Below, we set this option to *-y* to draw arrows bulging right, independent of the relative vertical positions of its source and target.
+
+[ ](https://observablehq.com/@observablehq/plot-arc-diagram?intent=fork)
+
+```js
+Plot.plot({
+ height: 1080,
+ marginLeft: 100,
+ axis: null,
+ x: {domain: [0, 1]}, // see https://github.com/observablehq/plot/issues/1541
+ color: {domain: d3.range(10), unknown: "#ccc"},
+ marks: [
+ Plot.dot(miserables.nodes, {x: 0, y: "id", fill: "group", sort: {y: "fill"}}),
+ Plot.text(miserables.nodes, {x: 0, y: "id", text: "id", textAnchor: "end", dx: -6, fill: "group"}),
+ Plot.arrow(miserables.links, {x: 0, y1: "source", y2: "target", sweep: "-y", bend: 90, headLength: 0, stroke: samegroup, sort: samegroup, reverse: true})
+ ]
+})
+```
+
+The [auto mark](https://observablehq.com/plot/marks/auto) now does a better job determining the appropriate bar mark implementation, such as with ordinal time series bar charts.
+
+
+
+```js
+Plot.auto(timeSeries, {x: "date", y: {value: "value", reduce: "sum"}, color: "type", mark: "bar"}).plot()
+```
+
+The [pointerX and pointerY transform](https://observablehq.com/plot/interactions/pointer) now use unscaled distance to decide the closest point across facets, preventing points from distant facets from being considered closest. The pointer transform now correctly reports the closest point when moving between facets, and no longer reports multiple closest points if they are the same distance across facets.
+
+Plot’s documentation now has an [API index](https://observablehq.com/plot/api), version badges pointing to the release notes for a particular feature (or to the pull request for a prerelease feature), and shorter anchors.
+
+The [tip mark](https://observablehq.com/plot/marks/tip) now shows both labels for paired channels such as *y1*–*y2* or *x1*–*y2* when the channel labels differ. When the **tip** option is set to true on a [geo mark](https://observablehq.com/plot/marks/geo) without a projection, as when using preprojected planar geometry, the display no longer collapses.
+
+The [stack transform](https://observablehq.com/plot/transforms/stack) now emits a friendlier error message when the supplied value is null.
+
+## 0.6.9
+
+[Released June 27, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.9)
+
+Time [axes](https://observablehq.com/plot/marks/axis) now default to multi-line ticks, greatly improving readability. When a tick has the same second field value as the previous tick (*e.g.*, “19 Jan” after “17 Jan”), only the first field (“19”) is shown for brevity. The tick format is now based on the tick interval and hence is always consistent, whereas the prior “multi-scale” format varied based on the date, such as “Jan 29” (for Sunday, January 29) and “Tue 31” (for Tuesday, January 31). The new ticks are similar to [Datawrapper](https://blog.datawrapper.de/new-axis-ticks/).
+
+Before:
+
+
+After:
+
+
+It is now easier to construct a “piecewise” [continuous scale](https://observablehq.com/plot/features/scales#continuous-scales) with more than two elements in the **domain** or **range**. This is most often used for a custom color scheme interpolating through fixed colors, such as this pleasing rainbow (sometimes used by artist [Dave Whyte](https://beesandbombs.com/), *a.k.a.* beesandbombs).
+
+
+
+```js
+Plot.plot({
+ color: {
+ type: "linear",
+ range: ["#d70441", "#f4e904", "#009978", "#5e3688"]
+ },
+ marks: [
+ Plot.cellX(d3.range(40), {fill: Plot.identity})
+ ]
+})
+```
+
+The [tree mark](https://observablehq.com/plot/marks/tree) now supports a **textLayout** option, which defaults to *mirrored* to alternate the orientation of labels for internal (non-leaf) *vs.* external (leaf) nodes. The treeNode and treeLink marks now also support a new **treeFilter** option, allowing these marks to be filtered without affecting the tree layout.
+
+
+
+```js
+Plot.plot({
+ axis: null,
+ height: 100,
+ margin: 10,
+ marginLeft: 40,
+ marginRight: 120,
+ marks: [
+ Plot.tree(gods, {textStroke: "white"})
+ ]
+})
+```
+
+The [barycentric interpolator](https://observablehq.com/plot/marks/raster#interpolatorBarycentric) used by the [raster](https://observablehq.com/plot/marks/raster) and [contour](https://observablehq.com/plot/marks/contour) marks now behaves correctly outside the convex hull of samples. The new algorithm (below right) radiates outwards from the hull, ensuring a continuous image; the old algorithm (below left) radiated inwards from values imputed on the frame’s edges, producing discontinuities.
+
+
+
+The [tip mark](https://observablehq.com/plot/marks/tip) now automatically sets the pointer-events attribute to *none* when associated with the [pointer transform](https://observablehq.com/plot/interactions/pointer) when the the pointer is not sticky, as when hovering a chart without clicking to lock the pointer. This prevents the tip mark from interfering with interaction on other marks, such as clickable links.
+
+The [auto mark](https://observablehq.com/plot/marks/auto) now renders as a cell, instead of a degenerate invisible rect, when **x** and **y** are both ordinal and the **mark** option is set to *bar*. The [tree mark](https://observablehq.com/plot/marks/tree) no longer produces duplicate tips with the **tip** option. The [rule mark](https://observablehq.com/plot/marks/rule) now respects the top-level **document** option, if any, when using the **clip** option. The [axis mark](https://observablehq.com/plot/marks/axis) now correctly handles the **sort**, **filter**, **reverse**, and **initializer** options.
+
+The **title**, **ariaLabel**, and **href** channels no longer filter by default; these channels may now be sparsely defined and the associated mark instance will still render.
+
+The [pointer transform](https://observablehq.com/plot/interactions/pointer) now handles non-faceted marks in faceted plots. The [window transform](https://observablehq.com/plot/transforms/window)’s *median*, *deviation*, *variance*, and percentile reducers have been fixed.
+
## 0.6.8
[Released June 2, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.8)
@@ -196,13 +335,13 @@ Fix the auto mark to choose the rect mark instead of rectX or rectY when appropr
👉 https://observablehq.com/plot 👈
-The [image mark](./README.md#image) can now generate circular images with the **r** channel, and rotate images with the **rotate** channel.
+The [image mark](https://observablehq.com/plot/marks/image) can now generate circular images with the **r** channel, and rotate images with the **rotate** channel.
-The [axis mark](./README.md#axis) now properly respects the **margin** shorthand option, changing the default for **marginTop**, **marginRight**, **marginBottom**, and **marginLeft**. The axis mark now correctly renders the axis label when the **href** option is used, or any other option that may be interpreted as a channel.
+The [axis mark](https://observablehq.com/plot/marks/axis) now properly respects the **margin** shorthand option, changing the default for **marginTop**, **marginRight**, **marginBottom**, and **marginLeft**. The axis mark now correctly renders the axis label when the **href** option is used, or any other option that may be interpreted as a channel.
Facet scale domains are now imputed correctly when the **sort** mark option is used with a **limit**, or otherwise causing the facet domain to be truncated. Plot no longer generates a spurious warning when faceting and using non-array data, such as an Arquero table. The **interval** scale option, when expressed as a fractional number such as 0.2, now has better floating point precision.
-The [Plot.indexOf](./README.md#plotindexof) channel transform, used internally by some mark shorthand, is now exported.
+The [Plot.indexOf](https://observablehq.com/plot/features/transforms#indexOf) channel transform, used internally by some mark shorthand, is now exported.
Plot has a few improvements for server-side rendering. Plot now assumes a high pixel density display when headless. The default class name for plots is now deterministically generated (`plot-d6a7b5`) rather than randomly generated; this makes it easier to apply overrides to Plot’s default styles with an external stylesheet. (The default class name will change if Plot’s default styles change in a future release.) The **className** plot option is now inherited by a plot’s legends, if any. The density mark now respects the Plot’s **document** option, and the **caption** option now uses a duck test instead of testing against the global Node.
@@ -244,7 +383,7 @@ The interfaces for reduce and map implementations have changed. To better disamb
[Released February 28, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.4)
-The new top-level [**aspectRatio** option](./README.md#layout-options) changes the default plot **height** such that, assuming both *x* and *y* are *linear* scales, a scaled unit distance in *x* divided by a scaled unit distance in *y* is the given aspect ratio. For example, if *x* and *y* represent the same units (say, degrees Fahrenheit), and if the **aspectRatio** is one, then scaled distances in *x* and *y* will be equivalent.
+The new top-level [**aspectRatio** option](https://observablehq.com/plot/features/plots#aspectRatio) changes the default plot **height** such that, assuming both *x* and *y* are *linear* scales, a scaled unit distance in *x* divided by a scaled unit distance in *y* is the given aspect ratio. For example, if *x* and *y* represent the same units (say, degrees Fahrenheit), and if the **aspectRatio** is one, then scaled distances in *x* and *y* will be equivalent.
@@ -266,17 +405,17 @@ Plot.plot({
})
```
-The new **textOverflow** option for the [text mark](./README.md#text) allows text to be truncated when a line of text is longer than the specified **lineWidth**. Overflowing characters can either be clipped (*clip*) or replaced with an ellipsis (*ellipsis*), either at the start, middle, or end of each line.
+The new **textOverflow** option for the [text mark](https://observablehq.com/plot/marks/text) allows text to be truncated when a line of text is longer than the specified **lineWidth**. Overflowing characters can either be clipped (*clip*) or replaced with an ellipsis (*ellipsis*), either at the start, middle, or end of each line.
When wrapping or truncating, the text mark now more accurately estimates the width of ellipses and emojis, and no longer separates combining marks or emoji character sequences such as 👨👩👧👦.
-The [link mark](./README.md#link) now respects the current [**projection**](./README.md#projection-options), if any, given the default [**curve**](./README.md#curves) of *auto*. This matches the behavior of the line mark. To opt-out of the projection and draw a straight line, set the **curve** to *linear*.
+The [link mark](https://observablehq.com/plot/marks/link) now respects the current [**projection**](https://observablehq.com/plot/features/projections), if any, given the default [**curve**](https://observablehq.com/plot/features/curves) of *auto*. This matches the behavior of the line mark. To opt-out of the projection and draw a straight line, set the **curve** to *linear*.
-The [image mark](./README.md#image) now supports the **imageRendering** option. (Note: Safari currently ignores the SVG image-rendering attribute.)
+The [image mark](https://observablehq.com/plot/marks/image) now supports the **imageRendering** option. (Note: Safari currently ignores the SVG image-rendering attribute.)
-You can now override the scale for a given [mark channel](./README.md#mark-options) by specifying the corresponding option as a {value, scale} object. For example, to force the **stroke** channel to be unscaled, interpreting the associated values as literal color strings:
+You can now override the scale for a given [mark channel](https://observablehq.com/plot/features/marks#marks-have-channels) by specifying the corresponding option as a {value, scale} object. For example, to force the **stroke** channel to be unscaled, interpreting the associated values as literal color strings:
```js
Plot.dot(data, {stroke: {value: "foo", scale: null}})
@@ -290,9 +429,9 @@ Plot.dot(data, {stroke: {value: "foo", scale: "color"}})
Color channels (**fill** and **stroke**) are bound to the *color* scale by default, unless the provided values are all valid CSS color strings or nullish, in which case the values are interpreted literally and unscaled. Likewise, if the dot mark’s **symbol** channel values are all symbols, symbol names, or nullish, values are interpreted literally and unscaled; otherwise, the channel is bound to the *symbol* scale. (If some color channels are literal values while other color channels are not, the channels with literal values will now automatically opt-out of the color scale; the same goes for symbol channels. This deviates from the previous behavior, where *all* channels associated with a scale were required to be literal values in order to have the scale default to an *identity* scale.)
-The mark [**facetAnchor** option](./README.md#facet-options) can now be set to *empty* such that a mark is only rendered on empty facets. This is typically used for annotation.
+The mark [**facetAnchor** option](https://observablehq.com/plot/features/facets#facetAnchor) can now be set to *empty* such that a mark is only rendered on empty facets. This is typically used for annotation.
-The new Plot.autoSpec method takes *data* and *options* suitable for [Plot.auto](./README.md#aut) and returns a corresponding *options* object with default options realized. While intended primarily as an internal helper, Plot.autoSpec may be useful for debugging by letting you inspect which mark and reducers are chosen by Plot.auto.
+The new Plot.autoSpec method takes *data* and *options* suitable for [Plot.auto](https://observablehq.com/plot/marks/auto) and returns a corresponding *options* object with default options realized. While intended primarily as an internal helper, Plot.autoSpec may be useful for debugging by letting you inspect which mark and reducers are chosen by Plot.auto.
Fix Plot.auto to only default to the *bar* mark if *x* or *y* is zeroed. Fix Plot.auto’s support for the *area* mark. Fix Plot.auto’s use of the *bar* mark with possibly ordinal reducers. Fix a bug where arrays of values could be erroneously interpreted as reducers. Fix a crash when the mark **facet** option is set to *exclude*, but the mark is not faceted; the option is now ignored. Fix a crash coercing BigInt values to numbers.
@@ -300,51 +439,51 @@ Fix Plot.auto to only default to the *bar* mark if *x* or *y* is zeroed. Fix Plo
[Released February 6, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.3)
-The new [auto mark](./README.md#auto) ([Plot.auto](./README.md#plotautodata-options)) automatically selects a mark type that best represents the given dimensions of data according to some simple heuristics. For example,
+The new [auto mark](https://observablehq.com/plot/marks/auto) ([Plot.auto](https://observablehq.com/plot/marks/auto#auto)) automatically selects a mark type that best represents the given dimensions of data according to some simple heuristics. For example,
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.auto(olympians, {x: "height", y: "weight"}).plot()
```
-makes a scatterplot (equivalent to [dot](./README.md#plotdotdata-options)); adding **color** as
+makes a scatterplot (equivalent to [dot](https://observablehq.com/plot/marks/dot)); adding **color** as
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.auto(olympians, {x: "height", y: "weight", color: "count"}).plot()
```
-makes a heatmap (equivalent to [rect](./README.md#plotrectdata-options) and [bin](./README.md#plotbinoutputs-options); chosen since _height_ and _weight_ are quantitative); switching to
+makes a heatmap (equivalent to [rect](https://observablehq.com/plot/marks/rect) and [bin](https://observablehq.com/plot/transforms/bin); chosen since _height_ and _weight_ are quantitative); switching to
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.auto(aapl, {x: "Date", y: "Close"}).plot()
```
-makes a line chart (equivalent to [lineY](./README.md#plotlineydata-options); chosen because the selected *x* dimension *Date* is temporal and monotonic, _i.e._, the data is in chronological order);
+makes a line chart (equivalent to [lineY](https://observablehq.com/plot/marks/line#lineY); chosen because the selected *x* dimension *Date* is temporal and monotonic, _i.e._, the data is in chronological order);
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.auto(penguins, {x: "body_mass_g"}).plot()
```
-makes a histogram (equivalent to [rectY](./README.md#plotrectydata-options) and [binX](./README.md#plotbinxoutputs-options); chosen because the _body_mass_g_ column is quantitative); and
+makes a histogram (equivalent to [rectY](https://observablehq.com/plot/marks/rect#rectY) and [binX](https://observablehq.com/plot/transforms/bin#binX); chosen because the _body_mass_g_ column is quantitative); and
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.auto(penguins, {x: "island"}).plot()
```
-makes a bar chart (equivalent to [barY](./README.md#plotbarydata-options) and [groupX](./README.md#plotgroupxoutputs-options); chosen because the _island_ column is categorical). The auto mark is intended to support fast exploratory analysis where the goal is to get a useful plot as quickly as possible. It’s also great if you’re new to Plot, since you can get started with a minimal API.
+makes a bar chart (equivalent to [barY](https://observablehq.com/plot/marks/bar#barY) and [groupX](https://observablehq.com/plot/transforms/group#groupX); chosen because the _island_ column is categorical). The auto mark is intended to support fast exploratory analysis where the goal is to get a useful plot as quickly as possible. It’s also great if you’re new to Plot, since you can get started with a minimal API.
-Plot’s new [axis](./README.md#axis) and [grid](./README.md#axis) marks allow customization and styling of axes. This has been one of our most asked-for features, closing more than a dozen feature requests (see [#1197](https://github.com/observablehq/plot/pull/1197))! The new axis mark composes a [vector](./README.md#vector) for tick marks and a [text](./README.md#text) for tick and axis labels. As such, you can use the rich capabilities of these marks, such the **lineWidth** option to wrap long text labels.
+Plot’s new [axis](https://observablehq.com/plot/marks/axis) and [grid](https://observablehq.com/plot/marks/grid) marks allow customization and styling of axes. This has been one of our most asked-for features, closing more than a dozen feature requests (see [#1197](https://github.com/observablehq/plot/pull/1197))! The new axis mark composes a [vector](https://observablehq.com/plot/marks/vector) for tick marks and a [text](https://observablehq.com/plot/marks/text) for tick and axis labels. As such, you can use the rich capabilities of these marks, such the **lineWidth** option to wrap long text labels.
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.plot({
@@ -360,7 +499,7 @@ Plot.plot({
And since axes and grids are now proper marks, you can interleave them with other marks, for example to produce ggplot2-style axes with a gray background and white grid lines.
-[ ](https://observablehq.com/@observablehq/plot-auto)
+[ ](https://observablehq.com/plot/marks/auto)
```js
Plot.plot({
@@ -376,7 +515,7 @@ Plot.plot({
The *x* and *y* axes are now automatically repeated in empty facets, improving readability by reducing eye travel to read tick values. Below, note that the *x* axis for culmen depth (with ticks at 15 and 20 mm) is rendered below the Adelie/null-sex facet in the top-right.
-[ ](https://observablehq.com/@observablehq/plot-axes)
+[ ](ttps://observablehq.com/plot/marks/axis)
```js
Plot.plot({
@@ -391,21 +530,21 @@ Plot.plot({
})
```
-See [Plot: Axes](https://observablehq.com/@observablehq/plot-axes) for more examples, including the new _both_ **axis** option to repeat axes on both sides of the plot, dashed grid lines via the **strokeDasharray** option, data-driven tick placement, and layering axes to show hierarchical time intervals (years, months, weeks).
+See [Plot: Axes](https://observablehq.com/plot/marks/axis) for more examples, including the new _both_ **axis** option to repeat axes on both sides of the plot, dashed grid lines via the **strokeDasharray** option, data-driven tick placement, and layering axes to show hierarchical time intervals (years, months, weeks).
Marks can now declare default margins via the **marginTop**, **marginRight**, **marginBottom**, and **marginLeft** options, and the **margin** shorthand. For each side, the maximum corresponding margin across marks becomes the plot’s default. While most marks default to zero margins (because they are drawn inside the chart area), Plot‘s axis mark provides default margins depending on their anchor. The facet margin options (*e.g.*, facet.**marginRight**) now correctly affect the positioning of the *x* and *y* axis labels.
-The new [*mark*.**facetAnchor**](#facetanchor) mark option controls the facets in which the mark will appear when faceting. It defaults to null for all marks except for axis marks, where it defaults to *top-empty* if the axis anchor is *top*, *right-empty* if anchor is *right*, *bottom-empty* if anchor is *bottom*, and *left-empty* if anchor is *left*. This ensures the proper positioning of the axes with respect to empty facets.
+The new [*mark*.**facetAnchor**](https://observablehq.com/plot/features/facets#facetAnchor) mark option controls the facets in which the mark will appear when faceting. It defaults to null for all marks except for axis marks, where it defaults to *top-empty* if the axis anchor is *top*, *right-empty* if anchor is *right*, *bottom-empty* if anchor is *bottom*, and *left-empty* if anchor is *left*. This ensures the proper positioning of the axes with respect to empty facets.
-The [frame mark](./README.md#frame)’s new **anchor** option allows you to draw a line on one side of the frame (as opposed to the default behavior where a rect is drawn around all four sides); this feature is now used by the *scale*.**line** option for *x* and *y* scales. The [text mark](./README.md#text) now supports soft hyphens (`\xad`); lines are now eligible to break at soft hyphens, in which case a hyphen (-) will appear at the end of the line before the break. The [raster mark](./README.md#raster) no longer crashes with an _identity_ color scale. The [voronoi mark](./README.md#plotvoronoidata-options) now correctly respects the **target**, **mixBlendMode**, and **opacity** options.
+The [frame mark](https://observablehq.com/plot/marks/frame)’s new **anchor** option allows you to draw a line on one side of the frame (as opposed to the default behavior where a rect is drawn around all four sides); this feature is now used by the *scale*.**line** option for *x* and *y* scales. The [text mark](https://observablehq.com/plot/marks/text) now supports soft hyphens (`\xad`); lines are now eligible to break at soft hyphens, in which case a hyphen (-) will appear at the end of the line before the break. The [raster mark](https://observablehq.com/plot/marks/raster) no longer crashes with an _identity_ color scale. The [voronoi mark](https://observablehq.com/plot/marks/delaunay#voronoi) now correctly respects the **target**, **mixBlendMode**, and **opacity** options.
## 0.6.2
[Released January 18, 2023.](https://github.com/observablehq/plot/releases/tag/v0.6.2)
-The new [raster mark](./README.md#raster) and [contour mark](./README.md#contour) generate a raster image and smooth contours, respectively, from spatial samples. For example, the plot below shows a gridded digital elevation model of Maungawhau (R’s [`volcano` dataset](./test/data/volcano.json)) with contours every 10 meters:
+The new [raster mark](https://observablehq.com/plot/marks/raster) and [contour mark](https://observablehq.com/plot/marks/contour) generate a raster image and smooth contours, respectively, from spatial samples. For example, the plot below shows a gridded digital elevation model of Maungawhau (R’s [`volcano` dataset](./test/data/volcano.json)) with contours every 10 meters:
-[ ](https://observablehq.com/@observablehq/plot-raster)
+[ ](https://observablehq.com/plot/marks/raster)
```js
Plot.plot({
@@ -417,9 +556,9 @@ Plot.plot({
})
```
-For non-gridded or sparse data, the raster and contour marks implement a variety of [spatial interpolation methods](./README.md#spatial-interpolation) to populate the raster grid. The *barycentric* interpolation method, shown below with data from the [Great Britain aeromagnetic survey](https://www.bgs.ac.uk/datasets/gb-aeromagnetic-survey/), uses barycentric coordinates from a Delaunay triangulation of the samples (small black dots).
+For non-gridded or sparse data, the raster and contour marks implement a variety of [spatial interpolation methods](https://observablehq.com/plot/marks/raster#spatial-interpolators) to populate the raster grid. The *barycentric* interpolation method, shown below with data from the [Great Britain aeromagnetic survey](https://www.bgs.ac.uk/datasets/gb-aeromagnetic-survey/), uses barycentric coordinates from a Delaunay triangulation of the samples (small black dots).
-[ ](https://observablehq.com/@observablehq/plot-raster)
+[ ](https://observablehq.com/plot/marks/raster)
```js
Plot.plot({
@@ -438,7 +577,7 @@ Plot.plot({
The same data, with a smidge of blur, as filled contours in projected coordinates:
-[ ](https://observablehq.com/@observablehq/plot-contour)
+[ ](https://observablehq.com/plot/marks/contour)
```js
Plot.plot({
@@ -450,9 +589,9 @@ Plot.plot({
})
```
-Naturally, the raster and contour mark are compatible with Plot’s [projection system](./README.md#projection-options), allowing spatial samples to be shown in any geographic projection and in conjunction with other geographic data. The *equirectangular* projection is the natural choice for this gridded global water vapor dataset from [NASA Earth Observations](https://neo.gsfc.nasa.gov/view.php?datasetId=MYDAL2_M_SKY_WV&date=2022-11-01).
+Naturally, the raster and contour mark are compatible with Plot’s [projection system](https://observablehq.com/plot/features/projections), allowing spatial samples to be shown in any geographic projection and in conjunction with other geographic data. The *equirectangular* projection is the natural choice for this gridded global water vapor dataset from [NASA Earth Observations](https://neo.gsfc.nasa.gov/view.php?datasetId=MYDAL2_M_SKY_WV&date=2022-11-01).
-[ ](https://observablehq.com/@observablehq/plot-raster)
+[ ](https://observablehq.com/plot/marks/raster)
```js
Plot.plot({
@@ -480,7 +619,7 @@ Plot.plot({
The raster and contour mark also support sampling continuous spatial functions *f*(*x*, *y*). For example, here is the famous Mandelbrot set, with color encoding the number of iterations before the point escapes:
-[ ](https://observablehq.com/@observablehq/plot-raster)
+[ ](https://observablehq.com/plot/marks/raster)
```js
Plot.plot({
@@ -503,7 +642,7 @@ Plot.plot({
})
```
-The [vector mark](./README.md#vector) now supports the **shape** constant option; the built-in shapes are *arrow* (default) and *spike*. A custom shape can also be implemented, returning the corresponding SVG path data for the desired shape. The new [spike convenience constructor](./README.md#plotspikedata-options) creates a vector suitable for spike maps. The vector mark also now supports an **r** constant option to set the shape radius.
+The [vector mark](https://observablehq.com/plot/marks/vector) now supports the **shape** constant option; the built-in shapes are *arrow* (default) and *spike*. A custom shape can also be implemented, returning the corresponding SVG path data for the desired shape. The new [spike convenience constructor](https://observablehq.com/plot/marks/vector#spike) creates a vector suitable for spike maps. The vector mark also now supports an **r** constant option to set the shape radius.
[ ](https://observablehq.com/@observablehq/plot-spike)
@@ -523,9 +662,9 @@ Plot.plot({
});
```
-The new [geoCentroid transform](./README.md#plotgeocentroidoptions) and [centroid initializer](./README.md#plotcentroidoptions) compute the spherical and projected planar centroids of geometry, respectively. The new [identity](./README.md#plotidentity) channel helper returns a source array as-is, avoiding an extra copy.
+The new [geoCentroid transform](https://observablehq.com/plot/transforms/centroid#geoCentroid) and [centroid initializer](https://observablehq.com/plot/transforms/centroid#centroid) compute the spherical and projected planar centroids of geometry, respectively. The new [identity](https://observablehq.com/plot/features/transforms#identity) channel helper returns a source array as-is, avoiding an extra copy.
-The **interval** option now supports named time intervals such as “sunday” and “hour”, equivalent to the corresponding d3-time interval (_e.g._, d3.utcSunday and d3.utcHour). The [bin transform](./README.md#bin) is now many times faster, especially when there are many bins and when binning temporal data.
+The **interval** option now supports named time intervals such as “sunday” and “hour”, equivalent to the corresponding d3-time interval (_e.g._, d3.utcSunday and d3.utcHour). The [bin transform](https://observablehq.com/plot/transforms/bin) is now many times faster, especially when there are many bins and when binning temporal data.
Diverging scales now correctly handle descending domains. When the stack **order** option is used without a *z* channel, a helpful error message is now thrown. The **clip** option *frame* now correctly handles band scales. Using D3 7.8, generated SVG path data is now rounded to three decimal points to reduce output size. Fix a crash when a facet scale’s domain includes a value for which there is no corresponding facet data. The bin, group, and hexbin transforms now correctly ignore undefined outputs. Upgrade D3 to 7.8.2.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2724123003..ce9e603351 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Observable Plot - Contributing
-Observable Plot is open source and released under the [ISC license](./LICENSE). You are welcome to [send us pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) to contribute bug fixes or new features. We also invite you to participate in [issues](https://github.com/observablehq/plot/issues) and [discussions](https://github.com/observablehq/plot/discussions). We use issues to track and diagnose bugs, as well as to debate and design enhancements to Plot. Discussions are intended for you to ask for help using Plot, or to share something cool you’ve built with Plot. You can also ask for help on the [Observable Forum](https://talk.observablehq.com) and the [Observable community Slack](https://observable-community.slack.com/ssb/redirect).
+Observable Plot is open source and released under the [ISC license](./LICENSE). You are welcome to [send us pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) to contribute bug fixes or new features. We also invite you to participate in [issues](https://github.com/observablehq/plot/issues) and [discussions](https://github.com/observablehq/plot/discussions). We use issues to track and diagnose bugs, as well as to debate and design enhancements to Plot. Discussions are intended for you to ask for help using Plot, or to share something cool you’ve built with Plot. You can also ask for help on the [Observable Forum](https://talk.observablehq.com) and the [Observable community Slack](https://join.slack.com/t/observable-community/shared_invite/zt-1x7gs4fck-UHhEFxUXKHVE8Qt3XmJCig).
We request that you abide by our [code of conduct](https://observablehq.com/@observablehq/code-of-conduct) when contributing and participating in discussions.
@@ -81,7 +81,7 @@ export async function lineUnemployment() {
}
```
-When a snapshot test is run, its output is compared against the SVG or HTML snapshot saved in the `test/output` folder. This makes it easier to review the effect of code changes and to catch unintended changes. Snapshot tests must have deterministic, reproducible behavior; they should not depend on live data, external servers, the current time, the weather, etc. To use randomness in a test, use a seeded random number generator such as [d3.randomLcg](https://github.com/d3/d3-random/blob/master/README.md#randomLcg).
+When a snapshot test is run, its output is compared against the SVG or HTML snapshot saved in the `test/output` folder. This makes it easier to review the effect of code changes and to catch unintended changes. Snapshot tests must have deterministic, reproducible behavior; they should not depend on live data, external servers, the current time, the weather, etc. To use randomness in a test, use a seeded random number generator such as [d3.randomLcg](https://d3js.org/d3-random#randomLcg).
To add a new snapshot test, create a new JavaScript file in the `test/plots` folder using the pattern shown above. Then export your snapshot test function from [`test/plots/index.ts`](./test/plots/index.ts). For example:
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index cea3e017cc..fa9a56dfa6 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,4 +1,5 @@
-import path from "path";
+import {fileURLToPath, URL} from "node:url";
+import path from "node:path";
import {defineConfig} from "vitepress";
import plot from "./markdown-it-plot.js";
@@ -11,9 +12,10 @@ export default defineConfig({
cleanUrls: true,
vite: {
resolve: {
- alias: {
- "@observablehq/plot": path.resolve("./src/index.js")
- }
+ alias: [
+ {find: "@observablehq/plot", replacement: path.resolve("./src/index.js")},
+ {find: /^.*\/VPFooter\.vue$/, replacement: fileURLToPath(new URL("./theme/CustomFooter.vue", import.meta.url))}
+ ]
}
},
markdown: {
@@ -27,6 +29,9 @@ export default defineConfig({
["script", {async: "", src: "https://www.googletagmanager.com/gtag/js?id=G-9B88TP6PKQ"}],
["script", {}, "window.dataLayer=window.dataLayer||[];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js',new Date());\ngtag('config','G-9B88TP6PKQ');"]
],
+ sitemap: {
+ hostname: 'https://observablehq.com/plot'
+ },
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
// Theme related configurations.
@@ -77,6 +82,7 @@ export default defineConfig({
{text: "Auto", link: "/marks/auto"},
{text: "Axis", link: "/marks/axis"},
{text: "Bar", link: "/marks/bar"},
+ {text: "Bollinger", link: "/marks/bollinger"},
{text: "Box", link: "/marks/box"},
{text: "Cell", link: "/marks/cell"},
{text: "Contour", link: "/marks/contour"},
@@ -89,7 +95,7 @@ export default defineConfig({
{text: "Hexgrid", link: "/marks/hexgrid"},
{text: "Image", link: "/marks/image"},
{text: "Line", link: "/marks/line"},
- {text: "Linear Regression", link: "/marks/linear-regression"},
+ {text: "Linear regression", link: "/marks/linear-regression"},
{text: "Link", link: "/marks/link"},
{text: "Raster", link: "/marks/raster"},
{text: "Rect", link: "/marks/rect"},
@@ -128,7 +134,8 @@ export default defineConfig({
{text: "Crosshair", link: "/interactions/crosshair"},
{text: "Pointer", link: "/interactions/pointer"}
]
- }
+ },
+ {text: "API index", link: "/api"}
],
search: {
provider: "local"
@@ -137,7 +144,7 @@ export default defineConfig({
{icon: "github", link: "https://github.com/observablehq/plot"},
{icon: "twitter", link: "https://twitter.com/observablehq"},
{icon: "mastodon", link: "https://vis.social/@observablehq"},
- {icon: "slack", link: "https://observable-community.slack.com/ssb/redirect"},
+ {icon: "slack", link: "https://join.slack.com/t/observable-community/shared_invite/zt-1x7gs4fck-UHhEFxUXKHVE8Qt3XmJCig"},
{icon: "linkedin", link: "https://www.linkedin.com/company/observable"},
{icon: "youtube", link: "https://www.youtube.com/c/Observablehq"}
],
diff --git a/docs/.vitepress/markdown-it-plot.ts b/docs/.vitepress/markdown-it-plot.ts
index f338f0ba3b..eb6253f421 100644
--- a/docs/.vitepress/markdown-it-plot.ts
+++ b/docs/.vitepress/markdown-it-plot.ts
@@ -26,7 +26,7 @@ export default function plot(md) {
directives.includes("hidden")
? `
\n`
: href
- ? `
Fork `
+ ? `
Fork `
: ""
}`;
if (/^Plot\.plot\(/.test(content)) {
diff --git a/docs/.vitepress/theme/CustomFooter.vue b/docs/.vitepress/theme/CustomFooter.vue
new file mode 100644
index 0000000000..868e4ad684
--- /dev/null
+++ b/docs/.vitepress/theme/CustomFooter.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/docs/.vitepress/theme/CustomLayout.vue b/docs/.vitepress/theme/CustomLayout.vue
new file mode 100644
index 0000000000..30c1a7ec13
--- /dev/null
+++ b/docs/.vitepress/theme/CustomLayout.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/ObservablePromo.vue b/docs/.vitepress/theme/ObservablePromo.vue
new file mode 100644
index 0000000000..0e199464e3
--- /dev/null
+++ b/docs/.vitepress/theme/ObservablePromo.vue
@@ -0,0 +1,131 @@
+
+
+
Build your best work with Plot on Observable
+
The only data workflow platform capable of supporting the full power of Plot
+
+
+ Connect to your data instantly
+ Pull live data from the cloud, files, and databases into one secure place — without installing anything, ever.
+
+
+ Code faster than you thought possible
+ Get everything you need and none of what you don’t with lightweight automatic versioning, instant sharing, and real-time multiplayer editing.
+
+
+ Accelerate your team’s analysis
+ Create a home for your team’s data analysis where you can spin up charts, maps, and data apps to explore, analyze, and iterate on together.
+
+
+
Build with Plot on Observable →
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/VersionBadge.vue b/docs/.vitepress/theme/VersionBadge.vue
new file mode 100644
index 0000000000..f58d251a77
--- /dev/null
+++ b/docs/.vitepress/theme/VersionBadge.vue
@@ -0,0 +1,12 @@
+
+
+
+
+ {{ version ? `^${version}` : "prerelease" }}
+
+
+
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index 573e7817ce..6c4e2c5c57 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -31,6 +31,27 @@
z-index: 1;
}
+.vp-doc .plot-figure {
+ margin: 16px 0 0;
+}
+
+.vp-doc .plot-figure h2,
+.vp-doc .plot-figure h3 {
+ all: unset;
+ display: block;
+}
+
+.vp-doc .plot-figure h2 {
+ line-height: 28px;
+ font-size: 20px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.vp-doc .plot a:hover {
+ text-decoration: initial;
+}
+
:root [aria-label="tip"][fill="white"],
:root [aria-label="tip"] [fill="white"] {
fill: var(--vp-c-bg-alt);
@@ -58,6 +79,7 @@
.label-input > label {
display: inline-flex;
+ align-items: center;
}
.label-input > input,
@@ -99,3 +121,53 @@ a.plot-fork:hover {
.vp-doc p:hover .header-anchor {
opacity: 1;
}
+
+.bg-alt {
+ background: var(--vp-c-bg-alt);
+}
+
+.lh-normal {
+ line-height: normal;
+}
+
+.flex {
+ display: flex;
+}
+
+.flex-grow-1 {
+ flex-grow: 1;
+}
+
+.ma4 {
+ margin: 2rem;
+}
+
+.ml1 {
+ margin-left: 1rem;
+}
+
+.mb2 {
+ margin-bottom: 0.5rem;
+}
+
+.f12 {
+ font-size: 12px;
+}
+
+.f14 {
+ font-size: 14px;
+}
+
+.fw6 {
+ font-weight: 600;
+}
+
+.fb6 {
+ flex-basis: 10rem;
+}
+
+@media screen and (min-width: 960px) {
+ .flex-l {
+ display: flex;
+ }
+}
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index 3e7f52c081..2586ce3da3 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -2,13 +2,17 @@ import DefaultTheme from "vitepress/theme-without-fonts";
import {useData} from "vitepress";
import {watch} from "vue";
import PlotRender from "../../components/PlotRender.js";
+import CustomLayout from "./CustomLayout.vue";
+import VersionBadge from "./VersionBadge.vue";
import "./custom.css";
export default {
extends: DefaultTheme,
+ Layout: CustomLayout,
enhanceApp({app, router}) {
Object.defineProperty(app.config.globalProperties, "$dark", {get: () => useData().isDark.value});
app.component("PlotRender", PlotRender);
+ app.component("VersionBadge", VersionBadge);
enableAnalytics(router);
}
};
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000000..fc5b914929
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,34 @@
+
+
+# API index
+
+## Methods
+
+
+
+## Options
+
+
+
+
diff --git a/docs/community.md b/docs/community.md
index 7ff2f5ced7..75d20a1aea 100644
--- a/docs/community.md
+++ b/docs/community.md
@@ -4,7 +4,11 @@ Learning Plot? Love data visualization? Don’t go it alone! Join our community
## Staying up-to-date
-Plot is getting better all the time. If you like Plot, please star ⭐️ our [GitHub repo](https://github.com/observablehq/plot) to help get the word out and to see updates on your GitHub dashboard.
+:::tip
+Please star ⭐️ our [GitHub repo](https://github.com/observablehq/plot) to show your support for us on GitHub!
+:::
+
+Plot is getting better all the time; catch up on [recent releases](https://github.com/observablehq/plot/releases) by reading our [CHANGELOG](https://github.com/observablehq/plot/blob/main/CHANGELOG.md).
For email updates, sign up for the [Observable Plot Twist](https://observablehq.com/@observablehq/plot-twist-newsletter-signup) newsletter. (See our [back issues](https://observablehq.com/collection/@observablehq/newsletters/2) and [blog](https://observablehq.com/blog), too.) This monthly newsletter will let you know about new features in Plot, inspiring work by the community, upcoming workshops and community events, and more.
@@ -12,9 +16,9 @@ And of course, follow us on [Observable](https://observablehq.com/@observablehq?
## Getting help
-We recommend asking for help on the [Observable forum](https://talk.observablehq.com/c/help/6). Or if you prefer chat, join the [Observable community Slack](https://observable-community.slack.com/ssb/redirect).
+We recommend asking for help on the [Observable forum](https://talk.observablehq.com/c/help/6). Or if you prefer chat, join the [Observable community Slack](https://join.slack.com/t/observable-community/shared_invite/zt-1x7gs4fck-UHhEFxUXKHVE8Qt3XmJCig).
-We encourage you to share your work, no matter how messy, on [Observable](https://observablehq.com). Sharing live code is the easiest way to let people see what you see, and to debug your problem. Strive for a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)—it helps people hone in on your problem more quickly.
+We encourage you to share your work, no matter how messy, on [Observable](https://observablehq.com). Sharing live code is the easiest way to let people see what you see, and to debug your problem. Strive for a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) — it helps people hone in on your problem more quickly.
When asking for help, don’t just post your code and ask people to fix it. Provide context, and say what you want help with. For example:
@@ -23,7 +27,7 @@ When asking for help, don’t just post your code and ask people to fix it. Prov
- What behavior are you currently seeing?
- Is the current behavior not what you expect?
-If you think you’ve found a bug in Plot, please file a [GitHub issue](https://github.com/observablehq/plot/issues). But don’t use an issue to ask for help—you’ll have better luck on the forum or Slack.
+If you think you’ve found a bug in Plot, please file a [GitHub issue](https://github.com/observablehq/plot/issues). But don’t use an issue to ask for help — you’ll have better luck on the forum or Slack.
## Getting involved
@@ -33,7 +37,7 @@ We’d love for you to join the community! Here are some ways to participate:
* Upvote 👍 or comment on [GitHub issues](https://github.com/observablehq/plot/issues). We’d love your input on what to build next. If your desired feature isn’t already there, or if you’ve found a bug, file an issue and tell us about it.
-* Answer questions or participate in discussions on the [Observable forum](https://talk.observablehq.com/) and the [Observable community Slack](https://observable-community.slack.com/ssb/redirect). You’ll help others, and might learn something yourself, too.
+* Answer questions or participate in discussions on the [Observable forum](https://talk.observablehq.com/) and the [Observable community Slack](https://join.slack.com/t/observable-community/shared_invite/zt-1x7gs4fck-UHhEFxUXKHVE8Qt3XmJCig). You’ll help others, and might learn something yourself, too.
* Open a pull request! Read our [guide to contributing](https://github.com/observablehq/plot/blob/main/CONTRIBUTING.md).
diff --git a/docs/components/PlotRender.js b/docs/components/PlotRender.js
index 07b6acf171..519d1f678f 100644
--- a/docs/components/PlotRender.js
+++ b/docs/components/PlotRender.js
@@ -69,6 +69,11 @@ class Element {
dispatchEvent() {
// ignored; interaction needs real DOM
}
+ append(...children) {
+ for (const child of children) {
+ this.appendChild(child?.ownerDocument ? child : this.ownerDocument.createTextNode(child));
+ }
+ }
appendChild(child) {
this.children.push(child);
child.parentNode = this;
@@ -212,7 +217,10 @@ export default {
}
if (typeof document !== "undefined") {
const plot = Plot[method](options);
- const replace = (el) => el.firstChild.replaceWith(plot);
+ const replace = (el) => {
+ while (el.lastChild) el.lastChild.remove();
+ el.append(plot);
+ };
return withDirectives(h("span", [toHyperScript(plot)]), [[{mounted: replace, updated: replace}]]);
}
return h("span", [Plot[method]({...options, document: new Document()}).toHyperScript()]);
diff --git a/docs/components/links.js b/docs/components/links.js
new file mode 100644
index 0000000000..ed46b5fdfe
--- /dev/null
+++ b/docs/components/links.js
@@ -0,0 +1,48 @@
+import {readdir, readFile, stat} from "fs/promises";
+
+// Anchors can be derived from headers, or explicitly written as {#names}.
+export function getAnchors(text) {
+ text = text.replace(/<(?:Version)?Badge[^/]*\/>/g, ""); // ignore badges
+ const anchors = [];
+ for (const [, header] of text.matchAll(/^#+ ([*\w][*().,\w\d -]+)\n/gm)) {
+ anchors.push(
+ header
+ .replace(/[^\w\d\s]+/g, " ")
+ .trim()
+ .replace(/ +/g, "-")
+ .toLowerCase()
+ );
+ }
+ for (const [, anchor] of text.matchAll(/ \{#([\w\d-]+)\}/g)) {
+ anchors.push(anchor);
+ }
+ return anchors;
+}
+
+// Internal links.
+export function getLinks(file, text) {
+ const links = [];
+ for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
+ const [, link] = match;
+ if (/^\w+:/.test(link)) continue; // absolute link with protocol
+ const {pathname, hash} = new URL(link, new URL(file, "https://example.com/"));
+ links.push({pathname, hash});
+ }
+ return links;
+}
+
+// In source files, ignore comments.
+export async function readMarkdownSource(f) {
+ return (await readFile(f, "utf8")).replaceAll(//gs, "");
+}
+
+// Recursively find all md files in the directory.
+export async function* readMarkdownFiles(root, subpath = "/") {
+ for (const fname of await readdir(root + subpath)) {
+ if (!fname.includes(".") && (await stat(root + subpath + fname)).isDirectory()) {
+ yield* readMarkdownFiles(root, subpath + fname + "/");
+ } else if (fname.endsWith(".md")) {
+ yield subpath + fname;
+ }
+ }
+}
diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts
new file mode 100644
index 0000000000..f159806e72
--- /dev/null
+++ b/docs/data/api.data.ts
@@ -0,0 +1,167 @@
+import {rollup, sort} from "d3";
+import {FunctionDeclaration, Node, Project, VariableStatement} from "ts-morph";
+import {readMarkdownFiles, readMarkdownSource, getAnchors} from "../components/links.js";
+
+// These interfaces tend to represent things that Plot constructs internally,
+// rather than objects that the user is expected to provide.
+function isInternalInterface(name) {
+ return (
+ name === "AutoSpec" ||
+ name === "Channel" ||
+ name === "ChannelDomainOptions" || // TODO
+ name === "ChannelTransform" ||
+ name === "Context" ||
+ name === "Dimensions" ||
+ name === "Plot" ||
+ name === "Scale"
+ );
+}
+
+// This tries to get a brief human-readable, one-sentence summary description of
+// the exported symbol. We might want to formalize this so that we can be more
+// intentional when authoring documentation.
+function getDescription(node: FunctionDeclaration | VariableStatement): string {
+ return node
+ .getJsDocs()[0]
+ ?.getDescription()
+ .replace(/\n/g, " ") // replace newlines with spaces
+ .replace(/[*_]/g, "") // remove bold and italics formatting
+ .replace(/[.:]($|\s+).*/g, "") // truncate at the first period or colon
+ .replace(/\[([^[]+)\]\[\d+\]/g, "$1") // strip links (assuming [1] syntax)
+ .trim();
+}
+
+// While we try to keep the source code file structure as close as possible to
+// the documentation URL structure, there are some inevitable deviations that
+// are codified by this function. When new files are added, please try to keep
+// this function up-to-date, and try to generalize patterns so that we
+// automatically generate correct links. (TODO Verify that the links are valid.)
+function getHref(name: string, path: string): string {
+ path = path.replace(/\.d\.ts$/, ""); // drop trailing .d.ts
+ path = path.replace(/([a-z0-9])([A-Z])/, (_, a, b) => `${a}-${b.toLowerCase()}`); // camel case conversion
+ if (path.split("/").length === 1) path = `features/${path}`; // top-level declarations are features
+ switch (path) {
+ case "features/curve":
+ case "features/format":
+ case "features/mark":
+ case "features/marker":
+ case "features/plot":
+ case "features/projection":
+ return `${path}s`;
+ case "features/inset":
+ return "features/scales";
+ case "features/options":
+ return "features/transforms";
+ case "marks/axis": {
+ switch (name) {
+ case "gridX":
+ case "gridY":
+ case "gridFx":
+ case "gridFy":
+ return "marks/grid";
+ }
+ break;
+ }
+ case "marks/crosshair":
+ return "interactions/crosshair";
+ case "transforms/basic": {
+ switch (name) {
+ case "filter":
+ return "transforms/filter";
+ case "reverse":
+ case "shuffle":
+ case "sort":
+ return "transforms/sort";
+ }
+ return "features/transforms";
+ }
+ }
+ return path;
+}
+
+function getInterfaceName(name: string, path: string): string {
+ name = name.replace(/(Transform|Corner|X|Y|Output)?(Defaults|Options|Styles)$/, "");
+ 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 (path.startsWith("marks/")) name += " mark";
+ else if (path.startsWith("transforms/")) name += " transform";
+ return name;
+}
+
+export default {
+ watch: [],
+ async load() {
+ // Parse the TypeScript declarations to get exported symbols.
+ const project = new Project({tsConfigFilePath: "tsconfig.json"});
+ const allMethods: {name: string; comment: string; href: string}[] = [];
+ const allOptions: {name: string; context: {name: string; href: string}}[] = [];
+ const index = project.getSourceFile("src/index.d.ts")!;
+ for (const [name, declarations] of index.getExportedDeclarations()) {
+ for (const declaration of declarations) {
+ 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}});
+ }
+ } else if (Node.isFunctionDeclaration(declaration)) {
+ const comment = getDescription(declaration);
+ if (comment) {
+ const href = getHref(name, index.getRelativePathTo(declaration.getSourceFile()));
+ allMethods.push({name, comment, href});
+ }
+ } else if (Node.isVariableDeclaration(declaration)) {
+ const comment = getDescription(declaration.getVariableStatement()!);
+ if (comment) {
+ const href = getHref(name, index.getRelativePathTo(declaration.getSourceFile()));
+ allMethods.push({name, comment, href});
+ }
+ }
+ }
+ }
+ // Parse the Markdown files to get all known anchors.
+ const root = "docs";
+ const anchors = new Map();
+ for await (const file of readMarkdownFiles(root)) {
+ const text = await readMarkdownSource(root + file);
+ anchors.set(file, getAnchors(text));
+ }
+ // Cross-reference the generated links.
+ for (const {name, href} of allMethods) {
+ if (!anchors.has(`/${href}.md`)) {
+ throw new Error(`file not found: ${href}`);
+ }
+ if (!anchors.get(`/${href}.md`).includes(name)) {
+ throw new Error(`anchor not found: ${href}#${name}`);
+ }
+ }
+ for (const {context: {href}} of allOptions) {
+ if (!anchors.has(`/${href}.md`)) {
+ throw new Error(`file not found: ${href}`);
+ }
+ }
+ return {
+ methods: sort(allMethods, ({name}) => name),
+ options: sort(
+ rollup(
+ allOptions,
+ (D) =>
+ sort(
+ rollup(
+ D.map((d) => d.context),
+ ([d]) => d,
+ (d) => d.name
+ ).values(),
+ (d) => d.name
+ ),
+ (d) => d.name
+ ),
+ ([name]) => name
+ )
+ };
+ }
+};
diff --git a/docs/data/miserables.data.ts b/docs/data/miserables.data.ts
new file mode 100644
index 0000000000..3862663751
--- /dev/null
+++ b/docs/data/miserables.data.ts
@@ -0,0 +1,8 @@
+import fs from "node:fs";
+
+export default {
+ watch: ["../public/data/miserables.json"],
+ load([file]) {
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
+ }
+};
diff --git a/docs/data/miserables.ts b/docs/data/miserables.ts
new file mode 100644
index 0000000000..aa1ecae79e
--- /dev/null
+++ b/docs/data/miserables.ts
@@ -0,0 +1,3 @@
+import {data} from "./miserables.data";
+
+export default Object.assign(data, {groups: new Map(data.nodes.map((d) => [d.id, d.group]))});
diff --git a/docs/features/curves.md b/docs/features/curves.md
index 28f15f0831..a01780b4c5 100644
--- a/docs/features/curves.md
+++ b/docs/features/curves.md
@@ -11,7 +11,7 @@ const numbers = d3.range(20).map(d3.randomLcg(42));
# Curves
-A **curve** defines how to turn a discrete representation of a line as a sequence of points [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] into a continuous path; *i.e.*, how to interpolate between points. Curves are used by the [line](../marks/line.md), [area](../marks/area.md), and [link](../marks/link.md) marks, and are implemented by [d3-shape](https://github.com/d3/d3-shape/blob/main/README.md#curves).
+A **curve** defines how to turn a discrete representation of a line as a sequence of points [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] into a continuous path; *i.e.*, how to interpolate between points. Curves are used by the [line](../marks/line.md), [area](../marks/area.md), and [link](../marks/link.md) marks, and are implemented by [d3-shape](https://d3js.org/d3-shape/curve).
@@ -79,8 +79,8 @@ The following named curve methods are supported:
* *step* - a piecewise constant function where *y* changes at the midpoint of *x*
* *step-after* - a piecewise constant function where *y* changes after *x*
* *step-before* - a piecewise constant function where *x* changes after *y*
-* *auto* - like *linear*, but use the (possibly spherical) [projection](./projections.md), if any
+* *auto* - like *linear*, but use the (possibly spherical) [projection](./projections.md), if any
-If **curve** is a function, it will be invoked with a given *context* in the same fashion as a [D3 curve factory](https://github.com/d3/d3-shape/blob/main/README.md#custom-curves). The *auto* curve is only available for the [line mark](../marks/line.md) and [link mark](../marks/link.md) and is typically used in conjunction with a spherical [projection](./projections.md) to interpolate along [geodesics](https://en.wikipedia.org/wiki/Geodesic).
+If **curve** is a function, it will be invoked with a given *context* in the same fashion as a [D3 curve factory](https://d3js.org/d3-shape/curve#custom-curves). The *auto* curve is only available for the [line mark](../marks/line.md) and [link mark](../marks/link.md) and is typically used in conjunction with a spherical [projection](./projections.md) to interpolate along [geodesics](https://en.wikipedia.org/wiki/Geodesic).
-The tension option only has an effect on bundle, cardinal and Catmull–Rom splines (*bundle*, *cardinal*, *cardinal-open*, *cardinal-closed*, *catmull-rom*, *catmull-rom-open*, and *catmull-rom-closed*). For bundle splines, it corresponds to [beta](https://github.com/d3/d3-shape/blob/main/README.md#curveBundle_beta); for cardinal splines, [tension](https://github.com/d3/d3-shape/blob/main/README.md#curveCardinal_tension); for Catmull–Rom splines, [alpha](https://github.com/d3/d3-shape/blob/main/README.md#curveCatmullRom_alpha).
+The tension option only has an effect on bundle, cardinal and Catmull–Rom splines (*bundle*, *cardinal*, *cardinal-open*, *cardinal-closed*, *catmull-rom*, *catmull-rom-open*, and *catmull-rom-closed*). For bundle splines, it corresponds to [beta](https://d3js.org/d3-shape/curve#curveBundle_beta); for cardinal splines, [tension](https://d3js.org/d3-shape/curve#curveCardinal_tension); for Catmull–Rom splines, [alpha](https://d3js.org/d3-shape/curve#curveCatmullRom_alpha).
diff --git a/docs/features/facets.md b/docs/features/facets.md
index a4bf230f69..3d75d61784 100644
--- a/docs/features/facets.md
+++ b/docs/features/facets.md
@@ -232,7 +232,7 @@ Plot.plot({
## Mark facet options
-Facets can be defined for each mark via the **fx** or **fy** channels. The **fx** and **fy** channels are computed prior to the [mark’s transform](./transforms.md), if any (*i.e.*, facet channels are not transformed). Alternatively, the [**facet** plot option](#plot-facet-options) allows top-level faceting based on data.
+Facets can be defined for each mark via the **fx** or **fy** channels. The **fx** and **fy** channels are computed prior to the [mark’s transform](./transforms.md), if any (*i.e.*, facet channels are not transformed). Alternatively, the [**facet** plot option](#plot-facet-options) allows top-level faceting based on data.
Faceting can be explicitly enabled or disabled on a mark with the **facet** option, which accepts the following values:
@@ -244,8 +244,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/formats.md b/docs/features/formats.md
index 2f687125ac..8455dd5902 100644
--- a/docs/features/formats.md
+++ b/docs/features/formats.md
@@ -7,9 +7,9 @@ import * as d3 from "d3";
# Formats
-These helper functions are provided for convenience as a **tickFormat** option for the [axis mark](../marks/axis.md), as the **text** option for a [text mark](../marks/text.md), or other use. See also [d3-format](https://github.com/d3/d3-format), [d3-time-format](https://github.com/d3/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting).
+These helper functions are provided for convenience as a **tickFormat** option for the [axis mark](../marks/axis.md), as the **text** option for a [text mark](../marks/text.md), or other use. See also [d3-format](https://d3js.org/d3-format), [d3-time-format](https://d3js.org/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting).
-## formatIsoDate(*date*)
+## formatIsoDate(*date*) {#formatIsoDate}
```js
Plot.formatIsoDate(new Date("2020-01-01T00:00:00.000Z")) // "2020-01-01"
@@ -17,7 +17,7 @@ Plot.formatIsoDate(new Date("2020-01-01T00:00:00.000Z")) // "2020-01-01"
Given a *date*, returns the shortest equivalent ISO 8601 UTC string. If the given *date* is not valid, returns `"Invalid Date"`. See [isoformat](https://github.com/mbostock/isoformat).
-## formatWeekday(*locale*, *format*)
+## formatWeekday(*locale*, *format*) {#formatWeekday}
:::plot https://observablehq.com/@observablehq/plot-format-helpers
```js
@@ -31,7 +31,7 @@ Plot.formatWeekday("es-MX", "long")(0) // "domingo"
Returns a function that formats a given week day number (from 0 = Sunday to 6 = Saturday) according to the specified *locale* and *format*. The *locale* is a [BCP 47 language tag](https://tools.ietf.org/html/bcp47) and defaults to U.S. English. The *format* is a [weekday format](https://tc39.es/ecma402/#datetimeformat-objects): either *narrow*, *short*, or *long*; if not specified, it defaults to *short*.
-## formatMonth(*locale*, *format*)
+## formatMonth(*locale*, *format*) {#formatMonth}
:::plot https://observablehq.com/@observablehq/plot-format-helpers
```js
diff --git a/docs/features/legends.md b/docs/features/legends.md
index 179ba7ddee..dd0cfb2762 100644
--- a/docs/features/legends.md
+++ b/docs/features/legends.md
@@ -24,7 +24,7 @@ onMounted(() => {
-# Legends
+# Legends
Plot can generate **legends** for *color*, *opacity*, and *symbol* [scales](./scales.md). For example, the scatterplot below of body measurements of Olympic athletes includes a legend for its *color* scale, allowing the meaning of color to be interpreted by the reader. (The axes similarly document the meaning of the *x* and *y* position scales.)
@@ -89,7 +89,7 @@ Categorical and ordinal color legends are rendered as swatches, unless the **leg
* **columns** - the number of swatches per row
* **marginLeft** - the legend’s left margin
* **className** - a class name, that defaults to a randomly generated string scoping the styles
-* **opacity** - the swatch fill opacity
+* **opacity** - the swatch fill opacity
* **width** - the legend’s width (in pixels)
Symbol legends are rendered as swatches and support the options above in addition to the following options:
@@ -120,7 +120,7 @@ Continuous color legends are rendered as a ramp, and can be configured with the
The **style** legend option allows custom styles to override Plot’s defaults; it has the same behavior as in Plot’s top-level [plot options](./plots.md). The **className** option is suffixed with *-ramp* or *-swatches*, reflecting the **legend** type.
-## legend(*options*)
+## legend(*options*) {#legend}
Renders a standalone legend for the scale defined by the given *options* object, returning a SVG or HTML figure element. This element can then be inserted into the page as described in the [getting started guide](../getting-started.md). The *options* object must define at least one scale; see [scale options](./scales.md) for how to define a scale.
diff --git a/docs/features/marks.md b/docs/features/marks.md
index 55042c4edc..927fcbd3f8 100644
--- a/docs/features/marks.md
+++ b/docs/features/marks.md
@@ -48,7 +48,7 @@ onMounted(() => {
-# Marks
+# Marks {#Marks}
:::tip
If you aren’t yet up and running with Plot, please read our [getting started guide](../getting-started.md) first. Tinkering with the code below will give a better sense of how Plot works.
@@ -58,7 +58,7 @@ Plot doesn’t have chart types; instead, you construct charts by layering **mar
## Marks are geometric shapes
-Plot provides a variety of mark types. Think of marks as the “visual vocabulary”—the painter’s palette 🎨, but of shapes instead of colors—that you pull from when composing a chart. Each mark type produces a certain type of geometric shape.
+Plot provides a variety of mark types. Think of marks as the “visual vocabulary” — the painter’s palette 🎨, but of shapes instead of colors — that you pull from when composing a chart. Each mark type produces a certain type of geometric shape.
For example, the [dot mark](../marks/dot.md) draws stroked circles (by default).
@@ -157,7 +157,7 @@ Plot.plot({
## Marks use scales
-Marks are (typically) not positioned in literal pixels, or colored in literal colors, as in a conventional graphics system. Instead you provide abstract values such as time and temperature—marks are drawn “in data space”—and [scales](./scales.md) encode these into visual values such as position and color. And best of all, Plot automatically creates [axes](../marks/axis.md) and [legends](./legends.md) to document the scales’ encodings.
+Marks are (typically) not positioned in literal pixels, or colored in literal colors, as in a conventional graphics system. Instead you provide abstract values such as time and temperature — marks are drawn “in data space” — and [scales](./scales.md) encode these into visual values such as position and color. And best of all, Plot automatically creates [axes](../marks/axis.md) and [legends](./legends.md) to document the scales’ encodings.
Data is passed through scales automatically during rendering; the mark controls which scales are used. The **x** and **y** options are typically bound to the *x* and *y* scales, respectively, while the **fill** and **stroke** options are typically bound to the *color* scale. Changing a scale’s definition, say by overriding its **domain** (the extent of abstract input values) or **type**, affects the appearance of all marks that use the scale.
@@ -178,7 +178,7 @@ Plot.plot({
## Marks have tidy data
-A single mark can draw multiple shapes. A mark generally produces a shape—such as a rectangle or circle—for each element in the data.
+A single mark can draw multiple shapes. A mark generally produces a shape — such as a rectangle or circle — for each element in the data.
:::plot defer https://observablehq.com/@observablehq/plot-tidy-data
```js
@@ -194,7 +194,7 @@ Plot.lineY(aapl, {x: "Date", y: "Close"}).plot()
```
:::
-And a line mark isn’t even guaranteed to produce a single polyline—there can be multiple polylines, as in a line chart with multiple series (using **z**).
+And a line mark isn’t even guaranteed to produce a single polyline — there can be multiple polylines, as in a line chart with multiple series (using **z**).
:::plot defer https://observablehq.com/@observablehq/plot-multiple-series-line-chart
```js
@@ -258,10 +258,10 @@ Note that when accessor functions or parallel arrays are used instead of field n
Data comes in different types: quantitative (or temporal) values can be subtracted, ordinal values can be ordered, and nominal (or categorical) values can only be the same or different.
:::info
-Because nominal values often need some arbitrary order for display purposes—often alphabetical—Plot uses the term *ordinal* to refer to both ordinal and nominal data.
+Because nominal values often need some arbitrary order for display purposes — often alphabetical — Plot uses the term *ordinal* to refer to both ordinal and nominal data.
:::
-Some marks work with any type of data, while other marks have certain requirements or assumptions of data. For example, a line should only be used when both *x* and *y* are quantitative or temporal, and when the data is in a meaningful order (such as chronological). This is because the line mark will interpolate between adjacent points to draw line segments. If *x* or *y* is nominal—say the names of countries—it doesn’t make sense to use a line because there is no half-way point between two nominal values.
+Some marks work with any type of data, while other marks have certain requirements or assumptions of data. For example, a line should only be used when both *x* and *y* are quantitative or temporal, and when the data is in a meaningful order (such as chronological). This is because the line mark will interpolate between adjacent points to draw line segments. If *x* or *y* is nominal — say the names of countries — it doesn’t make sense to use a line because there is no half-way point between two nominal values.
:::plot https://observablehq.com/@observablehq/plot-dont-do-this
```js
@@ -357,7 +357,7 @@ Channels are mark options that can be used to encode data. These options allow t
* an accessor function, or
* an array of values of the same length and order as the data.
-Not all mark options can be expressed as channels. For example, **stroke** can be a channel but **strokeDasharray** cannot. This is mostly a pragmatic limitation—it would be harder to implement Plot if every option were expressible as a channel—but it also serves to guide you towards options that are intended for encoding data.
+Not all mark options can be expressed as channels. For example, **stroke** can be a channel but **strokeDasharray** cannot. This is mostly a pragmatic limitation — it would be harder to implement Plot if every option were expressible as a channel — but it also serves to guide you towards options that are intended for encoding data.
:::tip
To vary the definition of a constant option with data, create multiple marks with your different constant options, and then filter the data for each mark to achieve the desired result.
@@ -433,7 +433,7 @@ sales = [
Plot.dot(sales, {x: "units", y: "fruit"})
```
-While a column name such as `"units"` is the most concise way of specifying channel values, values can also be specified as functions for greater flexibility, say to transform data or derive a new column on the fly. Channel functions are invoked for each datum (*d*) in the data and return the corresponding channel value. (This is similar to how D3’s [*selection*.attr](https://github.com/d3/d3-selection/blob/main/README.md#selection_attr) accepts functions, though note that Plot channel functions should return abstract values, not visual values.)
+While a column name such as `"units"` is the most concise way of specifying channel values, values can also be specified as functions for greater flexibility, say to transform data or derive a new column on the fly. Channel functions are invoked for each datum (*d*) in the data and return the corresponding channel value. (This is similar to how D3’s [*selection*.attr](https://d3js.org/d3-selection/modifying#selection_attr) accepts functions, though note that Plot channel functions should return abstract values, not visual values.)
```js
Plot.dot(sales, {x: (d) => d.units * 1000, y: (d) => d.fruit})
@@ -476,7 +476,7 @@ All marks support the following style options:
* **strokeDashoffset** - the [stroke dash offset](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset) (typically in pixels)
* **opacity** - object opacity (a number between 0 and 1)
* **mixBlendMode** - the [blend mode](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) (*e.g.*, *multiply*)
-* **imageFilter** - a CSS [filter](https://developer.mozilla.org/en-US/docs/Web/CSS/filter) (*e.g.*, *blur(5px)*)
+* **imageFilter** - a CSS [filter](https://developer.mozilla.org/en-US/docs/Web/CSS/filter) (*e.g.*, *blur(5px)*)
* **shapeRendering** - the [shape-rendering mode](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) (*e.g.*, *crispEdges*)
* **paintOrder** - the [paint order](https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order) (*e.g.*, *stroke*)
* **dx** - horizontal offset (in pixels; defaults to 0)
@@ -486,11 +486,11 @@ All marks support the following style options:
* **ariaHidden** - if true, hide this content from the accessibility tree
* **pointerEvents** - the [pointer events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) (*e.g.*, *none*)
* **clip** - whether and how to clip the mark
-* **tip** - whether to generate an implicit [pointer](../interactions/pointer.md) [tip](../marks/tip.md)
+* **tip** - whether to generate an implicit [pointer](../interactions/pointer.md) [tip](../marks/tip.md)
If the **clip** option is *frame* (or equivalently true), the mark is clipped to the frame’s dimensions; if the **clip** option is null (or equivalently false), the mark is not clipped. If the **clip** option is *sphere*, then a [geographic projection](./projections.md) is required and the mark will be clipped to the projected sphere (_e.g._, the front hemisphere when using the orthographic projection).
-If the **tip** option is true, a [tip mark](../marks/tip.md) with the [pointer transform](../interactions/pointer.md) will be derived from this mark and placed atop all other marks, offering details on demand. If the **tip** option is set to *x*, *y*, or *xy*, [pointerX](../interactions/pointer.md#pointerx-options), [pointerY](../interactions/pointer.md#pointery-options), or [pointer](../interactions/pointer.md#pointer-options) will be used, respectively; otherwise the pointing mode will be chosen automatically. (If the **tip** mark option is truthy, the **title** channel is no longer applied using an SVG title element as this would conflict with the tip mark.)
+If the **tip** option is true, a [tip mark](../marks/tip.md) with the [pointer transform](../interactions/pointer.md) will be derived from this mark and placed atop all other marks, offering details on demand. If the **tip** option is set to *x*, *y*, or *xy*, [pointerX](../interactions/pointer.md#pointerX), [pointerY](../interactions/pointer.md#pointerY), or [pointer](../interactions/pointer.md#pointer) will be used, respectively; otherwise the pointing mode will be chosen automatically. (If the **tip** mark option is truthy, the **title** channel is no longer applied using an SVG title element as this would conflict with the tip mark.)
For all marks except [text](../marks/text.md), the **dx** and **dy** options are rendered as a transform property, possibly including a 0.5px offset on low-density screens.
@@ -547,13 +547,13 @@ All marks support the following [transform](./transforms.md) options:
* **filter** - apply the [filter transform](../transforms/filter.md)
* **sort** - apply the [sort transform](../transforms/sort.md)
-* **reverse** - apply the [reverse transform](../transforms/sort.md#reverse-options)
+* **reverse** - apply the [reverse transform](../transforms/sort.md#reverse)
* **transform** - apply a [custom transform](./transforms.md#custom-transforms)
* **initializer** - apply a [custom initializer](./transforms.md#custom-initializers)
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).
-## marks(...*marks*)
+## marks(...*marks*) {#marks}
```js
Plot.marks(
diff --git a/docs/features/plots.md b/docs/features/plots.md
index 0096717885..13351e4241 100644
--- a/docs/features/plots.md
+++ b/docs/features/plots.md
@@ -26,7 +26,7 @@ onMounted(() => {
# Plots
-To render a **plot** in Observable Plot, call [plot](#plot-options) (typically as `Plot.plot`), passing in the desired *options*. This function returns an SVG or HTML figure element.
+To render a **plot** in Observable Plot, call [plot](#plot) (typically as `Plot.plot`), passing in the desired *options*. This function returns an SVG or HTML figure element.
:::plot https://observablehq.com/@observablehq/plot-hello-world
```js
@@ -70,7 +70,7 @@ aapl = [
```
:::tip
-Rather than baking data into JavaScript, use [JSON](https://en.wikipedia.org/wiki/JSON) or [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files to store data. You can use [d3.json](https://github.com/d3/d3-fetch/blob/main/README.md#json), [d3.csv](https://github.com/d3/d3-fetch/blob/main/README.md#csv), or [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to load a file. On Observable, you can also use a [file attachment](https://observablehq.com/@observablehq/file-attachments) or [SQL cell](https://observablehq.com/@observablehq/sql-cell).
+Rather than baking data into JavaScript, use [JSON](https://en.wikipedia.org/wiki/JSON) or [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files to store data. You can use [d3.json](https://d3js.org/d3-fetch#json), [d3.csv](https://d3js.org/d3-fetch#csv), or [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to load a file. On Observable, you can also use a [file attachment](https://observablehq.com/@observablehq/file-attachments) or [SQL cell](https://observablehq.com/@observablehq/sql-cell).
:::
To use data with Plot, pass the data as the first argument to the mark constructor. We can then assign columns of data such as *Date* and *Close* to visual properties of the mark (or “channels”) such as horizontal↔︎ position **x** and vertical↕︎ position **y**.
@@ -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.
@@ -248,15 +248,19 @@ When using facets, set the *fx* and *fy* scales’ **round** option to false if
## Other options
-If a **caption** is specified, Plot.plot wraps the generated SVG element in an HTML figure element with a figcaption, returning the figure. To specify an HTML caption, the caption can be specified as an HTML element, say using the [`html` tagged template literal](http://github.com/observablehq/htl); otherwise, the specified string represents text that will be escaped as needed.
+By default, [plot](#plot) returns an SVG element; however, if the plot includes a title, subtitle, [legend](./legends.md), or caption, plot wraps the SVG element with an HTML figure element. You can also force Plot to generate a figure element by setting the **figure** option to true.
+
+The **title** & **subtitle** options and the **caption** option accept either a string or an HTML element. If given an HTML element, say using the [`html` tagged template literal](http://github.com/observablehq/htl), the title and subtitle are used as-is while the caption is wrapped in a figcaption element; otherwise, the specified text will be escaped and wrapped in an h2, h3, or figcaption, respectively.
:::plot https://observablehq.com/@observablehq/plot-caption
```js
Plot.plot({
- caption: "Figure 1. A chart with a caption.",
+ title: "For charts, an informative title",
+ subtitle: "Subtitle to follow with additional context",
+ caption: "Figure 1. A chart with a title, subtitle, and caption.",
marks: [
Plot.frame(),
- Plot.text(["Hello, world!"], {frameAnchor: "middle"})
+ Plot.text(["Titles, subtitles, captions, and annotations assist interpretation by telling the reader what’s interesting. Don’t make the reader work to find what you already know."], {lineWidth: 30, frameAnchor: "middle"})
]
})
```
@@ -270,9 +274,11 @@ Unitless numbers ([quirky lengths](https://www.w3.org/TR/css-values-4/#deprecate
The generated SVG element has a class name which applies a default stylesheet. Use the top-level **className** option to specify that class name.
+The **clip** option determines the default clipping behavior if the [mark **clip** option](./marks.md#mark-options) is not specified; set it to true to enable clipping. This option does not affect [axis](../marks/axis.md), [grid](../marks/grid.md), and [frame](../marks/frame.md) marks, whose **clip** option defaults to false.
+
The **document** option specifies the [document](https://developer.mozilla.org/en-US/docs/Web/API/Document) used to create plot elements. It defaults to window.document, but can be changed to another document, say when using a virtual DOM implementation for server-side rendering in Node.
-## plot(*options*)
+## plot(*options*) {#plot}
```js
Plot.plot({
@@ -285,15 +291,15 @@ Plot.plot({
Renders a new plot with the specified *options*, returning a SVG or HTML figure element. This element can then be inserted into the page as described in the [getting started guide](../getting-started.md).
-## *mark*.plot(*options*)
+## *mark*.plot(*options*) {#mark_plot}
```js
Plot.barY(alphabet, {x: "letter", y: "frequency"}).plot({height: 200})
```
-Given a [*mark*](./marks.md), this is a convenience shorthand for calling [plot](#plot-options) where the **marks** option includes this *mark*. Any additional **marks** in *options* are drawn on top of this *mark*.
+Given a [*mark*](./marks.md), this is a convenience shorthand for calling [plot](#plot) where the **marks** option includes this *mark*. Any additional **marks** in *options* are drawn on top of this *mark*.
-## *plot*.scale(*name*)
+## *plot*.scale(*name*) {#plot_scale}
```js
const plot = Plot.plot(options); // render a plot
@@ -301,13 +307,13 @@ const color = plot.scale("color"); // get the color scale
console.log(color.range); // inspect the scale’s range
```
-Returns the [scale object](./scales.md#scale-options) for the scale with the specified *name* (such as *x* or *color*) on the given *plot*, where *plot* is a rendered plot element returned by [plot](#plot-options). If the associated *plot* has no scale with the given *name*, returns undefined.
+Returns the [scale object](./scales.md#scale-options) for the scale with the specified *name* (such as *x* or *color*) on the given *plot*, where *plot* is a rendered plot element returned by [plot](#plot). If the associated *plot* has no scale with the given *name*, returns undefined.
-## *plot*.legend(*name*, *options*)
+## *plot*.legend(*name*, *options*) {#plot_legend}
```js
const plot = Plot.plot(options); // render a plot
const legend = plot.legend("color"); // render a color legend
```
-Renders a standalone legend for the scale with the specified *name* (such as *x* or *color*) on the given *plot*, where *plot* is a rendered plot element returned by [plot](#plot-options), returning a SVG or HTML figure element. This element can then be inserted into the page as described in the [getting started guide](../getting-started.md). If the associated *plot* has no scale with the given *name*, returns undefined. Legends are currently only supported for *color*, *opacity*, and *symbol* scales.
+Renders a standalone legend for the scale with the specified *name* (such as *x* or *color*) on the given *plot*, where *plot* is a rendered plot element returned by [plot](#plot), returning a SVG or HTML figure element. This element can then be inserted into the page as described in the [getting started guide](../getting-started.md). If the associated *plot* has no scale with the given *name*, returns undefined. Legends are currently only supported for *color*, *opacity*, and *symbol* scales.
diff --git a/docs/features/projections.md b/docs/features/projections.md
index b3446ad32a..80a26e9079 100644
--- a/docs/features/projections.md
+++ b/docs/features/projections.md
@@ -28,7 +28,7 @@ onMounted(() => {
-# Projections
+# Projections
A **projection** maps abstract coordinates in *x* and *y* to pixel positions on screen. Most often, abstract coordinates are spherical (degrees longitude and latitude), as when rendering a geographic map. For example, below we show earthquakes in the last seven days with a magnitude of 2.5 or higher as reported by the [USGS](https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php). Use the slider to adjust the *orthographic* projection’s center of longitude.
@@ -54,9 +54,9 @@ Plot.plot({
```
:::
-Above, a [geo mark](../marks/geo.md) draws polygons representing land and a [sphere mark](../marks/geo.md#sphere-options) draws the outline of the globe. A [dot mark](../marks/dot.md) draws earthquakes as circles sized by magnitude.
+Above, a [geo mark](../marks/geo.md) draws polygons representing land and a [sphere mark](../marks/geo.md#sphere) draws the outline of the globe. A [dot mark](../marks/dot.md) draws earthquakes as circles sized by magnitude.
-The geo mark is “projection aware” so that it can handle all the nuances of projecting spherical polygons to the screen—leaning on [d3-geo](https://github.com/d3/d3-geo) to provide [adaptive sampling](https://observablehq.com/@d3/adaptive-sampling) with configurable precision, [antimeridian cutting](https://observablehq.com/@d3/antimeridian-cutting), and clipping. The dot mark is not; instead, Plot applies the projection in place of the *x* and *y* scales. Hence, projections work with any mark that consumes continuous **x** and **y** channels—as well as marks that use **x1** & **y1** and **x2** & **y2**. Each mark implementation decides whether to handle projections specially or to treat the projection as any other position scale. (For example, the [line mark](../marks/line.md) is projection-aware to draw geodesics.)
+The geo mark is “projection aware” so that it can handle all the nuances of projecting spherical polygons to the screen — leaning on [d3-geo](https://d3js.org/d3-geo) to provide [adaptive sampling](https://observablehq.com/@d3/adaptive-sampling) with configurable precision, [antimeridian cutting](https://observablehq.com/@d3/antimeridian-cutting), and clipping. The dot mark is not; instead, Plot applies the projection in place of the *x* and *y* scales. Hence, projections work with any mark that consumes continuous **x** and **y** channels — as well as marks that use **x1** & **y1** and **x2** & **y2**. Each mark implementation decides whether to handle projections specially or to treat the projection as any other position scale. (For example, the [line mark](../marks/line.md) is projection-aware to draw geodesics.)
:::info
Marks that require *band* scales (bars, cells, and ticks) cannot be used with projections. Likewise one-dimensional marks such as rules cannot be used, though see [#1164](https://github.com/observablehq/plot/issues/1164).
@@ -131,7 +131,7 @@ Plot.plot({
Use the *albers-usa* projection for U.S.-centric choropleth maps.
:::
-For maps that focus on a specific region, use the **domain** option to zoom in. This object should be a GeoJSON object. For example, you can use [d3.geoCircle](https://github.com/d3/d3-geo/blob/main/README.md#geoCircle) to generate a circle of a given radius centered at a given longitude and latitude. You can also use the **inset** options for a bit of padding around the **domain**.
+For maps that focus on a specific region, use the **domain** option to zoom in. This object should be a GeoJSON object. For example, you can use [d3.geoCircle](https://d3js.org/d3-geo/shape#geoCircle) to generate a circle of a given radius centered at a given longitude and latitude. You can also use the **inset** options for a bit of padding around the **domain**.
@@ -251,13 +251,13 @@ The following built-in named projections are supported:
* *reflect-y* - like the identity projection, but *y* points up
* null (default) - the null projection for pre-projected geometry in screen coordinates
-In addition to these named projections, the **projection** option may be specified as a [D3 projection](https://github.com/d3/d3-geo/blob/main/README.md#projections), or any custom projection that implements [*projection*.stream](https://github.com/d3/d3-geo/blob/main/README.md#projection_stream), or a function that receives a configuration object ({*width*, *height*, ...*options*}) and returns such a projection. In the last case, the width and height represent the frame dimensions minus any insets.
+In addition to these named projections, the **projection** option may be specified as a [D3 projection](https://d3js.org/d3-geo/projection), or any custom projection that implements [*projection*.stream](https://d3js.org/d3-geo/stream), or a function that receives a configuration object ({*width*, *height*, ...*options*}) and returns such a projection. In the last case, the width and height represent the frame dimensions minus any insets.
If the **projection** option is specified as an object, the following additional projection options are supported:
* **type** - one of the projection names above
-* **parallels** - the [standard parallels](https://github.com/d3/d3-geo/blob/main/README.md#conic_parallels) (for conic projections only)
-* **precision** - the [sampling threshold](https://github.com/d3/d3-geo/blob/main/README.md#projection_precision)
+* **parallels** - the [standard parallels](https://d3js.org/d3-geo/conic#conic_parallels) (for conic projections only)
+* **precision** - the [sampling threshold](https://d3js.org/d3-geo/projection#projection_precision)
* **rotate** - a two- or three- element array of Euler angles to rotate the sphere
* **domain** - a GeoJSON object to fit in the center of the (inset) frame
* **inset** - inset by the given amount in pixels when fitting to the frame (default zero)
diff --git a/docs/features/scales.md b/docs/features/scales.md
index db603bb843..cb263345c4 100644
--- a/docs/features/scales.md
+++ b/docs/features/scales.md
@@ -76,7 +76,7 @@ Plot.ruleX(gistemp, {x: "Date", stroke: "Anomaly"}).plot()
```
:::
-While the resulting chart looks different, the *color* scale here behaves similarly to the `y` function above—the only difference is that it interpolates colors (using [d3.interpolateTurbo](https://github.com/d3/d3-scale-chromatic/blob/main/README.md#interpolateTurbo)) instead of numbers (the top and bottom sides of the plot frame):
+While the resulting chart looks different, the *color* scale here behaves similarly to the `y` function above — the only difference is that it interpolates colors (using [d3.interpolateTurbo](https://d3js.org/d3-scale-chromatic/sequential#interpolateTurbo)) instead of numbers (the top and bottom sides of the plot frame):
```js
function color(anomaly) {
@@ -143,7 +143,7 @@ Plot.plot({x: {domain: [0, 100], reverse: true, grid: true}})
If the domain is dates, Plot will default to a UTC scale. This is a linear scale with ticks based on the Gregorian calendar.
-
+
:::plot https://observablehq.com/@observablehq/plot-continuous-scales
```js
@@ -151,10 +151,6 @@ Plot.plot({x: {domain: [new Date("2021-01-01"), new Date("2022-01-01")], grid: t
```
:::
-:::tip
-We are working on better multi-line ticks for time scales; please upvote [#1285](https://github.com/observablehq/plot/issues/1285) if you are interested.
-:::
-
To force a UTC scale, say when the data is milliseconds since UNIX epoch rather than Date instances, pass *utc* as the **type** option. Though we recommend coercing strings and numbers to more specific types when you load data, rather than relying on scales to do it.
:::plot https://observablehq.com/@observablehq/plot-continuous-scales
@@ -163,7 +159,7 @@ Plot.plot({x: {type: "utc", domain: [1609459200000, 1640995200000], grid: true}}
```
:::
-If the scale **type** is *time*, the ticks will be in local time—as with the dates below—rather than UTC.
+If the scale **type** is *time*, the ticks will be in local time — as with the dates below — rather than UTC.
:::plot https://observablehq.com/@observablehq/plot-continuous-scales
```js
@@ -179,7 +175,7 @@ Plot.plot({x: {type: "log", domain: [1e0, 1e5], grid: true}})
```
:::
-If you prefer conventional notation, you can specify the **tickFormat** option to change the behavior of the axis. The **tickFormat** option can either be a [d3.format](https://github.com/d3/d3-format) string or a function that takes a tick value and returns the corresponding string. Note, however, that this may result in overlapping text.
+If you prefer conventional notation, you can specify the **tickFormat** option to change the behavior of the axis. The **tickFormat** option can either be a [d3.format](https://d3js.org/d3-format) string or a function that takes a tick value and returns the corresponding string. Note, however, that this may result in overlapping text.
:::plot https://observablehq.com/@observablehq/plot-continuous-scales
```js
@@ -195,7 +191,7 @@ Plot.plot({x: {type: "log", base: 2, domain: [1e0, 1e4], ticks: 20, grid: true}}
```
:::
-The domain of a log scale cannot include (or cross) zero; for this, consider a [bi-symmetric log](https://github.com/d3/d3-scale#symlog-scales) scale instead.
+The domain of a log scale cannot include (or cross) zero; for this, consider a [bi-symmetric log](https://d3js.org/d3-scale/symlog) scale instead.
:::plot https://observablehq.com/@observablehq/plot-continuous-scales
```js
@@ -241,7 +237,7 @@ Plot
```
:::
-While *point* and *band* scales appear visually similar when only the grid is visible, the two are not identical—they differ respective to padding. Play with the options below to get a sense of their effect on the scale’s behavior.
+While *point* and *band* scales appear visually similar when only the grid is visible, the two are not identical — they differ respective to padding. Play with the options below to get a sense of their effect on the scale’s behavior.
@@ -604,7 +600,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.
@@ -707,7 +703,7 @@ The default range depends on the scale: for position scales (*x*, *y*, *fx*, and
The behavior of the **unknown** scale option depends on the scale type. For quantitative and temporal scales, the unknown value is used whenever the input value is undefined, null, or NaN. For ordinal or categorical scales, the unknown value is returned for any input value outside the domain. For band or point scales, the unknown option has no effect; it is effectively always equal to undefined. If the unknown option is set to undefined (the default), or null or NaN, then the affected input values will be considered undefined and filtered from the output.
-For data at regular intervals, such as integer values or daily samples, the [**interval** option](#scale-transforms) can be used to enforce uniformity. The specified *interval*—such as d3.utcMonth—must expose an *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) functions. The option can also be specified as a number, in which case it will be promoted to a numeric interval with the given step. The option can alternatively be specified as a string (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) naming the corresponding time interval, or a skip interval consisting of a number followed by the interval name (possibly pluralized), such as *3 months* or *10 years*. This option sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, the default *scale*.domain is an array of uniformly-spaced values spanning the extent of the values associated with the scale.
+For data at regular intervals, such as integer values or daily samples, the [**interval** option](#scale-transforms) can be used to enforce uniformity. The specified *interval* — such as d3.utcMonth — must expose an *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) functions. The option can also be specified as a number, in which case it will be promoted to a numeric interval with the given step. The option can alternatively be specified as a string (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) naming the corresponding time interval, or a skip interval consisting of a number followed by the interval name (possibly pluralized), such as *3 months* or *10 years*. This option sets the default *scale*.transform to the given interval’s *interval*.floor function. In addition, the default *scale*.domain is an array of uniformly-spaced values spanning the extent of the values associated with the scale.
Quantitative scales can be further customized with additional options:
@@ -732,14 +728,14 @@ Plot.plot({
### Color scale options
-The normal scale types—*linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordinal*—can be used to encode color. In addition, Plot supports special scale types for color:
+The normal scale types — *linear*, *sqrt*, *pow*, *log*, *symlog*, and *ordinal* — can be used to encode color. In addition, Plot supports special scale types for color:
* *categorical* - like *ordinal*, but defaults to *tableau10*
* *sequential* - like *linear*
* *cyclical* - like *linear*, but defaults to *rainbow*
-* *threshold* - encodes based on discrete thresholds specified as the **domain**; defaults to *rdylbu*
-* *quantile* - encodes based on the computed quantile thresholds; defaults to *rdylbu*
-* *quantize* - transforms a continuous domain into quantized thresholds; defaults to *rdylbu*
+* *threshold* - discretizes using thresholds given as the **domain**; defaults to *rdylbu*
+* *quantile* - discretizes by computing quantile thresholds; defaults to *rdylbu*
+* *quantize* - discretizes by computing uniform thresholds; defaults to *rdylbu*
* *diverging* - like *linear*, but with a pivot; defaults to *rdbu*
* *diverging-log* - like *log*, but with a pivot that defaults to 1; defaults to *rdbu*
* *diverging-pow* - like *pow*, but with a pivot; defaults to *rdbu*
@@ -921,7 +917,7 @@ Similarly, the *y* and *fy* scales support asymmetric insets with:
The inset scale options can provide “breathing room” to separate marks from axes or the plot’s edge. For example, in a scatterplot with a Plot.dot with the default 3-pixel radius and 1.5-pixel stroke width, an inset of 5 pixels prevents dots from overlapping with the axes. The *scale*.round option is useful for crisp edges by rounding to the nearest pixel boundary.
-In addition to the generic *ordinal* scale type, which requires an explicit output range value for each input domain value, Plot supports special *point* and *band* scale types for encoding ordinal data as position. These scale types accept a [*min*, *max*] range similar to quantitative scales, and divide this continuous interval into discrete points or bands based on the number of distinct values in the domain (*i.e.*, the domain’s cardinality). If the associated marks have no effective width along the ordinal dimension—such as a dot, rule, or tick—then use a *point* scale; otherwise, say for a bar, use a *band* scale.
+In addition to the generic *ordinal* scale type, which requires an explicit output range value for each input domain value, Plot supports special *point* and *band* scale types for encoding ordinal data as position. These scale types accept a [*min*, *max*] range similar to quantitative scales, and divide this continuous interval into discrete points or bands based on the number of distinct values in the domain (*i.e.*, the domain’s cardinality). If the associated marks have no effective width along the ordinal dimension — such as a dot, rule, or tick — then use a *point* scale; otherwise, say for a bar, use a *band* scale.
Ordinal position scales support additional options, all specified as proportions in [0, 1]:
@@ -947,7 +943,7 @@ Plot implicitly generates an [axis mark](../marks/axis.md) for position scales i
* **fontVariant** - the font-variant attribute for ticks; defaults to *tabular-nums* if quantitative
* **label** - a string to label the axis
* **labelAnchor** - the label anchor: *top*, *right*, *bottom*, *left*, or *center*
-* **labelArrow** - the label arrow: *auto* (default), *up*, *right*, *down*, *left*, *none*, or true
+* **labelArrow** - the label arrow: *auto* (default), *up*, *right*, *down*, *left*, *none*, or true
* **labelOffset** - the label position offset (in pixels; default depends on margins and orientation)
* **ariaLabel** - a short label representing the axis in the accessibility tree
* **ariaDescription** - a textual description for the axis
@@ -959,15 +955,15 @@ For an implicit [grid mark](../marks/grid.md), use the **grid** option. For an i
Top-level options are also supported as shorthand: **grid** (for *x* and *y* only; see [facets](./facets.md)), **label**, **axis**, **inset**, **round**, **align**, and **padding**. If the **grid** option is true, show a grid using *currentColor*; if specified as a string, show a grid with the specified color; if an approximate number of ticks, an interval, or an array of tick values, show corresponding grid lines.
-## Sort mark option
+## Sort mark option
-If an ordinal scale’s domain is not set, it defaults to natural ascending order; to order the domain by associated values in another dimension, either compute the domain manually (consider [d3.groupSort](https://github.com/d3/d3-array/blob/main/README.md#groupSort)) or use an associated mark’s **sort** option. For example, to sort bars by ascending frequency rather than alphabetically by letter:
+If an ordinal scale’s domain is not set, it defaults to natural ascending order; to order the domain by associated values in another dimension, either compute the domain manually (consider [d3.groupSort](https://d3js.org/d3-array/group#groupSort)) or use an associated mark’s **sort** option. For example, to sort bars by ascending frequency rather than alphabetically by letter:
```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y"}})
```
-The sort option is an object whose keys are ordinal scale names, such as *x* or *fx*, and whose values are mark channel names, such as **y**, **y1**, or **y2**. By specifying an existing channel rather than a new value, you avoid repeating the order definition and can refer to channels derived by [transforms](./transforms.md) (such as [stack](../transforms/stack.md) or [bin](../transforms/bin.md)). When sorting the *x* domain, if no **x** channel is defined, **x2** will be used instead if available, and similarly for *y* and **y2**; this is useful for marks that implicitly stack such as [area](../marks/area.md), [bar](../marks/bar.md), and [rect](../marks/rect.md). A sort value may also be specified as *width* or *height*, representing derived channels |*x2* - *x1*| and |*y2* - *y1*| respectively.
+The sort option is an object whose keys are ordinal scale names, such as *x* or *fx*, and whose values are mark channel names, such as **y**, **y1**, or **y2**. By specifying an existing channel rather than a new value, you avoid repeating the order definition and can refer to channels derived by [transforms](./transforms.md) (such as [stack](../transforms/stack.md) or [bin](../transforms/bin.md)). When sorting the *x* domain, if no **x** channel is defined, **x2** will be used instead if available, and similarly for *y* and **y2**; this is useful for marks that implicitly stack such as [area](../marks/area.md), [bar](../marks/bar.md), and [rect](../marks/rect.md). A sort value may also be specified as *width* or *height* , representing derived channels |*x2* - *x1*| and |*y2* - *y1*| respectively.
Note that there may be multiple associated values in the secondary dimension for a given value in the primary ordinal dimension. The secondary values are therefore grouped for each associated primary value, and each group is then aggregated by applying a reducer. The default reducer is *max*, but may be changed by specifying the **reduce** option. Lastly the primary values are by default sorted based on the associated reduced value in natural ascending order to produce the domain. The above code is shorthand for:
@@ -1011,9 +1007,9 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: {value: "y", order:
If the input channel is *data*, then the reducer is passed groups of the mark’s data; this is typically used in conjunction with a custom reducer function, as when the built-in single-channel reducers are insufficient.
-Note: when the value of the sort option is a string or a function, it is interpreted as a mark [sort transform](../transforms/sort.md). To use both sort options and a mark sort transform, use [Plot.sort](../transforms/sort.md#sort-order-options).
+Note: when the value of the sort option is a string or a function, it is interpreted as a mark [sort transform](../transforms/sort.md). To use both sort options and a mark sort transform, use [Plot.sort](../transforms/sort.md#sort).
-## scale(*options*)
+## scale(*options*) {#scale}
You can also create a standalone scale with Plot.**scale**(*options*). The *options* object must define at least one scale; see [Scale options](#scale-options) for how to define a scale. For example, here is a linear color scale with the default domain of [0, 1] and default scheme *turbo*:
@@ -1021,7 +1017,7 @@ You can also create a standalone scale with Plot.**scale**(*options*). The *opti
const color = Plot.scale({color: {type: "linear"}});
```
-Both [*plot*.scale](./plots.md#plot-scale-name) and [Plot.scale](#scale-options-1) return scale objects. These objects represent the actual (or “materialized”) scale options used by Plot, including the domain, range, interpolate function, *etc.* The scale’s label, if any, is also returned; however, note that other axis properties are not currently exposed. Point and band scales also expose their materialized bandwidth and step.
+Both [*plot*.scale](./plots.md#plot_scale) and [Plot.scale](#scale) return scale objects. These objects represent the actual (or “materialized”) scale options used by Plot, including the domain, range, interpolate function, *etc.* The scale’s label, if any, is also returned; however, note that other axis properties are not currently exposed. Point and band scales also expose their materialized bandwidth and step.
To reuse a scale across plots, pass the corresponding scale object into another plot specification:
diff --git a/docs/features/shorthand.md b/docs/features/shorthand.md
index 539640ca5d..a57da2cead 100644
--- a/docs/features/shorthand.md
+++ b/docs/features/shorthand.md
@@ -73,9 +73,9 @@ const gene = "AAAAGAGTGAAGATGCTGGAGACGAGTGAAGCATTCACTTTAGGGAAAGCGAGGCAAGAGCGTTTC
-# Shorthand
+# Shorthand
-The most concise form of Plot is its **shorthand** syntax where no options are specified—only data. To use this shorthand, the data must have a specific structure: either a one-dimensional array of values [*v₀*, *v₁*, *v₂*, …] or a two-dimensional array of tuples [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …].
+The most concise form of Plot is its **shorthand** syntax where no options are specified — only data. To use this shorthand, the data must have a specific structure: either a one-dimensional array of values [*v₀*, *v₁*, *v₂*, …] or a two-dimensional array of tuples [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …].
While none of these charts are particularly groundbreaking, we hope you find this shorthand convenient the next time you want a quick look at some data. And if the shorthand view is useful, you can then enhance it by adding options!
@@ -180,7 +180,7 @@ Plot.boxX(numbers).plot()
```
:::
-Some of Plot’s transforms support shorthand syntax, too. For example, we can use Plot.rectY with [Plot.binX](../transforms/bin.md) to generate a histogram—another common way to visualize a one-dimensional distribution.
+Some of Plot’s transforms support shorthand syntax, too. For example, we can use Plot.rectY with [Plot.binX](../transforms/bin.md) to generate a histogram — another common way to visualize a one-dimensional distribution.
:::plot https://observablehq.com/@observablehq/plot-shorthand-histogram
```js
diff --git a/docs/features/transforms.md b/docs/features/transforms.md
index 8da856d77b..70a9df992f 100644
--- a/docs/features/transforms.md
+++ b/docs/features/transforms.md
@@ -70,9 +70,9 @@ Plot.plot({
```
:::
-Plot includes many useful transforms! For example, you can compute a [rolling average](../transforms/window.md) to smooth a noisy signal, [stack layers](../transforms/stack.md) for a streamgraph, or [dodge dots](../transforms/dodge.md) for a beeswarm. Plot’s various built-in transforms include: [bin](../transforms/bin.md), [centroid](../transforms/centroid.md), [dodge](../transforms/dodge.md), [filter](../transforms/filter.md), [group](../transforms/group.md), [hexbin](../transforms/hexbin.md), [interval](../transforms/interval.md), [map](../transforms/map.md), [normalize](../transforms/normalize.md), [reverse](../transforms/sort.md#reverse-options), [select](../transforms/select.md), [shuffle](../transforms/sort.md#shuffle-options), [sort](../transforms/sort.md), [stack](../transforms/stack.md), [tree](../transforms/tree.md), and [window](../transforms/window.md). If these don’t meet your needs, you can even implement a [custom transform](#custom-transforms).
+Plot includes many useful transforms! For example, you can compute a [rolling average](../transforms/window.md) to smooth a noisy signal, [stack layers](../transforms/stack.md) for a streamgraph, or [dodge dots](../transforms/dodge.md) for a beeswarm. Plot’s various built-in transforms include: [bin](../transforms/bin.md), [centroid](../transforms/centroid.md), [dodge](../transforms/dodge.md), [filter](../transforms/filter.md), [group](../transforms/group.md), [hexbin](../transforms/hexbin.md), [interval](../transforms/interval.md), [map](../transforms/map.md), [normalize](../transforms/normalize.md), [reverse](../transforms/sort.md#reverse), [select](../transforms/select.md), [shuffle](../transforms/sort.md#shuffle), [sort](../transforms/sort.md), [stack](../transforms/stack.md), [tree](../transforms/tree.md), and [window](../transforms/window.md). If these don’t meet your needs, you can even implement a [custom transform](#custom-transforms).
-Transforms are never required—you can always aggregate and derive data yourself outside of Plot, and then pass in the binned values. For example, we could use [d3.bin](https://github.com/d3/d3-array/blob/main/README.md#bin) to compute a histogram of athletes’ weights as an array of {*x0*, *x1*, *length*} objects.
+Transforms are never required — you can always aggregate and derive data yourself outside of Plot, and then pass in the binned values. For example, we could use [d3.bin](https://d3js.org/d3-array/bin) to compute a histogram of athletes’ weights as an array of {*x0*, *x1*, *length*} objects.
```js
bins = d3.bin().thresholds(80).value((d) => d.weight)(olympians)
@@ -183,7 +183,7 @@ While transform functions often produce new *data* or *facets*, they may return
When implementing a custom transform for generic usage, keep in mind that it needs to be compatible with Plot’s [faceting system](./facets.md), which partitions the original dataset into discrete subsets.
-## Custom initializers
+## Custom initializers
Initializers are a special class of transform; whereas transforms operate in abstract data space, initializers operate in screen space such as pixel coordinates and colors. For example, initializers can modify a marks’ positions to avoid occlusion. Initializers are invoked *after* the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or may not, as desired) be passed to scales. Plot’s [hexbin](../transforms/hexbin.md) and [dodge](../transforms/dodge.md) transforms are initializers.
@@ -191,7 +191,7 @@ You can specify a custom initializer by specifying a function as the mark **init
If an initializer desires a channel that is not supported by the downstream mark, additional channels can be declared using the mark **channels** option.
-## transform(*options*, *transform*)
+## transform(*options*, *transform*) {#transform}
```js
Plot.transform(options, (data, facets) => {
@@ -203,11 +203,11 @@ Plot.transform(options, (data, facets) => {
```
Given an *options* object that may specify some basic transforms (**filter**, **sort**, or **reverse**) or a custom **transform** function, composes those transforms if any with the given *transform* function, returning a new *options* object. If a custom **transform** function is present on the given *options*, any basic transforms are ignored. Any additional input *options* are passed through in the returned *options* object. This method facilitates applying the basic transforms prior to applying the given custom *transform* and is used internally by Plot’s built-in transforms.
-## initializer(*options*, *initializer*)
+## initializer(*options*, *initializer*) {#initializer}
This helper composes the *initializer* function with any other transforms present in the *options*, and returns a new *options* object. It is used internally by Plot’s built-in initializer transforms.
-## valueof(*data*, *value*, *type*)
+## valueof(*data*, *value*, *type*) {#valueof}
```js
Plot.valueof(aapl, "Close")
@@ -226,7 +226,7 @@ If *type* is specified, it must be Array or a similar class that implements the
valueof is not guaranteed to return a new array. When a transform method is used, or when the given *value* is an array that is compatible with the requested *type*, the array may be returned as-is without making a copy.
-## column(*source*)
+## column(*source*) {#column}
```js
const [X, setX] = Plot.column();
@@ -236,7 +236,7 @@ This helper for constructing derived columns returns a [*column*, *setColumn*] a
This method is used by Plot’s transforms to derive channels; the associated columns are populated (derived) when the **transform** option function is invoked.
-## identity
+## identity {#identity}
```js
Plot.contour(data, {width: w, height: h, fill: Plot.identity})
@@ -244,7 +244,7 @@ Plot.contour(data, {width: w, height: h, fill: Plot.identity})
This channel helper returns a source array as-is, avoiding an extra copy when defining a channel as being equal to the data.
-## indexOf
+## indexOf {#indexOf}
```js
Plot.lineY(numbers, {x: Plot.indexOf, y: Plot.identity})
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 898fd5f224..29761ed1ce 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -78,7 +78,7 @@ div.append(plot);
```
:::
-Plot returns a detached DOM element—either an [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) or [HTML figure](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure) element. In vanilla web development, this means you need to insert the generated plot into the page to see it. Typically this is done by selecting a DOM element (such as a DIV with a unique identifier, like `myplot` above), and then calling [*element*.append](https://developer.mozilla.org/en-US/docs/Web/API/Element/append).
+Plot returns a detached DOM element — either an [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) or [HTML figure](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure) element. In vanilla web development, this means you need to insert the generated plot into the page to see it. Typically this is done by selecting a DOM element (such as a DIV with a unique identifier, like `myplot` above), and then calling [*element*.append](https://developer.mozilla.org/en-US/docs/Web/API/Element/append).
If you’d prefer to run Plot locally (or entirely offline), you can download the UMD bundle of Plot along with its dependency, D3, here:
@@ -289,3 +289,33 @@ export default {
```
As with React, to update your plot for whatever reason, simply render a new one and replace the old one. You can find more examples on [our GitHub](https://github.com/observablehq/plot/tree/main/docs) as this documentation site is built with VitePress and uses both client- and server-side rendering for plots!
+
+## Plot in Svelte
+
+Here’s an example of client-side rendering in Svelte. For server-side rendering, see [#1759](https://github.com/observablehq/plot/discussions/1759).
+
+:::code-group
+```svelte [App.svelte]
+
+
+
+```
+:::
+
+See our [Plot + Svelte REPL](https://svelte.dev/repl/ebf78a6a6c1145ecb84cf9345a7f82ae?version=4.2.0) for details.
diff --git a/docs/interactions/crosshair.md b/docs/interactions/crosshair.md
index 17e27c9644..8c58bc03f2 100644
--- a/docs/interactions/crosshair.md
+++ b/docs/interactions/crosshair.md
@@ -8,7 +8,7 @@ import penguins from "../data/penguins.ts";
-# Crosshair mark
+# Crosshair mark
The **crosshair mark** shows the *x* (horizontal↔︎ position) and *y* (vertical↕︎ position) value of the point closest to the [pointer](./pointer.md) on the bottom and left sides of the frame, respectively.
@@ -23,7 +23,7 @@ Plot.plot({
```
:::
-For charts which have a “dominant” dimension, such as time in a time-series chart, use the crosshairX or crosshairY mark for the [pointerX](./pointer.md#pointerx-options) or [pointerY](./pointer.md#pointery-options) transform as appropriate.
+For charts which have a “dominant” dimension, such as time in a time-series chart, use the crosshairX or crosshairY mark for the [pointerX](./pointer.md#pointerX) or [pointerY](./pointer.md#pointerY) transform as appropriate.
:::plot defer https://observablehq.com/@observablehq/plot-crosshairx
```js
@@ -84,7 +84,7 @@ The following options are supported:
The crosshair mark supports faceting, but most other mark options are ignored.
-## crosshair(*data*, *options*)
+## crosshair(*data*, *options*) {#crosshair}
```js
Plot.crosshair(cars, {x: "economy (mpg)", y: "cylinders"})
@@ -92,18 +92,18 @@ Plot.crosshair(cars, {x: "economy (mpg)", y: "cylinders"})
Returns a new crosshair for the given *data* and *options*, drawing horizontal and vertical rules. The corresponding **x** and **y** values are also drawn just outside the bottom and left sides of the frame, respectively, typically on top of the axes. If either **x** or **y** is not specified, the crosshair will be one-dimensional.
-## crosshairX(*data*, *options*)
+## crosshairX(*data*, *options*) {#crosshairX}
```js
Plot.crosshairX(aapl, {x: "Date", y: "Close"})
```
-Like crosshair, but using [pointerX](./pointer.md#pointerx-options) when *x* is the dominant dimension, like time in a time-series chart.
+Like crosshair, but using [pointerX](./pointer.md#pointerX) when *x* is the dominant dimension, like time in a time-series chart.
-## crosshairY(*data*, *options*)
+## crosshairY(*data*, *options*) {#crosshairY}
```js
Plot.crosshairY(aapl, {x: "Date", y: "Close"})
```
-Like crosshair, but using [pointerY](./pointer.md#pointery-options) when *y* is the dominant dimension.
+Like crosshair, but using [pointerY](./pointer.md#pointerY) when *y* is the dominant dimension.
diff --git a/docs/interactions/pointer.md b/docs/interactions/pointer.md
index 0a89d28e2d..89b6a8e253 100644
--- a/docs/interactions/pointer.md
+++ b/docs/interactions/pointer.md
@@ -21,7 +21,7 @@ onMounted(() => {
-# Pointer transform
+# Pointer transform
The **pointer transform** filters a mark interactively such that only the point closest to the pointer is rendered. It is typically used to show details on hover, often with a [tip](../marks/tip.md) or [crosshair](./crosshair.md) mark, but it can be paired with any mark.
@@ -72,7 +72,7 @@ Plot.plot({
The pointer transform is similar to the [filter](../transforms/filter.md) and [select](../transforms/select.md) transforms: it filters the mark’s index to show a subset of the data. The difference is that the pointer transform is *interactive*: it listens to [pointer events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) and re-renders the mark as the closest point changes. Since the mark is lazily rendered during interaction, it is fast: only the visible elements are rendered as needed. And, like the filter and select transforms, unfiltered channel values are incorporated into default scale domains.
-The pointer transform supports both one- and two-dimensional pointing modes. The two-dimensional mode, [pointer](#pointer-options-1), is used above and is suitable for scatterplots and the general case: it finds the point closest to the pointer by measuring distance in *x* and *y*. The one-dimensional modes, [pointerX](#pointerx-options) and [pointerY](#pointery-options), in contrast only consider distance in one dimension; this is desirable when a chart has a “dominant” dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
+The pointer transform supports both one- and two-dimensional pointing modes. The two-dimensional mode, [pointer](#pointer), is used above and is suitable for scatterplots and the general case: it finds the point closest to the pointer by measuring distance in *x* and *y*. The one-dimensional modes, [pointerX](#pointerX) and [pointerY](#pointerY), in contrast only consider distance in one dimension; this is desirable when a chart has a “dominant” dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
Try the different modes on the line chart below to get a feel for their behavior.
@@ -180,7 +180,7 @@ To resolve the horizontal target position, the pointer transform applies the fol
The same precedence applies to the **py**, **y**, **y1**, and **y2** channels.
-## pointer(*options*)
+## pointer(*options*) {#pointer}
```js
Plot.tip(penguins, Plot.pointer({x: "culmen_length_mm", y: "culmen_depth_mm"}))
@@ -188,18 +188,18 @@ Plot.tip(penguins, Plot.pointer({x: "culmen_length_mm", y: "culmen_depth_mm"}))
Applies the pointer render transform to the specified *options* to filter the mark index such that only the point closest to the pointer is rendered; the mark will re-render interactively in response to pointer events.
-## pointerX(*options*)
+## pointerX(*options*) {#pointerX}
```js
Plot.tip(aapl, Plot.pointerX({x: "Date", y: "Close"}))
```
-Like [pointer](#pointer-options-1), except the determination of the closest point considers mostly the *x* (horizontal↔︎) position; this should be used for plots where *x* is the dominant dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
+Like [pointer](#pointer), except the determination of the closest point considers mostly the *x* (horizontal↔︎) position; this should be used for plots where *x* is the dominant dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
-## pointerY(*options*)
+## pointerY(*options*) {#pointerY}
```js
Plot.tip(alphabet, Plot.pointerY({x: "frequency", y: "letter"}))
```
-Like [pointer](#pointer-options-1), except the determination of the closest point considers mostly the *y* (vertical↕︎) position; this should be used for plots where *y* is the dominant dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
+Like [pointer](#pointer), except the determination of the closest point considers mostly the *y* (vertical↕︎) position; this should be used for plots where *y* is the dominant dimension, such as time in a time-series chart, the binned quantitative dimension in a histogram, or the categorical dimension of a bar chart.
diff --git a/docs/marks/area.md b/docs/marks/area.md
index 3a6673b6da..bec175a637 100644
--- a/docs/marks/area.md
+++ b/docs/marks/area.md
@@ -18,7 +18,7 @@ Plot.areaY(aapl, {x: "Date", y: "Close"}).plot()
```
:::
-The area mark has three constructors: [areaY](#areay-data-options) for when the baseline and topline share *x* values, as in a time-series area chart where time goes right→ (or ←left); [areaX](#areax-data-options) for when the baseline and topline share *y* values, as in a time-series area chart where time goes up↑ (or down↓); and lastly the rarely-used [area](#area-data-options) where the baseline and topline share neither *x* nor *y* values.
+The area mark has three constructors: [areaY](#areaY) for when the baseline and topline share *x* values, as in a time-series area chart where time goes right→ (or ←left); [areaX](#areaX) for when the baseline and topline share *y* values, as in a time-series area chart where time goes up↑ (or down↓); and lastly the rarely-used [area](#area) where the baseline and topline share neither *x* nor *y* values.
The area mark is often paired with a [line](./line.md) and [rule](./rule.md) mark to accentuate the topline and baseline.
@@ -102,7 +102,7 @@ Plot.plot({
```
:::
-For a vertically-oriented baseline and topline, such as when time goes up↑ instead of right→, use [areaX](#areax-data-options) instead of [areaY](#areay-data-options) and swap **x** and **y**.
+For a vertically-oriented baseline and topline, such as when time goes up↑ instead of right→, use [areaX](#areaX) instead of [areaY](#areaY) and swap **x** and **y**.
:::plot defer https://observablehq.com/@observablehq/plot-vertical-area-chart
```js
@@ -282,7 +282,7 @@ In addition to the [standard mark options](../features/marks.md#mark-options), t
* **y2** - the vertical position of the topline; bound to the *y* scale
* **z** - a categorical value to group data into series
-If **x2** is not specified, it defaults to **x1**. If **y2** is not specified, it defaults to **y1**. These defaults facilitate sharing *x* or *y* coordinates between the baseline and topline. See also the implicit stack transform and shorthand **x** and **y** options supported by [areaY](#areay-data-options) and [areaX](#areax-data-options).
+If **x2** is not specified, it defaults to **x1**. If **y2** is not specified, it defaults to **y1**. These defaults facilitate sharing *x* or *y* coordinates between the baseline and topline. See also the implicit stack transform and shorthand **x** and **y** options supported by [areaY](#areaY) and [areaX](#areaX).
By default, the data is assumed to represent a single series (*i.e.*, a single value that varies over time). If the **z** channel is specified, data is grouped by **z** to form separate series. Typically **z** is a categorical value such as a series name. If **z** is not specified, it defaults to **fill** if a channel, or **stroke** if a channel.
@@ -290,9 +290,9 @@ The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if th
Points along the baseline and topline are connected in input order. Likewise, if there are multiple series via the **z**, **fill**, or **stroke** channel, the series are drawn in input order such that the last series is drawn on top. Typically, the data is already in sorted order, such as chronological for time series; if sorting is needed, consider a [sort transform](../transforms/sort.md).
-The area mark supports [curve options](../features/curves.md) to control interpolation between points. If any of the **x1**, **y1**, **x2**, or **y2** values are invalid (undefined, null, or NaN), the baseline and topline will be interrupted, resulting in a break that divides the area shape into multiple segments. (See [d3-shape’s *area*.defined](https://github.com/d3/d3-shape/blob/main/README.md#area_defined) for more.) If an area segment consists of only a single point, it may appear invisible unless rendered with rounded or square line caps. In addition, some curves such as *cardinal-open* only render a visible segment if it contains multiple points.
+The area mark supports [curve options](../features/curves.md) to control interpolation between points. If any of the **x1**, **y1**, **x2**, or **y2** values are invalid (undefined, null, or NaN), the baseline and topline will be interrupted, resulting in a break that divides the area shape into multiple segments. (See [d3-shape’s *area*.defined](https://d3js.org/d3-shape/area#area_defined) for more.) If an area segment consists of only a single point, it may appear invisible unless rendered with rounded or square line caps. In addition, some curves such as *cardinal-open* only render a visible segment if it contains multiple points.
-## areaY(*data*, *options*)
+## areaY(*data*, *options*) {#areaY}
```js
Plot.areaY(aapl, {x: "Date", y: "Close"})
@@ -310,7 +310,7 @@ The **interval** option is recommended to “regularize” sampled data; for exa
The **areaY** mark draws the region between a baseline (*y1*) and a topline (*y2*) as in an area chart. When the baseline is *y* = 0, the *y* channel can be specified instead of *y1* and *y2*. For example, here is an area chart of Apple’s stock price.
-## areaX(*data*, *options*)
+## areaX(*data*, *options*) {#areaX}
```js
Plot.areaX(aapl, {y: "Date", x: "Close"})
@@ -326,10 +326,10 @@ Plot.areaX(observations, {y: "date", x: "temperature", interval: "day"})
The **interval** option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use "day" as the interval.
-## area(*data*, *options*)
+## area(*data*, *options*) {#area}
```js
Plot.area(aapl, {x1: "Date", y1: 0, y2: "Close"})
```
-Returns a new area with the given *data* and *options*. This method is rarely used directly; it is only needed when the baseline and topline have neither common **x** nor **y** values. [areaY](#areay-data-options) is used in the common horizontal orientation where the baseline and topline share **x** values, while [areaX](#areax-data-options) is used in the vertical orientation where the baseline and topline share **y** values.
+Returns a new area with the given *data* and *options*. This method is rarely used directly; it is only needed when the baseline and topline have neither common **x** nor **y** values. [areaY](#areaY) is used in the common horizontal orientation where the baseline and topline share **x** values, while [areaX](#areaX) is used in the vertical orientation where the baseline and topline share **y** values.
diff --git a/docs/marks/arrow.md b/docs/marks/arrow.md
index 07023ff507..0c94fa4762 100644
--- a/docs/marks/arrow.md
+++ b/docs/marks/arrow.md
@@ -3,14 +3,28 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import metros from "../data/metros.ts";
+import miserables from "../data/miserables.ts";
-const matrix = [[3, 2, 5], [1, 7, 2], [1, 1, 8]];
-const nodes = matrix.map((m, i) => d3.pointRadial(((2 - i) * 2 * Math.PI) / matrix.length, 100));
-const edges = matrix.flatMap((m, i) => m.map((value, j) => ([nodes[i], nodes[j], value])));
+const markov = (() => {
+ const matrix = [[3, 2, 5], [1, 7, 2], [1, 1, 8]];
+ const nodes = matrix.map((m, i) => d3.pointRadial(((2 - i) * 2 * Math.PI) / matrix.length, 100));
+ const edges = matrix.flatMap((m, i) => m.map((value, j) => ([nodes[i], nodes[j], value])));
+ return {nodes, edges};
+})();
+
+function samegroup({source, target}) {
+ source = miserables.groups.get(source);
+ target = miserables.groups.get(target);
+ return source === target ? source : null;
+}
-# Arrow mark
+# Arrow mark
+
+:::tip
+See also the [vector mark](./vector.md), which draws arrows of a given length and direction.
+:::
The **arrow mark** draws arrows between two points [**x1**, **y1**] and [**x2**, **y2**] in quantitative dimensions. It is similar to the [link mark](./link.md), except it draws an arrowhead and is suitable for directed edges. With the **bend** option, it can be swoopy.⤵︎
@@ -67,8 +81,8 @@ Plot.plot({
aspectRatio: 1,
axis: null,
marks: [
- Plot.dot(nodes, {r: 40}),
- Plot.arrow(edges, {
+ Plot.dot(markov.nodes, {r: 40}),
+ Plot.arrow(markov.edges, {
x1: ([[x1]]) => x1,
y1: ([[, y1]]) => y1,
x2: ([, [x2]]) => x2,
@@ -79,8 +93,8 @@ Plot.plot({
headLength: 24,
inset: 48
}),
- Plot.text(nodes, {text: ["A", "B", "C"], dy: 12}),
- Plot.text(edges, {
+ Plot.text(markov.nodes, {text: ["A", "B", "C"], dy: 12}),
+ Plot.text(markov.edges, {
x: ([[x1, y1], [x2, y2]]) => (x1 + x2) / 2 + (y1 - y2) * 0.15,
y: ([[x1, y1], [x2, y2]]) => (y1 + y2) / 2 - (x1 - x2) * 0.15,
text: ([,, value]) => value
@@ -90,7 +104,24 @@ Plot.plot({
```
:::
-See also the [vector mark](./vector.md), which draws arrows of a given length and direction.
+For undirected edges, as in the arc diagram of character co-occurrence in *Les Misérables* below, set the **sweep** option to the desired orientation: *-y* for right-bulging links whose endpoints are vertically separated.
+
+:::plot https://observablehq.com/@observablehq/plot-arc-diagram
+```js
+Plot.plot({
+ height: 1080,
+ marginLeft: 100,
+ axis: null,
+ x: {domain: [0, 1]}, // see https://github.com/observablehq/plot/issues/1541
+ color: {domain: d3.range(10), unknown: "#ccc"},
+ marks: [
+ Plot.dot(miserables.nodes, {x: 0, y: "id", fill: "group", sort: {y: "fill"}}),
+ Plot.text(miserables.nodes, {x: 0, y: "id", text: "id", textAnchor: "end", dx: -6, fill: "group"}),
+ Plot.arrow(miserables.links, {x: 0, y1: "source", y2: "target", sweep: "-y", bend: 90, headLength: 0, stroke: samegroup, sort: samegroup, reverse: true})
+ ]
+})
+```
+:::
## Arrow options
@@ -111,10 +142,13 @@ The arrow mark supports the [standard mark options](../features/marks.md#mark-op
* **insetEnd** - inset at the end of the arrow (useful if the arrow points to a dot)
* **insetStart** - inset at the start of the arrow
* **inset** - shorthand for the two insets
+* **sweep** - the sweep order
The **bend** option sets the angle between the straight line connecting the two points and the outgoing direction of the arrow from the start point. It must be within ±90°. A positive angle will produce a clockwise curve; a negative angle will produce a counterclockwise curve; zero will produce a straight line. The **headAngle** determines how pointy the arrowhead is; it is typically between 0° and 180°. The **headLength** determines the scale of the arrowhead relative to the stroke width. Assuming the default of stroke width 1.5px, the **headLength** is the length of the arrowhead’s side in pixels.
-## arrow(*data*, *options*)
+The **sweep** option controls the bend orientation. It defaults to 1 indicating a positive (clockwise) bend angle; -1 indicates a negative (anticlockwise) bend angle; 0 effectively clears the bend angle. If *-x*, the bend angle is flipped when the ending point is to the left of the starting point — ensuring all arrows bulge up (down if bend is negative); if *-y*, the bend angle is flipped when the ending point is above the starting point — ensuring all arrows bulge right (left if bend is negative); the sign is negated for *+x* and *+y*.
+
+## arrow(*data*, *options*) {#arrow}
```js
Plot.arrow(inequality, {x1: "POP_1980", y1: "R90_10_1980", x2: "POP_2015", y2: "R90_10_2015", bend: true})
diff --git a/docs/marks/auto.md b/docs/marks/auto.md
index 582108a154..640c1e44ae 100644
--- a/docs/marks/auto.md
+++ b/docs/marks/auto.md
@@ -15,9 +15,9 @@ onMounted(() => {
-# Auto mark
+# Auto mark
-The magic ✨ **auto mark** automatically selects a mark type that best represents the given dimensions of the data according to some simple heuristics. The auto mark—which powers [Observable’s chart cell](https://observablehq.com/@observablehq/chart-cell)—is intended to support fast exploratory analysis where the goal is to get a useful plot as quickly as possible. For example, two quantitative dimensions make a scatterplot:
+The magic ✨ **auto mark** automatically selects a mark type that best represents the given dimensions of the data according to some simple heuristics. The auto mark — which powers [Observable’s chart cell](https://observablehq.com/@observablehq/chart-cell) — is intended to support fast exploratory analysis where the goal is to get a useful plot as quickly as possible. For example, two quantitative dimensions make a scatterplot:
:::plot https://observablehq.com/@observablehq/plot-auto-mark-scatterplot
```js
@@ -82,7 +82,7 @@ Plot.rect(olympians, Plot.bin({fill: "count"}, {x: "weight", y: "height"})).plot
Notice that the code above makes you think about nested functions and two different options objects, which the auto mark flattens. The auto mark infers that it should use a [rect](./rect.md); that it should [bin](../transforms/bin.md) on **x** and **y**; that the kind of color should be a **fill**; and that **fill** is an “output” of the reducer, whereas **x** and **y** are “inputs”.
-This saves you a little bit of typing, but, more importantly, it means that switching from showing one dimension to another only involves changing _one thing_. In the code above, if you change **y** from *weight* to *sex*, it’ll break, because *sex* is ordinal instead of quantitative. (You’d also have to change [rect](./rect.md) to [barX](./bar.md#barx-data-options), and [bin](../transforms/bin.md#bin-outputs-options) to [binX](../transforms/bin.md#binx-outputs-options).) With the auto mark, it just works:
+This saves you a little bit of typing, but, more importantly, it means that switching from showing one dimension to another only involves changing _one thing_. In the code above, if you change **y** from *weight* to *sex*, it’ll break, because *sex* is ordinal instead of quantitative. (You’d also have to change [rect](./rect.md) to [barX](./bar.md#barX), and [bin](../transforms/bin.md#bin) to [binX](../transforms/bin.md#binX).) With the auto mark, it just works:
:::plot defer https://observablehq.com/@observablehq/plot-auto-mark-heatmap-2
```js
@@ -90,7 +90,7 @@ Plot.auto(olympians, {x: "weight", y: "sex", color: "count"}).plot()
```
:::
-Similarly, with explicit marks and transforms, changing a vertical histogram to a horizontal histogram involves switching [rectY](./rect.md#recty-data-options) to [rectX](./rect.md#rectx-data-options), [binX](../transforms/bin.md#binx-outputs-options) to [binY](../transforms/bin.md#biny-outputs-options), **x** to **y**, and **y** to **x**. With the auto mark, just specify **y** instead of **x**:
+Similarly, with explicit marks and transforms, changing a vertical histogram to a horizontal histogram involves switching [rectY](./rect.md#rectY) to [rectX](./rect.md#rectX), [binX](../transforms/bin.md#binX) to [binY](../transforms/bin.md#binY), **x** to **y**, and **y** to **x**. With the auto mark, just specify **y** instead of **x**:
:::plot https://observablehq.com/@observablehq/plot-auto-mark-horizontal-histogram
```js
@@ -220,7 +220,7 @@ The auto mark chooses the mark type automatically based on several simple heuris
The chosen mark type depends both on the options you provide (*e.g.*, whether you specified **x** or **y** or both) and the inferred type of the corresponding data values (whether the associated dimension of data is quantitative, categorical, monotonic, *etc.*).
-## auto(*data*, *options*)
+## auto(*data*, *options*) {#auto}
```js
Plot.auto(olympians, {x: "weight", y: "height", color: "count"}) // equivalent to rect + bin, say
@@ -228,7 +228,7 @@ Plot.auto(olympians, {x: "weight", y: "height", color: "count"}) // equivalent t
Returns an automatically-chosen mark with the given *data* and *options*, suitable for a quick view of the data.
-## autoSpec(*data*, *options*)
+## autoSpec(*data*, *options*) {#autoSpec}
```js
Plot.autoSpec(olympians, {x: "weight", y: "height", color: "count"})
diff --git a/docs/marks/axis.md b/docs/marks/axis.md
index 0f9411640c..c792615cea 100644
--- a/docs/marks/axis.md
+++ b/docs/marks/axis.md
@@ -21,7 +21,7 @@ const responses = [
-# Axis mark
+# Axis mark
The **axis mark** conveys the meaning of a position [scale](../features/scales.md): _x_ or _y_, and _fx_ or _fy_ when [faceting](../features/facets.md). Plot automatically adds default axis marks as needed, but you can customize the appearance of axes either through scale options or by explicitly declaring an axis mark.
@@ -143,34 +143,22 @@ Plot.plot({
```
:::
-You can emulate [Datawrapper’s time axes](https://blog.datawrapper.de/new-axis-ticks/) using `\n` (the line feed character) for multi-line tick labels, plus a bit of date math to detect the first month of each year.
+Time axes default to a consistent multi-line tick format , [à la Datawrapper](https://blog.datawrapper.de/new-axis-ticks/), for example showing the first month of each quarter, and the year:
:::plot https://observablehq.com/@observablehq/plot-datawrapper-style-date-axis
```js
Plot.plot({
marks: [
Plot.ruleY([0]),
- Plot.line(aapl, {x: "Date", y: "Close"}),
+ Plot.axisX({ticks: "3 months"}),
Plot.gridX(),
- Plot.axisX({
- ticks: 20,
- tickFormat: (
- (formatYear, formatMonth) => (x) =>
- x.getUTCMonth() === 0
- ? `${formatMonth(x)}\n${formatYear(x)}`
- : formatMonth(x)
- )(d3.utcFormat("%Y"), d3.utcFormat("%b"))
- })
+ Plot.line(aapl, {x: "Date", y: "Close"})
]
})
```
:::
-:::tip
-In the future, Plot may generate multi-line time axis labels by default. If you’re interested in this feature, please upvote [#1285](https://github.com/observablehq/plot/issues/1285).
-:::
-
-Alternatively, you can add multiple axes with options for hierarchical time intervals, here showing weeks, months, and years.
+The format is inferred from the tick interval, and consists of two fields (*e.g.*, month and year, day and month, minutes and hours); when a tick has the same second field value as the previous tick (*e.g.*, “19 Jan” after “17 Jan”), only the first field (“19”) is shown for brevity. Alternatively, you can specify multiple explicit axes with options for hierarchical time intervals, here showing weeks, months, and years.
:::plot https://observablehq.com/@observablehq/plot-multiscale-date-axis
```js
@@ -357,7 +345,7 @@ In addition to the [standard mark options](../features/marks.md), the axis mark
* **fontVariant** - the ticks’ font-variant; defaults to *tabular-nums* for quantitative axes
* **label** - a string to label the axis; defaults to the scale’s label, perhaps with an arrow
* **labelAnchor** - the label anchor: *top*, *right*, *bottom*, *left*, or *center*
-* **labelArrow** - the label arrow: *auto* (default), *up*, *right*, *down*, *left*, *none*, or true
+* **labelArrow** - the label arrow: *auto* (default), *up*, *right*, *down*, *left*, *none*, or true
* **labelOffset** - the label position offset (in pixels; default depends on margins and orientation)
* **color** - the color of the ticks and labels (defaults to *currentColor*)
* **textStroke** - the color of the stroke around tick labels (defaults to *none*)
@@ -379,7 +367,7 @@ The axis mark’s default margins depend on its orientation (**anchor**) as foll
For simplicity’s sake and for consistent layout across plots, axis margins are not automatically sized to make room for tick labels; instead, shorten your tick labels (for example using the *k* SI-prefix tick format, or setting a *scale*.transform to show thousands or millions, or setting the **textOverflow** option to *ellipsis* and the **lineWidth** option to clip long labels) or increase the margins as needed.
-## axisX(*data*, *options*)
+## axisX(*data*, *options*) {#axisX}
```js
Plot.axisX({anchor: "bottom", tickSpacing: 80})
@@ -387,7 +375,7 @@ Plot.axisX({anchor: "bottom", tickSpacing: 80})
Returns a new *x* axis with the given *options*.
-## axisY(*data*, *options*)
+## axisY(*data*, *options*) {#axisY}
```js
Plot.axisY({anchor: "left", tickSpacing: 35})
@@ -395,7 +383,7 @@ Plot.axisY({anchor: "left", tickSpacing: 35})
Returns a new *y* axis with the given *options*.
-## axisFx(*data*, *options*)
+## axisFx(*data*, *options*) {#axisFx}
```js
Plot.axisFx({anchor: "top", label: null})
@@ -403,7 +391,7 @@ Plot.axisFx({anchor: "top", label: null})
Returns a new *fx* axis with the given *options*.
-## axisFy(*data*, *options*)
+## axisFy(*data*, *options*) {#axisFy}
```js
Plot.axisFy({anchor: "right", label: null})
diff --git a/docs/marks/bar.md b/docs/marks/bar.md
index 5ca6773851..9516c3b13b 100644
--- a/docs/marks/bar.md
+++ b/docs/marks/bar.md
@@ -29,7 +29,7 @@ const timeseries = [
The bar mark is one of several marks in Plot for drawing rectangles; it should be used when one dimension is ordinal and the other is quantitative. See also [rect](./rect.md) and [cell](./cell.md).
:::
-The **bar mark** comes in two orientations: [barY](#bary-data-options) extends vertically↑ as in a vertical bar chart or column chart, while [barX](#barx-data-options) extends horizontally→. For example, the bar chart below shows the frequency of letters in the English language.
+The **bar mark** comes in two orientations: [barY](#barY) extends vertically↑ as in a vertical bar chart or column chart, while [barX](#barX) extends horizontally→. For example, the bar chart below shows the frequency of letters in the English language.
:::plot https://observablehq.com/@observablehq/plot-vertical-bars
```js
@@ -134,7 +134,7 @@ Plot.plot({
The **percent** scale option is useful for showing percentages; it applies a [scale transform](../features/scales.md#scale-transforms) that multiplies associated channel values by 100.
:::
-When ordinal data is regular, such as the yearly observations of the time-series bar chart of world population below, use the **interval** option to enforce uniformity and show gaps for missing data. It can be set to a named interval such as *hour* or *day*, a number for numeric intervals, a [d3-time interval](https://github.com/d3/d3-time/blob/main/README.md#api-reference), or a custom implementation.
+When ordinal data is regular, such as the yearly observations of the time-series bar chart of world population below, use the **interval** option to enforce uniformity and show gaps for missing data. It can be set to a named interval such as *hour* or *day*, a number for numeric intervals, a [d3-time interval](https://d3js.org/d3-time#_interval), or a custom implementation.
@@ -213,9 +213,9 @@ Plot.plot({
## Bar options
-For required channels, see [barX](#barx-data-options) and [barY](#bary-data-options). 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 and 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(*data*, *options*) {#barX}
```js
Plot.barX(alphabet, {y: "letter", x: "frequency"})
@@ -232,9 +232,9 @@ The following optional channels are supported:
If neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX transform](../transforms/stack.md); this is the typical configuration for a horizontal bar chart with bars aligned at *x* = 0. If the **x** option is not specified, it defaults to [identity](../features/transforms.md#identity). If *options* is undefined, then it defaults to **x2** as identity and **y** as the zero-based index [0, 1, 2, …]; this allows an array of numbers to be passed to barX to make a quick sequential bar chart. If the **y** channel is not specified, the bar will span the full vertical extent of the plot (or facet).
-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*. 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#scale-options).
+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*. 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).
-## barY(*data*, *options*)
+## barY(*data*, *options*) {#barY}
```js
Plot.barY(alphabet, {x: "letter", y: "frequency"})
@@ -251,4 +251,4 @@ The following optional channels are supported:
If neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY transform](../transforms/stack.md); this is the typical configuration for a vertical bar chart with bars aligned at *y* = 0. If the **y** option is not specified, it defaults to [identity](../features/transforms.md#identity). If *options* is undefined, then it defaults to **y2** as identity and **x** as the zero-based index [0, 1, 2, …]; this allows an array of numbers to be passed to barY to make a quick sequential bar chart. If the **x** channel is not specified, the bar will span the full horizontal extent of the plot (or facet).
-If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
diff --git a/docs/marks/bollinger.md b/docs/marks/bollinger.md
new file mode 100644
index 0000000000..949ba8cb44
--- /dev/null
+++ b/docs/marks/bollinger.md
@@ -0,0 +1,132 @@
+
+
+# Bollinger mark
+
+The **bollinger mark** is a [composite mark](../features/marks.md#marks) consisting of a [line](./line.md) representing a moving average and an [area](./area.md) representing volatility as a band; the band thickness is proportional to the deviation of nearby values. The bollinger mark is often used to analyze the price of financial instruments such as stocks.
+
+For example, the chart below shows the price of Apple stock from 2013 to 2018, with a window size *n* of {{n}} days and radius *k* of {{k}} standard deviations.
+
+
+
+ Window size (n):
+
+ {{n.toLocaleString("en-US")}}
+
+
+ Radius (k):
+
+ {{k.toLocaleString("en-US")}}
+
+
+
+:::plot hidden
+```js
+Plot.bollingerY(aapl, {x: "Date", y: "Close", n, k}).plot()
+```
+:::
+
+```js-vue
+Plot.bollingerY(aapl, {x: "Date", y: "Close", n: {{n}}, k: {{k}}}).plot()
+```
+
+For more control, you can also use the [bollinger map method](#bollinger) directly with the [map transform](../transforms/map.md).
+
+:::plot
+```js
+Plot.plot({
+ marks: [
+ Plot.lineY(aapl, Plot.mapY(Plot.bollinger({n: 20, k: -2}), {x: "Date", y: "Close", stroke: "red"})),
+ Plot.lineY(aapl, Plot.mapY(Plot.bollinger({n: 20, k: 2}), {x: "Date", y: "Close", stroke: "green"})),
+ Plot.lineY(aapl, {x: "Date", y: "Close"})
+ ]
+})
+```
+:::
+
+Below a candlestick chart is constructed from two [rule marks](./rule.md), with a bollinger mark underneath to emphasize the days when the stock was more volatile.
+
+:::plot
+```js
+Plot.plot({
+ x: {domain: [new Date("2014-01-01"), new Date("2014-06-01")]},
+ y: {domain: [68, 92], grid: true},
+ color: {domain: [-1, 0, 1], range: ["red", "black", "green"]},
+ marks: [
+ Plot.bollingerY(aapl, {x: "Date", y: "Close", stroke: "none", clip: true}),
+ Plot.ruleX(aapl, {x: "Date", y1: "Low", y2: "High", strokeWidth: 1, clip: true}),
+ Plot.ruleX(aapl, {x: "Date", y1: "Open", y2: "Close", strokeWidth: 3, stroke: (d) => Math.sign(d.Close - d.Open), clip: true})
+ ]
+})
+```
+:::
+
+The bollinger mark has two constructors: the common [bollingerY](#bollingerY) for when time goes right→ (or ←left); and the rare [bollingerX](#bollingerX) for when time goes up↑ (or down↓).
+
+:::plot
+```js
+Plot.bollingerX(aapl, {y: "Date", x: "Close"}).plot()
+```
+:::
+
+As [shorthand](../features/shorthand.md), you can pass an array of numbers as data. Below, the *x* axis represents the zero-based index into the data (*i.e.*, trading days since May 13, 2013).
+
+:::plot
+```js
+Plot.bollingerY(aapl.map((d) => d.Close)).plot()
+```
+:::
+
+## Bollinger options
+
+The bollinger mark is a [composite mark](../features/marks.md#marks) consisting of two marks:
+
+* an [area](../marks/area.md) representing volatility as a band, and
+* a [line](../marks/line.md) representing a moving average
+
+The bollinger mark supports the following special options:
+
+* **n** - the window size (the window transform’s **k** option), an integer; defaults to 20
+* **k** - the band radius, a number representing a multiple of standard deviations; defaults to 2
+* **color** - the fill color of the area, and the stroke color of the line; defaults to *currentColor*
+* **opacity** - the fill opacity of the area; defaults to 0.2
+* **fill** - the fill color of the area; defaults to **color**
+* **fillOpacity** - the fill opacity of the area; defaults to **opacity**
+* **stroke** - the stroke color of the line; defaults to **color**
+* **strokeOpacity** - the stroke opacity of the line; defaults to 1
+* **strokeWidth** - the stroke width of the line in pixels; defaults to 1.5
+
+Any additional options are passed through to the underlying [line mark](./line.md), [area mark](./area.md), and [window transform](../transforms/window.md). Unlike the window transform, the **strict** option defaults to true, and the **anchor** option defaults to *end* (which assumes that the data is in chronological order).
+
+## bollingerX(*data*, *options*) {#bollingerX}
+
+```js
+Plot.bollingerX(aapl, {y: "Date", x: "Close"})
+```
+
+Returns a bollinger mark for when time goes up↑ (or down↓). If the **x** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*x₀*, *x₁*, *x₂*, …]. If the **y** option is not specified, it defaults to [0, 1, 2, …].
+
+## bollingerY(*data*, *options*) {#bollingerY}
+
+```js
+Plot.bollingerY(aapl, {x: "Date", y: "Close"})
+```
+
+Returns a bollinger mark for when time goes right→ (or ←left). If the **y** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*y₀*, *y₁*, *y₂*, …]. If the **x** option is not specified, it defaults to [0, 1, 2, …].
+
+## bollinger(*options*) {#bollinger}
+
+```js
+Plot.lineY(data, Plot.map({y: Plot.bollinger({n: 20})}, {x: "Date", y: "Close"}))
+```
+
+Returns a bollinger map method for use with the [map transform](../transforms/map.md). The **k** option here defaults to zero instead of two.
diff --git a/docs/marks/box.md b/docs/marks/box.md
index 31972b0e67..a4bdfa5970 100644
--- a/docs/marks/box.md
+++ b/docs/marks/box.md
@@ -13,9 +13,9 @@ onMounted(() => {
-# Box mark
+# Box mark
-The **box mark** summarizes one-dimensional distributions as boxplots. It is a [composite mark](../features/marks.md#marks-marks) consisting of a [rule](./rule.md) to represent the extreme values (not including outliers), a [bar](./bar.md) to represent the interquartile range (trimmed to the data), a [tick](./tick.md) to represent the median value, and a [dot](./dot.md) to represent any outliers. The [group transform](../transforms/group.md) is used to group and aggregate data.
+The **box mark** summarizes one-dimensional distributions as boxplots. It is a [composite mark](../features/marks.md#marks) consisting of a [rule](./rule.md) to represent the extreme values (not including outliers), a [bar](./bar.md) to represent the interquartile range (trimmed to the data), a [tick](./tick.md) to represent the median value, and a [dot](./dot.md) to represent any outliers. The [group transform](../transforms/group.md) is used to group and aggregate data.
For example, the boxplot below shows [A.A. Michelson’s experimental measurements](https://stat.ethz.ch/R-manual/R-devel/library/datasets/html/morley.html) of the speed of light. (Speed is in km/sec minus 299,000.)
@@ -33,7 +33,7 @@ Plot.plot({
```
:::
-[boxY](#boxy-data-options) produces vertical boxplots; for horizontal boxplots, use [boxX](#boxx-data-options) and swap **x** and **y**.
+[boxY](#boxY) produces vertical boxplots; for horizontal boxplots, use [boxX](#boxX) and swap **x** and **y**.
:::plot https://observablehq.com/@observablehq/plot-horizontal-box-plot
```js
@@ -107,7 +107,7 @@ Plot.plot({
## Box options
-The box mark is a [composite mark](../features/marks.md#marks-marks) consisting of four marks:
+The box mark is a [composite mark](../features/marks.md#marks) consisting of four marks:
* a [rule](../marks/rule.md) representing the extreme values (not including outliers)
* a [bar](../marks/bar.md) representing the interquartile range (trimmed to the data)
@@ -122,7 +122,7 @@ The given *options* are passed through to these underlying marks, with the excep
* **strokeOpacity** - the stroke opacity of the rule, tick, and dot; defaults to 1
* **strokeWidth** - the stroke width of the tick; defaults to 1
-## boxX(*data*, *options*)
+## boxX(*data*, *options*) {#boxX}
```js
Plot.boxX(simpsons.map((d) => d.imdb_rating))
@@ -130,7 +130,7 @@ Plot.boxX(simpsons.map((d) => d.imdb_rating))
Returns a horizontal box mark. If the **x** option is not specified, it defaults to the identity function, as when *data* is an array of numbers. If the **y** option is not specified, it defaults to null; if the **y** option is specified, it should represent an ordinal (discrete) value.
-## boxY(*data*, *options*)
+## boxY(*data*, *options*) {#boxY}
```js
Plot.boxY(simpsons.map((d) => d.imdb_rating))
diff --git a/docs/marks/cell.md b/docs/marks/cell.md
index 47481ba0b4..8a8d19b0dd 100644
--- a/docs/marks/cell.md
+++ b/docs/marks/cell.md
@@ -26,7 +26,7 @@ The cell mark is one of several marks in Plot for drawing rectangles; it should
The **cell mark** draws rectangles positioned in two ordinal dimensions. Hence, the plot’s *x* and *y* scales are [band scales](../features/scales.md). Cells typically also have a **fill** color encoding.
-For example, the heatmap below shows the decline of *The Simpsons* after Season 9: high IMDb ratings are dark green, while low ratings are dark pink. (The worst episode ever—cue Comic Book Guy—is season 23’s [“Lisa Goes Gaga”](https://en.wikipedia.org/wiki/Lisa_Goes_Gaga).)
+For example, the heatmap below shows the decline of *The Simpsons* after Season 9: high IMDb ratings are dark green, while low ratings are dark pink. (The worst episode ever — cue Comic Book Guy — is season 23’s [“Lisa Goes Gaga”](https://en.wikipedia.org/wiki/Lisa_Goes_Gaga).)
:::plot defer https://observablehq.com/@observablehq/plot-simpsons-ratings
```js
@@ -122,7 +122,7 @@ Plot.cell(alphabet, {x: "letter", fill: "frequency"}).plot()
```
:::
-When ordinal data is regular, such as the yearly observations of the warming stripes below, use the **interval** scale option to enforce uniformity and show gaps for missing data. It can be set to a named interval such as *hour* or *day*, a number for numeric intervals, a [d3-time interval](https://github.com/d3/d3-time/blob/main/README.md#api-reference), or a custom implementation.
+When ordinal data is regular, such as the yearly observations of the warming stripes below, use the **interval** scale option to enforce uniformity and show gaps for missing data. It can be set to a named interval such as *hour* or *day*, a number for numeric intervals, a [d3-time interval](https://d3js.org/d3-time#_interval), or a custom implementation.
:::plot https://observablehq.com/@observablehq/plot-ordinal-scale-interval-2
```js{5}
@@ -158,7 +158,7 @@ If **x** is not specified, the cell will span the full horizontal extent of the
The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
-## cell(*data*, *options*)
+## cell(*data*, *options*) {#cell}
```js
Plot.cell(simpsons, {x: "number_in_season", y: "season", fill: "imdb_rating"})
@@ -166,18 +166,18 @@ Plot.cell(simpsons, {x: "number_in_season", y: "season", fill: "imdb_rating"})
Returns a new cell with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
-## cellX(*data*, *options*)
+## cellX(*data*, *options*) {#cellX}
```js
Plot.cellX(simpsons.map((d) => d.imdb_rating))
```
-Equivalent to [cell](#cell-data-options), except that if the **x** option is not specified, it defaults to [0, 1, 2, …], and if the **fill** option is not specified and **stroke** is not a channel, the fill defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
+Equivalent to [cell](#cell), except that if the **x** option is not specified, it defaults to [0, 1, 2, …], and if the **fill** option is not specified and **stroke** is not a channel, the fill defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
-## cellY(*data*, *options*)
+## cellY(*data*, *options*) {#cellY}
```js
Plot.cellY(simpsons.map((d) => d.imdb_rating))
```
-Equivalent to [cell](#cell-data-options), except that if the **y** option is not specified, it defaults to [0, 1, 2, …], and if the **fill** option is not specified and **stroke** is not a channel, the fill defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
+Equivalent to [cell](#cell), except that if the **y** option is not specified, it defaults to [0, 1, 2, …], and if the **fill** option is not specified and **stroke** is not a channel, the fill defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
diff --git a/docs/marks/contour.md b/docs/marks/contour.md
index ec15e50624..234e3ae39e 100644
--- a/docs/marks/contour.md
+++ b/docs/marks/contour.md
@@ -23,7 +23,7 @@ function mandelbrot(x, y) {
-# Contour mark
+# Contour mark
:::tip
To produce a heatmap instead of contours, see the [raster mark](./raster.md). For contours of estimated point density, see the [density mark](./density.md).
@@ -271,7 +271,7 @@ As shorthand, a single channel may be specified, in which case it is promoted to
Plot.contour(volcano.values, {width: volcano.width, height: volcano.height, fill: Plot.identity})
```
-## contour(*data*, *options*)
+## contour(*data*, *options*) {#contour}
```js
Plot.contour(volcano.values, {width: volcano.width, height: volcano.height, fill: Plot.identity})
diff --git a/docs/marks/delaunay.md b/docs/marks/delaunay.md
index 8ef48883b0..ff1b4e7c26 100644
--- a/docs/marks/delaunay.md
+++ b/docs/marks/delaunay.md
@@ -17,11 +17,11 @@ onMounted(() => {
-# Delaunay marks
+# Delaunay marks
Given set of points in **x** and **y**, the **Delaunay marks** compute the [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation), its dual the [Voronoi tessellation](https://en.wikipedia.org/wiki/Voronoi_diagram), and the [convex hull](https://en.wikipedia.org/wiki/Convex_hull).
-The [voronoi mark](#voronoi-data-options) computes the region closest to each point (its *Voronoi cell*). The cell can be empty if another point shares the exact same coordinates. Together, the cells cover the entire plot. Voronoi diagrams can group related points with color, for example.
+The [voronoi mark](#voronoi) computes the region closest to each point (its *Voronoi cell*). The cell can be empty if another point shares the exact same coordinates. Together, the cells cover the entire plot. Voronoi diagrams can group related points with color, for example.
:::plot https://observablehq.com/@observablehq/plot-voronoi-scatterplot
```js
@@ -38,7 +38,7 @@ Plot.plot({
Each cell is associated with a particular data point, and channels such as **stroke**, **fill**, **fillOpacity**, **strokeOpacity**, **href**, _etc._, work as they do on other marks, such as [dots](./dot.md).
-To show the local density of a scatterplot, one can draw the whole boundary at once with [voronoiMesh](#voronoimesh-data-options). Whereas the [voronoi mark](#voronoi-data-options) will draw shared cell boundaries twice, the mesh will draw them only once.
+To show the local density of a scatterplot, one can draw the whole boundary at once with [voronoiMesh](#voronoiMesh). Whereas the [voronoi mark](#voronoi) will draw shared cell boundaries twice, the mesh will draw them only once.
:::plot https://observablehq.com/@observablehq/plot-voronoi-mesh
```js
@@ -51,7 +51,7 @@ Plot.plot({
```
:::
-The boundary between two neighboring Voronoi cells is a line segment defined by equal distance from their two respective points. The construction of the Voronoi diagram involves the computation of the Delaunay graph, which defines these neighbors. Use [delaunayMesh](#delaunaymesh-data-options) to draw the graph.
+The boundary between two neighboring Voronoi cells is a line segment defined by equal distance from their two respective points. The construction of the Voronoi diagram involves the computation of the Delaunay graph, which defines these neighbors. Use [delaunayMesh](#delaunayMesh) to draw the graph.
:::plot https://observablehq.com/@observablehq/plot-delaunay-mesh
```js
@@ -66,7 +66,7 @@ Plot.plot({
As shown above, the Delaunay graph is computed separately for each color; specifying **z**, **stroke**, or **fill** creates independent series.
-Another derivative of the Delaunay graph is the convex hull of a set of points: the polygon with the minimum perimeter that contains all the points. The [hull mark](#hull-data-options) will draw this hull.
+Another derivative of the Delaunay graph is the convex hull of a set of points: the polygon with the minimum perimeter that contains all the points. The [hull mark](#hull) will draw this hull.
:::plot defer https://observablehq.com/@observablehq/plot-convex-hull
```js
@@ -129,7 +129,7 @@ Distances between projected points are not exactly proportional to the correspon
:::
-## delaunayLink(*data*, *options*)
+## delaunayLink(*data*, *options*) {#delaunayLink}
```js
Plot.delaunayLink(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
@@ -139,7 +139,7 @@ Draws links for each edge of the Delaunay triangulation of the points given by t
If a **z** channel is specified, the input points are grouped by *z*, and separate Delaunay triangulations are constructed for each group.
-## delaunayMesh(*data*, *options*)
+## delaunayMesh(*data*, *options*) {#delaunayMesh}
```js
Plot.delaunayMesh(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
@@ -149,7 +149,7 @@ Draws a mesh of the Delaunay triangulation of the points given by the **x** and
If a **z** channel is specified, the input points are grouped by *z*, and separate Delaunay triangulations are constructed for each group.
-## hull(*data*, *options*)
+## hull(*data*, *options*) {#hull}
```js
Plot.hull(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
@@ -159,7 +159,7 @@ Draws a convex hull around the points given by the **x** and **y** channels. The
If a **z** channel is specified, the input points are grouped by *z*, and separate convex hulls are constructed for each group. If the **z** channel is not specified, it defaults to either the **fill** channel, if any, or the **stroke** channel, if any.
-## voronoi(*data*, *options*)
+## voronoi(*data*, *options*) {#voronoi}
```js
Plot.voronoi(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
@@ -169,7 +169,7 @@ Draws polygons for each cell of the Voronoi tessellation of the points given by
If a **z** channel is specified, the input points are grouped by *z*, and separate Voronoi tessellations are constructed for each group.
-## voronoiMesh(*data*, *options*)
+## voronoiMesh(*data*, *options*) {#voronoiMesh}
```js
Plot.voronoiMesh(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
diff --git a/docs/marks/density.md b/docs/marks/density.md
index 0358e7832f..45a8b40218 100644
--- a/docs/marks/density.md
+++ b/docs/marks/density.md
@@ -24,7 +24,7 @@ onMounted(() => {
-# Density mark
+# Density mark
:::tip
For contours of spatially-distributed quantitative values, see the [contour mark](./contour.md).
@@ -165,7 +165,7 @@ Plot.plot({
```
:::
-
+
The **weight** channel specifies the contribution of each data point to the estimated density; it defaults to 1, weighing each point equally. This can be used to give some points more influence than others. Try adjusting the skew slider below to transition between female- and male-weighted density.
@@ -245,7 +245,7 @@ The **thresholds** option, which defaults to 20, specifies one more than the num
If a **z**, **stroke** or **fill** channel is specified, the input points are grouped by series, and separate sets of contours are generated for each series. If the **stroke** or **fill** is specified as *density*, a color channel is constructed with values representing the density threshold value of each contour.
-## density(*data*, *options*)
+## density(*data*, *options*) {#density}
```js
Plot.density(faithful, {x: "waiting", y: "eruptions"})
diff --git a/docs/marks/dot.md b/docs/marks/dot.md
index 10da569beb..f82ddef678 100644
--- a/docs/marks/dot.md
+++ b/docs/marks/dot.md
@@ -241,7 +241,7 @@ Plot.dotX([
:::
:::info
-The stroked symbols are based on [Heman Robinson’s research](https://www.tandfonline.com/doi/abs/10.1080/10618600.2019.1637746). There is also a *hexagon* symbol; it is primarily intended for the [hexbin transform](../transforms/hexbin.md). You can even specify a D3 or custom symbol type as an object that implements the [*symbol*.draw(*context*, *size*)](https://github.com/d3/d3-shape/blob/main/README.md#custom-symbol-types) method.
+The stroked symbols are based on [Heman Robinson’s research](https://www.tandfonline.com/doi/abs/10.1080/10618600.2019.1637746). There is also a *hexagon* symbol; it is primarily intended for the [hexbin transform](../transforms/hexbin.md). You can even specify a D3 or custom symbol type as an object that implements the [*symbol*.draw(*context*, *size*)](https://d3js.org/d3-shape/symbol#symbolType_draw) method.
:::
The dot mark can be combined with the [stack transform](../transforms/stack.md). The diverging stacked dot plot below shows the age and gender distribution of the U.S. Congress in 2023.
@@ -281,7 +281,7 @@ The stackY2 transform places each dot at the upper bound of the associated stack
The [dodge transform](../transforms/dodge.md) can also be used to produce beeswarm plots; this is particularly effective when dots have varying radius.
:::
-Dots are sorted by descending radius by default to mitigate occlusion; the smallest dots are drawn on top. Set the **sort** option to null to draw them in input order. Use the checkbox below to see the effect of sorting on a bubble map of U.S. county population.
+Dots are sorted by descending radius by default to mitigate occlusion; the smallest dots are drawn on top. Set the **sort** option to null to draw them in input order. Use the checkbox below to see the effect of sorting on a bubble map of U.S. county population.
@@ -318,7 +318,7 @@ In addition to the [standard mark options](../features/marks.md#mark-options), t
* **y** - the vertical position; bound to the *y* scale
* **r** - the radius (area); bound to the *r* (radius) scale, which defaults to *sqrt*
* **rotate** - the rotation angle in degrees clockwise
-* **symbol** - the categorical symbol; bound to the *symbol* scale
+* **symbol** - the categorical symbol; bound to the *symbol* scale
If either of the **x** or **y** channels are not specified, the corresponding position is controlled by the **frameAnchor** option.
@@ -326,14 +326,14 @@ The following dot-specific constant options are also supported:
* **r** - the effective radius (length); a number in pixels
* **rotate** - the rotation angle in degrees clockwise; defaults to 0
-* **symbol** - the categorical symbol; defaults to circle
+* **symbol** - the categorical symbol; defaults to *circle*
* **frameAnchor** - how to position the dot within the frame; defaults to *middle*
The **r** option can be specified as either a channel or constant. When the radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The radius defaults to 4.5 pixels when using the **symbol** channel, and otherwise 3 pixels. Dots with a nonpositive radius are not drawn.
The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise. The **strokeWidth** defaults to 1.5. The **rotate** and **symbol** options can be specified as either channels or constants. When rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. When symbol is a valid symbol name or symbol object (implementing the draw method), it is interpreted as a constant; otherwise it is interpreted as a channel. If the **symbol** channel’s values are all symbols, symbol names, or nullish, the channel is unscaled (values are interpreted literally); otherwise, the channel is bound to the *symbol* scale.
-## dot(*data*, *options*)
+## dot(*data*, *options*) {#dot}
```js
Plot.dot(sales, {x: "units", y: "fruit"})
@@ -341,30 +341,30 @@ Plot.dot(sales, {x: "units", y: "fruit"})
Returns a new dot with the given *data* and *options*. If neither the **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
-## dotX(*data*, *options*)
+## dotX(*data*, *options*) {#dotX}
```js
Plot.dotX(cars.map((d) => d["economy (mpg)"]))
```
-Equivalent to [dot](#dot-data-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
+Equivalent to [dot](#dot) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
-If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
-## dotY(*data*, *options*)
+## dotY(*data*, *options*) {#dotY}
```js
Plot.dotY(cars.map((d) => d["economy (mpg)"]))
```
-Equivalent to [dot](#dot-data-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
+Equivalent to [dot](#dot) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
-If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of 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).
-## circle(*data*, *options*)
+## circle(*data*, *options*) {#circle}
-Equivalent to [dot](#dot-data-options) except that the **symbol** option is set to *circle*.
+Equivalent to [dot](#dot) except that the **symbol** option is set to *circle*.
-## hexagon(*data*, *options*)
+## hexagon(*data*, *options*) {#hexagon}
-Equivalent to [dot](#dot-data-options) except that the **symbol** option is set to *hexagon*.
+Equivalent to [dot](#dot) except that the **symbol** option is set to *hexagon*.
diff --git a/docs/marks/frame.md b/docs/marks/frame.md
index 386839fd44..ffc6449ecc 100644
--- a/docs/marks/frame.md
+++ b/docs/marks/frame.md
@@ -48,7 +48,7 @@ Plot.plot({
```
:::
-Unlike most marks, a frame never takes *data*; the first argument to [frame](#frame-options-1) is the *options* object. (For data-driven rectangles, see the [rect mark](./rect.md).)
+Unlike most marks, a frame never takes *data*; the first argument to [frame](#frame) is the *options* object. (For data-driven rectangles, see the [rect mark](./rect.md).)
:::plot
```js
@@ -93,7 +93,7 @@ Plot.plot({
Or: `Plot.rect({length: 1}, {fy: ["Gentoo"], stroke: "currentColor"})`.
:::
-The **anchor** option, if specified to a value of *left*, *right*, *top* or *bottom*, draws only that side of the frame. In that case, the **fill** and **rx**, **ry** options are ignored.
+The **anchor** option , if specified to a value of *left*, *right*, *top* or *bottom*, draws only that side of the frame. In that case, the **fill** and **rx**, **ry** options are ignored.
:::plot
```js
@@ -115,7 +115,7 @@ The frame mark supports the [standard mark options](../features/marks.md#mark-op
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).
-## frame(*options*)
+## frame(*options*) {#frame}
```js
Plot.frame({stroke: "red"})
diff --git a/docs/marks/geo.md b/docs/marks/geo.md
index 43d220b9dc..0d35629d6e 100644
--- a/docs/marks/geo.md
+++ b/docs/marks/geo.md
@@ -11,6 +11,7 @@ const walmarts = shallowRef({type: "FeatureCollection", features: []});
const world = shallowRef(null);
const statemesh = computed(() => us.value ? topojson.mesh(us.value, us.value.objects.states, (a, b) => a !== b) : {type: null});
const nation = computed(() => us.value ? topojson.feature(us.value, us.value.objects.nation) : {type: null});
+const states = computed(() => us.value ? topojson.feature(us.value, us.value.objects.states).features : []);
const counties = computed(() => us.value ? topojson.feature(us.value, us.value.objects.counties).features : []);
const land = computed(() => world.value ? topojson.feature(world.value, world.value.objects.land) : {type: null});
@@ -30,9 +31,9 @@ onMounted(() => {
-# Geo mark
+# Geo mark
-The **geo mark** draws geographic features—polygons, lines, points, and other geometry—often as thematic maps. It works with Plot’s [projection system](../features/projections.md). For example, the [choropleth map](https://en.wikipedia.org/wiki/Choropleth_map) below shows unemployment by county in the United States.
+The **geo mark** draws geographic features — polygons, lines, points, and other geometry — often as thematic maps. It works with Plot’s [projection system](../features/projections.md). For example, the [choropleth map](https://en.wikipedia.org/wiki/Choropleth_map) below shows unemployment by county in the United States.
:::plot defer https://observablehq.com/@observablehq/plot-us-choropleth
```js
@@ -85,7 +86,7 @@ Plot.plot({
Click on any of the earthquakes above to see details.
:::
-The [graticule](#graticule-options) helper draws a uniform grid of meridians (lines of constant longitude) and parallels (lines of constant latitude) every 10° between ±80° latitude; for the polar regions, meridians are drawn every 90°. The [sphere](#sphere-options) helper draws the outline of the projected sphere.
+The [graticule](#graticule) helper draws a uniform grid of meridians (lines of constant longitude) and parallels (lines of constant latitude) every 10° between ±80° latitude; for the polar regions, meridians are drawn every 90°. The [sphere](#sphere) helper draws the outline of the projected sphere.
:::plot https://observablehq.com/@observablehq/plot-sphere-and-graticule
```js
@@ -128,6 +129,22 @@ Plot.plot({
```
:::
+The geo mark doesn’t have **x** and **y** channels; to derive those, for example to add [interactive tips](./tip.md), you can apply a [centroid transform](../transforms/centroid.md) on the geometries.
+
+:::plot defer https://observablehq.com/@observablehq/plot-state-centroids
+```js
+Plot.plot({
+ projection: "albers-usa",
+ marks: [
+ Plot.geo(statemesh, {strokeOpacity: 0.2}),
+ Plot.geo(nation),
+ Plot.dot(states, Plot.centroid({fill: "red", stroke: "var(--vp-c-bg-alt)"})),
+ Plot.tip(states, Plot.pointer(Plot.centroid({title: (d) => d.properties.name})))
+ ]
+})
+```
+:::
+
The geo mark supports [faceting](../features/facets.md). Below, a comic strip of sorts shows the locations of Walmart store openings in past decades.
:::plot defer https://observablehq.com/@observablehq/plot-map-large-multiples
@@ -151,7 +168,7 @@ Plot.plot({
This uses the [**interval** scale option](../features/scales.md#scale-transforms) to bin temporal data into facets by decade.
:::
-Lastly, the geo mark is not limited to spherical geometries! [Plot’s projection system](../features/projections.md) includes planar projections, which allow you to work with shapes—such as contours—generated on an arbitrary flat surface.
+Lastly, the geo mark is not limited to spherical geometries! [Plot’s projection system](../features/projections.md) includes planar projections, which allow you to work with shapes — such as contours — generated on an arbitrary flat surface.
## Geo options
@@ -159,7 +176,7 @@ The **geometry** channel specifies the geometry (GeoJSON object) to draw; if not
In addition to the [standard mark options](../features/marks.md#mark-options), the **r** option controls the size of Point and MultiPoint geometries. It can be specified as either a channel or constant. When **r** is specified as a number, it is interpreted as a constant radius in pixels; otherwise it is interpreted as a channel and the effective radius is controlled by the *r* scale. If the **r** option is not specified it defaults to 3 pixels. Geometries with a nonpositive radius are not drawn. If **r** is a channel, geometries will be sorted by descending radius by default.
-## geo(*data*, *options*)
+## geo(*data*, *options*) {#geo}
```js
Plot.geo(counties, {fill: (d) => d.properties.rate})
@@ -167,7 +184,7 @@ Plot.geo(counties, {fill: (d) => d.properties.rate})
Returns a new geo mark with the given *data* and *options*. If *data* is a GeoJSON feature collection, then the mark’s data is *data*.features; if *data* is a GeoJSON geometry collection, then the mark’s data is *data*.geometries; if *data* is some other GeoJSON object, then the mark’s data is the single-element array [*data*]. If the **geometry** option is not specified, *data* is assumed to be a GeoJSON object or an iterable of GeoJSON objects.
-## sphere(*options*)
+## sphere(*options*) {#sphere}
```js
Plot.sphere()
@@ -175,10 +192,10 @@ Plot.sphere()
Returns a new geo mark with a *Sphere* geometry object and the given *options*.
-## graticule(*options*)
+## graticule(*options*) {#graticule}
```js
Plot.graticule()
```
-Returns a new geo mark with a [10° global graticule](https://github.com/d3/d3-geo/blob/main/README.md#geoGraticule10) geometry object and the given *options*.
+Returns a new geo mark with a [10° global graticule](https://d3js.org/d3-geo/shape#geoGraticule10) geometry object and the given *options*.
diff --git a/docs/marks/grid.md b/docs/marks/grid.md
index fad2456c16..d9d20180da 100644
--- a/docs/marks/grid.md
+++ b/docs/marks/grid.md
@@ -9,7 +9,7 @@ const atop = ref(true);
-# Grid mark
+# Grid mark
The **grid mark** is a specially-configured [rule](./rule.md) for drawing an axis-aligned grid. Like the [axis mark](./axis.md), a grid mark is automatically generated by Plot when you use the **grid** scale option. But you can also declare a grid mark explicitly, for example to draw grid lines atop rather than below bars.
@@ -61,7 +61,7 @@ See the [axis mark](./axis.md) for more details and examples.
## Grid options
-The optional *data* is an array of tick values—it defaults to the scale’s ticks. The grid mark draws a line for each tick value, across the whole frame.
+The optional *data* is an array of tick values — it defaults to the scale’s ticks. The grid mark draws a line for each tick value, across the whole frame.
The following options are supported:
@@ -77,7 +77,7 @@ The following options are supported as constant or data-driven channels:
All the other common options are supported when applicable (*e.g.*, **title**).
-## gridX(*data*, *options*)
+## gridX(*data*, *options*) {#gridX}
```js
Plot.gridX({strokeDasharray: "5,3"})
@@ -85,7 +85,7 @@ Plot.gridX({strokeDasharray: "5,3"})
Returns a new *x* grid with the given *options*.
-## gridY(*data*, *options*)
+## gridY(*data*, *options*) {#gridY}
```js
Plot.gridY({strokeDasharray: "5,3"})
@@ -93,7 +93,7 @@ Plot.gridY({strokeDasharray: "5,3"})
Returns a new *y* grid with the given *options*.
-## gridFx(*data*, *options*)
+## gridFx(*data*, *options*) {#gridFx}
```js
Plot.gridFx({strokeDasharray: "5,3"})
@@ -101,7 +101,7 @@ Plot.gridFx({strokeDasharray: "5,3"})
Returns a new *fx* grid with the given *options*.
-## gridFy(*data*, *options*)
+## gridFy(*data*, *options*) {#gridFy}
```js
Plot.gridFy({strokeDasharray: "5,3"})
diff --git a/docs/marks/hexgrid.md b/docs/marks/hexgrid.md
index de10e61e5a..c1bd505034 100644
--- a/docs/marks/hexgrid.md
+++ b/docs/marks/hexgrid.md
@@ -6,7 +6,7 @@ import penguins from "../data/penguins.ts";
-# Hexgrid mark
+# Hexgrid mark
The **hexgrid mark** draws a hexagonal grid spanning the frame. It can be used with the [hexbin transform](../transforms/hexbin.md) to show how points are binned. The **binWidth** option specifies the distance between centers of neighboring hexagons in pixels; it defaults to 20, matching the hexbin transform.
@@ -25,7 +25,7 @@ Plot.plot({
The hexgrid mark supports the [standard mark options](../features/marks.md#mark-options). It does not accept any data or support channels. The default **stroke** is *currentColor*, the default **strokeOpacity** is 0.1, and the default **clip** is true. The **binWidth** defaults to 20, matching the [hexbin transform](../transforms/hexbin.md). The **fill** option is not supported, but a [frame mark](./frame.md) can be used to the same effect.
-## hexgrid(*options*)
+## hexgrid(*options*) {#hexgrid}
```js
Plot.hexgrid({stroke: "red"})
diff --git a/docs/marks/image.md b/docs/marks/image.md
index 5c4e0c356b..a696c6cb58 100644
--- a/docs/marks/image.md
+++ b/docs/marks/image.md
@@ -13,7 +13,7 @@ onMounted(() => {
-# Image mark
+# Image mark
The **image mark** draws images centered at the given position in **x** and **y**. It is often used to construct scatterplots in place of a [dot mark](./dot.md). For example, the chart below, based on one by [Robert Lesser](https://observablehq.com/@rlesser/when-presidents-fade-away), shows the favorability of U.S. presidents over time alongside their portraits.
@@ -111,7 +111,7 @@ Plot.plot({
```
:::
-If—*for reasons*—you want to style the plot with a background image, you can do that using the top-level **style** option rather than an image mark. Below, Kristen Gorman’s penguins dataset is visualized atop her photograph of sea ice near Palmer Station on the Antarctic peninsula, where she collected the measurements.
+If — *for reasons* — you want to style the plot with a background image, you can do that using the top-level **style** option rather than an image mark. Below, Kristen Gorman’s penguins dataset is visualized atop her photograph of sea ice near Palmer Station on the Antarctic peninsula, where she collected the measurements.
:::plot defer https://observablehq.com/@observablehq/plot-background-image
```js
@@ -143,7 +143,8 @@ In addition to the [standard mark options](../features/marks.md#mark-options), t
* **y** - the vertical position; bound to the *y* scale
* **width** - the image width (in pixels)
* **height** - the image height (in pixels)
-* **r** - the image radius; bound to the *r* scale
+* **r** - the image radius; bound to the *r* scale
+* **rotate** - the rotation angle in degrees clockwise
If either of the **x** or **y** channels are not specified, the corresponding position is controlled by the **frameAnchor** option.
@@ -156,13 +157,13 @@ The following image-specific constant options are also supported:
* **frameAnchor** - how to position the image within the frame; defaults to *middle*
* **preserveAspectRatio** - the [aspect ratio](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio); defaults to *xMidYMid meet*
* **crossOrigin** - the [cross-origin](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/crossorigin) behavior
-* **imageRendering** - the [image-rendering attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/image-rendering); defaults to *auto* (bilinear)
+* **imageRendering** - the [image-rendering attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/image-rendering); defaults to *auto* (bilinear)
To crop the image instead of scaling it to fit, set **preserveAspectRatio** to *xMidYMid slice*. The **imageRendering** option may be set to *pixelated* to disable bilinear interpolation on enlarged images; however, note that this is not supported in WebKit.
Images are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting, consider a [sort transform](../transforms/sort.md).
-## image(*data*, *options*)
+## image(*data*, *options*) {#image}
```js
Plot.image(presidents, {x: "inauguration", y: "favorability", src: "portrait"})
diff --git a/docs/marks/line.md b/docs/marks/line.md
index 3ef2c8e699..398ab4cf1d 100644
--- a/docs/marks/line.md
+++ b/docs/marks/line.md
@@ -57,7 +57,7 @@ Plot.line(aapl.map((d) => [d.Date, d.Close])).plot()
This shorthand loses the automatic *x*- and *y*-axis labels, reducing legibility. Use the **label** [scale option](../features/scales.md) to restore them.
:::
-The [lineY constructor](#liney-data-options) provides default channel definitions of **x** = index and **y** = [identity](../features/transforms.md#identity), letting you pass an array of numbers as data. The [lineX constructor](#linex-data-options) similarly provides **x** = identity and **y** = index defaults for lines that go up↑ instead of to the right→. Below, a random walk is made using [d3.cumsum](https://observablehq.com/@d3/d3-cumsum?collection=@d3/d3-array) and [d3.randomNormal](https://observablehq.com/@d3/d3-random?collection=@d3/d3-random).
+The [lineY constructor](#lineY) provides default channel definitions of **x** = index and **y** = [identity](../features/transforms.md#identity), letting you pass an array of numbers as data. The [lineX constructor](#lineX) similarly provides **x** = identity and **y** = index defaults for lines that go up↑ instead of to the right→. Below, a random walk is made using [d3.cumsum](https://observablehq.com/@d3/d3-cumsum?collection=@d3/d3-array) and [d3.randomNormal](https://observablehq.com/@d3/d3-random?collection=@d3/d3-random).
:::plot defer https://observablehq.com/@observablehq/plot-shorthand-liney
```js
@@ -81,7 +81,7 @@ Plot.lineY(d3.shuffle(aapl.slice()), {x: "Date", y: "Close", sort: "Date"}).plot
```
:::
-While the *x* scale of a line chart often represents time, this is not required. For example, we can plot the elevation profile of a Tour de France stage—and imagine how tiring it must be to start a climb after riding 160km! ⛰🚴💦
+While the *x* scale of a line chart often represents time, this is not required. For example, we can plot the elevation profile of a Tour de France stage — and imagine how tiring it must be to start a climb after riding 160km! ⛰🚴💦
:::plot defer https://observablehq.com/@observablehq/plot-tour-de-france-elevation-profile
```js
@@ -359,9 +359,9 @@ The **fill** defaults to *none*. The **stroke** defaults to *currentColor* if th
Points along the line are connected in input order. Likewise, if there are multiple series via the **z**, **fill**, or **stroke** channel, the series are drawn in input order such that the last series is drawn on top. Typically, the data is already in sorted order, such as chronological for time series; if sorting is needed, consider a [sort transform](../transforms/sort.md).
-The line mark supports [curve options](../features/curves.md) to control interpolation between points, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) on each of the control points. The default curve is *auto*, which is equivalent to *linear* if there is no [projection](../features/projections.md), and otherwise uses the associated projection. If any of the **x** or **y** values are invalid (undefined, null, or NaN), the line will be interrupted, resulting in a break that divides the line shape into multiple segments. (See [d3-shape’s *line*.defined](https://github.com/d3/d3-shape/blob/main/README.md#line_defined) for more.) If a line segment consists of only a single point, it may appear invisible unless rendered with rounded or square line caps. In addition, some curves such as *cardinal-open* only render a visible segment if it contains multiple points.
+The line mark supports [curve options](../features/curves.md) to control interpolation between points, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) on each of the control points. The default curve is *auto*, which is equivalent to *linear* if there is no [projection](../features/projections.md), and otherwise uses the associated projection. If any of the **x** or **y** values are invalid (undefined, null, or NaN), the line will be interrupted, resulting in a break that divides the line shape into multiple segments. (See [d3-shape’s *line*.defined](https://d3js.org/d3-shape/line#line_defined) for more.) If a line segment consists of only a single point, it may appear invisible unless rendered with rounded or square line caps. In addition, some curves such as *cardinal-open* only render a visible segment if it contains multiple points.
-## line(*data*, *options*)
+## line(*data*, *options*) {#line}
```js
Plot.line(aapl, {x: "Date", y: "Close"})
@@ -369,13 +369,13 @@ Plot.line(aapl, {x: "Date", y: "Close"})
Returns a new line with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
-## lineX(*data*, *options*)
+## lineX(*data*, *options*) {#lineX}
```js
Plot.lineX(aapl.map((d) => d.Close))
```
-Similar to [line](#line-data-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …]. If the **y** option is not specified, it defaults to [0, 1, 2, …].
+Similar to [line](#line) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …]. If the **y** option is not specified, it defaults to [0, 1, 2, …].
If the **interval** option is specified, the [binY transform](../transforms/bin.md) is implicitly applied to the specified *options*. The reducer of the output *x* channel may be specified via the **reduce** option, which defaults to *first*. To default to zero instead of showing gaps in data, as when the observed value represents a quantity, use the *sum* reducer.
@@ -385,13 +385,13 @@ Plot.lineX(observations, {y: "date", x: "temperature", interval: "day"})
The **interval** option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use "day" as the interval.
-## lineY(*data*, *options*)
+## lineY(*data*, *options*) {#lineY}
```js
Plot.lineY(aapl.map((d) => d.Close))
```
-Similar to [line](#line-data-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. If the **x** option is not specified, it defaults to [0, 1, 2, …].
+Similar to [line](#line) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. If the **x** option is not specified, it defaults to [0, 1, 2, …].
If the **interval** option is specified, the [binX transform](../transforms/bin.md) is implicitly applied to the specified *options*. The reducer of the output *y* channel may be specified via the **reduce** option, which defaults to *first*. To default to zero instead of showing gaps in data, as when the observed value represents a quantity, use the *sum* reducer.
diff --git a/docs/marks/linear-regression.md b/docs/marks/linear-regression.md
index 888122a277..2c9d2d7c73 100644
--- a/docs/marks/linear-regression.md
+++ b/docs/marks/linear-regression.md
@@ -10,7 +10,7 @@ const m = ref(0);
-# Linear regression mark
+# Linear regression mark
The **linear regression** mark draws [linear regression](https://en.wikipedia.org/wiki/Linear_regression) lines with confidence bands, representing the estimated linear relation of a dependent variable (typically **y**) on an independent variable (typically **x**). Below we can see that, in this example dataset at least, the weight of a car is a good linear predictor of its power.
@@ -99,7 +99,7 @@ The given *options* are passed through to these underlying marks, with the excep
Multiple regressions can be defined by specifying **z**, **fill**, or **stroke**.
-## linearRegressionX(*data*, *options*)
+## linearRegressionX(*data*, *options*) {#linearRegressionX}
```js
Plot.linearRegressionX(mtcars, {y: "wt", x: "hp"})
@@ -107,7 +107,7 @@ Plot.linearRegressionX(mtcars, {y: "wt", x: "hp"})
Returns a linear regression mark where **x** is the dependent variable and **y** is the independent variable. (This is the uncommon orientation.)
-## linearRegressionY(*data*, *options*)
+## linearRegressionY(*data*, *options*) {#linearRegressionY}
```js
Plot.linearRegressionY(mtcars, {x: "wt", y: "hp"})
diff --git a/docs/marks/link.md b/docs/marks/link.md
index a04c6055bb..ee6983c3d0 100644
--- a/docs/marks/link.md
+++ b/docs/marks/link.md
@@ -20,7 +20,7 @@ onMounted(() => {
# Link mark
-The **link mark** draws straight lines between two points [**x1**, **y1**] and [**x2**, **y2**] in quantitative dimensions. It is similar to the [arrow mark](./arrow.md), except it draws a straight line—or geodesic when used with a [spherical projection](../features/projections.md).
+The **link mark** draws straight lines between two points [**x1**, **y1**] and [**x2**, **y2**] in quantitative dimensions. It is similar to the [arrow mark](./arrow.md), except it draws a straight line — or geodesic when used with a [spherical projection](../features/projections.md).
For example, the chart below shows the rising inequality (and population) in various U.S. cities from 1980 to 2015. Each link represents two observations of a city: the city’s population (**x**) and inequality (**y**) in 1980, and the same in 2015. The link’s **stroke** redundantly encodes the change in inequality: red indicates rising inequality, while blue (there are only four) indicates declining inequality.
@@ -170,7 +170,7 @@ The link mark supports the [standard mark options](../features/marks.md). The **
The link mark supports [curve options](../features/curves.md) to control interpolation between points, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) on each of the control points. Since a link always has two points by definition, only the following curves (or a custom curve) are recommended: *linear*, *step*, *step-after*, *step-before*, *bump-x*, or *bump-y*. Note that the *linear* curve is incapable of showing a fill since a straight line has zero area. For a curved link, you can use a bent [arrow](./arrow.md) (with no arrowhead, if desired).
-## link(*data*, *options*)
+## link(*data*, *options*) {#link}
```js
Plot.link(inequality, {x1: "POP_1980", y1: "R90_10_1980", x2: "POP_2015", y2: "R90_10_2015"})
diff --git a/docs/marks/raster.md b/docs/marks/raster.md
index 2ea422c15c..ae1c8cf11a 100644
--- a/docs/marks/raster.md
+++ b/docs/marks/raster.md
@@ -24,13 +24,13 @@ function mandelbrot(x, y) {
-# Raster mark
+# Raster mark
:::tip
To produce contours instead of a heatmap, see the [contour mark](./contour.md).
:::
-The **raster mark** renders a [raster image](https://en.wikipedia.org/wiki/Raster_graphics)—that is, an image formed by discrete pixels in a grid, not a vector graphic like other marks. And whereas the [image mark](./image.md) shows an *existing* image, the raster mark *creates* one from abstract data, either by [interpolating spatial samples](#spatial-interpolators) (arbitrary points in **x** and **y**) or by sampling a function *f*(*x*,*y*) along the grid.
+The **raster mark** renders a [raster image](https://en.wikipedia.org/wiki/Raster_graphics) — that is, an image formed by discrete pixels in a grid, not a vector graphic like other marks. And whereas the [image mark](./image.md) shows an *existing* image, the raster mark *creates* one from abstract data, either by [interpolating spatial samples](#spatial-interpolators) (arbitrary points in **x** and **y**) or by sampling a function *f*(*x*,*y*) along the grid.
For example, the heatmap below shows the topography of the [Maungawhau volcano](https://en.wikipedia.org/wiki/Maungawhau), produced from a {{volcano.width}}×{{volcano.height}} grid of elevation samples.
@@ -292,7 +292,7 @@ The following raster-specific constant options are supported:
The **imageRendering** option may be set to *pixelated* for a sharper image. The **interpolate** option is ignored when **fill** or **fillOpacity** is a function of *x* and *y*.
-## raster(*data*, *options*)
+## raster(*data*, *options*) {#raster}
```js
Plot.raster(volcano.values, {width: volcano.width, height: volcano.height})
@@ -320,15 +320,15 @@ The **interpolate** option can also be specified as a function with the followin
So, *x*[*index*[0]] represents the *x*-position of the first sample, *y*[*index*[0]] its *y*-position, and *value*[*index*[0]] its value (*e.g.*, the observed height for a topographic map).
-## interpolateNone(*index*, *width*, *height*, *x*, *y*, *value*)
+## interpolateNone(*index*, *width*, *height*, *x*, *y*, *value*) {#interpolateNone}
```js
Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolateNone})
```
-Applies a simple forward mapping of samples, binning them into pixels in the raster grid without any blending or interpolation. If multiple samples map to the same pixel, the last one wins; this can introduce bias if the points are not in random order, so use [Plot.shuffle](../transforms/sort.md#shuffle-options) to randomize the input if needed.
+Applies a simple forward mapping of samples, binning them into pixels in the raster grid without any blending or interpolation. If multiple samples map to the same pixel, the last one wins; this can introduce bias if the points are not in random order, so use [Plot.shuffle](../transforms/sort.md#shuffle) to randomize the input if needed.
-## interpolateNearest(*index*, *width*, *height*, *x*, *y*, *value*)
+## interpolateNearest(*index*, *width*, *height*, *x*, *y*, *value*) {#interpolateNearest}
```js
Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolateNearest})
@@ -336,18 +336,18 @@ Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolat
Assigns each pixel in the raster grid the value of the closest sample; effectively a Voronoi diagram.
-## interpolatorBarycentric(*options*)
+## interpolatorBarycentric(*options*) {#interpolatorBarycentric}
```js
Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolatorBarycentric()})
```
-Constructs a Delaunay triangulation of the samples, and then for each pixel in the raster grid, determines the triangle that covers the pixel’s centroid and interpolates the values associated with the triangle’s vertices using [barycentric coordinates](https://en.wikipedia.org/wiki/Barycentric_coordinate_system). If the interpolated values are ordinal or categorical (_i.e._, anything other than numbers or dates), then one of the three values will be picked randomly weighted by the barycentric coordinates; the given **random** number generator will be used, which defaults to a [linear congruential generator](https://github.com/d3/d3-random/blob/main/README.md#randomLcg) with a fixed seed (for deterministic results).
+Constructs a Delaunay triangulation of the samples, and then for each pixel in the raster grid, determines the triangle that covers the pixel’s centroid and interpolates the values associated with the triangle’s vertices using [barycentric coordinates](https://en.wikipedia.org/wiki/Barycentric_coordinate_system). If the interpolated values are ordinal or categorical (_i.e._, anything other than numbers or dates), then one of the three values will be picked randomly weighted by the barycentric coordinates; the given **random** number generator will be used, which defaults to a [linear congruential generator](https://d3js.org/d3-random#randomLcg) with a fixed seed (for deterministic results).
-## interpolatorRandomWalk(*options*)
+## interpolatorRandomWalk(*options*) {#interpolatorRandomWalk}
```js
Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolatorRandomWalk()})
```
-For each pixel in the raster grid, initiates a random walk, stopping when either the walk is within a given distance (**minDistance**) of a sample or the maximum allowable number of steps (**maxSteps**) have been taken, and then assigning the current pixel the closest sample’s value. The random walk uses the “walk on spheres” algorithm in two dimensions described by [Sawhney and Crane](https://www.cs.cmu.edu/~kmcrane/Projects/MonteCarloGeometryProcessing/index.html), SIGGRAPH 2020; the given **random** number generator will be used, which defaults to a [linear congruential generator](https://github.com/d3/d3-random/blob/main/README.md#randomLcg) with a fixed seed (for deterministic results).
+For each pixel in the raster grid, initiates a random walk, stopping when either the walk is within a given distance (**minDistance**) of a sample or the maximum allowable number of steps (**maxSteps**) have been taken, and then assigning the current pixel the closest sample’s value. The random walk uses the “walk on spheres” algorithm in two dimensions described by [Sawhney and Crane](https://www.cs.cmu.edu/~kmcrane/Projects/MonteCarloGeometryProcessing/index.html), SIGGRAPH 2020; the given **random** number generator will be used, which defaults to a [linear congruential generator](https://d3js.org/d3-random#randomLcg) with a fixed seed (for deterministic results).
diff --git a/docs/marks/rect.md b/docs/marks/rect.md
index 883ed41a16..a0431fb19f 100644
--- a/docs/marks/rect.md
+++ b/docs/marks/rect.md
@@ -49,7 +49,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://github.com/d3/d3-array/blob/main/README.md#bins), we can produce a basic histogram with [rectY](#recty-data-options) as follows:
+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:
:::plot https://observablehq.com/@observablehq/plot-rects-and-bins
```js
@@ -73,7 +73,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-data-options) is for horizontal→ rects and applies an implicit [stackX transform](../transforms/stack.md#stackx-stack-options), while [rectY](#recty-data-options) is for vertical↑ rects and applies an implicit [stackY transform](../transforms/stack.md#stacky-stack-options).
+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).
:::plot defer https://observablehq.com/@observablehq/plot-vertical-histogram
```js
@@ -119,7 +119,7 @@ Plot.plot({
```
:::
-The [rect constructor](#rect-data-options), again with the [bin transform](../transforms/bin.md), can produce two-dimensional histograms (heatmaps) where density is represented by the **fill** color encoding.
+The [rect constructor](#rect), again with the [bin transform](../transforms/bin.md), can produce two-dimensional histograms (heatmaps) where density is represented by the **fill** color encoding.
:::plot defer https://observablehq.com/@observablehq/plot-continuous-dimensions-heatmap
```js-vue
@@ -201,11 +201,11 @@ The following channels are optional:
Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both.
-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#scale-options).
+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.
-## rect(*data*, *options*)
+## rect(*data*, *options*) {#rect}
```js
Plot.rect(olympians, Plot.bin({fill: "count"}, {x: "weight", y: "height"}))
@@ -213,18 +213,18 @@ Plot.rect(olympians, Plot.bin({fill: "count"}, {x: "weight", y: "height"}))
Returns a new rect with the given *data* and *options*.
-## rectX(*data*, *options*)
+## rectX(*data*, *options*) {#rectX}
```js
Plot.rectX(olympians, Plot.binY({x: "count"}, {y: "weight"}))
```
-Equivalent to [rect](#rect-data-options), except that if neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX transform](../transforms/stack.md); this is the typical configuration for a histogram with horizontal→ rects aligned at *x* = 0. If the **x** option is not specified, it defaults to the identity function.
+Equivalent to [rect](#rect), except that if neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX transform](../transforms/stack.md); this is the typical configuration for a histogram with horizontal→ rects aligned at *x* = 0. If the **x** option is not specified, it defaults to the identity function.
-## rectY(*data*, *options*)
+## rectY(*data*, *options*) {#rectY}
```js
Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight"}))
```
-Equivalent to [rect](#rect-data-options), except that if neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY transform](../transforms/stack.md); this is the typical configuration for a histogram with vertical↑ rects aligned at *y* = 0. If the **y** option is not specified, it defaults to the identity function.
+Equivalent to [rect](#rect), except that if neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY transform](../transforms/stack.md); this is the typical configuration for a histogram with vertical↑ rects aligned at *y* = 0. If the **y** option is not specified, it defaults to the identity function.
diff --git a/docs/marks/rule.md b/docs/marks/rule.md
index 5f60766c66..e81758ba0a 100644
--- a/docs/marks/rule.md
+++ b/docs/marks/rule.md
@@ -22,7 +22,7 @@ onMounted(() => {
The rule mark is one of two marks in Plot for drawing horizontal or vertical lines; it should be used when the secondary position dimension, if any, is quantitative. When it is ordinal, use a [tick](./tick.md).
:::
-The **rule mark** comes in two orientations: [ruleY](#ruley-data-options) draws a horizontal↔︎ line with a given *y* value, while [ruleX](#rulex-data-options) draws a vertical↕︎ line with a given *x* value. Rules are often used as annotations, say to mark the *y* = 0 baseline (in red below for emphasis) in a line chart.
+The **rule mark** comes in two orientations: [ruleY](#ruleY) draws a horizontal↔︎ line with a given *y* value, while [ruleX](#ruleX) draws a vertical↕︎ line with a given *x* value. Rules are often used as annotations, say to mark the *y* = 0 baseline (in red below for emphasis) in a line chart.
:::plot https://observablehq.com/@observablehq/plot-rule-zero
```js
@@ -89,7 +89,7 @@ Plot.plot({
```
:::
-In the dense [candlestick chart](https://observablehq.com/@observablehq/observable-plot-candlestick) below, three rules are drawn for each trading day: a gray rule spans the chart, showing gaps for weekends and holidays; a black rule spans the day’s low and high; and a green or red rule spans the day’s open and close.
+In the dense [candlestick chart](https://observablehq.com/@observablehq/observable-plot-candlestick) below, three rules are drawn for each trading day: a gray rule spans the chart, showing gaps for weekends and holidays; a {{$dark ? "white" : "black"}} rule spans the day’s low and high; and a green or red rule spans the day’s open and close.
:::plot defer https://observablehq.com/@observablehq/plot-candlestick-chart
```js
@@ -97,7 +97,7 @@ Plot.plot({
inset: 6,
label: null,
y: {grid: true, label: "Stock price ($)"},
- color: {type: "threshold", range: ["#e41a1c", "#4daf4a"]},
+ color: {type: "threshold", range: ["red", "green"]},
marks: [
Plot.ruleX(aapl, {x: "Date", y1: "Low", y2: "High"}),
Plot.ruleX(aapl, {x: "Date", y1: "Open", y2: "Close", stroke: (d) => d.Close - d.Open, strokeWidth: 4})
@@ -135,13 +135,13 @@ Plot.plot({
```
:::
-Rules are also used by the [grid mark](./grid) to draw grid lines.
+Rules are also used by the [grid mark](./grid.md) to draw grid lines.
## Rule options
-For the required channels, see [ruleX](#rulex-data-options) and [ruleY](#ruley-data-options). The rule mark supports the [standard mark options](../features/marks.md#mark-options), including insets along its secondary dimension, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) to the start or end of the rule. The **stroke** defaults to *currentColor*.
+For the required channels, see [ruleX](#ruleX) and [ruleY](#ruleY). The rule mark supports the [standard mark options](../features/marks.md#mark-options), including insets along its secondary dimension, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) to the start or end of the rule. The **stroke** defaults to *currentColor*.
-## ruleX(*data*, *options*)
+## ruleX(*data*, *options*) {#ruleX}
```js
Plot.ruleX([0]) // as annotation
@@ -160,9 +160,9 @@ If **x** is not specified, it defaults to [identity](../features/transforms.md#i
If **y** is specified, it is shorthand for **y2** with **y1** equal to zero; this is the typical configuration for a vertical lollipop chart with rules aligned at *y* = 0. If **y1** is not specified, the rule will start at the top of the plot (or facet). If **y2** is not specified, the rule will end at the bottom of the plot (or facet).
-If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
-## ruleY(*data*, *options*)
+## ruleY(*data*, *options*) {#ruleY}
```js
Plot.ruleY([0]) // as annotation
@@ -181,4 +181,4 @@ If **y** is not specified, it defaults to [identity](../features/transforms.md#i
If **x** is specified, it is shorthand for **x2** with **x1** equal to zero; this is the typical configuration for a horizontal lollipop chart with rules aligned at *x* = 0. If **x1** is not specified, the rule will start at the left edge of the plot (or facet). If **x2** is not specified, the rule will end at the right edge of the plot (or facet).
-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*. 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#scale-options).
+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*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
diff --git a/docs/marks/text.md b/docs/marks/text.md
index 52cb5f0a20..45fd814a47 100644
--- a/docs/marks/text.md
+++ b/docs/marks/text.md
@@ -38,7 +38,7 @@ Plot.plot({
:::
:::tip
-For formatting numbers and dates, consider [*number*.toLocaleString](https://observablehq.com/@mbostock/number-formatting), [*date*.toLocaleString](https://observablehq.com/@mbostock/date-formatting), [d3-format](https://github.com/d3/d3-format), or [d3-time-format](https://github.com/d3/d3-time-format).
+For formatting numbers and dates, consider [*number*.toLocaleString](https://observablehq.com/@mbostock/number-formatting), [*date*.toLocaleString](https://observablehq.com/@mbostock/date-formatting), [d3-format](https://d3js.org/d3-format), or [d3-time-format](https://d3js.org/d3-time-format).
:::
If there are too many data points, labels may overlap, making them hard to read. Use the [filter transform](../transforms/filter.md) to choose which points to label. In the connected scatterplot below, recreating Hannah Fairfield’s [“Driving Shifts Into Reverse”](http://www.nytimes.com/imagepages/2010/05/02/business/02metrics.html) from 2009, every fifth year is labeled.
@@ -170,9 +170,9 @@ Plot.plot({
marks: [
Plot.text(
[
- "Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before cof\xadfin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.",
- "There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf. Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there.",
- "Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears Hook to Coenties Slip, and from thence, by Whitehall, northward. What do you see?—Posted like silent sentinels all around the town, stand thousands upon thousands of mortal men fixed in ocean reveries. Some leaning against the spiles; some seated upon the pier-heads; some looking over the bulwarks of ships from China; some high aloft in the rigging, as if striving to get a still better seaward peep. But these are all landsmen; of week days pent up in lath and plaster—tied to counters, nailed to benches, clinched to desks. How then is this? Are the green fields gone? What do they here?"
+ "Call me Ishmael. Some years ago — never mind how long precisely — having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before cof\xadfin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off — then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.",
+ "There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs — commerce surrounds it with her surf. Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there.",
+ "Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears Hook to Coenties Slip, and from thence, by Whitehall, northward. What do you see? — Posted like silent sentinels all around the town, stand thousands upon thousands of mortal men fixed in ocean reveries. Some leaning against the spiles; some seated upon the pier-heads; some looking over the bulwarks of ships from China; some high aloft in the rigging, as if striving to get a still better seaward peep. But these are all landsmen; of week days pent up in lath and plaster — tied to counters, nailed to benches, clinched to desks. How then is this? Are the green fields gone? What do they here?"
],
{
x: (d, i) => 1 + i, // paragraph number
@@ -213,7 +213,7 @@ The following text-specific constant options are also supported:
* **lineAnchor** - the line anchor for vertical position; *top*, *bottom*, or *middle*
* **lineHeight** - the line height in ems; defaults to 1
* **lineWidth** - the line width in ems, for wrapping; defaults to Infinity
-* **textOverflow** - how to wrap or clip lines longer than the specified line width
+* **textOverflow** - how to wrap or clip lines longer than the specified line width
* **monospace** - if true, changes the default **fontFamily** and metrics to monospace
* **fontFamily** - the font name; defaults to [*system-ui*](https://drafts.csswg.org/css-fonts-4/#valdef-font-family-system-ui)
* **fontSize** - the font size in pixels; defaults to 10
@@ -240,7 +240,7 @@ If the **frameAnchor** option is not specified, then **textAnchor** and **lineAn
The **paintOrder** option defaults to *stroke* and the **strokeWidth** option defaults to 3. By setting **fill** to the foreground color and **stroke** to the background color (such as *black* and *white*, respectively), you can surround text with a “halo” which may improve legibility against a busy background.
-## text(*data*, *options*)
+## text(*data*, *options*) {#text}
```js
Plot.text(driving, {x: "miles", y: "gas", text: "year"})
@@ -248,22 +248,22 @@ Plot.text(driving, {x: "miles", y: "gas", text: "year"})
Returns a new text mark with the given *data* and *options*. If neither the **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
-## textX(*data*, *options*)
+## textX(*data*, *options*) {#textX}
```js
Plot.textX(alphabet.map((d) => d.frequency))
```
-Equivalent to [text](#text-data-options), except **x** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
+Equivalent to [text](#text), except **x** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
-If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
-## textY(*data*, *options*)
+## textY(*data*, *options*) {#textY}
```js
Plot.textY(alphabet.map((d) => d.frequency))
```
-Equivalent to [text](#text-data-options), except **y** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
+Equivalent to [text](#text), except **y** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
-If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
+If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
diff --git a/docs/marks/tick.md b/docs/marks/tick.md
index b0b467a37c..9212f4b810 100644
--- a/docs/marks/tick.md
+++ b/docs/marks/tick.md
@@ -23,7 +23,7 @@ onMounted(() => {
The tick mark is one of two marks in Plot for drawing horizontal or vertical lines; it should be used when the secondary position dimension, if any, is ordinal. When it is quantitative, use a [rule](./rule.md).
:::
-The **tick mark** comes in two orientations: [tickY](#ticky-data-options) draws a horizontal↔︎ line with a given *y* value, while [tickX](#tickx-data-options) draws a vertical↕︎ line with a given *x* value. Ticks have an optional secondary position dimension (**x** for tickY and **y** for tickX); this second dimension is ordinal, unlike a [rule](./rule.md), and requires a corresponding [band scale](../features/scales.md).
+The **tick mark** comes in two orientations: [tickY](#tickY) draws a horizontal↔︎ line with a given *y* value, while [tickX](#tickX) draws a vertical↕︎ line with a given *x* value. Ticks have an optional secondary position dimension (**x** for tickY and **y** for tickX); this second dimension is ordinal, unlike a [rule](./rule.md), and requires a corresponding [band scale](../features/scales.md).
Ticks are often used to show one-dimensional distributions, as in the “barcode” plot below showing the proportion of the population in each age bracket across U.S. states.
@@ -86,9 +86,9 @@ Ticks are also used by the [box mark](./box.md) to denote the median value for e
## Tick options
-For the required channels, see [tickX](#tickx-data-options) and [tickY](#ticky-data-options). The tick mark supports the [standard mark options](../features/marks.md#mark-options), including insets, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) to the start or end of the rule. The **stroke** defaults to *currentColor*.
+For the required channels, see [tickX](#tickX) and [tickY](#tickY). The tick mark supports the [standard mark options](../features/marks.md#mark-options), including insets, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) to the start or end of the rule. The **stroke** defaults to *currentColor*.
-## tickX(*data*, *options*)
+## tickX(*data*, *options*) {#tickX}
```js
Plot.tickX(stateage, {x: "population", y: "age"})
@@ -104,7 +104,7 @@ The following optional channels are supported:
If the **y** channel is not specified, the tick will span the full vertical extent of the frame.
-## tickY(*data*, *options*)
+## tickY(*data*, *options*) {#tickY}
```js
Plot.tickY(stateage, {y: "population", x: "age"})
diff --git a/docs/marks/tip.md b/docs/marks/tip.md
index 26b99de327..75aadc0cc1 100644
--- a/docs/marks/tip.md
+++ b/docs/marks/tip.md
@@ -22,7 +22,7 @@ onMounted(() => {
-# Tip mark
+# Tip mark
The **tip mark** displays text, or several name-value pairs, in a floating box anchored to a given position in **x** and **y**. The tip mark is often paired with the [pointer transform](../interactions/pointer.md) to reveal details on demand when hovering over a chart, as in this line chart of Apple stock price:
@@ -93,7 +93,7 @@ Plot.plot({
```
:::
-If no **title** channel is supplied, the tip mark displays all channel values. You can supply additional name-value pairs by registering extra channels using the **channels** mark option. In the scatterplot of Olympic athletes below, you can hover to see the *name* and *sport* of each athlete. This is helpful for noticing patterns—tall basketball players, giant weightlifters and judoka, diminutive gymnasts—and for seeing individuals.
+If no **title** channel is supplied, the tip mark displays all channel values. You can supply additional name-value pairs by registering extra channels using the **channels** mark option. In the scatterplot of Olympic athletes below, you can hover to see the *name* and *sport* of each athlete. This is helpful for noticing patterns — tall basketball players, giant weightlifters and judoka, diminutive gymnasts — and for seeing individuals.
:::plot defer https://observablehq.com/@observablehq/plot-tips-additional-channels
```js
@@ -221,7 +221,7 @@ These [standard text options](./text.md#text-options) control the display of tex
- **lineWidth** - the line width in ems, for wrapping; defaults to Infinity
- **textOverflow** - how to wrap or clip lines longer than the specified line width
-## tip(*data*, *options*)
+## tip(*data*, *options*) {#tip}
```js
Plot.tip(aapl, {x: "Date", y: "Close"})
diff --git a/docs/marks/tree.md b/docs/marks/tree.md
index 7ea61bbce2..1d56bfed93 100644
--- a/docs/marks/tree.md
+++ b/docs/marks/tree.md
@@ -30,9 +30,9 @@ function indent() {
-# Tree mark
+# Tree mark
-The **tree mark** produces tree diagrams using the [tree transform](../transforms/tree.md). It is a [composite mark](../features/marks.md#marks-marks), consisting of a [link](./link.md) to render links from parent to child, an optional [dot](./dot.md) for nodes, and a [text](./text.md) for node labels. The link mark uses the [treeLink transform](../transforms/tree.md#treelink-options), while the dot and text marks use the [treeNode transform](../transforms/tree.md#treenode-options).
+The **tree mark** produces tree diagrams using the [tree transform](../transforms/tree.md). It is a [composite mark](../features/marks.md#marks), consisting of a [link](./link.md) to render links from parent to child, an optional [dot](./dot.md) for nodes, and one or two [text](./text.md) for node labels. The link mark uses the [treeLink transform](../transforms/tree.md#treeLink), while the dot and text marks use the [treeNode transform](../transforms/tree.md#treeNode).
For example, here is a little family tree of Greek gods.
@@ -41,7 +41,8 @@ For example, here is a little family tree of Greek gods.
Plot.plot({
axis: null,
height: 100,
- margin: 20,
+ margin: 10,
+ marginLeft: 40,
marginRight: 120,
marks: [
Plot.tree(gods, {textStroke: "var(--vp-c-bg)"})
@@ -63,6 +64,7 @@ As a more complete example, here is a visualization of a software package hierar
Plot.plot({
axis: null,
margin: 10,
+ marginLeft: 30,
marginRight: 160,
width: 688,
height: 1800,
@@ -73,13 +75,14 @@ Plot.plot({
```
:::
-The **treeLayout** option specifies the layout algorithm. The tree mark uses the Reingold–Tilford “tidy” tree algorithm, [d3.tree](https://github.com/d3/d3-hierarchy/blob/main/README.md#tree), by default. The [cluster](#cluster-data-options) convenience method sets **treeLayout** to [d3.cluster](https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster), aligning the leaf nodes.
+The **treeLayout** option specifies the layout algorithm. The tree mark uses the Reingold–Tilford “tidy” tree algorithm, [d3.tree](https://d3js.org/d3-hierarchy/tree), by default. The [cluster](#cluster) convenience method sets **treeLayout** to [d3.cluster](https://d3js.org/d3-hierarchy/cluster), aligning the leaf nodes.
:::plot https://observablehq.com/@observablehq/plot-cluster-flare
```js
Plot.plot({
axis: null,
margin: 10,
+ marginLeft: 30,
marginRight: 160,
width: 688,
height: 2400,
@@ -148,12 +151,20 @@ The following options are supported:
* **title** - the text and dot title; defaults to *node:path*
* **text** - the text label; defaults to *node:name*
* **textStroke** - the text stroke; defaults to *white*
-* **dx** - the text horizontal offset; defaults to 6 if left-anchored, or -6 if right-anchored
+* **textLayout** - the text anchoring layout
+* **dx** - the text horizontal offset; defaults to 6
* **dy** - the text vertical offset; defaults to 0
Any additional *options* are passed through to the constituent link, dot, and text marks and their corresponding treeLink or treeNode transform.
-## tree(*data*, *options*)
+The **textLayout** option controls how text labels are anchored to the node. Two layouts are supported:
+
+* *mirrored* - leaf-node labels are left-anchored, and non-leaf nodes right-anchored
+* *normal* - all labels are left-anchored
+
+If the **treeLayout** is d3.tree or d3.cluster, the **textLayout** defaults to *mirrored*; otherwise it defaults to *normal*.
+
+## tree(*data*, *options*) {#tree}
```js
Plot.tree(flare, {path: "name", delimiter: "."})
@@ -161,10 +172,10 @@ Plot.tree(flare, {path: "name", delimiter: "."})
Returns a new tree mark with the given *data* and *options*.
-## cluster(*data*, *options*)
+## cluster(*data*, *options*) {#cluster}
```js
Plot.cluster(flare, {path: "name", delimiter: "."})
```
-Like [tree](#tree-data-options), except sets the **treeLayout** option to [d3.cluster](https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster), aligning leaf nodes.
+Like [tree](#tree), except sets the **treeLayout** option to [d3.cluster](https://d3js.org/d3-hierarchy/cluster), aligning leaf nodes, and defaults the **textLayout** option to *mirrored*.
diff --git a/docs/marks/vector.md b/docs/marks/vector.md
index d108cff3c6..0011ea4b85 100644
--- a/docs/marks/vector.md
+++ b/docs/marks/vector.md
@@ -37,7 +37,11 @@ onMounted(() => {
-# Vector mark
+# Vector mark
+
+:::tip
+See also the [arrow mark](./arrow.md), which draws arrows between two points.
+:::
The **vector mark** draws little arrows, typically positioned in **x** and **y** quantitative dimensions, with an optional magnitude (**length**) and direction (**rotate**), as in a vector field. For example, the chart below, based on a [LitVis example](https://github.com/gicentre/litvis/blob/main/examples/windVectors.md), shows wind speed and direction for a section of western Europe.
@@ -88,7 +92,7 @@ Plot.plot({
```
:::
-The **shape** option controls the vector’s appearance, while the **anchor** option positions the vector relative to its anchor point specified in **x** and **y**. The [spike constructor](#spike-data-options) sets the **shape** to *spike* and the **anchor** to *start*. For example, this can be used to produce a [spike map](https://observablehq.com/@observablehq/plot-spike) of U.S. county population.
+The **shape** option controls the vector’s appearance, while the **anchor** option positions the vector relative to its anchor point specified in **x** and **y**. The [spike constructor](#spike) sets the **shape** to *spike* and the **anchor** to *start*. For example, this can be used to produce a [spike map](https://observablehq.com/@observablehq/plot-spike) of U.S. county population.
:::plot defer https://observablehq.com/@observablehq/plot-spike-map-example
```js
@@ -168,7 +172,7 @@ The **stroke** defaults to *currentColor*. The **strokeWidth** defaults to 1.5,
Vectors are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting by drawing the smallest vectors on top, consider a [sort transform](../transforms/sort.md).
-## vector(*data*, *options*)
+## vector(*data*, *options*) {#vector}
```js
Plot.vector(wind, {x: "longitude", y: "latitude", length: "speed", rotate: "direction"})
@@ -176,26 +180,26 @@ Plot.vector(wind, {x: "longitude", y: "latitude", length: "speed", rotate: "dire
Returns a new vector with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
-## vectorX(*data*, *options*)
+## vectorX(*data*, *options*) {#vectorX}
```js
Plot.vectorX(cars.map((d) => d["economy (mpg)"]))
```
-Equivalent to [vector](#vector-data-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
+Equivalent to [vector](#vector) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
-## vectorY(*data*, *options*)
+## vectorY(*data*, *options*) {#vectorY}
```js
Plot.vectorY(cars.map((d) => d["economy (mpg)"]))
```
-Equivalent to [vector](#vector-data-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
+Equivalent to [vector](#vector) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
-## spike(*data*, *options*)
+## spike(*data*, *options*) {#spike}
```js
Plot.spike(counties, Plot.geoCentroid({length: (d) => d.properties.population}))
```
-Equivalent to [vector](#vector-data-options) except that the **shape** defaults to *spike*, the **stroke** defaults to *currentColor*, the **strokeWidth** defaults to 1, the **fill** defaults to **stroke**, the **fillOpacity** defaults to 0.3, and the **anchor** defaults to *start*.
+Equivalent to [vector](#vector) except that the **shape** defaults to *spike*, the **stroke** defaults to *currentColor*, the **strokeWidth** defaults to 1, the **fill** defaults to **stroke**, the **fillOpacity** defaults to 0.3, and the **anchor** defaults to *start*.
diff --git a/docs/public/data/miserables.json b/docs/public/data/miserables.json
new file mode 120000
index 0000000000..b669390323
--- /dev/null
+++ b/docs/public/data/miserables.json
@@ -0,0 +1 @@
+../../../test/data/miserables.json
\ No newline at end of file
diff --git a/docs/transforms/bin.md b/docs/transforms/bin.md
index ab9c28062f..d624d40c80 100644
--- a/docs/transforms/bin.md
+++ b/docs/transforms/bin.md
@@ -20,7 +20,7 @@ onMounted(() => {
The bin transform is for aggregating quantitative or temporal data. For ordinal or nominal data, use the [group transform](./group.md). See also the [hexbin transform](./hexbin.md).
:::
-The **bin transform** groups quantitative or temporal data—continuous measurements such as heights, weights, or temperatures—into discrete bins. You can then compute summary statistics for each bin, such as a count, sum, or proportion. The bin transform is most often used to make histograms or heatmaps with the [rect mark](../marks/rect.md).
+The **bin transform** groups quantitative or temporal data — continuous measurements such as heights, weights, or temperatures — into discrete bins. You can then compute summary statistics for each bin, such as a count, sum, or proportion. The bin transform is most often used to make histograms or heatmaps with the [rect mark](../marks/rect.md).
For example, here is a histogram showing the distribution of weights of Olympic athletes.
@@ -87,9 +87,9 @@ While the **mixBlendMode** option is useful for mitigating occlusion, it can be
The bin transform comes in three orientations:
-- [binX](#binx-outputs-options) bins on **x**, and often outputs **y** as in a histogram with vertical↑ rects;
-- [binY](#biny-outputs-options) bins on **y**, and often outputs **x** as in a histogram with horizontal→ rects; and
-- [bin](#bin-outputs-options) bins on both **x** and **y**, and often outputs to **fill** or **r** as in a heatmap.
+- [binX](#binX) bins on **x**, and often outputs **y** as in a histogram with vertical↑ rects;
+- [binY](#binY) bins on **y**, and often outputs **x** as in a histogram with horizontal→ rects; and
+- [bin](#bin) bins on both **x** and **y**, and often outputs to **fill** or **r** as in a heatmap.
As you might guess, the binY transform with the rectX mark produces a histogram with horizontal→ rects.
@@ -334,7 +334,7 @@ The **thresholds** option may be specified as a named method or a variety of oth
* an interval or time interval (for temporal binning; see below)
* a function that returns an array, count, or time interval
-If the **thresholds** option is specified as a function, it is passed three arguments: the array of input values, the domain minimum, and the domain maximum. If a number, [d3.ticks](https://github.com/d3/d3-array/blob/main/README.md#ticks) or [d3.utcTicks](https://github.com/d3/d3-time/blob/main/README.md#ticks) is used to choose suitable nice thresholds. If an interval, it must expose an *interval*.floor(*value*), *interval*.ceil(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) methods. If the interval is a time interval such as "day" (equivalently, d3.utcDay), or if the thresholds are specified as an array of dates, then the binned values are implicitly coerced to dates. Time intervals are intervals that are also functions that return a Date instance when called with no arguments.
+If the **thresholds** option is specified as a function, it is passed three arguments: the array of input values, the domain minimum, and the domain maximum. If a number, [d3.ticks](https://d3js.org/d3-array/ticks) or [d3.utcTicks](https://d3js.org/d3-time#utcTicks) is used to choose suitable nice thresholds. If an interval, it must expose an *interval*.floor(*value*), *interval*.ceil(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) methods. If the interval is a time interval such as "day" (equivalently, d3.utcDay), or if the thresholds are specified as an array of dates, then the binned values are implicitly coerced to dates. Time intervals are intervals that are also functions that return a Date instance when called with no arguments.
If the **interval** option is used instead of **thresholds**, it may be either an interval, a time interval, or a number. If a number *n*, threshold values are consecutive multiples of *n* that span the domain; otherwise, the **interval** option is equivalent to the **thresholds** option. When the thresholds are specified as an interval, and the default **domain** is used, the domain will automatically be extended to start and end to align with the interval.
@@ -346,7 +346,7 @@ Plot.binX({y: "count"}, {x: "body_mass_g", fill: "species"})
Lastly, the bin transform changes the default [mark insets](../features/marks.md#mark-options): binX changes the defaults for **insetLeft** and **insetRight**; binY changes the defaults for **insetTop** and **insetBottom**; bin changes all four.
-## bin(*outputs*, *options*)
+## bin(*outputs*, *options*) {#bin}
```js
Plot.rect(olympians, Plot.bin({fill: "count"}, {x: "weight", y: "height"}))
@@ -354,7 +354,7 @@ Plot.rect(olympians, Plot.bin({fill: "count"}, {x: "weight", y: "height"}))
Bins on **x** and **y**. Also groups on the first channel of **z**, **fill**, or **stroke**, if any.
-## binX(*outputs*, *options*)
+## binX(*outputs*, *options*) {#binX}
```js
Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight"}))
@@ -362,7 +362,7 @@ Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight"}))
Bins on **x**. Also groups on **y** and the first channel of **z**, **fill**, or **stroke**, if any.
-## binY(*outputs*, *options*)
+## binY(*outputs*, *options*) {#binY}
```js
Plot.rectX(olympians, Plot.binY({x: "count"}, {y: "weight"}))
diff --git a/docs/transforms/centroid.md b/docs/transforms/centroid.md
index 409704c29d..489772d40a 100644
--- a/docs/transforms/centroid.md
+++ b/docs/transforms/centroid.md
@@ -6,10 +6,10 @@ import * as topojson from "topojson-client";
import {computed, shallowRef, onMounted} from "vue";
const us = shallowRef(null);
-const countymesh = computed(() => us.value ? topojson.mesh(us.value, us.value.objects.counties) : {type: null});
const statemesh = computed(() => us.value ? topojson.mesh(us.value, us.value.objects.states) : {type: null});
const states = computed(() => us.value ? topojson.feature(us.value, us.value.objects.states).features : []);
const counties = computed(() => us.value ? topojson.feature(us.value, us.value.objects.counties).features : []);
+const nation = computed(() => us.value ? topojson.feature(us.value, us.value.objects.nation) : []);
onMounted(() => {
d3.json("../data/us-counties-10m.json").then((data) => (us.value = data));
@@ -17,9 +17,9 @@ onMounted(() => {
-# Centroid transform
+# Centroid transform
-Plot offers two transforms that derive centroids from GeoJSON geometries: [centroid](#centroid-options) and [geoCentroid](#geocentroid-options). These transforms can be used by any mark that accepts **x** and **y** channels. For instance, to label U.S. states we can use a [text mark](../marks/text.md).
+Plot offers two transforms that derive centroids from GeoJSON geometries: [centroid](#centroid) and [geoCentroid](#geoCentroid). These transforms can be used by any mark that accepts **x** and **y** channels. Below, a [text mark](../marks/text.md) labels the U.S. states.
:::plot defer https://observablehq.com/@observablehq/plot-state-labels
```js
@@ -43,7 +43,7 @@ Plot.voronoi(counties, Plot.centroid()).plot({projection: "albers"})
While the centroid transform computes the centroid of a geometry _after_ projection, the geoCentroid transform computes it _before_ projection, then projects the resulting coordinates. This difference has a few implications, as follows.
-As an [initializer](../features/transforms.md#custom-initializers), the centroid transform operates _after_ the geometries have been projected to screen coordinates. The resulting **x** and **y** channels reference the pixel coordinates of the planar centroid of the _projected_ shapes. No assumption is made about the geometries: they can be in any coordinate system, and the returned value is in the frame—as long as the projected geometry returns at least one visible point.
+As an [initializer](../features/transforms.md#custom-initializers), the centroid transform operates _after_ the geometries have been projected to screen coordinates. The resulting **x** and **y** channels reference the pixel coordinates of the planar centroid of the _projected_ shapes. No assumption is made about the geometries: they can be in any coordinate system, and the returned value is in the frame — as long as the projected geometry returns at least one visible point.
:::plot defer https://observablehq.com/@observablehq/plot-centroid-dot
```js
@@ -52,7 +52,7 @@ Plot.dot(counties, Plot.centroid()).plot({projection: "albers-usa"})
:::
-The geoCentroid transform is more specialized as the **x** and **y** channels it derives represent the longitudes and latitudes of the centroids of the given GeoJSON geometries, before projection. It expects the geometries to be specified in _spherical_ coordinates. It is more correct, in a geospatial sense—for example, the spherical centroid always represents the center of mass of the original shape, and it will be rotated exactly in line with the projection’s rotate argument. However, this also means that it might land outside the frame if only a part of the land mass is visible, and might be clipped by the projection. In practice, the difference is generally imperceptible.
+The geoCentroid transform is more specialized as the **x** and **y** channels it derives represent the longitudes and latitudes of the centroids of the given GeoJSON geometries, before projection. It expects the geometries to be specified in _spherical_ coordinates. It is more correct, in a geospatial sense — for example, the spherical centroid always represents the center of mass of the original shape, and it will be rotated exactly in line with the projection’s rotate argument. However, this also means that it might land outside the frame if only a part of the land mass is visible, and might be clipped by the projection. In practice, the difference is generally imperceptible.
:::plot defer https://observablehq.com/@observablehq/plot-centroid-dot
```js
@@ -60,7 +60,7 @@ Plot.dot(counties, Plot.geoCentroid()).plot({projection: "albers-usa"})
```
:::
-The geoCentroid transform is slightly faster than the centroid initializer—which might be useful if you have tens of thousands of features and want to show their density on a [hexbin map](../transforms/hexbin.md):
+The geoCentroid transform is slightly faster than the centroid initializer — which might be useful if you have tens of thousands of features and want to show their density on a [hexbin map](../transforms/hexbin.md):
:::plot defer https://observablehq.com/@observablehq/plot-centroid-hexbin
```js
@@ -68,7 +68,23 @@ Plot.dot(counties, Plot.hexbin({r:"count"}, Plot.geoCentroid())).plot({projectio
```
:::
-## centroid(*options*)
+Combined with the [pointer transform](../interactions/pointer.md), the centroid transform can add [interactive tips](../marks/tip.md) on a map:
+
+:::plot defer https://observablehq.com/@observablehq/plot-state-centroids
+```js
+Plot.plot({
+ projection: "albers-usa",
+ marks: [
+ Plot.geo(statemesh, {strokeOpacity: 0.2}),
+ Plot.geo(nation),
+ Plot.dot(states, Plot.centroid({fill: "red", stroke: "var(--vp-c-bg-alt)"})),
+ Plot.tip(states, Plot.pointer(Plot.centroid({title: (d) => d.properties.name})))
+ ]
+})
+```
+:::
+
+## centroid(*options*) {#centroid}
```js
Plot.centroid({geometry: Plot.identity})
@@ -76,7 +92,7 @@ Plot.centroid({geometry: Plot.identity})
The centroid initializer derives **x** and **y** channels representing the planar (projected) centroids for the given GeoJSON geometry. If the **geometry** option is not specified, the mark’s data is assumed to be GeoJSON objects.
-## geoCentroid(*options*)
+## geoCentroid(*options*) {#geoCentroid}
```js
Plot.geoCentroid({geometry: Plot.identity})
diff --git a/docs/transforms/dodge.md b/docs/transforms/dodge.md
index 45e007f539..136ae19437 100644
--- a/docs/transforms/dodge.md
+++ b/docs/transforms/dodge.md
@@ -17,9 +17,9 @@ onMounted(() => {
-# Dodge transform
+# Dodge transform
-Given one position dimension (either **x** or **y**), the **dodge** transform computes the other position dimension such that dots are packed densely without overlapping. The [dodgeX transform](#dodgex-dodgeoptions-options) computes **x** (horizontal position) given **y** (vertical position), while the [dodgeY transform](#dodgey-dodgeoptions-options) computes **y** given **x**.
+Given one position dimension (either **x** or **y**), the **dodge** transform computes the other position dimension such that dots are packed densely without overlapping. The [dodgeX transform](#dodgeX) computes **x** (horizontal position) given **y** (vertical position), while the [dodgeY transform](#dodgeY) computes **y** given **x**.
The dodge transform is commonly used to produce beeswarm 🐝 plots, a way of showing a one-dimensional distribution that preserves the visual identity of individual data points. For example, the dots below represent the weights of cars; the rough shape of the pile gives a sense of the overall distribution (peaking around 2,100 pounds), and you can hover an individual dot to see which car it represents.
@@ -245,7 +245,7 @@ The dodge transforms accept the following options:
The **anchor** option may one of *middle*, *right*, and *left* for dodgeX, and one of *middle*, *top*, and *bottom* for dodgeY. With the *middle* anchor the piles will grow from the center in both directions; with the other anchors, the piles will grow from the specified anchor towards the opposite direction.
-## dodgeY(*dodgeOptions*, *options*)
+## dodgeY(*dodgeOptions*, *options*) {#dodgeY}
```js
Plot.dodgeY({x: "date"})
@@ -253,7 +253,7 @@ Plot.dodgeY({x: "date"})
Given marks arranged along the *x* axis, the dodgeY transform piles them vertically by defining a *y* position channel that avoids overlapping. The *x* position channel is unchanged.
-## dodgeX(*dodgeOptions*, *options*)
+## dodgeX(*dodgeOptions*, *options*) {#dodgeX}
```js
Plot.dodgeX({y: "value"})
diff --git a/docs/transforms/filter.md b/docs/transforms/filter.md
index 4eaca5aff1..09587e1f67 100644
--- a/docs/transforms/filter.md
+++ b/docs/transforms/filter.md
@@ -48,9 +48,9 @@ Plot.plot({
As an alternative to the filter transform here, you could set the **text** channel value to null using a function: `text: (d) => d.highlight ? d.nyt_display : null`.
:::
-The filter transform can be applied either via the **filter** [mark option](../features/marks.md#mark-options), as above, or as an explicit [filter transform](#filter-test-options). The latter is generally only needed when composing multiple transforms.
+The filter transform can be applied either via the **filter** [mark option](../features/marks.md#mark-options), as above, or as an explicit [filter transform](#filter). The latter is generally only needed when composing multiple transforms.
-To highlight the vowels in a bar chart of English letter frequency, you can use a filtered bar with a red stroke. A filtered mark allows you to set options on a subset of the data, even if those options—such as mark insets—are not expressible as a channels.
+To highlight the vowels in a bar chart of English letter frequency, you can use a filtered bar with a red stroke. A filtered mark allows you to set options on a subset of the data, even if those options — such as mark insets — are not expressible as a channels.
:::plot https://observablehq.com/@observablehq/plot-filtered-bars
```js{8}
@@ -104,7 +104,7 @@ Plot.plot({
```
:::
-## filter(*test*, *options*)
+## filter(*test*, *options*) {#filter}
```js
Plot.filter((d) => /[aeiouy]/i.test(d.letter), {x: "letter", y: "frequency"})
diff --git a/docs/transforms/group.md b/docs/transforms/group.md
index df85aea289..0584a1413b 100644
--- a/docs/transforms/group.md
+++ b/docs/transforms/group.md
@@ -3,7 +3,6 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {shallowRef, onMounted} from "vue";
-import penguins from "../data/penguins.ts";
const olympians = shallowRef([{weight: 31, height: 1.21, sex: "female"}, {weight: 170, height: 2.21, sex: "male"}]);
@@ -19,7 +18,7 @@ onMounted(() => {
The group transform is for aggregating ordinal or nominal data. For quantitative or temporal data, use the [bin transform](./bin.md).
:::
-The **group transform** groups ordinal or nominal data—discrete values such as name, type, or category. You can then compute summary statistics for each group, such as a count, sum, or proportion. The group transform is most often used to make bar charts with the [bar mark](../marks/bar.md).
+The **group transform** groups ordinal or nominal data — discrete values such as name, type, or category. You can then compute summary statistics for each group, such as a count, sum, or proportion. The group transform is most often used to make bar charts with the [bar mark](../marks/bar.md).
For example, the bar chart below shows a distribution of Olympic athletes by sport.
@@ -41,7 +40,7 @@ Plot.plot({
Ordinal domains are sorted naturally (alphabetically) by default. Either set the [scale **domain**](../features/scales.md) explicitly to change the order, or use the mark [**sort** option](../features/scales.md#sort-mark-option) to derive the scale domain from a channel.
:::
-The groupX transform groups on **x**. The *outputs* argument (here `{y: "count"}`) declares desired output channels (**y**) and the associated reducer (*count*). Hence the height of each bar above represents the number of penguins of each species.
+The groupX transform groups on **x**. The *outputs* argument (here `{y: "count"}`) declares desired output channels (**y**) and the associated reducer (*count*). Hence the height of each bar above represents the number of Olympic athletes by sport.
@@ -200,10 +199,10 @@ Plot.plot({
The group transform comes in four orientations:
-- [groupX](#groupx-outputs-options) groups on **x**, and often outputs **y** as in a vertical↑ bar chart;
-- [groupY](#groupy-outputs-options) groups on **y**, and often outputs **x** as in a horizontal→ bar chart;
-- [groupZ](#groupz-outputs-options) groups on *neither* **x** nor **y**, combining everything into one group; and
-- [group](#group-outputs-options) groups on *both* **x** and **y**, and often outputs to **fill** or **r** as in a heatmap.
+- [groupX](#groupX) groups on **x**, and often outputs **y** as in a vertical↑ bar chart;
+- [groupY](#groupY) groups on **y**, and often outputs **x** as in a horizontal→ bar chart;
+- [groupZ](#groupZ) groups on *neither* **x** nor **y**, combining everything into one group; and
+- [group](#group) groups on *both* **x** and **y**, and often outputs to **fill** or **r** as in a heatmap.
As you might guess, the groupY transform with the barX mark produces a horizontal→ bar chart. (We must increase the **marginLeft** to avoid the *y* axis labels from being cut off.)
@@ -285,7 +284,7 @@ Plot.plot({
```
:::
-To group solely on **z** (or **fill** or **stroke**), use [groupZ](#groupz-outputs-options). The single stacked bar chart below (an alternative to a pie chart) shows the proportion of athletes by sport. The *proportion* reducer converts counts into normalized proportions adding up to 1, while the *first* reducer pulls out the name of the sport for labeling.
+To group solely on **z** (or **fill** or **stroke**), use [groupZ](#groupZ). The single stacked bar chart below (an alternative to a pie chart) shows the proportion of athletes by sport. The *proportion* reducer converts counts into normalized proportions adding up to 1, while the *first* reducer pulls out the name of the sport for labeling.
:::plot defer https://observablehq.com/@observablehq/plot-single-stacked-bar
```js
@@ -405,7 +404,7 @@ If any of **z**, **fill**, or **stroke** is a channel, the first of these channe
The default reducer for the **title** channel returns a summary list of the top 5 values with the corresponding number of occurrences.
-## group(*outputs*, *options*)
+## group(*outputs*, *options*) {#group}
```js
Plot.group({fill: "count"}, {x: "island", y: "species"})
@@ -413,7 +412,7 @@ Plot.group({fill: "count"}, {x: "island", y: "species"})
Groups on **x**, **y**, and the first channel of **z**, **fill**, or **stroke**, if any.
-## groupX(*outputs*, *options*)
+## groupX(*outputs*, *options*) {#groupX}
```js
Plot.groupX({y: "sum"}, {x: "species", y: "body_mass_g"})
@@ -421,7 +420,7 @@ Plot.groupX({y: "sum"}, {x: "species", y: "body_mass_g"})
Groups on **x** and the first channel of **z**, **fill**, or **stroke**, if any.
-## groupY(*outputs*, *options*)
+## groupY(*outputs*, *options*) {#groupY}
```js
Plot.groupY({x: "sum"}, {y: "species", x: "body_mass_g"})
@@ -429,7 +428,7 @@ Plot.groupY({x: "sum"}, {y: "species", x: "body_mass_g"})
Groups on **y** and the first channel of **z**, **fill**, or **stroke**, if any.
-## groupZ(*outputs*, *options*)
+## groupZ(*outputs*, *options*) {#groupZ}
```js
Plot.groupZ({x: "proportion"}, {fill: "species"})
diff --git a/docs/transforms/hexbin.md b/docs/transforms/hexbin.md
index fb99940e04..2e3031a4f4 100644
--- a/docs/transforms/hexbin.md
+++ b/docs/transforms/hexbin.md
@@ -21,9 +21,9 @@ onMounted(() => {
-# Hexbin transform
+# Hexbin transform
-The **hexbin transform** groups two-dimensional quantitative or temporal data—continuous measurements such as heights, weights, or temperatures—into discrete hexagonal bins. You can then compute summary statistics for each bin, such as a count, sum, or proportion. The hexbin transform is most often used to make heatmaps with the [dot mark](../marks/dot.md).
+The **hexbin transform** groups two-dimensional quantitative or temporal data — continuous measurements such as heights, weights, or temperatures — into discrete hexagonal bins. You can then compute summary statistics for each bin, such as a count, sum, or proportion. The hexbin transform is most often used to make heatmaps with the [dot mark](../marks/dot.md).
For example, the heatmap below shows the weights and heights of Olympic athletes. The color of each hexagon represents the number (*count*) of athletes with similar weight and height.
@@ -133,7 +133,7 @@ Plot.plot({
```
:::
-The hexbin transform defaults the **symbol** option to *hexagon*, but you can override it. The [circle constructor](../marks/dot.md#circle-data-options) changes it to *circle*.
+The hexbin transform defaults the **symbol** option to *hexagon*, but you can override it. The [circle constructor](../marks/dot.md#circle) changes it to *circle*.
:::plot defer https://observablehq.com/@observablehq/plot-hexbin-circle
```js
@@ -191,14 +191,14 @@ The following aggregation methods are supported:
* *max-index* - the zero-based index of the maximum value
* *mean* - the mean value (average)
* *median* - the median value
-* *deviation* - the [standard deviation](https://github.com/d3/d3-array/blob/master/README.md#deviation)
+* *deviation* - the [standard deviation](https://d3js.org/d3-array/summarize#deviation)
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
* *mode* - the value with the most occurrences
* *identity* - the array of values
* a function to be passed the array of values for each bin and the extent of the bin
* an object with a *reduceIndex* method
-## hexbin(*outputs*, *options*)
+## hexbin(*outputs*, *options*) {#hexbin}
```js
Plot.dot(olympians, Plot.hexbin({fill: "count"}, {x: "weight", y: "height"}))
diff --git a/docs/transforms/interval.md b/docs/transforms/interval.md
index 67c7fc71cc..7b19052a83 100644
--- a/docs/transforms/interval.md
+++ b/docs/transforms/interval.md
@@ -38,7 +38,7 @@ Plot.plot({
```
:::
-In contrast, a [rectY mark](../marks/rect.md) with the **interval** option and the *day* interval produces a temporal (*utc*) *x* scale. This allows Plot to compute ticks at meaningful intervals: here weekly boundaries, UTC midnight on Sundays. Furthermore, we can see that this isn’t truly daily data—it’s missing weekends and holidays when the stock market was closed.
+In contrast, a [rectY mark](../marks/rect.md) with the **interval** option and the *day* interval produces a temporal (*utc*) *x* scale. This allows Plot to compute ticks at meaningful intervals: here weekly boundaries, UTC midnight on Sundays. Furthermore, we can see that this isn’t truly daily data — it’s missing weekends and holidays when the stock market was closed.
:::plot https://observablehq.com/@observablehq/plot-temporal-interval-option
```js
@@ -85,4 +85,4 @@ Plot.plot({
```
:::
-While the **interval** option is most commonly specified as a named time interval or a number, it can also be specified as a [D3 time interval](https://github.com/d3/d3-time/blob/main/README.md#api-reference) or any object that implements *interval*.floor and *interval*.offset.
+While the **interval** option is most commonly specified as a named time interval or a number, it can also be specified as a [D3 time interval](https://d3js.org/d3-time#_interval) or any object that implements *interval*.floor and *interval*.offset.
diff --git a/docs/transforms/map.md b/docs/transforms/map.md
index c2b6f739a9..79201cc86b 100644
--- a/docs/transforms/map.md
+++ b/docs/transforms/map.md
@@ -20,7 +20,7 @@ function bollinger(N, K) {
# Map transform
-The **map transform** groups data into series and then transforms each series’ values, say to normalize them relative to some basis or to apply a moving average. For example, below the map transform computes a cumulative sum (*cumsum*) of a series of random numbers sampled from a normal distribution—in other words, a random walk.
+The **map transform** groups data into series and then transforms each series’ values, say to normalize them relative to some basis or to apply a moving average. For example, below the map transform computes a cumulative sum (*cumsum*) of a series of random numbers sampled from a normal distribution — in other words, a random walk.
:::plot https://observablehq.com/@observablehq/plot-random-walk-cumsum-map
```js
@@ -47,9 +47,9 @@ Plot.plot({
```
:::
-The [mapY transform](#mapy-map-options) above is shorthand for applying the given map method to all *y* channels. There’s also a less-common [mapX transform](#mapx-map-options) for *x* channels.
+The [mapY transform](#mapY) above is shorthand for applying the given map method to all *y* channels. There’s also a less-common [mapX transform](#mapX) for *x* channels.
-The more explicit [map](#map-outputs-options) transform lets you specify which channels to map, and what map method to use for each channel. Like the [group](./group.md) and [bin](./bin.md) transforms, it takes two arguments: an *outputs* object that describes the output channels to compute, and an *options* object that describes the input channels and additional options. So this:
+The more explicit [map](#map) transform lets you specify which channels to map, and what map method to use for each channel. Like the [group](./group.md) and [bin](./bin.md) transforms, it takes two arguments: an *outputs* object that describes the output channels to compute, and an *options* object that describes the input channels and additional options. So this:
```js
Plot.mapY("cumsum", {y: d3.randomNormal()})
@@ -119,7 +119,7 @@ The following map methods are supported:
If a function is used, it must return an array of the same length as the given input. If a *mapIndex* method is used, it is repeatedly passed the index for each series (an array of integers), the corresponding input channel’s array of values, and the output channel’s array of values; it must populate the slots specified by the index in the output array.
-## map(*outputs*, *options*)
+## map(*outputs*, *options*) {#map}
```js
Plot.map({y: "cumsum"}, {y: d3.randomNormal()})
@@ -127,7 +127,7 @@ Plot.map({y: "cumsum"}, {y: d3.randomNormal()})
Groups on the first channel of **z**, **fill**, or **stroke**, if any, and then for each channel declared in the specified *outputs* object, applies the corresponding map method. Each channel in *outputs* must have a corresponding input channel in *options*.
-## mapX(*map*, *options*)
+## mapX(*map*, *options*) {#mapX}
```js
Plot.mapX("cumsum", {x: d3.randomNormal()})
@@ -135,7 +135,7 @@ Plot.mapX("cumsum", {x: d3.randomNormal()})
Equivalent to Plot.map({x: *map*, x1: *map*, x2: *map*}, *options*), but ignores any of **x**, **x1**, and **x2** not present in *options*. In addition, if none of **x**, **x1**, or **x2** are specified, then **x** defaults to [identity](../features/transforms.md#identity).
-## mapY(*map*, *options*)
+## mapY(*map*, *options*) {#mapY}
```js
Plot.mapY("cumsum", {y: d3.randomNormal()})
diff --git a/docs/transforms/normalize.md b/docs/transforms/normalize.md
index 984214e9f0..52a7d19b00 100644
--- a/docs/transforms/normalize.md
+++ b/docs/transforms/normalize.md
@@ -27,7 +27,7 @@ onMounted(() => {
# Normalize transform
-The **normalize transform** is a specialized [map transform](./map.md) that normalizes series values relative to some basis, say to convert absolute values into relative values. For example, here is an index chart—a type of multi-series line chart—showing the return of several stocks relative to their closing price on a particular date.
+The **normalize transform** is a specialized [map transform](./map.md) that normalizes series values relative to some basis, say to convert absolute values into relative values. For example, here is an index chart — a type of multi-series line chart — showing the return of several stocks relative to their closing price on a particular date.
:::plot defer https://observablehq.com/@observablehq/plot-index-chart
```js
@@ -63,10 +63,10 @@ The [select transform](./select.md) is used to label the endpoints of each line.
:::
:::info
-This example uses an [immediately-invoked function expression (IIFE)](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) for the **tickFormat** option so that the [d3.format](https://github.com/d3/d3-format) only needs to be constructed once.
+This example uses an [immediately-invoked function expression (IIFE)](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) for the **tickFormat** option so that the [d3.format](https://d3js.org/d3-format) only needs to be constructed once.
:::
-The normalize transform converts absolute values into relative ones. So, if **y** is [*y₀*, *y₁*, *y₂*, …] and the *first* basis is used with [normalizeY](#normalizey-basis-options), the resulting output **y** channel is [*y₀* / *y₀*, *y₁* / *y₀*, *y₂* / *y₀*, …]. But it’s a bit more complicated than this in practice since **y** is first grouped by **z**, **fill**, or **stroke** into separate series.
+The normalize transform converts absolute values into relative ones. So, if **y** is [*y₀*, *y₁*, *y₂*, …] and the *first* basis is used with [normalizeY](#normalizeY), the resulting output **y** channel is [*y₀* / *y₀*, *y₁* / *y₀*, *y₂* / *y₀*, …]. But it’s a bit more complicated than this in practice since **y** is first grouped by **z**, **fill**, or **stroke** into separate series.
As another example, the normalize transform can be used to compute proportional demographics from absolute populations. The plot below compares the demographics of U.S. states: color represents age group, **y** represents the state, and **x** represents the proportion of the state’s population in that age group.
@@ -121,7 +121,7 @@ The **basis** option specifies how to normalize the series values; it is one of:
* a function to be passed an array of values, returning the desired basis
* a function to be passed an index and channel value array, returning the desired basis
-## normalize(*basis*)
+## normalize(*basis*) {#normalize}
```js
Plot.map({y: Plot.normalize("first")}, {x: "Date", y: "Close", stroke: "Symbol"})
@@ -129,13 +129,13 @@ Plot.map({y: Plot.normalize("first")}, {x: "Date", y: "Close", stroke: "Symbol"}
Returns a normalize map method for the given *basis*, suitable for use with the [map transform](./map.md).
-## normalizeX(*basis*, *options*)
+## normalizeX(*basis*, *options*) {#normalizeX}
```js
Plot.normalizeX("first", {y: "Date", x: "Close", stroke: "Symbol"})
```
-Like [mapX](./map.md#mapx-map-options), but applies the normalize map method with the given *basis*. The **basis** option can also be mixed into the specified *options* like so:
+Like [mapX](./map.md#mapX), but applies the normalize map method with the given *basis*. The **basis** option can also be mixed into the specified *options* like so:
```js
Plot.normalizeX({basis: "first", y: "Date", x: "Close", stroke: "Symbol"})
@@ -143,13 +143,13 @@ Plot.normalizeX({basis: "first", y: "Date", x: "Close", stroke: "Symbol"})
If not specified, the *basis* defaults to *first*.
-## normalizeY(*basis*, *options*)
+## normalizeY(*basis*, *options*) {#normalizeY}
```js
Plot.normalizeY("first", {x: "Date", y: "Close", stroke: "Symbol"})
```
-Like [mapY](./map.md#mapy-map-options), but applies the normalize map method with the given *basis*. The **basis** option can also be mixed into the specified *options* like so:
+Like [mapY](./map.md#mapY), but applies the normalize map method with the given *basis*. The **basis** option can also be mixed into the specified *options* like so:
```js
Plot.normalizeY({basis: "first", x: "Date", y: "Close", stroke: "Symbol"})
diff --git a/docs/transforms/select.md b/docs/transforms/select.md
index 66bbeff211..0905b8fd8a 100644
--- a/docs/transforms/select.md
+++ b/docs/transforms/select.md
@@ -19,7 +19,7 @@ onMounted(() => {
-# Select transform
+# Select transform
The **select transform** filters a mark’s index to show a subset of the data. It is a specialized [filter transform](./filter.md) that pulls a single value or a sample subset out of each series. For example, below selectLast is used to label the last value in a line chart.
@@ -69,7 +69,7 @@ Plot.plot({
```
:::
-## select(*selector*, *options*)
+## select(*selector*, *options*) {#select}
```js
Plot.select("first", {x: "Date", y: "Close"}) // selectFirst
@@ -130,7 +130,7 @@ function selectorSample(I) {
}
```
-## selectFirst(*options*)
+## selectFirst(*options*) {#selectFirst}
```js
Plot.selectFirst({x: "Date", y: "Close"})
@@ -138,7 +138,7 @@ Plot.selectFirst({x: "Date", y: "Close"})
Selects the first point of each series according to input order.
-## selectLast(*options*)
+## selectLast(*options*) {#selectLast}
```js
Plot.selectLast({x: "Date", y: "Close"})
@@ -146,7 +146,7 @@ Plot.selectLast({x: "Date", y: "Close"})
Selects the last point of each series according to input order.
-## selectMinX(*options*)
+## selectMinX(*options*) {#selectMinX}
```js
Plot.selectMinX({x: "Date", y: "Close"})
@@ -154,7 +154,7 @@ Plot.selectMinX({x: "Date", y: "Close"})
Selects the leftmost point of each series.
-## selectMinY(*options*)
+## selectMinY(*options*) {#selectMinY}
```js
Plot.selectMinY({x: "Date", y: "Close"})
@@ -162,7 +162,7 @@ Plot.selectMinY({x: "Date", y: "Close"})
Selects the lowest point of each series.
-## selectMaxX(*options*)
+## selectMaxX(*options*) {#selectMaxX}
```js
Plot.selectMaxX({x: "Date", y: "Close"})
@@ -170,7 +170,7 @@ Plot.selectMaxX({x: "Date", y: "Close"})
Selects the rightmost point of each series.
-## selectMaxY(*options*)
+## selectMaxY(*options*) {#selectMaxY}
```js
Plot.selectMaxY({x: "Date", y: "Close"})
diff --git a/docs/transforms/sort.md b/docs/transforms/sort.md
index 02032dd4fc..bad1dc26f7 100644
--- a/docs/transforms/sort.md
+++ b/docs/transforms/sort.md
@@ -60,7 +60,7 @@ Plot.plot({
Dots are sorted by descending **r** by default, so you may not need the **sort** option.
:::
-The sort transform can be applied either via the **sort** [mark option](../features/marks.md#mark-options), as above, or as an explicit [sort transform](#sort-order-options). The latter is generally only needed when composing multiple transforms, or to disambiguate the sort transform from imputed ordinal scale domains, *i.e.*, [scale sorting](../features/scales.md#sort-mark-option).
+The sort transform can be applied either via the **sort** [mark option](../features/marks.md#mark-options), as above, or as an explicit [sort transform](#sort). The latter is generally only needed when composing multiple transforms, or to disambiguate the sort transform from imputed ordinal scale domains, *i.e.*, [scale sorting](../features/scales.md#sort-mark-option).
As another example, in the line chart of unemployment rates below, lines for metropolitan areas in Michigan (which saw exceptionally high unemployment following the [financial crisis of 2008](https://en.wikipedia.org/wiki/2007–2008_financial_crisis), in part due to the [auto industry collapse](https://en.wikipedia.org/wiki/2008–2010_automotive_industry_crisis)) are highlighted in red , and the **sort** option is used to draw them on top of other series.
@@ -119,9 +119,9 @@ Plot.plot({
```
:::
-The closely-related [reverse transform](#reverse-options) likewise reverses the mark index, while the [shuffle transform](#shuffle-options) for randomizes the mark index’s order.
+The closely-related [reverse transform](#reverse) likewise reverses the mark index, while the [shuffle transform](#shuffle) for randomizes the mark index’s order.
-## sort(*order*, *options*)
+## sort(*order*, *options*) {#sort}
```js
Plot.sort("body_mass_g", {x: "culmen_length_mm", y: "culmen_depth_mm"})
@@ -134,19 +134,19 @@ Sorts the data by the specified *order*, which is one of:
- a field name
- a {*channel*, *order*} object
-In the object case, the **channel** option specifies the name of the channel, while the **order** option specifies *ascending* (the default) or *descending* order. You can also use the shorthand *-name* to sort by descending order of the channel with the given *name*. For example, `sort: {channel: "-r"}` will sort by descending radius (**r**).
+In the object case, the **channel** option specifies the name of the channel, while the **order** option specifies *ascending* (the default) or *descending* order. You can also use the shorthand *-name* to sort by descending order of the channel with the given *name*. For example, `sort: {channel: "-r"}` will sort by descending radius (**r**).
In the function case, if the sort function does not take exactly one argument, it is interpreted as a comparator function; otherwise it is interpreted as an accessor function.
-## shuffle(*options*)
+## shuffle(*options*) {#shuffle}
```js
Plot.shuffle({x: "culmen_length_mm", y: "culmen_depth_mm"})
```
-Shuffles the data randomly. If a **seed** option is specified, a [linear congruential generator](https://github.com/d3/d3-random/blob/main/README.md#randomLcg) with the given seed is used to generate random numbers; otherwise, Math.random is used.
+Shuffles the data randomly. If a **seed** option is specified, a [linear congruential generator](https://d3js.org/d3-random#randomLcg) with the given seed is used to generate random numbers; otherwise, Math.random is used.
-## reverse(*options*)
+## reverse(*options*) {#reverse}
```js
Plot.reverse({x: "culmen_length_mm", y: "culmen_depth_mm"})
diff --git a/docs/transforms/stack.md b/docs/transforms/stack.md
index 9350b55834..2275c52b75 100644
--- a/docs/transforms/stack.md
+++ b/docs/transforms/stack.md
@@ -51,9 +51,9 @@ const likert = Likert([
# Stack transform
-The **stack transform** comes in two orientations: [stackY](#stacky-stack-options) replaces **y** with **y1** and **y2** to form vertical↑ stacks grouped on **x**, while [stackX](#stackx-stack-options) replaces **x** with **x1** and **x2** for horizontal→ stacks grouped on **y**. In effect, stacking transforms a *length* into *lower* and *upper* positions: the upper position of each element equals the lower position of the next element in the stack. Stacking makes it easier to perceive a total while still showing its parts.
+The **stack transform** comes in two orientations: [stackY](#stackY) replaces **y** with **y1** and **y2** to form vertical↑ stacks grouped on **x**, while [stackX](#stackX) replaces **x** with **x1** and **x2** for horizontal→ stacks grouped on **y**. In effect, stacking transforms a *length* into *lower* and *upper* positions: the upper position of each element equals the lower position of the next element in the stack. Stacking makes it easier to perceive a total while still showing its parts.
-For example, below is a stacked area chart of [deaths in the Crimean War](https://en.wikipedia.org/wiki/Florence_Nightingale#Crimean_War)—predominantly from disease —using Florence Nightingale’s data.
+For example, below is a stacked area chart of [deaths in the Crimean War](https://en.wikipedia.org/wiki/Florence_Nightingale#Crimean_War) — predominantly from disease — using Florence Nightingale’s data.
:::plot https://observablehq.com/@observablehq/plot-crimean-war-casualties
```js
@@ -278,7 +278,7 @@ Plot.plot({
marks: [
Plot.dot(
congress,
- Plot.stackY({
+ Plot.stackY2({
x: (d) => 2023 - d.birthday.getUTCFullYear(),
y: (d) => d.gender === "M" ? 1 : -1,
fill: "gender",
@@ -371,7 +371,7 @@ The following **order** methods are supported:
- a named field or function of data - order data by priority
- an array of *z* values
-The **reverse** option reverses the effective order. For the *value* order, stackY uses the *y* value while stackX uses the *x* value. For the *appearance* order, stackY uses the *x* position of the maximum *y* value while stackX uses the *y* position of the maximum *x* value. If an array of *z* values are specified, they should enumerate the *z* values for all series in the desired order; this array is typically hard-coded or computed with [d3.groupSort](https://github.com/d3/d3-array/blob/main/README.md#groupSort). Note that the input order (null) and *value* order can produce crossing paths: they do not guarantee a consistent series order across stacks.
+The **reverse** option reverses the effective order. For the *value* order, stackY uses the *y* value while stackX uses the *x* value. For the *appearance* order, stackY uses the *x* position of the maximum *y* value while stackX uses the *y* position of the maximum *x* value. If an array of *z* values are specified, they should enumerate the *z* values for all series in the desired order; this array is typically hard-coded or computed with [d3.groupSort](https://d3js.org/d3-array/group#groupSort). Note that the input order (null) and *value* order can produce crossing paths: they do not guarantee a consistent series order across stacks.
The stack transform supports diverging stacks: negative values are stacked below zero while positive values are stacked above zero. For stackY, the **y1** channel contains the value of lesser magnitude (closer to zero) while the **y2** channel contains the value of greater magnitude (farther from zero); the difference between the two corresponds to the input **y** channel value. For stackX, the same is true, except for **x1**, **x2**, and **x** respectively.
@@ -391,7 +391,7 @@ In addition to the **y1** and **y2** output channels, stackY computes a **y** ou
If two arguments are passed to the stack transform functions below, the stack-specific options (**offset**, **order**, and **reverse**) are pulled exclusively from the first *options* argument, while any channels (*e.g.*, **x**, **y**, and **z**) are pulled from second *options* argument. Options from the second argument that are not consumed by the stack transform will be passed through. Using two arguments is sometimes necessary is disambiguate the option recipient when chaining transforms.
-## stackY(*stack*, *options*)
+## stackY(*stack*, *options*) {#stackY}
```js
Plot.stackY({x: "year", y: "revenue", z: "format", fill: "group"})
@@ -399,42 +399,42 @@ Plot.stackY({x: "year", y: "revenue", z: "format", fill: "group"})
Creates new channels **y1** and **y2**, obtained by stacking the original **y** channel for data points that share a common **x** (and possibly **z**) value. A new **y** channel is also returned, which lazily computes the middle value of **y1** and **y2**. The input **y** channel defaults to a constant 1, resulting in a count of the data points. The stack options (**offset**, **order**, and **reverse**) may be specified as part of the *options* object, if the only argument, or as a separate *stack* options argument.
-## stackY1(*stack*, *options*)
+## stackY1(*stack*, *options*) {#stackY1}
```js
Plot.stackY1({x: "year", y: "revenue", z: "format", fill: "group"})
```
-Like [stackY](#stacky-stack-options), except that the **y1** channel is returned as the **y** channel. This can be used, for example, to draw a line at the bottom of each stacked area.
+Like [stackY](#stackY), except that the **y1** channel is returned as the **y** channel. This can be used, for example, to draw a line at the bottom of each stacked area.
-## stackY2(*stack*, *options*)
+## stackY2(*stack*, *options*) {#stackY2}
```js
Plot.stackY2({x: "year", y: "revenue", z: "format", fill: "group"})
```
-Like [stackY](#stacky-stack-options), except that the **y2** channel is returned as the **y** channel. This can be used, for example, to draw a line at the top of each stacked area.
+Like [stackY](#stackY), except that the **y2** channel is returned as the **y** channel. This can be used, for example, to draw a line at the top of each stacked area.
-## stackX(*stack*, *options*)
+## stackX(*stack*, *options*) {#stackX}
```js
Plot.stackX({y: "year", x: "revenue", z: "format", fill: "group"})
```
-Like [stackY](#stacky-stack-options), but with *x* as the input value channel, *y* as the stack index, *x1*, *x2* and *x* as the output channels.
+Like [stackY](#stackY), but with *x* as the input value channel, *y* as the stack index, *x1*, *x2* and *x* as the output channels.
-## stackX1(*stack*, *options*)
+## stackX1(*stack*, *options*) {#stackX1}
```js
Plot.stackX1({y: "year", x: "revenue", z: "format", fill: "group"})
```
-Like [stackX](#stackx-stack-options), except that the **x1** channel is returned as the **x** channel. This can be used, for example, to draw a line at the left edge of each stacked area.
+Like [stackX](#stackX), except that the **x1** channel is returned as the **x** channel. This can be used, for example, to draw a line at the left edge of each stacked area.
-## stackX2(*stack*, *options*)
+## stackX2(*stack*, *options*) {#stackX2}
```js
Plot.stackX2({y: "year", x: "revenue", z: "format", fill: "group"})
```
-Like [stackX](#stackx-stack-options), except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area.
+Like [stackX](#stackX), except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area.
diff --git a/docs/transforms/tree.md b/docs/transforms/tree.md
index 8c573c0dd0..c836b30f65 100644
--- a/docs/transforms/tree.md
+++ b/docs/transforms/tree.md
@@ -23,9 +23,9 @@ function indent() {
-# Tree transform
+# Tree transform
-The **tree transform** is rarely used directly; the two variants, [treeNode](#treenode-options) and [treeLink](#treelink-options), are typically used internally by the composite [tree mark](../marks/tree.md). The tree transform arranges a tabular dataset into a hierarchy according to the given **path** channel, which is typically a slash-separated string; it then executes a tree layout algorithm to compute **x** and **y**; these channels can then be used to construct a node-link diagram.
+The **tree transform** is rarely used directly; the two variants, [treeNode](#treeNode) and [treeLink](#treeLink), are typically used internally by the composite [tree mark](../marks/tree.md). The tree transform arranges a tabular dataset into a hierarchy according to the given **path** channel, which is typically a slash-separated string; it then executes a tree layout algorithm to compute **x** and **y**; these channels can then be used to construct a node-link diagram.
As a contrived example, we can construct the equivalent of the tree mark using a [link](../marks/link.md), [dot](../marks/dot.md), and [text](../marks/text.md), and the corresponding tree transforms.
@@ -66,20 +66,20 @@ The **path** column is typically slash-separated, as with UNIX-based file system
The following options control how the node-link diagram is laid out:
-* **treeLayout** - a tree layout algorithm; defaults to [d3.tree](https://github.com/d3/d3-hierarchy/blob/main/README.md#tree)
+* **treeLayout** - a tree layout algorithm; defaults to [d3.tree](https://d3js.org/d3-hierarchy/tree)
* **treeAnchor** - a tree layout orientation, either *left* or *right*; defaults to *left*
* **treeSort** - a node comparator, or null to preserve input order
* **treeSeparation** - a node separation function, or null for uniform separation
-The default **treeLayout** implements the Reingold–Tilford “tidy” algorithm based on Buchheim _et al._’s linear time approach. Use [d3.cluster](https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster) instead to align leaf nodes; see also the [cluster mark](../marks/tree.md#cluster-data-options).
+The default **treeLayout** implements the Reingold–Tilford “tidy” algorithm based on Buchheim _et al._’s linear time approach. Use [d3.cluster](https://d3js.org/d3-hierarchy/cluster) instead to align leaf nodes; see also the [cluster mark](../marks/tree.md#cluster).
If **treeAnchor** is *left*, the root of the tree will be aligned with the left side of the frame; if **treeAnchor** is *right*, the root of the tree will be aligned with the right side of the frame; use the **insetLeft** and **insetRight** [scale options](../features/scales.md) if horizontal padding is desired, say to make room for labels.
-If the **treeSort** option is not null, it is typically a function that is passed two nodes in the hierarchy and compares them, similar to [_array_.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort); see [d3-hierarchy’s _node_.sort](https://github.com/d3/d3-hierarchy/blob/main/README.md#node_sort) for more. The **treeSort** option can also be specified as a string, in which case it refers either to a named column in data, or if it starts with “node:”, a node value.
+If the **treeSort** option is not null, it is typically a function that is passed two nodes in the hierarchy and compares them, similar to [_array_.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort); see [d3-hierarchy’s _node_.sort](https://d3js.org/d3-hierarchy/hierarchy#node_sort) for more. The **treeSort** option can also be specified as a string, in which case it refers either to a named column in data, or if it starts with “node:”, a node value.
-If the **treeSeparation** is not null, it is a function that is passed two nodes in the hierarchy and returns the desired (relative) amount of separation; see [d3-hierarchy’s _tree_.separation](https://github.com/d3/d3-hierarchy/blob/main/README.md#tree_separation) for more. By default, non-siblings are at least twice as far apart as siblings.
+If the **treeSeparation** is not null, it is a function that is passed two nodes in the hierarchy and returns the desired (relative) amount of separation; see [d3-hierarchy’s _tree_.separation](https://d3js.org/d3-hierarchy/tree#tree_separation) for more. By default, non-siblings are at least twice as far apart as siblings.
-## treeNode(*options*)
+## treeNode(*options*) {#treeNode}
Populates **x** and **y** with the positions for each node in the tree. The default **frameAnchor** inherits the **treeAnchor**. This transform is often used with the [dot](../marks/dot.md) or [text](../marks/text.md) mark.
@@ -88,12 +88,13 @@ The treeNode transform will derive output columns for any *options* that have on
* *node:name* - the node’s name (the last part of its path)
* *node:path* - the node’s full, normalized, slash-separated path
* *node:internal* - true if the node is internal, or false for leaves
+* *node:external* - true if the node is a leaf, or false for internal nodes
* *node:depth* - the distance from the node to the root
* *node:height* - the distance from the node to its deepest descendant
In addition, if any option value is specified as an object with a **node** method, a derived output column will be generated by invoking the **node** method for each node in the tree.
-## treeLink(*options*)
+## treeLink(*options*) {#treeLink}
Populates **x1**, **y1**, **x2**, and **y2** with the positions for each link in the tree, where **x1** & **y1** represents the position of the parent node and **x2** & **y2** the position of the child node. The default **curve** is *bump-x*, the default **stroke** is #555, the default **strokeWidth** is 1.5, and the default **strokeOpacity** is 0.5. This transform is often used with the [link](../marks/link.md) or [arrow](../marks/arrow.md) mark.
@@ -102,6 +103,7 @@ The treeLink transform will likewise derive output columns for any *options* tha
* *node:name* - the child node’s name (the last part of its path)
* *node:path* - the child node’s full, normalized, slash-separated path
* *node:internal* - true if the child node is internal, or false for leaves
+* *node:external* - true if the child node is a leaf, or false for internal nodes
* *node:depth* - the distance from the child node to the root
* *node:height* - the distance from the child node to its deepest descendant
* *parent:name* - the parent node’s name (the last part of its path)
diff --git a/docs/transforms/window.md b/docs/transforms/window.md
index 6b0eefc6c9..592454e2d1 100644
--- a/docs/transforms/window.md
+++ b/docs/transforms/window.md
@@ -84,7 +84,7 @@ Plot.plot({
The window transform uses input order, not natural order by value, to determine the meaning of *start* and *end*. When the data is in reverse chronological order, the meaning of *start* and *end* is effectively reversed because the first data point is the most recent. Use a [sort transform](./sort.md) to change the order as needed.
-If **strict** is false (the default), the window size is effectively reduced at the start or end of each series or both, depending on the **anchor**. Values computed with a truncated window may be noisy; if you would prefer to not show this data instead, set the **strict** option to true.
+If **strict** is false (the default), the window size is effectively reduced at the start or end of each series or both, depending on the **anchor**. Values computed with a truncated window may be noisy; if you would prefer to not show this data instead, set the **strict** option to true. The **strict** option can also have a dramatic effect if some data is missing: when strict, the reducer will be skipped if any of the values in the current window are null, undefined, or NaN.
@@ -108,8 +108,6 @@ Plot.plot({
```
:::
-The **strict** option can also have a dramatic effect if some data is missing: when strict, the reducer will be skipped if any of the values in the current window are null, undefined, or NaN.
-
The **reduce** option specifies how to compute the output value for the current window. It defaults to *mean* for a rolling average. Below, the rolling minimum , maximum , and median are shown. The window transform supports most of the same reducers as [bin](./bin.md) and [group](./group.md), and you can implement a custom reducer as a function if needed.
:::plot https://observablehq.com/@observablehq/plot-window-reduce
@@ -183,7 +181,7 @@ The following named reducers are supported:
A reducer may also be specified as a function to be passed an index of size **k** and the corresponding input channel array; or if the function only takes one argument, an array of **k** values.
-## window(*k*)
+## window(*k*) {#window}
```js
Plot.map({y: Plot.window(24)}, {x: "Date", y: "Close", stroke: "Symbol"})
@@ -191,18 +189,18 @@ Plot.map({y: Plot.window(24)}, {x: "Date", y: "Close", stroke: "Symbol"})
Returns a window map method for the given window size *k*, suitable for use with Plot.map. For additional options to the window transform, replace the number *k* with an object with properties **k**, **anchor**, **reduce**, or **strict**.
-## windowX(*k*, *options*)
+## windowX(*k*, *options*) {#windowX}
```js
Plot.windowX(24, {y: "Date", x: "Anomaly"})
```
-Like [mapX](./map.md#mapx-map-options), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties **k**, **anchor**, or **reduce**.
+Like [mapX](./map.md#mapX), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties **k**, **anchor**, or **reduce**.
-## windowY(*k*, *options*)
+## windowY(*k*, *options*) {#windowY}
```js
Plot.windowY(24, {x: "Date", y: "Anomaly"})
```
-Like [mapY](./map.md#mapy-map-options), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties **k**, **anchor**, or **reduce**.
+Like [mapY](./map.md#mapY), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties **k**, **anchor**, or **reduce**.
diff --git a/docs/what-is-plot.md b/docs/what-is-plot.md
index 6ab71cf364..84a8813175 100644
--- a/docs/what-is-plot.md
+++ b/docs/what-is-plot.md
@@ -2,7 +2,9 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
-import {shallowRef, onMounted} from "vue";
+import {computed, onMounted, shallowRef} from "vue";
+import {useData} from "vitepress";
+import PlotRender from "./components/PlotRender.js";
const olympians = shallowRef([
{weight: 31, height: 1.21, sex: "female"},
@@ -13,6 +15,28 @@ onMounted(() => {
d3.csv("./data/athletes.csv", d3.autoType).then((data) => (olympians.value = data));
});
+const {site: {value: {themeConfig: {sidebar}}}} = useData();
+
+const paths = computed(() => {
+ const paths = [];
+ (function visit(node, path) {
+ paths.push({path, link: node.link && `.${node.link}`});
+ if (node.items) {
+ for (const item of node.items) {
+ visit(item, (path === "/" ? path : path + "/") + item.text);
+ }
+ }
+ })({items: sidebar}, "/Plot");
+ return paths;
+});
+
+// https://github.com/observablehq/plot/issues/1703
+function computeTreeWidth(paths) {
+ const root = d3.tree().nodeSize([1, 1])(d3.stratify().path((d) => d.path)(paths));
+ const [x1, x2] = d3.extent(root, (d) => d.x);
+ return x2 - x1;
+}
+
# What is Plot?
@@ -68,3 +92,19 @@ Plot.plot({
})
```
:::
+
+## What can Plot do?
+
+Because marks are composable, and because you can extend Plot with custom marks, you can make almost anything with it — much more than the charts above! The following [tree diagram](./marks/tree.md) of the documentation gives a sense of what’s ”in the box” with Plot. Peruse our [gallery of examples](https://observablehq.com/@observablehq/plot-gallery) for more inspiration.
+
+
diff --git a/docs/why-plot.md b/docs/why-plot.md
index fc3116463a..236e62a935 100644
--- a/docs/why-plot.md
+++ b/docs/why-plot.md
@@ -19,7 +19,7 @@ function arealineY(data, {color, fillOpacity = 0.1, ...options} = {}) {
**Observable Plot** is for exploratory data visualization. It’s for finding insights quickly. Its API, while expressive and configurable, optimizes for conciseness and memorability. We want the time to first chart to be as fast as possible.
-And the speed doesn’t stop there: Plot helps you quickly pivot and refine your views of data. Our hope with Plot is that you’ll spend less time reading the docs, searching for code to copy-paste, and debugging—and more time asking questions of data.
+And the speed doesn’t stop there: Plot helps you quickly pivot and refine your views of data. Our hope with Plot is that you’ll spend less time reading the docs, searching for code to copy-paste, and debugging — and more time asking questions of data.
Compared to other visualization tools, including low-level tools such as D3 and less expressive high-level tools such as chart templates, we think you’ll be more productive exploring data with Plot. You’ll spend more time “using vision to think” and less time wrangling the machinery of programming.
@@ -35,7 +35,7 @@ Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", stroke: "specie
```
:::
-What makes Plot concise? In a word: *defaults*. If you specify the semantics—your data and the desired encodings—Plot will figure out the rest.
+What makes Plot concise? In a word: *defaults*. If you specify the semantics — your data and the desired encodings — Plot will figure out the rest.
The beauty of defaults is that you can override them as needed. This is ideal for exploring: you invest minimally in the initial chart, and as you start to see something interesting, you progressively customize to improve the display. Perhaps the plot above would be easier to read with an aspect ratio proportional to the data, a grid, and a legend?
@@ -163,7 +163,7 @@ We’ve long said that *D3 makes things possible, not necessarily easy.* And tha
**Plot’s goal is to make the easy things easy, and fast, and then some.**
:::tip
-Whether or not Plot succeeds at this goal is up to you—so we’d love [your feedback](https://talk.observablehq.com/c/site-feedback/3) on what you find easy or hard to do with Plot. And we encourage you to [ask for help](https://talk.observablehq.com/c/help/6) when you get stuck. We learn a lot from helping!
+Whether or not Plot succeeds at this goal is up to you — so we’d love [your feedback](https://talk.observablehq.com/c/site-feedback/3) on what you find easy or hard to do with Plot. And we encourage you to [ask for help](https://talk.observablehq.com/c/help/6) when you get stuck. We learn a lot from helping!
:::
Since Plot and D3 have different goals, they make different trade-offs. Plot is more efficient: you can make charts quickly. But it is also necessarily less expressive: bespoke visualizations with extensive animation and interaction, advanced techniques like force-directed graph layout, or even developing your own charting library, are better done with D3’s low-level API.
diff --git a/img/arc-diagram.png b/img/arc-diagram.png
new file mode 100644
index 0000000000..d078000eea
Binary files /dev/null and b/img/arc-diagram.png differ
diff --git a/img/auto-bar.png b/img/auto-bar.png
new file mode 100644
index 0000000000..7709798871
Binary files /dev/null and b/img/auto-bar.png differ
diff --git a/img/barycentric-before-after.png b/img/barycentric-before-after.png
new file mode 100644
index 0000000000..b49c91e993
Binary files /dev/null and b/img/barycentric-before-after.png differ
diff --git a/img/bollinger.png b/img/bollinger.png
new file mode 100644
index 0000000000..7782daf54b
Binary files /dev/null and b/img/bollinger.png differ
diff --git a/img/clip.png b/img/clip.png
new file mode 100644
index 0000000000..8bc61834fd
Binary files /dev/null and b/img/clip.png differ
diff --git a/img/piecewise-rainbow.png b/img/piecewise-rainbow.png
new file mode 100644
index 0000000000..9bb72f33af
Binary files /dev/null and b/img/piecewise-rainbow.png differ
diff --git a/img/time-axis-after.png b/img/time-axis-after.png
new file mode 100644
index 0000000000..25ab0898dc
Binary files /dev/null and b/img/time-axis-after.png differ
diff --git a/img/time-axis-before.png b/img/time-axis-before.png
new file mode 100644
index 0000000000..0369e4abc8
Binary files /dev/null and b/img/time-axis-before.png differ
diff --git a/img/title-subtitle.png b/img/title-subtitle.png
new file mode 100644
index 0000000000..40a307a6ad
Binary files /dev/null and b/img/title-subtitle.png differ
diff --git a/img/tree-gods.png b/img/tree-gods.png
new file mode 100644
index 0000000000..8940d7ef4a
Binary files /dev/null and b/img/tree-gods.png differ
diff --git a/package.json b/package.json
index d04286f21f..52e38cbfa3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@observablehq/plot",
"description": "A JavaScript library for exploratory data visualization.",
- "version": "0.6.8",
+ "version": "0.6.10",
"author": {
"name": "Observable, Inc.",
"url": "https://observablehq.com"
@@ -47,13 +47,14 @@
],
"devDependencies": {
"@esbuild-kit/core-utils": "^3.1.0",
- "@rollup/plugin-commonjs": "^24.0.1",
+ "@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.4.0",
"@types/d3": "^7.4.0",
- "@typescript-eslint/eslint-plugin": "^5.54.1",
- "@typescript-eslint/parser": "^5.54.1",
+ "@types/node": "^20.5.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
"canvas": "^2.0.0",
"d3-geo-projection": "^4.0.0",
"eslint": "^8.16.0",
@@ -61,16 +62,17 @@
"get-tsconfig": "^4.1.0",
"htl": "^0.3.0",
"js-beautify": "1",
- "jsdom": "^21.0.0",
+ "jsdom": "^22.1.0",
"markdown-it-container": "^3.0.0",
"mocha": "^10.0.0",
"module-alias": "^2.0.0",
- "prettier": "^2.7.1",
+ "prettier": "^3.0.0",
"rollup": "^3.7.0",
"topojson-client": "^3.1.0",
+ "ts-morph": "^19.0.0",
"typescript": "^5.0.2",
- "vite": "^4.0.0",
- "vitepress": "^1.0.0-beta.2"
+ "vite": "4.4.7",
+ "vitepress": "1.0.0-beta.7"
},
"dependencies": {
"d3": "^7.8.0",
diff --git a/src/axes.js b/src/axes.js
index 8ff27f4084..6dc4026230 100644
--- a/src/axes.js
+++ b/src/axes.js
@@ -1,22 +1,5 @@
-import {format, utcFormat} from "d3";
-import {formatIsoDate} from "./format.js";
-import {constant, isTemporal, string} from "./options.js";
import {isOrdinalScale} from "./scales.js";
export function inferFontVariant(scale) {
return isOrdinalScale(scale) && scale.interval === undefined ? undefined : "tabular-nums";
}
-
-// D3 doesn’t provide a tick format for ordinal scales; we want shorthand when
-// an ordinal domain is numbers or dates, and we want null to mean the empty
-// string, not the default identity format. TODO Remove this in favor of the
-// axis mark’s inferTickFormat.
-export function maybeAutoTickFormat(tickFormat, domain) {
- return tickFormat === undefined
- ? isTemporal(domain)
- ? formatIsoDate
- : string
- : typeof tickFormat === "function"
- ? tickFormat
- : (typeof tickFormat === "string" ? (isTemporal(domain) ? utcFormat : format) : constant)(tickFormat);
-}
diff --git a/src/context.d.ts b/src/context.d.ts
index 84b8d13646..ce2c3568d8 100644
--- a/src/context.d.ts
+++ b/src/context.d.ts
@@ -1,4 +1,5 @@
import type {GeoStreamWrapper} from "d3";
+import type {MarkOptions} from "./mark.js";
/** Additional rendering context provided to marks and initializers. */
export interface Context {
@@ -16,4 +17,7 @@ export interface Context {
/** The current projection, if any. */
projection?: GeoStreamWrapper;
+
+ /** The default clip for all marks. */
+ clip?: MarkOptions["clip"];
}
diff --git a/src/context.js b/src/context.js
index c1911f9118..32faa96f3a 100644
--- a/src/context.js
+++ b/src/context.js
@@ -1,8 +1,9 @@
import {creator, select} from "d3";
+import {maybeClip} from "./style.js";
export function createContext(options = {}) {
- const {document = typeof window !== "undefined" ? window.document : undefined} = options;
- return {document};
+ const {document = typeof window !== "undefined" ? window.document : undefined, clip} = options;
+ return {document, clip: maybeClip(clip)};
}
export function create(name, {document}) {
diff --git a/src/curve.d.ts b/src/curve.d.ts
index c3a8668ae9..69acaf77b3 100644
--- a/src/curve.d.ts
+++ b/src/curve.d.ts
@@ -56,8 +56,9 @@ export interface CurveOptions extends CurveAutoOptions {
* * *step-before* - a piecewise constant function where *x* changes after *y*
*
* If *curve* is a function, it will be invoked with a given CanvasPath
- * *context* in the same fashion as a [D3 curve
- * factory](https://github.com/d3/d3-shape/blob/main/README.md#custom-curves).
+ * *context* in the same fashion as a [D3 curve factory][1].
+ *
+ * [1]: https://d3js.org/d3-shape/curve#custom-curves
*/
curve?: Curve;
}
@@ -92,8 +93,9 @@ export interface CurveAutoOptions {
* The *auto* curve is typically used in conjunction with a spherical
* projection to interpolate along geodesics. If *curve* is a function, it
* will be invoked with a given CanvasPath *context* in the same fashion as a
- * [D3 curve
- * factory](https://github.com/d3/d3-shape/blob/main/README.md#custom-curves).
+ * [D3 curve factory][1].
+ *
+ * [1]: https://d3js.org/d3-shape/curve#custom-curves
*/
curve?: Curve | "auto";
@@ -101,12 +103,12 @@ export interface CurveAutoOptions {
* The tension option only has an effect on bundle, cardinal and Catmull–Rom
* splines (*bundle*, *cardinal*, *cardinal-open*, *cardinal-closed*,
* *catmull-rom*, *catmull-rom-open*, and *catmull-rom-closed*). For bundle
- * splines, it corresponds to
- * [beta](https://github.com/d3/d3-shape/blob/main/README.md#curveBundle_beta);
- * for cardinal splines,
- * [tension](https://github.com/d3/d3-shape/blob/main/README.md#curveCardinal_tension);
- * for Catmull–Rom splines,
- * [alpha](https://github.com/d3/d3-shape/blob/main/README.md#curveCatmullRom_alpha).
+ * splines, it corresponds to [beta][1]; for cardinal splines, [tension][2];
+ * for Catmull–Rom splines, [alpha][3].
+ *
+ * [1]: https://d3js.org/d3-shape/curve#curveBundle_beta
+ * [2]: https://d3js.org/d3-shape/curve#curveCardinal_tension
+ * [3]: https://d3js.org/d3-shape/curve#curveCatmullRom_alpha
*/
tension?: number;
}
diff --git a/src/format.d.ts b/src/format.d.ts
index 60fd7c59a4..160041cd36 100644
--- a/src/format.d.ts
+++ b/src/format.d.ts
@@ -2,11 +2,11 @@
* Returns a function that formats a given month number (from 0 = January to 11
* = December) according to the specified *locale* and *format*.
*
- * @param locale a [BCP 47 language tag](https://tools.ietf.org/html/bcp47);
- * defaults to U.S. English.
- * @param format a [month
- * format](https://tc39.es/ecma402/#datetimeformat-objects): either *2-digit*,
- * *numeric*, *narrow*, *short*, *long*; defaults to *short*.
+ * [1]: https://tools.ietf.org/html/bcp47
+ * [2]: https://tc39.es/ecma402/#datetimeformat-objects
+ *
+ * @param locale - a [BCP 47 language tag][1]; defaults to U.S. English.
+ * @param format - a [month format][2]; defaults to *short*.
*/
export function formatMonth(
locale?: string,
@@ -17,11 +17,11 @@ export function formatMonth(
* Returns a function that formats a given week day number (from 0 = Sunday to 6
* = Saturday) according to the specified *locale* and *format*.
*
- * @param locale a [BCP 47 language tag](https://tools.ietf.org/html/bcp47);
- * defaults to U.S. English.
- * @param format a [weekday
- * format](https://tc39.es/ecma402/#datetimeformat-objects): either *narrow*,
- * *short*, or *long*; defaults to *short*.
+ * [1]: https://tools.ietf.org/html/bcp47
+ * [2]: https://tc39.es/ecma402/#datetimeformat-objects
+ *
+ * @param locale a [BCP 47 language tag][1]; defaults to U.S. English.
+ * @param format a [weekday format][2]; defaults to *short*.
*/
export function formatWeekday(locale?: string, format?: "long" | "short" | "narrow"): (i: number) => string;
diff --git a/src/index.d.ts b/src/index.d.ts
index 19e9442a31..24e2344eef 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -14,6 +14,7 @@ export * from "./marks/arrow.js";
export * from "./marks/auto.js";
export * from "./marks/axis.js";
export * from "./marks/bar.js";
+export * from "./marks/bollinger.js";
export * from "./marks/box.js";
export * from "./marks/cell.js";
export * from "./marks/contour.js";
diff --git a/src/index.js b/src/index.js
index fec7e243ec..98f5772b0d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
export {auto, autoSpec} from "./marks/auto.js";
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
+export {bollinger, bollingerX, bollingerY} from "./marks/bollinger.js";
export {boxX, boxY} from "./marks/box.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
export {Contour, contour} from "./marks/contour.js";
diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js
index 72f1b2d7a8..c92addcff7 100644
--- a/src/interactions/pointer.js
+++ b/src/interactions/pointer.js
@@ -21,6 +21,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
// outermost render function because it will re-render dynamically in
// response to pointer events.
render: composeRender(function (index, scales, values, dimensions, context, next) {
+ context = {...context, pointerSticky: false};
const svg = context.ownerSVGElement;
const {data} = context.getMarkState(this);
@@ -68,6 +69,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
let i; // currently focused index
let g; // currently rendered mark
+ let s; // currently rendered stickiness
let f; // current animation frame
// When faceting, if more than one pointer would be visible, only show
@@ -82,8 +84,8 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
facetState.set(index.fi, ri);
f = requestAnimationFrame(() => {
f = null;
- for (const r of facetState.values()) {
- if (r < ri) {
+ for (const [fi, r] of facetState) {
+ if (r < ri || (r === ri && fi < index.fi)) {
ii = null;
break;
}
@@ -97,8 +99,9 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
}
function render(ii) {
- if (i === ii) return; // the tooltip hasn’t moved
+ if (i === ii && s === state.sticky) return; // the tooltip hasn’t moved
i = ii;
+ s = context.pointerSticky = state.sticky;
const I = i == null ? [] : [i];
if (faceted) (I.fx = index.fx), (I.fy = index.fy), (I.fi = index.fi);
const r = next(I, scales, values, dimensions, context);
@@ -120,22 +123,36 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
g.replaceWith(r);
}
state.roots[renderIndex] = g = r;
- context.dispatchValue(i == null ? null : data[i]);
+
+ // Dispatch the value. When simultaneously exiting this facet and
+ // entering a new one, prioritize the entering facet.
+ if (!(i == null && facetState?.size > 1)) context.dispatchValue(i == null ? null : data[i]);
return r;
}
+ // Select the closest point to the mouse in the current facet; for
+ // pointerX or pointerY, the orthogonal component of the distance is
+ // squashed, selecting primarily on the dominant dimension. Across facets,
+ // use unsquashed distance to determine the winner.
function pointermove(event) {
if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging
let [xp, yp] = pointof(event);
(xp -= tx), (yp -= ty); // correct for facets and band scales
+ const kpx = xp < dimensions.marginLeft || xp > dimensions.width - dimensions.marginRight ? 1 : kx;
+ const kpy = yp < dimensions.marginTop || yp > dimensions.height - dimensions.marginBottom ? 1 : ky;
let ii = null;
let ri = maxRadius * maxRadius;
for (const j of index) {
- const dx = kx * (px(j) - xp);
- const dy = ky * (py(j) - yp);
+ const dx = kpx * (px(j) - xp);
+ const dy = kpy * (py(j) - yp);
const rj = dx * dx + dy * dy;
if (rj <= ri) (ii = j), (ri = rj);
}
+ if (ii != null && (kx !== 1 || ky !== 1)) {
+ const dx = px(ii) - xp;
+ const dy = py(ii) - yp;
+ ri = dx * dx + dy * dy;
+ }
update(ii, ri);
}
@@ -144,7 +161,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
if (i == null) return; // not pointing
if (state.sticky && state.roots.some((r) => r?.contains(event.target))) return; // stay sticky
if (state.sticky) (state.sticky = false), state.renders.forEach((r) => r(null)); // clear all pointers
- else state.sticky = true;
+ else (state.sticky = true), render(i);
event.stopImmediatePropagation(); // suppress other pointers
}
diff --git a/src/legends.d.ts b/src/legends.d.ts
index 6e7e28055e..38dcaf159d 100644
--- a/src/legends.d.ts
+++ b/src/legends.d.ts
@@ -23,8 +23,8 @@ export interface LegendOptions {
* domain is dates, the tick format may also be expressed as a [d3-time-format
* string][2].
*
- * [1]: https://github.com/d3/d3-format/blob/main/README.md#locale_format
- * [2]: https://github.com/d3/d3-time-format/blob/main/README.md#locale_format
+ * [1]: https://d3js.org/d3-format#locale_format
+ * [2]: https://d3js.org/d3-time-format#locale_format
*/
tickFormat?: ScaleOptions["tickFormat"];
diff --git a/src/legends/swatches.js b/src/legends/swatches.js
index 560645377f..5685f5c9e3 100644
--- a/src/legends/swatches.js
+++ b/src/legends/swatches.js
@@ -1,9 +1,10 @@
import {pathRound as path} from "d3";
-import {inferFontVariant, maybeAutoTickFormat} from "../axes.js";
-import {createContext, create} from "../context.js";
+import {inferFontVariant} from "../axes.js";
+import {create, createContext} from "../context.js";
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
import {isOrdinalScale, isThresholdScale} from "../scales.js";
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
+import {inferTickFormat} from "../marks/axis.js";
function maybeScale(scale, key) {
if (key == null) return key;
@@ -85,7 +86,7 @@ function legendItems(scale, options = {}, swatch) {
} = options;
const context = createContext(options);
className = maybeClassName(className);
- tickFormat = maybeAutoTickFormat(tickFormat, scale.domain);
+ if (typeof tickFormat !== "function") tickFormat = inferTickFormat(scale.scale, scale.domain, undefined, tickFormat);
const swatches = create("div", context).attr(
"class",
diff --git a/src/mark.d.ts b/src/mark.d.ts
index 21921fdd79..01983a3ff8 100644
--- a/src/mark.d.ts
+++ b/src/mark.d.ts
@@ -477,5 +477,5 @@ export class RenderableMark extends Mark {
/** A compound Mark, comprising other marks. */
export type CompoundMark = Markish[] & Pick;
-/** Given an array of marks, returns a compound mark; supports *mark.plot shorthand. */
+/** Given an array of marks, returns a compound mark; supports *mark*.plot shorthand. */
export function marks(...marks: Markish[]): CompoundMark;
diff --git a/src/mark.js b/src/mark.js
index 831048fa4a..4cd3d854ab 100644
--- a/src/mark.js
+++ b/src/mark.js
@@ -22,7 +22,7 @@ export class Mark {
marginRight = margin,
marginBottom = margin,
marginLeft = margin,
- clip,
+ clip = defaults?.clip,
channels: extraChannels,
tip,
render
diff --git a/src/marks/area.d.ts b/src/marks/area.d.ts
index 797456a052..49aa3011be 100644
--- a/src/marks/area.d.ts
+++ b/src/marks/area.d.ts
@@ -125,7 +125,7 @@ export interface AreaYOptions extends Omit, BinOptions
}
/**
- * Returns a new area with the given *data* and *options*. The area mark is
+ * Returns a new area mark with the given *data* and *options*. The area mark is
* rarely used directly; it is only needed when the baseline and topline have
* neither *x* nor *y* values in common. Use areaY for a horizontal orientation
* where the baseline and topline share *x* values, or areaX for a vertical
@@ -134,9 +134,10 @@ export interface AreaYOptions extends Omit, BinOptions
export function area(data?: Data, options?: AreaOptions): Area;
/**
- * Returns a new vertically-oriented area for the given *data* and *options*,
- * where the baseline and topline share **y** values, as in a time-series area
- * chart where time goes up↑. For example, to plot Apple’s daily stock price:
+ * Returns a new vertically-oriented area mark for the given *data* and
+ * *options*, where the baseline and topline share **y** values, as in a
+ * time-series area chart where time goes up↑. For example, to plot Apple’s
+ * daily stock price:
*
* ```js
* Plot.areaX(aapl, {y: "Date", x: "Close"})
@@ -165,9 +166,10 @@ export function area(data?: Data, options?: AreaOptions): Area;
export function areaX(data?: Data, options?: AreaXOptions): Area;
/**
- * Returns a new horizontally-oriented area for the given *data* and *options*,
- * where the baseline and topline share **x** values, as in a time-series area
- * chart where time goes right→. For example, to plot Apple’s daily stock price:
+ * Returns a new horizontally-oriented area mark for the given *data* and
+ * *options*, where the baseline and topline share **x** values, as in a
+ * time-series area chart where time goes right→. For example, to plot Apple’s
+ * daily stock price:
*
* ```js
* Plot.areaY(aapl, {x: "Date", y: "Close"})
@@ -179,8 +181,8 @@ export function areaX(data?: Data, options?: AreaXOptions): Area;
* specified, the other defaults to **y**, which defaults to zero.
*
* If an **interval** is specified, **x** values are binned accordingly,
- * allowing zeroes for empty bins instead of interpolating across gaps. This
- * is recommended to “regularize” sampled data; for example, if your data
+ * allowing zeroes for empty bins instead of interpolating across gaps. This is
+ * recommended to “regularize” sampled data; for example, if your data
* represents timestamped observations and you expect one observation per day,
* use *day* as the **interval**.
*
diff --git a/src/marks/arrow.d.ts b/src/marks/arrow.d.ts
index d2bf620dc5..1b6397eca5 100644
--- a/src/marks/arrow.d.ts
+++ b/src/marks/arrow.d.ts
@@ -84,6 +84,18 @@ export interface ArrowOptions extends MarkOptions {
* points to a dot.
*/
insetEnd?: number;
+
+ /**
+ * The sweep order; defaults to 1 indicating a positive (clockwise) bend
+ * angle; -1 indicates a negative (anticlockwise) bend angle; 0 effectively
+ * clears the bend angle. If set to *-x*, the bend angle is flipped when the
+ * ending point is to the left of the starting point — ensuring all arrows
+ * bulge up (down if bend is negative); if set to *-y*, the bend angle is
+ * flipped when the ending point is above the starting point — ensuring all
+ * arrows bulge right (left if bend is negative); the sign is negated for *+x*
+ * and *+y*.
+ */
+ sweep?: number | "+x" | "-x" | "+y" | "-y" | ((x1: number, y1: number, x2: number, y2: number) => number);
}
/**
diff --git a/src/marks/arrow.js b/src/marks/arrow.js
index ccb62256eb..7d1bdaf217 100644
--- a/src/marks/arrow.js
+++ b/src/marks/arrow.js
@@ -1,7 +1,8 @@
+import {ascending, descending} from "d3";
import {create} from "../context.js";
import {Mark} from "../mark.js";
import {radians} from "../math.js";
-import {constant} from "../options.js";
+import {constant, keyword} from "../options.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
import {maybeSameValue} from "./link.js";
@@ -26,7 +27,8 @@ export class Arrow extends Mark {
headLength = 8, // Disable the arrow with headLength = 0; or, use Plot.link.
inset = 0,
insetStart = inset,
- insetEnd = inset
+ insetEnd = inset,
+ sweep
} = options;
super(
data,
@@ -44,19 +46,13 @@ export class Arrow extends Mark {
this.headLength = +headLength;
this.insetStart = +insetStart;
this.insetEnd = +insetEnd;
+ this.sweep = maybeSweep(sweep);
}
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
const {strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
const sw = SW ? (i) => SW[i] : constant(strokeWidth === undefined ? 1 : strokeWidth);
- // When bending, the offset between the straight line between the two points
- // and the outgoing tangent from the start point. (Also the negative
- // incoming tangent to the end point.) This must be within ±π/2. A positive
- // angle will produce a clockwise curve; a negative angle will produce a
- // counterclockwise curve; zero will produce a straight line.
- const bendAngle = bend * radians;
-
// The angle between the arrow’s shaft and one of the wings; the “head”
// angle between the wings is twice this value.
const wingAngle = (headAngle * radians) / 2;
@@ -91,6 +87,13 @@ export class Arrow extends Mark {
// wings, but that’s okay since vectors are usually small.)
const headLength = Math.min(wingScale * sw(i), lineLength / 3);
+ // When bending, the offset between the straight line between the two points
+ // and the outgoing tangent from the start point. (Also the negative
+ // incoming tangent to the end point.) This must be within ±π/2. A positive
+ // angle will produce a clockwise curve; a negative angle will produce a
+ // counterclockwise curve; zero will produce a straight line.
+ const bendAngle = this.sweep(x1, y1, x2, y2) * bend * radians;
+
// The radius of the circle that intersects with the two endpoints
// and has the specified bend angle.
const r = Math.hypot(lineLength / Math.tan(bendAngle), lineLength) / 2;
@@ -141,9 +144,9 @@ export class Arrow extends Mark {
// If the radius is very large (or even infinite, as when the bend
// angle is zero), then render a straight line.
- return `M${x1},${y1}${
- r < 1e5 ? `A${r},${r} 0,0,${bendAngle > 0 ? 1 : 0} ` : `L`
- }${x2},${y2}M${x3},${y3}L${x2},${y2}L${x4},${y4}`;
+ const a = r < 1e5 ? `A${r},${r} 0,0,${bendAngle > 0 ? 1 : 0} ` : `L`;
+ const h = headLength ? `M${x3},${y3}L${x2},${y2}L${x4},${y4}` : "";
+ return `M${x1},${y1}${a}${x2},${y2}${h}`;
})
.call(applyChannelStyles, this, channels)
)
@@ -151,6 +154,22 @@ export class Arrow extends Mark {
}
}
+// Maybe flip the bend angle, depending on the arrow orientation.
+function maybeSweep(sweep = 1) {
+ if (typeof sweep === "number") return constant(Math.sign(sweep));
+ if (typeof sweep === "function") return (x1, y1, x2, y2) => Math.sign(sweep(x1, y1, x2, y2));
+ switch (keyword(sweep, "sweep", ["+x", "-x", "+y", "-y"])) {
+ case "+x":
+ return (x1, y1, x2) => ascending(x1, x2);
+ case "-x":
+ return (x1, y1, x2) => descending(x1, x2);
+ case "+y":
+ return (x1, y1, x2, y2) => ascending(y1, y2);
+ case "-y":
+ return (x1, y1, x2, y2) => descending(y1, y2);
+ }
+}
+
// Returns the center of a circle that goes through the two given points ⟨ax,ay⟩
// and ⟨bx,by⟩ and has radius r. There are two such points; use the sign +1 or
// -1 to choose between them. Returns [NaN, NaN] if r is too small.
diff --git a/src/marks/auto.js b/src/marks/auto.js
index b53c632ef0..4724a98f68 100644
--- a/src/marks/auto.js
+++ b/src/marks/auto.js
@@ -1,6 +1,6 @@
import {ascending, InternSet} from "d3";
import {marks} from "../mark.js";
-import {isColor, isObject, isOptions, isOrdinal, labelof, valueof} from "../options.js";
+import {isColor, isNumeric, isObject, isOptions, isOrdinal, labelof, valueof} from "../options.js";
import {bin, binX, binY} from "../transforms/bin.js";
import {group, groupX, groupY} from "../transforms/group.js";
import {areaX, areaY} from "./area.js";
@@ -113,27 +113,32 @@ export function autoSpec(data, options) {
colorMode = "stroke";
break;
case "bar":
- markImpl = yZero
- ? isOrdinalReduced(xReduce, X)
- ? barY
- : rectY
- : xZero
- ? isOrdinalReduced(yReduce, Y)
- ? barX
- : rectX
- : isOrdinalReduced(xReduce, X) && isOrdinalReduced(yReduce, Y)
- ? cell
- : isOrdinalReduced(xReduce, X)
- ? barY
- : isOrdinalReduced(yReduce, Y)
- ? barX
- : xReduce != null
- ? rectX
- : yReduce != null
- ? rectY
- : colorReduce != null
- ? rect
- : cell;
+ markImpl =
+ xReduce != null // bin or group on y
+ ? isOrdinal(Y)
+ ? isSelectReducer(xReduce) && X && isOrdinal(X)
+ ? cell
+ : barX
+ : rectX
+ : yReduce != null // bin or group on x
+ ? isOrdinal(X)
+ ? isSelectReducer(yReduce) && Y && isOrdinal(Y)
+ ? cell
+ : barY
+ : rectY
+ : colorReduce != null || sizeReduce != null // bin or group on both x and y
+ ? X && isOrdinal(X) && Y && isOrdinal(Y)
+ ? cell
+ : X && isOrdinal(X)
+ ? barY
+ : Y && isOrdinal(Y)
+ ? barX
+ : rect
+ : X && isNumeric(X) && !(Y && isNumeric(Y))
+ ? barX // if y is temporal, treat as ordinal
+ : Y && isNumeric(Y) && !(X && isNumeric(X))
+ ? barY // if x is temporal, treat as ordinal
+ : cell;
colorMode = "fill";
break;
default:
@@ -300,12 +305,6 @@ function isSelectReducer(reduce) {
return /^(?:first|last|mode)$/i.test(reduce);
}
-// We can’t infer the type of a custom reducer without invoking it, so
-// assume most reducers produce quantitative values.
-function isOrdinalReduced(reduce, value) {
- return (reduce != null && !isSelectReducer(reduce)) || !value ? false : isOrdinal(value);
-}
-
// https://github.com/observablehq/plot/blob/818562649280e155136f730fc496e0b3d15ae464/src/transforms/group.js#L236
function isReducer(reduce) {
if (reduce == null) return false;
diff --git a/src/marks/axis.js b/src/marks/axis.js
index b10cc1188e..28c5180d06 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -7,7 +7,7 @@ import {isIterable, isNoneish, isTemporal, orderof} from "../options.js";
import {maybeColorChannel, maybeNumberChannel, maybeRangeInterval} from "../options.js";
import {isTemporalScale} from "../scales.js";
import {offset} from "../style.js";
-import {isTimeYear, isUtcYear} from "../time.js";
+import {formatTimeTicks, isTimeYear, isUtcYear} from "../time.js";
import {initializer} from "../transforms/basic.js";
import {ruleX, ruleY} from "./rule.js";
import {text, textX, textY} from "./text.js";
@@ -366,9 +366,9 @@ function axisTextKy(
...options,
dx: anchor === "left" ? +dx - tickSize - tickPadding + +insetLeft : +dx + +tickSize + +tickPadding - insetRight
},
- function (scale, ticks, channels) {
+ function (scale, data, ticks, channels) {
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
- if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
+ if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor);
}
);
}
@@ -413,9 +413,9 @@ function axisTextKx(
...options,
dy: anchor === "bottom" ? +dy + +tickSize + +tickPadding - insetBottom : +dy - tickSize - tickPadding + +insetTop
},
- function (scale, ticks, channels) {
+ function (scale, data, ticks, channels) {
if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
- if (text === undefined) channels.text = inferTextChannel(scale, ticks, tickFormat);
+ if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor);
}
);
}
@@ -482,7 +482,18 @@ function gridDefaults({
}
function labelOptions(
- {fill, fillOpacity, fontFamily, fontSize, fontStyle, fontWeight, monospace, pointerEvents, shapeRendering},
+ {
+ fill,
+ fillOpacity,
+ fontFamily,
+ fontSize,
+ fontStyle,
+ fontWeight,
+ monospace,
+ pointerEvents,
+ shapeRendering,
+ clip = false
+ },
initializer
) {
// Only propagate these options if constant.
@@ -501,60 +512,63 @@ function labelOptions(
monospace,
pointerEvents,
shapeRendering,
+ clip,
initializer
};
}
function axisMark(mark, k, ariaLabel, data, options, initialize) {
let channels;
- const m = mark(
- data,
- initializer(options, function (data, facets, _channels, scales, dimensions, context) {
- const initializeFacets = data == null && (k === "fx" || k === "fy");
- const {[k]: scale} = scales;
- if (!scale) throw new Error(`missing scale: ${k}`);
- let {ticks, tickSpacing, interval} = options;
- if (isTemporalScale(scale) && typeof ticks === "string") (interval = ticks), (ticks = undefined);
- if (data == null) {
- if (isIterable(ticks)) {
- data = arrayify(ticks);
- } else if (scale.ticks) {
- if (ticks !== undefined) {
- data = scale.ticks(ticks);
+
+ function axisInitializer(data, facets, _channels, scales, dimensions, context) {
+ const initializeFacets = data == null && (k === "fx" || k === "fy");
+ const {[k]: scale} = scales;
+ if (!scale) throw new Error(`missing scale: ${k}`);
+ let {ticks, tickSpacing, interval} = options;
+ if (isTemporalScale(scale) && typeof ticks === "string") (interval = ticks), (ticks = undefined);
+ if (data == null) {
+ if (isIterable(ticks)) {
+ data = arrayify(ticks);
+ } else if (scale.ticks) {
+ if (ticks !== undefined) {
+ data = scale.ticks(ticks);
+ } else {
+ interval = maybeRangeInterval(interval === undefined ? scale.interval : interval, scale.type);
+ if (interval !== undefined) {
+ // For time scales, we could pass the interval directly to
+ // scale.ticks because it’s supported by d3.utcTicks; but
+ // quantitative scales and d3.ticks do not support numeric
+ // intervals for scale.ticks, so we compute them here.
+ const [min, max] = extent(scale.domain());
+ data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max
} else {
- interval = maybeRangeInterval(interval === undefined ? scale.interval : interval, scale.type);
- if (interval !== undefined) {
- // For time scales, we could pass the interval directly to
- // scale.ticks because it’s supported by d3.utcTicks; but
- // quantitative scales and d3.ticks do not support numeric
- // intervals for scale.ticks, so we compute them here.
- const [min, max] = extent(scale.domain());
- data = interval.range(min, interval.offset(interval.floor(max))); // inclusive max
- } else {
- const [min, max] = extent(scale.range());
- ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
- data = scale.ticks(ticks);
- }
+ const [min, max] = extent(scale.range());
+ ticks = (max - min) / (tickSpacing === undefined ? (k === "x" ? 80 : 35) : tickSpacing);
+ data = scale.ticks(ticks);
}
- } else {
- data = scale.domain();
- }
- if (k === "y" || k === "x") {
- facets = [range(data)];
- } else {
- channels[k] = {scale: k, value: identity};
}
+ } else {
+ data = scale.domain();
}
- initialize?.call(this, scale, ticks, channels);
- const initializedChannels = Object.fromEntries(
- Object.entries(channels).map(([name, channel]) => {
- return [name, {...channel, value: valueof(data, channel.value)}];
- })
- );
- if (initializeFacets) facets = context.filterFacets(data, initializedChannels);
- return {data, facets, channels: initializedChannels};
- })
- );
+ if (k === "y" || k === "x") {
+ facets = [range(data)];
+ } else {
+ channels[k] = {scale: k, value: identity};
+ }
+ }
+ initialize?.call(this, scale, data, ticks, channels);
+ const initializedChannels = Object.fromEntries(
+ Object.entries(channels).map(([name, channel]) => {
+ return [name, {...channel, value: valueof(data, channel.value)}];
+ })
+ );
+ if (initializeFacets) facets = context.filterFacets(data, initializedChannels);
+ return {data, facets, channels: initializedChannels};
+ }
+
+ // Apply any basic initializers after the axis initializer computes the ticks.
+ const basicInitializer = initializer(options).initializer;
+ const m = mark(data, initializer({...options, initializer: axisInitializer}, basicInitializer));
if (data == null) {
channels = m.channels;
m.channels = {};
@@ -562,18 +576,21 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
channels = {};
}
m.ariaLabel = ariaLabel;
+ if (m.clip === undefined) m.clip = false; // don’t clip axes by default
return m;
}
-function inferTextChannel(scale, ticks, tickFormat) {
- return {value: inferTickFormat(scale, ticks, tickFormat)};
+function inferTextChannel(scale, data, ticks, tickFormat, anchor) {
+ return {value: inferTickFormat(scale, data, ticks, tickFormat, anchor)};
}
// D3’s ordinal scales simply use toString by default, but if the ordinal scale
// domain (or ticks) are numbers or dates (say because we’re applying a time
// interval to the ordinal scale), we want Plot’s default formatter.
-export function inferTickFormat(scale, ticks, tickFormat) {
- return scale.tickFormat
+export function inferTickFormat(scale, data, ticks, tickFormat, anchor) {
+ return tickFormat === undefined && isTemporalScale(scale)
+ ? formatTimeTicks(scale, data, ticks, anchor)
+ : scale.tickFormat
? scale.tickFormat(isIterable(ticks) ? null : ticks, tickFormat)
: tickFormat === undefined
? isUtcYear(scale.interval)
diff --git a/src/marks/bollinger.d.ts b/src/marks/bollinger.d.ts
new file mode 100644
index 0000000000..8a2087c140
--- /dev/null
+++ b/src/marks/bollinger.d.ts
@@ -0,0 +1,93 @@
+import type {CompoundMark, Data, MarkOptions} from "../mark.js";
+import type {Map} from "../transforms/map.js";
+import type {WindowOptions} from "../transforms/window.js";
+import type {AreaXOptions, AreaYOptions} from "./area.js";
+import type {LineXOptions, LineYOptions} from "./line.js";
+
+/** Options for the bollinger window transform. */
+export interface BollingerWindowOptions {
+ /** The number of consecutive values in the window; defaults to 20. */
+ n?: number;
+
+ /** The number of standard deviations to offset the bands; defaults to 2. */
+ k?: number;
+
+ /**
+ * How to align the rolling window, placing the current value:
+ *
+ * - *start* - as the first element in the window
+ * - *middle* - in the middle of the window, rounding down if **n** is even
+ * - *end* (default) - as the last element in the window
+ *
+ * Note that *start* and *end* are relative to input order, not natural
+ * ascending order by value. For example, if the data is in reverse
+ * chronological order, then the meaning of *start* and *end* is effectively
+ * reversed because the first data point is the most recent.
+ */
+ anchor?: WindowOptions["anchor"];
+
+ /**
+ * If true (the default), the output start values or end values or both
+ * (depending on the **anchor**) of each series may be undefined since there
+ * are not enough elements to create a window of size **n**; output values may
+ * also be undefined if some of the input values in the corresponding window
+ * are undefined.
+ *
+ * If false, the window will be automatically truncated as needed, and
+ * undefined input values are ignored. For example, if **n** is 24 and
+ * **anchor** is *middle*, then the initial 11 values have effective window
+ * sizes of 13, 14, 15, … 23, and likewise the last 12 values have effective
+ * window sizes of 23, 22, 21, … 12. Values computed with a truncated window
+ * may be noisy.
+ */
+ strict?: WindowOptions["strict"];
+}
+
+/** Options for the bollinger mark. */
+export interface BollingerOptions extends BollingerWindowOptions {
+ /**
+ * Shorthand for setting both **fill** and **stroke**; affects the stroke of
+ * the line and the fill of the area; defaults to *currentColor*.
+ */
+ color?: MarkOptions["stroke"];
+}
+
+/** Options for the bollingerX mark. */
+export type BollingerXOptions = BollingerOptions & AreaXOptions & LineXOptions;
+
+/** Options for the bollingerY mark. */
+export type BollingerYOptions = BollingerOptions & AreaYOptions & LineYOptions;
+
+/**
+ * Returns a new vertically-oriented bollinger mark for the given *data* and
+ * *options*, as in a time-series area chart where time goes up↑ (or down↓).
+ *
+ * If the *x* option is not specified, it defaults to the identity function, as
+ * when data is an array of numbers [*x*₀, *x*₁, *x*₂, …]. If the *y* option is
+ * not specified, it defaults to [0, 1, 2, …].
+ */
+export function bollingerX(data?: Data, options?: BollingerXOptions): CompoundMark;
+
+/**
+ * Returns a new horizontally-oriented bollinger mark for the given *data* and
+ * *options*, as in a time-series area chart where time goes right→ (or ←left).
+ *
+ * If the *y* option is not specified, it defaults to the identity function, as
+ * when data is an array of numbers [*y*₀, *y*₁, *y*₂, …]. If the *x* option is
+ * not specified, it defaults to [0, 1, 2, …].
+ */
+export function bollingerY(data?: Data, options?: BollingerYOptions): CompoundMark;
+
+/**
+ * Given the specified bollinger *options*, returns a corresponding map
+ * implementation for use with the map transform, allowing the bollinger
+ * transform to be applied to arbitrary channels instead of only *x* and *y*.
+ * For example, to compute the upper volatility band:
+ *
+ * ```js
+ * Plot.map({y: Plot.bollinger({n: 20, k: 2})}, {x: "Date", y: "Close"})
+ * ```
+ *
+ * Here the *k* option defaults to zero instead of two.
+ */
+export function bollinger(options?: BollingerWindowOptions): Map;
diff --git a/src/marks/bollinger.js b/src/marks/bollinger.js
new file mode 100644
index 0000000000..8b0528785d
--- /dev/null
+++ b/src/marks/bollinger.js
@@ -0,0 +1,84 @@
+import {deviation, mean} from "d3";
+import {marks} from "../mark.js";
+import {identity, isNoneish} from "../options.js";
+import {map} from "../transforms/map.js";
+import {window} from "../transforms/window.js";
+import {areaX, areaY} from "./area.js";
+import {lineX, lineY} from "./line.js";
+
+const defaults = {
+ n: 20,
+ k: 2,
+ color: "currentColor",
+ opacity: 0.2,
+ strict: true,
+ anchor: "end"
+};
+
+export function bollingerX(
+ data,
+ {
+ x = identity,
+ y,
+ k = defaults.k,
+ color = defaults.color,
+ opacity = defaults.opacity,
+ fill = color,
+ fillOpacity = opacity,
+ stroke = color,
+ strokeOpacity,
+ strokeWidth,
+ ...options
+ } = {}
+) {
+ return marks(
+ isNoneish(fill)
+ ? null
+ : areaX(
+ data,
+ map(
+ {x1: bollinger({k: -k, ...options}), x2: bollinger({k, ...options})},
+ {x1: x, x2: x, y, fill, fillOpacity, ...options}
+ )
+ ),
+ isNoneish(stroke)
+ ? null
+ : lineX(data, map({x: bollinger(options)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
+ );
+}
+
+export function bollingerY(
+ data,
+ {
+ x,
+ y = identity,
+ k = defaults.k,
+ color = defaults.color,
+ opacity = defaults.opacity,
+ fill = color,
+ fillOpacity = opacity,
+ stroke = color,
+ strokeOpacity,
+ strokeWidth,
+ ...options
+ } = {}
+) {
+ return marks(
+ isNoneish(fill)
+ ? null
+ : areaY(
+ data,
+ map(
+ {y1: bollinger({k: -k, ...options}), y2: bollinger({k, ...options})},
+ {x, y1: y, y2: y, fill, fillOpacity, ...options}
+ )
+ ),
+ isNoneish(stroke)
+ ? null
+ : lineY(data, map({y: bollinger(options)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
+ );
+}
+
+export function bollinger({n = defaults.n, k = 0, strict = defaults.strict, anchor = defaults.anchor} = {}) {
+ return window({k: n, reduce: (Y) => mean(Y) + k * (deviation(Y) || 0), strict, anchor});
+}
diff --git a/src/marks/frame.js b/src/marks/frame.js
index 43abe05159..372678cf33 100644
--- a/src/marks/frame.js
+++ b/src/marks/frame.js
@@ -6,14 +6,16 @@ import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransfo
const defaults = {
ariaLabel: "frame",
fill: "none",
- stroke: "currentColor"
+ stroke: "currentColor",
+ clip: false
};
const lineDefaults = {
ariaLabel: "frame",
fill: null,
stroke: "currentColor",
- strokeLinecap: "square"
+ strokeLinecap: "square",
+ clip: false
};
export class Frame extends Mark {
diff --git a/src/marks/geo.d.ts b/src/marks/geo.d.ts
index 74a6729c1e..d37a23e7fe 100644
--- a/src/marks/geo.d.ts
+++ b/src/marks/geo.d.ts
@@ -53,7 +53,7 @@ export function sphere(options?: GeoOptions): Geo;
* a spherical **projection** only.) For more control, use [d3.geoGraticule][1]
* with the geo mark.
*
- * [1]: https://github.com/d3/d3-geo/blob/main/README.md#geoGraticule
+ * [1]: https://d3js.org/d3-geo/shape#geoGraticule
*/
export function graticule(options?: GeoOptions): Geo;
diff --git a/src/marks/line.d.ts b/src/marks/line.d.ts
index d17e06a7b3..0f1692a978 100644
--- a/src/marks/line.d.ts
+++ b/src/marks/line.d.ts
@@ -83,10 +83,10 @@ export interface LineYOptions extends LineOptions, BinOptions {
}
/**
- * Returns a new line for the given *data* and *options* by connecting control
- * points. If neither the **x** nor **y** options are specified, *data* is
- * assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …]
- * such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
+ * Returns a new line mark for the given *data* and *options* by connecting
+ * control points. If neither the **x** nor **y** options are specified, *data*
+ * is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*],
+ * …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
*
* Points along the line are connected in input order. If there are multiple
* series via the **z**, **fill**, or **stroke** channel, series are drawn in
diff --git a/src/marks/raster.d.ts b/src/marks/raster.d.ts
index fdf6fe9d0f..8a1a9248d6 100644
--- a/src/marks/raster.d.ts
+++ b/src/marks/raster.d.ts
@@ -209,7 +209,7 @@ export const interpolateNone: RasterInterpolateFunction;
* congruential generator][2] with a fixed seed (for deterministic results).
*
* [1]: https://en.wikipedia.org/wiki/Barycentric_coordinate_system
- * [2]: https://github.com/d3/d3-random/blob/main/README.md#randomLcg
+ * [2]: https://d3js.org/d3-random#randomLcg
*/
export function interpolatorBarycentric(options?: {random?: RandomSource}): RasterInterpolateFunction;
diff --git a/src/marks/raster.js b/src/marks/raster.js
index fd601e3ced..61abbc2eea 100644
--- a/src/marks/raster.js
+++ b/src/marks/raster.js
@@ -1,7 +1,7 @@
import {blurImage, Delaunay, randomLcg, rgb} from "d3";
import {valueObject} from "../channel.js";
import {create} from "../context.js";
-import {map, first, second, third, isTuples, isNumeric, isTemporal, take, identity} from "../options.js";
+import {map, first, second, third, isTuples, isNumeric, isTemporal, identity} from "../options.js";
import {maybeColorChannel, maybeNumberChannel} from "../options.js";
import {Mark} from "../mark.js";
import {applyAttr, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString} from "../style.js";
@@ -282,31 +282,16 @@ export function interpolateNone(index, width, height, X, Y, V) {
export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
return (index, width, height, X, Y, V) => {
- // Flatten the input coordinates to prepare to insert extrapolated points
- // along the perimeter of the grid (so there’s no blank output).
- const n = index.length;
- const nw = width >> 2;
- const nh = (height >> 2) - 1;
- const m = n + nw * 2 + nh * 2;
- const XY = new Float64Array(m * 2);
- for (let i = 0; i < n; ++i) (XY[i * 2] = X[index[i]]), (XY[i * 2 + 1] = Y[index[i]]);
-
- // Add points along each edge, making sure to include the four corners for
- // complete coverage (with no chamfered edges).
- let i = n;
- const addPoint = (x, y) => ((XY[i * 2] = x), (XY[i * 2 + 1] = y), i++);
- for (let j = 0; j <= nw; ++j) addPoint((j / nw) * width, 0), addPoint((j / nw) * width, height);
- for (let j = 0; j < nh; ++j) addPoint(width, (j / nh) * height), addPoint(0, (j / nh) * height);
-
- // To each edge point, assign the closest (non-extrapolated) value.
- V = take(V, index);
- const delaunay = new Delaunay(XY.subarray(0, n * 2));
- for (let j = n, ij; j < m; ++j) V[j] = V[(ij = delaunay.find(XY[j * 2], XY[j * 2 + 1], ij))];
-
// Interpolate the interior of all triangles with barycentric coordinates
- const {points, triangles} = new Delaunay(XY);
- const W = new V.constructor(width * height);
+ const {points, triangles, hull} = Delaunay.from(
+ index,
+ (i) => X[i],
+ (i) => Y[i]
+ );
+ const W = new V.constructor(width * height).fill(NaN);
+ const S = new Uint8Array(width * height); // 1 if pixel has been seen.
const mix = mixer(V, random);
+
for (let i = 0; i < triangles.length; i += 3) {
const ta = triangles[i];
const tb = triangles[i + 1];
@@ -323,9 +308,9 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
const y2 = Math.max(Ay, By, Cy);
const z = (By - Cy) * (Ax - Cx) + (Ay - Cy) * (Cx - Bx);
if (!z) continue;
- const va = V[ta];
- const vb = V[tb];
- const vc = V[tc];
+ const va = V[index[ta]];
+ const vb = V[index[tb]];
+ const vc = V[index[tc]];
for (let x = Math.floor(x1); x < x2; ++x) {
for (let y = Math.floor(y1); y < y2; ++y) {
if (x < 0 || x >= width || y < 0 || y >= height) continue;
@@ -337,14 +322,91 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
if (gb < 0) continue;
const gc = 1 - ga - gb;
if (gc < 0) continue;
- W[x + width * y] = mix(va, ga, vb, gb, vc, gc, x, y);
+ const i = x + width * y;
+ W[i] = mix(va, ga, vb, gb, vc, gc, x, y);
+ S[i] = 1;
}
}
}
+ extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix);
return W;
};
}
+// Extrapolate by finding the closest point on the hull.
+function extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix) {
+ X = Float64Array.from(hull, (i) => X[index[i]]);
+ Y = Float64Array.from(hull, (i) => Y[index[i]]);
+ V = Array.from(hull, (i) => V[index[i]]);
+ const n = X.length;
+ const rays = Array.from({length: n}, (_, j) => ray(j, X, Y));
+ let k = 0;
+ for (let y = 0; y < height; ++y) {
+ const yp = y + 0.5;
+ for (let x = 0; x < width; ++x) {
+ const i = x + width * y;
+ if (!S[i]) {
+ const xp = x + 0.5;
+ for (let l = 0; l < n; ++l) {
+ const j = (n + k + (l % 2 ? (l + 1) / 2 : -l / 2)) % n;
+ if (rays[j](xp, yp)) {
+ const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
+ W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
+ k = j;
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Projects a point p = [x, y] onto the line segment [p1, p2], returning the
+// projected coordinates p’ as t in [0, 1] with p’ = t p1 + (1 - t) p2.
+function segmentProject(x1, y1, x2, y2, x, y) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const a = dx * (x2 - x) + dy * (y2 - y);
+ const b = dx * (x - x1) + dy * (y - y1);
+ return a > 0 && b > 0 ? a / (a + b) : +(a > b);
+}
+
+function cross(xa, ya, xb, yb) {
+ return xa * yb - xb * ya;
+}
+
+function ray(j, X, Y) {
+ const n = X.length;
+ const xc = X.at(j - 2);
+ const yc = Y.at(j - 2);
+ const xa = X.at(j - 1);
+ const ya = Y.at(j - 1);
+ const xb = X[j];
+ const yb = Y[j];
+ const xd = X.at(j + 1 - n);
+ const yd = Y.at(j + 1 - n);
+ const dxab = xa - xb;
+ const dyab = ya - yb;
+ const dxca = xc - xa;
+ const dyca = yc - ya;
+ const dxbd = xb - xd;
+ const dybd = yb - yd;
+ const hab = Math.hypot(dxab, dyab);
+ const hca = Math.hypot(dxca, dyca);
+ const hbd = Math.hypot(dxbd, dybd);
+ return (x, y) => {
+ const dxa = x - xa;
+ const dya = y - ya;
+ const dxb = x - xb;
+ const dyb = y - yb;
+ return (
+ cross(dxa, dya, dxb, dyb) > -1e-6 &&
+ cross(dxa, dya, dxab, dyab) * hca - cross(dxa, dya, dxca, dyca) * hab > -1e-6 &&
+ cross(dxb, dyb, dxbd, dybd) * hab - cross(dxb, dyb, dxab, dyab) * hbd <= 0
+ );
+ };
+}
+
export function interpolateNearest(index, width, height, X, Y, V) {
const W = new V.constructor(width * height);
const delaunay = Delaunay.from(
diff --git a/src/marks/rule.js b/src/marks/rule.js
index 09e67aebc6..8fc091dfe3 100644
--- a/src/marks/rule.js
+++ b/src/marks/rule.js
@@ -35,7 +35,7 @@ export class RuleX extends Mark {
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
const {insetTop, insetBottom} = this;
return create("svg:g", context)
- .call(applyIndirectStyles, this, dimensions)
+ .call(applyIndirectStyles, this, dimensions, context)
.call(applyTransform, this, {x: X && x}, offset, 0)
.call((g) =>
g
diff --git a/src/marks/text.d.ts b/src/marks/text.d.ts
index 78f8a78a59..8ad171cfbb 100644
--- a/src/marks/text.d.ts
+++ b/src/marks/text.d.ts
@@ -205,8 +205,8 @@ export interface TextYOptions extends Omit {
*
* [1]: https://observablehq.com/@mbostock/number-formatting
* [2]: https://observablehq.com/@mbostock/date-formatting
- * [3]: https://github.com/d3/d3-format
- * [4]: https://github.com/d3/d3-time-format
+ * [3]: https://d3js.org/d3-format
+ * [4]: https://d3js.org/d3-time-format
*/
export function text(data?: Data, options?: TextOptions): Text;
diff --git a/src/marks/tip.js b/src/marks/tip.js
index fcb50a1056..b745084d65 100644
--- a/src/marks/tip.js
+++ b/src/marks/tip.js
@@ -45,6 +45,7 @@ export class Tip extends Mark {
textAnchor = "start",
textOverflow,
textPadding = 8,
+ title,
pointerSize = 12,
pathFilter = "drop-shadow(0 3px 4px rgba(0,0,0,0.2))"
} = options;
@@ -56,7 +57,8 @@ export class Tip extends Mark {
x1: {value: x1, scale: "x", optional: x2 == null},
y1: {value: y1, scale: "y", optional: y2 == null},
x2: {value: x2, scale: "x", optional: x1 == null},
- y2: {value: y2, scale: "y", optional: y1 == null}
+ y2: {value: y2, scale: "y", optional: y1 == null},
+ title: {value: title, optional: true} // filter: defined
},
options,
defaults
@@ -133,9 +135,9 @@ export class Tip extends Mark {
const value = channel.value[i];
if (!defined(value) && channel.scale == null) continue;
if (key === "x2" && "x1" in sources) {
- yield {name: formatLabel(scales, channel, "x"), value: formatPair(sources.x1, channel, i)};
+ yield {name: formatPairLabel(scales, sources.x1, channel, "x"), value: formatPair(sources.x1, channel, i)};
} else if (key === "y2" && "y1" in sources) {
- yield {name: formatLabel(scales, channel, "y"), value: formatPair(sources.y1, channel, i)};
+ yield {name: formatPairLabel(scales, sources.y1, channel, "y"), value: formatPair(sources.y1, channel, i)};
} else {
const scale = channel.scale;
const line = {name: formatLabel(scales, channel, key), value: formatDefault(value)};
@@ -332,6 +334,12 @@ function formatPair(c1, c2, i) {
: `${formatDefault(c1.value[i])}–${formatDefault(c2.value[i])}`;
}
+function formatPairLabel(scales, c1, c2, defaultLabel) {
+ const l1 = formatLabel(scales, c1, defaultLabel);
+ const l2 = formatLabel(scales, c2, defaultLabel);
+ return l1 === l2 ? l1 : `${l1}–${l2}`;
+}
+
function formatLabel(scales, c, defaultLabel) {
return String(scales[c.scale]?.label ?? c?.label ?? defaultLabel);
}
diff --git a/src/marks/tree.d.ts b/src/marks/tree.d.ts
index 7798218484..7530f3ab9c 100644
--- a/src/marks/tree.d.ts
+++ b/src/marks/tree.d.ts
@@ -4,7 +4,7 @@ import type {DotOptions} from "./dot.js";
import type {LinkOptions} from "./link.js";
import type {TextOptions} from "./text.js";
-// TODO tree channels, e.g., "node:name" | "node:path" | "node:internal"?
+// TODO tree channels, e.g., "node:name" | "node:path" | "node:internal" | "node:external"?
/** Options for the compound tree mark. */
export interface TreeOptions extends DotOptions, LinkOptions, TextOptions, TreeTransformOptions {
@@ -19,6 +19,14 @@ export interface TreeOptions extends DotOptions, LinkOptions, TextOptions, TreeT
* atop other marks by creating a halo effect; defaults to *white*.
*/
textStroke?: MarkOptions["stroke"];
+
+ /**
+ * Layout for node labels: if *mirrored*, leaf-node labels are left-anchored,
+ * and non-leaf nodes right-anchored (with a -dx offset). If *normal*, all
+ * labels are left-anchored. Defaults to *mirrored* unless a **treeLayout**
+ * has been specified.
+ */
+ textLayout?: "mirrored" | "normal";
}
/**
@@ -31,7 +39,7 @@ export interface TreeOptions extends DotOptions, LinkOptions, TextOptions, TreeT
* slash by default); then executes a tree layout algorithm, by default
* [Reingold–Tilford’s “tidy” algorithm][1].
*
- * [1]: https://github.com/d3/d3-hierarchy/blob/main/README.md#tree
+ * [1]: https://d3js.org/d3-hierarchy/tree
*/
export function tree(data?: Data, options?: TreeOptions): CompoundMark;
@@ -40,9 +48,9 @@ export function tree(data?: Data, options?: TreeOptions): CompoundMark;
* option, placing leaf nodes of the tree at the same depth. Equivalent to:
*
* ```js
- * Plot.tree(data, {...options, treeLayout: d3.cluster})
+ * Plot.tree(data, {...options, treeLayout: d3.cluster, textLayout: "mirrored"})
* ```
*
- * [1]: https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster
+ * [1]: https://d3js.org/d3-hierarchy/cluster
*/
export function cluster(data?: Data, options?: TreeOptions): CompoundMark;
diff --git a/src/marks/tree.js b/src/marks/tree.js
index ebc6bf02e1..a58238fc7f 100644
--- a/src/marks/tree.js
+++ b/src/marks/tree.js
@@ -1,10 +1,11 @@
-import {cluster as Cluster} from "d3";
-import {isNoneish} from "../options.js";
+import {cluster as Cluster, tree as Tree} from "d3";
import {marks} from "../mark.js";
+import {isNoneish} from "../options.js";
import {maybeTreeAnchor, treeLink, treeNode} from "../transforms/tree.js";
import {dot} from "./dot.js";
import {link} from "./link.js";
import {text} from "./text.js";
+import {keyword} from "../options.js";
export function tree(
data,
@@ -27,14 +28,39 @@ export function tree(
title = "node:path",
dx,
dy,
+ textAnchor,
+ treeLayout = Tree,
+ textLayout = treeLayout === Tree || treeLayout === Cluster ? "mirrored" : "normal",
+ tip,
...options
} = {}
) {
if (dx === undefined) dx = maybeTreeAnchor(options.treeAnchor).dx;
+ if (textAnchor !== undefined) throw new Error("textAnchor is not a configurable tree option");
+ textLayout = keyword(textLayout, "textLayout", ["mirrored", "normal"]);
+
+ function treeText(textOptions) {
+ return text(
+ data,
+ treeNode({
+ treeLayout,
+ text: textText,
+ fill: fill === undefined ? "currentColor" : fill,
+ stroke: textStroke,
+ dx,
+ dy,
+ title,
+ ...textOptions,
+ ...options
+ })
+ );
+ }
+
return marks(
link(
data,
treeLink({
+ treeLayout,
markerStart,
markerEnd,
stroke: stroke !== undefined ? stroke : fill === undefined ? "node:internal" : fill,
@@ -48,20 +74,16 @@ export function tree(
...options
})
),
- dotDot ? dot(data, treeNode({fill: fill === undefined ? "node:internal" : fill, title, ...options})) : null,
+ dotDot
+ ? dot(data, treeNode({treeLayout, fill: fill === undefined ? "node:internal" : fill, title, tip, ...options}))
+ : null,
textText != null
- ? text(
- data,
- treeNode({
- text: textText,
- fill: fill === undefined ? "currentColor" : fill,
- stroke: textStroke,
- dx,
- dy,
- title,
- ...options
- })
- )
+ ? textLayout === "mirrored"
+ ? [
+ treeText({textAnchor: "start", treeFilter: "node:external"}),
+ treeText({textAnchor: "end", treeFilter: "node:internal", dx: -dx})
+ ]
+ : treeText()
: null
);
}
diff --git a/src/options.js b/src/options.js
index 0a1603eaa7..f70a118ef8 100644
--- a/src/options.js
+++ b/src/options.js
@@ -231,6 +231,11 @@ export function taker(f) {
return f.length === 1 ? (index, values) => f(take(values, index)) : f;
}
+// Uses subarray if available, and otherwise slice.
+export function subarray(I, i, j) {
+ return I.subarray ? I.subarray(i, j) : I.slice(i, j);
+}
+
// Based on InternMap (d3.group).
export function keyof(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
diff --git a/src/plot.d.ts b/src/plot.d.ts
index fbf407d806..b6019631b1 100644
--- a/src/plot.d.ts
+++ b/src/plot.d.ts
@@ -1,6 +1,6 @@
import type {ChannelValue} from "./channel.js";
import type {LegendOptions} from "./legends.js";
-import type {Data, Markish} from "./mark.js";
+import type {Data, MarkOptions, Markish} from "./mark.js";
import type {ProjectionFactory, ProjectionImplementation, ProjectionName, ProjectionOptions} from "./projection.js";
import type {Scale, ScaleDefaults, ScaleName, ScaleOptions} from "./scales.js";
@@ -9,11 +9,11 @@ export interface PlotOptions extends ScaleDefaults {
/**
* The outer width of the plot in pixels, including margins. Defaults to 640.
- * On Observable, this can be set to the built-in
- * [width](https://github.com/observablehq/stdlib/blob/main/README.md#width)
- * for full-width responsive plots. Note: the default style has a max-width of
- * 100%; the plot will automatically shrink to fit even when a fixed width is
- * specified.
+ * On Observable, this can be set to the built-in [width][1] for full-width
+ * responsive plots. Note: the default style has a max-width of 100%; the plot
+ * will automatically shrink to fit even when a fixed width is specified.
+ *
+ * [1]: https://github.com/observablehq/stdlib/blob/main/README.md#width
*/
width?: number;
@@ -77,20 +77,15 @@ export interface PlotOptions extends ScaleDefaults {
/**
* Custom styles to override Plot’s defaults. Styles may be specified either
* as a string of inline styles (*e.g.*, `"color: red;"`, in the same fashion
- * as assigning
- * [*element*.style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style))
- * or an object of properties (*e.g.*, `{color: "red"}`, in the same fashion
- * as assigning [*element*.style
- * properties](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration)).
- * Note that unitless numbers ([quirky
- * lengths](https://www.w3.org/TR/css-values-4/#deprecated-quirky-length))
- * such as `{padding: 20}` may not supported by some browsers; you should
- * instead specify a string with units such as `{padding: "20px"}`. By
- * default, the returned plot has a white background, a max-width of 100%, and
- * the system-ui font. Plot’s marks and axes default to
- * [currentColor](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword),
- * meaning that they will inherit the surrounding content’s color. For
- * example, a dark theme:
+ * as assigning [*element*.style][1]) or an object of properties (*e.g.*,
+ * `{color: "red"}`, in the same fashion as assigning [*element*.style
+ * properties][2]). Note that unitless numbers ([quirky lengths][3]) such as
+ * `{padding: 20}` may not supported by some browsers; you should instead
+ * specify a string with units such as `{padding: "20px"}`. By default, the
+ * returned plot has a white background, a max-width of 100%, and the
+ * system-ui font. Plot’s marks and axes default to [currentColor][4], meaning
+ * that they will inherit the surrounding content’s color. For example, a dark
+ * theme:
*
* ```js
* Plot.plot({
@@ -98,6 +93,11 @@ export interface PlotOptions extends ScaleDefaults {
* marks: …
* })
* ```
+ *
+ * [1]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
+ * [2]: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
+ * [3]: https://www.w3.org/TR/css-values-4/#deprecated-quirky-length
+ * [4]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword
*/
style?: string | Partial | null;
@@ -107,12 +107,48 @@ export interface PlotOptions extends ScaleDefaults {
*/
className?: string;
+ /**
+ * The figure title. If present, Plot wraps the generated SVG element in an
+ * HTML figure element with the title in a h2 element, returning the figure.
+ * To specify an HTML title, consider using the [`html` tagged template
+ * literal][1]; otherwise, the specified string represents text that will be
+ * escaped as needed.
+ *
+ * ```js
+ * Plot.plot({
+ * title: html`