D3Graph WIP

This commit is contained in:
Vincent Fiduccia 2015-04-29 09:49:24 -07:00
parent 65ffd0e504
commit f5780a4936
5 changed files with 158 additions and 140 deletions

View File

@ -10,7 +10,7 @@
"Terminal", "Terminal",
"Prism", "Prism",
"Ui", "Ui",
"cytoscape" "dagreD3"
], ],
"browser" : true, "browser" : true,
"boss" : true, "boss" : true,

View File

@ -59,7 +59,9 @@ app.import('bower_components/bootstrap-multiselect/dist/js/bootstrap-multiselect
app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css'); app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css');
app.import('bower_components/prism/prism.js'); app.import('bower_components/prism/prism.js');
app.import('bower_components/prism/components/prism-yaml.js'); app.import('bower_components/prism/components/prism-yaml.js');
app.import('bower_components/cytoscape/dist/cytoscape.js'); app.import('bower_components/lodash/dist/lodash.js');
app.import('bower_components/cytoscape/lib/dagre.js'); app.import('bower_components/graphlib/dist/graphlib.core.js');
app.import('bower_components/dagre/dist/dagre.core.js');
app.import('bower_components/dagre-d3/dist/dagre-d3.core.js');
module.exports = app.toTree(); module.exports = app.toTree();

View File

@ -1,173 +1,129 @@
import Ember from 'ember'; import Ember from 'ember';
import ThrottledResize from 'ui/mixins/throttled-resize'; import Util from 'ui/utils/util';
//import ThrottledResize from 'ui/mixins/throttled-resize';
export default Ember.View.extend(ThrottledResize,{ export default Ember.View.extend({
cy: null, graphZoom: null,
cyElem: null, graphInner: null,
graphOuter: null,
onResize: function() { graphRender: null,
var cy = this.get('cy'); graph: null,
if ( cy )
{
setTimeout(function() {
cy.fit(null, 20);
},100);
}
},
didInsertElement: function() { didInsertElement: function() {
this._super(); this._super();
var elem = $('<div id="environment-graph"></div>').appendTo('BODY'); var elem = $('<div id="environment-graph"><svg style="width: 100%; height: 100%;"><g/></svg></div>').appendTo('BODY');
elem.css('top', $('MAIN').position().top + $('MAIN').height() + 'px'); elem.css('top', $('MAIN').position().top + $('MAIN').height() + 'px');
this.set('cyElem', elem[0]); this.set('graphElem', elem[0]);
Ember.run.later(this,'graph',250); Ember.run.later(this,'initGraph',100);
},
initGraph: function() {
var outer = d3.select("#environment-graph svg");
var inner = outer.select("g");
var zoom = d3.behavior.zoom().on("zoom", function() {
// inner.attr("transform", "translate(" + d3.event.translate + ")" +
// "scale(" + d3.event.scale + ")");
});
outer.call(zoom);
var g = new dagreD3.graphlib.Graph().setGraph({
rankdir: "TB",
nodesep: 70,
ranksep: 50,
marginx: 30,
marginy: 30
});
var render = new dagreD3.render();
this.setProperties({
graphZoom: zoom,
graphOuter: outer,
graphInner: inner,
graphRender: render,
graph: g
});
this.updateGraph();
}, },
updateGraph: function() { updateGraph: function() {
console.log('starting updateGraph'); var g = this.get('graph');
var cy = this.get('cy');
cy.startBatch();
var services = this.get('context.services'); var services = this.get('context.services');
var relayout = false;
var startNodes = cy.nodes().length;
var startEdges = cy.edges().length;
var expectedNodes = cy.collection();
var expectedEdges = cy.collection();
var unremovedServices = services.filter(function(service) { var unremovedServices = services.filter(function(service) {
return ['removed','purging','purged'].indexOf(service.get('state')) === -1; return ['removed','purging','purged'].indexOf(service.get('state')) === -1;
}); });
unremovedServices.forEach(function(service) { unremovedServices.forEach(function(service) {
var serviceId = service.get('id'); var serviceId = service.get('id');
var node = cy.getElementById(serviceId)[0]; var html = '<h4>'+ Util.escapeHtml(service.get('name')) + '</h4><h6 class="state">' + Util.escapeHtml(Util.ucFirst(service.get('state'))) + '</h6>';
if ( !node )
{ g.setNode(serviceId, {
relayout = true; labelType: "html",
node = cy.add({ label: html,
group: 'nodes', padding: 0,
data: { class: (service.get('state') === 'active' ? 'green' : (service.get('state') === 'inactive' ? 'red' : 'yellow')),
id: serviceId, });
shape: 'ellipse',
},
position: {x: 0, y: 0}
})[0];
}
node.data({
name: service.get('name'),
color: (service.get('state') === 'active' ? '#41c77d' : (service.get('state') === 'inactive' ? '#f00' : '#f3bd24')),
}); });
expectedNodes = expectedNodes.add(node); unremovedServices.forEach(function(service) {
var serviceId = service.get('id');
var edge;
(service.get('consumedservices')||[]).map(function(target) { (service.get('consumedservices')||[]).map(function(target) {
var targetId = target.get('id'); var targetId = target.get('id');
edge = cy.edges().filter(function(i, e) { var color = '#f3bd24';
return e.source().id() === serviceId && e.target().id() === targetId; if ( target.get('state') === 'active' )
})[0];
if ( !edge )
{ {
relayout = true; color = '#41c77d';
edge = cy.add({ }
group: 'edges', else if (target.get('state') === 'inactive' )
data: { {
source: serviceId, color = '#ff0000';
target: targetId,
},
})[0];
} }
edge.data({ g.setEdge(serviceId, targetId, {
color: (target.get('state') === 'active' ? '#41c77d' : (target.get('state') === 'inactive' ? '#f00' : '#f3bd24')), label: '',
arrowhead: 'vee',
lineInterpolate: 'bundle',
class: (target.get('state') === 'active' ? 'green' : (target.get('state') === 'inactive' ? 'red' : 'yellow')),
}); });
expectedEdges = expectedEdges.add(edge);
}); });
}); });
// Remove nodes & edges that shouldn't be there // Remove nodes & edges that shouldn't be there
cy.nodes().not(expectedNodes).remove();
cy.edges().not(expectedEdges).remove(); this.renderGraph();
console.log('end batch');
cy.endBatch();
if ( relayout || startNodes !== cy.nodes().length || startEdges !== cy.edges().length )
{
console.log('layout');
cy.layout();
console.log('end layout');
}
console.log('done updateGraph'); console.log('done updateGraph');
}, },
renderGraph: function() {
console.log('start renderGraph');
var zoom = this.get('graphZoom');
var render = this.get('graphRender');
var inner = this.get('graphInner');
var outer = this.get('graphOuter');
var g = this.get('graph');
inner.call(render, g);
// Zoom and scale to fit
var zoomScale = zoom.scale();
var graphWidth = g.graph().width + 80;
var graphHeight = g.graph().height + 40;
var width = $('#environment-graph').width();
var height = $('#environment-graph').height();
zoomScale = Math.min(width / graphWidth, height / graphHeight);
var translate = [(width/2) - ((graphWidth*zoomScale)/2), (height/2) - ((graphHeight*zoomScale)/2)];
zoom.translate(translate);
zoom.scale(zoomScale);
zoom.event(outer.transition().duration(500));
console.log('done renderGraph');
},
throttledUpdateGraph: function() { throttledUpdateGraph: function() {
Ember.run.throttle(this,'updateGraph',500); Ember.run.throttle(this,'updateGraph',500);
}.observes('context.services.@each.{id,name,state,consumedServicesUpdated}'), }.observes('context.services.@each.{id,name,state,consumedServicesUpdated}'),
graph: function() {
var style = cytoscape.stylesheet()
.selector('node')
.css({
'shape': 'data(shape)',
'content': 'data(name)',
'text-valign': 'center',
'text-outline-width': 2,
'text-outline-color': 'data(color)',
'border-width': 2,
'border-color': '#f4f5f8',
'background-color': 'data(color)',
'font-family': '"Open Sans"',
'font-weight': 300,
'font-size': 13,
'color': '#fff',
})
.selector(':selected')
.css({
'border-width': 3,
'border-color': '#333'
})
.selector('edge')
.css({
'opacity': 0.8,
'width': 'mapData(strength, 70, 100, 2, 6)',
'target-arrow-shape': 'triangle-backcurve',
'source-arrow-shape': 'none',
'line-color': 'data(color)',
'source-arrow-color': 'data(color)',
'target-arrow-color': 'data(color)'
})
.selector('edge.questionable')
.css({
'line-style': 'dotted',
'target-arrow-shape': 'diamond'
})
.selector('.faded')
.css({
'opacity': 0.25,
'text-opacity': 0
});
// End: style
var cy = cytoscape({
container: this.get('cyElem'),
style: style,
userZoomingEnabled: false,
userPanningEnabled: false,
layout: {
name: 'dagre',
animate: false,
padding: 20,
edgeSep: 20,
},
});
this.set('cy', cy);
this.updateGraph();
},
willDestroyElement: function() { willDestroyElement: function() {
this._super(); this._super();
var cy = this.get('cy'); var cy = this.get('cy');
@ -176,7 +132,7 @@ export default Ember.View.extend(ThrottledResize,{
cy.destroy(); cy.destroy();
} }
var elem = this.get('cyElem'); var elem = this.get('graphElem');
if ( elem ) if ( elem )
{ {
$(elem).remove(); $(elem).remove();

View File

@ -6,4 +6,64 @@
bottom: 0px; bottom: 0px;
z-index: 3; z-index: 3;
overflow: hidden; overflow: hidden;
.node {
&.green rect {
stroke: #41c77d;
}
&.yellow rect {
stroke: #f3bd24;
}
&.red rect {
stroke: #ff0000;
}
g div {
width: 100px;
height: 40px;
padding: 10px 5px;
}
.label {
color: black;
display: initial;
padding: initial;
font-size: initial;
font-weight: initial;
line-height: initial;
color: initial;
text-align: center;
white-space: initial;
vertical-align: initial;
border-radius: initial;
}
rect {
fill: #fff;
stroke-width: 3px;
color: black;
}
}
.edgePath {
path.path {
stroke: #333;
fill: none;
stroke-width: 3px;
}
&.green path.path {
stroke: #41c77d;
}
&.yellow path.path {
stroke: #f3bd24;
}
&.red path.path {
stroke: #ff0000;
}
}
} }

View File

@ -20,7 +20,7 @@
"bootstrap-multiselect": "~0.9.10", "bootstrap-multiselect": "~0.9.10",
"zeroclipboard": "~2.2.0", "zeroclipboard": "~2.2.0",
"prism": "gh-pages", "prism": "gh-pages",
"cytoscape": "~2.3.15", "dagre": "~0.7.1",
"dagre": "~0.7.1" "dagre-d3": "~0.4.3"
} }
} }