I have the following code:
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", function(d) { toggle(d); update(d); });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) {
if (d.size != undefined) {
return d.name + " :: " + d.size + " :: " + d.diff_day;
} else {
return d.name;
}
})
.style("fill-opacity", 1e-6)
.style("font-size", "15px");
nodeEnter.append("svg:path").attr("d", d3.svg.symbol.type("triangle-up").style("fill", "black"));
Now, the problem is that the svg:circle and svg:text are coming fine. But the "d3.svg.symbol.type("triangle-up").style("fill", "black"));" doesn't seem to work. Any help would be appreciated...
You cannot set the attribute properties on the symbol you create itself like this:
nodeEnter.append("svg:path") // Wrong.
.attr("d", d3.svg.symbol.type("triangle-up").style("fill", "black"));
You have to first set the d attribute, and then set the style fill on the path:
nodeEnter.append("svg:path") // Fixed.
.attr("d", d3.svg.symbol().type("triangle-up"))
.style("fill", "black");
Related
var graph = Vue.component('graph', {
template: '#graph-template',
props: {
data:Array,
},
attached: function(){
this.genGraph();
},
methods:{
genGraph:function(){
this.data.forEach(function(obj) {
root = obj;
root.x0 = h / 2;
root.y0 = 0;
this.updateGraph(root);
})
},
updateGraph: function(source) {
var i = 0;
var duration = d3.event && d3.event.altKey ? 5000 : 500;
var nodes = this._tree.nodes(root).reverse();
nodes.forEach(function(d) { d.y = d.depth * 180; });
var node = this._svg.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("click", function(d) {
this.updateGraph(d);
});
nodeEnter.append("svg:circle")
.attr("r", 0)
.style("fill", function( d ) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append('svg:foreignObject')
.attr("x", -3)
.attr("y", -15)
.append("xhtml:body")
.html(function(d){
return d.children || d._children ? "<i class='fa fa-server fa-2x' style='color:#e29e3d;'></i>" : "<i class='fa fa-server fa-2x' style='color:#03a9f4;'></i>";
});
nodeEnter.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? - 15 : 25; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
nodeEnter.append("a")
.attr("xlink:href", function (d) {
return "/" + d.id;
})
.append("rect")
.attr("class", "clickable")
.attr("y", -10)
.attr("x", function (d) { return d.children || d._children ? -70 : 10; })
.attr("width", 60)
.attr("height", 16)
.style("fill","rgba(3,169,244,0)")
.style("fill-opacity", .3);
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function( d ) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 0)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeUpdate.select("text")
.style("fill-opacity", 1)
.text(function(d){
return d.name.split("/").pop();
});
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
var link = this._svg.selectAll("path.link")
.data(this._tree.links(nodes), function(d) { return d.target.id; });
link.enter().insert("svg:path", "g")
.attr("class", function (d) { return (d.source != root) ? "link_dashed" : "link_continuous" ; })
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal)
.attr("marker-end", "url(#end)");
link.transition()
.duration(duration)
.attr("d", diagonal)
.attr("marker-end", "url(#end)");
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
}
}
When the updateGraph() method is called, an error occurs:
Uncaught TypeError: this.updateGraph is not a function.
The reason you'll be getting this issue (based on your code above) is because this.updateGraph(root); is not in the same scope as your component.
Above the line this.data.forEach(function(obj) { add the following:
var self = this;
and then change:
this.updateGraph(root);
to:
self.updateGraph(root);
Hope this helps!
I have a situation where I want users to browse 3 different data sets using circle pack layout. I am using Bostock's Zoomable circle packing.
I have added 3 options additionally which loads data and creates new nodes. Here chartid is passed from these elements.
function changeDataSet(chartid)
{
console.log(chartid);
//console.log(nodes);
//add new chart data depending on the selected option
if(chartid === "plevels")
{
root = JSON.parse(newKmap_slevels);
focus = root;
nodes = pack.nodes(root);
}
else if (chartid === "pduration")
{
root = JSON.parse(newKmap_sduration);
focus = root;
nodes = pack.nodes(root);
}
else
{
root = JSON.parse(newKmap_stype);
focus = root;
nodes = pack.nodes(root);
}
refresh();
}
Then I have the refresh function, but I am not understnading how to remove existting nodes, and add new ones based on the new data set. Showing some transition animations would be nice also.
Currently I am trying to remove and recreate the initial elements, but the chart goes blank when I do that.
var refresh = function() {
//var nodes = pack.nodes(root);
var duration = 10;
console.log(nodes);
d3.select("#conf_knowledge_map").selectAll("g")
.remove();
svg
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
So my question is how can I remove and then create new nodes on click?
UPDATE
I was able to remove all nodes and add the new nodes based on the new data, but now on click to zoom the layout is all messed up. The transform function is not applied to the new nodes somehow.
var refresh = function() {
svg.selectAll(".node").remove();
svg.selectAll(".label").remove();
var nodes = pack.nodes(root);
focus = root;
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
console.log(d);
// if(d.depth === 0)
// return 0;
// else
// return d.x - (diameter/2);
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
// d.x = d.x - (diameter/2);
// d.y = d.y - (diameter/2);
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.attr("x", function(d) {return d.x - (diameter/2);})
.attr("y", function(d) {return d.y - (diameter/2);})
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
How can I transform the new nodes to be in place and for the zoom to work properly?
UPDATE 2
Now the transform is working properly after attaching 'g' element to the circles, and showing all the nodes and text correctly. The only problem now is that the zoom does not work when I click on the circles!!
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
if(d.depth === 0)
return 0;
else
return d.x - (diameter/2);
})
.attr("cy", function(d) {
if(d.depth === 0)
return 0;
else
return d.y - (diameter/2);
})
.style("fill", function(d) { return d.children ? color(d.depth) : null;})
.on("click", function(d) {
if (focus !== d) zoom(d); d3.event.stopPropagation();
})
.append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
;
How to make the zoom work??
It so happens that I was making the mistake of simultaneously creating three independent circle pack layouts, with mixed statements one after the other. This was problematic as when you have to select svg sections, different elements were all getting selected and wrongly attached with different events.
So I decided to separate the three implementations and create the three layouts one after the other, only after each one finished. This way I kept the section selection and attaching events etc, separate and then load them as and when required.
Hi so I'm trying to add multiple text elements to a d3 svg g node. However, for the following code, all the first text assignments to a node show up but none of the second text elements appear. Is my approach right? or is there a bug I'm missing
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
// .on("mouseenter",setdatavis)
// .on("mouseleave",setdatainvis);
nodeEnter.append("text")
// .attr("x",0)
// .attr("y",0)
// .append("tspan")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("y",-10)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6)
.style("visibility","visible");
// .on("mouseenter", function(){d3.select(this)
// .style("visibility", "hidden")
// .transition()
// ;})
// .on("mouseleave", function(){d3.select(this)
// .style("visibility", "visible")
// .transition()
// ;});
nodeEnter.append("text")
.attr("x",10)
.attr("text-anchor","start")
.attr("y",10)
.attr("dy",".35em")
.text(function(d){return "hi";})
.style("visibility","visible")
.style("fill-opacity",1e-6);
I was expecting my HTML page to display the json content in the HTML element specified for example in this case Display data in red colour However it would just not do it and display it as it is ?. I am using D3 data driven document to display json data in the form of a tree(parent-child relationship). Is it possible of what i am trying ?
IMAGE:
Json file:
{
"name":"Business Direction IM RM",
"children":[
{
"name":" <font color=\"red\">Sean /Bur/XYZ<\/font> ",
"children":[
]
},
{
"name":" <font color=\"red\">Vijay /Fish/XYZ<\/font> ",
"children":[
]
}
]
}
HTML:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 11px sans-serif;
font-weight:900;
font-size:12px;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 40, right: 220, bottom: 20, left: 220},
width = 500 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0,
duration = 1750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("flare.json", function(error, flare) {
root = flare;
root.x0 = height / 2;
root.y0 = 0;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
update(root);
});
d3.select(self.frameElement).style("height", "800px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
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.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.selectAll("path.link")
.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")
.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;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
</script>
Solution:
//Edited Code
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start";})
.attr('fill', function(d) { return d.name.color; })
.style("fill-opacity", 1e-6);
It depends how much data you're working with, and whether you are able to go back through it all and clean it up manually. The cleanest solution would be as Justin Niessner mentioned, to remove the html markup from the json file, and add a color property to each entry:
{
"name":"Business Direction IM RM",
"children":[
{
"name": "Sean /Bur/XYZ",
"color": "red",
"children":[
]
},
{
"name": "Vijay /Fish/XYZ",
"color": "red",
"children":[
]
}
]
}
If you aren't able to edit the data directly, or there is too much of it to edit manually, you could use regular expressions to get the parts of the string that you want to use.
var name_regexp = /.*?\<.*?\>(.*?)\<.*?\>/;
var color_regexp = /.*?color\=\"(.*?)\".*/;
then when you draw your text element you would write:
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
// USE THE REGEXPs HERE
.attr('fill', function(d) {
return d.name.replace(color_regexp, '$1');
})
.text(function(d) {
return d.name.replace(name_regexp, '$1');
})
.style("fill-opacity", 1e-6);
Check out this fiddle to see what those regular expressions are doing.
All of your javaScript code creates SVG elements, not HTML. D3 will simply render everything you give the <text> block as text rather than render it as HTML somehow and then convert to SVG.
I would suggest simply adding a color property to your JSON object. You could then change your rendering code to:
nodeEnter.append('text')
.attr('fill', function(d) { return d.color; })
// the rest of your rendering
I'm new to the d3.js library.
I'm trying to make a tree like this one, but with a link that goes to an external page on each node.
Is it possible?
I tried to add a "svg:a" to each node but in makes all the tree to disappear.
Update:
I'm taking this code from the html of the page linked above.
The libraries linked are:
d3.js
d3.layout.js
This is all the code:
<style type="text/css">
.node circle {
cursor: pointer;
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size: 11px;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
<script>
var m = [20, 120, 20, 120],
w = 1280 - m[1] - m[3],
h = 800 - m[0] - m[2],
i = 0,
root;
var tree = d3.layout.tree()
.size([h, w]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#body").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
d3.json("flare.json", function(json) {
root = json;
root.x0 = h / 2;
root.y0 = 0;
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
// Initialize the display to show a few nodes.
root.children.forEach(toggleAll);
toggle(root.children[1]);
toggle(root.children[1].children[2]);
toggle(root.children[9]);
toggle(root.children[9].children[0]);
update(root);
});
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", function(d) { toggle(d); update(d); });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
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.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// 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;
});
}
// Toggle children.
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
</script>
Basically what I have tried was to add this piece of code just before the nodeEnter.append("svg:text")
nodeEnter.append("svg:a")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text("http://example.com")
.style("fill-opacity", 1e-6);
Try adding the action on the node itself, like below, and change the cursor to pointer to give a hint to the user.
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); })
.style("cursor", "pointer")
.on("click", function() {
window.open("http://www.stackoverflow.com", '_blank').focus();
});
You should be able to add an HTML link to any of the objects that you want by adding an a tag as follows (this example is for the text that is associated with each node);
nodeEnter.append("a")
.attr("xlink:href", "http://example.com")
.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
If you had a seperate URL associated with each node (let's imagine that it's called link) it would just be a matter of retrieving the url with a function call similar to this;
nodeEnter.append("a")
.attr("xlink:href", function(d) { return d.link; })
.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
For a fuller description of adding a link see here: https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-adding-web-links-to-d3js-objects
For information on tree diagrams in general see here: https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-tree-diagrams