diff --git a/README.md b/README.md index 6fb96f2eb3..7359aec171 100644 --- a/README.md +++ b/README.md @@ -1220,6 +1220,38 @@ 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₂*, …]. +## Interactions + +Interactions are special marks that handle user input and define interactive selections. When a plot has an interaction mark, the returned *plot*.value represents the current selection as an array subset of the interaction mark’s data. As the user modifies the selection through interaction with the plot, *input* events are emitted. This design is compatible with [Observable’s viewof operator](https://observablehq.com/@observablehq/introduction-to-views), but you can also listen to *input* events directly via the [EventTarget interface](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget). + +### Brush + +[Source](./src/marks/brush.js) · [Examples](https://observablehq.com/@observablehq/plot-brush) · Selects points within a single contiguous rectangular region, such as nearby dots in a scatterplot. + +#### Plot.brush(*data*, *options*) + +```js +Plot.brush(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"}) +``` + +Returns a new brush 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₂*, …]. + +#### Plot.brushX(*data*, *options*) + +```js +Plot.brushX(penguins, {x: "culmen_depth_mm"}) +``` + +Equivalent to [Plot.brush](#plotbrushdata-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …]. + +#### Plot.brushY(*data*, *options*) + +```js +Plot.brushY(penguins, {y: "culmen_length_mm"}) +``` + +Equivalent to [Plot.brush](#plotbrushdata-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. + ## Decorations Decorations are static marks that do not represent data. Currently this includes only [Plot.frame](#frame), although internally Plot’s axes are implemented as decorations and may in the future be exposed here for more flexible configuration. diff --git a/src/index.js b/src/index.js index 7ac7a63778..3d282cd381 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ export {plot, Mark, marks} from "./plot.js"; export {Area, area, areaX, areaY} from "./marks/area.js"; export {Arrow, arrow} from "./marks/arrow.js"; export {BarX, BarY, barX, barY} from "./marks/bar.js"; +export {brush, brushX, brushY} from "./marks/brush.js"; export {Cell, cell, cellX, cellY} from "./marks/cell.js"; export {Dot, dot, dotX, dotY} from "./marks/dot.js"; export {Frame, frame} from "./marks/frame.js"; @@ -13,6 +14,7 @@ export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; export {TickX, TickY, tickX, tickY} from "./marks/tick.js"; export {Vector, vector} from "./marks/vector.js"; +export {selection} from "./selection.js"; export {valueof} from "./options.js"; export {filter, reverse, sort, shuffle} from "./transforms/basic.js"; export {bin, binX, binY} from "./transforms/bin.js"; diff --git a/src/marks/brush.js b/src/marks/brush.js new file mode 100644 index 0000000000..d319d6b732 --- /dev/null +++ b/src/marks/brush.js @@ -0,0 +1,87 @@ +import {brush as brusher, brushX as brusherX, brushY as brusherY, create, select} from "d3"; +import {identity, maybeTuple} from "../options.js"; +import {Mark} from "../plot.js"; +import {selection, selectionEquals} from "../selection.js"; +import {applyDirectStyles, applyIndirectStyles} from "../style.js"; + +const defaults = { + ariaLabel: "brush", + fill: "#777", + fillOpacity: 0.3, + stroke: "#fff" +}; + +export class Brush extends Mark { + constructor(data, {x, y, ...options} = {}) { + super( + data, + [ + {name: "x", value: x, scale: "x", optional: true}, + {name: "y", value: y, scale: "y", optional: true} + ], + options, + defaults + ); + this.activeElement = null; + } + render(index, {x, y}, {x: X, y: Y}, dimensions) { + const {ariaLabel, ariaDescription, ariaHidden, ...options} = this; + const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions; + const brush = this; + const g = create("svg:g") + .call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden}) + .call((X && Y ? brusher : X ? brusherX : brusherY)() + .extent([[marginLeft, marginTop], [width - marginRight, height - marginBottom]]) + .on("start brush end", function(event) { + const {type, selection: extent} = event; + // For faceting, when starting a brush in a new facet, clear the + // brush and selection on the old facet. In the future, we might + // allow independent brushes across facets by disabling this? + if (type === "start" && brush.activeElement !== this) { + if (brush.activeElement !== null) { + select(brush.activeElement).call(event.target.clear, event); + brush.activeElement[selection] = null; + } + brush.activeElement = this; + } + let S = null; + if (extent) { + S = index; + if (X) { + let [x0, x1] = Y ? [extent[0][0], extent[1][0]] : extent; + if (x.bandwidth) x0 -= x.bandwidth(); + S = S.filter(i => x0 <= X[i] && X[i] <= x1); + } + if (Y) { + let [y0, y1] = X ? [extent[0][1], extent[1][1]] : extent; + if (y.bandwidth) y0 -= y.bandwidth(); + S = S.filter(i => y0 <= Y[i] && Y[i] <= y1); + } + } + if (!selectionEquals(this[selection], S)) { + this[selection] = S; + this.dispatchEvent(new Event("input", {bubbles: true})); + } + })) + .call(g => g.selectAll(".selection") + .attr("shape-rendering", null) // reset d3-brush + .call(applyIndirectStyles, options) + .call(applyDirectStyles, options)) + .node(); + g[selection] = null; + return g; + } +} + +export function brush(data, {x, y, ...options} = {}) { + ([x, y] = maybeTuple(x, y)); + return new Brush(data, {...options, x, y}); +} + +export function brushX(data, {x = identity, ...options} = {}) { + return new Brush(data, {...options, x, y: null}); +} + +export function brushY(data, {y = identity, ...options} = {}) { + return new Brush(data, {...options, x: null, y}); +} diff --git a/src/plot.js b/src/plot.js index 8e7ed9e028..a26ba715cf 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,11 +1,12 @@ -import {create, cross, difference, groups, InternMap} from "d3"; +import {create, cross, difference, groups, InternMap, union} from "d3"; import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js"; import {Channel, channelSort} from "./channel.js"; import {defined} from "./defined.js"; import {Dimensions} from "./dimensions.js"; import {Legends, exposeLegends} from "./legends.js"; -import {arrayify, isOptions, keyword, range, first, second, where} from "./options.js"; +import {arrayify, isOptions, keyword, range, first, second, where, take} from "./options.js"; import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js"; +import {selection} from "./selection.js"; import {applyInlineStyles, maybeClassName, styles} from "./style.js"; import {basic} from "./transforms/basic.js"; @@ -95,12 +96,21 @@ export function plot(options = {}) { .call(applyInlineStyles, style) .node(); + let initialValue; for (const mark of marks) { const channels = markChannels.get(mark) ?? []; const values = applyScales(channels, scales); const index = filter(markIndex.get(mark), channels, values); const node = mark.render(index, scales, values, dimensions, axes); - if (node != null) svg.appendChild(node); + if (node != null) { + if (node[selection] !== undefined) { + initialValue = markValue(mark, node[selection]); + node.addEventListener("input", () => { + figure.value = markValue(mark, node[selection]); + }); + } + svg.appendChild(node); + } } // Wrap the plot in a figure with a caption, if desired. @@ -119,6 +129,7 @@ export function plot(options = {}) { figure.scale = exposeScales(scaleDescriptors); figure.legend = exposeLegends(scaleDescriptors, options); + figure.value = initialValue; return figure; } @@ -189,6 +200,10 @@ function markify(mark) { return mark instanceof Mark ? mark : new Render(mark); } +function markValue(mark, selection) { + return selection === null ? mark.data : take(mark.data, selection); +} + class Render extends Mark { constructor(render) { super(); @@ -263,8 +278,8 @@ class Facet extends Mark { } return {index, channels: [...channels, ...subchannels]}; } - render(I, scales, channels, dimensions, axes) { - const {marks, marksChannels, marksIndexByFacet} = this; + render(I, scales, _, dimensions, axes) { + const {data, channels, marks, marksChannels, marksIndexByFacet} = this; const {fx, fy} = scales; const fyDomain = fy && fy.domain(); const fxDomain = fx && fx.domain(); @@ -272,7 +287,8 @@ class Facet extends Mark { const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()}; const subdimensions = {...dimensions, ...fxMargins, ...fyMargins}; const marksValues = marksChannels.map(channels => applyScales(channels, scales)); - return create("svg:g") + let selectionByFacet; + const parent = create("svg:g") .call(g => { if (fy && axes.y) { const axis1 = axes.y, axis2 = nolabel(axis1); @@ -316,10 +332,25 @@ class Facet extends Mark { const values = marksValues[i]; const index = filter(marksFacetIndex[i], marksChannels[i], values); const node = marks[i].render(index, scales, values, subdimensions); - if (node != null) this.appendChild(node); + if (node != null) { + if (node[selection] !== undefined) { + if (marks[i].data !== data) throw new Error("selection must use facet data"); + if (selectionByFacet === undefined) selectionByFacet = facetMap(channels); + selectionByFacet.set(key, node[selection]); + node.addEventListener("input", () => { + selectionByFacet.set(key, node[selection]); + parent[selection] = facetSelection(selectionByFacet); + }); + } + this.appendChild(node); + } } })) .node(); + if (selectionByFacet !== undefined) { + parent[selection] = facetSelection(selectionByFacet); + } + return parent; } } @@ -362,6 +393,20 @@ function facetTranslate(fx, fy) { : ky => `translate(0,${fy(ky)})`; } +// If multiple facets define a selection, then the overall selection is the +// union of the defined selections. As with non-faceted plots, we assume that +// only a single mark is defining the selection; if multiple marks define a +// selection, generally speaking the last one wins, although the behavior is not +// explicitly defined. +function facetSelection(selectionByFacet) { + let selection = null; + for (const value of selectionByFacet.values()) { + if (value === null) continue; + selection = selection === null ? value : union(selection, value); + } + return selection; +} + function facetMap(channels) { return new (channels.length > 1 ? FacetMap2 : FacetMap); } @@ -379,6 +424,9 @@ class FacetMap { set(key, value) { return this._.set(key, value), this; } + values() { + return this._.values(); + } } // A Map-like interface that supports paired keys. @@ -397,4 +445,9 @@ class FacetMap2 extends FacetMap { else super.set(key1, new InternMap([[key2, value]])); return this; } + *values() { + for (const map of this._.values()) { + yield* map.values(); + } + } } diff --git a/src/selection.js b/src/selection.js new file mode 100644 index 0000000000..228a22743c --- /dev/null +++ b/src/selection.js @@ -0,0 +1,18 @@ +// This symbol is used by interactive marks to define which data are selected. A +// node returned by mark.render may expose a selection as node[selection], whose +// value may be an array of numbers (e.g., [0, 1, 2, …]) representing an +// in-order subset of the rendered index, or null if the selection is undefined. +// The selection can be updated during interaction by emitting an input event. +export const selection = Symbol("selection"); + +// Given two (possibly null, possibly an index, but not undefined) selections, +// returns true if the two represent the same selection, and false otherwise. +// This assumes that the selection is a in-order subset of the original index. +export function selectionEquals(s1, s2) { + if (s1 === s2) return true; + if (s1 == null || s2 == null) return false; + const n = s1.length; + if (n !== s2.length) return false; + for (let i = 0; i < n; ++i) if (s1[i] !== s2[i]) return false; + return true; +} diff --git a/src/style.js b/src/style.js index e45c4bc550..138df2e06c 100644 --- a/src/style.js +++ b/src/style.js @@ -32,7 +32,9 @@ export function styles( { ariaLabel: cariaLabel, fill: defaultFill = "currentColor", + fillOpacity: defaultFillOpacity, stroke: defaultStroke = "none", + strokeOpacity: defaultStrokeOpacity, strokeWidth: defaultStrokeWidth, strokeLinecap: defaultStrokeLinecap, strokeLinejoin: defaultStrokeLinejoin, @@ -66,9 +68,9 @@ export function styles( } const [vfill, cfill] = maybeColorChannel(fill, defaultFill); - const [vfillOpacity, cfillOpacity] = maybeNumberChannel(fillOpacity); + const [vfillOpacity, cfillOpacity] = maybeNumberChannel(fillOpacity, defaultFillOpacity); const [vstroke, cstroke] = maybeColorChannel(stroke, defaultStroke); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumberChannel(strokeOpacity); + const [vstrokeOpacity, cstrokeOpacity] = maybeNumberChannel(strokeOpacity, defaultStrokeOpacity); const [vopacity, copacity] = maybeNumberChannel(opacity); // For styles that have no effect if there is no stroke, only apply the diff --git a/test/jsdom.js b/test/jsdom.js index 28781b2f70..94e9193c25 100644 --- a/test/jsdom.js +++ b/test/jsdom.js @@ -19,6 +19,7 @@ function withJsdom(run) { const jsdom = new JSDOM(""); global.window = jsdom.window; global.document = jsdom.window.document; + global.navigator = jsdom.window.navigator; global.Event = jsdom.window.Event; global.Node = jsdom.window.Node; global.NodeList = jsdom.window.NodeList; @@ -29,6 +30,7 @@ function withJsdom(run) { } finally { delete global.window; delete global.document; + delete global.navigator; delete global.Event; delete global.Node; delete global.NodeList; diff --git a/test/output/gistempAnomalyBrush.svg b/test/output/gistempAnomalyBrush.svg new file mode 100644 index 0000000000..e374013d66 --- /dev/null +++ b/test/output/gistempAnomalyBrush.svg @@ -0,0 +1,1742 @@ + + + + + + −0.6 + + + + −0.4 + + + + −0.2 + + + + +0.0 + + + + +0.2 + + + + +0.4 + + + + +0.6 + + + + +0.8 + + + + +1.0 + + + + +1.2 + ↑ Temperature anomaly (°C) + + + + 1880 + + + 1900 + + + 1920 + + + 1940 + + + 1960 + + + 1980 + + + 2000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinCulmenBrush.html b/test/output/penguinCulmenBrush.html new file mode 100644 index 0000000000..b5a07679a1 --- /dev/null +++ b/test/output/penguinCulmenBrush.html @@ -0,0 +1,3036 @@ + + + + + Adelie + + + Chinstrap + + + Gentoo + species + + + + FEMALE + + + MALE + + + + sex + + + + + + 35 + + + + 40 + + + + 45 + + + + 50 + + + + 55 + + ↑ culmen_length_mm + + + + + + 35 + + + + 40 + + + + 45 + + + + 50 + + + + 55 + + + + + + + + 35 + + + + 40 + + + + 45 + + + + 50 + + + + 55 + + + + + + + + 15 + + + + 20 + + + + + + + + 15 + + + + 20 + + + + + + + + 15 + + + + 20 + + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 344 \ No newline at end of file diff --git a/test/plots/gistemp-anomaly-brush.js b/test/plots/gistemp-anomaly-brush.js new file mode 100644 index 0000000000..3a3b2f97d8 --- /dev/null +++ b/test/plots/gistemp-anomaly-brush.js @@ -0,0 +1,22 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/gistemp.csv", d3.autoType); + return Plot.plot({ + y: { + label: "↑ Temperature anomaly (°C)", + tickFormat: "+f", + grid: true + }, + color: { + type: "diverging", + reverse: true + }, + marks: [ + Plot.ruleY([0]), + Plot.dot(data, {x: "Date", y: "Anomaly", stroke: "Anomaly"}), + Plot.brush(data, {x: "Date", y: "Anomaly"}) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 88ef7e17ed..a4e4b8c788 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -46,6 +46,7 @@ export {default as footballCoverage} from "./football-coverage.js"; export {default as fruitSales} from "./fruit-sales.js"; export {default as fruitSalesDate} from "./fruit-sales-date.js"; export {default as gistempAnomaly} from "./gistemp-anomaly.js"; +export {default as gistempAnomalyBrush} from "./gistemp-anomaly-brush.js"; export {default as gistempAnomalyMoving} from "./gistemp-anomaly-moving.js"; export {default as gistempAnomalyTransform} from "./gistemp-anomaly-transform.js"; export {default as googleTrendsRidgeline} from "./google-trends-ridgeline.js"; @@ -87,6 +88,7 @@ export {default as musicRevenue} from "./music-revenue.js"; export {default as ordinalBar} from "./ordinal-bar.js"; export {default as penguinCulmen} from "./penguin-culmen.js"; export {default as penguinCulmenArray} from "./penguin-culmen-array.js"; +export {default as penguinCulmenBrush} from "./penguin-culmen-brush.js"; export {default as penguinIslandUnknown} from "./penguin-island-unknown.js"; export {default as penguinMass} from "./penguin-mass.js"; export {default as penguinMassSex} from "./penguin-mass-sex.js"; diff --git a/test/plots/penguin-culmen-brush.js b/test/plots/penguin-culmen-brush.js new file mode 100644 index 0000000000..4f08af205e --- /dev/null +++ b/test/plots/penguin-culmen-brush.js @@ -0,0 +1,39 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {html} from "htl"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + const plot = Plot.plot({ + height: 600, + grid: true, + facet: { + data, + x: "sex", + y: "species", + marginRight: 80 + }, + marks: [ + Plot.frame(), + Plot.dot(data, { + facet: "exclude", + x: "culmen_depth_mm", + y: "culmen_length_mm", + r: 2, + fill: "#ddd" + }), + Plot.dot(data, { + x: "culmen_depth_mm", + y: "culmen_length_mm" + }), + Plot.brush(data, { + x: "culmen_depth_mm", + y: "culmen_length_mm" + }) + ] + }); + const output = html``; + plot.oninput = () => output.value = plot.value.length; + plot.oninput(); + return html`${plot}${output}`; +}