Files
weave-scope/client/app/scripts/charts/nodes-chart.js
2015-06-12 09:23:32 +02:00

239 lines
5.8 KiB
JavaScript

const _ = require('lodash');
const d3 = require('d3');
const debug = require('debug')('nodes-chart');
const React = require('react');
const timely = require('timely');
const Edge = require('./edge');
const Naming = require('../constants/naming');
const NodesLayout = require('./nodes-layout');
const Node = require('./node');
const MARGINS = {
top: 130,
left: 40,
right: 40,
bottom: 0
};
const NodesChart = React.createClass({
getInitialState: function() {
return {
nodes: {},
edges: {},
nodeScale: 1,
translate: '0,0',
scale: 1,
hasZoomed: false
};
},
componentWillMount: function() {
this.updateGraphState(this.props);
},
componentDidMount: function() {
this.zoom = d3.behavior.zoom()
.scaleExtent([0.1, 2])
.on('zoom', this.zoomed);
d3.select('.nodes-chart')
.call(this.zoom);
},
componentWillReceiveProps: function(nextProps) {
if (this.getTopologyFingerprint(nextProps.nodes) !== this.getTopologyFingerprint(this.props.nodes)) {
this.setState({
nodes: {},
edges: {}
});
}
this.updateGraphState(nextProps);
},
componentWillUnmount: function() {
// undoing .call(zoom)
d3.select('.nodes-chart')
.on('mousedown.zoom', null)
.on('onwheel', null)
.on('onmousewheel', null)
.on('dblclick.zoom', null)
.on('touchstart.zoom', null);
},
getTopologyFingerprint: function(topology) {
const fingerprint = [];
_.each(topology, function(node) {
fingerprint.push(node.id);
if (node.adjacency) {
fingerprint.push(node.adjacency.join(','));
}
});
return fingerprint.join(';');
},
renderGraphNodes: function(nodes, scale) {
return _.map(nodes, function(node) {
const highlighted = _.includes(this.props.highlightedNodeIds, node.id);
return (
<Node
highlighted={highlighted}
onClick={this.props.onNodeClick}
key={node.id}
id={node.id}
label={node.label}
pseudo={node.pseudo}
subLabel={node.subLabel}
scale={scale}
dx={node.x}
dy={node.y}
/>
);
}, this);
},
renderGraphEdges: function(edges) {
return _.map(edges, function(edge) {
const highlighted = _.includes(this.props.highlightedEdgeIds, edge.id);
return (
<Edge key={edge.id} id={edge.id} points={edge.points} highlighted={highlighted} />
);
}, this);
},
render: function() {
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
const transform = 'translate(' + this.state.translate + ')' +
' scale(' + this.state.scale + ')';
return (
<svg width="100%" height="100%" className="nodes-chart">
<g className="canvas" transform={transform}>
<g className="edges">
{edgeElements}
</g>
<g className="nodes">
{nodeElements}
</g>
</g>
</svg>
);
},
initNodes: function(topology, prevNodes) {
const centerX = this.props.width / 2;
const centerY = this.props.height / 2;
const nodes = {};
_.each(topology, function(node, id) {
nodes[id] = prevNodes[id] || {};
// initialize position for new nodes
_.defaults(nodes[id], {
x: centerX,
y: centerY,
textAnchor: 'start'
});
// copy relevant fields to state nodes
_.assign(nodes[id], {
adjacency: node.adjacency,
id: id,
label: node.label_major,
pseudo: node.pseudo,
subLabel: node.label_minor,
degree: _.size(node.adjacency)
});
}, this);
return nodes;
},
initEdges: function(topology, nodes) {
const edges = {};
_.each(topology, function(node) {
_.each(node.adjacency, function(adjacent) {
const edge = [node.id, adjacent];
const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR);
if (!edges[edgeId]) {
const source = nodes[edge[0]];
const target = nodes[edge[1]];
if (!source || !target) {
debug('Missing edge node', edge[0], source, edge[1], target);
}
edges[edgeId] = {
id: edgeId,
value: 1,
source: source,
target: target
};
}
});
}, this);
return edges;
},
updateGraphState: function(props) {
const nodes = this.initNodes(props.nodes, this.state.nodes);
const edges = this.initEdges(props.nodes, nodes);
const expanse = Math.min(props.height, props.width);
const nodeSize = expanse / 2;
const n = _.size(props.nodes);
const nodeScale = d3.scale.linear().range([0, nodeSize / Math.pow(n, 0.7)]);
const timedLayouter = timely(NodesLayout.doLayout);
const graph = timedLayouter(
nodes,
edges,
props.width,
props.height,
nodeScale,
MARGINS
);
debug('graph layout took ' + timedLayouter.time + 'ms');
// adjust layout based on viewport
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
const yFactor = props.height / graph.height;
const zoomFactor = Math.min(xFactor, yFactor);
let zoomScale = this.state.scale;
if (this.zoom && !this.state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
zoomScale = zoomFactor;
// saving in d3's behavior cache
this.zoom.scale(zoomFactor);
}
this.setState({
nodes: nodes,
edges: edges,
nodeScale: nodeScale,
scale: zoomScale
});
},
zoomed: function() {
this.setState({
hasZoomed: true,
translate: d3.event.translate,
scale: d3.event.scale
});
}
});
module.exports = NodesChart;