angular-chart.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. (function (factory) {
  2. 'use strict';
  3. if (typeof exports === 'object') {
  4. // Node/CommonJS
  5. module.exports = factory(
  6. typeof angular !== 'undefined' ? angular : require('angular'),
  7. typeof Chart !== 'undefined' ? Chart : require('chart.js'));
  8. } else if (typeof define === 'function' && define.amd) {
  9. // AMD. Register as an anonymous module.
  10. define(['chart'], factory);
  11. } else {
  12. // Browser globals
  13. if (typeof angular === 'undefined') {
  14. throw new Error('AngularJS framework needs to be included, see https://angularjs.org/');
  15. } else if (typeof Chart === 'undefined') {
  16. throw new Error('Chart.js library needs to be included, see http://jtblin.github.io/angular-chart.js/');
  17. }
  18. factory(angular, Chart);
  19. }
  20. }(function (Chart) {
  21. 'use strict';
  22. Chart.defaults.global.multiTooltipTemplate = '<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>';
  23. Chart.defaults.global.tooltips.mode = 'label';
  24. Chart.defaults.global.elements.line.borderWidth = 2;
  25. Chart.defaults.global.elements.rectangle.borderWidth = 2;
  26. Chart.defaults.global.legend.display = false;
  27. Chart.defaults.global.colors = [
  28. '#97BBCD', // blue
  29. '#DCDCDC', // light grey
  30. '#F7464A', // red
  31. '#46BFBD', // green
  32. '#FDB45C', // yellow
  33. '#949FB1', // grey
  34. '#4D5360' // dark grey
  35. ];
  36. var useExcanvas = typeof window.G_vmlCanvasManager === 'object' &&
  37. window.G_vmlCanvasManager !== null &&
  38. typeof window.G_vmlCanvasManager.initElement === 'function';
  39. if (useExcanvas) Chart.defaults.global.animation = false;
  40. return angular.module('chart.js', [])
  41. .provider('ChartJs', ChartJsProvider)
  42. .factory('ChartJsFactory', ['ChartJs', '$timeout', ChartJsFactory])
  43. .directive('chartBase', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory(); }])
  44. .directive('chartLine', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('line'); }])
  45. .directive('chartBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bar'); }])
  46. .directive('chartHorizontalBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('horizontalBar'); }])
  47. .directive('chartRadar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('radar'); }])
  48. .directive('chartDoughnut', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('doughnut'); }])
  49. .directive('chartPie', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('pie'); }])
  50. .directive('chartPolarArea', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('polarArea'); }])
  51. .directive('chartBubble', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bubble'); }])
  52. .name;
  53. /**
  54. * Wrapper for chart.js
  55. * Allows configuring chart js using the provider
  56. *
  57. * angular.module('myModule', ['chart.js']).config(function(ChartJsProvider) {
  58. * ChartJsProvider.setOptions({ responsive: false });
  59. * ChartJsProvider.setOptions('Line', { responsive: true });
  60. * })))
  61. */
  62. function ChartJsProvider () {
  63. var options = { responsive: true };
  64. var ChartJs = {
  65. Chart: Chart,
  66. getOptions: function (type) {
  67. var typeOptions = type && options[type] || {};
  68. return angular.extend({}, options, typeOptions);
  69. }
  70. };
  71. /**
  72. * Allow to set global options during configuration
  73. */
  74. this.setOptions = function (type, customOptions) {
  75. // If no type was specified set option for the global object
  76. if (! customOptions) {
  77. customOptions = type;
  78. options = angular.merge(options, customOptions);
  79. } else {
  80. // Set options for the specific chart
  81. options[type] = angular.merge(options[type] || {}, customOptions);
  82. }
  83. angular.merge(ChartJs.Chart.defaults, options);
  84. };
  85. this.$get = function () {
  86. return ChartJs;
  87. };
  88. }
  89. function ChartJsFactory (ChartJs, $timeout) {
  90. return function chart (type) {
  91. return {
  92. restrict: 'CA',
  93. scope: {
  94. chartGetColor: '=?',
  95. chartType: '=',
  96. chartData: '=?',
  97. chartLabels: '=?',
  98. chartOptions: '=?',
  99. chartSeries: '=?',
  100. chartColors: '=?',
  101. chartClick: '=?',
  102. chartHover: '=?',
  103. chartDatasetOverride: '=?'
  104. },
  105. link: function (scope, elem/*, attrs */) {
  106. if (useExcanvas) window.G_vmlCanvasManager.initElement(elem[0]);
  107. // Order of setting "watch" matter
  108. scope.$watch('chartData', watchData, true);
  109. scope.$watch('chartSeries', watchOther, true);
  110. scope.$watch('chartLabels', watchOther, true);
  111. scope.$watch('chartOptions', watchOther, true);
  112. scope.$watch('chartColors', watchOther, true);
  113. scope.$watch('chartDatasetOverride', watchOther, true);
  114. scope.$watch('chartType', watchType, false);
  115. scope.$on('$destroy', function () {
  116. destroyChart(scope);
  117. });
  118. scope.$on('$resize', function () {
  119. if (scope.chart) scope.chart.resize();
  120. });
  121. function watchData (newVal, oldVal) {
  122. if (! newVal || ! newVal.length || (Array.isArray(newVal[0]) && ! newVal[0].length)) {
  123. destroyChart(scope);
  124. return;
  125. }
  126. var chartType = type || scope.chartType;
  127. if (! chartType) return;
  128. if (scope.chart && canUpdateChart(newVal, oldVal))
  129. return updateChart(newVal, scope);
  130. createChart(chartType, scope, elem);
  131. }
  132. function watchOther (newVal, oldVal) {
  133. if (isEmpty(newVal)) return;
  134. if (angular.equals(newVal, oldVal)) return;
  135. var chartType = type || scope.chartType;
  136. if (! chartType) return;
  137. // chart.update() doesn't work for series and labels
  138. // so we have to re-create the chart entirely
  139. createChart(chartType, scope, elem);
  140. }
  141. function watchType (newVal, oldVal) {
  142. if (isEmpty(newVal)) return;
  143. if (angular.equals(newVal, oldVal)) return;
  144. createChart(newVal, scope, elem);
  145. }
  146. }
  147. };
  148. };
  149. function createChart (type, scope, elem) {
  150. var options = getChartOptions(type, scope);
  151. if (! hasData(scope) || ! canDisplay(type, scope, elem, options)) return;
  152. var cvs = elem[0];
  153. var ctx = cvs.getContext('2d');
  154. scope.chartGetColor = getChartColorFn(scope);
  155. var data = getChartData(type, scope);
  156. // Destroy old chart if it exists to avoid ghost charts issue
  157. // https://github.com/jtblin/angular-chart.js/issues/187
  158. destroyChart(scope);
  159. scope.chart = new ChartJs.Chart(ctx, {
  160. type: type,
  161. data: data,
  162. options: options
  163. });
  164. scope.$emit('chart-create', scope.chart);
  165. bindEvents(cvs, scope);
  166. }
  167. function canUpdateChart (newVal, oldVal) {
  168. if (newVal && oldVal && newVal.length && oldVal.length) {
  169. return Array.isArray(newVal[0]) ?
  170. newVal.length === oldVal.length && newVal.every(function (element, index) {
  171. return element.length === oldVal[index].length; }) :
  172. oldVal.reduce(sum, 0) > 0 ? newVal.length === oldVal.length : false;
  173. }
  174. return false;
  175. }
  176. function sum (carry, val) {
  177. return carry + val;
  178. }
  179. function getEventHandler (scope, action, triggerOnlyOnChange) {
  180. var lastState = {
  181. point: void 0,
  182. points: void 0
  183. };
  184. return function (evt) {
  185. var atEvent = scope.chart.getElementAtEvent || scope.chart.getPointAtEvent;
  186. var atEvents = scope.chart.getElementsAtEvent || scope.chart.getPointsAtEvent;
  187. if (atEvents) {
  188. var points = atEvents.call(scope.chart, evt);
  189. var point = atEvent ? atEvent.call(scope.chart, evt)[0] : void 0;
  190. if (triggerOnlyOnChange === false ||
  191. (! angular.equals(lastState.points, points) && ! angular.equals(lastState.point, point))
  192. ) {
  193. lastState.point = point;
  194. lastState.points = points;
  195. scope[action](points, evt, point);
  196. }
  197. }
  198. };
  199. }
  200. function getColors (type, scope) {
  201. var colors = angular.copy(scope.chartColors ||
  202. ChartJs.getOptions(type).chartColors ||
  203. Chart.defaults.global.colors
  204. );
  205. var notEnoughColors = colors.length < scope.chartData.length;
  206. while (colors.length < scope.chartData.length) {
  207. colors.push(scope.chartGetColor());
  208. }
  209. // mutate colors in this case as we don't want
  210. // the colors to change on each refresh
  211. if (notEnoughColors) scope.chartColors = colors;
  212. return colors.map(convertColor);
  213. }
  214. function convertColor (color) {
  215. // Allows RGB and RGBA colors to be input as a string: e.g.: "rgb(159,204,0)", "rgba(159,204,0, 0.5)"
  216. if (typeof color === 'string' && color[0] === 'r') return getColor(rgbStringToRgb(color));
  217. // Allows hex colors to be input as a string.
  218. if (typeof color === 'string' && color[0] === '#') return getColor(hexToRgb(color.substr(1)));
  219. // Allows colors to be input as an object, bypassing getColor() entirely
  220. if (typeof color === 'object' && color !== null) return color;
  221. return getRandomColor();
  222. }
  223. function getRandomColor () {
  224. var color = [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
  225. return getColor(color);
  226. }
  227. function getColor (color) {
  228. var alpha = color[3] || 1;
  229. color = color.slice(0, 3);
  230. return {
  231. backgroundColor: rgba(color, 0.2),
  232. pointBackgroundColor: rgba(color, alpha),
  233. pointHoverBackgroundColor: rgba(color, 0.8),
  234. borderColor: rgba(color, alpha),
  235. pointBorderColor: '#fff',
  236. pointHoverBorderColor: rgba(color, alpha)
  237. };
  238. }
  239. function getRandomInt (min, max) {
  240. return Math.floor(Math.random() * (max - min + 1)) + min;
  241. }
  242. function rgba (color, alpha) {
  243. // rgba not supported by IE8
  244. return useExcanvas ? 'rgb(' + color.join(',') + ')' : 'rgba(' + color.concat(alpha).join(',') + ')';
  245. }
  246. // Credit: http://stackoverflow.com/a/11508164/1190235
  247. function hexToRgb (hex) {
  248. var bigint = parseInt(hex, 16),
  249. r = (bigint >> 16) & 255,
  250. g = (bigint >> 8) & 255,
  251. b = bigint & 255;
  252. return [r, g, b];
  253. }
  254. function rgbStringToRgb (color) {
  255. var match = color.match(/^rgba?\(([\d,.]+)\)$/);
  256. if (! match) throw new Error('Cannot parse rgb value');
  257. color = match[1].split(',');
  258. return color.map(Number);
  259. }
  260. function hasData (scope) {
  261. return scope.chartData && scope.chartData.length;
  262. }
  263. function getChartColorFn (scope) {
  264. return typeof scope.chartGetColor === 'function' ? scope.chartGetColor : getRandomColor;
  265. }
  266. function getChartData (type, scope) {
  267. var colors = getColors(type, scope);
  268. return Array.isArray(scope.chartData[0]) ?
  269. getDataSets(scope.chartLabels, scope.chartData, scope.chartSeries || [], colors, scope.chartDatasetOverride) :
  270. getData(scope.chartLabels, scope.chartData, colors, scope.chartDatasetOverride);
  271. }
  272. function getDataSets (labels, data, series, colors, datasetOverride) {
  273. return {
  274. labels: labels,
  275. datasets: data.map(function (item, i) {
  276. var dataset = angular.extend({}, colors[i], {
  277. label: series[i],
  278. data: item
  279. });
  280. if (datasetOverride && datasetOverride.length >= i) {
  281. angular.merge(dataset, datasetOverride[i]);
  282. }
  283. return dataset;
  284. })
  285. };
  286. }
  287. function getData (labels, data, colors, datasetOverride) {
  288. var dataset = {
  289. labels: labels,
  290. datasets: [{
  291. data: data,
  292. backgroundColor: colors.map(function (color) {
  293. return color.pointBackgroundColor;
  294. }),
  295. hoverBackgroundColor: colors.map(function (color) {
  296. return color.backgroundColor;
  297. })
  298. }]
  299. };
  300. if (datasetOverride) {
  301. angular.merge(dataset.datasets[0], datasetOverride);
  302. }
  303. return dataset;
  304. }
  305. function getChartOptions (type, scope) {
  306. return angular.extend({}, ChartJs.getOptions(type), scope.chartOptions);
  307. }
  308. function bindEvents (cvs, scope) {
  309. cvs.onclick = scope.chartClick ? getEventHandler(scope, 'chartClick', false) : angular.noop;
  310. cvs.onmousemove = scope.chartHover ? getEventHandler(scope, 'chartHover', true) : angular.noop;
  311. }
  312. function updateChart (values, scope) {
  313. if (Array.isArray(scope.chartData[0])) {
  314. scope.chart.data.datasets.forEach(function (dataset, i) {
  315. dataset.data = values[i];
  316. });
  317. } else {
  318. scope.chart.data.datasets[0].data = values;
  319. }
  320. scope.chart.update();
  321. scope.$emit('chart-update', scope.chart);
  322. }
  323. function isEmpty (value) {
  324. return ! value ||
  325. (Array.isArray(value) && ! value.length) ||
  326. (typeof value === 'object' && ! Object.keys(value).length);
  327. }
  328. function canDisplay (type, scope, elem, options) {
  329. // TODO: check parent?
  330. if (options.responsive && elem[0].clientHeight === 0) {
  331. $timeout(function () {
  332. createChart(type, scope, elem);
  333. }, 50, false);
  334. return false;
  335. }
  336. return true;
  337. }
  338. function destroyChart(scope) {
  339. if(! scope.chart) return;
  340. scope.chart.destroy();
  341. scope.$emit('chart-destroy', scope.chart);
  342. }
  343. }
  344. }));