exbert / client /src /ts /vis /VisComponent.ts
bhoov's picture
First commit
63858e7
/**
* 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<DataInterface> {
// 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 <g> 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 <g> 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('');
}
}