From 8ad914288def6e7c31896e5fd4f76ec92354275c Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 11:44:50 -0500 Subject: [PATCH 01/59] WIP Add initial commit of DatasetExplorer --- src/visualizers/Visualizers.json | 6 + .../DatasetExplorer/DatasetExplorerControl.js | 178 ++++++++++++++++++ .../DatasetExplorer/DatasetExplorerPanel.js | 101 ++++++++++ .../DatasetExplorer/DatasetExplorerWidget.js | 113 +++++++++++ .../styles/DatasetExplorerWidget.css | 10 + .../styles/DatasetExplorerWidget.scss | 7 + webgme-setup.json | 9 +- 7 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 src/visualizers/panels/DatasetExplorer/DatasetExplorerControl.js create mode 100644 src/visualizers/panels/DatasetExplorer/DatasetExplorerPanel.js create mode 100644 src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js create mode 100644 src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css create mode 100644 src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss diff --git a/src/visualizers/Visualizers.json b/src/visualizers/Visualizers.json index 9a42df9eb..68d75e470 100644 --- a/src/visualizers/Visualizers.json +++ b/src/visualizers/Visualizers.json @@ -148,5 +148,11 @@ "title": "OperationDepEditor", "panel": "panels/OperationDepEditor/OperationDepEditorPanel", "DEBUG_ONLY": false + }, + { + "id": "DatasetExplorer", + "title": "DatasetExplorer", + "panel": "panels/DatasetExplorer/DatasetExplorerPanel", + "DEBUG_ONLY": false } ] diff --git a/src/visualizers/panels/DatasetExplorer/DatasetExplorerControl.js b/src/visualizers/panels/DatasetExplorer/DatasetExplorerControl.js new file mode 100644 index 000000000..33590fc4e --- /dev/null +++ b/src/visualizers/panels/DatasetExplorer/DatasetExplorerControl.js @@ -0,0 +1,178 @@ +/*globals define, WebGMEGlobal*/ +/** + * Generated by VisualizerGenerator 1.7.0 from webgme on Mon May 04 2020 17:09:31 GMT-0500 (Central Daylight Time). + */ + +define([ + 'deepforge/viz/ConfigDialog', + 'js/Constants', + 'js/Utils/GMEConcepts', + 'js/NodePropertyNames' +], function ( + ConfigDialog, + CONSTANTS, + GMEConcepts, + nodePropertyNames +) { + + 'use strict'; + + function DatasetExplorerControl(options) { + + this._logger = options.logger.fork('Control'); + + this._client = options.client; + this._embedded = options.embedded; + + // Initialize core collections and variables + this._widget = options.widget; + + this._currentNodeId = null; + this._currentNodeParentId = undefined; + + this._initWidgetEventHandlers(); + + this._logger.debug('ctor finished'); + } + + DatasetExplorerControl.prototype._initWidgetEventHandlers = function () { + this._widget.getConfigDialog = () => new ConfigDialog(this._client); + }; + + /* * * * * * * * Visualizer content update callbacks * * * * * * * */ + // One major concept here is with managing the territory. The territory + // defines the parts of the project that the visualizer is interested in + // (this allows the browser to then only load those relevant parts). + DatasetExplorerControl.prototype.selectedObjectChanged = function (nodeId) { + var desc = this._getObjectDescriptor(nodeId), + self = this; + + self._logger.debug('activeObject nodeId \'' + nodeId + '\''); + + // Remove current territory patterns + if (self._currentNodeId) { + self._client.removeUI(self._territoryId); + } + + self._currentNodeId = nodeId; + self._currentNodeParentId = undefined; + + if (typeof self._currentNodeId === 'string') { + // Put new node's info into territory rules + self._selfPatterns = {}; + self._selfPatterns[nodeId] = {children: 0}; // Territory "rule" + + self._widget.setTitle(desc.name.toUpperCase()); + + self._currentNodeParentId = desc.parentId; + + self._territoryId = self._client.addUI(self, function (events) { + self._eventCallback(events); + }); + + self._client.updateTerritory(self._territoryId, self._selfPatterns); + } + }; + + // This next function retrieves the relevant node information for the widget + DatasetExplorerControl.prototype._getObjectDescriptor = function (nodeId) { + var node = this._client.getNode(nodeId), + objDescriptor; + + if (node) { + objDescriptor = { + id: node.getId(), + name: node.getAttribute(nodePropertyNames.Attributes.name), + data: node.getAttribute('data'), + type: node.getAttribute('type'), + childrenIds: node.getChildrenIds(), + parentId: node.getParentId(), + isConnection: GMEConcepts.isConnection(nodeId) + }; + } + + return objDescriptor; + }; + + /* * * * * * * * Node Event Handling * * * * * * * */ + DatasetExplorerControl.prototype._eventCallback = function (events) { + var i = events ? events.length : 0, + event; + + this._logger.debug('_eventCallback \'' + i + '\' items'); + + while (i--) { + event = events[i]; + switch (event.etype) { + + case CONSTANTS.TERRITORY_EVENT_LOAD: + this._onLoad(event.eid); + break; + case CONSTANTS.TERRITORY_EVENT_UPDATE: + this._onUpdate(event.eid); + break; + case CONSTANTS.TERRITORY_EVENT_UNLOAD: + this._onUnload(event.eid); + break; + default: + break; + } + } + + this._logger.debug('_eventCallback \'' + events.length + '\' items - DONE'); + }; + + DatasetExplorerControl.prototype._onLoad = function (gmeId) { + var description = this._getObjectDescriptor(gmeId); + this._widget.addNode(description); + }; + + DatasetExplorerControl.prototype._onUpdate = function (gmeId) { + var description = this._getObjectDescriptor(gmeId); + this._widget.updateNode(description); + }; + + DatasetExplorerControl.prototype._onUnload = function (gmeId) { + this._widget.removeNode(gmeId); + }; + + DatasetExplorerControl.prototype._stateActiveObjectChanged = function (model, activeObjectId) { + if (this._currentNodeId === activeObjectId) { + // The same node selected as before - do not trigger + } else { + this.selectedObjectChanged(activeObjectId); + } + }; + + /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ + DatasetExplorerControl.prototype.destroy = function () { + this._detachClientEventListeners(); + }; + + DatasetExplorerControl.prototype._attachClientEventListeners = function () { + this._detachClientEventListeners(); + if (!this._embedded) { + WebGMEGlobal.State.on('change:' + CONSTANTS.STATE_ACTIVE_OBJECT, this._stateActiveObjectChanged, this); + } + }; + + DatasetExplorerControl.prototype._detachClientEventListeners = function () { + if (!this._embedded) { + WebGMEGlobal.State.off('change:' + CONSTANTS.STATE_ACTIVE_OBJECT, this._stateActiveObjectChanged); + } + }; + + DatasetExplorerControl.prototype.onActivate = function () { + this._attachClientEventListeners(); + + if (typeof this._currentNodeId === 'string') { + WebGMEGlobal.State.registerActiveObject(this._currentNodeId, {suppressVisualizerFromNode: true}); + } + }; + + DatasetExplorerControl.prototype.onDeactivate = function () { + this._detachClientEventListeners(); + }; + + return DatasetExplorerControl; +}); diff --git a/src/visualizers/panels/DatasetExplorer/DatasetExplorerPanel.js b/src/visualizers/panels/DatasetExplorer/DatasetExplorerPanel.js new file mode 100644 index 000000000..14b543289 --- /dev/null +++ b/src/visualizers/panels/DatasetExplorer/DatasetExplorerPanel.js @@ -0,0 +1,101 @@ +/*globals define, _, WebGMEGlobal*/ +/** + * Generated by VisualizerGenerator 1.7.0 from webgme on Mon May 04 2020 17:09:31 GMT-0500 (Central Daylight Time). + */ + +define([ + 'js/PanelBase/PanelBaseWithHeader', + 'js/PanelManager/IActivePanel', + 'widgets/DatasetExplorer/DatasetExplorerWidget', + './DatasetExplorerControl' +], function ( + PanelBaseWithHeader, + IActivePanel, + DatasetExplorerWidget, + DatasetExplorerControl +) { + 'use strict'; + + function DatasetExplorerPanel(layoutManager, params) { + var options = {}; + //set properties from options + options[PanelBaseWithHeader.OPTIONS.LOGGER_INSTANCE_NAME] = 'DatasetExplorerPanel'; + options[PanelBaseWithHeader.OPTIONS.FLOATING_TITLE] = true; + + //call parent's constructor + PanelBaseWithHeader.apply(this, [options, layoutManager]); + + this._client = params.client; + this._embedded = params.embedded; + + //initialize UI + this._initialize(); + + this.logger.debug('ctor finished'); + } + + //inherit from PanelBaseWithHeader + _.extend(DatasetExplorerPanel.prototype, PanelBaseWithHeader.prototype); + _.extend(DatasetExplorerPanel.prototype, IActivePanel.prototype); + + DatasetExplorerPanel.prototype._initialize = function () { + var self = this; + + //set Widget title + this.setTitle(''); + + this.widget = new DatasetExplorerWidget(this.logger, this.$el); + + this.widget.setTitle = function (title) { + self.setTitle(title); + }; + + this.control = new DatasetExplorerControl({ + logger: this.logger, + client: this._client, + embedded: this._embedded, + widget: this.widget + }); + + this.onActivate(); + }; + + /* OVERRIDE FROM WIDGET-WITH-HEADER */ + /* METHOD CALLED WHEN THE WIDGET'S READ-ONLY PROPERTY CHANGES */ + DatasetExplorerPanel.prototype.onReadOnlyChanged = function (isReadOnly) { + //apply parent's onReadOnlyChanged + PanelBaseWithHeader.prototype.onReadOnlyChanged.call(this, isReadOnly); + + }; + + DatasetExplorerPanel.prototype.onResize = function (width, height) { + this.logger.debug('onResize --> width: ' + width + ', height: ' + height); + this.widget.onWidgetContainerResize(width, height); + }; + + /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ + DatasetExplorerPanel.prototype.destroy = function () { + this.control.destroy(); + this.widget.destroy(); + + PanelBaseWithHeader.prototype.destroy.call(this); + WebGMEGlobal.KeyboardManager.setListener(undefined); + WebGMEGlobal.Toolbar.refresh(); + }; + + DatasetExplorerPanel.prototype.onActivate = function () { + this.widget.onActivate(); + this.control.onActivate(); + WebGMEGlobal.KeyboardManager.setListener(this.widget); + WebGMEGlobal.Toolbar.refresh(); + }; + + DatasetExplorerPanel.prototype.onDeactivate = function () { + this.widget.onDeactivate(); + this.control.onDeactivate(); + WebGMEGlobal.KeyboardManager.setListener(undefined); + WebGMEGlobal.Toolbar.refresh(); + }; + + return DatasetExplorerPanel; +}); diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js new file mode 100644 index 000000000..399ae964b --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -0,0 +1,113 @@ +/*globals define */ + +define([ + 'deepforge/storage/index', + 'deepforge/compute/interactive/session', + 'widgets/PlotlyGraph/lib/plotly.min', + 'css!./styles/DatasetExplorerWidget.css' +], function ( + Storage, + Session, + Plotly, +) { + 'use strict'; + + // TODO: Get access to the interactive compute + var WIDGET_CLASS = 'dataset-explorer'; + + class DatasetExplorerWidget { + constructor(logger, container) { + this._logger = logger.fork('Widget'); + + this.$el = container; + this.$el.addClass(WIDGET_CLASS); + + this.session = new Session('local'); + this.nodeId = null; + + this._logger.debug('ctor finished'); + } + + async getAuthenticationConfig (dataInfo) { + const {backend} = dataInfo; + const metadata = Storage.getStorageMetadata(backend); + metadata.configStructure = metadata.configStructure + .filter(option => option.isAuth); + + if (metadata.configStructure.length) { + const configDialog = this.getConfigDialog(); + const title = `Authenticate with ${metadata.name}`; + const iconClass = `glyphicon glyphicon-download-alt`; + const config = await configDialog.show(metadata, {title, iconClass}); + + return config[backend]; + } + } + + async getYValues (desc) { + await this.session.whenConnected(); + + // TODO: Ask if we should load the data? + const dataInfo = JSON.parse(desc.data); + const config = await this.getAuthenticationConfig(dataInfo); + // TODO: Show loading message... + await this.session.addArtifact('data', dataInfo, desc.type, config); + const command = [ + 'from artifacts.data import data', + 'import json', + 'print(json.dumps([l[0] for l in data["y"]]))' + ].join(';'); + const {stdout} = await this.session.exec(`python -c '${command}'`); // TODO: Add error handling + return JSON.parse(stdout); + } + + async getPlotData (desc) { + return [ + { + y: await this.getYValues(desc), + boxpoints: 'all', + jitter: 0.3, + pointpos: -1.8, + type: 'box' + } + ]; + } + + onWidgetContainerResize (/*width, height*/) { + this._logger.debug('Widget is resizing...'); + } + + // Adding/Removing/Updating items + async addNode (desc) { + this.nodeId = desc.id; + const plotData = await this.getPlotData(desc); + const isStillShown = this.nodeId === desc.id; + if (isStillShown) { + const title = `Distribution of Labels for ${desc.name}`; + Plotly.newPlot(this.$el[0], plotData, {title}); + } + } + + removeNode (/*gmeId*/) { + } + + updateNode (/*desc*/) { + } + + /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ + destroy () { + Plotly.purge(this.$el[0]); + this.session.close(); + } + + onActivate () { + this._logger.debug('DatasetExplorerWidget has been activated'); + } + + onDeactivate () { + this._logger.debug('DatasetExplorerWidget has been deactivated'); + } + } + + return DatasetExplorerWidget; +}); diff --git a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css new file mode 100644 index 000000000..6b76675b4 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css @@ -0,0 +1,10 @@ +/** + * This file is for any css that you may want for this visualizer. + * + * Ideally, you would use the scss file also provided in this directory + * and then generate this file automatically from that. However, you can + * simply write css if you prefer + */ + +.dataset-explorer { + outline: none; } diff --git a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss new file mode 100644 index 000000000..b4a71ca82 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss @@ -0,0 +1,7 @@ +/** + * This file is for any scss that you may want for this visualizer. + */ + +.dataset-explorer { + outline: none; +} diff --git a/webgme-setup.json b/webgme-setup.json index 338d116d1..6124950d7 100644 --- a/webgme-setup.json +++ b/webgme-setup.json @@ -281,6 +281,13 @@ "panel": "src/visualizers/panels/OperationDepEditor", "secondary": false, "widget": "src/visualizers/widgets/OperationDepEditor" + }, + "DatasetExplorer": { + "src": "panels/DatasetExplorer/DatasetExplorerPanel", + "title": "DatasetExplorer", + "panel": "src/visualizers/panels/DatasetExplorer", + "secondary": false, + "widget": "src/visualizers/widgets/DatasetExplorer" } }, "addons": {}, @@ -408,4 +415,4 @@ "seeds": {}, "routers": {} } -} \ No newline at end of file +} From ae337e6d051ba1b3ea9ea4dfb01b9937d10ee145 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 13:18:41 -0500 Subject: [PATCH 02/59] WIP use artifact name in DataExplorer --- .../widgets/DatasetExplorer/DatasetExplorerWidget.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index 399ae964b..8c8e298c8 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -50,10 +50,12 @@ define([ // TODO: Ask if we should load the data? const dataInfo = JSON.parse(desc.data); const config = await this.getAuthenticationConfig(dataInfo); + // TODO: Show loading message... - await this.session.addArtifact('data', dataInfo, desc.type, config); + const name = desc.name.replace(/[^a-zA-Z_]/g, '_'); + await this.session.addArtifact(name, dataInfo, desc.type, config); const command = [ - 'from artifacts.data import data', + `from artifacts.${name} import data`, 'import json', 'print(json.dumps([l[0] for l in data["y"]]))' ].join(';'); From a164ce2282029cd10edd91cf6122cbcf65951eb4 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 7 May 2020 12:15:53 -0500 Subject: [PATCH 03/59] WIP Add plot editor to dataset explorer --- .../DatasetExplorer/DatasetExplorerWidget.js | 52 ++++++++-- .../styles/DatasetExplorerWidget.css | 99 +++++++++++++++++-- .../styles/DatasetExplorerWidget.scss | 98 +++++++++++++++++- 3 files changed, 228 insertions(+), 21 deletions(-) diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index 8c8e298c8..a0263a0ec 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -1,19 +1,20 @@ -/*globals define */ +/*globals define, $ */ define([ 'deepforge/storage/index', 'deepforge/compute/interactive/session', 'widgets/PlotlyGraph/lib/plotly.min', - 'css!./styles/DatasetExplorerWidget.css' + './PlotEditor', + 'css!./styles/DatasetExplorerWidget.css', ], function ( Storage, Session, Plotly, + PlotEditor, ) { 'use strict'; - // TODO: Get access to the interactive compute - var WIDGET_CLASS = 'dataset-explorer'; + const WIDGET_CLASS = 'dataset-explorer'; class DatasetExplorerWidget { constructor(logger, container) { @@ -21,6 +22,17 @@ define([ this.$el = container; this.$el.addClass(WIDGET_CLASS); + const row = $('
', {class: 'row'}); + this.$el.append(row); + + this.$plot = $('
', {class: 'plot col-9'}); + this.$plotEditor = $('
', {class: 'plot-editor col-3'}); + this.plotEditor = new PlotEditor(this.$plotEditor); + this.plotEditor.on('update', values => { + this.setLayout(values); + }); + row.append(this.$plot); + row.append(this.$plotEditor); this.session = new Session('local'); this.nodeId = null; @@ -44,7 +56,7 @@ define([ } } - async getYValues (desc) { + async importDataToSession (desc) { await this.session.whenConnected(); // TODO: Ask if we should load the data? @@ -54,6 +66,12 @@ define([ // TODO: Show loading message... const name = desc.name.replace(/[^a-zA-Z_]/g, '_'); await this.session.addArtifact(name, dataInfo, desc.type, config); + } + + async getYValues (desc) { + const name = desc.name.replace(/[^a-zA-Z_]/g, '_'); + await this.importDataToSession(desc); + const command = [ `from artifacts.${name} import data`, 'import json', @@ -79,14 +97,30 @@ define([ this._logger.debug('Widget is resizing...'); } + defaultLayout(desc) { + const title = `Distribution of Labels for ${desc.name}`; + return {title}; + } + + setLayout(newVals) { + this.layout = newVals; + this.onPlotUpdated(); + } + + onPlotUpdated () { + Plotly.newPlot(this.$plot[0], this.plotData, this.layout); + } + // Adding/Removing/Updating items async addNode (desc) { this.nodeId = desc.id; - const plotData = await this.getPlotData(desc); + this.plotData = await this.getPlotData(desc); const isStillShown = this.nodeId === desc.id; if (isStillShown) { - const title = `Distribution of Labels for ${desc.name}`; - Plotly.newPlot(this.$el[0], plotData, {title}); + this.layout = this.defaultLayout(desc); + this.plotEditor.set(this.layout); + this.onPlotUpdated(); + //Plotly.newPlot(this.$plot[0], this.plotData, this.layout); } } @@ -98,7 +132,7 @@ define([ /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ destroy () { - Plotly.purge(this.$el[0]); + Plotly.purge(this.$plot[0]); this.session.close(); } diff --git a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css index 6b76675b4..7d7718b4b 100644 --- a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css +++ b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.css @@ -1,10 +1,93 @@ -/** - * This file is for any css that you may want for this visualizer. - * - * Ideally, you would use the scss file also provided in this directory - * and then generate this file automatically from that. However, you can - * simply write css if you prefer - */ - .dataset-explorer { outline: none; } + .dataset-explorer .plot { + display: inline-block; } + .dataset-explorer .plot-editor { + padding: 20px; + display: inline-block; } + .dataset-explorer .container { + width: 90%; + margin-left: auto; + margin-right: auto; } + @media only screen and (min-width: 33.75em) { + .dataset-explorer .container { + width: 80%; } } + @media only screen and (min-width: 60em) { + .dataset-explorer .container { + width: 75%; + max-width: 60rem; } } + .dataset-explorer .row { + position: relative; + width: 100%; } + .dataset-explorer .row [class^="col"] { + float: left; + margin: 0.5rem 2%; + min-height: 0.125rem; } + .dataset-explorer .row::after { + content: ""; + display: table; + clear: both; } + .dataset-explorer .col-1, + .dataset-explorer .col-2, + .dataset-explorer .col-3, + .dataset-explorer .col-4, + .dataset-explorer .col-5, + .dataset-explorer .col-6, + .dataset-explorer .col-7, + .dataset-explorer .col-8, + .dataset-explorer .col-9, + .dataset-explorer .col-10, + .dataset-explorer .col-11, + .dataset-explorer .col-12 { + width: 96%; } + .dataset-explorer .col-1-sm { + width: 4.33333%; } + .dataset-explorer .col-2-sm { + width: 12.66667%; } + .dataset-explorer .col-3-sm { + width: 21%; } + .dataset-explorer .col-4-sm { + width: 29.33333%; } + .dataset-explorer .col-5-sm { + width: 37.66667%; } + .dataset-explorer .col-6-sm { + width: 46%; } + .dataset-explorer .col-7-sm { + width: 54.33333%; } + .dataset-explorer .col-8-sm { + width: 62.66667%; } + .dataset-explorer .col-9-sm { + width: 71%; } + .dataset-explorer .col-10-sm { + width: 79.33333%; } + .dataset-explorer .col-11-sm { + width: 87.66667%; } + .dataset-explorer .col-12-sm { + width: 96%; } + @media only screen and (min-width: 45em) { + .dataset-explorer .col-1 { + width: 4.33333%; } + .dataset-explorer .col-2 { + width: 12.66667%; } + .dataset-explorer .col-3 { + width: 21%; } + .dataset-explorer .col-4 { + width: 29.33333%; } + .dataset-explorer .col-5 { + width: 37.66667%; } + .dataset-explorer .col-6 { + width: 46%; } + .dataset-explorer .col-7 { + width: 54.33333%; } + .dataset-explorer .col-8 { + width: 62.66667%; } + .dataset-explorer .col-9 { + width: 71%; } + .dataset-explorer .col-10 { + width: 79.33333%; } + .dataset-explorer .col-11 { + width: 87.66667%; } + .dataset-explorer .col-12 { + width: 96%; } + .dataset-explorer .hidden-sm { + display: block; } } diff --git a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss index b4a71ca82..19f1b7368 100644 --- a/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss +++ b/src/visualizers/widgets/DatasetExplorer/styles/DatasetExplorerWidget.scss @@ -1,7 +1,97 @@ -/** - * This file is for any scss that you may want for this visualizer. - */ - .dataset-explorer { outline: none; + .plot { + display: inline-block; + } + .plot-editor { + padding: 20px; + display: inline-block; + } + + // grid (using simple-grid) + + $width: 96%; + $gutter: 4%; + $breakpoint-small: 33.75em; // 540px + $breakpoint-med: 45em; // 720px + $breakpoint-large: 60em; // 960px + + .container { + width: 90%; + margin-left: auto; + margin-right: auto; + + @media only screen and (min-width: $breakpoint-small) { + width: 80%; + } + + @media only screen and (min-width: $breakpoint-large) { + width: 75%; + max-width: 60rem; + } + } + + .row { + position: relative; + width: 100%; + } + + .row [class^="col"] { + float: left; + margin: 0.5rem 2%; + min-height: 0.125rem; + } + + .row::after { + content: ""; + display: table; + clear: both; + } + + .col-1, + .col-2, + .col-3, + .col-4, + .col-5, + .col-6, + .col-7, + .col-8, + .col-9, + .col-10, + .col-11, + .col-12 { + width: $width; + } + + .col-1-sm { width:($width / 12) - ($gutter * 11 / 12); } + .col-2-sm { width: ($width / 6) - ($gutter * 10 / 12); } + .col-3-sm { width: ($width / 4) - ($gutter * 9 / 12); } + .col-4-sm { width: ($width / 3) - ($gutter * 8 / 12); } + .col-5-sm { width: ($width / (12 / 5)) - ($gutter * 7 / 12); } + .col-6-sm { width: ($width / 2) - ($gutter * 6 / 12); } + .col-7-sm { width: ($width / (12 / 7)) - ($gutter * 5 / 12); } + .col-8-sm { width: ($width / (12 / 8)) - ($gutter * 4 / 12); } + .col-9-sm { width: ($width / (12 / 9)) - ($gutter * 3 / 12); } + .col-10-sm { width: ($width / (12 / 10)) - ($gutter * 2 / 12); } + .col-11-sm { width: ($width / (12 / 11)) - ($gutter * 1 / 12); } + .col-12-sm { width: $width; } + + @media only screen and (min-width: $breakpoint-med) { + .col-1 { width:($width / 12) - ($gutter * 11 / 12); } + .col-2 { width: ($width / 6) - ($gutter * 10 / 12); } + .col-3 { width: ($width / 4) - ($gutter * 9 / 12); } + .col-4 { width: ($width / 3) - ($gutter * 8 / 12); } + .col-5 { width: ($width / (12 / 5)) - ($gutter * 7 / 12); } + .col-6 { width: ($width / 2) - ($gutter * 6 / 12); } + .col-7 { width: ($width / (12 / 7)) - ($gutter * 5 / 12); } + .col-8 { width: ($width / (12 / 8)) - ($gutter * 4 / 12); } + .col-9 { width: ($width / (12 / 9)) - ($gutter * 3 / 12); } + .col-10 { width: ($width / (12 / 10)) - ($gutter * 2 / 12); } + .col-11 { width: ($width / (12 / 11)) - ($gutter * 1 / 12); } + .col-12 { width: $width; } + + .hidden-sm { + display: block; + } + } } From 277608bb2df501ecb237330cbe6bc8d4a395a28d Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 7 May 2020 12:16:40 -0500 Subject: [PATCH 04/59] WIP add PlotEditor and css --- .../widgets/DatasetExplorer/PlotEditor.html | 15 +++++ .../widgets/DatasetExplorer/PlotEditor.js | 60 +++++++++++++++++++ .../styles/simple-grid.min.css | 1 + 3 files changed, 76 insertions(+) create mode 100644 src/visualizers/widgets/DatasetExplorer/PlotEditor.html create mode 100644 src/visualizers/widgets/DatasetExplorer/PlotEditor.js create mode 100644 src/visualizers/widgets/DatasetExplorer/styles/simple-grid.min.css diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.html b/src/visualizers/widgets/DatasetExplorer/PlotEditor.html new file mode 100644 index 000000000..40da2eecb --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.html @@ -0,0 +1,15 @@ +
+
+ + +
+
+ + +
+
+ + +
+ +
diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js new file mode 100644 index 000000000..2508ed4c8 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js @@ -0,0 +1,60 @@ +/* globals define, $ */ +define([ + 'underscore', + 'deepforge/EventEmitter', + 'text!./PlotEditor.html', +], function( + _, + EventEmitter, + PlotEditorHtml, +) { + + class PlotEditor extends EventEmitter { + constructor(container) { + super(); + this.$el = container; + this.$el.append($(PlotEditorHtml)); + + const dataFields = ['title', 'xaxis', 'yaxis']; + this.$elements = {}; + dataFields.forEach(name => { + this.$elements[name] = this.$el.find(`#${name}`); + }); + + this.$update = this.$el.find('button'); + this.$update.on('click', event => { + this.updateClicked(); + event.preventDefault(); + event.stopPropagation(); + }); + } + + set(values) { + Object.entries(this.$elements).map(entry => { + const [name, element] = entry; + if (values.hasOwnProperty(name)) { + element.val(values[name]); + } + }); + } + + data() { + const entries = Object.entries(this.$elements).map(entry => { + const [name, element] = entry; + const value = element.val(); + return [name, value]; + }); + return _.object(entries); + } + + updateClicked() { + const values = this.data(); + this.emit('update', values); + } + } + // TODO: add input for title value + // TODO: add input for labels + // TODO: add input for data? + + return PlotEditor; +}); diff --git a/src/visualizers/widgets/DatasetExplorer/styles/simple-grid.min.css b/src/visualizers/widgets/DatasetExplorer/styles/simple-grid.min.css new file mode 100644 index 000000000..d3f2cc2cd --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/styles/simple-grid.min.css @@ -0,0 +1 @@ +@import url(https://fonts.googleapis.com/css?family=Lato:400,300,300italic,400italic,700,700italic);body,html{height:100%;width:100%;margin:0;padding:0;left:0;top:0;font-size:100%}.center,.container{margin-left:auto;margin-right:auto}*{font-family:Lato,Helvetica,sans-serif;color:#333447;line-height:1.5}h1{font-size:2.5rem}h2{font-size:2rem}h3{font-size:1.375rem}h4{font-size:1.125rem}h5{font-size:1rem}h6{font-size:.875rem}p{font-size:1.125rem;font-weight:200;line-height:1.8}.font-light{font-weight:300}.font-regular{font-weight:400}.font-heavy{font-weight:700}.left{text-align:left}.right{text-align:right}.center{text-align:center}.justify{text-align:justify}.container{width:90%}.row{position:relative;width:100%}.row [class^=col]{float:left;margin:.5rem 2%;min-height:.125rem}.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9{width:96%}.col-1-sm{width:4.33%}.col-2-sm{width:12.66%}.col-3-sm{width:21%}.col-4-sm{width:29.33%}.col-5-sm{width:37.66%}.col-6-sm{width:46%}.col-7-sm{width:54.33%}.col-8-sm{width:62.66%}.col-9-sm{width:71%}.col-10-sm{width:79.33%}.col-11-sm{width:87.66%}.col-12-sm{width:96%}.row::after{content:"";display:table;clear:both}.hidden-sm{display:none}@media only screen and (min-width:33.75em){.container{width:80%}}@media only screen and (min-width:45em){.col-1{width:4.33%}.col-2{width:12.66%}.col-3{width:21%}.col-4{width:29.33%}.col-5{width:37.66%}.col-6{width:46%}.col-7{width:54.33%}.col-8{width:62.66%}.col-9{width:71%}.col-10{width:79.33%}.col-11{width:87.66%}.col-12{width:96%}.hidden-sm{display:block}}@media only screen and (min-width:60em){.container{width:75%;max-width:60rem}} From 7fe251d18b880a0c3446ae17e04fcbff7d702dd7 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 8 May 2020 13:51:39 -0500 Subject: [PATCH 05/59] Add UI elements for adding/removing data --- .../widgets/DatasetExplorer/DataEditorBase.js | 54 +++++++++ .../DatasetExplorer/DatasetExplorerWidget.js | 33 +++++- .../widgets/DatasetExplorer/PlotEditor.html | 8 +- .../widgets/DatasetExplorer/PlotEditor.js | 106 ++++++++++++------ .../widgets/DatasetExplorer/PlottedData.js | 9 ++ .../DatasetExplorer/PlottedDataEditor.hml | 0 .../DatasetExplorer/PlottedDataEditor.html | 36 ++++++ .../DatasetExplorer/PlottedDataEditor.js | 51 +++++++++ 8 files changed, 262 insertions(+), 35 deletions(-) create mode 100644 src/visualizers/widgets/DatasetExplorer/DataEditorBase.js create mode 100644 src/visualizers/widgets/DatasetExplorer/PlottedData.js create mode 100644 src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml create mode 100644 src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html create mode 100644 src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js diff --git a/src/visualizers/widgets/DatasetExplorer/DataEditorBase.js b/src/visualizers/widgets/DatasetExplorer/DataEditorBase.js new file mode 100644 index 000000000..0e2b220a4 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/DataEditorBase.js @@ -0,0 +1,54 @@ +/* globals define, $ */ +define([ + 'underscore', + 'deepforge/EventEmitter', +], function( + _, + EventEmitter, +) { + + class DataEditorBase extends EventEmitter { + constructor(html, dataFields, updateOnChange) { + super(); + this.$el = $(html); + + this.$elements = {}; + dataFields.forEach(name => { + this.$elements[name] = this.$el.find(`#${name}`); + if (updateOnChange) { + this.$elements[name].change(() => this.onUpdate()); // FIXME + } + }); + } + + set(values) { + Object.entries(this.$elements).map(entry => { + const [name, element] = entry; + if (values.hasOwnProperty(name)) { + element.val(values[name]); + } + }); + } + + data() { + const entries = Object.entries(this.$elements) + .map(entry => { + const [name, element] = entry; + const value = element.val(); + return [name, value]; + }) + .filter(entry => !!entry[1]); + return _.object(entries); + } + + onUpdate() { + const values = this.data(); + this.emit('update', values); + } + } + // TODO: add input for title value + // TODO: add input for labels + // TODO: add input for data? + + return DataEditorBase; +}); diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index a0263a0ec..93fd6a648 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -5,12 +5,14 @@ define([ 'deepforge/compute/interactive/session', 'widgets/PlotlyGraph/lib/plotly.min', './PlotEditor', + 'underscore', 'css!./styles/DatasetExplorerWidget.css', ], function ( Storage, Session, Plotly, PlotEditor, + _, ) { 'use strict'; @@ -29,8 +31,11 @@ define([ this.$plotEditor = $('
', {class: 'plot-editor col-3'}); this.plotEditor = new PlotEditor(this.$plotEditor); this.plotEditor.on('update', values => { - this.setLayout(values); + // TODO: fetch the layout values and the data values + console.log('update:', values); + //this.setLayout(values); }); + row.append(this.$plot); row.append(this.$plotEditor); @@ -70,6 +75,7 @@ define([ async getYValues (desc) { const name = desc.name.replace(/[^a-zA-Z_]/g, '_'); + return [1,2,3,4]; await this.importDataToSession(desc); const command = [ @@ -81,6 +87,17 @@ define([ return JSON.parse(stdout); } + async getMetadata (desc) { + // TODO: Load the data into the current session + return { + name: desc.name, + data: { + X: [7500, 64, 64, 5], + y: [7500, 1] + } + }; + } + async getPlotData (desc) { return [ { @@ -88,6 +105,7 @@ define([ boxpoints: 'all', jitter: 0.3, pointpos: -1.8, + name: `${desc.name}['y']`, type: 'box' } ]; @@ -114,11 +132,22 @@ define([ // Adding/Removing/Updating items async addNode (desc) { this.nodeId = desc.id; + // TODO: Use a different method of storing what to plot this.plotData = await this.getPlotData(desc); const isStillShown = this.nodeId === desc.id; + // getMetadata if (isStillShown) { this.layout = this.defaultLayout(desc); - this.plotEditor.set(this.layout); + const data = _.extend({}, this.layout); + data.plottedData = [ // FIXME: remove this + { + id: 123, + name: 'Example Data', + data: `combined_dataset['y']`, + dataSlice: '[:,0]', + } + ]; + this.plotEditor.set(data); this.onPlotUpdated(); //Plotly.newPlot(this.$plot[0], this.plotData, this.layout); } diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.html b/src/visualizers/widgets/DatasetExplorer/PlotEditor.html index 40da2eecb..4034b710e 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlotEditor.html +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.html @@ -11,5 +11,11 @@
- +

Plotted Data

+
    +
  • Example data + +
  • +
+ diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js index 2508ed4c8..ea040dc2c 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js @@ -1,55 +1,97 @@ /* globals define, $ */ define([ 'underscore', - 'deepforge/EventEmitter', + './DataEditorBase', + './PlottedDataEditor', 'text!./PlotEditor.html', ], function( _, - EventEmitter, + DataEditorBase, + PlottedDataEditor, PlotEditorHtml, ) { - class PlotEditor extends EventEmitter { + class PlotEditor extends DataEditorBase { constructor(container) { - super(); - this.$el = container; - this.$el.append($(PlotEditorHtml)); - - const dataFields = ['title', 'xaxis', 'yaxis']; - this.$elements = {}; - dataFields.forEach(name => { - this.$elements[name] = this.$el.find(`#${name}`); - }); - - this.$update = this.$el.find('button'); - this.$update.on('click', event => { - this.updateClicked(); - event.preventDefault(); + container.append($(PlotEditorHtml)); + super(container, ['title', 'xaxis', 'yaxis'], true); + this.$addData = this.$el.find('button'); + this.$addData.on('click', event => { event.stopPropagation(); + event.preventDefault(); + this.onAddDataClicked(); }); + + this.$plottedData = this.$el.find('.plotted-data'); + this.plottedData = []; } + async onAddDataClicked() { + const editor = new PlottedDataEditor(); + const data = await editor.show(); + if (data) { + this.plottedData.push(data); + this.refreshPlottedDataList(); + this.onUpdate(); + } + } + set(values) { - Object.entries(this.$elements).map(entry => { - const [name, element] = entry; - if (values.hasOwnProperty(name)) { - element.val(values[name]); - } - }); + super.set(values); + if (values.plottedData) { + this.plottedData = values.plottedData; + this.refreshPlottedDataList(); + } } - data() { - const entries = Object.entries(this.$elements).map(entry => { - const [name, element] = entry; - const value = element.val(); - return [name, value]; + refreshPlottedDataList() { + this.$plottedData.empty(); + this.plottedData.forEach(data => { + const element = this.createPlottedElement(data); + this.$plottedData.append(element); }); - return _.object(entries); } - updateClicked() { - const values = this.data(); - this.emit('update', values); + createPlottedElement(data) { + const editClass = 'glyphicon-pencil'; + const removeClass = 'glyphicon-remove'; + const element = $( + `
  • ${data.name} + + +
  • ` + ); + element.find(`.${editClass}`) + .on('click', () => this.editPlottedData(data)); + element.find(`.${removeClass}`) + .on('click', () => this.removePlottedData(data)); + return element; + } + + async editPlottedData(data) { + const editor = new PlottedDataEditor(data); + const newData = await editor.show(); + const index = this.plottedData.findIndex(d => d.id === newData.id); + if (index > -1) { + this.plottedData.splice(index, 1, data); + this.refreshPlottedDataList(); + this.onUpdate(); + } + } + + removePlottedData(data) { + const index = this.plottedData.findIndex(d => d.id === data.id); + if (index > -1) { + this.plottedData.splice(index, 1); + this.refreshPlottedDataList(); + this.onUpdate(); + } + } + + data() { + const values = super.data(); + values.data = this.plottedData; + return values; } } // TODO: add input for title value diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedData.js b/src/visualizers/widgets/DatasetExplorer/PlottedData.js new file mode 100644 index 000000000..46f6c4947 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PlottedData.js @@ -0,0 +1,9 @@ +/*globals define*/ +define([ +], function( +) { + class PlottedData { + } + + return PlottedData; +}); diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml new file mode 100644 index 000000000..e69de29bb diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html new file mode 100644 index 000000000..95295b0fb --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html @@ -0,0 +1,36 @@ + diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js new file mode 100644 index 000000000..93818f0bb --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js @@ -0,0 +1,51 @@ +/* globals define */ +define([ + 'underscore', + './DataEditorBase', + 'text!./PlottedDataEditor.html', +], function( + _, + DataEditorBase, + Html, +) { + + Html = _.template(Html); + class PlottedDataEditor extends DataEditorBase { + constructor(plottedData) { + const isNewData = !plottedData; + const title = isNewData ? `Add data to figure` : + `Edit "${plottedData.name}"`; // FIXME: fix capitalization? + + super(Html({title}), ['id', 'name', 'data', 'dataSlice']); + + if (!isNewData) { + this.set(plottedData); + this.id = plottedData.id; + } else { + this.id = Date.now(); + } + + this.$update = this.$el.find('.btn-primary'); + } + + async show() { + this.$el.modal('show'); + return new Promise(resolve => { + this.$update.on('click', event => { + this.$el.modal('hide'); + event.stopPropagation(); + event.preventDefault(); + resolve(this.data()); + }); + }); + } + + data() { + const data = super.data(); + data.id = this.id; + return data; + } + } + + return PlottedDataEditor; +}); From 00e7694b344a230c16735adc755b2b0a89f2edab Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 8 May 2020 14:00:02 -0500 Subject: [PATCH 06/59] Fix UI issue with editing plotted data --- src/visualizers/widgets/DatasetExplorer/PlotEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js index ea040dc2c..3e73070bd 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js @@ -73,7 +73,7 @@ define([ const newData = await editor.show(); const index = this.plottedData.findIndex(d => d.id === newData.id); if (index > -1) { - this.plottedData.splice(index, 1, data); + this.plottedData.splice(index, 1, newData); this.refreshPlottedDataList(); this.onUpdate(); } From 9792e13348b24715bcfc75250cc0f917d7c26196 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 10:15:11 -0500 Subject: [PATCH 07/59] WIP Add utility for parsing python slices --- .../DatasetExplorer/PythonSliceParser.js | 98 +++++++++++++++++++ .../DatasetExplorer/PythonSliceParser.js | 72 ++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js create mode 100644 test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js diff --git a/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js new file mode 100644 index 000000000..4b7a101a4 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js @@ -0,0 +1,98 @@ +/* globals define */ +define([ +], function( +) { + + class OutOfRangeError extends Error { + constructor(len, index) { + super(`Index out of range: ${index} (length is ${len})`); + } + } + + class ArrayAccessor { + select(/*dims*/) { + const typeName = this.constructor.name; + throw new Error(`"select" not implemented for ${typeName}`); + } + } + + class Index extends ArrayAccessor { + constructor(index) { + super(); + this.index = index; + } + + select(dims) { + this.ensureValidIndex(dims); + return dims.slice(1); + } + + isInRange(len) { + return this.index < len; + } + + ensureValidIndex(dims) { + if (!this.isInRange(dims[0])) { + throw new OutOfRangeError(dims[0], this.index); + } + } + } + + class Slice extends ArrayAccessor { + constructor(start, stop, step) { + super(); + this.start = start || 0; + this.stop = stop || Infinity; + this.step = step || 1; + } + + resolveIndex(len, index) { + if (index < 0) { + return len + index; + } + if (index > len) { + return len; + } + return index; + } + + select(dims, position) { + const [dim, ...nextDims] = dims.slice(position); + const start = this.resolveIndex(dim, this.start); + const end = this.resolveIndex(dim, this.stop); + const newDim = Math.ceil((end - start)/this.step); + const newDims = newDim > 0 ? [newDim, ...nextDims] : []; + + return dims.slice(0, position).concat(newDims); + } + + static from(string) { + let [start, stop, step] = string.split(':') + .map(num => num && +num); + + const isSingleIndex = stop === undefined; + if (isSingleIndex) { + return new Index(start); + } + + return new Slice(start, stop, step); + } + } + + function getSliceStrings(rawString) { + return rawString + .replace(/(^\[|\]$)/g, '') + .split('][') + .flatMap(chunk => chunk.split(',')); + } + + function getSlicedShape(startShape, sliceString) { + const slices = getSliceStrings(sliceString).map(Slice.from); + return slices.reduce( + (shape, slice, position) => slice.select(shape, position), + startShape + ); + } + + return getSlicedShape; +}); diff --git a/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js b/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js new file mode 100644 index 000000000..f06eaafe6 --- /dev/null +++ b/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js @@ -0,0 +1,72 @@ +describe.only('PythonSliceParser', function() { + const {requirejs} = require('../../../../globals'); + const SliceParser = requirejs('widgets/DatasetExplorer/PythonSliceParser'); + const assert = require('assert').strict; + + describe('indices', function() { + it('should remove first dimension using "[0]"', function() { + const shape = [10, 5]; + const newShape = SliceParser(shape, '[0]'); + assert.deepEqual(newShape, [5]); + }); + + it('should remove multiple dimensions using "[0,1]"', function() { + const shape = [10, 5]; + const newShape = SliceParser(shape, '[0,1]'); + assert.deepEqual(newShape, []); + }); + + it('should remove multiple dimensions using "[0][1]"', function() { + const shape = [10, 5]; + const newShape = SliceParser(shape, '[0][1]'); + assert.deepEqual(newShape, []); + }); + + it('should remove multiple dimensions using "[0,-1]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[0,-1]'); + assert.deepEqual(newShape, [4]); + }); + }); + + describe('slices', function() { + it('should not remove any dimensions using "[:]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[:]'); + assert.deepEqual(newShape, shape); + }); + + it('should compute dimensions using step "[0:10:2]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[0:10:2]'); + assert.deepEqual(newShape, [5, 5, 4]); + }); + + it('should compute dimensions from odd len "[0:10:2]"', function() { + const shape = [9, 5, 4]; + const newShape = SliceParser(shape, '[0:10:2]'); + assert.deepEqual(newShape, [5, 5, 4]); + }); + + it('should compute dimensions using step "[0:10:2,0:6:2]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[0:10:2,0:6:2]'); + assert.deepEqual(newShape, [5, 3, 4]); + }); + + it('should remove dimensions using negative indices "[-2:-1]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[-2:-1]'); + assert.deepEqual(newShape, [1, 5, 4]); + }); + + it('should remove all dims if slice is too large "[100:1]"', function() { + const shape = [10, 5, 4]; + const newShape = SliceParser(shape, '[100:1]'); + assert.deepEqual(newShape, []); + }); + }); + + describe('slices and indices', function() { + }); +}); From 7ef7b6daf4f34552b3561731233ccc6cebbd83a0 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 10:23:29 -0500 Subject: [PATCH 08/59] Add slice string validation --- .../widgets/DatasetExplorer/PythonSliceParser.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js index 4b7a101a4..562d8f26e 100644 --- a/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js +++ b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js @@ -9,6 +9,12 @@ define([ } } + class InvalidSliceError extends Error { + constructor(text) { + super(`Invalid slice string: ${text}`); + } + } + class ArrayAccessor { select(/*dims*/) { const typeName = this.constructor.name; @@ -79,6 +85,14 @@ define([ } } + function ensureValidSliceString(sliceString) { + const sliceRegex = /^\[-?[0-9]*:?-?[0-9]*:?-?[0-9]*((,|\]\[)-?[0-9]*:?-?[0-9]*:?-?[0-9]*)?\]$/; + const isValid = sliceRegex.test(sliceString); + if (!isValid) { + throw new InvalidSliceError(sliceString); + } + } + function getSliceStrings(rawString) { return rawString .replace(/(^\[|\]$)/g, '') @@ -87,6 +101,7 @@ define([ } function getSlicedShape(startShape, sliceString) { + ensureValidSliceString(sliceString); const slices = getSliceStrings(sliceString).map(Slice.from); return slices.reduce( (shape, slice, position) => slice.select(shape, position), From 6b39f9bc59ca7a13fcc61cf0227d0b00ee2e1564 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 14:16:33 -0500 Subject: [PATCH 09/59] UI support for validating data slice --- .../DatasetExplorer/DatasetExplorerWidget.js | 22 ++++--- .../widgets/DatasetExplorer/PlotEditor.js | 6 +- .../DatasetExplorer/PlottedDataEditor.hml | 0 .../DatasetExplorer/PlottedDataEditor.html | 3 +- .../DatasetExplorer/PlottedDataEditor.js | 63 ++++++++++++++++++- .../DatasetExplorer/PythonSliceParser.js | 26 ++++---- .../DatasetExplorer/PythonSliceParser.js | 11 ++++ 7 files changed, 107 insertions(+), 24 deletions(-) delete mode 100644 src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index 93fd6a648..2839f6d8d 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -30,10 +30,8 @@ define([ this.$plot = $('
    ', {class: 'plot col-9'}); this.$plotEditor = $('
    ', {class: 'plot-editor col-3'}); this.plotEditor = new PlotEditor(this.$plotEditor); - this.plotEditor.on('update', values => { - // TODO: fetch the layout values and the data values - console.log('update:', values); - //this.setLayout(values); + this.plotEditor.on('update', plotData => { + this.updatePlot(plotData); }); row.append(this.$plot); @@ -91,10 +89,16 @@ define([ // TODO: Load the data into the current session return { name: desc.name, - data: { - X: [7500, 64, 64, 5], - y: [7500, 1] - } + entries: [ + { + name: 'X', + shape: [7500, 64, 64, 5], + }, + { + name: 'y', + shape: [7500, 1] + } + ] }; } @@ -147,9 +151,9 @@ define([ dataSlice: '[:,0]', } ]; + data.metadata = [await this.getMetadata(desc)]; this.plotEditor.set(data); this.onPlotUpdated(); - //Plotly.newPlot(this.$plot[0], this.plotData, this.layout); } } diff --git a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js index 3e73070bd..03e8d78bb 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlotEditor.js +++ b/src/visualizers/widgets/DatasetExplorer/PlotEditor.js @@ -24,10 +24,11 @@ define([ this.$plottedData = this.$el.find('.plotted-data'); this.plottedData = []; + this.metadata = null; } async onAddDataClicked() { - const editor = new PlottedDataEditor(); + const editor = new PlottedDataEditor(null, this.metadata); const data = await editor.show(); if (data) { this.plottedData.push(data); @@ -42,6 +43,7 @@ define([ this.plottedData = values.plottedData; this.refreshPlottedDataList(); } + this.metadata = values.metadata; } refreshPlottedDataList() { @@ -69,7 +71,7 @@ define([ } async editPlottedData(data) { - const editor = new PlottedDataEditor(data); + const editor = new PlottedDataEditor(data, this.metadata); const newData = await editor.show(); const index = this.plottedData.findIndex(d => d.id === newData.id); if (index > -1) { diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.hml deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html index 95295b0fb..e8ea0eeeb 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html @@ -15,7 +15,7 @@ - + + (10,5,4)
    diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js index 93818f0bb..ae8c42479 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js @@ -2,16 +2,18 @@ define([ 'underscore', './DataEditorBase', + './PythonSliceParser', 'text!./PlottedDataEditor.html', ], function( _, DataEditorBase, + PythonSliceParser, Html, ) { Html = _.template(Html); class PlottedDataEditor extends DataEditorBase { - constructor(plottedData) { + constructor(plottedData, dataShapes) { const isNewData = !plottedData; const title = isNewData ? `Add data to figure` : `Edit "${plottedData.name}"`; // FIXME: fix capitalization? @@ -26,10 +28,17 @@ define([ } this.$update = this.$el.find('.btn-primary'); + const onDataUpdate = _.debounce(() => this.onPythonDataUpdate(), 250); + this.dataShapes = dataShapes; + this.$el.find('#dataSlice').on('input', onDataUpdate); + this.$el.find('#data').on('change', onDataUpdate); + + this.$dataDims = this.$el.find('#dataDims'); } async show() { this.$el.modal('show'); + this.onPythonDataUpdate(); return new Promise(resolve => { this.$update.on('click', event => { this.$el.modal('hide'); @@ -45,6 +54,58 @@ define([ data.id = this.id; return data; } + + onPythonDataUpdate() { + try { + const shape = this.getPythonDataShape(); + const displayShape = `(${shape.join(', ')})`; + this.$dataDims.text(displayShape); + this.$elements.dataSlice.parent().removeClass('has-error'); + } catch (err) { + this.$elements.dataSlice.parent().addClass('has-error'); + this.$dataDims.text(err.message); + } + } + + findMetadataEntry(metadata, varName) { + if (varName === metadata.name) { + return metadata; + } + + const isDict = !!metadata.entries; + if (isDict && varName.startsWith(metadata.name)) { + const nextEntry = metadata.entries + .find(md => { + const {name} = md; + const entryVarName = `${metadata.name}['${name}']`; + + return varName.startsWith(entryVarName); + }); + + const relVarName = varName + .replace(`${metadata.name}['${nextEntry.name}']`, nextEntry.name); + return this.findMetadataEntry(nextEntry, relVarName); + } + } + + getInitialDataShape(nameString) { + const metadata = this.dataShapes + .map(metadata => this.findMetadataEntry(metadata, nameString)) + .find(md => md); + + if (!metadata) { + throw new Error(`Could not find metadata for ${nameString}`); + } + + return metadata.shape; + } + + getPythonDataShape() { + const {data, dataSlice=''} = this.data(); + const startShape = this.getInitialDataShape(data); + const shape = PythonSliceParser(startShape, dataSlice); + return shape; + } } return PlottedDataEditor; diff --git a/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js index 562d8f26e..9ea1e516f 100644 --- a/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js +++ b/src/visualizers/widgets/DatasetExplorer/PythonSliceParser.js @@ -11,7 +11,7 @@ define([ class InvalidSliceError extends Error { constructor(text) { - super(`Invalid slice string: ${text}`); + super(`Invalid slice string: "${text}"`); } } @@ -28,18 +28,19 @@ define([ this.index = index; } - select(dims) { - this.ensureValidIndex(dims); - return dims.slice(1); + select(dims, position) { + const len = dims.splice(position, 1, null); + this.ensureValidIndex(len); + return dims; } isInRange(len) { return this.index < len; } - ensureValidIndex(dims) { - if (!this.isInRange(dims[0])) { - throw new OutOfRangeError(dims[0], this.index); + ensureValidIndex(len) { + if (!this.isInRange(len)) { + throw new OutOfRangeError(len, this.index); } } } @@ -87,7 +88,8 @@ define([ function ensureValidSliceString(sliceString) { const sliceRegex = /^\[-?[0-9]*:?-?[0-9]*:?-?[0-9]*((,|\]\[)-?[0-9]*:?-?[0-9]*:?-?[0-9]*)?\]$/; - const isValid = sliceRegex.test(sliceString); + const isEmpty = sliceString.length === 0; + const isValid = isEmpty || sliceRegex.test(sliceString); if (!isValid) { throw new InvalidSliceError(sliceString); } @@ -97,16 +99,18 @@ define([ return rawString .replace(/(^\[|\]$)/g, '') .split('][') - .flatMap(chunk => chunk.split(',')); + .flatMap(chunk => chunk.split(',')) + .filter(chunk => !!chunk); } function getSlicedShape(startShape, sliceString) { + sliceString = sliceString.trim(); ensureValidSliceString(sliceString); const slices = getSliceStrings(sliceString).map(Slice.from); return slices.reduce( (shape, slice, position) => slice.select(shape, position), - startShape - ); + startShape.slice() + ).filter(dim => dim !== null); } return getSlicedShape; diff --git a/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js b/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js index f06eaafe6..0388d44d6 100644 --- a/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js +++ b/test/unit/visualizers/widgets/DatasetExplorer/PythonSliceParser.js @@ -3,6 +3,12 @@ describe.only('PythonSliceParser', function() { const SliceParser = requirejs('widgets/DatasetExplorer/PythonSliceParser'); const assert = require('assert').strict; + it('should return start shape if passed blank string', function() { + const shape = [10, 5]; + const newShape = SliceParser(shape, ''); + assert.deepEqual(newShape, shape); + }); + describe('indices', function() { it('should remove first dimension using "[0]"', function() { const shape = [10, 5]; @@ -68,5 +74,10 @@ describe.only('PythonSliceParser', function() { }); describe('slices and indices', function() { + it('should compute dimensions using "[:,0]"', function() { + const shape = [100, 1]; + const newShape = SliceParser(shape, '[:,0]'); + assert.deepEqual(newShape, [100]); + }); }); }); From 5a2bb74439e9e5cd30f6cc118ddbaa10ab950fbb Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 15:27:53 -0500 Subject: [PATCH 10/59] Update figure updates given the figure data --- .../DatasetExplorer/DatasetExplorerWidget.js | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index 2839f6d8d..f13572008 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -71,8 +71,8 @@ define([ await this.session.addArtifact(name, dataInfo, desc.type, config); } - async getYValues (desc) { - const name = desc.name.replace(/[^a-zA-Z_]/g, '_'); + async getYValues (data) { + //const name = data.name.replace(/[^a-zA-Z_]/g, '_'); return [1,2,3,4]; await this.importDataToSession(desc); @@ -102,17 +102,15 @@ define([ }; } - async getPlotData (desc) { - return [ - { - y: await this.getYValues(desc), - boxpoints: 'all', - jitter: 0.3, - pointpos: -1.8, - name: `${desc.name}['y']`, - type: 'box' - } - ]; + async getPlotData (line) { + return { + y: await this.getYValues(line), + boxpoints: 'all', + jitter: 0.3, + pointpos: -1.8, + name: line.name, + type: 'box' + }; } onWidgetContainerResize (/*width, height*/) { @@ -124,9 +122,12 @@ define([ return {title}; } - setLayout(newVals) { - this.layout = newVals; - this.onPlotUpdated(); + async updatePlot (figureData) { + const layout = _.pick(figureData, ['title', 'xaxis', 'yaxis']); + const data = await Promise.all( + figureData.data.map(data => this.getPlotData(data)) + ); + Plotly.newPlot(this.$plot[0], data, layout); } onPlotUpdated () { @@ -135,21 +136,18 @@ define([ // Adding/Removing/Updating items async addNode (desc) { + // TODO: update the loading messages + // - loading data? + // - prompt about the type of compute to use? + // TODO: start loading messages this.nodeId = desc.id; // TODO: Use a different method of storing what to plot - this.plotData = await this.getPlotData(desc); const isStillShown = this.nodeId === desc.id; // getMetadata if (isStillShown) { this.layout = this.defaultLayout(desc); const data = _.extend({}, this.layout); data.plottedData = [ // FIXME: remove this - { - id: 123, - name: 'Example Data', - data: `combined_dataset['y']`, - dataSlice: '[:,0]', - } ]; data.metadata = [await this.getMetadata(desc)]; this.plotEditor.set(data); From 6be4a6f83140002d46c66a8d64d8165d97a60612 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 16:57:33 -0500 Subject: [PATCH 11/59] Add explorer_helpers.py for DatasetExplorer --- .../widgets/DatasetExplorer/files/explorer_helpers.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/visualizers/widgets/DatasetExplorer/files/explorer_helpers.py diff --git a/src/visualizers/widgets/DatasetExplorer/files/explorer_helpers.py b/src/visualizers/widgets/DatasetExplorer/files/explorer_helpers.py new file mode 100644 index 000000000..8d9ea3ee7 --- /dev/null +++ b/src/visualizers/widgets/DatasetExplorer/files/explorer_helpers.py @@ -0,0 +1,9 @@ +def metadata(name, data): + info = {} + info['name'] = name + if type(data) is dict: + info['entries'] = [metadata(k, v) for (k, v) in data.items()] + else: + info['shape'] = data.shape + + return info From 1c5eb58a4f039ffa892772be9eb007e98567c66a Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 16:58:31 -0500 Subject: [PATCH 12/59] Add explorer helpers to session for getting metadata --- .../DatasetExplorer/DatasetExplorerWidget.js | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js index f13572008..090bc4116 100644 --- a/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js +++ b/src/visualizers/widgets/DatasetExplorer/DatasetExplorerWidget.js @@ -5,14 +5,18 @@ define([ 'deepforge/compute/interactive/session', 'widgets/PlotlyGraph/lib/plotly.min', './PlotEditor', + './ArtifactLoader', 'underscore', + 'text!./files/explorer_helpers.py', 'css!./styles/DatasetExplorerWidget.css', ], function ( Storage, Session, Plotly, PlotEditor, + ArtifactLoader, _, + HELPERS_PY, ) { 'use strict'; @@ -22,24 +26,31 @@ define([ constructor(logger, container) { this._logger = logger.fork('Widget'); + // TODO: Prompt for compute info + this.session = new Session('local'); + this.$el = container; this.$el.addClass(WIDGET_CLASS); const row = $('
    ', {class: 'row'}); this.$el.append(row); this.$plot = $('
    ', {class: 'plot col-9'}); - this.$plotEditor = $('
    ', {class: 'plot-editor col-3'}); - this.plotEditor = new PlotEditor(this.$plotEditor); + + const rightPanel = $('
    ', {class: 'col-3'}); + const $plotEditor = $('
    ', {class: 'plot-editor'}); + this.plotEditor = new PlotEditor($plotEditor); this.plotEditor.on('update', plotData => { this.updatePlot(plotData); }); + const $artifactLoader = $('
    ', {class: 'artifact-loader'}); + this.artifactLoader = new ArtifactLoader($artifactLoader, this.session); row.append(this.$plot); - row.append(this.$plotEditor); - - this.session = new Session('local'); - this.nodeId = null; + rightPanel.append($plotEditor); + //rightPanel.append($artifactLoader); + row.append(rightPanel); + // TODO: start loading message... this._logger.debug('ctor finished'); } @@ -61,6 +72,8 @@ define([ async importDataToSession (desc) { await this.session.whenConnected(); + await this.session.addFile('utils/explorer_helpers.py', HELPERS_PY); + console.log('adding file...'); // TODO: Ask if we should load the data? const dataInfo = JSON.parse(desc.data); @@ -71,35 +84,30 @@ define([ await this.session.addArtifact(name, dataInfo, desc.type, config); } - async getYValues (data) { + async getYValues (lineInfo) { //const name = data.name.replace(/[^a-zA-Z_]/g, '_'); return [1,2,3,4]; - await this.importDataToSession(desc); + const {data} = lineInfo; const command = [ - `from artifacts.${name} import data`, + `from artifacts.${data} import data as ${data}`, 'import json', - 'print(json.dumps([l[0] for l in data["y"]]))' + `print(json.dumps([l[0] for l in ${data}]))` ].join(';'); const {stdout} = await this.session.exec(`python -c '${command}'`); // TODO: Add error handling return JSON.parse(stdout); } async getMetadata (desc) { - // TODO: Load the data into the current session - return { - name: desc.name, - entries: [ - { - name: 'X', - shape: [7500, 64, 64, 5], - }, - { - name: 'y', - shape: [7500, 1] - } - ] - }; + const {name} = desc; + const command = [ + `from artifacts.${name} import data`, + 'from utils.explorer_helpers import metadata', + 'import json', + `print(json.dumps(metadata("${name}", data)))` + ].join(';'); + const {stdout} = await this.session.exec(`python -c '${command}'`); // TODO: Add error handling + return JSON.parse(stdout); } async getPlotData (line) { @@ -130,29 +138,22 @@ define([ Plotly.newPlot(this.$plot[0], data, layout); } - onPlotUpdated () { - Plotly.newPlot(this.$plot[0], this.plotData, this.layout); - } - // Adding/Removing/Updating items async addNode (desc) { // TODO: update the loading messages // - loading data? // - prompt about the type of compute to use? // TODO: start loading messages - this.nodeId = desc.id; - // TODO: Use a different method of storing what to plot - const isStillShown = this.nodeId === desc.id; - // getMetadata - if (isStillShown) { - this.layout = this.defaultLayout(desc); - const data = _.extend({}, this.layout); - data.plottedData = [ // FIXME: remove this - ]; - data.metadata = [await this.getMetadata(desc)]; - this.plotEditor.set(data); - this.onPlotUpdated(); - } + + await this.importDataToSession(desc); + const layout = this.defaultLayout(desc); + + const data = _.extend({}, layout); + data.plottedData = []; // FIXME: remove this + data.metadata = [await this.getMetadata(desc)]; + this.plotEditor.set(data); + + Plotly.react(this.$plot[0]); // FIXME } removeNode (/*gmeId*/) { From 1d1e42a030500eac1849fc28c1136921871ac3bb Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 16:59:00 -0500 Subject: [PATCH 13/59] dynamically create variable names for dropdown --- .../DatasetExplorer/PlottedDataEditor.html | 5 ++- .../DatasetExplorer/PlottedDataEditor.js | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html index e8ea0eeeb..2295fb4ec 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.html @@ -11,18 +11,17 @@
    - -
    - + (10,5,4)
    diff --git a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js index ae8c42479..60645f422 100644 --- a/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js +++ b/src/visualizers/widgets/DatasetExplorer/PlottedDataEditor.js @@ -1,4 +1,4 @@ -/* globals define */ +/* globals define, $ */ define([ 'underscore', './DataEditorBase', @@ -31,11 +31,39 @@ define([ const onDataUpdate = _.debounce(() => this.onPythonDataUpdate(), 250); this.dataShapes = dataShapes; this.$el.find('#dataSlice').on('input', onDataUpdate); - this.$el.find('#data').on('change', onDataUpdate); + const $dataDropdown = this.$el.find('#data'); + this.setDataOptions($dataDropdown, dataShapes); + $dataDropdown.on('change', onDataUpdate); this.$dataDims = this.$el.find('#dataDims'); } + setDataOptions($data, dataShapes) { + $data.empty(); + const names = dataShapes + .flatMap(md => PlottedDataEditor.getAllVariableNames(md)); + const options = names.map(name => { + const $el = $('