Skip to content

Instantly share code, notes, and snippets.

@shunpochang
Last active October 10, 2017 20:45
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save shunpochang/66620bad0e6b201f261c to your computer and use it in GitHub Desktop.
Save shunpochang/66620bad0e6b201f261c to your computer and use it in GitHub Desktop.
D3 Bi-directional Drag and Zoom Tree on D3 development

This example shows how to interact with D3.js to create a Bi-Directional Tree (a variation of robschmuecker@7880033)

The tree shows the dependencies related to D3 development:

  • The upward branches are the repos that D3 is dependent on, from a direct dependency to further parent files that these repos were dependent on.

  • The lower branches are repos that are dependent on D3, and the children files that are dependent on these repos.

The main logic to pull these dependencies and generating tree data is in my GitHub repo, where package.json and bower.json files (to include both NPM and Bower installation) are crawled to get the top matching libraries.

Tree interaction and display:

  • To quickly unfold all branches, click on the label "Click to unfold/expand all" twice to first collapse and then expand, and refresh the browser if it does not work properly.

  • Each node text will inlcude the repo's full path followed by the repo's created year in the parenthesis.

  • If a repo has occurred already at an earlier depth level (meaning closer to origin), the text "Recurring" will be in the node name and the node stroke will be thickened to indicate that the node has already appeared and will thus have no future generations.

  • Zooming is only activated by mouse scrolling, and right-click is deactivated for dragging.

For help/questions,

  • Check out other asked questions in the comment section or ask one there (and tag me @shunpochang) to allow reusable responses. Questions asked so far:

    • How to re-align the children center.

    • How to change layout directions.

  • Or contact me (shunpochang@gmail) if I missed your question or comment.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg{
cursor: all-scroll;
}
.centralText{
font: 23spx sans-serif;
fill: #222;
font-weight: bold;
}
.downwardNode circle{
fill: #fff;
stroke: #8b4513;
stroke-width: 2.5px;
}
.upwardNode circle {
fill: #fff;
stroke: #37592b;
stroke-width: 2.5px;
}
.downwardNode text,
.upwardNode text {
font: 12px sans-serif;
font-weight:bold;
}
.downwardLink {
fill: none;
stroke: #8b4513;
stroke-width: 3px;
opacity: 0.2;
}
.upwardLink {
fill: none;
stroke: #37592b;
stroke-width: 3px;
opacity: 0.2;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
/**
* Initialize tree chart object and data loading.
* @param {Object} d3Object Object for d3, injection used for testing.
*/
var treeChart = function(d3Object) {
this.d3 = d3Object;
// Initialize the direction texts.
this.directions = ['upward', 'downward'];
};
/**
* Set variable and draw chart.
*/
treeChart.prototype.drawChart = function() {
// First get tree data for both directions.
this.treeData = {};
var self = this;
d3.json('raw_all_tree_data.json', function(error, allData) {
self.directions.forEach(function(direction) {
self.treeData[direction] = allData[direction];
});
self.graphTree(self.getTreeConfig());
});
};
/**
* Get tree dimension configuration.
* @return {Object} treeConfig Object containing tree dimension size
* and central point location.
*/
treeChart.prototype.getTreeConfig = function() {
var treeConfig = {'margin': {'top': 10, 'right': 5, 'bottom': 0, 'left': 30}}
// This will be the maximum dimensions
treeConfig.chartWidth = (960 - treeConfig.margin.right -
treeConfig.margin.left);
treeConfig.chartHeight = (500 - treeConfig.margin.top -
treeConfig.margin.bottom);
treeConfig.centralHeight = treeConfig.chartHeight / 2;
treeConfig.centralWidth = treeConfig.chartWidth / 2;
treeConfig.linkLength = 100;
treeConfig.duration = 200;
return treeConfig;
};
/**
* Graph tree based on the tree config.
* @param {Object} config Object for chart dimension and central location.
*/
treeChart.prototype.graphTree = function(config) {
var self = this;
var d3 = this.d3;
var linkLength = config.linkLength;
var duration = config.duration;
// id is used to name all the nodes;
var id = 0;
var diagonal = d3.svg.diagonal()
.projection(function(d) {return [d.x, d.y]; });
var zoom = d3.behavior.zoom()
.scaleExtent([0.1, 2])
.on('zoom', redraw);
var svg = d3.select('body')
.append('svg')
.attr('width',
config.chartWidth + config.margin.right + config.margin.left)
.attr('height',
config.chartHeight + config.margin.top + config.margin.bottom)
.on('mousedown', disableRightClick)
.call(zoom)
.on('dblclick.zoom', null);
var treeG = svg.append('g')
.attr('transform',
'translate(' + config.margin.left + ',' + config.margin.top + ')');
treeG.append('text').text('D3 Development Dependency')
.attr('class', 'centralText')
.attr('x', config.centralWidth)
.attr('y', config.centralHeight + 5)
.attr('text-anchor', 'middle');
// Initialize the tree nodes and update chart.
for (var d in this.directions) {
var direction = this.directions[d];
var data = self.treeData[direction];
data.x0 = config.centralWidth;
data.y0 = config.centralHeight;
// Hide all children nodes other than direct generation.
data.children.forEach(collapse);
update(data, data, treeG);
}
/**
* Update nodes and links based on direction data.
* @param {Object} source Object for current chart distribution to identify
* where the children nodes will branch from.
* @param {Object} originalData Original data object to get configurations.
* @param {Object} g Handle to svg.g.
*/
function update(source, originalData, g) {
// Set up the upward vs downward separation.
var direction = originalData['direction'];
var forUpward = direction == 'upward';
var node_class = direction + 'Node';
var link_class = direction + 'Link';
var downwardSign = (forUpward) ? -1 : 1;
var nodeColor = (forUpward) ? '#37592b' : '#8b4513';
// Reset tree layout based on direction, since the downward chart has
// way too many nodes to fit in the screen, while we want a symmetric
// view for upward chart.
var nodeSpace = 50;
var tree = d3.layout.tree().sort(sortByDate).nodeSize([nodeSpace, 0]);
if (forUpward) {
tree.size([config.chartWidth, config.chartHeight]);
}
var nodes = tree.nodes(originalData);
var links = tree.links(nodes);
// Offset x-position for downward to view the left most record.
var offsetX = 0;
if (!forUpward) {
var childrenNodes = originalData[
(originalData.children) ? 'children' : '_children'];
offsetX = d3.min([childrenNodes[0].x, 0]);
}
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = downwardSign * (d.depth * linkLength) + config.centralHeight;
d.x = d.x - offsetX;
// Position for origin node.
if (d.name == 'origin') {
d.x = config.centralWidth;
d.y += downwardSign * 25;
}
});
// Update the node.
var node = g.selectAll('g.' + node_class)
.data(nodes, function(d) {return d.id || (d.id = ++id); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', node_class)
.attr('transform', function(d) {
return 'translate(' + source.x0 + ',' + source.y0 + ')'; })
.style('cursor', function(d) {
return (d.children || d._children) ? 'pointer' : '';})
.on('click', click);
nodeEnter.append('circle')
.attr('r', 1e-6);
// Add Text stylings for node main texts
nodeEnter.append('text')
.attr('x', function(d) {
return forUpward ? -10 : 10;})
.attr('dy', '.35em')
.attr('text-anchor', function(d) {
return forUpward ? 'end' : 'start';})
.text(function(d) {
// Text for origin node.
if (d.name == 'origin') {
return ((forUpward) ?
'Dependency of D3' :
'Files dependent on D3'
) + ' [Click to fold/expand all]';
}
// Text for summary nodes.
if (d.repeated) {
return '[Recurring] ' + d.name;
}
return d.name; })
.style('fill-opacity', 1e-6)
.style({'fill': function(d) {
if (d.name == 'origin') {return nodeColor;}
}})
.attr('transform', function(d) {
if (d.name != 'origin') {return 'rotate(-20)';}
})
;
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')'; });
nodeUpdate.select('circle')
.attr('r', 6)
.style('fill', function(d) {
if (d._children || d.children) {return nodeColor;}
})
.style('fill-opacity', function(d) {
if (d.children) {return 0.35;}
})
// Setting summary node style as class as mass style setting is
// not compatible to circles.
.style('stroke-width', function(d) {
if (d.repeated) {return 5;}
});
nodeUpdate.select('text').style('fill-opacity', 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + source.x + ',' + source.y + ')'; })
.remove();
nodeExit.select('circle')
.attr('r', 1e-6);
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// Update the links.
var link = g.selectAll('path.' + link_class)
.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert('path', 'g')
.attr('class', link_class)
.attr('d', function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr('d', diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
/**
* Tree function to toggle on click.
* @param {Object} d data object for D3 use.
*/
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
}else {
d.children = d._children;
d._children = null;
// expand all if it's the first node
if (d.name == 'origin') {d.children.forEach(expand);}
}
update(d, originalData, g);
}
}
// Collapse and Expand can be modified to include touched nodes.
/**
* Tree function to expand all nodes.
* @param {Object} d data object for D3 use.
*/
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
/**
* Tree function to collapse children nodes.
* @param {Object} d data object for D3 use.
*/
function collapse(d) {
if (d.children && d.children.length != 0) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
/**
* Tree function to redraw and zoom.
*/
function redraw() {
treeG.attr('transform', 'translate(' + d3.event.translate + ')' +
' scale(' + d3.event.scale + ')');
}
/**
* Tree functions to disable right click.
*/
function disableRightClick() {
// stop zoom
if (d3.event.button == 2) {
console.log('No right click allowed');
d3.event.stopImmediatePropagation();
}
}
/**
* Tree sort function to sort and arrange nodes.
* @param {Object} a First element to compare.
* @param {Object} b Second element to compare.
* @return {Boolean} boolean indicating the predicate outcome.
*/
function sortByDate(a, b) {
// Compare the individuals based on participation date
//(no need to compare when there is only 1 summary)
var aNum = a.name.substr(a.name.lastIndexOf('(') + 1, 4);
var bNum = b.name.substr(b.name.lastIndexOf('(') + 1, 4);
// Sort by date, name, id.
return d3.ascending(aNum, bNum) ||
d3.ascending(a.name, b.name) ||
d3.ascending(a.id, b.id);
}
};
var d3GenerationChart = new treeChart(d3);
d3GenerationChart.drawChart();
</script>
{
"downward":{
"direction":"downward",
"name":"origin",
"children":[
{
"name":"qrohlf/trianglify (2014)",
"children":[
{
"name":"gstf/trianglify-wallpaper (2014)",
"children":[]
},
{
"name":"kimar/trianglify-api (2014)",
"children":[]
}
]
},
{
"name":"NathanEpstein/D3xter (2014)",
"children":[]
},
{
"name":"andredumas/techan.js (2014)",
"children":[]
},
{
"name":"jiahuang/d3-timeline (2012)",
"children":[]
},
{
"name":"MinnPost/simple-map-d3 (2013)",
"children":[]
},
{
"name":"benkeen/d3pie (2013)",
"children":[]
},
{
"name":"lithiumtech/li-visualizations (2014)",
"children":[]
},
{
"name":"misoproject/d3.chart (2013)",
"children":[
{
"name":"stucco/ontology-editor (2013)",
"children":[]
},
{
"name":"knownasilya/d3.chart.pie (2014)",
"children":[]
},
{
"name":"chartyjs/charty (2013)",
"children":[]
}
]
},
{
"name":"racker/glimpse.js (2012)",
"children":[]
},
{
"name":"jdarling/d3rrc (2014)",
"children":[]
},
{
"name":"zmaril/d3-bootstrap-plugins (2012)",
"children":[]
},
{
"name":"marmelab/ArcheoloGit (2014)",
"children":[]
},
{
"name":"topheman/topheman-datavisual (2014)",
"children":[]
},
{
"name":"krispo/angular-nvd3 (2014)",
"children":[
{
"name":"AngeloCiffa/ionicTest1 (2014)",
"children":[]
}
]
},
{
"name":"heavysixer/d4 (2014)",
"children":[]
},
{
"name":"alexandersimoes/d3plus (2013)",
"children":[]
},
{
"name":"chinmaymk/angular-charts (2013)",
"children":[
{
"name":"chenop/angular-charts-example (2013)",
"children":[]
}
]
},
{
"name":"turban/d3.slider (2013)",
"children":[]
},
{
"name":"markmarkoh/datamaps (2012)",
"children":[
{
"name":"dmachat/angular-datamaps (2014)",
"children":[]
}
]
},
{
"name":"Caged/d3-tip (2012)",
"children":[
{
"name":"deciob/d3-tip-amd-example (2014)",
"children":[]
}
]
},
{
"name":"enjalot/tributary (2012)",
"children":[
{
"name":"mrdaniellewis/gulp-tributary (2015)",
"children":[]
}
]
},
{
"name":"palantir/plottable (2014)",
"children":[
{
"name":"palantir/plottable-website (2014)",
"children":[]
},
{
"name":"palantir/chartographer (2014)",
"children":[]
}
]
},
{
"name":"esbullington/react-d3 (2014)",
"children":[]
},
{
"name":"cpettitt/dagre-d3 (2013)",
"children":[
{
"name":"andrvb/dagre-d3-umd (2014)",
"children":[]
}
]
},
{
"name":"masayuki0812/c3 (2013)",
"children":[
{
"name":"dentboard/c3-angularjs (2014)",
"children":[]
},
{
"name":"joneshf/purescript-c3 (2014)",
"children":[]
},
{
"name":"zebrajs/c3-line-backbone (2014)",
"children":[]
},
{
"name":"maxklenk/angular-chart (2014)",
"children":[
{
"name":"maxklenk/angular-chart-presentation (2014)",
"children":[]
}
]
},
{
"name":"Glavin001/ember-c3 (2014)",
"children":[
{
"name":"chrism/d3c3 (2014)",
"children":[]
}
]
},
{
"name":"carlosmontes002/ng-c3 (2014)",
"children":[]
},
{
"name":"jgasteiz/moneys (2014)",
"children":[]
},
{
"name":"arunsivasankaran/Simle-C3-Graph-demo (2014)",
"children":[]
},
{
"name":"wasilak/angular-c3-simple (2014)",
"children":[]
},
{
"name":"jettro/c3-angular-sample (2014)",
"children":[]
},
{
"name":"aZerato/c3-sample (2014)",
"children":[]
}
]
},
{
"name":"dc-js/dc.js (2012)",
"children":[
{
"name":"TomNeyland/angular-dc (2013)",
"children":[]
},
{
"name":"TechToThePeople/bdc (2015)",
"children":[]
},
{
"name":"andrewreedy/ember-dc (2014)",
"children":[]
}
]
},
{
"name":"plouc/mozaik (2014)",
"children":[]
},
{
"name":"adnan-wahab/pathgl (2013)",
"children":[]
},
{
"name":"ripple/ripplecharts-frontend (2013)",
"children":[]
},
{
"name":"NickQiZhu/d3-cookbook (2013)",
"children":[]
},
{
"name":"emeeks/d3-carto-map (2014)",
"children":[
{
"name":"lmullen/asch-2015-talk (2014)",
"children":[]
}
]
},
{
"name":"Asymmetrik/leaflet-d3 (2014)",
"children":[]
},
{
"name":"pearson-enabling-technologies/bridle (2013)",
"children":[]
},
{
"name":"Wildhoney/Leaflet.FreeDraw (2014)",
"children":[]
},
{
"name":"interactivethings/d3-grid (2013)",
"children":[]
}
]
},
"upward":{
"direction":"upward",
"name":"origin",
"children":[
{
"name":"tmpvar/jsdom (2010)",
"children":[
{
"name":"dperini/nwmatcher (2009)",
"children":[]
},
{
"name":"request/request (2011)",
"children":[
{
"name":"hapijs/qs (2014)",
"children":[]
},
{
"name":"rvagg/bl (2013)",
"children":[
{
"name":"iojs/readable-stream (2012)",
"children":[
{
"name":"isaacs/inherits (2011)",
"children":[]
},
{
"name":"juliangruber/isarray (2013)",
"children":[]
},
{
"name":"isaacs/core-util-is (2013)",
"children":[]
},
{
"name":"substack/string_decoder (2013)",
"children":[]
},
{
"name":"TooTallNate/util-deprecate (2014)",
"children":[]
}
]
}
]
},
{
"name":"broofa/node-uuid (2010)",
"children":[]
},
{
"name":"isaacs/json-stringify-safe (2013)",
"children":[]
},
{
"name":"mikeal/tunnel-agent (2013)",
"children":[]
},
{
"name":"request/caseless (2013)",
"children":[]
},
{
"name":"rvagg/isstream (2014)",
"children":[]
},
{
"name":"lelandtseng/form-data (2011)",
"children":[]
},
{
"name":"request/oauth-sign (2013)",
"children":[]
},
{
"name":"goinstant/tough-cookie (2011)",
"children":[]
},
{
"name":"jshttp/mime-types (2014)",
"children":[
{
"name":"jshttp/mime-db (2014)",
"children":[]
}
]
},
{
"name":"hueniverse/hawk (2012)",
"children":[
{
"name":"hapijs/hoek (2012)",
"children":[]
},
{
"name":"hapijs/boom (2013)",
"children":[
{
"repeated":true,
"name":"hapijs/hoek (2012)",
"children":[]
}
]
},
{
"name":"hapijs/cryptiles (2013)",
"children":[
{
"repeated":true,
"name":"hapijs/boom (2013)",
"children":[]
}
]
},
{
"name":"hueniverse/sntp (2013)",
"children":[
{
"repeated":true,
"name":"hapijs/hoek (2012)",
"children":[]
}
]
}
]
},
{
"name":"request/forever-agent (2013)",
"children":[]
}
]
},
{
"name":"brianmcd/contextify (2011)",
"children":[]
},
{
"name":"inikulin/parse5 (2013)",
"children":[]
},
{
"name":"jsdom/xml-name-validator (2014)",
"children":[]
},
{
"name":"iriscouch/browser-request (2011)",
"children":[]
},
{
"name":"ilinsky/xmlhttprequest (2011)",
"children":[]
}
]
}
]
}
}
@dbhout
Copy link

dbhout commented May 6, 2015

Hi Shunpo,

First of all thank you for the example at
http://bl.ocks.org/shunpochang/66620bad0e6b201f261c

It is pretty much exactly what I need. I'm using it as the basis for
a process dependency graph in a business resiliency planning
application. Prospective customers are loving it in our demos so far.

The reason I'm writing is I've noticed that the downward side of the
tree always scrunches everything over to the left. The upward side
seems to spread things out and center things nicely. I'm wondering if
you are aware of that and know why that is?

For example when I run with this data:
{
"downward":{
"direction":"downward",
"name":"origin",
"children":[
{
"name":"Process 1"
},
{
"name":"Process 2"
},
{
"name":"Process 3"
}
]
},
"upward":{
"direction":"upward",
"name":"origin",
"children":[
{
"name":"App 1"
},
{
"name":"App 2"
},
{
"name":"App 3"
}
]
}
}

I see these results:
http://screencast.com/t/bEutfRoVI7

I figured I would check with you before I dive too deeply into the code.

Thanks again for the awesome example.

Dave

@shunpochang
Copy link
Author

Hey Dave,
Sorry for the late response, and I didn't get email notification for the comment you made.

The different behavior was intended and desired for this particular graph, because there were too many downward children nodes to fit in the div size, and I used a different layout method so that the downward nodes are not centered at the middle, but instead are shifted such that the leftmost node is viewable.

The codes responsible for such behavior goes from line 148-174, and if you want to use the symmetrical layout, just take out line 160-164, where the downward methods are declared.

Hope this helps, and if it takes me too long to reply next time, feel free to message directly again at shunpochang@gmail.com

Cheers
Sean

@dbhout
Copy link

dbhout commented Jun 9, 2015

Thanks Sean that helps!

@ve-ronica
Copy link

Hi Shunpo,

Thank you so much for sharing this code!

I have a question... if I wanted to have this going left and right instead of up and down, how could I do that?

Many thanks in advance!

Best,
V

@shunpochang
Copy link
Author

@ve-ronica, I haven't tested yet, but the layout direction could be changed at line 100,101 (https://gist.github.com/shunpochang/66620bad0e6b201f261c#file-index-html-L100-L101) by returning [d.y, d.x] instead.

@ve-ronica
Copy link

ve-ronica commented Oct 10, 2017

Thank you for such a fast response! That switches the orientation of the curved lines connecting the nodes, but not the nodes themselves.

After some further play I was able to get it to switch the nodes as well, by changing your line 221-222 to:

    .attr('transform', function(d) {
      return 'translate(' + d.y + ',' + d.x + ')'; });

Again I appreciate you so much for making this code available! Very helpful stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment