You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
angularjs-batarang/js/directives/d3.js

275 lines
8.4 KiB
JavaScript

// D3 visualization
// TODO: D3 as a service
panelApp.directive('batD3', function () {
return {
restrict: 'E',
terminal: true,
scope: {
val: '=val'
},
link: function (scope, element, attrs) {
// Based on code from: http://mbostock.github.com/d3/talk/20111116/bundle.html
// Initialize Element
// ------------------
var div = d3.select(element[0]);
// Constants
// ---------
var w = 600,
h = 600,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0;
// Helpers
// -------
// generate element ids that do not have '$'
var sanitize = function (key) {
return key.replace('$', 'dollar')
}
// TODO: refactor the data transformation to make it faster
// For instance, build up the ideal structure in inject/degug.js
var packages = {
// Lazily construct the package hierarchy from class names.
root: function(classes) {
var map = {};
// add "classes" with no dependencies
var exist = {},
toAdd = [];
classes.forEach(function (cl) {
exist[cl.name] = true;
});
classes.forEach(function (cl) {
cl.imports.forEach(function (im) {
if (!exist[im]) {
toAdd.push(im);
exist[im] = true;
}
});
});
toAdd.forEach(function (a) {
classes.push({
name: a,
imports: []
});
});
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return map[""];
},
// Return a list of imports for the given array of nodes.
imports: function(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
};
// Instantiate and Style D3 Objects
// --------------------------------
var cluster = d3.layout.cluster().
size([360, ry - 120]).
sort(function(a, b) { return d3.ascending(a.key, b.key); });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial().
interpolate("bundle").
tension(.85).
radius(function(d) { return d.y; }).
angle(function(d) { return d.x / 180 * Math.PI; });
var svg = div.append("svg:svg").
attr("preserveAspectRatio", "xMinYMin meet").
attr("viewBox", [0, 0, w, h].join(' ')).
attr("height", h).
append("svg:g").
attr("transform", "translate(" + rx + "," + ry + ")");
// Render the data whenever "val" changes
// --------------------------------------
scope.$watch('val', function (newVal, oldVal) {
var classes;
if (!newVal || newVal.length === 0) {
svg.selectAll('*').remove();
return;
}
if (oldVal && oldVal.length === newVal.length) {
var changed = false;
for (i = 0; i < oldVal.length; i++) {
if (oldVal[i].name !== newVal[i].name || newVal[i].imports.length !== oldVal[i].imports.length) {
changed = true;
break;
}
}
if (!changed) {
return;
}
}
classes = newVal.slice(0);
classes.sort(function (a, b) {
return .5 - (a.name < b.name);
});
svg.selectAll('*').remove();
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 120).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) { return "link source-" + sanitize(d.source.key) + " target-" + sanitize(d.target.key); })
.attr("d", function(d, i) { return line(splines[i]); });
svg.selectAll("g.node")
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) { return "node-" + sanitize(d.key); })
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
.append("svg:text")
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
.text(function(d) { return d.key; })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
});
/*
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) { return line(splines[i]); });
});
*/
//TODO: decide where to attach these events
/*
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
*/
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");
svg
.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) { return (d.x + rotate) % 360 < 180 ? 8 : -8; })
.attr("text-anchor", function(d) { return (d.x + rotate) % 360 < 180 ? "start" : "end"; })
.attr("transform", function(d) { return (d.x + rotate) % 360 < 180 ? null : "rotate(180)"; });
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + sanitize(d.key))
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + sanitize(d.key))
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + sanitize(d.key))
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + sanitize(d.key))
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + sanitize(d[name].key)).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
}
};
});