From 0fdc66e21aef8d78b9fcdb3d7953ef1c3a54f249 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 10:11:20 +0100 Subject: [PATCH 1/6] Track gapminder attributes #10 --- data/gapminder/gapminder.vg.json | 12 ++- src/internal/VegaView.ts | 149 ++++++++----------------------- src/internal/cmds.ts | 7 +- 3 files changed, 51 insertions(+), 117 deletions(-) diff --git a/data/gapminder/gapminder.vg.json b/data/gapminder/gapminder.vg.json index 0d5d84b..7a6ad2c 100644 --- a/data/gapminder/gapminder.vg.json +++ b/data/gapminder/gapminder.vg.json @@ -98,22 +98,26 @@ { "name": "xField", "value": "fertility", - "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]} + "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, + "track": true }, { "name": "yField", "value": "life_expect", - "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]} + "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, + "track": true }, { "name": "sizeField", "value": "population", - "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]} + "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, + "track": true }, { "name": "colorField", "value": "continent", - "bind": {"input": "select", "options": ["continent", "main_religion"]} + "bind": {"input": "select", "options": ["continent", "main_religion"]}, + "track": true } ], diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index 7620819..46878f2 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -3,9 +3,16 @@ import {IView} from '../AppWrapper'; import {cat, IObjectRef, ref} from 'phovea_core/src/provenance'; import * as d3 from 'd3'; import * as vega from 'vega-lib'; -import {Spec, Signal, View} from 'vega-lib'; +import {Spec, View, NewSignal, BindCheckbox, BindRadioSelect, BindRange} from 'vega-lib'; import {setState} from './cmds'; +/** + * Extend the Vega Signal specification about tracking + */ +interface ClueSignal extends NewSignal { + track: boolean; +} + interface IVegaViewOptions { /** * Set the renderer for Vega (svg or canvas) @@ -19,51 +26,37 @@ export class VegaView implements IView { vegaRenderer: 'svg', }; - /** - * RegExp to activate the signal when the string contains `mouseup`, `touchend`, or `click`. - * @type {RegExp} - */ - private readonly activateSignal: RegExp = /(mouseup|touchend|click)/g; - - /** - * List of signals that are used by this CLUE connector - * @type {Signal[]} - */ - private readonly clueSignals: Signal[] = [ - { - 'name': 'CLUE_captureState', - 'value': null, - 'on': [ - {'events': 'mousedown, touchstart', 'update': 'null', 'force': true} - ] - } - ]; - private readonly $node: d3.Selection; - private readonly activeSignals: Map = new Map(); - readonly ref: IObjectRef; private currentState: any = null; private signalHandler = (name, value) => { - // ignore signals that are not listed or disabled - if (!this.activeSignals.has(name) || !this.activeSignals.get(name)) { - return; - } - // cast to , because `getState()` is missing in 'vega-typings' const vegaView = (this.$node.datum()); - console.log(name, value, vegaView.getState()); + const signalSpec: ClueSignal = this.spec.signals.find((d) => d.name === name)!; + console.log(name, value, vegaView.getState(), signalSpec); + + let actionName = ``; + + if(signalSpec.bind) { + switch ((signalSpec.bind).input) { + case 'select': + case 'radio': + actionName = `${name} = ${value}`; + break; + case 'range': + console.log('changed range binding'); + break; + } + } // capture vega state and add to history - if (name === this.clueSignals[0].name) { - const bak = this.currentState; - this.currentState = vegaView.getState(); - this.graph.pushWithResult(setState(this.ref, vegaView.getState()), { - inverse: setState(this.ref, bak) - }); - } + const bak = this.currentState; + this.currentState = vegaView.getState(); + this.graph.pushWithResult(setState(this.ref, `${name} = ${value}`, vegaView.getState()), { + inverse: setState(this.ref, `${name} = ${value}`, bak) + }); } constructor(parent: HTMLElement, private readonly graph: ProvenanceGraph, private spec: Spec) { @@ -73,24 +66,11 @@ export class VegaView implements IView { .append('div') .classed('vega-view', true) .html(` -
`); } init(): Promise { - this.spec.signals = this.initClueSignals(this.spec.signals); - - // set default values for signals -- default: true - //this.spec.signals.forEach((d) => this.activeSignals.set(d.name, this.shouldSignalBeActive(d))); - - //this.initSelector('.signal-selector', this.spec.signals, this.activeSignals); - const vegaView: View = new View(vega.parse(this.spec)) //.logLevel(vega.Warn) // set view logging level .renderer(this.options.vegaRenderer) // set renderer (canvas or svg) @@ -106,51 +86,9 @@ export class VegaView implements IView { vegaView.run(); // run after defining the promise this.$node.datum(vegaView); - /*this.$node.select('.btn-undo') - .on('click', () => { - this.graph.undo(); - }); - - this.$node.select('.btn-redo') - .on('click', () => { - // NOT possible - const next = this.graph.act.nextState; - if (next) { - this.graph.jumpTo(next); - } - });*/ - return vegaViewReady.then(() => this); } - private initClueSignals(signals: Signal[]): Signal[] { - if (!signals) { - signals = []; - } - // activate all CLUE signals by default - this.clueSignals.forEach((d) => this.activeSignals.set(d.name, true)); - return [...this.clueSignals, ...signals]; - } - - private initSelector(selector: string, data: any[], isActiveMap: Map) { - const $signals = this.$node.select(selector) - .selectAll('.checkbox').data(data); - - $signals.enter() - .append('div') - .classed('checkbox', true) - .html(``); - - $signals.select('span').text((d) => d.name); - $signals.select('input') - .attr('checked', (d) => (isActiveMap.get(d.name)) ? 'checked' : null) - .on('change', (d) => { - isActiveMap.set(d.name, !isActiveMap.get(d.name)); - }); - - $signals.exit().remove(); - } - setStateImpl(state: any) { const vegaView = this.$node.datum(); const bak = this.currentState; @@ -168,30 +106,21 @@ export class VegaView implements IView { private addSignalListener(vegaView: View) { if (this.spec.signals) { - this.spec.signals.forEach((signal) => { - vegaView.addSignalListener(signal.name, this.signalHandler); - }); + this.spec.signals + .filter((signal: ClueSignal) => signal.track) + .forEach((signal) => { + vegaView.addSignalListener(signal.name, this.signalHandler); + }); } } private removeSignalListener(vegaView: View) { if (this.spec.signals) { - this.spec.signals.forEach((signal) => { - vegaView.removeSignalListener(signal.name, this.signalHandler); - }); - } - } - - /** - * Check all events of the signal. - * If the event contains a `mouseup`, `touchend`, or `click` then activate the signal. - * Otherwise deactivate the signal. - * @param {Signal} signal - */ - private shouldSignalBeActive(signal: Signal): boolean { - if (!signal.on) { - return false; + this.spec.signals + .filter((signal: ClueSignal) => signal.track) + .forEach((signal) => { + vegaView.removeSignalListener(signal.name, this.signalHandler); + }); } - return signal.on.some((d) => this.activateSignal.test(d.events.toString())); } } diff --git a/src/internal/cmds.ts b/src/internal/cmds.ts index 0394d05..56bc399 100644 --- a/src/internal/cmds.ts +++ b/src/internal/cmds.ts @@ -8,12 +8,13 @@ export async function setStateImpl(inputs: IObjectRef[], parameter: any) { const view = await inputs[0].v; const old = view.setStateImpl(parameter.state); return { - inverse: setState(inputs[0], old) + inverse: setState(inputs[0], parameter.name, old) }; } -export function setState(view: IObjectRef, state: any) { - return action(meta('Change State', cat.visual, op.update), CMD_SET_STATE, setStateImpl, [view], { +export function setState(view: IObjectRef, name: string, state: any) { + return action(meta(name, cat.visual, op.update), CMD_SET_STATE, setStateImpl, [view], { + name, state }); } From bf58448ef3fd8a3301d3376280397392cd9fa88a Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 12:43:34 +0100 Subject: [PATCH 2/6] Prevent signal handler execution on playing provenance graph --- src/internal/VegaView.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index 46878f2..20f79bd 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -31,7 +31,13 @@ export class VegaView implements IView { readonly ref: IObjectRef; private currentState: any = null; + private blockSignalHandler: boolean = false; + private signalHandler = (name, value) => { + if(this.blockSignalHandler) { + return; + } + // cast to , because `getState()` is missing in 'vega-typings' const vegaView = (this.$node.datum()); const signalSpec: ClueSignal = this.spec.signals.find((d) => d.name === name)!; @@ -93,7 +99,12 @@ export class VegaView implements IView { const vegaView = this.$node.datum(); const bak = this.currentState; this.currentState = state; + + // prevent adding the provenance graph node twice + this.blockSignalHandler = true; vegaView.setState(state); + this.blockSignalHandler = false; + return bak; } From 1a95b2449923a07073e0a722f0cadd92de145916 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 12:44:23 +0100 Subject: [PATCH 3/6] Track currentYear (range signal) #10 --- data/gapminder/gapminder.vg.json | 7 +-- src/internal/VegaView.ts | 74 +++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/data/gapminder/gapminder.vg.json b/data/gapminder/gapminder.vg.json index 7a6ad2c..08eff89 100644 --- a/data/gapminder/gapminder.vg.json +++ b/data/gapminder/gapminder.vg.json @@ -89,11 +89,8 @@ { "name": "currentYear", "value": 1980, - "on":[{ - "events": "mousemove, touchmove", - "update": "currentYear" - }], - "bind": {"input": "range", "min": 1800, "max": 2015, "step": 1} + "bind": {"input": "range", "min": 1800, "max": 2015, "step": 1}, + "track": true }, { "name": "xField", diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index 20f79bd..5c2991f 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -20,6 +20,12 @@ interface IVegaViewOptions { vegaRenderer: 'canvas' | 'svg' | 'none'; } +interface IRangeDOMListener { + dragging: boolean; + elem: d3.Selection; + listener: Map void>; +} + export class VegaView implements IView { private readonly options: IVegaViewOptions = { @@ -31,6 +37,8 @@ export class VegaView implements IView { readonly ref: IObjectRef; private currentState: any = null; + private rangeDOMListener: IRangeDOMListener[] = []; + private blockSignalHandler: boolean = false; private signalHandler = (name, value) => { @@ -43,20 +51,6 @@ export class VegaView implements IView { const signalSpec: ClueSignal = this.spec.signals.find((d) => d.name === name)!; console.log(name, value, vegaView.getState(), signalSpec); - let actionName = ``; - - if(signalSpec.bind) { - switch ((signalSpec.bind).input) { - case 'select': - case 'radio': - actionName = `${name} = ${value}`; - break; - case 'range': - console.log('changed range binding'); - break; - } - } - // capture vega state and add to history const bak = this.currentState; this.currentState = vegaView.getState(); @@ -119,8 +113,13 @@ export class VegaView implements IView { if (this.spec.signals) { this.spec.signals .filter((signal: ClueSignal) => signal.track) - .forEach((signal) => { - vegaView.addSignalListener(signal.name, this.signalHandler); + .forEach((signal: ClueSignal) => { + // check for range input + if(signal.bind && (signal.bind).input === 'range') { + this.addRangeDOMListener(signal.name, (signal.bind).input); + } else { + vegaView.addSignalListener(signal.name, this.signalHandler); + } }); } } @@ -133,5 +132,48 @@ export class VegaView implements IView { vegaView.removeSignalListener(signal.name, this.signalHandler); }); } + // remove all DOM listener at once + this.removeRangeDOMListener(); + } + + private addRangeDOMListener(signalName, inputType) { + const domListener: IRangeDOMListener = { + dragging: false, + elem: this.$node.select(`input[type="${inputType}"][name="${signalName}"]`), + listener: new Map() + }; + + const startListener = () => { domListener.dragging = true; }; + const endListener = () => { + if(!domListener.dragging) { + return; + } + this.signalHandler(signalName, domListener.elem.property('value')); + domListener.dragging = false; + }; + + const listener: [string, () => void][] = [ + ['mousedown', startListener], + ['mouseup', endListener], + ['touchstart', startListener], + ['touchend', endListener] + ]; + + listener.forEach((d) => { + domListener.listener.set(d[0], d[1]); + domListener.elem.on(d[0], d[1]); + }); + + this.rangeDOMListener = [...this.rangeDOMListener, domListener]; + } + + private removeRangeDOMListener() { + this.rangeDOMListener.forEach((d) => { + const listener = Array.from(d.listener.entries()); + listener.forEach((e) => { + d.elem.on(e[0], null); + }); + }); + } } From f96b27a0020106dcb37958def1169da12651dfed Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 16:58:48 +0100 Subject: [PATCH 4/6] Track selected countries #10 --- data/gapminder/gapminder.vg.json | 69 ++++++++++++++++++++++++++------ src/internal/VegaView.ts | 55 ++++++++++++++++++------- src/internal/spec.ts | 58 +++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/internal/spec.ts diff --git a/data/gapminder/gapminder.vg.json b/data/gapminder/gapminder.vg.json index 08eff89..c0c3479 100644 --- a/data/gapminder/gapminder.vg.json +++ b/data/gapminder/gapminder.vg.json @@ -70,51 +70,94 @@ ] }, { - "name": "trackCountries", + "name": "selectedCountries", "on": [ - {"trigger": "active", "toggle": "{country: active.country}"} + {"trigger": "clear", "remove": true}, + {"trigger": "!shift", "remove": true}, + {"trigger": "!shift && clicked", "insert": "clicked"}, + {"trigger": "shift && clicked", "toggle": "clicked"} ] } ], "signals": [ { - "name": "active", - "value": {}, + "name": "selectedCountries", "on": [ - {"events": "@point:mousedown, @point:touchstart", "update": "datum"}, - {"events": "window:mouseup, window:touchend", "update": "{}"} + {"events": {"signal": "clicked"}, "update": "null", "force": true} + ], + "track": { + "async": [ + {"data": "selectedCountries", "as": "allCountries"}, + {"signal": "clicked", "as": "lastCountry"} + ], + "title": "Selected {{lastCountry.country}} ({{allCountries.length}} Countries)" + } + }, + { + "name": "clear", + "value": true, + "on": [ + {"events": "mouseup[!event.item]", "update": "true", "force": true} + ], + "track": { + "async": [], + "title": "No Countries Selected" + } + }, + { + "name": "shift", + "value": false, + "on": [ + {"events": "@point:click", "update": "event.shiftKey", "force": true} + ] + }, + { + "name": "clicked", + "value": null, + "on": [ + {"events": "@point:click", "update": "datum", "force": true} ] }, { "name": "currentYear", "value": 1980, "bind": {"input": "range", "min": 1800, "max": 2015, "step": 1}, - "track": true + "track": { + "title": "Selected Year {{value}}" + } }, { "name": "xField", "value": "fertility", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, - "track": true + "track": { + "title": "X = {{value}}" + } }, { "name": "yField", "value": "life_expect", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, - "track": true + "track": { + "title": "Y = {{value}}" + } }, { "name": "sizeField", "value": "population", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, - "track": true + "track": { + "title": "Size = {{value}}" + } }, { "name": "colorField", "value": "continent", "bind": {"input": "select", "options": ["continent", "main_religion"]}, - "track": true + "track": { + "title": "Color = {{value}}" + } } ], @@ -205,7 +248,7 @@ "size": {"scale": "size", "field": "size"}, "fillOpacity": [ { - "test": "indata('trackCountries', 'country', datum.country)", + "test": "indata('selectedCountries', 'country', datum.country)", "value": 1 }, {"value": 0.5} @@ -237,7 +280,7 @@ "y": {"scale": "y", "field": "y", "offset": -7}, "fillOpacity": [ { - "test": "indata('trackCountries', 'country', datum.country)", + "test": "indata('selectedCountries', 'country', datum.country)", "value": 0.8 }, {"value": 0} diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index 5c2991f..a08a12d 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -1,17 +1,14 @@ import ProvenanceGraph from 'phovea_core/src/provenance/ProvenanceGraph'; import {IView} from '../AppWrapper'; import {cat, IObjectRef, ref} from 'phovea_core/src/provenance'; +// best solution to import Handlebars (@see https://github.com/wycats/handlebars.js/issues/1174) +import * as handlebars from 'handlebars/dist/handlebars'; import * as d3 from 'd3'; import * as vega from 'vega-lib'; -import {Spec, View, NewSignal, BindCheckbox, BindRadioSelect, BindRange} from 'vega-lib'; +import {Spec, View, BindRange} from 'vega-lib'; import {setState} from './cmds'; +import {ClueSignal, IAsyncData, IAsyncSignal} from './spec'; -/** - * Extend the Vega Signal specification about tracking - */ -interface ClueSignal extends NewSignal { - track: boolean; -} interface IVegaViewOptions { /** @@ -49,14 +46,35 @@ export class VegaView implements IView { // cast to , because `getState()` is missing in 'vega-typings' const vegaView = (this.$node.datum()); const signalSpec: ClueSignal = this.spec.signals.find((d) => d.name === name)!; - console.log(name, value, vegaView.getState(), signalSpec); + const context = {name, value}; + + if(signalSpec.track.async) { + vegaView.runAsync().then((view) => { + const async = signalSpec.track.async; + + async.filter((d: IAsyncSignal) => d.signal) + .forEach((d: IAsyncSignal) => { + const key = (d.as) ? d.as : d.signal; + context[key] = view.signal(d.signal); + }); + + async.filter((d: IAsyncData) => d.data) + .forEach((d: IAsyncData) => { + const key = (d.as) ? d.as : d.data; + context[key] = view.data(d.data); + }); + + const template = handlebars.compile(signalSpec.track.title); + const title = template(context); + this.pushNewGraphNode(title, vegaView.getState()); + }); - // capture vega state and add to history - const bak = this.currentState; - this.currentState = vegaView.getState(); - this.graph.pushWithResult(setState(this.ref, `${name} = ${value}`, vegaView.getState()), { - inverse: setState(this.ref, `${name} = ${value}`, bak) - }); + } else { + const rawTitle = (signalSpec.track.title) ? signalSpec.track.title : `{{name}} = {{value}}`; + const template = handlebars.compile(rawTitle); + const title = template(context); + this.pushNewGraphNode(title, vegaView.getState()); + } } constructor(parent: HTMLElement, private readonly graph: ProvenanceGraph, private spec: Spec) { @@ -102,6 +120,15 @@ export class VegaView implements IView { return bak; } + private pushNewGraphNode(title: string, state: any) { + // capture vega state and add to history + const bak = this.currentState; + this.currentState = state; + this.graph.pushWithResult(setState(this.ref, title, state), { + inverse: setState(this.ref, title, bak) + }); + } + remove() { const vegaView = this.$node.datum(); this.removeSignalListener(vegaView); diff --git a/src/internal/spec.ts b/src/internal/spec.ts new file mode 100644 index 0000000..cc391bd --- /dev/null +++ b/src/internal/spec.ts @@ -0,0 +1,58 @@ +import {NewSignal} from 'vega-lib'; + +/** + * Extend the Vega Signal specification about tracking + */ +export interface ClueSignal extends NewSignal { + track: ITrackedSignal; +} + +/** + * Extend the Vega Signal specification about tracking + */ +export interface ITrackedSignal { + + /** + * Title of the graph node + * It can contain Handlebar.js syntax to replace variables. + * @see http://handlebarsjs.com/ + * + * Available default variables: + * * `{{name}}`: signal name + * * `{{value}}`: signal value + * + * Further variables can be added by using the `async` option. + */ + title: string; + + /** + * If set wait for completing dataflow evaluation + */ + async?: TrackedSignalAsync[]; + +} + +interface IBaseAsync { + /** + * If set use this alias name for replacement in the title + */ + as?: string; +} + +export interface IAsyncData extends IBaseAsync { + /** + * A valid dataset name + */ + data: string; +} + +export interface IAsyncSignal extends IBaseAsync { + /** + * A valid signal name + */ + signal: string; +} + +type TrackedSignalAsync = IAsyncData | IAsyncSignal; + + From cd1474ce96b9e2b3c2e523ff572247460b07fe46 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 17:38:26 +0100 Subject: [PATCH 5/6] Make `category` and `operation` customizable #10 --- data/gapminder/gapminder.vg.json | 24 ++++++++++++++++++------ src/internal/VegaView.ts | 24 ++++++++++++++++-------- src/internal/cmds.ts | 24 ++++++++++++++++++++++-- src/internal/spec.ts | 14 ++++++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/data/gapminder/gapminder.vg.json b/data/gapminder/gapminder.vg.json index c0c3479..90be15b 100644 --- a/data/gapminder/gapminder.vg.json +++ b/data/gapminder/gapminder.vg.json @@ -91,7 +91,9 @@ {"data": "selectedCountries", "as": "allCountries"}, {"signal": "clicked", "as": "lastCountry"} ], - "title": "Selected {{lastCountry.country}} ({{allCountries.length}} Countries)" + "title": "Selected {{lastCountry.country}} ({{allCountries.length}} Countries)", + "category": "selection", + "operation": "update" } }, { @@ -102,7 +104,9 @@ ], "track": { "async": [], - "title": "No Countries Selected" + "title": "No Countries Selected", + "category": "selection", + "operation": "update" } }, { @@ -132,7 +136,9 @@ "value": "fertility", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, "track": { - "title": "X = {{value}}" + "title": "X = {{value}}", + "category": "data", + "operation": "update" } }, { @@ -140,7 +146,9 @@ "value": "life_expect", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, "track": { - "title": "Y = {{value}}" + "title": "Y = {{value}}", + "category": "data", + "operation": "update" } }, { @@ -148,7 +156,9 @@ "value": "population", "bind": {"input": "select", "options": ["gdp", "child_mortality", "fertility", "life_expect", "population"]}, "track": { - "title": "Size = {{value}}" + "title": "Size = {{value}}", + "category": "data", + "operation": "update" } }, { @@ -156,7 +166,9 @@ "value": "continent", "bind": {"input": "select", "options": ["continent", "main_religion"]}, "track": { - "title": "Color = {{value}}" + "title": "Color = {{value}}", + "category": "data", + "operation": "update" } } ], diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index a08a12d..fff46f1 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -6,7 +6,7 @@ import * as handlebars from 'handlebars/dist/handlebars'; import * as d3 from 'd3'; import * as vega from 'vega-lib'; import {Spec, View, BindRange} from 'vega-lib'; -import {setState} from './cmds'; +import {ISetStateMetadata, setState} from './cmds'; import {ClueSignal, IAsyncData, IAsyncSignal} from './spec'; @@ -65,15 +65,23 @@ export class VegaView implements IView { }); const template = handlebars.compile(signalSpec.track.title); - const title = template(context); - this.pushNewGraphNode(title, vegaView.getState()); + const metadata: ISetStateMetadata = { + name: template(context), + category: signalSpec.track.category || 'data', + operation: signalSpec.track.operation || 'update' + }; + this.pushNewGraphNode(metadata, vegaView.getState()); }); } else { const rawTitle = (signalSpec.track.title) ? signalSpec.track.title : `{{name}} = {{value}}`; const template = handlebars.compile(rawTitle); - const title = template(context); - this.pushNewGraphNode(title, vegaView.getState()); + const metadata: ISetStateMetadata = { + name: template(context), + category: signalSpec.track.category || 'data', + operation: signalSpec.track.operation || 'update' + }; + this.pushNewGraphNode(metadata, vegaView.getState()); } } @@ -120,12 +128,12 @@ export class VegaView implements IView { return bak; } - private pushNewGraphNode(title: string, state: any) { + private pushNewGraphNode(metadata: ISetStateMetadata, state: any) { // capture vega state and add to history const bak = this.currentState; this.currentState = state; - this.graph.pushWithResult(setState(this.ref, title, state), { - inverse: setState(this.ref, title, bak) + this.graph.pushWithResult(setState(this.ref, metadata, state), { + inverse: setState(this.ref, metadata, bak) }); } diff --git a/src/internal/cmds.ts b/src/internal/cmds.ts index 56bc399..71b2fe7 100644 --- a/src/internal/cmds.ts +++ b/src/internal/cmds.ts @@ -2,6 +2,26 @@ import {cat, IObjectRef, meta, action, op, ActionNode} from 'phovea_core/src/pro import {VegaView} from './VegaView'; import {lastOnly} from 'phovea_clue/src/compress'; + +export interface ISetStateMetadata { + /** + * Title of the graph node + */ + name: string, + + /** + * Category of this signal + * @see 'phovea_core/src/provenance/ObjectNode.ts' + */ + category: 'data' | 'selection' | 'visual' | 'layout' | 'logic' | 'custom' | 'annotation'; + + /** + * Operations of this signal + * @see 'phovea_core/src/provenance/ObjectNode.ts' + */ + operation: 'create' | 'update' | 'remove'; +} + export const CMD_SET_STATE = 'vegaSetState'; export async function setStateImpl(inputs: IObjectRef[], parameter: any) { @@ -12,8 +32,8 @@ export async function setStateImpl(inputs: IObjectRef[], parameter: any) { }; } -export function setState(view: IObjectRef, name: string, state: any) { - return action(meta(name, cat.visual, op.update), CMD_SET_STATE, setStateImpl, [view], { +export function setState(view: IObjectRef, metadata: ISetStateMetadata, state: any) { + return action(meta(metadata.name, metadata.category, metadata.operation), CMD_SET_STATE, setStateImpl, [view], { name, state }); diff --git a/src/internal/spec.ts b/src/internal/spec.ts index cc391bd..b21d6c1 100644 --- a/src/internal/spec.ts +++ b/src/internal/spec.ts @@ -30,6 +30,20 @@ export interface ITrackedSignal { */ async?: TrackedSignalAsync[]; + /** + * Category of this signal + * + * Default value: `data` + */ + category?: 'data' | 'selection' | 'visual' | 'layout' | 'logic' | 'custom' | 'annotation'; + + /** + * Operations of this signal + * + * Default value: `update` + */ + operation?: 'create' | 'update' | 'remove'; + } interface IBaseAsync { From 43ca7bec2bd9f65cf96a370e71c0e00d1ec0ecc5 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Fri, 23 Mar 2018 17:40:56 +0100 Subject: [PATCH 6/6] Refactor code #10 --- src/internal/VegaView.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/internal/VegaView.ts b/src/internal/VegaView.ts index fff46f1..e535701 100644 --- a/src/internal/VegaView.ts +++ b/src/internal/VegaView.ts @@ -48,6 +48,16 @@ export class VegaView implements IView { const signalSpec: ClueSignal = this.spec.signals.find((d) => d.name === name)!; const context = {name, value}; + function createMetadata(): ISetStateMetadata { + const rawTitle = (signalSpec.track.title) ? signalSpec.track.title : `{{name}} = {{value}}`; + const template = handlebars.compile(rawTitle); + return { + name: template(context), + category: signalSpec.track.category || 'data', + operation: signalSpec.track.operation || 'update' + }; + } + if(signalSpec.track.async) { vegaView.runAsync().then((view) => { const async = signalSpec.track.async; @@ -64,24 +74,11 @@ export class VegaView implements IView { context[key] = view.data(d.data); }); - const template = handlebars.compile(signalSpec.track.title); - const metadata: ISetStateMetadata = { - name: template(context), - category: signalSpec.track.category || 'data', - operation: signalSpec.track.operation || 'update' - }; - this.pushNewGraphNode(metadata, vegaView.getState()); + this.pushNewGraphNode(createMetadata(), vegaView.getState()); }); } else { - const rawTitle = (signalSpec.track.title) ? signalSpec.track.title : `{{name}} = {{value}}`; - const template = handlebars.compile(rawTitle); - const metadata: ISetStateMetadata = { - name: template(context), - category: signalSpec.track.category || 'data', - operation: signalSpec.track.operation || 'update' - }; - this.pushNewGraphNode(metadata, vegaView.getState()); + this.pushNewGraphNode(createMetadata(), vegaView.getState()); } }