Skip to content

Instantly share code, notes, and snippets.

@martinjc
Last active December 22, 2022 17:37
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save martinjc/e46f38d44a049a61ab1c2d97a2413439 to your computer and use it in GitHub Desktop.
D3 - Donut chart with labels and connectors (Data: random teaching evaluation survey results)
license: MIT
border: no

This examples creates a d3 donut chart, with labels and lines connecting labels to segments.

Labels are arranged to avoid overlap, label text is wrapped to ensure it fits on the page

The pie chart code is modular, so can be reused simply.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Labelled Donut</title>
<link rel="stylesheet" href="style.css">
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script src="utils.js"></script>
<script src="pie.js"></script>
<script src="script.js"></script>
</body>
</html>
function pieChart() {
var width = 400;
var height = 300;
var margin = {
top: 80,
bottom: 80,
left: 120,
right: 120,
};
var columns = [];
var svg;
var pformat = d3.format('.1%');
var colourScale = d3.scaleOrdinal()
.domain(['N/A', 'Disagree', 'Neither Agree nor Disagree', 'Agree'])
.range(["#222", "hsla(0, 60%, 50%, 1)", "hsla(45, 70%, 60%, 1)", "hsla(90, 50%, 50%, 1)"]);
var pie = d3.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var key = function(d) {
return d.data.key;
}
function midAngle(d) {
return d.startAngle + (d.endAngle - d.startAngle) / 2;
}
function chart(selection) {
selection.each(function(data) {
//draw the pie chart
svg = d3.select(this)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
svg = svg
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("g")
.attr("class", "slices");
svg.append("g")
.attr("class", "labels");
svg.append("g")
.attr("class", "lines");
pie_data = [];
columns.forEach(function(c) {
if (+data[c] > 0) {
pie_data.push({
key: c,
value: +data[c]
});
}
});
var radius = Math.min(width, height) / 2;
var arc = d3.arc()
.outerRadius(radius * 0.6)
.innerRadius(radius * 0.1)
.padAngle(.02)
.padRadius(100)
.cornerRadius(2);
var labelArc = d3.arc()
.outerRadius(radius * 0.4)
.innerRadius(radius);
var slice = svg.select(".slices")
.selectAll("path.slice")
.data(pie(pie_data), key);
slice
.enter()
.insert("path")
.attr("d", arc)
.attr("class", "slice")
.style("stroke", "black")
.style("stroke-width", "0.5px")
.style("fill", function(d) {
return colourScale(d.data.key);
});
var text = svg.select(".labels")
.selectAll("text")
.data(pie(pie_data), key);
text
.enter()
.append("text")
.attr('class', 'label')
.attr('id', function(d, j) {
return 'l-' + j;
})
.attr("transform", function(d) {
var pos = labelArc.centroid(d);
pos[0] = radius * (midAngle(d) < Math.PI ? 1 : -1);
return "translate(" + pos + ")";
})
.style("text-anchor", function(d) {
return midAngle(d) < Math.PI ? "start" : "end";
})
.attr("dy", ".35em")
.attr("dx", ".35em")
.attr("fill", "#111")
.text(function(d) {
return d.data.key + " (" + pformat(d.data.value) + ")";
})
.call(wrap, margin.right - 20);
arrangeLabels(svg, ".label");
var polyline = svg.select(".lines")
.selectAll("polyline")
.data(pie(pie_data), key);
polyline.enter()
.append("polyline")
.attr("points", function(d, j) {
var offset = midAngle(d) < Math.PI ? 0 : 10;
var label = d3.select('#l-' + j);
var transform = getTransformation(label.attr("transform"));
var pos = labelArc.centroid(d);
pos[0] = transform.translateX + offset;
pos[1] = transform.translateY;
var mid = labelArc.centroid(d);
mid[1] = transform.translateY;
return [arc.centroid(d), mid, pos];
});
})
}
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.columns = function(_) {
if (!arguments.length) return columns;
columns = _;
return chart;
};
return chart;
}
var module_data = {
"question": "Was good at explaining things",
"N/A": 0.02763018065887354,
"Disagree": 0.10839532412327312,
"Neither Agree nor Disagree": 0.13177470775770456,
"Agree": 0.7321997874601488
};
var columns = ["N/A", "Disagree", "Neither Agree nor Disagree", "Agree"];
var chart = pieChart()
.width(400)
.height(400)
.margin({
top: 20,
bottom: 20,
left: 80,
right: 100
})
.columns(columns);
var container = d3.select("body")
.datum(module_data)
.call(chart);
body {
font-family: "tee_franklin_light", 'Source Sans Pro', "Helvetica Neue", Helvetica, "Open Sans", Arial, sans-serif;
}
polyline {
opacity: .3;
stroke: black;
stroke-width: 2px;
fill: none;
}
function getTransformation(transform) {
/*
* This code comes from a StackOverflow answer to a question looking
* to replace the d3.transform() functionality from v3.
* http://stackoverflow.com/questions/38224875/replacing-d3-transform-in-d3-v4
*/
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttributeNS(null, "transform", transform);
var matrix = g.transform.baseVal.consolidate()
.matrix;
var {
a,
b,
c,
d,
e,
f
} = matrix;
var scaleX, scaleY, skewX;
if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;
if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;
if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;
if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;
return {
translateX: e,
translateY: f,
rotate: Math.atan2(b, a) * Math.PI / 180,
skewX: Math.atan(skewX) * Math.PI / 180,
scaleX: scaleX,
scaleY: scaleY
};
}
function arrangeLabels(selection, label_class) {
var move = 1;
while (move > 0) {
move = 0;
selection.selectAll(label_class)
.each(function() {
var that = this;
var a = this.getBoundingClientRect();
selection.selectAll(label_class)
.each(function() {
if (this != that) {
var b = this.getBoundingClientRect();
if ((Math.abs(a.left - b.left) * 2 < (a.width + b.width)) && (Math.abs(a.top - b.top) * 2 < (a.height + b.height))) {
var dx = (Math.max(0, a.right - b.left) + Math.min(0, a.left - b.right)) * 0.01;
var dy = (Math.max(0, a.bottom - b.top) + Math.min(0, a.top - b.bottom)) * 0.02;
var tt = getTransformation(d3.select(this)
.attr("transform"));
var to = getTransformation(d3.select(that)
.attr("transform"));
move += Math.abs(dx) + Math.abs(dy);
to.translate = [to.translateX + dx, to.translateY + dy];
tt.translate = [tt.translateX - dx, tt.translateY - dy];
d3.select(this)
.attr("transform", "translate(" + tt.translate + ")");
d3.select(that)
.attr("transform", "translate(" + to.translate + ")");
a = this.getBoundingClientRect();
}
}
});
});
}
}
function wrap(text, width) {
text.each(function() {
var text = d3.select(this);
var words = text.text()
.split(/\s+/)
.reverse();
var word;
var line = [];
var lineHeight = 1;
var y = 0 //text.attr("y");
var x = 0;
var dy = parseFloat(text.attr("dy"));
var dx = parseFloat(text.attr("dx"));
var tspan = text.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y);
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node()
.getComputedTextLength() > width - x) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", x)
.attr("dy", lineHeight + "em")
.attr("dx", dx + "em")
.text(word);
}
}
});
}
@vrevanna
Copy link

vrevanna commented Apr 6, 2018

@martinjc - Overlapping works really well with less number of data, For more number of data it goes for infinite loop and looks messy. Please suggest some changes which accommodates more value range. It can hide the closest range values by showing the highest value in the overlapped position. Please suggest some fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment