Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/common/viz/FigureExtractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ define(['./Utils'], function (Utils) {
SUB_GRAPH: 'SubGraph',
IMAGE: 'Image',
LINE: 'Line',
SCATTER_POINTS: 'ScatterPoints'
};

FigureExtractor.prototype._initializeMetaNodesMap = function () {
Expand All @@ -19,7 +20,11 @@ define(['./Utils'], function (Utils) {

FigureExtractor.prototype.extract = function(node) {
const extractorFn = this.getMetaType(node);
return this[extractorFn](node);
if (!Object.values(EXTRACTORS).includes(extractorFn)){
throw new Error(`Node of type ${extractorFn} is not supported yet.`);
} else {
return this[extractorFn](node);
}
};

FigureExtractor.prototype.constructor = FigureExtractor;
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/plugins/ExecuteJob/metadata/Figure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand Down Expand Up @@ -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';
}
Expand Down
166 changes: 163 additions & 3 deletions src/plugins/GenerateJob/templates/backend_deepforge.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@

from __future__ import (absolute_import, division, print_function,
unicode_literals)
import math
import base64

import io
import itertools
import six

import numpy as np
Expand All @@ -75,10 +77,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.
Expand Down Expand Up @@ -272,6 +380,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):
Expand All @@ -292,6 +401,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 = {}
Expand Down Expand Up @@ -327,6 +439,54 @@ 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': self.colors_to_hex(styles['facecolor'].tolist()),
'points': offsets.tolist(),
'marker': '.', #TODO: Detect markers from Paths
'label': '',
'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:
Expand Down
Binary file modified src/seeds/pipeline/pipeline.webgmex
Binary file not shown.
2 changes: 1 addition & 1 deletion src/seeds/pipeline/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.0
0.18.0
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/visualizers/panels/PlotlyGraph/PlotlyGraphControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 22 additions & 1 deletion src/visualizers/widgets/PlotlyGraph/PlotlyDescExtractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = {
Expand All @@ -124,6 +144,7 @@ define([], function () {
}
return traceData;
}));

return traceArr;
};

Expand Down