Skip to content

Instantly share code, notes, and snippets.

@kirjavascript
Last active December 15, 2015 01:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kirjavascript/a8555a26b14f4088bf51 to your computer and use it in GitHub Desktop.
Save kirjavascript/a8555a26b14f4088bf51 to your computer and use it in GitHub Desktop.
"Correctly" Animated Tree

This is my solution (hack) for fixing the incorrectly animated tree.

The issue occurs because animations are interrupted when new data comes from an API at this speed. I know I can queue the data instead but I would prefer to try without doing that (it doesn't look as stuttery).

When loading a new path, d3 seems to give you the position the source node will be upon finishing the animation instead of a transitional value.

My fix for this is to assign the id of each node as an attribute so that when a path needs to load mid animation it can directly pull the position from the node instead of the data position used by d3. This feels ugly.

Does anyone have a better fix?

<!DOCTYPE html>
<meta charset="utf-8">
<title>"Correctly" Animated Tree</title>
<body>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script>
<!-- -->
<script>
window.addEventListener("load", e => {
var timer = setInterval(() => {
mapTree(api.shift());
drawTree();
if(api.length==0) clearInterval(timer);
},250)
})
var data = {
tree: {"depth":0,"path":[],"id":0,"children": []}
};
var tree = d3.layout.tree()
.size([960, 500])
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
var linkContainer = svg.append('g');
var diagonal = d3.svg.diagonal();
var nodeStyle = {
"stroke-width": '3px',
"fill": "black",
"stroke": "black"
}
var linkStyle = {
"stroke": "black",
"fill": "none",
"stroke-opacity": 1,
}
function drawTree() {
// duration
var du = 500;
var nodes = tree.nodes(clone(data.tree));
var node = svg.selectAll(".node")
.data(nodes, d => d.id)
var nodeGroup = node.enter()
.append("g")
.attr("class", "node")
.attr("id", d => 'n'+d.id) // id saved to reference later
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.style(nodeStyle)
nodeGroup.append("circle")
.attr("r", 4)
// update
node.transition()
.duration(du)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
var link = linkContainer.selectAll(".link")
.data(tree.links(nodes), d => d.source.id + "-" + d.target.id );
link.enter().append("path")
.attr("class", "link")
.style(linkStyle)
.attr("d", findSrc)
link.transition()
.duration(du)
.attr("d", diagonal)
link.exit().remove();
node.exit().remove();
}
// hack to fix path source location
function findSrc(d,i) {
var src = d3.select('#n'+d.source.id).attr("transform").match(/translate\((.*?),(.*?)\)/);
var o = {x: src[1]|0, y: src[2]|0};
var o2 = {x: d.target.x, y: d.target.y};
return diagonal({source: o, target: o2});
}
// you can ignore the rest, it's just how I generate the data...
var treeLookup = {};
var treeID = 0;
function mapTree(node) {
node.children = [];
node.id = ++treeID;
var path = node.path;
var obj = data.tree.children;
var lku = treeLookup;
for(var i=0;i<path.length;i++) {
if(lku[path[i]]==null) {
lku[path[i]] = {"__index":obj.length};
if(i!=path.length-1) {
obj.push({"id":++treeID,"path":path.slice(0,i+1),"children":[],"ghost":1})
}
else {
obj.push(clone(node))
}
}
lku = lku[path[i]];
obj = obj[lku.__index].children
}
}
var api = [{"index":0,"path":[],"status":200},
{"index":1,"path":["digital-marketing-services"],"status":200},
{"index":2,"path":["xmlrpc.php"],"status":405},
{"index":3,"path":["seo-marketing"],"status":200},
{"index":4,"path":["social-media-marketing"],"status":200},
{"index":5,"path":["web-design"],"status":200},
{"index":6,"path":["digital-marketing-strategy"],"status":200},
{"index":7,"path":["the-team"],"status":200},
{"index":8,"path":["about-us"],"status":200},
{"index":9,"path":["training"],"status":200},
{"index":10,"path":["contact"],"status":200},
{"index":11,"path":["blog"],"status":200},
{"index":12,"path":["blog","why-our-best-clients-fire-us-within-a-year"],"status":200},
{"index":13,"path":["blog","why-you-dont-need-a-meta-description"],"status":200},
{"index":14,"path":["blog","digital-account-executive-content-social-pr-job-bristol"],"status":200},
{"index":15,"path":["blog","the-art-of-the-emoji-using-emojis-for-businesses"],"status":200},
{"index":16,"path":["blog","category","blogs"],"status":200},
{"index":18,"path":["blog","category","social-media-advice"],"status":200},
{"index":19,"path":["blog","category","social-media-marketing"],"status":200},
{"index":20,"path":["blog","category","funnies"],"status":200},
{"index":21,"path":["blog","internet-slang-dictionary-2015"],"status":200},
{"index":22,"path":["blog","category","news-tag"],"status":200},
{"index":23,"path":["blog","google-analytics-data-protection-at-city-of-bath-college"],"status":200},
{"index":24,"path":["blog","author","natalienoisylittlemonkey-com"],"status":200},
{"index":26,"path":["blog","category","talks"],"status":200},
{"index":27,"path":["blog","author","nicnoisylittlemonkey-com"],"status":200},
{"index":28,"path":["blog","ghost-referrer-spam"],"status":200},
{"index":29,"path":["blog","google-analytics-data-protection-at-city-nary-2015"],"status":404},
{"index":32,"path":["blog","category","reporting-advice"],"status":200},
{"index":33,"path":["blog","author","stevennoisylittlemonkey-com"],"status":200},
{"index":34,"path":["blog","social-media-for-beginners"],"status":200},
{"index":35,"path":["blog","author","jonoisylittlemonkey-com"],"status":200},
{"index":36,"path":["blog","10-of-the-best-lorem-ipsums"],"status":200}];
function clone(data) {
return JSON.parse(JSON.stringify(data));
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment