mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 02:30:45 +00:00
302 lines
8.5 KiB
JavaScript
302 lines
8.5 KiB
JavaScript
var d3 = require('d3');
|
|
var _ = require('lodash');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var Node = require('./node');
|
|
|
|
|
|
function determineAllEdges(nodes, allNodes) {
|
|
var edges = [],
|
|
edgeIds = {},
|
|
nodeIds = _.pluck(nodes, 'id');
|
|
|
|
_.each(nodes, function(node) {
|
|
var nodeId = node.id;
|
|
|
|
_.each(node.adjacency, function(adjacent) {
|
|
var edge = [nodeId, adjacent],
|
|
edgeId = edge.join('-');
|
|
|
|
if (!edgeIds[edgeId] && _.contains(nodeIds, nodeId) && _.contains(nodeIds, adjacent)) {
|
|
edges.push({
|
|
id: edgeId,
|
|
value: 5,
|
|
source: allNodes[edge[0]],
|
|
target: allNodes[edge[1]]
|
|
});
|
|
edgeIds[edgeId] = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
return edges;
|
|
}
|
|
|
|
function getAdjacentNodes(nodes, rootNodeIds) {
|
|
var rootNodes = _.compact(_.map(rootNodeIds, function(nodeId) {
|
|
return nodes[nodeId];
|
|
}));
|
|
|
|
var allAdjacentNodeIds = _.union(_.flatten(_.map(rootNodes, function(node) {
|
|
return node.adjacency;
|
|
})), rootNodeIds);
|
|
|
|
return _.compact(_.map(allAdjacentNodeIds, function(nodeId) {
|
|
return nodes[nodeId];
|
|
}));
|
|
}
|
|
|
|
function getAdjacentEdges(nodes, root) {
|
|
return _.map(root.adjacency, function(nodeId) {
|
|
var edge = [root.id, nodeId],
|
|
edgeId = edge.join('-');
|
|
|
|
return {
|
|
id: edgeId,
|
|
value: 10,
|
|
source: nodes[edge[0]],
|
|
target: nodes[edge[1]]
|
|
};
|
|
});
|
|
}
|
|
|
|
function id(d) {
|
|
return d.id;
|
|
}
|
|
|
|
function degree(d) {
|
|
return d.adjacency ? d.adjacency.length : 1;
|
|
}
|
|
|
|
function getChildren(node, allNodes) {
|
|
return _.map(node.adjacency, function(nodeId) {
|
|
return allNodes[nodeId];
|
|
});
|
|
}
|
|
|
|
function dblclick(d) {
|
|
d.fixed = false;
|
|
}
|
|
|
|
function nodeExplorer() {
|
|
|
|
var color = d3.scale.category20();
|
|
|
|
var pie = d3.layout.pie()
|
|
.value(degree);
|
|
|
|
var radius = d3.scale.sqrt()
|
|
.range([1, 8]);
|
|
|
|
var drag = d3.behavior.drag();
|
|
|
|
var dispatcher = new EventEmitter();
|
|
|
|
var nodeLocations = {};
|
|
|
|
var width, height;
|
|
|
|
function circleRadius() {
|
|
return width / 4;
|
|
}
|
|
|
|
function radialLayout(centerNode, nodes, radius) {
|
|
var slices = pie(_.sortBy(_.filter(nodes, function(node) {
|
|
return !node.layedout && _.contains(centerNode.adjacency, node.id);
|
|
}), 'id'));
|
|
|
|
_.each(slices, function(slice) {
|
|
var previousXY = nodeLocations[slice.data.id];
|
|
|
|
slice.data.x = centerNode.x + circleRadius() * Math.sin((slice.startAngle + slice.endAngle) / 2);
|
|
slice.data.y = centerNode.y + circleRadius() * Math.cos((slice.startAngle + slice.endAngle) / 2);
|
|
slice.data.layedout = true;
|
|
// nodeLocations[slice.data.id] = [slice.data.forceX, slice.data.forceY];
|
|
});
|
|
}
|
|
|
|
|
|
function chart(selection) {
|
|
selection.each(function(data) {
|
|
var allNodes = data.nodes;
|
|
var root = allNodes[data.root];
|
|
var expandedNodeIds = data.expandedNodes;
|
|
|
|
var centerX = width / 2;
|
|
var centerY = height / 2;
|
|
|
|
var nodes = getAdjacentNodes(allNodes, expandedNodeIds);
|
|
|
|
if (root) {
|
|
var previousXY = nodeLocations[root.id];
|
|
root.x = previousXY ? previousXY[0] : centerX;
|
|
root.y = previousXY ? previousXY[1] : centerY;
|
|
root.layedout = true;
|
|
|
|
_.each(nodes, function(node) {
|
|
if (node.id !== root.id) {
|
|
node.layedout = false;
|
|
}
|
|
});
|
|
|
|
_.each(expandedNodeIds, function(nodeId, i) {
|
|
var centerNode = allNodes[nodeId];
|
|
if (centerNode) {
|
|
radialLayout(centerNode, nodes, i ? circleRadius() / 4 : circleRadius());
|
|
}
|
|
});
|
|
|
|
if (nodes.length == 0) {
|
|
nodes.push(root);
|
|
}
|
|
}
|
|
|
|
var edges = determineAllEdges(nodes, allNodes);
|
|
|
|
// Select the svg element, if it exists.
|
|
var svg = d3.select(this).selectAll("svg").data([data]);
|
|
|
|
// Otherwise, create the skeletal chart.
|
|
var gEnter = svg.enter().append("svg")
|
|
.attr('width', "100%")
|
|
.attr('height', "100%");
|
|
|
|
gEnter.append('g')
|
|
.classed('links', true);
|
|
gEnter.append('g')
|
|
.classed('nodes', true);
|
|
|
|
var link = svg.select('.links').selectAll(".link")
|
|
.data(edges, id);
|
|
link.exit()
|
|
.remove()
|
|
link.enter().append("line")
|
|
.attr("class", "link")
|
|
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
|
|
|
|
var node = svg.select('.nodes').selectAll(".node")
|
|
.data(nodes, id);
|
|
|
|
node.exit()
|
|
.remove();
|
|
|
|
gEnter = node.enter().append("g")
|
|
.attr("class", "node")
|
|
.on("dblclick", dblclick)
|
|
.on('click', function(d) {
|
|
if (d3.event.defaultPrevented) {
|
|
return; // click suppressed
|
|
}
|
|
dispatcher.emit('node.click', d);
|
|
})
|
|
.call(drag);
|
|
|
|
gEnter.append("circle")
|
|
.attr('class', 'outer')
|
|
.style("fill", function(d) {
|
|
return color(d.label_major);
|
|
})
|
|
.attr("r", function(d) {
|
|
return radius(degree(d)) + 4;
|
|
});
|
|
|
|
gEnter.append("circle")
|
|
.style("fill", function(d) {
|
|
return color(d.label_major);
|
|
})
|
|
.attr("r", function(d) {
|
|
return radius(degree(d));
|
|
});
|
|
|
|
gEnter.append("text")
|
|
.attr('class', 'label-major')
|
|
.attr("dy", "-.25em")
|
|
.text(function(d) { return d.label_major; });
|
|
|
|
gEnter.append("text")
|
|
.attr('class', 'label-minor')
|
|
.attr("dy", ".75em")
|
|
.text(function(d) { return d.label_minor; });
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
node
|
|
.classed('node-root', function(d) {
|
|
return d.id === root.id;
|
|
})
|
|
.classed('node-expanded', function(d) {
|
|
return _.contains(expandedNodeIds, d.id);
|
|
})
|
|
.classed('node-leaf', function(d) {
|
|
return _.size(d.adjacency) === 0;
|
|
});
|
|
|
|
function updatePositions() {
|
|
link.attr("x1", function(d) { return d.source.x; })
|
|
.attr("y1", function(d) { return d.source.y; })
|
|
.attr("x2", function(d) { return d.target.x; })
|
|
.attr("y2", function(d) { return d.target.y; });
|
|
|
|
node.attr("transform", function(d) {
|
|
return "translate(" + d.x + "," + d.y + ")";
|
|
});
|
|
|
|
node.selectAll('text')
|
|
.attr("dx", function(d) { return d.x > centerX ? radius(degree(d)) + 6 : - radius(degree(d)) - 6})
|
|
.attr("text-anchor", function(d) { return d.x > centerX ? "start" : "end"; });
|
|
}
|
|
|
|
var density = Math.sqrt(nodes.length / (width * height));
|
|
|
|
drag.on('drag', function(d) {
|
|
d.x = d3.event.x;
|
|
d.y = d3.event.y;
|
|
updatePositions();
|
|
});
|
|
|
|
updatePositions();
|
|
});
|
|
}
|
|
|
|
|
|
chart.create = function(el, state) {
|
|
d3.select(el)
|
|
.datum(state)
|
|
.call(chart);
|
|
|
|
return chart;
|
|
};
|
|
|
|
chart.update = _.throttle(function(el, state) {
|
|
d3.select(el)
|
|
.datum(state)
|
|
.call(chart);
|
|
|
|
return chart;
|
|
}, 500);
|
|
|
|
chart.on = function(event, callback) {
|
|
dispatcher.on(event, callback);
|
|
};
|
|
|
|
chart.teardown = function() {
|
|
force.on('tick', null);
|
|
};
|
|
|
|
chart.width = function(_) {
|
|
if (!arguments.length) return width;
|
|
width = _;
|
|
return chart;
|
|
};
|
|
|
|
chart.height = function(_) {
|
|
if (!arguments.length) return height;
|
|
height = _;
|
|
return chart;
|
|
};
|
|
|
|
return chart;
|
|
}
|
|
|
|
module.exports = nodeExplorer; |