2 * angular-chart.js - An angular.js wrapper for Chart.js
3 * http://jtblin.github.io/angular-chart.js/
6 * Copyright 2016 Jerome Touffe-Blin
7 * Released under the BSD-2-Clause license
8 * https://github.com/jtblin/angular-chart.js/blob/master/LICENSE
12 if (typeof exports === 'object') {
14 module.exports = factory(
15 typeof angular !== 'undefined' ? angular : require('angular'),
16 typeof Chart !== 'undefined' ? Chart : require('chart.js'));
17 } else if (typeof define === 'function' && define.amd) {
18 // AMD. Register as an anonymous module.
19 define(['angular', 'app/mwtnCommons/bower_components/chart.js/dist/Chart'], factory);
22 if (typeof angular === 'undefined') {
23 throw new Error('AngularJS framework needs to be included, see https://angularjs.org/');
24 } else if (typeof Chart === 'undefined') {
25 throw new Error('Chart.js library needs to be included, see http://jtblin.github.io/angular-chart.js/');
27 factory(angular, Chart);
29 }(function (angular, Chart) {
32 Chart.defaults.global.multiTooltipTemplate = '<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>';
33 Chart.defaults.global.tooltips.mode = 'label';
34 Chart.defaults.global.elements.line.borderWidth = 2;
35 Chart.defaults.global.elements.rectangle.borderWidth = 2;
36 Chart.defaults.global.legend.display = false;
37 Chart.defaults.global.colors = [
39 '#DCDCDC', // light grey
44 '#4D5360' // dark grey
47 var useExcanvas = typeof window.G_vmlCanvasManager === 'object' &&
48 window.G_vmlCanvasManager !== null &&
49 typeof window.G_vmlCanvasManager.initElement === 'function';
51 if (useExcanvas) Chart.defaults.global.animation = false;
53 return angular.module('chart.js', [])
54 .provider('ChartJs', ChartJsProvider)
55 .factory('ChartJsFactory', ['ChartJs', '$timeout', ChartJsFactory])
56 .directive('chartBase', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory(); }])
57 .directive('chartLine', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('line'); }])
58 .directive('chartBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bar'); }])
59 .directive('chartHorizontalBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('horizontalBar'); }])
60 .directive('chartRadar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('radar'); }])
61 .directive('chartDoughnut', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('doughnut'); }])
62 .directive('chartPie', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('pie'); }])
63 .directive('chartPolarArea', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('polarArea'); }])
64 .directive('chartBubble', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bubble'); }])
68 * Wrapper for chart.js
69 * Allows configuring chart js using the provider
71 * angular.module('myModule', ['chart.js']).config(function(ChartJsProvider) {
72 * ChartJsProvider.setOptions({ responsive: false });
73 * ChartJsProvider.setOptions('Line', { responsive: true });
76 function ChartJsProvider () {
77 var options = { responsive: true };
80 getOptions: function (type) {
81 var typeOptions = type && options[type] || {};
82 return angular.extend({}, options, typeOptions);
87 * Allow to set global options during configuration
89 this.setOptions = function (type, customOptions) {
90 // If no type was specified set option for the global object
91 if (! customOptions) {
93 options = angular.merge(options, customOptions);
95 // Set options for the specific chart
96 options[type] = angular.merge(options[type] || {}, customOptions);
99 angular.merge(ChartJs.Chart.defaults, options);
102 this.$get = function () {
107 function ChartJsFactory (ChartJs, $timeout) {
108 return function chart (type) {
121 chartDatasetOverride: '=?'
123 link: function (scope, elem/*, attrs */) {
124 if (useExcanvas) window.G_vmlCanvasManager.initElement(elem[0]);
126 // Order of setting "watch" matter
127 scope.$watch('chartData', watchData, true);
128 scope.$watch('chartSeries', watchOther, true);
129 scope.$watch('chartLabels', watchOther, true);
130 scope.$watch('chartOptions', watchOther, true);
131 scope.$watch('chartColors', watchOther, true);
132 scope.$watch('chartDatasetOverride', watchOther, true);
133 scope.$watch('chartType', watchType, false);
135 scope.$on('$destroy', function () {
139 scope.$on('$resize', function () {
140 if (scope.chart) scope.chart.resize();
143 function watchData (newVal, oldVal) {
144 if (! newVal || ! newVal.length || (Array.isArray(newVal[0]) && ! newVal[0].length)) {
148 var chartType = type || scope.chartType;
149 if (! chartType) return;
151 if (scope.chart && canUpdateChart(newVal, oldVal))
152 return updateChart(newVal, scope);
154 createChart(chartType, scope, elem);
157 function watchOther (newVal, oldVal) {
158 if (isEmpty(newVal)) return;
159 if (angular.equals(newVal, oldVal)) return;
160 var chartType = type || scope.chartType;
161 if (! chartType) return;
163 // chart.update() doesn't work for series and labels
164 // so we have to re-create the chart entirely
165 createChart(chartType, scope, elem);
168 function watchType (newVal, oldVal) {
169 if (isEmpty(newVal)) return;
170 if (angular.equals(newVal, oldVal)) return;
171 createChart(newVal, scope, elem);
177 function createChart (type, scope, elem) {
178 var options = getChartOptions(type, scope);
179 if (! hasData(scope) || ! canDisplay(type, scope, elem, options)) return;
182 var ctx = cvs.getContext('2d');
184 scope.chartGetColor = getChartColorFn(scope);
185 var data = getChartData(type, scope);
186 // Destroy old chart if it exists to avoid ghost charts issue
187 // https://github.com/jtblin/angular-chart.js/issues/187
190 scope.chart = new ChartJs.Chart(ctx, {
195 scope.$emit('chart-create', scope.chart);
196 bindEvents(cvs, scope);
199 function canUpdateChart (newVal, oldVal) {
200 if (newVal && oldVal && newVal.length && oldVal.length) {
201 return Array.isArray(newVal[0]) ?
202 newVal.length === oldVal.length && newVal.every(function (element, index) {
203 return element.length === oldVal[index].length; }) :
204 oldVal.reduce(sum, 0) > 0 ? newVal.length === oldVal.length : false;
209 function sum (carry, val) {
213 function getEventHandler (scope, action, triggerOnlyOnChange) {
218 return function (evt) {
219 var atEvent = scope.chart.getElementAtEvent || scope.chart.getPointAtEvent;
220 var atEvents = scope.chart.getElementsAtEvent || scope.chart.getPointsAtEvent;
222 var points = atEvents.call(scope.chart, evt);
223 var point = atEvent ? atEvent.call(scope.chart, evt)[0] : void 0;
225 if (triggerOnlyOnChange === false ||
226 (! angular.equals(lastState.points, points) && ! angular.equals(lastState.point, point))
228 lastState.point = point;
229 lastState.points = points;
230 scope[action](points, evt, point);
236 function getColors (type, scope) {
237 var colors = angular.copy(scope.chartColors ||
238 ChartJs.getOptions(type).chartColors ||
239 Chart.defaults.global.colors
241 var notEnoughColors = colors.length < scope.chartData.length;
242 while (colors.length < scope.chartData.length) {
243 colors.push(scope.chartGetColor());
245 // mutate colors in this case as we don't want
246 // the colors to change on each refresh
247 if (notEnoughColors) scope.chartColors = colors;
248 return colors.map(convertColor);
251 function convertColor (color) {
252 // Allows RGB and RGBA colors to be input as a string: e.g.: "rgb(159,204,0)", "rgba(159,204,0, 0.5)"
253 if (typeof color === 'string' && color[0] === 'r') return getColor(rgbStringToRgb(color));
254 // Allows hex colors to be input as a string.
255 if (typeof color === 'string' && color[0] === '#') return getColor(hexToRgb(color.substr(1)));
256 // Allows colors to be input as an object, bypassing getColor() entirely
257 if (typeof color === 'object' && color !== null) return color;
258 return getRandomColor();
261 function getRandomColor () {
262 var color = [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
263 return getColor(color);
266 function getColor (color) {
267 var alpha = color[3] || 1;
268 color = color.slice(0, 3);
270 backgroundColor: rgba(color, 0.2),
271 pointBackgroundColor: rgba(color, alpha),
272 pointHoverBackgroundColor: rgba(color, 0.8),
273 borderColor: rgba(color, alpha),
274 pointBorderColor: '#fff',
275 pointHoverBorderColor: rgba(color, alpha)
279 function getRandomInt (min, max) {
280 return Math.floor(Math.random() * (max - min + 1)) + min;
283 function rgba (color, alpha) {
284 // rgba not supported by IE8
285 return useExcanvas ? 'rgb(' + color.join(',') + ')' : 'rgba(' + color.concat(alpha).join(',') + ')';
288 // Credit: http://stackoverflow.com/a/11508164/1190235
289 function hexToRgb (hex) {
290 var bigint = parseInt(hex, 16),
291 r = (bigint >> 16) & 255,
292 g = (bigint >> 8) & 255,
298 function rgbStringToRgb (color) {
299 var match = color.match(/^rgba?\(([\d,.]+)\)$/);
300 if (! match) throw new Error('Cannot parse rgb value');
301 color = match[1].split(',');
302 return color.map(Number);
305 function hasData (scope) {
306 return scope.chartData && scope.chartData.length;
309 function getChartColorFn (scope) {
310 return typeof scope.chartGetColor === 'function' ? scope.chartGetColor : getRandomColor;
313 function getChartData (type, scope) {
314 var colors = getColors(type, scope);
315 return Array.isArray(scope.chartData[0]) ?
316 getDataSets(scope.chartLabels, scope.chartData, scope.chartSeries || [], colors, scope.chartDatasetOverride) :
317 getData(scope.chartLabels, scope.chartData, colors, scope.chartDatasetOverride);
320 function getDataSets (labels, data, series, colors, datasetOverride) {
323 datasets: data.map(function (item, i) {
324 var dataset = angular.extend({}, colors[i], {
328 if (datasetOverride && datasetOverride.length >= i) {
329 angular.merge(dataset, datasetOverride[i]);
336 function getData (labels, data, colors, datasetOverride) {
341 backgroundColor: colors.map(function (color) {
342 return color.pointBackgroundColor;
344 hoverBackgroundColor: colors.map(function (color) {
345 return color.backgroundColor;
349 if (datasetOverride) {
350 angular.merge(dataset.datasets[0], datasetOverride);
355 function getChartOptions (type, scope) {
356 return angular.extend({}, ChartJs.getOptions(type), scope.chartOptions);
359 function bindEvents (cvs, scope) {
360 cvs.onclick = scope.chartClick ? getEventHandler(scope, 'chartClick', false) : angular.noop;
361 cvs.onmousemove = scope.chartHover ? getEventHandler(scope, 'chartHover', true) : angular.noop;
364 function updateChart (values, scope) {
365 if (Array.isArray(scope.chartData[0])) {
366 scope.chart.data.datasets.forEach(function (dataset, i) {
367 dataset.data = values[i];
370 scope.chart.data.datasets[0].data = values;
373 scope.chart.update();
374 scope.$emit('chart-update', scope.chart);
377 function isEmpty (value) {
379 (Array.isArray(value) && ! value.length) ||
380 (typeof value === 'object' && ! Object.keys(value).length);
383 function canDisplay (type, scope, elem, options) {
384 // TODO: check parent?
385 if (options.responsive && elem[0].clientHeight === 0) {
386 $timeout(function () {
387 createChart(type, scope, elem);
394 function destroyChart(scope) {
395 if(! scope.chart) return;
396 scope.chart.destroy();
397 scope.$emit('chart-destroy', scope.chart);