forked from stephlocke‘s block: Working Site Tree
<!DOCTYPE html>
<script src="https://unpkg.com/d3@4.12.2/build/d3.min.js"></script>
<script src="./htmlwidgets.js"></script>
<script src="./collapsibleTree.js"></script>
<script src="https://use.fontawesome.com/9f12702d26.js"></script>
<link href="./collapsibleTree.css" rel="stylesheet" />
<link href="./LockeTree.css" rel="stylesheet" />
<script type="application/json" data-for="htmlwidget-219e4959c6412cc5305e">{"x":{"data":{"name":"Locke Data","fa":"fa-home","SizeOfNode":30,"children":[{"name":"For your organisation","fa":"fa-users","SizeOfNode":25,"children":[{"name":"Training","fa":"fa-graduation-cap","SizeOfNode":20,"children":[{"name":"On-site »","fa":"fa-building","SizeOfNode":15,"url":"https://itsalocke.com/company/recommendedtraining"},{"name":"Public »","fa":"fa-ticket","SizeOfNode":15,"url":"https://itsalocke.com/#calendar"},{"name":"Online»","fa":"fa-laptop","SizeOfNode":15,"url":"https://itsalocke.com/company/onlinetraining"}]},{"name":"Support","fa":"fa-headphones","SizeOfNode":20,"children":[{"name":"Remote Guru »","fa":"fa-user-circle","SizeOfNode":15,"url":"https://itsalocke.com/company/remoteguru"}]},{"name":"Consultancy","fa":"fa-user-md","SizeOfNode":20,"children":[{"name":"Data Science Readiness »","fa":"fa-flask","SizeOfNode":15,"url":"https://itsalocke.com/company/readiness"},{"name":"Help on projects »","fa":"fa-gears","SizeOfNode":15,"url":"https://itsalocke.com/company/techleadership"},{"name":"Need a review »","fa":"fa-edit","SizeOfNode":15,"url":"https://itsalocke.com/company/peerreview"},{"name":"Other »","fa":"fa-envelope","SizeOfNode":15,"url":"https://itsalocke.com/#contact"}]}]},{"name":"For you","fa":"fa-user","SizeOfNode":25,"children":[{"name":"Read","fa":"fa-book","SizeOfNode":20,"children":[{"name":"Books »","fa":"fa-book","SizeOfNode":15,"url":"https://itsalocke.com/company/books"},{"name":"Blog »","fa":"fa-newspaper-o","SizeOfNode":15,"url":"https://itsalocke.com/blog"}]},{"name":"Events","fa":"fa-calendar","SizeOfNode":20,"children":[{"name":"Upcoming events »","fa":"fa-ticket","SizeOfNode":15,"url":"https://itsalocke.com/#calendar"},{"name":"Past talks and slides »","fa":"fa-upload","SizeOfNode":15,"url":"https://itsalocke.com/talks"}]},{"name":"Online","fa":"fa-globe","SizeOfNode":20,"children":[{"name":"Learn R by email »","fa":"fa-inbox","SizeOfNode":15,"url":"//eepurl.com/df8me1"},{"name":"Newsletter »","fa":"fa-envelope-open","SizeOfNode":15,"url":"//eepurl.com/df8me1"},{"name":"GitHub »","fa":"fa-github","SizeOfNode":15,"url":"https://github.com/lockedata"},{"name":"Packages »","fa":"fa-cube","SizeOfNode":15,"url":"https://itsalocke.com/oss/packages"},{"name":"Videos »","fa":"fa-youtube-play","SizeOfNode":15,"url":"https://www.youtube.com/channel/UCAg9ZUc9v3YedOuyAd-nDZQ"}]}]}]},"options":{"hierarchy":["L1","L2","L3"],"input":null,"attribute":"leafCount","linkLength":null,"fontSize":24,"tooltip":false,"collapsed":true,"zoomable":true,"margin":{"top":20,"bottom":20,"left":179.05,"right":190}}},"evals":[],"jsHooks":[]}</script>
<script type="application/htmlwidget-sizing" data-for="htmlwidget-219e4959c6412cc5305e">{"viewer":{"width":450,"height":350,"padding":0,"fill":true},"browser":{"width":960,"height":500,"padding":40,"fill":false}}</script>
<div id="htmlwidget_container" style="background-color:#2165B6;">
<div id="htmlwidget-219e4959c6412cc5305e" style="width:100%;height:500px;" class="collapsibleTree html-widget"></div>
</div>
.collapsibleTree .node circle {
fill: #E8830C;
stroke: #E8830C;
}
.collapsibleTree .node text {
fill: white;
}
.collapsibleTree .link {
stroke-opacity: 0.5;
stroke: white;
}
.collapsibleTree .node a {
fill: white;
text-decoration: underline;
}
.fa {
color: white;
font-size: 3em;
}
.collapsibleTree .node {
cursor: pointer;
}
.collapsibleTree .node circle {
fill: #fff;
stroke: #000;
}
.collapsibleTree .node text {
font: 10px sans-serif;
}
.collapsibleTree .link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.collapsibleTree div.tooltip {
position: absolute;
text-align: center;
padding: 3px;
font: 10px sans-serif;
background: #fff;
border: 2px;
border-radius: 8px;
border-style: solid;
pointer-events: none;
}
HTMLWidgets.widget({
name: 'collapsibleTree',
type: 'output',
factory: function(el, width, height) {
var i = 0,
duration = 750,
root = {},
options = {},
treemap;
// Optionally enable zooming, and limit to 1/5x or 5x of the original viewport
var zoom = d3.zoom()
.scaleExtent([1/5, 5])
.on('zoom', function () {
if (options.zoomable) svg.attr('transform', d3.event.transform)
})
// create our tree object and bind it to the element
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(el).append('svg')
.attr('width', width)
.attr('height', height)
.call(zoom)
.append('g');
// Define the div for the tooltip
var tooltip = d3.select(el).append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d) {d.y = d.depth * options.linkLength});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// Enter any new modes 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);
// Add tooltips, if specified in options
if (options.tooltip) {
nodeEnter = nodeEnter
.on('mouseover', mouseover)
.on('mouseout', mouseout);
}
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#E8830C');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
});
// Add fontawesome node circles
nodeEnter.append('svg:foreignObject')
.attr('class', 'handle')
.html(function(d) { return d.data.url ? '<a href="'+d.data.url+'"><i class="fa ' + d.data.fa + '"></i></a>': '<i class="fa ' + d.data.fa + '"></i>'; });
// Add links for the nodes with no url
nodeEnter.filter(function(d) {return typeof(d.data.url) === "undefined"})
.append("text")
.attr('text-anchor', 'end')
.style('font-size', options.fontSize + 'px')
.text(function(d) { return d.data.name; });
// Add links for the nodes with url
nodeEnter.filter(function(d) {return d.data.url})
.append("text")
.append("a")
.attr("href", function(d) {return d.data.url})
.attr('text-anchor', 'end')
.style('font-size', options.fontSize + 'px')
.text(function(d) { return d.data.name; });
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + d.y + ',' + d.x + ')';
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', function(d) {
return d.data.SizeOfNode || 10; // default radius is 10
})
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#E8830C');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + source.y + ',' + source.x + ')';
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', 'g')
.attr('class', 'link')
// Potentially, this may one day be mappable
// .style('stroke-width', function(d) { return d.data.linkWidth || 1 })
.attr('d', function(d){
var o = { x: source.x0, y: source.y0 }
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = 'M ' + s.y + ' ' + s.x + ' C ' +
(s.y + d.y) / 2 + ' ' + s.x + ', ' +
(s.y + d.y) / 2 + ' ' + d.x + ', ' +
d.y + ' ' + d.x;
return path
}
// 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);
// Hide the tooltip after clicking
tooltip.transition()
.duration(100)
.style('opacity', 0)
// Update Shiny inputs, if applicable
if (options.input) {
var nest = {},
obj = d;
// Navigate up the list and recursively find parental nodes
for (var n = d.depth; n > 0; n--) {
nest[options.hierarchy[n-1]] = obj.data.name
obj = obj.parent
}
Shiny.onInputChange(options.input, nest)
}
}
// Show tooltip on mouseover
function mouseover(d) {
tooltip.transition()
.duration(200)
.style('opacity', .9);
// Show either a constructed tooltip, or override with one from the data
tooltip.html(
d.data.tooltip || d.data.name + '<br>' +
options.attribute + ': ' + d.data.WeightOfNode
)
// Make the tooltip font size just a little bit bigger
.style('font-size', (options.fontSize + 1) + 'px')
.style('left', (d3.event.layerX) + 'px')
.style('top', (d3.event.layerY - 30) + 'px');
}
// Hide tooltip on mouseout
function mouseout(d) {
tooltip.transition()
.duration(500)
.style('opacity', 0);
}
}
return {
renderValue: function(x) {
// Assigns parent, children, height, depth
root = d3.hierarchy(x.data, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Attach options as a property of the instance
options = x.options;
// Update the canvas with the new dimensions
svg = svg.attr('transform', 'translate('
+ options.margin.left + ',' + options.margin.top + ')')
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// declares a tree layout and assigns the size
treemap = d3.tree().size([heightMargin, widthMargin]);
// Calculate a reasonable link length, if not otherwise specified
if (!options.linkLength) {
options.linkResponsive = true
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Optionally collapse after the second level
if (options.collapsed) root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
},
resize: function(width, height) {
// Resize the canvas
d3.select(el).select('svg')
.attr('width', width)
.attr('height', height);
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// Calculate a reasonable link length, if not originally specified
if (options.linkResponsive) {
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Update the treemap to fit the new canvas size
treemap = d3.tree().size([heightMargin, widthMargin]);
update(root)
},
// Make the instance properties available as a property of the widget
svg: svg,
root: root,
options: options
};
}
});