UNPKG

d3-visualize

Version:

d3-view components for data visualization

218 lines (193 loc) 6.32 kB
import {pop, inBrowser} from 'd3-let'; import {select} from 'd3-selection'; import createVisual, {visuals} from './base'; import warn from '../utils/warn'; import {getSize} from '../utils/size'; // // Visual // ============= // // A Visual is a a container of visual layers and it is // associated with an HTML element // // Usually a Visual contains one layer only, however it is possible to // have more than one by combining several layers together. Importantly, // layers in one visual generate HTMLElements which are children of the visual // element and inherit both the width and height. // // A visual register itself with the visuals.live array // export default createVisual('visual', { schema: { render: { type: 'string', enum: ['canvas', 'svg'], default: 'svg' }, // width set by the parent element width: { description: 'Width of the visual', '$refs': '#/definitions/size' }, // height set as percentage of width height: { description: 'Height of the visual', '$refs': '#/definitions/size', default: "70%" } }, initialise (element) { if (!element) throw new Error('HTMLElement required by visual group'); if (this.visualParent && this.visualParent.visualType !== 'container') throw new Error('Visual parent can be a container only'); if (!this.select(element).select('.paper').node()) this.select(element).append('div').classed('paper', true); Object.defineProperties(this, { element : { get () { return element; } }, paperElement : { get () { return this.sel.select('.paper'); } }, sel: { get () { return this.select(element); } }, size: { get () { return [this.width, this.height]; } } }); this.sel.classed('d3-visual', true); // list of layers which define the visual this.layers = []; this.drawCount = 0; visuals.live.push(this); element.__visual__ = this; if (this.visualParent) this.visualParent.live.push(this); }, activate () { this.layers.forEach(layer => layer.activate()); }, deactivate () { this.layers.forEach(layer => layer.deactivate()); }, getVisual () { return this; }, // Draw the visual draw (fetchData) { if (this.drawing) { warn(`${this.toString()} already drawing`); return this.drawing; } else if (!this.drawCount) { this.drawCount = 1; this.fit(); } else { this.drawCount++; this.clear(); } var self = this; visuals.events.call('before-draw', undefined, this); return Promise.all(this.layers.map(visual => { return visual.redraw(fetchData); })).then(() => { delete self.drawing; visuals.events.call('after-draw', undefined, self); }, err => { delete self.drawing; warn(`Could not draw ${self.toString()}: ${err}`, err); }); }, clear () {}, // Add a new visual to this group addVisual (options) { var type = pop(options, 'type'); var VisualClass = visuals.types[type]; if (!VisualClass) warn(`Cannot add visual "${type}", not available`); else return new VisualClass(this.element, options, this); }, // // Fit the root element to the size of the parent element fit () { this.resize(null, true); return this; }, // resize the chart resize (size, fit) { if (!size) size = getSize(this.element.parentNode || this.element, this.getModel()); var currentSize = this.size; if (fit || (currentSize[0] !== size.width || currentSize[1] !== size.height)) { if (!fit) this.logDebug('resizing'); this.width = size.width; this.height = size.height; // this.paper.style('width', this.width + 'px').style('height', this.height + 'px'); this.paperElement.style('height', this.height + 'px'); visuals.events.call('resize', undefined, this); // if we are not just fitting draw the visual without fetching data!! if (!fit) this.draw(false); } return this; }, paper () { var paper = this.__paper, render = this.getModel().render; if (paper && paper.paperType === render) return paper; var PaperType = visuals.papers[render]; if (!PaperType) throw new Error(`Unknown paper ${render}`); paper = new PaperType(this); this.__paper = paper; return paper; }, getPaperGroup (gname) { return this.paper().group(gname); }, destroy () { this.pop(this.visualParent); this.pop(visuals); } }); if (inBrowser) { // DOM observer // Check for changes in the DOM that leads to visual actions const observer = new MutationObserver(visualManager); observer.observe(document.documentElement, { childList: true, subtree: true }); } // // Clears visualisation going out of scope function visualManager (records) { let nodes, node; records.forEach(record => { nodes = record.removedNodes; if (!nodes || !nodes.length) return; for (let i=0; i<nodes.length; ++i) { node = nodes[i]; if (node.querySelectorAll) { if (!node.__visual__) select(node).selectAll('.d3-visual').each(destroy); else destroy.call(node); } } }); } function destroy () { var viz = this.__visual__; if (viz) { viz.destroy(); viz.logDebug(`removed from DOM, ${visuals.live.length} live visuals left`); } else warn('d3-visual without __visual__ object'); }