angular-chart.js 16 KB

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