controller.doughnut.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. 'use strict';
  2. module.exports = function(Chart) {
  3. var helpers = Chart.helpers,
  4. defaults = Chart.defaults;
  5. defaults.doughnut = {
  6. animation: {
  7. // Boolean - Whether we animate the rotation of the Doughnut
  8. animateRotate: true,
  9. // Boolean - Whether we animate scaling the Doughnut from the centre
  10. animateScale: false
  11. },
  12. aspectRatio: 1,
  13. hover: {
  14. mode: 'single'
  15. },
  16. legendCallback: function(chart) {
  17. var text = [];
  18. text.push('<ul class="' + chart.id + '-legend">');
  19. var data = chart.data;
  20. var datasets = data.datasets;
  21. var labels = data.labels;
  22. if (datasets.length) {
  23. for (var i = 0; i < datasets[0].data.length; ++i) {
  24. text.push('<li><span style="background-color:' + datasets[0].backgroundColor[i] + '"></span>');
  25. if (labels[i]) {
  26. text.push(labels[i]);
  27. }
  28. text.push('</li>');
  29. }
  30. }
  31. text.push('</ul>');
  32. return text.join('');
  33. },
  34. legend: {
  35. labels: {
  36. generateLabels: function(chart) {
  37. var data = chart.data;
  38. if (data.labels.length && data.datasets.length) {
  39. return data.labels.map(function(label, i) {
  40. var meta = chart.getDatasetMeta(0);
  41. var ds = data.datasets[0];
  42. var arc = meta.data[i];
  43. var custom = arc && arc.custom || {};
  44. var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault;
  45. var arcOpts = chart.options.elements.arc;
  46. var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
  47. var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
  48. var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
  49. return {
  50. text: label,
  51. fillStyle: fill,
  52. strokeStyle: stroke,
  53. lineWidth: bw,
  54. hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
  55. // Extra data used for toggling the correct item
  56. index: i
  57. };
  58. });
  59. }
  60. return [];
  61. }
  62. },
  63. onClick: function(e, legendItem) {
  64. var index = legendItem.index;
  65. var chart = this.chart;
  66. var i, ilen, meta;
  67. for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
  68. meta = chart.getDatasetMeta(i);
  69. // toggle visibility of index if exists
  70. if (meta.data[index]) {
  71. meta.data[index].hidden = !meta.data[index].hidden;
  72. }
  73. }
  74. chart.update();
  75. }
  76. },
  77. // The percentage of the chart that we cut out of the middle.
  78. cutoutPercentage: 50,
  79. // The rotation of the chart, where the first data arc begins.
  80. rotation: Math.PI * -0.5,
  81. // The total circumference of the chart.
  82. circumference: Math.PI * 2.0,
  83. // Need to override these to give a nice default
  84. tooltips: {
  85. callbacks: {
  86. title: function() {
  87. return '';
  88. },
  89. label: function(tooltipItem, data) {
  90. var dataLabel = data.labels[tooltipItem.index];
  91. var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
  92. if (helpers.isArray(dataLabel)) {
  93. // show value on first line of multiline label
  94. // need to clone because we are changing the value
  95. dataLabel = dataLabel.slice();
  96. dataLabel[0] += value;
  97. } else {
  98. dataLabel += value;
  99. }
  100. return dataLabel;
  101. }
  102. }
  103. }
  104. };
  105. defaults.pie = helpers.clone(defaults.doughnut);
  106. helpers.extend(defaults.pie, {
  107. cutoutPercentage: 0
  108. });
  109. Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({
  110. dataElementType: Chart.elements.Arc,
  111. linkScales: helpers.noop,
  112. // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
  113. getRingIndex: function(datasetIndex) {
  114. var ringIndex = 0;
  115. for (var j = 0; j < datasetIndex; ++j) {
  116. if (this.chart.isDatasetVisible(j)) {
  117. ++ringIndex;
  118. }
  119. }
  120. return ringIndex;
  121. },
  122. update: function(reset) {
  123. var me = this;
  124. var chart = me.chart,
  125. chartArea = chart.chartArea,
  126. opts = chart.options,
  127. arcOpts = opts.elements.arc,
  128. availableWidth = chartArea.right - chartArea.left - arcOpts.borderWidth,
  129. availableHeight = chartArea.bottom - chartArea.top - arcOpts.borderWidth,
  130. minSize = Math.min(availableWidth, availableHeight),
  131. offset = {
  132. x: 0,
  133. y: 0
  134. },
  135. meta = me.getMeta(),
  136. cutoutPercentage = opts.cutoutPercentage,
  137. circumference = opts.circumference;
  138. // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc
  139. if (circumference < Math.PI * 2.0) {
  140. var startAngle = opts.rotation % (Math.PI * 2.0);
  141. startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0);
  142. var endAngle = startAngle + circumference;
  143. var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)};
  144. var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)};
  145. var contains0 = (startAngle <= 0 && 0 <= endAngle) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle);
  146. var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle);
  147. var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle);
  148. var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle);
  149. var cutout = cutoutPercentage / 100.0;
  150. var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))};
  151. var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))};
  152. var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5};
  153. minSize = Math.min(availableWidth / size.width, availableHeight / size.height);
  154. offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5};
  155. }
  156. chart.borderWidth = me.getMaxBorderWidth(meta.data);
  157. chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0);
  158. chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 1, 0);
  159. chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount();
  160. chart.offsetX = offset.x * chart.outerRadius;
  161. chart.offsetY = offset.y * chart.outerRadius;
  162. meta.total = me.calculateTotal();
  163. me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index));
  164. me.innerRadius = me.outerRadius - chart.radiusLength;
  165. helpers.each(meta.data, function(arc, index) {
  166. me.updateElement(arc, index, reset);
  167. });
  168. },
  169. updateElement: function(arc, index, reset) {
  170. var me = this;
  171. var chart = me.chart,
  172. chartArea = chart.chartArea,
  173. opts = chart.options,
  174. animationOpts = opts.animation,
  175. centerX = (chartArea.left + chartArea.right) / 2,
  176. centerY = (chartArea.top + chartArea.bottom) / 2,
  177. startAngle = opts.rotation, // non reset case handled later
  178. endAngle = opts.rotation, // non reset case handled later
  179. dataset = me.getDataset(),
  180. circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)),
  181. innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius,
  182. outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius,
  183. valueAtIndexOrDefault = helpers.getValueAtIndexOrDefault;
  184. helpers.extend(arc, {
  185. // Utility
  186. _datasetIndex: me.index,
  187. _index: index,
  188. // Desired view properties
  189. _model: {
  190. x: centerX + chart.offsetX,
  191. y: centerY + chart.offsetY,
  192. startAngle: startAngle,
  193. endAngle: endAngle,
  194. circumference: circumference,
  195. outerRadius: outerRadius,
  196. innerRadius: innerRadius,
  197. label: valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index])
  198. }
  199. });
  200. var model = arc._model;
  201. // Resets the visual styles
  202. this.removeHoverStyle(arc);
  203. // Set correct angles if not resetting
  204. if (!reset || !animationOpts.animateRotate) {
  205. if (index === 0) {
  206. model.startAngle = opts.rotation;
  207. } else {
  208. model.startAngle = me.getMeta().data[index - 1]._model.endAngle;
  209. }
  210. model.endAngle = model.startAngle + model.circumference;
  211. }
  212. arc.pivot();
  213. },
  214. removeHoverStyle: function(arc) {
  215. Chart.DatasetController.prototype.removeHoverStyle.call(this, arc, this.chart.options.elements.arc);
  216. },
  217. calculateTotal: function() {
  218. var dataset = this.getDataset();
  219. var meta = this.getMeta();
  220. var total = 0;
  221. var value;
  222. helpers.each(meta.data, function(element, index) {
  223. value = dataset.data[index];
  224. if (!isNaN(value) && !element.hidden) {
  225. total += Math.abs(value);
  226. }
  227. });
  228. /* if (total === 0) {
  229. total = NaN;
  230. }*/
  231. return total;
  232. },
  233. calculateCircumference: function(value) {
  234. var total = this.getMeta().total;
  235. if (total > 0 && !isNaN(value)) {
  236. return (Math.PI * 2.0) * (value / total);
  237. }
  238. return 0;
  239. },
  240. // gets the max border or hover width to properly scale pie charts
  241. getMaxBorderWidth: function(elements) {
  242. var max = 0,
  243. index = this.index,
  244. length = elements.length,
  245. borderWidth,
  246. hoverWidth;
  247. for (var i = 0; i < length; i++) {
  248. borderWidth = elements[i]._model ? elements[i]._model.borderWidth : 0;
  249. hoverWidth = elements[i]._chart ? elements[i]._chart.config.data.datasets[index].hoverBorderWidth : 0;
  250. max = borderWidth > max ? borderWidth : max;
  251. max = hoverWidth > max ? hoverWidth : max;
  252. }
  253. return max;
  254. }
  255. });
  256. };