var times, root; var frames = [], ids = []; var times, tStart, tEnd; // the frame with the maximum total size var chartHeight = 100, chartPadding = 10, chartTop = chartHeight + chartPadding; var margin = {top: 0, right: 0, bottom: 0, left: 0}, width = 960, height = 500, diameter = height - chartTop, radius = diameter / 2; var tree = d3.layout.balloon() .size([diameter, diameter]); var attrs = ['r', 'x', 'y', 'value'], attrLen = attrs.length; var svg = d3.select("#vis").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var vis = svg.append("g") .attr("transform", "translate(" + radius + "," + radius + ")"); function computeFrames() { var revalue = d3.layout.hierarchy().revalue, nodes, data, tData; var _computeNodes = tree.nodes; function computeNodes() { nodes = _computeNodes(root); } function cloneNode(node) { var k = -1, clone = {}, attr = null; while (++k < attrLen) { attr = attrs[k]; clone[attr] = node[attr]; } return clone; } function cloneNodes() { var n = nodes.length, clones = [], i = -1, node = null; while (++i < n) { clones.push(cloneNode(nodes[i])); } return clones; } // compute first frame // init node size to 0 if it does not yet exist tree.value(function(d) { return ((data = d.data) && (tData = data[tStart])) ? tData : 0; }); // actually compute the first layout computeNodes(); // extract an id array // (prevents needing to store it for each frame) nodes.forEach(function(d) { ids.push(d._id); }); // set first frame frames[0] = cloneNodes(); // compute the rest of the frames var i = 0; t = times[1], tN = times.length; // get the nodes size at the current time or use the previous // node size if it does the node does not have a size at the // specified time (implying the node size has not changed // since the last time frame) tree.value(function(d) { return ((data = d.data) && (tData = data[t])) ? tData: d.value; }); // compute frame layouts while (++i < tN) { t = times[i]; revalue(root); // revalue the hierarchy with the new t value computeNodes(); frames[i] = cloneNodes(); } } // interpolates each attribute of the passed in nodes // to create a new node function interpolateNode(node0, node1, u, v) { var k = -1, node = {}, attr = null; while (++k < attrLen) { attr = attrs[k]; node[attr] = (node0[attr] * u) + (node1[attr] * v); } return node; } // interpolate between the layouts at t0 and t1 function interpolateNodes(t, i1) { // compute the interpolation parameter // (d3 uses 't' instead of 's', but I've decided to use // 't' to represent time variables, so 'u' it is) var i0 = i1 - 1, t0 = times[i0], t1 = times[i1]; u = (t1 - t) / (t1 - t0); v = 1 - u; var lerps = [], // the interpolated nodes nodes0 = frames[i0], nodes1 = frames[i1], nodes = [], n = nodes0.length, j = -1; node0 = null, node1 = null; while (++j < n) { node0 = nodes0[j]; node1 = nodes1[j]; lerps.push(interpolateNode(node0, node1, u, v)); } return lerps; } // draw the balloon layout at time t var bisect = d3.bisectLeft; function drawBalloonAt(t) { var i = bisect(times, t); console.log(i); if (i > 0) { drawBalloon(interpolateNodes(t, i)); } else { drawBalloon(frames[tStart]); } } // accessor functions function nid(d, i) { return ids[i]; } function nr(d) { return d.r; } function nx(d) { return d.x; } function ny(d) { return d.y; } var nodeCircles; function drawBalloon(nodes) { nodeCircles = vis.selectAll("circle.node") .data(nodes, nid); nodeCircles.enter().append("circle") .attr("class", "node"); d3.selectAll("circle.node") .attr("r", nr) .attr("cx", nx) .attr("cy", ny); } d3.json("./readme.json", function(json) { root = json.root; times = json.times; tStart = times[0]; tEnd = times[times.length - 1]; // compute balloon frames computeFrames(); // draw first balloon frame drawBalloon(frames[tStart]); // draw chart // compute the x scale for both the chart and // interpolating the balloon frames var chartWidth = (width - margin.left - margin.right); var x = d3.scale.linear() .domain([tStart, tEnd]) .range([0, chartWidth]); var yMax = d3.max(frames, function(d) { return d[0].value; }); var y = d3.scale.linear() .domain([0, yMax]) .range([chartHeight - margin.top - margin.bottom, 0]); var area = d3.svg.area() .interpolate('cardinal') .x(function(d, i) { return x(times[i]); }) .y0(0) .y1(function(d) { return -y(d[0].value); }); // root's value var chart = svg.append("g") .datum(frames) .attr("width", width) .attr("height", chartHeight) .attr("transform", "translate(" + 0 + "," + (diameter + chartTop) + ")"); chart.append("path") .attr("class", "area") .attr("d", area); // "draw" hover overlay var overlay = svg.append("rect") .attr("class", "overlay") .attr("x", 0) .attr("y", diameter + chartPadding) .style("fill-opacity", 0) .attr("width", chartWidth) .attr("height", chartHeight) .on("mouseover", function() { var el = d3.select("path.area"), color = el.style("fill"); // darken the chart el.transition() .duration("250") .style("fill", d3.rgb(color).darker()); }) .on("mouseout", function() { var el = d3.select("path.area"), color = el.style("fill"); // lighten the chart el.transition() .duration("250") .style("fill", d3.rgb(color).brighter()); }) .on("mousemove", function() { // interpolate balloon layout at // mouse position and draw drawBalloonAt(x.invert(d3.mouse(this)[0])); }); });