Donut/Pie Chart (Discrete Values)

A Pie or Donut chart is used to show the percentage of a given population that have a particular characteristic, or the percentage of time that things were in given states.

Note that generally pie charts are not the best visualisation. Because the human eye has more difficulty comparing angles than lines, it's usually better to show this sort of information through a bar-chart.

An example can be found in the Roof Support Mode Donut Chart.

Example

Description

You should replace the following…

ElementReplace With
[PROPERTY]The name of the property you want to report on
[PROPERTYID]The ARDI ID number for the property you want to report on
class ActiveReport extends SVGReport {
	initialise() {
		super.initialise();		
	};
 
	create() {
		this.liveReport("'[PROPERTY]' PROPERTY ALLPOINTS");		
	}
 
	update() {
		this.draw([]);	
	}
 
	draw(data) {
		//Calculate resulting size...		
 
		if (this.group == null)
		{
			// append the svg object to the div called 'my_dataviz'		
			this.group = d3.select("#reportsvg")		  
				.attr("width", width)
				.attr("height", height)
			  .append("g")
				.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
		}
 
		var width = this.width;
		var height = this.height;
		var margin = this.margin;
 
 
		// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
		var radius = Math.min(width, height) / 2 - (margin.left + margin.right);
 
		var svg = this.group;		
		svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
 
		// Create variables to keep information
		var finaldata = [];
		var domain_names = [];
		var domain_colours = [];
		var totalitems = 0;
 
		//For every possible value (in ValueSet[PROPERTYID])...
		for(var q=0;q<ValueSet[PROPERTYID].length;q++)
		{
			//Get the name for that value
			var nm = MapValue[PROPERTYID](ValueSet[PROPERTYID][q]);
 
			//Place a segment in the pie-chart to represent that value
			finaldata.push({v: ValueSet[PROPERTYID][q],name: nm,value: 0});
 
			//If needed, add a colour lookup for that name
			if (!domain_names.includes(nm))
			{
				domain_names.push(nm);
				domain_colours.push(MapColour[PROPERTYID](ValueSet[PROPERTYID][q]));
			}
		}
 
		//Count up the number of items that fit in each category
		for(var q=0;q<this.livedata.length;q++)
		{
			totalitems++;
			var vl = this.livedata[q];
			for(var n=0;n<finaldata.length;n++)
			{
				if (finaldata[n].v == vl)
				{
					finaldata[n].value += 1;
					break;
				}
			}
		}
 
		data = finaldata;
 
		// Create the colour scale
		var color = d3.scaleOrdinal()
		  .domain(domain_names)
		  .range(domain_colours);
 
		// Compute the position of each group on the pie:
		var pie = d3.pie()
		  .sort(null) // Do not sort group by size
		  .value(function(d) {
				return d.value; 
			})		
 
		//Convert the data into pie-chart information
		var data_ready = pie(data);
 
		// The arc generator
		var arc = d3.arc()
			.outerRadius(radius * 0.8)
		    .innerRadius(radius * 0.5);         // This is the size of the donut hole		  
 
		// Another, larger arc. This isn't drawn but is used to position the labels.
		var outerArc = d3.arc()
			.outerRadius(radius * 0.9)
		    .innerRadius(radius * 0.9);		  
 
		// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
		svg.selectAll('.slices')
		  .data(data_ready)
			.join(
				enter => enter.append('path')		  
					.attr('d', arc)
					.attr('fill', function(d){ return(color(d.data.name)) })
					.attr("stroke", "white")
					.style("stroke-width", "2px")
					.style("opacity", 0.7)
					.attr("name",function(d) { return d.data.name; })
					.attr("value",function(d) { return d.data.value; })
					.attr("units"," supports")
					.call(this.tip)
					.attr("class","slices"),
				update => update					
					.attr('d', arc)
					.attr('fill', function(d){ return(color(d.data.name)) })
					.attr("value",function(d) { return d.data.value; })
				);
 
		//NOTE: Strange distortions will occur if you try to transition the arc. 
 
		// Add the polylines between chart and labels:
		svg.selectAll('.pointerlines')
		  .data(data_ready)
		  .join(
			enter => enter.append("polyline")		  
			.attr("stroke", "white")
			.style("fill", "none")
			.attr("class","pointerlines")
			.attr("stroke-width", 1)
			.attr("opacity", function (d) {
				if (d.value == 0) return 0;
				return 1;
			})
			.attr('points', function(d,i) {
			  var posA = arc.centroid(d) // line insertion in the slice
			  var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
			  var posC = outerArc.centroid(d); // Label position = almost the same as posB
			  var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
			  posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
			  posC[1] -= i*8;
			  posB[1] -= i*8;
			  return [posA, posB, posC]
			}),
		  update => update
			.transition()
			.duration(1000)
			.attr('points', function(d,i) {
			  var posA = arc.centroid(d) // line insertion in the slice
			  var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
			  var posC = outerArc.centroid(d); // Label position = almost the same as posB
			  var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
			  posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
			  posC[1] -= i*8;
			  posB[1] -= i*8;
			  return [posA, posB, posC]
			})
			.attr("opacity", function (d) {
					if (d.value == 0) return 0;
					return 1;
				})
		);
 
		// Add the polylines between chart and labels:
		svg
		  .selectAll('.labels')
		  .data(data_ready)
		  .join(
			enter => enter		  
			  .append('text')
				.attr("class","labels")
				.attr("opacity", function (d) {
					if (d.value == 0) return 0;
					return 1;
				})
				.text( function(d) { return d.data.name + " ( " + d.data.value + "/" + totalitems + " )" } )
				.attr('transform', function(d,i) {
					var pos = outerArc.centroid(d);
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
					pos[1] -= i*8;
					return 'translate(' + pos + ')';
				})
				.attr("fill","white")
				.style('text-anchor', function(d) {
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					return (midangle < Math.PI ? 'start' : 'end')
				}),
			update => update
				.text( function(d) { return d.data.name + " ( " + d.data.value + "/" + totalitems + " )" } )
				.style('text-anchor', function(d) {
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					return (midangle < Math.PI ? 'start' : 'end')
				})
				.transition()
				.duration(1000)
				.attr('transform', function(d,i) {
					var pos = outerArc.centroid(d);
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
					pos[1] -= i*8;
					return 'translate(' + pos + ')';
				})
				.attr("opacity", function (d) {
					if (d.value == 0) return 0;
					return 1;
				})
			);
	}	
}