Skip to content

Instantly share code, notes, and snippets.

@lorenzopub
Created April 22, 2017 05:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save lorenzopub/dfd90f8a3a8c76de939f857de24e7d89 to your computer and use it in GitHub Desktop.
Save lorenzopub/dfd90f8a3a8c76de939f857de24e7d89 to your computer and use it in GitHub Desktop.
Dendrogram + Grouped Horizontal Bar Chart
license: mit
<!DOCTYPE html>
<meta charset="utf-8">
<style>
circle,
path {
cursor: pointer;
}
circle {
fill: none;
pointer-events: all;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 350, right: 480, bottom: 350, left: 480},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 10;
var hue = d3.scale.category10();
var luminance = d3.scale.sqrt()
.domain([0, 1e6])
.clamp(true)
.range([90, 20]);
var svg = d3.select("body").append("svg")
.attr("width", margin.left + margin.right)
.attr("height", margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, radius]);
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx ; })
.padAngle(.01)
.padRadius(radius / 3)
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });
d3.json("flare.json", function(error, root) {
if (error) throw error;
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.size; })
.nodes(root)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition
.children(function(d, depth) { return depth < 2 ? d._children : null; })
.value(function(d) { return d.sum; });
var center = svg.append("circle")
.attr("r", radius / 3)
.on("click", zoomOut);
center.append("title")
.text("zoom out");
var path = svg.selectAll("path")
.data(partition.nodes(root).slice(1))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn);
function zoomIn(p) {
if (p.depth > 1) p = p.parent;
if (!p.children) return;
zoom(p, p);
}
function zoomOut(p) {
if (!p.parent) return;
zoom(p.parent, p);
}
// Zoom to the specified new root.
function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
return p.key > d.key
? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
: {depth: 0, x: 0, dx: 2 * Math.PI};
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
path = path.data(partition.nodes(root).slice(1), function(d) { return d.key; });
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
path.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
path.enter().append("path")
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.on("click", zoomIn)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
}
});
function key(d) {
var k = [], p = d;
while (p.depth) k.push(p.name), p = p.parent;
return k.reverse().join(".");
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.name));
c.l = luminance(d.sum);
return c;
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");
</script>
id value
UNC-CH
UNC-CH.QuickFactsData
UNC-CH.QuickFactsData.Gender
UNC-CH.QuickFactsData.Gender.Female 16827
UNC-CH.QuickFactsData.Gender.Male 12642
UNC-CH.QuickFactsData.Age
UNC-CH.QuickFactsData.Age.Under 25 20873
UNC-CH.QuickFactsData.Age.25 and Over 8595
UNC-CH.QuickFactsData.Attendance
UNC-CH.QuickFactsData.Attendance.Full-Time 25084
UNC-CH.QuickFactsData.Attendance.Part-Time 4585
UNC-CH.QuickFactsData.Residency
UNC-CH.QuickFactsData.Residency.In-State 21232
UNC-CH.QuickFactsData.Residency.Out-of-State 8237
UNC-CH.QuickFactsData.Enrollment
UNC-CH.QuickFactsData.Enrollment.Undergraduate 18523
UNC-CH.QuickFactsData.Enrollment.Graduate 8427
UNC-CH.QuickFactsData.Enrollment.Professional 2519
UNC-CH.QuickFactsData.Citizenship
UNC-CH.QuickFactsData.Citizenship.US Citizen 27114
UNC-CH.QuickFactsData.Citizenship.Resident Alien 763
UNC-CH.QuickFactsData.Citizenship.Nonresident Alien 1592
UNC-CH.QuickFactsData.Proximity
UNC-CH.QuickFactsData.Proximity.On-Campus 27100
UNC-CH.QuickFactsData.Proximity.Off-Campus 2369
UNC-CH.QuickFactsData.Race
UNC-CH.QuickFactsData.Race.American Indian or Alaskan Native 129
UNC-CH.QuickFactsData.Race.Asian 3759
UNC-CH.QuickFactsData.Race.Black or African-American 2342
UNC-CH.QuickFactsData.Race.Hispanic 2145
UNC-CH.QuickFactsData.Race.Native Hawaiian or Pacific Islander 43
UNC-CH.QuickFactsData.Race.Race/Ethnicity Unknown 1184
UNC-CH.QuickFactsData.Race.Two or More Races 1610
UNC-CH.QuickFactsData.Race.White 18257
{
"name": "UNC-CH Quick Facts Data",
"children": [
{
"name": "Gender",
"children":[
{
"name": "Female", "size": 16827},
{"name":"Male", "size":12642}]},
{"name":"Age",
"children":
[{"name":"Under 25", "size":20873},
{"name":"25 and Over", "size":8595}]},
{"name":"Attendance",
"children":
[{"name":"Full-Time", "size":25084},
{"name":"Part-Time", "size":4385}]},
{"name":"Residency",
"children":
[{"name":"In-State", "size":21232},
{"name":"Out-of-State", "size":8237}]},
{"name":"Enrollment",
"children":
[{"name":"Undergraduate", "size":18523},
{"name":"Graduate", "size":8427},
{"name":"Professional", "size":2519}]},
{"name":"Citizenship",
"children":
[{"name":"US Citizen", "size":27114},
{"name":"Resident Alien", "size":763},
{"name":"Nonresident Alien", "size":1592}]},
{"name":"Proximity",
"children":
[{"name":"On-Campus", "size":27100},
{"name":"Off-Campus", "size":2369}]},
{"name":"Race",
"children":
[{"name":"American Indian or Alaskan Native", "size":129}]},
{"name":"Asian", "size":27114},
{"name":"Black or African-American", "size":3759},
{"name":"Hispanic", "size":2342},
{"name":"Native Hawaiian or Pacific Islander", "size":2145},
{"name":"Race/Ethnicity Unknown", "size":43},
{"name":"Two or More Races", "size":1184},
{"name":"White", "size":18257}]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
.link {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
stroke-width: 1px;
}
text {
font-family: "Arial Black", Gadget, sans-serif;
fill: black;
font-weight: bold;
font-size: 14px
}
.xAxis .tick text{
fill: black;
}
.grid .tick line{
stroke: grey;
stroke-dasharray: 5, 10;
opacity: 0.7;
}
.grid path{
stroke-width: 0;
}
.node circle {
fill: #999;
}
.node--internal circle {
fill: #555;
}
.node--internal text {
font-size: 16px;
text-shadow: 0 2px 0 #fff, 0 -2px 0 #fff, 2px 0 0 #fff, -2px 0 0 #fff;
}
.node--leaf text {
fill: black;
}
.ballG text {
fill: black;
}
.shadow {
-webkit-filter: drop-shadow( -1.5px -1.5px 1.5px #000 );
filter: drop-shadow( -1.5px -1.5px 1.5px #000 );
}
</style>
</head>
<body>
<svg width="960" height="1100"></svg>
<script>
// main svg
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(20,0)"); // move right 20px.
// x-scale and x-axis
var experienceName = ["", "","","","",""];
var formatSkillPoints = function (d) {
return experienceName[d % 6];
}
var xScale = d3.scaleLinear()
.domain([0, 28000])
.range([0, 280]);
var xAxis = d3.axisTop()
.scale(xScale)
.ticks(5)
.tickFormat(formatSkillPoints);
// Setting up a way to handle the data
var tree = d3.cluster() // This D3 API method setup the Dendrogram datum position.
.size([height, width - 460]) // Total width - bar chart width = Dendrogram chart width
.separation(function separate(a, b) {
return a.parent == b.parent // 2 levels tree grouping for category
|| a.parent.parent == b.parent
|| a.parent == b.parent.parent ? 0.4 : 0.8;
});
var stratify = d3.stratify() // This D3 API method gives cvs file flat data array dimensions.
.parentId(function(d) { return d.id.substring(0, d.id.lastIndexOf(".")); });
d3.csv("https://gist.githubusercontent.com/vpletzke/9607d012e725638343ee01a8e8fa6310/raw/568f6f255250192b955c3e16ff151c12cc50800d/skillsdata.csv", row, function(error, data) {
if (error) throw error;
var root = stratify(data);
tree(root);
// Draw every datum a line connecting to its parent.
var link = g.selectAll(".link")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("class", "link")
.attr("d", function(d) {
return "M" + d.y + "," + d.x
+ "C" + (d.parent.y + 100) + "," + d.x
+ " " + (d.parent.y + 100) + "," + d.parent.x
+ " " + d.parent.y + "," + d.parent.x;
});
// Setup position for every datum; Applying different css classes to parents and leafs.
var node = g.selectAll(".node")
.data(root.descendants())
.enter().append("g")
.attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// Draw every datum a small circle.
node.append("circle")
.attr("r", 4);
// Setup G for every leaf datum.
var leafNodeG = g.selectAll(".node--leaf")
.append("g")
.attr("class", "node--leaf-g")
.attr("transform", "translate(" + 8 + "," + -13 + ")");
leafNodeG.append("rect")
.attr("class","shadow")
.style("fill", function (d) {return d.data.color;})
.attr("width", 2)
.attr("height", 30)
.attr("rx", 2)
.attr("ry", 2)
.transition()
.duration(800)
.attr("width", function (d) {return xScale(d.data.value);});
leafNodeG.append("text")
.attr("dy", 19.5)
.attr("x", 8)
.style("text-anchor", "start")
.text(function (d) {
return d.data.id.substring(d.data.id.lastIndexOf(".") + 1) + " - " + d.data.value;
});
// Write down text for every parent datum
var internalNode = g.selectAll(".node--internal");
internalNode.append("text")
.attr("y", -10)
.style("text-anchor", "middle")
.text(function (d) {
return d.data.id.substring(d.data.id.lastIndexOf(".") + 1);
});
// Attach axis on top of the first leaf datum.
var firstEndNode = g.select(".node--leaf");
firstEndNode.insert("g")
.attr("class","xAxis")
.attr("transform", "translate(" + 7 + "," + -14 + ")")
.call(xAxis);
// tick mark for x-axis
firstEndNode.insert("g")
.attr("class", "grid")
.attr("transform", "translate(7," + (height - 15) + ")")
.call(d3.axisBottom()
.scale(xScale)
.ticks(5)
.tickSize(-height, 0, 0)
.tickFormat("")
);
// Emphasize the y-axis baseline.
svg.selectAll(".grid").select("line")
.style("stroke-dasharray","20,1")
.style("stroke","black");
// The moving ball
/* var ballG = svg.insert("g")
.attr("class","ballG")
.attr("transform", "translate(" + 1100 + "," + height/2 + ")");
ballG.insert("circle")
.attr("class","shadow")
.style("fill","steelblue")
.attr("r", 8);
ballG.insert("text")
.style("text-anchor", "middle")
.attr("dy",5)
.text("0.0");
*/
// Animation functions for mouse on and off events.
d3.selectAll(".node--leaf-g")
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);
function handleMouseOver(d) {
var leafG = d3.select(this);
leafG.select("rect")
.attr("stroke","#4D4D4D")
.attr("stroke-width","2");
var ballGMovement = ballG.transition()
.duration(400)
.attr("transform", "translate(" + (d.y
+ xScale(d.data.value) + 90) + ","
+ (d.x + 1.5) + ")");
ballGMovement.select("circle")
.style("fill", d.data.color)
.attr("r", 18);
ballGMovement.select("text")
.delay(300)
.text(Number(d.data.value).toFixed(1));
}
function handleMouseOut() {
var leafG = d3.select(this);
leafG.select("rect")
.attr("stroke-width","0");
}
});
function row(d) {
return {
id: d.id,
value: +d.value,
color: d.color
};
}
</script>
</body>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
position: relative;
}
.node {
box-sizing: border-box;
position: absolute;
overflow: hidden;
}
.node-label {
padding: 4px;
line-height: 1em;
white-space: pre;
}
.node-value {
color: rgba(0,0,0,0.8);
font-size: 9px;
margin-top: 1px;
}
</style>
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var width = 961,
height = 1061,
ratio = 4;
var format = d3.format(",d");
var color = d3.scaleOrdinal()
.range(d3.schemeCategory10
.map(function(c) { c = d3.rgb(c); c.opacity = 0.6; return c; }));
var stratify = d3.stratify()
.parentId(function(d) { return d.id.substring(0, d.id.lastIndexOf(".")); });
var treemap = d3.treemap()
.tile(d3.treemapSquarify.ratio(1))
.size([width / ratio, height]);
d3.csv("flare.csv", type, function(error, data) {
if (error) throw error;
var root = stratify(data)
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.height - a.height || b.value - a.value; });
treemap(root);
d3.select("body")
.selectAll(".node")
.data(root.leaves())
.enter().append("div")
.attr("class", "node")
.attr("title", function(d) { return d.id + "\n" + format(d.value); })
.style("left", function(d) { return Math.round(d.x0 * ratio) + "px"; })
.style("top", function(d) { return Math.round(d.y0) + "px"; })
.style("width", function(d) { return Math.round(d.x1 * ratio) - Math.round(d.x0 * ratio) - 1 + "px"; })
.style("height", function(d) { return Math.round(d.y1) - Math.round(d.y0) - 1 + "px"; })
.style("background", function(d) { while (d.depth > 1) d = d.parent; return color(d.id); })
.append("div")
.attr("class", "node-label")
.text(function(d) { return d.id.substring(d.id.lastIndexOf(".") + 1); })
.append("div")
.attr("class", "node-value")
.text(function(d) { return format(d.value); });
});
function type(d) {
d.value = +d.value;
return d;
}
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: 10px sans-serif;
text-anchor: middle;
}
.node--hover circle {
stroke: #000;
stroke-width: 1.2px;
}
</style>
<svg width="960" height="960"><g transform="translate(1,1)"></g></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var format = d3.format(",d");
var color = d3.scaleSequential(d3.interpolateMagma)
.domain([-4, 4]);
var stratify = d3.stratify()
.parentId(function(d) { return d.id.substring(0, d.id.lastIndexOf(".")); });
var pack = d3.pack()
.size([width - 2, height - 2])
.padding(3);
d3.csv("flare.csv", function(error, data) {
if (error) throw error;
var root = stratify(data)
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; });
pack(root);
var node = svg.select("g")
.selectAll("g")
.data(root.descendants())
.enter().append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("class", function(d) { return "node" + (!d.children ? " node--leaf" : d.depth ? "" : " node--root"); })
.each(function(d) { d.node = this; })
.on("mouseover", hovered(true))
.on("mouseout", hovered(false));
node.append("circle")
.attr("id", function(d) { return "node-" + d.id; })
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.depth); });
var leaf = node.filter(function(d) { return !d.children; });
leaf.append("clipPath")
.attr("id", function(d) { return "clip-" + d.id; })
.append("use")
.attr("xlink:href", function(d) { return "#node-" + d.id + ""; });
leaf.append("text")
.attr("clip-path", function(d) { return "url(#clip-" + d.id + ")"; })
.selectAll("tspan")
.data(function(d) { return d.id.substring(d.id.lastIndexOf(".") + 1).split(/(?=[A-Z][^A-Z])/g); })
.enter().append("tspan")
.attr("x", 0)
.attr("y", function(d, i, nodes) { return 13 + (i - nodes.length / 2 - 0.5) * 10; })
.text(function(d) { return d; });
node.append("title")
.text(function(d) { return d.id + "\n" + format(d.value); });
});
function hovered(hover) {
return function(d) {
d3.selectAll(d.ancestors().map(function(d) { return d.node; })).classed("node--hover", hover);
};
}
</script>
We can make this file beautiful and searchable if this error is corrected: It looks like row 2 should actually have 3 columns, instead of 2. in line 1.
id,value,color
UNC-CH,
UNC-CH.Gender,
UNC-CH.Gender.Female,16827,#e9ded9
UNC-CH.Gender.Male,12642,#d5c5be
UNC-CH.Age,
UNC-CH.Age.Under 25,20873,#808080
UNC-CH.Age.25 and Over,8595,#80A0A0
UNC-CH.Attendance,
UNC-CH.Attendance.Full-Time,25084,#808080
UNC-CH.Attendance.Part-Time,4585,#80A0A0
UNC-CH.Residency,
UNC-CH.Residency.In-State,21232,#808080
UNC-CH.Residency.Out-of-State,8237,#80A0A0
UNC-CH.Enrollment,
UNC-CH.Enrollment.Undergraduate,18523,#808080
UNC-CH.Enrollment.Graduate,8427,#80A0A0
UNC-CH.Enrollment.Professional,2519,#E54F24
UNC-CH.Citizenship,
UNC-CH.Citizenship.US Citizen,27114,#808080
UNC-CH.Citizenship.Resident Alien,763,#80A0A0
UNC-CH.Citizenship.Nonresident Alien,1592,#E54F24
UNC-CH.Proximity,
UNC-CH.Proximity.On-Campus,27100,#808080
UNC-CH.Proximity.Off-Campus,2369,#80A0A0
UNC-CH.Race,
UNC-CH.Race.American Indian or Alaskan Native,129,#808080
UNC-CH.Race.Asian,3759,#80A0A0
UNC-CH.Race.Black or African-American,2342,#E54F24
UNC-CH.Race.Hispanic,2145,#FF9900
UNC-CH.Race.Native Hawaiian or Pacific Islander,43,#563B7E
UNC-CH.Race.Race/Ethnicity Unknown,1184,#D6BA33
UNC-CH.Race.Two or More Races,1610,#1169AE
UNC-CH.Race.White,18257,#F6854D
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
position: relative;
}
.node {
box-sizing: border-box;
position: absolute;
overflow: hidden;
}
.node-label {
padding: 4px;
line-height: 1em;
white-space: pre;
}
.node-value {
color: rgba(0,0,0,0.8);
font-size: 9px;
margin-top: 1px;
}
</style>
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var width = 960,
height = 1060;
var format = d3.format(",d");
var color = d3.scaleOrdinal()
.range(d3.schemeCategory10
.map(function(c) { c = d3.rgb(c); c.opacity = 0.6; return c; }));
var stratify = d3.stratify()
.parentId(function(d) { return d.id.substring(0, d.id.lastIndexOf(".")); });
var treemap = d3.treemap()
.size([width, height])
.padding(1)
.round(true);
d3.csv("flare.csv", type, function(error, data) {
if (error) throw error;
var root = stratify(data)
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.height - a.height || b.value - a.value; });
treemap(root);
d3.select("body")
.selectAll(".node")
.data(root.leaves())
.enter().append("div")
.attr("class", "node")
.attr("title", function(d) { return d.id + "\n" + format(d.value); })
.style("left", function(d) { return d.x0 + "px"; })
.style("top", function(d) { return d.y0 + "px"; })
.style("width", function(d) { return d.x1 - d.x0 + "px"; })
.style("height", function(d) { return d.y1 - d.y0 + "px"; })
.style("background", function(d) { while (d.depth > 2) d = d.parent; return color(d.id); })
.append("div")
.attr("class", "node-label")
.text(function(d) { return d.id.substring(d.id.lastIndexOf(".") + 1).split(/(?=[A-Z][^A-Z])/g).join("\n"); })
.append("div")
.attr("class", "node-value")
.text(function(d) { return format(d.value); });
});
function type(d) {
d.value = +d.value;
return d;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment