Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: export plot to vega editor #714

Merged
merged 4 commits into from Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 68 additions & 1 deletion emperor/support_files/js/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,24 @@ define([
disabled: isLargeDataset
}
}
}
},
'openInVegaEditor': {
name: 'Open in Vega Editor',
icon: 'file-picture-o',
callback: function(key, opts) {
scope.exportToVega();
},
disabled: function(key, opt) {
// Only enable if this is a "vanilla" plot
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine there is a better way to identify when the plot isn't a biplot or a procrustes plot. The reason I want to disable then is because the spec is not aware of non-point glyphs (yet).

if (scope.decViews.scatter.lines.left === null &&
scope.decViews.scatter.lines.right === null &&
scope.decViews.biplot === undefined) {
return false;
}
return true;
},
},

}
});

Expand Down Expand Up @@ -931,5 +948,55 @@ define([
return obj;
};

/**
*
* Helper that posts messages between browser tabs
*
* @private
*
*/
_postMessage = function(url, payload) {
// Shamelessly pulled from https://github.com/vega/vega-embed/
var editor = window.open(url);
var wait = 10000;
var step = 250;
var count = ~~(wait / step);

function listen(e) {
if (e.source === editor) {
count = 0;
window.removeEventListener('message', listen, false);
}
}

window.addEventListener('message', listen, false);

function send() {
if (count <= 0) {
return;
}
editor.postMessage(payload, '*');
setTimeout(send, step);
count -= 1;
}
setTimeout(send, step);
}

/**
*
* Open in Vega editor
*
*/
EmperorController.prototype.exportToVega = function() {
var url = 'https://vega.github.io/editor/';
var spec = this.decViews.scatter._buildVegaSpec();
var payload = {
mode: 'vega',
renderer: 'canvas',
spec: JSON.stringify(spec),
};
_postMessage(url, payload);
};

return EmperorController;
});
28 changes: 28 additions & 0 deletions emperor/support_files/js/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,34 @@ function($, _, util) {
this.axesNames = names.nonNumeric.concat(replacement);
}
}
this._buildAxesLabels();
};

/**
*
* Helper method to build labels for all axes
*
*/
DecompositionModel.prototype._buildAxesLabels = function() {
var axesLabels = [], index, text;
for (index = 0; index < this.axesNames.length; index++) {
// when the labels get too long, it's a bit hard to look at
if (this.axesNames[index].length > 25) {
text = this.axesNames[index].slice(0, 20) + '...';
}
else {
text = this.axesNames[index];
}

// account for custom axes (their percentage explained will be -1 to
// indicate that this attribute is not meaningful).
if (this.percExpl[index] >= 0) {
text += ' (' + this.percExpl[index].toPrecision(4) + ' %)';
}

axesLabels.push(text);
}
this.axesLabels = axesLabels;
};

/**
Expand Down
14 changes: 1 addition & 13 deletions emperor/support_files/js/sceneplotview3d.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,19 +525,7 @@ define([
decomp = this.decViews[firstKey].decomp;

this._dimensionsIterator(function(start, end, index) {
// when the labels get too long, it's a bit hard to look at
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this into the model that way I could pluck it off in the view (I went back and forth on where the rendering code would live a few times, so this might just be a remnant of an old iteration).

if (decomp.axesNames[index].length > 25) {
text = decomp.axesNames[index].slice(0, 20) + '...';
}
else {
text = decomp.axesNames[index];
}

// account for custom axes (their percentage explained will be -1 to
// indicate that this attribute is not meaningful).
if (decomp.percExpl[index] >= 0) {
text += ' (' + decomp.percExpl[index].toPrecision(4) + ' %)';
}
text = decomp.axesLabels[index];

axisLabel = makeLabel(end, text, color);
axisLabel.scale.set(axisLabel.scale.x * scaling,
Expand Down
1 change: 1 addition & 0 deletions emperor/support_files/js/shape-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ define([
_.each(group, function(element) {
idx = element.idx;
scope.markers[idx].geometry = geometry;
scope.markers[idx].userData.shape = shape;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Track the shape label, that way we can translate to a vega shape later.

});
scope.needsUpdate = true;
};
Expand Down
132 changes: 132 additions & 0 deletions emperor/support_files/js/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ DecompositionView.prototype._initBaseView = function() {
mesh.position.set(plottable.coordinates[x], plottable.coordinates[y],
plottable.coordinates[z] || 0);

mesh.userData.shape = 'Sphere'

scope.markers.push(mesh);

if (hasConfidenceIntervals) {
Expand Down Expand Up @@ -828,6 +830,136 @@ DecompositionView.prototype.toggleLabelVisibility = function() {
this.needsUpdate = true;
};

/**
*
* Helper that builds a vega specification off of the current view state
*
* @private
*/
DecompositionView.prototype._buildVegaSpec = function() {
function rgbColor(colorObj) {
var r = colorObj.r * 255;
var g = colorObj.g * 255;
var b = colorObj.b * 255;
return 'rgb(' + r + ',' + g + ',' + b + ')';
}

// Maps THREE.js geometries to vega shapes
var getShape = {
Sphere: 'circle',
Diamond: 'diamond',
Cone: 'triangle-down',
Cylinder: 'square',
Ring: 'circle',
Square: 'square',
Icosahedron: 'cross',
Star: 'cross',
};

function viewMarkersAsVegaDataset(markers) {
var points = [], marker, i;
for (i = 0; i < markers.length; i++) {
marker = markers[i];
if (marker.visible) {
points.push({
id: marker.name,
x: marker.position.x,
y: marker.position.y,
color: rgbColor(marker.material.color),
originalShape: marker.userData.shape,
shape: getShape[marker.userData.shape],
scale: { x: marker.scale.x, y: marker.scale.y, },
opacity: marker.material.opacity,
});
}
}
return points;
};

// This is probably horribly slow on QIITA-scale MD files, probably needs some attention
function plottablesAsMetadata(points, header) {
var md = [], point, row, i, j;
for (i = 0; i < points.length; i++) {
point = points[i];
row = {};
for (j = 0; j < header.length; j++) {
row[header[j]] = point.metadata[j];
}
md.push(row);
}
return md;
}

var scope = this;
var model = scope.decomp;

var axisX = scope.visibleDimensions[0];
var axisY = scope.visibleDimensions[1];

var dimRanges = model.dimensionRanges;
var rangeX = [dimRanges.min[axisX], dimRanges.max[axisX]];
var rangeY = [dimRanges.min[axisY], dimRanges.max[axisY]];

var baseWidth = 800;

return {
'$schema': 'https://vega.github.io/schema/vega/v5.json',
padding: 5,
background: scope.backgroundColor,
config: {
axis: { labelColor: scope.axesColor, titleColor: scope.axesColor, },
title: { color: scope.axesColor, },
},
title: 'Emperor PCoA',
data: [
{ name: 'metadata', values: plottablesAsMetadata(model.plottable, model.md_headers), },
{
name: 'points', values: viewMarkersAsVegaDataset(scope.markers),
transform: [
{
type: 'lookup',
from: 'metadata',
key: model.md_headers[0],
fields: ['id'],
as: ['metadata'],
}
],
},
],
signals: [
{name: 'width', update: baseWidth + ' * ((' + rangeX[1] + ') - (' + rangeX[0] + '))'},
{name: 'height', update: baseWidth + ' * ((' + rangeY[1] + ') - (' + rangeY[0] + '))'}
],
scales: [
{ name: 'xScale', range: 'width', domain: [rangeX[0], rangeX[1]] },
{ name: 'yScale', range: 'height', domain: [rangeY[0], rangeY[1]] }
],
axes: [
{ orient: 'bottom', scale: 'xScale', title: model.axesLabels[axisX], },
{ orient: 'left', scale: 'yScale', title: model.axesLabels[axisY], }
],
marks: [
{
type: 'symbol',
from: {data: 'points'},
encode: {
enter: {
fill: { field: 'color', },
x: { scale: 'xScale', field: 'x', },
y: { scale: 'yScale', field: 'y', },
shape: { field: 'shape', },
size: { signal: 'datum.scale.x * datum.scale.y * 100', },
opacity: { field: 'opacity', },
},
update: {
tooltip: { signal: 'datum.metadata' },
},
},
},
],
};
}

/**
* Helper function to change the opacity of an arrow object.
*
Expand Down