/** * Created by Hendrik Strobelt (hendrik.strobelt.com) on 12/3/16. * Modified by Ben Hoover on 4/16/2019 */ import * as d3 from 'd3' import {D3Sel, Util} from "../etc/Util"; import {SimpleEventHandler} from "../etc/SimpleEventHandler"; import {SVG} from "../etc/SVGplus"; /** * Should have VComponentHTML and VComponentSVG * * Common Properties: * - events * - eventHandler (V important) * - options (Maintains public state. Can expose these with get/set functions with auto update) * - _current (Maintains private state) * - cssName (synced with corresponding CSS file) * - parent (HTML is div containing the base, SVG is SVG element) * - base (HTML is div with css_name established) * - _data (Data used to create and render the component) * - _renderData (Data needed to display. This may not be needed, but is currently used in histogram) * * Common Methods: * - constructor * - _render() Consider replacing with `_updateData()` that updates all data at once * - update() Consider replacing this with `data()` that auto updates data * - redraw() * - destroy() */ export abstract class VComponent { // STATIC FIELDS ============================================================ /** * The static property that contains all class related events. * Should be overwritten and event strings have to be unique!! */ static events: {} = {noEvent: 'VComponent_noEvent'}; /** * Defines the layers in SVG for bg,main,fg,... */ // protected abstract readonly layout: { name: string, pos: number[] }[] = [{name: 'main', pos: [0, 0]}]; protected id: string; // Mostly obsolete, nice to have simple ID to assign in CSS protected parent: D3Sel; protected abstract options: { [key: string]: any }; protected base: D3Sel; // Mostly obsolete, represents in svg protected layers: { main?: D3Sel, fg?: D3Sel, bg?: D3Sel, [key: string]: D3Sel }; // Still useful protected eventHandler: SimpleEventHandler; protected _visibility: { hidden: boolean, hideElement?: D3Sel | null; [key: string]: any }; // Enables transitions from visible to invisible. Mostly obsolete. protected _data: DataInterface; protected renderData: any; // Unnecessary protected abstract css_name: string; // Make the same as the corresponding css file protected abstract _current: {}; // Private state information contained in the object itself. // CONSTRUCTOR ============================================================ /** * Simple constructor. Subclasses should call @superInit(options) as well. * see why here: https://stackoverflow.com/questions/43595943/why-are-derived-class-property-values-not-seen-in-the-base-class-constructor * * template: constructor(d3Parent: D3Sel, eventHandler?: SimpleEventHandler, options: {} = {}) { super(d3Parent, eventHandler); // -- access to subclass params: this.superInit(options); } * * @param {D3Sel} d3parent D3 selection of parent SVG DOM Element * @param {SimpleEventHandler} eventHandler a global event handler object or 'null' for local event handler */ protected constructor(d3parent: D3Sel, eventHandler?: SimpleEventHandler) { this.id = Util.simpleUId({}); this.parent = d3parent; // If not further specified - create a local event handler bound to the bas element this.eventHandler = eventHandler || new SimpleEventHandler(this.parent.node()); // Object for storing internal states and variables this._visibility = {hidden: false}; } protected superInitHTML(options: {} = {}) { Object.keys(options).forEach(key => this.options[key] = options[key]); this.base = this.parent.append('div') .classed(this.css_name, true) } /** * Has to be called as last call in subclass constructor. * * @param {{}} options * @param defaultLayers -- create the default layers: bg -> main -> fg */ protected superInitSVG(options: {} = {}, defaultLayers = ['bg', 'main', 'fg']) { // Set default options if not specified in constructor call // const defaults = this.defaultOptions; // this.options = {}; // const keys = new Set([...Object.keys(defaults), ...Object.keys(options)]); // keys.forEach(key => this.options[key] = (key in options) ? options[key] : defaults[key]); Object.keys(options).forEach(key => this.options[key] = options[key]); this.layers = {}; // Create the base group element const svg = this.parent; this.base = SVG.group(svg, this.css_name + ' ID' + this.id, this.options.pos); // create default layers: background, main, foreground if (defaultLayers) { // construction order is important ! defaultLayers.forEach(layer =>{ this.layers[layer] = SVG.group(this.base, layer); }); } } /** * Should be overwritten to create the static DOM elements * @abstract * @return {*} --- */ protected abstract _init(); // DATA UPDATE & RENDER ============================================================ /** * Every time data has changed, update is called and * triggers wrangling and re-rendering * @param {Object} data data object * @return {*} --- */ update(data: DataInterface) { this._data = data; if (this._visibility.hidden) return; this.renderData = this._wrangle(data); this._render(this.renderData); } /** * Data wrangling method -- implement in subclass. Returns this.renderData. * Simplest implementation: `return data;` * @param {Object} data data * @returns {*} -- data in render format * @abstract */ protected abstract _wrangle(data); /** * Is responsible for mapping data to DOM elements * @param {Object} renderData pre-processed (wrangled) data * @abstract * @returns {*} --- */ protected abstract _render(renderData): void; // UPDATE OPTIONS ============================================================ /** * Updates instance options * @param {Object} options only the options that should be updated * @param {Boolean} reRender if option change requires a re-rendering (default:false) * @returns {*} --- */ updateOptions({options, reRender = false}) { Object.keys(options).forEach(k => this.options[k] = options[k]); if (reRender) this._render(this.renderData); } // === CONVENIENCE ==== redraw(){ this._render(this.renderData); } setHideElement(hE: D3Sel) { this._visibility.hideElement = hE; } hideView() { if (!this._visibility.hidden) { const hE = this._visibility.hideElement || this.parent; hE.transition().styles({ 'opacity': 0, 'pointer-events': 'none' }).style('display', 'none'); this._visibility.hidden = true; } } unhideView() { if (this._visibility.hidden) { const hE = this._visibility.hideElement || this.parent; hE.transition().styles({ 'opacity': 1, 'pointer-events': null, 'display': null }); this._visibility.hidden = false; // this.update(this.data); } } destroy() { this.base.remove(); } clear() { this.base.html(''); } }