diff --git a/src/index.js b/src/index.js index 3d282cd381..ced844c56b 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,15 @@ 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 {Brush, 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"; export {Image, image} from "./marks/image.js"; +export {Lasso, lasso} from "./marks/lasso.js"; export {Line, line, lineX, lineY} from "./marks/line.js"; export {Link, link} from "./marks/link.js"; +export {Pointer, pointer, pointerX, pointerY} from "./marks/pointer.js"; export {Rect, rect, rectX, rectY} from "./marks/rect.js"; export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; diff --git a/src/marks/brush.js b/src/marks/brush.js index d319d6b732..ab7a24d06c 100644 --- a/src/marks/brush.js +++ b/src/marks/brush.js @@ -79,9 +79,9 @@ export function brush(data, {x, y, ...options} = {}) { } export function brushX(data, {x = identity, ...options} = {}) { - return new Brush(data, {...options, x, y: null}); + return new Brush(data, {...options, x}); } export function brushY(data, {y = identity, ...options} = {}) { - return new Brush(data, {...options, x: null, y}); + return new Brush(data, {...options, y}); } diff --git a/src/marks/lasso.js b/src/marks/lasso.js new file mode 100644 index 0000000000..7a832187a7 --- /dev/null +++ b/src/marks/lasso.js @@ -0,0 +1,169 @@ +import {create, select, dispatch as dispatcher, line, pointer, polygonContains, curveNatural} from "d3"; +import {maybeTuple} from "../options.js"; +import {Mark} from "../plot.js"; +import {selection, selectionEquals} from "../selection.js"; +import {applyIndirectStyles} from "../style.js"; + +const defaults = { + ariaLabel: "lasso", + fill: "#777", + fillOpacity: 0.3, + stroke: "#666", + strokeWidth: 2 +}; + +export class Lasso extends Mark { + constructor(data, {x, y, ...options} = {}) { + super( + data, + [ + {name: "x", value: x, scale: "x"}, + {name: "y", value: y, scale: "y"} + ], + options, + defaults + ); + this.activeElement = null; + } + + // The lasso polygons follow the even-odd rule in css, matching the way + // they are computed by polygonContains. + render(index, {x, y}, {x: X, y: Y}, dimensions) { + const margin = 5; + const {ariaLabel, ariaDescription, ariaHidden, fill, fillOpacity, stroke, strokeWidth} = this; + const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions; + + const path = line().curve(curveNatural); + const g = create("svg:g") + .call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden, fill, fillOpacity, stroke, strokeWidth}); + g.append("rect") + .attr("x", marginLeft) + .attr("y", marginTop) + .attr("width", width - marginLeft - marginRight) + .attr("height", height - marginTop - marginBottom) + .attr("stroke", "none") + .attr("fill", "none") + .attr("cursor", "cross") // TODO + .attr("pointer-events", "all") + .attr("fill-rule", "evenodd"); + + g.call(lassoer() + .extent([[marginLeft - margin, marginTop - margin], [width - marginRight + margin, height - marginBottom + margin]]) + .on("start lasso end cancel", (polygons) => { + g.selectAll("path") + .data(polygons) + .join("path") + .attr("d", path); + let S = null; + let activePolygons = polygons.filter(polygon => polygon.length > 2); + if (activePolygons.length > 0) { + let bw; + if (x.bandwidth && (bw = x.bandwidth() / 2)) activePolygons = activePolygons.map(polygon => polygon.map(p => [p[0] - bw, p[1]])); + if (y.bandwidth && (bw = y.bandwidth() / 2)) activePolygons = activePolygons.map(polygon => polygon.map(p => [p[0], p[1] - bw])); + S = index.filter(i => activePolygons.some(polygon => polygonContains(polygon, [X[i], Y[i]]))); + + } + if (!selectionEquals(node[selection], S)) { + node[selection] = S; + node.dispatchEvent(new Event("input", {bubbles: true})); + } + })); + const node = g.node(); + node[selection] = null; + return node; + } +} + +export function lasso(data, {x, y, ...options} = {}) { + ([x, y] = maybeTuple(x, y)); + return new Lasso(data, {...options, x, y}); +} + +// set up listeners that will follow this gesture all along +// (even outside the target canvas) +// TODO: in a supporting file +function trackPointer(e, { start, move, out, end }) { + const tracker = {}, + id = (tracker.id = e.pointerId), + target = e.target; + tracker.point = pointer(e, target); + target.setPointerCapture(id); + + select(target) + .on(`pointerup.${id} pointercancel.${id}`, e => { + if (e.pointerId !== id) return; + tracker.sourceEvent = e; + select(target).on(`.${id}`, null); + target.releasePointerCapture(id); + end && end(tracker); + }) + .on(`pointermove.${id}`, e => { + if (e.pointerId !== id) return; + tracker.sourceEvent = e; + tracker.prev = tracker.point; + tracker.point = pointer(e, target); + move && move(tracker); + }) + .on(`pointerout.${id}`, e => { + if (e.pointerId !== id) return; + tracker.sourceEvent = e; + tracker.point = null; + out && out(tracker); + }); + + start && start(tracker); +} + +function lassoer() { + const polygons = []; + const dispatch = dispatcher("start", "lasso", "end", "cancel"); + let extent; + const lasso = selection => { + const node = selection.node(); + let currentPolygon; + + selection + .on("touchmove", e => e.preventDefault()) // prevent scrolling + .on("pointerdown", e => { + const p = pointer(e, node); + for (let i = polygons.length - 1; i >= 0; --i) { + if (polygonContains(polygons[i], p)) { + polygons.splice(i, 1); + dispatch.call("cancel", node, polygons); + return; + } + } + trackPointer(e, { + start: p => { + currentPolygon = [constrainExtent(p.point)]; + polygons.push(currentPolygon); + dispatch.call("start", node, polygons); + }, + move: p => { + currentPolygon.push(constrainExtent(p.point)); + dispatch.call("lasso", node, polygons); + }, + end: () => { + dispatch.call("end", node, polygons); + } + }); + }); + }; + lasso.on = function(type, _) { + return _ ? (dispatch.on(...arguments), lasso) : dispatch.on(...arguments); + }; + lasso.extent = function(_) { + return _ ? (extent = _, lasso) : extent; + }; + + function constrainExtent(p) { + if (!extent) return p; + return [clamp(p[0], extent[0][0], extent[1][0]), clamp(p[1], extent[0][1], extent[1][1])]; + } + + function clamp(x, a, b) { + return x < a ? a : x > b ? b : x; + } + + return lasso; +} \ No newline at end of file diff --git a/src/marks/pointer.js b/src/marks/pointer.js new file mode 100644 index 0000000000..08120f1f7c --- /dev/null +++ b/src/marks/pointer.js @@ -0,0 +1,225 @@ +import {create, namespaces, pointer as pointerof, quickselect, union} from "d3"; +import {identity, maybeFrameAnchor, maybeTuple} from "../options.js"; +import {Mark} from "../plot.js"; +import {selection} from "../selection.js"; +import {applyDirectStyles, applyFrameAnchor, applyIndirectStyles} from "../style.js"; + +const defaults = { + ariaLabel: "pointer", + fill: "none", + stroke: "#3b5fc0", + strokeWidth: 1.5 +}; + +export class Pointer extends Mark { + constructor(data, { + x, + y, + n = Infinity, + r = isFinite(n) ? 120 : 20, + mode = "auto", + frameAnchor, + ...options + } = {}) { + super( + data, + [ + {name: "x", value: x, scale: "x", optional: true}, + {name: "y", value: y, scale: "y", optional: true} + ], + options, + defaults + ); + this.n = +n; + this.r = +r; + this.mode = maybeMode(mode, x, y); + this.frameAnchor = maybeFrameAnchor(frameAnchor); + } + render(index, scales, {x: X, y: Y}, dimensions) { + const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions; + const {mode, n, r} = this; + const [cx, cy] = applyFrameAnchor(this, dimensions); + const r2 = r * r; // the squared radius; to determine points in proximity to the pointer + const down = new Set(); // the set of pointers that are currently down + let C = []; // a sparse index from index[i] to an svg:circle element + let P = null; // the persistent selection; a subset of index, or null + + const g = create("svg:g"); + + const parent = g.append("g") + .call(applyIndirectStyles, this) + .call(applyDirectStyles, this) + .node(); + + // Renders the given logical selection S, a subset of index. Applies + // copy-on-write to the array of circles C. Returns true if the selection + // changed, and false otherwise. + function render(S) { + const SC = []; + let changed = false; + + // Enter (append) the newly-selected elements. The order of the circles is + // arbitrary, with the most recently selected datum on top. + S.forEach(i => { + let c = C[i]; + if (!c) { + c = document.createElementNS(namespaces.svg, "circle"); + c.setAttribute("id", i); + c.setAttribute("r", 4); + c.setAttribute("cx", X ? X[i] : cx); + c.setAttribute("cy", Y ? Y[i] : cy); + parent.appendChild(c); + changed = true; + } + SC[i] = c; + }); + + // Exit (remove) the no-longer-selected elements. + C.forEach((c, i) => { + if (!SC[i]) { + c.remove(); + changed = true; + } + }); + + if (changed) C = SC; + return changed; + } + + // Selects the given logical selection S, a subset of index, or null if + // there is no selection. + function select(S) { + if (S === null) render([]); + else if (!render(S)) return; + node[selection] = S; + node.dispatchEvent(new Event("input", {bubbles: true})); + } + + g.append("rect") + .attr("fill", "none") + .attr("pointer-events", "all") + .attr("width", width + marginLeft + marginRight) + .attr("height", height + marginTop + marginBottom) + .on("pointerdown pointerover pointermove", event => { + + // On pointerdown, initiate a new persistent selection, P, or extend + // the existing persistent selection if the shift key is down; then + // add to P for as long as the pointer remains down. If there is no + // existing persistent selection on pointerdown, initialize P to the + // empty selection rather than the points near the pointer such that + // you can clear the persistent selection with a pointerdown followed + // by a pointerup. (See below.) + if (event.type === "pointerdown") { + const nop = !P; + down.add(event.pointerId); + if (nop || !event.shiftKey) P = []; + if (!nop && !event.shiftKey) return select(P); + } + + // If any pointer is down, only consider pointers that are down. + if (P && !down.has(event.pointerId)) return; + + // Compute the current selection, S: the subset of index that is + // logically selected. Normally this should be an in-order subset of + // index, but it isn’t here because quickselect will reorder in-place + // if the n option is used! + const [mx, my] = pointerof(event); + let S = index; + switch (mode) { + case "xy": { + if (r < Infinity) { + S = S.filter(i => { + const dx = X[i] - mx, dy = Y[i] - my; + return dx * dx + dy * dy <= r2; + }); + } + if (S.length > n) { + S = S.slice(); + quickselect(S, n, undefined, undefined, (i, j) => { + const ix = X[i] - mx, iy = Y[i] - my; + const jx = X[j] - mx, jy = Y[j] - my; + return (ix * ix + iy * iy) - (jx * jx + jy * jy); + }); + S = S.slice(0, n); + } + break; + } + case "x": { + if (r < Infinity) { + const [x0, x1] = [mx - r, mx + r]; + S = S.filter(i => x0 <= X[i] && X[i] <= x1); + } + if (S.length > n) { + S = S.slice(); + quickselect(S, n, undefined, undefined, (i, j) => { + const ix = X[i] - mx; + const jx = X[j] - mx; + return ix * ix - jx * jx; + }); + S = S.slice(0, n); + } + break; + } + case "y": { + if (r < Infinity) { + const [y0, y1] = [my - r, my + r]; + S = S.filter(i => y0 <= Y[i] && Y[i] <= y1); + } + if (S.length > n) { + S = S.slice(); + quickselect(S, n, undefined, undefined, (i, j) => { + const iy = Y[i] - my; + const jy = Y[j] - my; + return iy * iy - jy * jy; + }); + S = S.slice(0, n); + } + break; + } + } + + // If there is a persistent selection, add the new selection to the + // persistent selection; otherwise just use the current selection. + select(P ? (P = Array.from(union(P, S))) : S); + }) + .on("pointerup", event => { + // On pointerup, if the selection is empty, clear the persistent to + // selection to allow the ephemeral selection on subsequent hover. + if (!P.length) select(P = null); + down.delete(event.pointerId); + }) + .on("pointerout", () => { + // On pointerout, if there is no persistent selection, clear the + // ephemeral selection. + if (!P) select(null); + }); + + const node = g.node(); + node[selection] = null; + return node; + } +} + +function maybeMode(mode = "auto", x, y) { + switch (mode = `${mode}`.toLowerCase()) { + case "auto": mode = y == null ? "x" : x == null ? "y" : "xy"; break; + case "x": case "y": case "xy": break; + default: throw new Error(`invalid mode: ${mode}`); + } + if (/^x/.test(mode) && x == null) throw new Error("missing channel: x"); + if (/y$/.test(mode) && y == null) throw new Error("missing channel: y"); + return mode; +} + +export function pointer(data, {x, y, ...options} = {}) { + ([x, y] = maybeTuple(x, y)); + return new Pointer(data, {...options, x, y}); +} + +export function pointerX(data, {mode = "x", x = identity, ...options} = {}) { + return new Pointer(data, {...options, mode, x}); +} + +export function pointerY(data, {mode = "y", y = identity, ...options} = {}) { + return new Pointer(data, {...options, mode, y}); +} diff --git a/test/output/gistempAnomalyBrush.html b/test/output/gistempAnomalyBrush.html new file mode 100644 index 0000000000..9f33ad8da9 --- /dev/null +++ b/test/output/gistempAnomalyBrush.html @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1644 \ No newline at end of file diff --git a/test/output/gistempAnomalyBrush.svg b/test/output/gistempAnomalyBrush.svg deleted file mode 100644 index 4f7b3e8ac3..0000000000 --- a/test/output/gistempAnomalyBrush.svg +++ /dev/null @@ -1,1742 +0,0 @@ - - - - - - −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/gistempAnomalyPointer.html b/test/output/gistempAnomalyPointer.html new file mode 100644 index 0000000000..f86c9ef973 --- /dev/null +++ b/test/output/gistempAnomalyPointer.html @@ -0,0 +1,1734 @@ + + + + + + −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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1644 \ No newline at end of file diff --git a/test/output/penguinCulmenBrushX.html b/test/output/penguinCulmenBrushX.html new file mode 100644 index 0000000000..e499d3780b --- /dev/null +++ b/test/output/penguinCulmenBrushX.html @@ -0,0 +1,2988 @@ + + + + + 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/output/penguinCulmenLasso.html b/test/output/penguinCulmenLasso.html new file mode 100644 index 0000000000..a4ca0c096e --- /dev/null +++ b/test/output/penguinCulmenLasso.html @@ -0,0 +1,1143 @@ + + + + + 35 + + + + 40 + + + + 45 + + + + 50 + + + + 55 + + ↑ culmen_length_mm + + + + FEMALE + + + MALE + + + + sex + + + + + + + 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 index 3a3b2f97d8..cad771c8a7 100644 --- a/test/plots/gistemp-anomaly-brush.js +++ b/test/plots/gistemp-anomaly-brush.js @@ -1,9 +1,10 @@ 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/gistemp.csv", d3.autoType); - return Plot.plot({ + const plot = Plot.plot({ y: { label: "↑ Temperature anomaly (°C)", tickFormat: "+f", @@ -19,4 +20,8 @@ export default async function() { Plot.brush(data, {x: "Date", y: "Anomaly"}) ] }); + const output = html``; + plot.oninput = () => output.value = plot.value.length; + plot.oninput(); + return html`${plot}${output}`; } diff --git a/test/plots/gistemp-anomaly-pointer.js b/test/plots/gistemp-anomaly-pointer.js new file mode 100644 index 0000000000..82480f948d --- /dev/null +++ b/test/plots/gistemp-anomaly-pointer.js @@ -0,0 +1,27 @@ +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/gistemp.csv", d3.autoType); + const plot = 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.pointer(data, {x: "Date", y: "Anomaly", n: 5}) + ] + }); + const output = html``; + plot.oninput = () => output.value = plot.value.length; + plot.oninput(); + return html`${plot}${output}`; +} diff --git a/test/plots/index.js b/test/plots/index.js index 8aac7a927e..3f24f32a31 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -50,6 +50,7 @@ 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 gistempAnomalyPointer} from "./gistemp-anomaly-pointer.js"; export {default as gistempAnomalyTransform} from "./gistemp-anomaly-transform.js"; export {default as googleTrendsRidgeline} from "./google-trends-ridgeline.js"; export {default as gridChoropleth} from "./grid-choropleth.js"; @@ -91,6 +92,8 @@ 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 penguinCulmenBrushX} from "./penguin-culmen-brush-x.js"; +export {default as penguinCulmenLasso} from "./penguin-culmen-lasso.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-x.js b/test/plots/penguin-culmen-brush-x.js new file mode 100644 index 0000000000..09dd023e4d --- /dev/null +++ b/test/plots/penguin-culmen-brush-x.js @@ -0,0 +1,38 @@ +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.brushX(data, { + x: "culmen_depth_mm" + }) + ] + }); + const output = html``; + plot.oninput = () => output.value = plot.value.length; + plot.oninput(); + return html`${plot}${output}`; +} diff --git a/test/plots/penguin-culmen-lasso.js b/test/plots/penguin-culmen-lasso.js new file mode 100644 index 0000000000..de85904be8 --- /dev/null +++ b/test/plots/penguin-culmen-lasso.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: 300, + grid: true, + facet: { + data, + x: "sex", + 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.lasso(data, { + x: "culmen_depth_mm", + y: "culmen_length_mm", + fill: "green" + }) + ] + }); + const output = html``; + plot.oninput = () => output.value = plot.value.length; + plot.oninput(); + return html`${plot}${output}`; +}