From 2044fb0a7e5df93291320aca652089640612d675 Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Tue, 24 Mar 2020 14:03:11 -0500 Subject: [PATCH 1/3] WIP- #1394: Update Pipeline seed and create metadata node 1. Update pipeline seed to incorporate scatterPoints (0.18.0) 2. Create metadata nodes for scatter plot support 3. Update backend_deepforge.py to include PathCollections --- src/plugins/ExecuteJob/metadata/Figure.js | 16 ++ .../templates/backend_deepforge.py | 150 +++++++++++++++++- src/seeds/pipeline/pipeline.webgmex | Bin 28371 -> 29362 bytes src/seeds/pipeline/version.txt | 2 +- 4 files changed, 164 insertions(+), 4 deletions(-) diff --git a/src/plugins/ExecuteJob/metadata/Figure.js b/src/plugins/ExecuteJob/metadata/Figure.js index c422e7345..5ee9cf0ef 100644 --- a/src/plugins/ExecuteJob/metadata/Figure.js +++ b/src/plugins/ExecuteJob/metadata/Figure.js @@ -21,6 +21,7 @@ define([ this.core.setAttribute(axesNode, 'ylim', axes.ylim); this.addAxesLines(axesNode, this.node, axes); this.addAxesImage(axesNode, this.node, axes); + this.addAxesScatterPoints(axesNode, this.node, axes); }); } @@ -59,6 +60,21 @@ define([ }); } + addAxesScatterPoints (parent, job, axes) { + axes.scatterPoints.forEach(scatterPoint => { + const scatterPointsNode = this.core.createNode({ + parent: parent, + base: this.META.ScatterPoints, + }); + this.core.setAttribute(scatterPointsNode, 'color', scatterPoint.color); + this.core.setAttribute(scatterPointsNode, 'label', scatterPoint.label); + this.core.setAttribute(scatterPointsNode, 'marker', scatterPoint.marker); + const points = scatterPoint.points.map(pts => pts.join(',')).join(';'); + this.core.setAttribute(scatterPointsNode, 'points', points); + this.core.setAttribute(scatterPointsNode, 'width', scatterPoint.width); + }); + } + static getCommand() { return 'PLOT'; } diff --git a/src/plugins/GenerateJob/templates/backend_deepforge.py b/src/plugins/GenerateJob/templates/backend_deepforge.py index 58d742c66..cba07ae17 100644 --- a/src/plugins/GenerateJob/templates/backend_deepforge.py +++ b/src/plugins/GenerateJob/templates/backend_deepforge.py @@ -65,7 +65,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import base64 - +import io +import itertools import six import numpy as np @@ -75,10 +76,116 @@ FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) from matplotlib.figure import Figure from matplotlib.colors import to_hex -from matplotlib.collections import LineCollection - +from matplotlib import transforms, collections +from matplotlib.collections import LineCollection, PathCollection +from matplotlib.path import Path +from matplotlib.pyplot import gcf, close import simplejson as json +# The following functions are used as they are from the mplexporter library +# Available at: https://github.com/mpld3/mplexporter +PATH_DICT = {Path.LINETO: 'L', + Path.MOVETO: 'M', + Path.CURVE3: 'S', + Path.CURVE4: 'C', + Path.CLOSEPOLY: 'Z'} + +def SVG_path(path, transform=None, simplify=False): + """Construct the vertices and SVG codes for the path + + Parameters + ---------- + path : matplotlib.Path object + + transform : matplotlib transform (optional) + if specified, the path will be transformed before computing the output. + + Returns + ------- + vertices : array + The shape (M, 2) array of vertices of the Path. Note that some Path + codes require multiple vertices, so the length of these vertices may + be longer than the list of path codes. + path_codes : list + A length N list of single-character path codes, N <= M. Each code is + a single character, in ['L','M','S','C','Z']. See the standard SVG + path specification for a description of these. + """ + if transform is not None: + path = path.transformed(transform) + + vc_tuples = [(vertices if path_code != Path.CLOSEPOLY else [], + PATH_DICT[path_code]) + for (vertices, path_code) + in path.iter_segments(simplify=simplify)] + + if not vc_tuples: + # empty path is a special case + return np.zeros((0, 2)), [] + else: + vertices, codes = zip(*vc_tuples) + vertices = np.array(list(itertools.chain(*vertices))).reshape(-1, 2) + return vertices, list(codes) + +def process_transform(transform, ax=None, data=None, return_trans=False, + force_trans=None): + """Process the transform and convert data to figure or data coordinates + + Parameters + ---------- + transform : matplotlib Transform object + The transform applied to the data + ax : matplotlib Axes object (optional) + The axes the data is associated with + data : ndarray (optional) + The array of data to be transformed. + return_trans : bool (optional) + If true, return the final transform of the data + force_trans : matplotlib.transform instance (optional) + If supplied, first force the data to this transform + + Returns + ------- + code : string + Code is either "data", "axes", "figure", or "display", indicating + the type of coordinates output. + transform : matplotlib transform + the transform used to map input data to output data. + Returned only if return_trans is True + new_data : ndarray + Data transformed to match the given coordinate code. + Returned only if data is specified + """ + if isinstance(transform, transforms.BlendedGenericTransform): + warnings.warn("Blended transforms not yet supported. " + "Zoom behavior may not work as expected.") + + if force_trans is not None: + if data is not None: + data = (transform - force_trans).transform(data) + transform = force_trans + + code = "display" + if ax is not None: + for (c, trans) in [("data", ax.transData), + ("axes", ax.transAxes), + ("figure", ax.figure.transFigure), + ("display", transforms.IdentityTransform())]: + if transform.contains_branch(trans): + code, transform = (c, transform - trans) + break + + if data is not None: + if return_trans: + return code, transform.transform(data), transform + else: + return code, transform.transform(data) + else: + if return_trans: + return code, transform + else: + return code + class RendererTemplate(RendererBase): """ The renderer handles drawing/rendering operations. @@ -272,6 +379,7 @@ def figure_to_state(self): axes_data['ylim'] = axes.get_ylim() axes_data['lines'] = [] axes_data['images'] = [] + axes_data['scatterPoints'] = [] # Line Data for i, line in enumerate(axes.lines): @@ -292,6 +400,9 @@ def figure_to_state(self): if isinstance(collection, LineCollection): axes_data['lines'].extend(self.process_line_collection(collection)) + if isinstance(collection, PathCollection): + axes_data['scatterPoints'].append(self.process_collection(axes, collection, force_pathtrans=axes.transAxes)) + # Image data for i, image in enumerate(axes.images): imageDict = {} @@ -327,6 +438,39 @@ def process_line_collection(self, collection): line_collections.append(line_collection_data) return line_collections + def process_collection(self, ax, collection, + force_pathtrans=None, + force_offsettrans=None): + fig = gcf() + fig.savefig(io.BytesIO(), format='png', dpi=fig.dpi) + close(fig) + + (transform, transOffset, + offsets, paths) = collection._prepare_points() + offset_coords, offsets = process_transform( + transOffset, ax, offsets, force_trans=force_offsettrans) + processed_paths = [SVG_path(path) for path in paths] + processed_paths = [(process_transform( + transform, ax, path[0], force_trans=force_pathtrans)[1], path[1]) + for path in processed_paths] + path_transforms = collection.get_transforms() + styles = {'linewidth': collection.get_linewidths(), + 'facecolor': collection.get_facecolors(), + 'edgecolor': collection.get_edgecolors(), + 'alpha': collection._alpha, + 'zorder': collection.get_zorder()} + + offset_dict = {"data": "before", + "screen": "after"} + offset_order = offset_dict[collection.get_offset_position()] + return { + 'color': styles['facecolor'].tolist(), + 'points': offsets.tolist(), + 'marker': '.', #TODO: Detect markers from Paths + 'label': '', + 'width': styles['linewidth'].tolist() + } + def umask_b64_encode(self, masked_array): # Unmask invalid data if present if masked_array.mask: diff --git a/src/seeds/pipeline/pipeline.webgmex b/src/seeds/pipeline/pipeline.webgmex index 3bf96817a0eff737657e99810408479300285a3e..5a03610a591b47740921085556a10dc4fd96facd 100644 GIT binary patch delta 1255 zcmb7EO-vI(6z-M+O-K`olwgCGX`(eOWp`(PW@{v92w<9MMTrq(>dfqnEwmKc20|L- z;6aa)IeFkDC*wigvj-B-UOba{@xTp_+PGVOBC`w zv7tyj5{bNRt?wkpGA_3QB0SIKJ|zL8wja2H5K1USq~&AAu*tEp^R%!`NMJhSOQtLfXMVB@H78 zfX-tM1rCcj3|rV?wr!il@@(QdK$vz-NrV{)ZVA_O9h18RTT*zwWkR}z3AH>4&4p{) zmuN<~u8lqFQWsOpm&D-2w<&e7<1i9n$vomY+$Z6a*emT?*SgjIj33VS-MSFd(L`Q- zPv(Yy&PW9 zNO&;3*AvB7_#&mp<2vd*rq&QjIJTvuO_Xy9+1gU~?pzNK(PE-sM<-I$DoPJn#OtJ3 zemG@l+V+1;x>#I?9?YzjHDK;}z+KNCnroqAIcV3I4yHafd`YG4gs)8&&2%e5{hUmO z6ZY45w%av=&}o!Y#n>1=C81l(wE4DeQ-HC+C^85LlSER?r3hR@5Zi_bP^{3ZfY$DF z+H8t?p;lf2-|J|#19Av(g)&ir9Dr3?Up}z|U|y@#+RjMN%oy;&%|WH;Vru6DT&bu>8~-j>H2__kwlRh*wvlcf<2 utIss8K9pkN^U`temF)ZCV5T04v`-?7IDAz~{cL2%7D<6NAspI1f#rkJ2Y zXaP8agks4kW56iYl!p{VLKMV=^7G|%9aNOhg2j?bKnzK&l3cDtL^Ki+mXSa#Cc>}z z4YAiydw%`E--0!j?dR>1UoC3p*UVe%=x~*SN`NN}X8Y#bmQ%MdxY?PnnQby)PujwEbJRWO zGW*hfRq`;g&AwXM;K{n}9bBxK8}vO~G_h}{S9)we+46#-5_8z-vX9YIxiF}?A0M+H z6W2j;#jnTf>Zfb)Y=?(Jld7s+*!XH4a%VPfCpR7Ex6&!s8!i34QTo3#&E2r=I6rZm EA7F8~S^xk5 diff --git a/src/seeds/pipeline/version.txt b/src/seeds/pipeline/version.txt index 07feb8234..47d04a528 100644 --- a/src/seeds/pipeline/version.txt +++ b/src/seeds/pipeline/version.txt @@ -1 +1 @@ -0.17.0 \ No newline at end of file +0.18.0 \ No newline at end of file From 8928d19c19e62079d1c6531da0ad9b929c0b767b Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Wed, 25 Mar 2020 18:28:58 -0500 Subject: [PATCH 2/3] Add inital support for scatter plots. Closes #1394 --- src/common/viz/FigureExtractor.js | 36 +++++++++++++++++- .../templates/backend_deepforge.py | 20 +++++++++- src/seeds/pipeline/pipeline.webgmex | Bin 29362 -> 29365 bytes .../ExecutionIndex/ExecutionIndexControl.js | 4 ++ .../panels/PlotlyGraph/PlotlyGraphControl.js | 2 + .../PlotlyGraph/PlotlyDescExtractor.js | 23 ++++++++++- 6 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/common/viz/FigureExtractor.js b/src/common/viz/FigureExtractor.js index 0b447a558..525fe2439 100644 --- a/src/common/viz/FigureExtractor.js +++ b/src/common/viz/FigureExtractor.js @@ -9,6 +9,7 @@ define(['./Utils'], function (Utils) { SUB_GRAPH: 'SubGraph', IMAGE: 'Image', LINE: 'Line', + SCATTER_POINTS: 'ScatterPoints' }; FigureExtractor.prototype._initializeMetaNodesMap = function () { @@ -19,7 +20,11 @@ define(['./Utils'], function (Utils) { FigureExtractor.prototype.extract = function(node) { const extractorFn = this.getMetaType(node); - return this[extractorFn](node); + if (!_.values(EXTRACTORS).includes(extractorFn)){ + throw new Error(`Node of type ${extractorFn} is not supported yet.`); + } else { + return this[extractorFn](node); + } }; FigureExtractor.prototype.constructor = FigureExtractor; @@ -74,6 +79,9 @@ define(['./Utils'], function (Utils) { desc.images = children.filter(node => this.getMetaType(node) === EXTRACTORS.IMAGE) .map(imageNode => this.extract(imageNode)); + desc.scatterPoints = children.filter(node => this.getMetaType(node) == EXTRACTORS.SCATTER_POINTS) + .map(scatterPointsNode => this.extract(scatterPointsNode)); + return desc; }; @@ -123,6 +131,32 @@ define(['./Utils'], function (Utils) { }; }; + FigureExtractor.prototype[EXTRACTORS.SCATTER_POINTS] = function(node) { + const id = node.getId(), + execId = this.getExecutionId(node); + let points, desc; + + points = node.getAttribute('points').split(';') + .filter(data => !!data) // remove any '' + .map(pair => { + const [x, y] = pair.split(',').map(num => parseFloat(num)); + return {x, y}; + }); + desc = { + id: id, + execId: execId, + subgraphId: this._client.getNode(node.getParentId()).getAttribute('id'), + marker: node.getAttribute('marker'), + name: node.getAttribute('name'), + type: 'scatterPoints', + points: points, + width: node.getAttribute('width'), + color: node.getAttribute('color') + }; + + return desc; + }; + FigureExtractor.prototype.compareSubgraphIDs = function (desc1, desc2) { if (desc1.subgraphId >= desc2.subgraphId) return 1; else return -1; diff --git a/src/plugins/GenerateJob/templates/backend_deepforge.py b/src/plugins/GenerateJob/templates/backend_deepforge.py index cba07ae17..c1c963e5d 100644 --- a/src/plugins/GenerateJob/templates/backend_deepforge.py +++ b/src/plugins/GenerateJob/templates/backend_deepforge.py @@ -64,6 +64,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +import math import base64 import io import itertools @@ -464,13 +465,28 @@ def process_collection(self, ax, collection, "screen": "after"} offset_order = offset_dict[collection.get_offset_position()] return { - 'color': styles['facecolor'].tolist(), + 'color': self.colors_to_hex(styles['facecolor'].tolist()), 'points': offsets.tolist(), 'marker': '.', #TODO: Detect markers from Paths 'label': '', - 'width': styles['linewidth'].tolist() + 'width': self.convert_size_array(collection.get_sizes()) } + def convert_size_array(self, size_array): + size = [math.sqrt(s) for s in size_array] + if len(size) == 1: + return size[0] + else: + return size + + def colors_to_hex(self, colors_list): + hex_colors = [] + for color in colors_list: + hex_colors.append(to_hex(color, keep_alpha=True)) + if len(hex_colors) == 1: + return hex_colors[0] + return hex_colors + def umask_b64_encode(self, masked_array): # Unmask invalid data if present if masked_array.mask: diff --git a/src/seeds/pipeline/pipeline.webgmex b/src/seeds/pipeline/pipeline.webgmex index 5a03610a591b47740921085556a10dc4fd96facd..703819455141a3e551b7f0d1ca7cd37332e21363 100644 GIT binary patch delta 455 zcma)(y-EW?0EG9FcqkGP(Z)(vgh1|K|98(ODTRm}2sSqE-tKWKqUJ}CR2Du!+}hI8 z#=~du87wT*Xl?J=q`qQcU_Kazd7B6C^Prs$nypQ+3;=j}y`Qza9c?%!L|cQ5%xT6l zuCU6Lk(esXge8gL3C4PMmR<@eb0ch?Ihk;QI8!QNS|Deb=2!|zB{iHn7{Nl13O9oL zQ+RDITsE%kDcr4F1|$Eq6#Ii9?l6Z5<;F6F2$|QKgBgz8TAh?%gEnquFJoD_(_z=$=KJOEP?EFD c_lHA&g3$k_BfJwfwgshI<`xsuR#gI_KOdxkbpQYW diff --git a/src/visualizers/panels/ExecutionIndex/ExecutionIndexControl.js b/src/visualizers/panels/ExecutionIndex/ExecutionIndexControl.js index 4fa511bb3..b7d0b99c4 100644 --- a/src/visualizers/panels/ExecutionIndex/ExecutionIndexControl.js +++ b/src/visualizers/panels/ExecutionIndex/ExecutionIndexControl.js @@ -139,6 +139,10 @@ define([ lineClone.label = (lineClone.label || `line${index}`) + ` (${abbr})`; currentSubGraph.lines.push(lineClone); }); + subGraphs[i].scatterPoints.forEach(scatterPoint => { + let scatterClone = JSON.parse(JSON.stringify(scatterPoint)); + currentSubGraph.scatterPoints.push(scatterClone); + }); } // Check if there are more subgraphs let extraSubGraphIdx = consolidatedDesc.subGraphs.length; diff --git a/src/visualizers/panels/PlotlyGraph/PlotlyGraphControl.js b/src/visualizers/panels/PlotlyGraph/PlotlyGraphControl.js index 31321ad82..7232eef0b 100644 --- a/src/visualizers/panels/PlotlyGraph/PlotlyGraphControl.js +++ b/src/visualizers/panels/PlotlyGraph/PlotlyGraphControl.js @@ -78,9 +78,11 @@ define([ graphNode = node; break; case 'Line': + case 'ScatterPoints': graphNodeId = this._client.getNode(node.getParentId()).getParentId(); graphNode = this._client.getNode(graphNodeId); break; + } if(graphNode) { desc = this.figureExtractor.extract(graphNode); diff --git a/src/visualizers/widgets/PlotlyGraph/PlotlyDescExtractor.js b/src/visualizers/widgets/PlotlyGraph/PlotlyDescExtractor.js index fd725cae2..5487e9555 100644 --- a/src/visualizers/widgets/PlotlyGraph/PlotlyDescExtractor.js +++ b/src/visualizers/widgets/PlotlyGraph/PlotlyDescExtractor.js @@ -22,7 +22,8 @@ define([], function () { /*** Helper Methods For Creating The plotly JSON Reference ***/ const TraceTypes = { SCATTER: 'scatter', - IMAGE: 'image' + IMAGE: 'image', + SCATTER_POINTS: 'scatter' }; const descHasMultipleSubPlots = function (desc) { @@ -111,6 +112,25 @@ define([], function () { } return traceData; }); + traceArr.push(...subGraph.scatterPoints.map(scatterPoint => { + let points = pointsToCartesianArray(scatterPoint.points); + let traceData = { + x: points[0], + y: points[1], + name: 'scatter points', + type: TraceTypes.SCATTER_POINTS, + mode: 'markers', + marker: { + color: scatterPoint.color, + size: scatterPoint.width, + } + }; + if (index !== 0) { + traceData.xaxis = `x${index + 1}`; + traceData.yaxis = `y${index + 1}`; + } + return traceData; + })); traceArr.push(...subGraph.images.map(image => { let traceData = { @@ -124,6 +144,7 @@ define([], function () { } return traceData; })); + return traceArr; }; From 91b2652a3a05332595fa1542acca602ac1c759ab Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Thu, 26 Mar 2020 09:45:34 -0500 Subject: [PATCH 3/3] change _.values to Object.values --- src/common/viz/FigureExtractor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/viz/FigureExtractor.js b/src/common/viz/FigureExtractor.js index 525fe2439..d169bf6db 100644 --- a/src/common/viz/FigureExtractor.js +++ b/src/common/viz/FigureExtractor.js @@ -20,7 +20,7 @@ define(['./Utils'], function (Utils) { FigureExtractor.prototype.extract = function(node) { const extractorFn = this.getMetaType(node); - if (!_.values(EXTRACTORS).includes(extractorFn)){ + if (!Object.values(EXTRACTORS).includes(extractorFn)){ throw new Error(`Node of type ${extractorFn} is not supported yet.`); } else { return this[extractorFn](node);