Created
February 11, 2021 16:55
-
-
Save freaktiful/5aa20944b783a04d4d1ff3050a74138f to your computer and use it in GitHub Desktop.
Radial stacked bar chart with donut chart around it (tooltips and transitions)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<link rel="stylesheet" href="style.css"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/bumbeishvili/d3-tip-for-v6@4/d3-tip.min.css"> | |
<meta charset="utf-8"> | |
<svg width="500" height="500" font-family="sans-serif" font-size="10"></svg> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script src="https://unpkg.com/d3-v6-tip@1.0.6/build/d3-v6-tip.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/underscore@1.12.0/underscore-min.js"></script> | |
<script> | |
var svg = d3.select("svg"), | |
width = +svg.attr("width"), | |
height = +svg.attr("height"), | |
innerRadius = 30, | |
outerRadius = Math.min(width, height) / 4, | |
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); | |
var xScaleOffset = 0; | |
var x = d3.scaleBand() | |
.range([xScaleOffset, 2 * Math.PI + xScaleOffset]) | |
.align(xScaleOffset); | |
var y = d3.scaleLinear() | |
.domain([0, 1]) | |
.range([innerRadius, outerRadius]); | |
var z = d3.scaleOrdinal() | |
.domain(['completion', 'potential']) | |
.range(['#000078', 'rgba(0,0,120, 0.2)']) | |
var c = d3.scaleOrdinal() | |
.domain(["transversal", "specific", "general"]) | |
.range(["#00ecff", "#f7e98b", "#00ffd0"]) | |
var l = d3.scaleOrdinal() | |
.domain(["transversal", "specific", "general"]) | |
.range(["C. TRANSVERSALES", "C. ESPECÍFICAS", "C. GENERALES"]) | |
var zClasses = ['completion', 'potential']; | |
var arc = d3.arc() | |
.outerRadius(outerRadius + 15) | |
.innerRadius(outerRadius + 1); | |
var pie = d3.pie() | |
.sort(null) | |
.value((d) => d.number); | |
d3.json("progress.json").then(data => { | |
x.domain(data.map((d) => d.label)) | |
//adjust the donut chart with the stacked bar chart. | |
pie.startAngle(x(data[0].label)).endAngle(x(data[0].label) + 2 * Math.PI) | |
var tip = d3.tip().attr('class', 'd3-tip') | |
.html((d) => { | |
var completion = Math.round(d.data.completion * 100) | |
var potential = Math.round((d.data.potential + d.data.completion) * 100) | |
if (this.selectedSemester) { | |
return completion | |
} else { | |
return completion + "/" + potential + "%" | |
} | |
}) | |
this.g.call(tip) | |
var arc = d3.arc() | |
.innerRadius((d) => y(d[0])) | |
.outerRadius((d) => y(d[1])) | |
.startAngle((d) => x(d.data.label)) | |
.endAngle((d) => x(d.data.label) + x.bandwidth()) | |
.padAngle(0.01) | |
.padRadius(innerRadius) | |
var graf = g.append('g') | |
.selectAll("g") | |
.data(d3.stack().keys(['completion', 'potential'])(data)) | |
.enter().append("g") | |
.attr("fill", d => z(d.key)) | |
.selectAll("path") | |
.data((d) => d) | |
.enter().append("path") | |
.attr("d", arc) | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide) | |
.transition() | |
.ease(d3.easeBounce) | |
.duration(1500) | |
//.delay((d, i) => i * 100) | |
.attrTween("d", function (d, index) { | |
var i = d3.interpolate(d[0], d[1]); | |
return function(t) { d[1] = i(t); return arc(d, index)}; | |
}); | |
//radial lines xAxis | |
var lines = g.selectAll("line") | |
.data(data.map((d) => d.label)) | |
.enter().append("line") | |
.attr("y1", -80) | |
.attr("y2", -outerRadius) | |
.style("stroke", "rgba(0,0,120, 0.2)") | |
.attr("transform", (d, i) => { | |
var value = (i * 360 / data.length) + (xScaleOffset * (180 / Math.PI)) | |
return "rotate(" + value + ")"; | |
}) | |
//yAxis | |
var yAxis = g.append("g") | |
.attr("text-anchor", "middle") | |
.call(g => g.selectAll("g") | |
.data(y.ticks(4)) | |
.join("g") | |
.attr("fill", "none") | |
.call(g => g.append("circle") | |
.attr("stroke", "rgba(0,0,120, 0.2)") | |
.attr("r", y)) | |
.call(g => g.append("text") | |
.attr("y", d => y(d) - 7) | |
.attr("dy", "0.35em") | |
.style("font-size", "11px") | |
.text(y.tickFormat(4, "%")) | |
.clone(true) | |
.attr("fill", "#000") | |
.attr("stroke", "none"))) | |
// Labels for xAxis | |
xLabels = g.append("g") | |
.selectAll("g") | |
.data(data) | |
.enter() | |
.append("g") | |
.attr("text-anchor", function(d) { return (x(d.label) + x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "end" : "start"; }) | |
.attr("transform", function(d) { | |
var lines = wordwrap(d.label.toUpperCase(), 15).length + 1; | |
var offset = ((x(d.label) + x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI) ? 10 : -10; | |
if (lines > 1) offset = offset * lines/2 | |
return "rotate(" + ((x(d.label) + x.bandwidth() / 2) * 180 / Math.PI - 90) + ")"+"translate(" + (y(1) + 20) + ", " + offset + ")"; }) | |
.append("text") | |
.attr("transform", function(d) { return (x(d.label) + x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "rotate(180)" : "rotate(0)"; }) | |
.style("font-size", "8px") | |
.attr("font-weight", "640") | |
.attr("id", (d,i) => "text_"+i) | |
.style("fill", "#000078") | |
.attr("alignment-baseline", "middle") | |
.each((d, i) => { | |
var lines = wordwrap(d.label.toUpperCase(), 15) | |
for (var j = 0; j < lines.length; j++) { | |
d3.select("#text_"+i).append("tspan") | |
.text(lines[j]) | |
.attr("x", 0) | |
.attr("dy", 10) | |
} | |
var completion = Math.round(d.completion * 100) | |
var potential = Math.round((d.completion + d.potential) * 100) | |
d3.select("#text_"+i).append("tspan") | |
.text(completion + "/" + potential + "%") | |
.attr("x", 0) | |
.attr("dy", 12) | |
}) | |
// Legend | |
var legend = g.append("text") | |
.attr("font-size", "7px") | |
.attr("class", "legend-text") | |
.attr("font-weight", "640") | |
.attr("width", 80) | |
.attr("transform", "translate(0, -25)") | |
.style("fill", "#000078") | |
var title = "Disseny,\nidentitat visual\ni construcció de\nmarca i a més\nlorem ip..." | |
// Add a <tspan class="text"> for every text line. | |
legend.selectAll("tspan.text") | |
.data(title.split("\n")) | |
.enter() | |
.append("tspan") | |
.attr("class", "text") | |
.text(d => d) | |
.attr("text-anchor", "middle") | |
.attr("x", 0) | |
.attr("dy", 8); | |
}); | |
d3.json("progress.json").then(data => { | |
var donutData = calculateDonutData(data) | |
console.log(donutData) | |
// donut chart outside radial the stacked bar chart | |
var g2 = g.selectAll(".arc") | |
.data(pie(donutData)) | |
.enter().append("g") | |
.attr("class", "arc"); | |
var path = g2.append("path") | |
.attr("d", arc) | |
.attr("id", function(d,i) { return "genusArc_"+i; }) | |
.style("fill", (d) => c(d.data.genus)); | |
path.transition().duration(1000).attrTween('d', function(d) { | |
var i = d3.interpolate(d.startAngle + 0.1, d.endAngle); | |
return function(t) { | |
d.endAngle = i(t); | |
return arc(d); | |
} | |
}); | |
// Text inside the donut chart | |
var text = g2.selectAll(".genusText") | |
.data(donutData) | |
.enter().append("text") | |
.attr("class", "genusText") | |
.attr("font-weight", "640") | |
.attr("font-size", "12px") | |
.attr("x",20) | |
.attr("dy", 12) | |
.attr("text-anchor", "middle") | |
.append("textPath") | |
.attr("xlink:href",function(d,i){return "#genusArc_"+i;}) | |
.text((d) => l(d.genus)) | |
.attr("startOffset", (d, i) => { | |
var length = path.nodes()[i].getTotalLength(); | |
return (25-(50 *80)/length+(50 *20)/length) + "%"; | |
}); | |
}) | |
function wordwrap(text, max) { | |
var regex = new RegExp(".{0,"+max+"}(?:\\s|$)","g"); | |
var lines = []; | |
var line; | |
while ((line = regex.exec(text))!="") { | |
lines.push(line);} | |
return lines | |
} | |
function calculateDonutData (data) { | |
var res = [] | |
var aux = data | |
var numGenerals = 0 | |
var numSpecific = 0 | |
var numTransversals = 0 | |
for (var i = 0; i < aux.length; i++) { | |
if (aux[i].genus === 'transversal') numTransversals += 1 | |
if (aux[i].genus === 'specific') numSpecific += 1 | |
if (aux[i].genus === 'general') numGenerals += 1 | |
} | |
if (numTransversals > 0) res.push({genus: 'transversal', number: numTransversals}) | |
if (numSpecific > 0) res.push({genus: 'specific', number: numSpecific}) | |
if (numGenerals > 0) res.push({genus: 'general', number: numGenerals}) | |
return res | |
} | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{"label": "Liderazgo en la gestión de personas y equipos de trabajo", "completion": 0.444, "potential": 0.35, "genus": "transversal"}, | |
{"label": "Resolució de situacions complexes", "completion": 0.17, "potential": 0.35, "genus": "transversal"}, | |
{"label": "Creativitat", "completion": 0.414, "potential": 0.35, "genus": "transversal"}, | |
{"label": "Planificació i organització", "completion": 0.29, "potential": 0.35, "genus": "transversal"}, | |
{"label": "Aprendre a aprendre", "completion": 0.4, "potential": 0.35, "genus": "specific"}, | |
{"label": "Ús i aplicació de les tic", "completion": 0, "potential": 0.35, "genus": "specific"}, | |
{"label": "Anàlisi, síntesi i pensament crític", "completion": 0.4667, "potential": 0.35, "genus": "general"} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
svg { | |
margin-left: auto; | |
margin-right: auto; | |
display: block; | |
} | |
.legend-text { | |
color: #000078; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment