Skip to content

Commit bf42c54

Browse files
committed
Merge branch 'main' into mbostock/pointer
2 parents a702c02 + 36df05d commit bf42c54

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+7275
-338
lines changed

CHANGELOG.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# Observable Plot - Changelog
22

3-
## 0.4.1
3+
## 0.4.2
44

55
*Not yet released. These are forthcoming changes in the main branch.*
66

77
Plot now supports [interaction marks](./README.md#interactions)! An interaction mark defines an interactive selection represented as a subset of the mark’s data. For example, the [brush mark](./README.md#brush) allows rectangular selection by clicking and dragging; you can use a brush to select points of interest from a scatterplot and show them in a table. The interactive selection is exposed as *plot*.value. When the selection changes during interaction, the plot emits *input* events. This allows plots to be [Observable views](https://observablehq.com/@observablehq/introduction-to-views), but you can also [listen to *input* events](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) directly.
88

9+
## 0.4.1
10+
11+
*Not yet released. These are forthcoming changes in the main branch.*
12+
913
The [text mark](./README.md#text) now supports automatic wrapping! 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.
1014

1115
Plot now supports ARIA attributes for improved accessibility: aria-label, aria-description, aria-hidden. The top-level **ariaLabel** and **ariaDescription** options apply to the root SVG element. The new **ariaLabel** and **ariaDescription** scale options apply to axes; the label defaults to *e.g.* “y-axis” and the description defaults to the scale’s label (*e.g.*, “↑ temperature”). Marks define a group-level aria-label (*e.g.*, “dot”). There is also an optional **ariaLabel** channel for labeling data (*e.g.*, “E 12.7%”), and a group-level **ariaDescription** option for a human-readable description. The **ariaHidden** mark option allows the hiding of decorative elements from the accessibility tree.
@@ -14,13 +18,15 @@ The line and link marks now support [marker options](./README.md#markers) for dr
1418

1519
The new **paintOrder** mark option controls the [paint order](https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order). The text mark’s paint order now defaults to *stroke*, with a stroke width of 3px and a stroke linejoin of *round*, making it easier to create a halo for separating labels from a busy background, improving legibility.
1620

17-
The *fill* and *stroke* mark options can now be expressed as patterns or gradients using funciri color definitions, *e.g.* “url(#pattern)”.
21+
The *fill* and *stroke* mark options can now be expressed as patterns or gradients using funciri color definitions, *e.g.* “url(#pattern)”. All marks now support the *strokeDashoffset* option (for use with *strokeDasharray*).
22+
23+
When a color scale is associated exclusively with boolean values (true and false), a smarter default range is now chosen: light gray for false, and dark gray for true. Light and dark colors from different sequential schemes, such as *reds*, can be specified via the *scheme* option.
1824

19-
Better boolean color schemes.
25+
The bin transform now supports the *interval* option, allowing numeric intervals such as integer binning with a nice default domain that aligns with interval boundaries. (The bin transform already supported time intervals as the *thresholds* option; time intervals can now also be specified as the *interval* option.)
2026

21-
Fix crash in default tuple accessors for *x* and *y* when data is undefined. Fix a bug where “none” with surrounding whitespace or capital letters would not be recognized as a valid color. When a channel is specified as a boolean (*e.g.*, `fill: true`), it is now considered a constant value rather than undefined.
27+
The returned scale object now exposes *bandwidth* and *step* values for *band* and *point* scales.
2228

23-
The vector mark now supports *frameAnchor*.
29+
Fix a crash in default tuple accessors for *x* and *y* when data is undefined. Fix a bug where “none” with surrounding whitespace or capital letters would not be recognized as a valid color. When a channel is specified as a boolean value (*e.g.*, `fill: true`), it is now considered a constant value rather than undefined. Fix a bug where an identity color legend would be rendered as the text “undefined” instead of showing nothing. The vector mark now respects the *frameAnchor* option.
2430

2531
## 0.4.0
2632

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ const plot2 = Plot.plot({…, color: plot1.scale("color")});
231231

232232
The returned scale object represents the actual (or “materialized”) values encountered in the 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.
233233

234-
For convenience, an apply method is exposed, which returns the scale’s output for any given input. When applicable, an invert method is exposed, which returns the corresponding input from the scale’s domain for any given output.
234+
For convenience, an apply method is exposed, which returns the scale’s output for any given input. When applicable, an invert method is exposed, which returns the corresponding input from the scale’s domain for any given output. Point and band scales also expose their materialized bandwidth and step.
235235

236236
The scale object is undefined if the associated plot has no scale with the given *name*, and throws an error if the *name* is invalid (*i.e.*, not one of the known scale names: *x*, *y*, *fx*, *fy*, *r*, *color*, or *opacity*).
237237

@@ -450,7 +450,7 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: {value: "y", reverse
450450

451451
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.
452452

453-
Note: when a string or a function, the sort option is interpreted as a [basic sort transform](#transforms). To use both sort options and a sort transform, use [Plot.sort](#plotsortorder-options).
453+
Note: when the value of the sort option is a string or a function, it is interpreted as a [basic sort transform](#transforms). To use both sort options and a sort transform, use [Plot.sort](#plotsortorder-options).
454454

455455
### Facet options
456456

@@ -622,7 +622,8 @@ All marks support the following style options:
622622
* **strokeLinejoin** - how to join lines (*bevel*, *miter*, *miter-clip*, or *round*)
623623
* **strokeLinecap** - how to cap lines (*butt*, *round*, or *square*)
624624
* **strokeMiterlimit** - to limit the length of *miter* joins
625-
* **strokeDasharray** - a comma-separated list of dash lengths (in pixels)
625+
* **strokeDasharray** - a comma-separated list of dash lengths (typically in pixels)
626+
* **strokeDashoffset** - the [stroke dash offset](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset) (typically in pixels)
626627
* **opacity** - object opacity (a number between 0 and 1)
627628
* **mixBlendMode** - the [blend mode](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) (*e.g.*, *multiply*)
628629
* **shapeRendering** - the [shape-rendering mode](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) (*e.g.*, *crispEdges*)
@@ -646,6 +647,7 @@ All marks support the following optional channels:
646647
* **title** - a tooltip (a string of text, possibly with newlines)
647648
* **href** - a URL to link to
648649
* **ariaLabel** - a short label representing the value in the accessibility tree
650+
* **clip** - if true, the mark is clipped to the frame’s dimensions
649651

650652
The **fill**, **fillOpacity**, **stroke**, **strokeWidth**, **strokeOpacity**, and **opacity** options can be specified as either channels or constants. When the fill or stroke is specified as a function or array, it is interpreted as a channel; when the fill or stroke is specified as a string, it is interpreted as a constant if a valid CSS color and otherwise it is interpreted as a column name for a channel. Similarly when the fill opacity, stroke opacity, object opacity, stroke width, or radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel.
651653

src/legends.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export function Legends(scales, options) {
7676
for (const [key, value] of legendRegistry) {
7777
const o = options[key];
7878
if (o && o.legend) {
79-
legends.push(value(scales[key], legendOptions(scales[key], o), key => scales[key]));
79+
const legend = value(scales[key], legendOptions(scales[key], o), key => scales[key]);
80+
if (legend != null) legends.push(legend);
8081
}
8182
}
8283
return legends;

src/marks/area.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export class Area extends Mark {
3030
);
3131
this.curve = Curve(curve, tension);
3232
}
33-
render(I, {x, y}, channels) {
33+
render(I, {x, y}, channels, dimensions) {
3434
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels;
3535
const {dx, dy} = this;
3636
return create("svg:g")
37-
.call(applyIndirectStyles, this)
37+
.call(applyIndirectStyles, this, dimensions)
3838
.call(applyTransform, x, y, dx, dy)
3939
.call(g => g.selectAll()
4040
.data(Z ? group(I, i => Z[i]).values() : [I])

src/marks/arrow.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Arrow extends Mark {
4444
this.insetStart = +insetStart;
4545
this.insetEnd = +insetEnd;
4646
}
47-
render(index, {x, y}, channels) {
47+
render(index, {x, y}, channels, dimensions) {
4848
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
4949
const {dx, dy, strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
5050
const sw = SW ? i => SW[i] : () => strokeWidth;
@@ -65,7 +65,7 @@ export class Arrow extends Mark {
6565
const wingScale = headLength / 1.5;
6666

6767
return create("svg:g")
68-
.call(applyIndirectStyles, this)
68+
.call(applyIndirectStyles, this, dimensions)
6969
.call(applyTransform, x, y, offset + dx, offset + dy)
7070
.call(g => g.selectAll()
7171
.data(index)

src/marks/bar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class AbstractBar extends Mark {
2121
render(index, scales, channels, dimensions) {
2222
const {dx, dy, rx, ry} = this;
2323
return create("svg:g")
24-
.call(applyIndirectStyles, this)
24+
.call(applyIndirectStyles, this, dimensions)
2525
.call(this._transform, scales, dx, dy)
2626
.call(g => g.selectAll()
2727
.data(index)

src/marks/brush.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class Brush extends Mark {
2929
const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions;
3030
const brush = this;
3131
const g = create("svg:g")
32-
.call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden})
32+
.call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden}, dimensions)
3333
.call((X && Y ? brusher : X ? brusherX : brusherY)()
3434
.extent([[marginLeft, marginTop], [width - marginRight, height - marginBottom]])
3535
.on("start brush end", function(event) {
@@ -65,7 +65,7 @@ export class Brush extends Mark {
6565
}))
6666
.call(g => g.selectAll(".selection")
6767
.attr("shape-rendering", null) // reset d3-brush
68-
.call(applyIndirectStyles, options)
68+
.call(applyIndirectStyles, options, dimensions)
6969
.call(applyDirectStyles, options))
7070
.node();
7171
g[selection] = null;

src/marks/dot.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class Dot extends Mark {
5454
const [cx, cy] = applyFrameAnchor(this, dimensions);
5555
const circle = this.symbol === symbolCircle;
5656
return create("svg:g")
57-
.call(applyIndirectStyles, this)
57+
.call(applyIndirectStyles, this, dimensions)
5858
.call(applyTransform, x, y, offset + dx, offset + dy)
5959
.call(g => g.selectAll()
6060
.data(index)

src/marks/frame.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class Frame extends Mark {
2828
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
2929
const {insetTop, insetRight, insetBottom, insetLeft, dx, dy} = this;
3030
return create("svg:rect")
31-
.call(applyIndirectStyles, this)
31+
.call(applyIndirectStyles, this, dimensions)
3232
.call(applyDirectStyles, this)
3333
.call(applyTransform, null, null, offset + dx, offset + dy)
3434
.attr("x", marginLeft + insetLeft)

src/marks/image.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class Image extends Mark {
6464
const {dx, dy} = this;
6565
const [cx, cy] = applyFrameAnchor(this, dimensions);
6666
return create("svg:g")
67-
.call(applyIndirectStyles, this)
67+
.call(applyIndirectStyles, this, dimensions)
6868
.call(applyTransform, x, y, offset + dx, offset + dy)
6969
.call(g => g.selectAll()
7070
.data(index)

src/marks/line.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export class Line extends Mark {
3030
this.curve = Curve(curve, tension);
3131
markers(this, options);
3232
}
33-
render(I, {x, y}, channels) {
33+
render(I, {x, y}, channels, dimensions) {
3434
const {x: X, y: Y, z: Z} = channels;
3535
const {dx, dy} = this;
3636
return create("svg:g")
37-
.call(applyIndirectStyles, this)
37+
.call(applyIndirectStyles, this, dimensions)
3838
.call(applyTransform, x, y, offset + dx, offset + dy)
3939
.call(g => g.selectAll()
4040
.data(Z ? group(I, i => Z[i]).values() : [I])

src/marks/link.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ export class Link extends Mark {
2828
this.curve = Curve(curve, tension);
2929
markers(this, options);
3030
}
31-
render(index, {x, y}, channels) {
31+
render(index, {x, y}, channels, dimensions) {
3232
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
3333
const {dx, dy, curve} = this;
3434
return create("svg:g")
35-
.call(applyIndirectStyles, this)
35+
.call(applyIndirectStyles, this, dimensions)
3636
.call(applyTransform, x, y, offset + dx, offset + dy)
3737
.call(g => g.selectAll()
3838
.data(index)

src/marks/pointer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Pointer extends Mark {
4444
const g = create("svg:g");
4545

4646
const parent = g.append("g")
47-
.call(applyIndirectStyles, this)
47+
.call(applyIndirectStyles, this, dimensions)
4848
.call(applyDirectStyles, this)
4949
.node();
5050

src/marks/rect.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class Rect extends Mark {
4949
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
5050
const {insetTop, insetRight, insetBottom, insetLeft, dx, dy, rx, ry} = this;
5151
return create("svg:g")
52-
.call(applyIndirectStyles, this)
52+
.call(applyIndirectStyles, this, dimensions)
5353
.call(applyTransform, x, y, dx, dy)
5454
.call(g => g.selectAll()
5555
.data(index)

src/marks/rule.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class RuleX extends Mark {
3939
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
4040
const {insetTop, insetBottom} = this;
4141
return create("svg:g")
42-
.call(applyIndirectStyles, this)
42+
.call(applyIndirectStyles, this, dimensions)
4343
.call(applyTransform, X && x, null, offset, 0)
4444
.call(g => g.selectAll("line")
4545
.data(index)
@@ -82,7 +82,7 @@ export class RuleY extends Mark {
8282
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
8383
const {insetLeft, insetRight, dx, dy} = this;
8484
return create("svg:g")
85-
.call(applyIndirectStyles, this)
85+
.call(applyIndirectStyles, this, dimensions)
8686
.call(applyTransform, null, Y && y, dx, offset + dy)
8787
.call(g => g.selectAll("line")
8888
.data(index)

src/marks/text.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export class Text extends Mark {
6363
const {dx, dy, rotate} = this;
6464
const [cx, cy] = applyFrameAnchor(this, dimensions);
6565
return create("svg:g")
66-
.call(applyIndirectTextStyles, this, T)
66+
.call(applyIndirectStyles, this, dimensions)
67+
.call(applyIndirectTextStyles, this, T, dimensions)
6768
.call(applyTransform, x, y, offset + dx, offset + dy)
6869
.call(g => g.selectAll()
6970
.data(index)
@@ -129,7 +130,6 @@ export function textY(data, {y = identity, ...options} = {}) {
129130
}
130131

131132
function applyIndirectTextStyles(selection, mark, T) {
132-
applyIndirectStyles(selection, mark);
133133
applyAttr(selection, "text-anchor", mark.textAnchor);
134134
applyAttr(selection, "font-family", mark.fontFamily);
135135
applyAttr(selection, "font-size", mark.fontSize);

src/marks/tick.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AbstractTick extends Mark {
1616
render(index, scales, channels, dimensions) {
1717
const {dx, dy} = this;
1818
return create("svg:g")
19-
.call(applyIndirectStyles, this)
19+
.call(applyIndirectStyles, this, dimensions)
2020
.call(this._transform, scales, dx, dy)
2121
.call(g => g.selectAll("line")
2222
.data(index)

src/marks/vector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Vector extends Mark {
4444
const k = anchor === "start" ? 0 : anchor === "end" ? 1 : 0.5;
4545
return create("svg:g")
4646
.attr("fill", "none")
47-
.call(applyIndirectStyles, this)
47+
.call(applyIndirectStyles, this, dimensions)
4848
.call(applyTransform, x, y, offset + dx, offset + dy)
4949
.call(g => g.selectAll()
5050
.data(index)

src/options.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {parse as isoParse} from "isoformat";
12
import {color, descending} from "d3";
23
import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3";
34
import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3";
@@ -210,6 +211,26 @@ export function isTemporal(values) {
210211
}
211212
}
212213

214+
// Are these strings that might represent dates? This is stricter than ISO 8601
215+
// because we want to ignore false positives on numbers; for example, the string
216+
// "1192" is more likely to represent a number than a date even though it is
217+
// valid ISO 8601 representing 1192-01-01.
218+
export function isTemporalString(values) {
219+
for (const value of values) {
220+
if (value == null) continue;
221+
return typeof value === "string" && isNaN(value) && isoParse(value);
222+
}
223+
}
224+
225+
// Are these strings that might represent numbers? This is stricter than
226+
// coercion because we want to ignore false positives on e.g. empty strings.
227+
export function isNumericString(values) {
228+
for (const value of values) {
229+
if (value == null || value === "") continue;
230+
return typeof value === "string" && !isNaN(value);
231+
}
232+
}
233+
213234
export function isNumeric(values) {
214235
for (const value of values) {
215236
if (value == null) continue;

0 commit comments

Comments
 (0)