Donut/Pie Chart (Custom Logic)

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 Supports On Roof display.

Example

Description

You should replace the following…

ElementReplace With
[PROPERTY]The name of the property you want to report on
class ActiveReport extends SVGReport {
	initialise() {
		super.initialise();		
	};
 
	create() {
		this.liveReport("'[PROPERTY]' ALLPOINTS");		
	}
 
	update() {
		this.draw([]);	
	}
 
	draw(data) {
		//Calculate resulting size...		
 
		if (this.group == null)
		{
			//Create an interior group to draw into
			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 empty totals
		var totalitems = 0;
 
                //Here's where you define the different pie segments
		var finaldata = [{name: "Off-Roof",value: 0},{name: "On-Roof", value: 0}];
 
		// Create the colour scale
		var color = d3.scaleOrdinal()
		  .domain(["Off-Roof","On-Roof"])
		  .range(["yellow","green"]);
 
		//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];
 
                        //Here's where you place your conditions. In this case, if it's less than or more than 300.
			if (vl > 300)
				finaldata[1].value++;
			else
				finaldata[0].value++;			
		}
 
		data = finaldata;
 
		// 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) {
			  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
			  return [posA, posB, posC]
			}),
		  update => update
			.transition()
			.duration(1000)
			.attr('points', function(d) {
			  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
			  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) {
					var pos = outerArc.centroid(d);
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
					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) {
					var pos = outerArc.centroid(d);
					var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
					pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
					return 'translate(' + pos + ')';
				})
				.attr("opacity", function (d) {
					if (d.value == 0) return 0;
					return 1;
				})
			);
	}	
}