UNPKG

monte

Version:

A visualization framework for D3.js and SVG. Ships with prebuilt charts and components.

292 lines (245 loc) 9 kB
import { ANY, HORIZONTAL, NONE, VERTICAL } from '../const/direction'; import { isDefined, isFunc } from '../tools/is'; import { BRUSH_HANDLE_SIZE } from '../const/d3'; import { Extension } from './Extension'; import { MonteOptionError } from '../support/MonteOptionError'; import { noop } from '../tools'; const SELECTION_CHANGED = 'selectionChanged'; const SELECTION_MOVE = 'selectionMove'; const SELECTION_START ='selectionStart'; const SELECTION_END = 'selectionEnd'; const SELECTION_EVENTS = [ SELECTION_CHANGED, SELECTION_MOVE, SELECTION_START, SELECTION_END ]; const SELECTION_RECT_DEFAULTS = { css: '', layer: 'selection', eventPrefix: 'selectionRect', selectionDirection: null, handleSize: BRUSH_HANDLE_SIZE, extent: function(w, h, selectionDirection) { // eslint-disable-line no-unused-vars return [[0, 0], [w, h]]; }, initialSelection: function(w, h, selectionDirection) { if (this.isXYBrush(selectionDirection)) { return [[0, 0], [0, 0]]; // Select none and allow user to make a custom selection. } else if (this.isXBrush(selectionDirection)) { return [0, w]; // Select entire region, because no small generalization can be made. } else if (this.isYBrush(selectionDirection)) { return [0, h]; // Select entire region, because no small generalization can be made. } }, filter: null, customHandlePath: null, }; export class SelectionRect extends Extension { _initOptions(...options) { super._initOptions(...options, SELECTION_RECT_DEFAULTS); } _initPublicEvents(...events) { events.push(...SELECTION_EVENTS); super._initPublicEvents(events); } _brush(selectionDirection) { let brush; let css; if (this.isXYBrush(selectionDirection)) { brush = d3.brush(); css = 'select-dir-all'; } else if (this.isXBrush(selectionDirection)) { brush = d3.brushX(); css = 'select-dir-x'; } else if (this.isYBrush(selectionDirection)) { brush = d3.brushY(); css = 'select-dir-y'; } else { let msg = `The "selectionDirection" option must be a direction (${HORIZONTAL}, ${VERTICAL}, or ${NONE}), axis (x or y), or an unset value (null or undefined).`; throw new MonteOptionError(msg); } return { brush, css }; } _handles(selectionDirection) { if (this.isXYBrush(selectionDirection)) { return [{ type: 'n' }, { type: 'e' }, { type: 's' }, { type: 'w' }]; } else if (this.isXBrush(selectionDirection)) { return [{ type: 'e' }, { type: 'w' }]; } else if (this.isYBrush(selectionDirection)) { return [{ type: 'n' }, { type: 's' }]; } } _render() { const selectionDirection = this.tryInvoke(this.opts.selectionDirection); const handleSize = this.tryInvoke(this.opts.handleSize); const css = this.tryInvoke(this.opts.css); const w = this.chart.width; const h = this.chart.height; const extent = this.tryInvoke(this.opts.extent, w, h, selectionDirection); const initialSelection = this.tryInvoke(this.opts.initialSelection, w, h, selectionDirection); const brushDetails = this._brush(selectionDirection); this.selectionDirection = selectionDirection; this.extent = extent; this.brush = brushDetails.brush.extent(extent); this.brush.handleSize(handleSize) .on('start', this._brushMove.bind(this, SELECTION_START)) .on('brush', this._brushMove.bind(this, SELECTION_MOVE)) .on('end', this._brushMove.bind(this, SELECTION_END)); // Setup brush / selection layer. this.brushSel = this.layer .classed('monte-ext-selection-rect', true) .classed(brushDetails.css, true) .classed(css, true) .call(this.brush); // Associate filter function if defined. if (isDefined(this.opts.filter) && isFunc(this.opts.filter)) { this.brush.filter(this.opts.filter); } // Draw custom handles if (isDefined(this.opts.customHandlePath) && this.opts.customHandlePath !== noop) { this._renderCustomHandles(); this.brushSel.classed('custom-handles', true); } else { this.brushSel.classed('standard-handles', true); } // Break apart chain to ensure `brushSel` is assigned to before move and event listeners are // invoked. this.brush.move(this.brushSel, initialSelection); } _renderCustomHandles() { this.brushSel.selectAll('.handle--custom') .data(this._handles(this.selectionDirection)) .enter().append('path') .attr('display', 'none') .attr('class', (d) => `handle--custom ${d.type}`) .attr('d', (d, i, nodes) => this.tryInvoke(this.opts.customHandlePath, d, i, nodes)); this.usingCustomHandles = true; } _boundsUpdate() { const w = this.chart.width; const h = this.chart.height; const extent = this.tryInvoke(this.opts.extent, w, h, this.selectionDirection); this.extent = extent; this.brush.extent(extent); this.brushSel.call(this.brush); // Update proper scale of rect if the chart is resized. this.brush.move(this.brushSel, this.unscaleSelection(this._selection.scaled)); } _update() { } _brushMove(ev) { const s = this.selection(); this._selection = s; if (this.usingCustomHandles) { const handle = this.brushSel.selectAll('.handle--custom'); if (s.raw == null) { handle.attr('display', 'none'); } else { handle.attr('display', null) .attr('transform', (d) => { const x = this._handleX(d.type, s.raw); const y = this._handleY(d.type, s.raw); return 'translate(' + x + ',' + y + ')'; }); } } this.emit(ev, s); this.emit(SELECTION_CHANGED, s); } // NOTE: The scaled y0 and y1 are flipped because height and origin are flipped by convention // in the axes charts. scaleSelection(rawSelection) { const selectionDirection = this.selectionDirection; const s = rawSelection; if (!s) { return s; } if (this.isXYBrush(selectionDirection)) { // [[x0, y0], [x1, y1]] return [ [this.chart.x.invert(s[0][0]), this.chart.y.invert(s[1][1])], [this.chart.x.invert(s[1][0]), this.chart.y.invert(s[0][1])], ]; } else if (this.isXBrush(selectionDirection)) { // [x0, x1] return [this.chart.x.invert(s[0]), this.chart.x.invert(s[1])]; } else if (this.isYBrush(selectionDirection)) { // [y0, y1] return [this.chart.y.invert(s[1]), this.chart.y.invert(s[0])]; } } unscaleSelection(scaledSelection) { const selectionDirection = this.selectionDirection; const s = scaledSelection; if (!s) { return s; } if (this.isXYBrush(selectionDirection)) { // [[x0, y0], [x1, y1]] return [ [this.chart.x(s[0][0]), this.chart.y(s[1][1])], [this.chart.x(s[1][0]), this.chart.y(s[0][1])], ]; } else if (this.isXBrush(selectionDirection)) { // [x0, x1] return [this.chart.x(s[0]), this.chart.x(s[1])]; } else if (this.isYBrush(selectionDirection)) { // [y0, y1] return [this.chart.y(s[1]), this.chart.y(s[0])]; } } selection() { const raw = d3.brushSelection(this.brushSel.node()); const scaled = this.scaleSelection(raw); return { raw, scaled }; } clearSelection() { this.brushSel.call(this.brush.move, null); } destroy() { } isXYBrush(selectionDirection) { return !isDefined(selectionDirection) || selectionDirection === NONE || selectionDirection === ANY; } isXBrush(selectionDirection) { selectionDirection = selectionDirection || ''; return selectionDirection.toLowerCase() === 'x' || selectionDirection === HORIZONTAL; } isYBrush(selectionDirection) { selectionDirection = selectionDirection || ''; return selectionDirection.toLowerCase() === 'y' || selectionDirection === VERTICAL; } _handleX(type, rawSelection) { const dir = this.selectionDirection; const s = rawSelection; const isX = this.isXBrush(dir); const isY = this.isYBrush(dir); if (!s) { return 0; } switch (type) { case 'n': return isY ? mid(this.extent[0][0], this.extent[1][0]) : mid(s[0][0], s[1][0]); case 's': return isY ? mid(this.extent[0][0], this.extent[1][0]) : mid(s[0][0], s[1][0]); case 'e': return isX ? s[1] : s[1][0]; case 'w': return isX ? s[0] : s[0][0]; } } _handleY(type, rawSelection) { const dir = this.selectionDirection; const s = rawSelection; const isX = this.isXBrush(dir); const isY = this.isYBrush(dir); if (!s) { return 0; } switch (type) { case 'n': return isY ? s[0] : s[0][1]; case 's': return isY ? s[1] : s[1][1]; case 'w': return isX ? mid(this.extent[0][1], this.extent[1][1]) : mid(s[0][1], s[1][1]); case 'e': return isX ? mid(this.extent[0][1], this.extent[1][1]) : mid(s[0][1], s[1][1]); } } } function mid(a, b) { return (a + b) / 2; }