;(function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module with d3 as a dependency. define(['d3'], factory) } else if (typeof module === 'object' && module.exports) { /* eslint-disable global-require */ // CommonJS var d3 = require('d3') module.exports = factory(d3) /* eslint-enable global-require */ } else { // Browser global. // eslint-disable-next-line no-param-reassign donutChart = factory(root.d3) } }(this, function(d3) { return function() { var data = [], width, height, margin = {top: 10, right: 10, bottom: 10, left: 10}, colour = d3.scaleOrdinal(d3.schemeCategory20), // colour scheme variable, // value in data that will dictate proportions on chart category, // compare data by padAngle, // effectively dictates the gap between slices transTime, // transition time showLabels = true, showLegend = false, updateData, floatFormat = d3.format('.4r'), cornerRadius, // sets how rounded the corners are on each slice percentFormat = d3.format(',.2%'); function chart(selection){ // margin.right += 70; console.log("selection.node(): ", selection.node()) selection.each(function() { // generate chart // =========================================================================================== // Set up constructors for making donut. See https://github.com/d3/d3-shape/blob/master/README.md var radius = Math.min(width, height) / 2; // creates a new pie generator var pie = d3.pie() .value(function(d) { return floatFormat(d[variable]); }) .sort(null); // contructs and arc generator. This will be used for the donut. The difference between outer and inner // radius will dictate the thickness of the donut var arc = d3.arc() .outerRadius(radius * 0.8) .innerRadius(radius * 0.6) .cornerRadius(cornerRadius) .padAngle(padAngle); // this arc is used for aligning the text labels var outerArc = d3.arc() .outerRadius(radius * 0.9) .innerRadius(radius * 0.9); // =========================================================================================== // =========================================================================================== // append the svg object to the selection // var svg = selection.append('svg') var node = selection.node(); var svg = selection.append('svg') // .attr('width', node.offsetWidth) // .attr('height', node.offsetHeight); .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom); var donutWidth = showLegend ? 0.7 * width : width; var g = svg.append('g') .attr('transform', 'translate(' + width / 2 + ',' + 2*height / 3 + ')'); // .attr('transform', 'translate(' + (((width - margin.left - margin.right)/ 2)) + ',' + height / 2 + ')'); // .attr("transform", "translate(" + margin.left + "," + margin.top + ")") // =========================================================================================== // =========================================================================================== // g elements to keep elements within svg modular g.append('g').attr('class', 'slices'); g.append('g').attr('class', 'labelName'); g.append('g').attr('class', 'lines'); // =========================================================================================== // =========================================================================================== // add and colour the donut slices var path = g.select('.slices') .selectAll('path') .data(pie(data)) .enter().append('path') .attr('fill', function(d) { return colour(d.data[category]); }) .attr('d', arc); // =========================================================================================== if (showLabels) { // =========================================================================================== // add text labels var label = g.select('.labelName').selectAll('text') .data(pie(data)) .enter().append('text') .attr('dy', '.35em') .html(updateLabelText) .attr('transform', labelTransform) .style('text-anchor', function(d) { // if slice centre is on the left, anchor text to start, otherwise anchor to end return (midAngle(d)) < Math.PI ? 'start' : 'end'; }); // =========================================================================================== // =========================================================================================== // add lines connecting labels to slice. A polyline creates straight lines connecting several points var polyline = g.select('.lines') .selectAll('polyline') .data(pie(data)) .enter().append('polyline') .attr('points', calculatePoints); // =========================================================================================== } // =========================================================================================== // add tooltip to mouse events on slices and labels selection.selectAll('.labelName text, .slices path').call(toolTip); // =========================================================================================== if (showLegend) { var legend = svg.append("g") .attr("font-family", "sans-serif") .attr("font-size", 10) .attr("text-anchor", "start") .selectAll("g") .data(data) .enter().append("g") .attr("transform", function(d, i) { return "translate(0," + i * 16 + ")"; }); legend.append("circle") .attr("cx", width - 70) .attr("cy", 9) .attr("r", 4) .attr("fill", function(d) { return colour(d[category]) }); legend.append("text") .attr("x", width - 64) .attr("y", 9.5) .attr("dy", "0.32em") .text(function(d) { return d[category]; }); } // =========================================================================================== // FUNCTION TO UPDATE CHART updateData = function() { var updatePath = d3.select('.slices').selectAll('path'); var updateLines = d3.select('.lines').selectAll('polyline'); var updateLabels = d3.select('.labelName').selectAll('text'); var data0 = path.data(), // store the current data before updating to the new data1 = pie(data); // update data attached to the slices, labels, and polylines. the key function assigns the data to // the correct element, rather than in order of how the data appears. This means that if a category // already exists in the chart, it will have its data updated rather than removed and re-added. updatePath = updatePath.data(data1, key); updateLines = updateLines.data(data1, key); updateLabels = updateLabels.data(data1, key); // adds new slices/lines/labels updatePath.enter().append('path') .each(function(d, i) { this._current = findNeighborArc(i, data0, data1, key) || d; }) .attr('fill', function(d) { return colour(d.data[category]); }) .attr('d', arc); updateLines.enter().append('polyline') .each(function(d, i) { this._current = findNeighborArc(i, data0, data1, key) || d; }) .attr('points', calculatePoints); updateLabels.enter().append('text') .each(function(d, i) { this._current = findNeighborArc(i, data0, data1, key) || d; }) .html(updateLabelText) .attr('transform', labelTransform) .style('text-anchor', function(d) { return (midAngle(d)) < Math.PI ? 'start' : 'end'; }); // removes slices/labels/lines that are not in the current dataset updatePath.exit() .transition() .duration(transTime) .attrTween("d", arcTween) .remove(); updateLines.exit() .transition() .duration(transTime) .attrTween("points", pointTween) .remove(); updateLabels.exit() .remove(); // animates the transition from old angle to new angle for slices/lines/labels updatePath.transition().duration(transTime) .attrTween('d', arcTween); updateLines.transition().duration(transTime) .attrTween('points', pointTween); updateLabels.transition().duration(transTime) .attrTween('transform', labelTween) .styleTween('text-anchor', labelStyleTween); updateLabels.html(updateLabelText); // update the label text // add tooltip to mouse events on slices and labels d3.selectAll('.labelName text, .slices path').call(toolTip); }; // =========================================================================================== // Functions // calculates the angle for the middle of a slice function midAngle(d) { return d.startAngle + (d.endAngle - d.startAngle) / 2; } // function that creates and adds the tool tip to a selected element function toolTip(selection) { // add tooltip (svg circle element) when mouse enters label or slice selection.on('mouseenter', showToolCircle); // remove the tooltip when mouse leaves the slice/label selection.on('mouseout', function () { d3.selectAll('.toolCircle').remove(); }); } function showToolCircle(data) { g.append('text') .attr('class', 'toolCircle') .attr('dy', -15) // hard-coded. can adjust this to adjust text vertical alignment in tooltip .html(toolTipHTML(data)) // add text to the circle. .style('font-size', '.7em') .style('text-anchor', 'middle'); // centres text in tooltip g.append('circle') .attr('class', 'toolCircle') .attr('r', radius * 0.55) // radius of tooltip circle .style('fill', colour(data.data[category])) // colour based on category mouse is over .style('fill-opacity', 0.35); } // function to create the HTML string for the tool tip. Loops through each key in data object // and returns the html string key: value function toolTipHTML(data) { var tip = '', i = 0; for (var key in data.data) { // if value is a number, format it as a percentage // var value = (!isNaN(parseFloat(data.data[key]))) ? percentFormat(data.data[key]) : data.data[key]; var value = data.data[key]; // leave off 'dy' attr for first tspan so the 'dy' attr on text element works. The 'dy' attr on // tspan effectively imitates a line break. if (i === 0) tip += '' + key + ': ' + value + ''; else tip += '' + key + ': ' + value + ''; i++; } return tip; } // calculate the points for the polyline to pass through function calculatePoints(d) { // see label transform function for explanations of these three lines. var pos = outerArc.centroid(d); pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1); return [arc.centroid(d), outerArc.centroid(d), pos] } function labelTransform(d) { // effectively computes the centre of the slice. // see https://github.com/d3/d3-shape/blob/master/README.md#arc_centroid var pos = outerArc.centroid(d); // changes the point to be on left or right depending on where label is. pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1); return 'translate(' + pos + ')'; } function updateLabelText(d) { return d.data[category] + ': ' + percentFormat(d.data[variable]) + ''; } // function that calculates transition path for label and also it's text anchoring function labelStyleTween(d) { this._current = this._current || d; var interpolate = d3.interpolate(this._current, d); this._current = interpolate(0); return function(t){ var d2 = interpolate(t); return midAngle(d2) < Math.PI ? 'start':'end'; }; } function labelTween(d) { this._current = this._current || d; var interpolate = d3.interpolate(this._current, d); this._current = interpolate(0); return function(t){ var d2 = interpolate(t), pos = outerArc.centroid(d2); // computes the midpoint [x,y] of the centre line that would be // generated by the given arguments. It is defined as startangle + endangle/2 and innerR + outerR/2 pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1); // aligns the labels on the sides return 'translate(' + pos + ')'; }; } function pointTween(d) { this._current = this._current || d; var interpolate = d3.interpolate(this._current, d); this._current = interpolate(0); return function(t){ var d2 = interpolate(t), pos = outerArc.centroid(d2); pos[0] = radius * 0.95 * (midAngle(d2) < Math.PI ? 1 : -1); return [arc.centroid(d2), outerArc.centroid(d2), pos]; }; } // function to calculate the tween for an arc's transition. // see http://bl.ocks.org/mbostock/5100636 for a thorough explanation. function arcTween(d) { var i = d3.interpolate(this._current, d); this._current = i(0); return function(t) { return arc(i(t)); }; } function findNeighborArc(i, data0, data1, key) { var d; return (d = findPreceding(i, data0, data1, key)) ? {startAngle: d.endAngle, endAngle: d.endAngle} : (d = findFollowing(i, data0, data1, key)) ? {startAngle: d.startAngle, endAngle: d.startAngle} : null; } // Find the element in data0 that joins the highest preceding element in data1. function findPreceding(i, data0, data1, key) { var m = data0.length; while (--i >= 0) { var k = key(data1[i]); for (var j = 0; j < m; ++j) { if (key(data0[j]) === k) return data0[j]; } } } function key(d) { return d.data[category]; } // Find the element in data0 that joins the lowest following element in data1. function findFollowing(i, data0, data1, key) { var n = data1.length, m = data0.length; while (++i < n) { var k = key(data1[i]); for (var j = 0; j < m; ++j) { if (key(data0[j]) === k) return data0[j]; } } } // =========================================================================================== }); } // getter and setter functions. See Mike Bostocks post "Towards Reusable Charts" for a tutorial on how this works. chart.width = function(value) { if (!arguments.length) return width; width = value; return chart; }; chart.height = function(value) { if (!arguments.length) return height; height = value; return chart; }; chart.margin = function(value) { if (!arguments.length) return margin; margin = value; // if (showLegend) { // margin.right += 60; // } return chart; }; chart.radius = function(value) { if (!arguments.length) return radius; radius = value; return chart; }; chart.padAngle = function(value) { if (!arguments.length) return padAngle; padAngle = value; return chart; }; chart.cornerRadius = function(value) { if (!arguments.length) return cornerRadius; cornerRadius = value; return chart; }; chart.colour = function(value) { if (!arguments.length) return colour; colour = value; return chart; }; chart.variable = function(value) { if (!arguments.length) return variable; variable = value; return chart; }; chart.category = function(value) { if (!arguments.length) return category; category = value; return chart; }; chart.transTime = function(value) { if (!arguments.length) return transTime; transTime = value; return chart; }; chart.showLabels = function(value) { if (!arguments.length) return showLabels; showLabels = value; return chart; }; // chart.toolTipHTML = function(value) { // if (!arguments.length) return toolTipHTML; // toolTipHTML = value; // return chart; // }; chart.showLegend = function(value) { if (!arguments.length) return showLegend; showLegend = value; // margin.right += 60; return chart; }; chart.data = function(value) { if (!arguments.length) return data; data = value; if (typeof updateData === 'function') updateData(); return chart; }; return chart; } }));