Skip to content

Instantly share code, notes, and snippets.

@mayblue9
Forked from shunpochang/README.md
Created May 3, 2016 04:04
Show Gist options
  • Save mayblue9/22cfd2147116bc18ccfec9f9615ca3d2 to your computer and use it in GitHub Desktop.
Save mayblue9/22cfd2147116bc18ccfec9f9615ca3d2 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] (https://github.com/shunpochang/d3tree/tree/master/get_tree_from_git), 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 any help/questions, shunpochang at gmail or tag me @shunpochang.

<!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":[]
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment