let __ = undefined; function GetterSetter(get=[], set=[], ...defaults){ // save defaults to closure let state = defaults.map( (d,i) => set[i] ? set[i](d) : d ) return function(...values){ // If empty call, return state after applying getters if(!values.length){ let got = state.map( (d,i) => i { // If setting a value... if(values[i]) set[i] // ...check if there is corresponding setter... ? d = set[i](values[i]) // YES: both val/setter -> assign value returned from setter : d = values[i] // NO: setter, just value: assign value // If not setting a value, state value (d) remains unchanged return d }) return this } } function GetterSetterObj(defaultObj={}){ // save default object to closure let state = defaultObj; let obj = function(addObj={}){ if( Object.keys(addObj).length == 0 && addObj.constructor === Object ){ return state; } for(key in addObj){ if(typeof addObj[key] === "function"){ addObj[key] = addObj[key].bind(this) } } state = Object.assign(state, addObj); return this; } return obj; } let viz = function(vizname){ // Setting closure defaults width = new GetterSetter(__, __, 500); height = new GetterSetter(__, __, 500); viewBox = new GetterSetter(__, __, 500, 500); // Set up the lists to hold objects of the 4 main component types... let mainsList = [[],[],[],[]]; mainsList.forEach(function(currentList){ currentList.get = function(name){ return this.find( d => d.name===name ) } }); [layers, renderers, interactions, markSets] = mainsList; //============================================================== // Functions to ADD NEW components //============================================================== // Function to add layer object (ensuring correct properties exist) function newLayer(name){ let layer = { name: name, tags: new GetterSetter(__,__,[]), filter: new GetterSetter(__,__,null), sort: new GetterSetter(__,__,null), scales: new GetterSetterObj(), mappings: new GetterSetterObj(), statistics: new GetterSetterObj(), onLayer: new GetterSetter(__,__,null), _data: [] } layers.push(layer); return layer; } // Function to add renderer object (ensuring correct properties exist) function newRenderer(name){ let renderer = { name: name, tags: new GetterSetter(__,__,[]), enter: new GetterSetter(__,__,null), exit: new GetterSetter(__,__,null), update: new GetterSetter(__,__,null), markSet: new GetterSetter(__,__,null), init: new GetterSetter(__,__,null), _init: false, render: null } renderer.render = render.bind(renderer) renderers.push(renderer); return renderer; } // Function to add interaction object (ensuring correct properties exist) function newInteraction(name){ let interaction = { name: name, tags: new GetterSetter(__,__,[]), markSet: new GetterSetter(__,__,null), eventType: new GetterSetter(__,__,null), listener: new GetterSetter(__,__,null), } interactions.push(interaction); return interaction; } // Function to add mark set object (ensuring correct properties exist) function newMarkSet(name){ let markSet = { name: name, tags: new GetterSetter(__,__,null), onLayer: new GetterSetter(__,__,null), type: new GetterSetter(__,__,null), key: new GetterSetter(__,__,null), _data: [], updateMarkData: updateMarkData } markSets.push(markSet); return markSet; } //============================================================== // viz init //============================================================== function init(hostSelector, initRenderers){ d3.select(hostSelector).append(`svg`) .classed(vizname, true) .attr(`width`, width()) .attr(`height`, height()) .attr(`viewBox`, `0 0 ${viewBox()[0]} ${viewBox()[1]}`) for(let iRenderer=0, nRenderers=initRenderers.length; iRenderer d.markSet().name===markSet.name) let marks = d3.select(`svg.${vizname}`).selectAll(`${markSet.type()}.${markSet.name}`) .data(markSet._data, markSet.key()) let updateMarks = marks, exitMarks = marks.exit(), enterMarks = marks.enter().append(markSet.type()) .classed(markSet.name, true) this.update() ? this.update()(updateMarks) : ()=>{}; this.exit() ? this.exit()(exitMarks) : ()=>{}; if(!this._init){ this.init() === null ? this.enter() ? this.enter()(enterMarks) : ()=>{} : this.init()(enterMarks) this._init = true; }else{ this.enter() ? this.enter()(enterMarks) : ()=>{}; } markInteractions.map( d => { updateMarks.merge(enterMarks).on(d.eventType(), d.listener()) }) } //============================================================== // update marks //============================================================== function updateMarkData(){ let layer = this.onLayer(), data = [], layerStack=[]; // Push layers onto a stack to be executed in reverse order do{ layerStack.push(layer); layer = layer.onLayer() }while(layer !== null) // Get source data from root layer this._data = layerStack[layerStack.length-1]._data; // Transform source data through each layer to get mark set data do{ layer = layerStack.pop() this._data = processLayer(this._data, layer); }while(layerStack.length > 0) } //============================================================== // process layer //============================================================== function processLayer(dataIn, layer){ dataOut = _.cloneDeep(dataIn); // Filter and sort data according to layer instructions ... dataOut = layer.filter() ? dataOut.filter(layer.filter()) : dataOut; dataOut = layer.sort() ? dataOut.sort(layer.sort()) : dataOut; // Apply mapping if(!_.isEmpty(layer.mappings())){ for(field in layer.mappings()){ dataOut = dataOut.map(function(val, ind, arr){ let callback = layer.mappings()[field].bind(layer) return callback(val, ind, arr); }); } } // Apply summary statistics if(!_.isEmpty(layer.statistics())){ for(field in layer.statistics()){ dataOut = dataOut.reduce(function(stat, val, ind, arr){ let callback = layer.statistics()[field].bind(layer) return callback(stat, val, ind, arr); }); } } return dataOut; } return { width, height, viewBox, layers, renderers, markSets, interactions, newLayer, newRenderer, newMarkSet, newInteraction, init } }