|
| 1 | +import {brush as brusher, brushX as brusherX, brushY as brusherY, create, select} from "d3"; |
| 2 | +import {identity, maybeTuple} from "../options.js"; |
| 3 | +import {Mark} from "../plot.js"; |
| 4 | +import {selection, selectionEquals} from "../selection.js"; |
| 5 | +import {applyDirectStyles, applyIndirectStyles} from "../style.js"; |
| 6 | + |
| 7 | +const defaults = { |
| 8 | + ariaLabel: "brush", |
| 9 | + fill: "#777", |
| 10 | + fillOpacity: 0.3, |
| 11 | + stroke: "#fff" |
| 12 | +}; |
| 13 | + |
| 14 | +export class Brush extends Mark { |
| 15 | + constructor(data, {x, y, ...options} = {}) { |
| 16 | + super( |
| 17 | + data, |
| 18 | + [ |
| 19 | + {name: "x", value: x, scale: "x", optional: true}, |
| 20 | + {name: "y", value: y, scale: "y", optional: true} |
| 21 | + ], |
| 22 | + options, |
| 23 | + defaults |
| 24 | + ); |
| 25 | + this.activeElement = null; |
| 26 | + } |
| 27 | + render(index, {x, y}, {x: X, y: Y}, dimensions) { |
| 28 | + const {ariaLabel, ariaDescription, ariaHidden, ...options} = this; |
| 29 | + const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions; |
| 30 | + const brush = this; |
| 31 | + const g = create("svg:g") |
| 32 | + .call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden}, dimensions) |
| 33 | + .call((X && Y ? brusher : X ? brusherX : brusherY)() |
| 34 | + .extent([[marginLeft, marginTop], [width - marginRight, height - marginBottom]]) |
| 35 | + .on("start brush end", function(event) { |
| 36 | + const {type, selection: extent} = event; |
| 37 | + // For faceting, when starting a brush in a new facet, clear the |
| 38 | + // brush and selection on the old facet. In the future, we might |
| 39 | + // allow independent brushes across facets by disabling this? |
| 40 | + if (type === "start" && brush.activeElement !== this) { |
| 41 | + if (brush.activeElement !== null) { |
| 42 | + select(brush.activeElement).call(event.target.clear, event); |
| 43 | + brush.activeElement[selection] = null; |
| 44 | + } |
| 45 | + brush.activeElement = this; |
| 46 | + } |
| 47 | + let S = null; |
| 48 | + if (extent) { |
| 49 | + S = index; |
| 50 | + if (X) { |
| 51 | + let [x0, x1] = Y ? [extent[0][0], extent[1][0]] : extent; |
| 52 | + if (x.bandwidth) x0 -= x.bandwidth(); |
| 53 | + S = S.filter(i => x0 <= X[i] && X[i] <= x1); |
| 54 | + } |
| 55 | + if (Y) { |
| 56 | + let [y0, y1] = X ? [extent[0][1], extent[1][1]] : extent; |
| 57 | + if (y.bandwidth) y0 -= y.bandwidth(); |
| 58 | + S = S.filter(i => y0 <= Y[i] && Y[i] <= y1); |
| 59 | + } |
| 60 | + } |
| 61 | + if (!selectionEquals(this[selection], S)) { |
| 62 | + this[selection] = S; |
| 63 | + this.dispatchEvent(new Event("input", {bubbles: true})); |
| 64 | + } |
| 65 | + })) |
| 66 | + .call(g => g.selectAll(".selection") |
| 67 | + .attr("shape-rendering", null) // reset d3-brush |
| 68 | + .call(applyIndirectStyles, options, dimensions) |
| 69 | + .call(applyDirectStyles, options)) |
| 70 | + .node(); |
| 71 | + g[selection] = null; |
| 72 | + return g; |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +export function brush(data, {x, y, ...options} = {}) { |
| 77 | + ([x, y] = maybeTuple(x, y)); |
| 78 | + return new Brush(data, {...options, x, y}); |
| 79 | +} |
| 80 | + |
| 81 | +export function brushX(data, {x = identity, ...options} = {}) { |
| 82 | + return new Brush(data, {...options, x}); |
| 83 | +} |
| 84 | + |
| 85 | +export function brushY(data, {y = identity, ...options} = {}) { |
| 86 | + return new Brush(data, {...options, y}); |
| 87 | +} |
0 commit comments