blob: 26ac9fedf0c04c5e417bf02b792254fef189189f [file] [log] [blame]
Jian Li46770fc2016-08-03 02:32:45 +09001/*!
2 * angular-chart.js - An angular.js wrapper for Chart.js
3 * http://jtblin.github.io/angular-chart.js/
4 * Version: 1.0.0
5 *
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
9 */
Jian Lid7a5a742016-02-12 13:51:18 -080010(function (factory) {
11 'use strict';
12 if (typeof exports === 'object') {
13 // Node/CommonJS
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', 'chart'], factory);
20 } else {
21 // Browser globals
Jian Li46770fc2016-08-03 02:32:45 +090022 if (typeof angular === 'undefined' || typeof Chart === 'undefined')
23 throw new Error('Chart.js library needs to included, see http://jtblin.github.io/angular-chart.js/');
Jian Lid7a5a742016-02-12 13:51:18 -080024 factory(angular, Chart);
25 }
26}(function (angular, Chart) {
27 'use strict';
28
Jian Lid7a5a742016-02-12 13:51:18 -080029 Chart.defaults.global.multiTooltipTemplate = '<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>';
Jian Li46770fc2016-08-03 02:32:45 +090030 Chart.defaults.global.tooltips.mode = 'label';
31 Chart.defaults.global.elements.line.borderWidth = 2;
32 Chart.defaults.global.elements.rectangle.borderWidth = 2;
33 Chart.defaults.global.legend.display = false;
34 Chart.defaults.global.colors = [
Jian Lid7a5a742016-02-12 13:51:18 -080035 '#97BBCD', // blue
36 '#DCDCDC', // light grey
37 '#F7464A', // red
38 '#46BFBD', // green
39 '#FDB45C', // yellow
40 '#949FB1', // grey
41 '#4D5360' // dark grey
42 ];
43
Jian Li46770fc2016-08-03 02:32:45 +090044 var useExcanvas = typeof window.G_vmlCanvasManager === 'object' &&
Jian Lid7a5a742016-02-12 13:51:18 -080045 window.G_vmlCanvasManager !== null &&
46 typeof window.G_vmlCanvasManager.initElement === 'function';
47
Jian Li46770fc2016-08-03 02:32:45 +090048 if (useExcanvas) Chart.defaults.global.animation = false;
Jian Lid7a5a742016-02-12 13:51:18 -080049
50 return angular.module('chart.js', [])
51 .provider('ChartJs', ChartJsProvider)
52 .factory('ChartJsFactory', ['ChartJs', '$timeout', ChartJsFactory])
53 .directive('chartBase', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory(); }])
Jian Li46770fc2016-08-03 02:32:45 +090054 .directive('chartLine', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('line'); }])
55 .directive('chartBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bar'); }])
56 .directive('chartHorizontalBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('horizontalBar'); }])
57 .directive('chartRadar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('radar'); }])
58 .directive('chartDoughnut', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('doughnut'); }])
59 .directive('chartPie', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('pie'); }])
60 .directive('chartPolarArea', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('polarArea'); }])
61 .directive('chartBubble', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bubble'); }])
62 .name;
Jian Lid7a5a742016-02-12 13:51:18 -080063
64 /**
65 * Wrapper for chart.js
66 * Allows configuring chart js using the provider
67 *
68 * angular.module('myModule', ['chart.js']).config(function(ChartJsProvider) {
Jian Li46770fc2016-08-03 02:32:45 +090069 * ChartJsProvider.setOptions({ responsive: false });
70 * ChartJsProvider.setOptions('Line', { responsive: true });
Jian Lid7a5a742016-02-12 13:51:18 -080071 * })))
72 */
73 function ChartJsProvider () {
Jian Li46770fc2016-08-03 02:32:45 +090074 var options = { responsive: true };
Jian Lid7a5a742016-02-12 13:51:18 -080075 var ChartJs = {
76 Chart: Chart,
77 getOptions: function (type) {
78 var typeOptions = type && options[type] || {};
79 return angular.extend({}, options, typeOptions);
80 }
81 };
82
83 /**
84 * Allow to set global options during configuration
85 */
86 this.setOptions = function (type, customOptions) {
87 // If no type was specified set option for the global object
88 if (! customOptions) {
89 customOptions = type;
90 options = angular.extend(options, customOptions);
91 return;
92 }
93 // Set options for the specific chart
94 options[type] = angular.extend(options[type] || {}, customOptions);
95 };
96
97 this.$get = function () {
98 return ChartJs;
99 };
100 }
101
102 function ChartJsFactory (ChartJs, $timeout) {
103 return function chart (type) {
104 return {
105 restrict: 'CA',
106 scope: {
Jian Li46770fc2016-08-03 02:32:45 +0900107 chartGetColor: '=?',
Jian Lid7a5a742016-02-12 13:51:18 -0800108 chartType: '=',
Jian Lid7a5a742016-02-12 13:51:18 -0800109 chartData: '=?',
110 chartLabels: '=?',
111 chartOptions: '=?',
112 chartSeries: '=?',
Jian Li46770fc2016-08-03 02:32:45 +0900113 chartColors: '=?',
Jian Lid7a5a742016-02-12 13:51:18 -0800114 chartClick: '=?',
Jian Li46770fc2016-08-03 02:32:45 +0900115 chartHover: '=?',
116 chartDatasetOverride: '=?'
Jian Lid7a5a742016-02-12 13:51:18 -0800117 },
118 link: function (scope, elem/*, attrs */) {
Jian Li46770fc2016-08-03 02:32:45 +0900119 if (useExcanvas) window.G_vmlCanvasManager.initElement(elem[0]);
Jian Lid7a5a742016-02-12 13:51:18 -0800120
121 // Order of setting "watch" matter
Jian Li46770fc2016-08-03 02:32:45 +0900122 scope.$watch('chartData', watchData, true);
123 scope.$watch('chartSeries', watchOther, true);
124 scope.$watch('chartLabels', watchOther, true);
125 scope.$watch('chartOptions', watchOther, true);
126 scope.$watch('chartColors', watchOther, true);
127 scope.$watch('chartDatasetOverride', watchOther, true);
128 scope.$watch('chartType', watchType, false);
Jian Lid7a5a742016-02-12 13:51:18 -0800129
Jian Li46770fc2016-08-03 02:32:45 +0900130 scope.$on('$destroy', function () {
131 destroyChart(scope);
132 });
133
134 scope.$on('$resize', function () {
135 if (scope.chart) scope.chart.resize();
136 });
137
138 function watchData (newVal, oldVal) {
Jian Li82101d92016-05-04 12:00:46 -0700139 if (! newVal || ! newVal.length || (Array.isArray(newVal[0]) && ! newVal[0].length)) {
Jian Li46770fc2016-08-03 02:32:45 +0900140 destroyChart(scope);
Jian Li82101d92016-05-04 12:00:46 -0700141 return;
142 }
Jian Lid7a5a742016-02-12 13:51:18 -0800143 var chartType = type || scope.chartType;
144 if (! chartType) return;
145
Jian Li46770fc2016-08-03 02:32:45 +0900146 if (scope.chart && canUpdateChart(newVal, oldVal))
147 return updateChart(newVal, scope);
Jian Lid7a5a742016-02-12 13:51:18 -0800148
Jian Li46770fc2016-08-03 02:32:45 +0900149 createChart(chartType, scope, elem);
150 }
Jian Lid7a5a742016-02-12 13:51:18 -0800151
Jian Li46770fc2016-08-03 02:32:45 +0900152 function watchOther (newVal, oldVal) {
Jian Lid7a5a742016-02-12 13:51:18 -0800153 if (isEmpty(newVal)) return;
154 if (angular.equals(newVal, oldVal)) return;
155 var chartType = type || scope.chartType;
156 if (! chartType) return;
157
158 // chart.update() doesn't work for series and labels
159 // so we have to re-create the chart entirely
Jian Li46770fc2016-08-03 02:32:45 +0900160 createChart(chartType, scope, elem);
Jian Lid7a5a742016-02-12 13:51:18 -0800161 }
162
Jian Li46770fc2016-08-03 02:32:45 +0900163 function watchType (newVal, oldVal) {
164 if (isEmpty(newVal)) return;
165 if (angular.equals(newVal, oldVal)) return;
166 createChart(newVal, scope, elem);
Jian Lid7a5a742016-02-12 13:51:18 -0800167 }
168 }
169 };
170 };
171
Jian Li46770fc2016-08-03 02:32:45 +0900172 function createChart (type, scope, elem) {
173 var options = getChartOptions(type, scope);
174 if (! hasData(scope) || ! canDisplay(type, scope, elem, options)) return;
175
176 var cvs = elem[0];
177 var ctx = cvs.getContext('2d');
178
179 scope.chartGetColor = getChartColorFn(scope);
180 var data = getChartData(type, scope);
181
182 // Destroy old chart if it exists to avoid ghost charts issue
183 // https://github.com/jtblin/angular-chart.js/issues/187
184 destroyChart(scope);
185
186 scope.chart = new ChartJs.Chart(ctx, {
187 type: type,
188 data: data,
189 options: options
190 });
191 scope.$emit('chart-create', scope.chart);
192 bindEvents(cvs, scope);
193 }
194
Jian Lid7a5a742016-02-12 13:51:18 -0800195 function canUpdateChart (newVal, oldVal) {
196 if (newVal && oldVal && newVal.length && oldVal.length) {
197 return Array.isArray(newVal[0]) ?
198 newVal.length === oldVal.length && newVal.every(function (element, index) {
199 return element.length === oldVal[index].length; }) :
200 oldVal.reduce(sum, 0) > 0 ? newVal.length === oldVal.length : false;
201 }
202 return false;
203 }
204
205 function sum (carry, val) {
206 return carry + val;
207 }
208
Jian Li46770fc2016-08-03 02:32:45 +0900209 function getEventHandler (scope, action, triggerOnlyOnChange) {
Jian Lid7a5a742016-02-12 13:51:18 -0800210 var lastState = null;
211 return function (evt) {
Jian Li46770fc2016-08-03 02:32:45 +0900212 var atEvent = scope.chart.getElementsAtEvent || scope.chart.getPointsAtEvent;
Jian Lid7a5a742016-02-12 13:51:18 -0800213 if (atEvent) {
Jian Li46770fc2016-08-03 02:32:45 +0900214 var activePoints = atEvent.call(scope.chart, evt);
Jian Lid7a5a742016-02-12 13:51:18 -0800215 if (triggerOnlyOnChange === false || angular.equals(lastState, activePoints) === false) {
216 lastState = activePoints;
217 scope[action](activePoints, evt);
Jian Lid7a5a742016-02-12 13:51:18 -0800218 }
219 }
220 };
221 }
222
Jian Li46770fc2016-08-03 02:32:45 +0900223 function getColors (type, scope) {
224 var colors = angular.copy(scope.chartColors ||
225 ChartJs.getOptions(type).chartColors ||
226 Chart.defaults.global.colors
Jian Lid7a5a742016-02-12 13:51:18 -0800227 );
Jian Li46770fc2016-08-03 02:32:45 +0900228 var notEnoughColors = colors.length < scope.chartData.length;
229 while (colors.length < scope.chartData.length) {
230 colors.push(scope.chartGetColor());
Jian Lid7a5a742016-02-12 13:51:18 -0800231 }
Jian Li46770fc2016-08-03 02:32:45 +0900232 // mutate colors in this case as we don't want
233 // the colors to change on each refresh
234 if (notEnoughColors) scope.chartColors = colors;
235 return colors.map(convertColor);
Jian Lid7a5a742016-02-12 13:51:18 -0800236 }
237
Jian Li46770fc2016-08-03 02:32:45 +0900238 function convertColor (color) {
239 if (typeof color === 'object' && color !== null) return color;
240 if (typeof color === 'string' && color[0] === '#') return getColor(hexToRgb(color.substr(1)));
241 return getRandomColor();
Jian Lid7a5a742016-02-12 13:51:18 -0800242 }
243
Jian Li46770fc2016-08-03 02:32:45 +0900244 function getRandomColor () {
245 var color = [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
246 return getColor(color);
Jian Lid7a5a742016-02-12 13:51:18 -0800247 }
248
Jian Li46770fc2016-08-03 02:32:45 +0900249 function getColor (color) {
Jian Lid7a5a742016-02-12 13:51:18 -0800250 return {
Jian Li46770fc2016-08-03 02:32:45 +0900251 backgroundColor: rgba(color, 0.2),
252 pointBackgroundColor: rgba(color, 1),
253 pointHoverBackgroundColor: rgba(color, 0.8),
254 borderColor: rgba(color, 1),
255 pointBorderColor: '#fff',
256 pointHoverBorderColor: rgba(color, 1)
Jian Lid7a5a742016-02-12 13:51:18 -0800257 };
258 }
259
260 function getRandomInt (min, max) {
261 return Math.floor(Math.random() * (max - min + 1)) + min;
262 }
263
Jian Li46770fc2016-08-03 02:32:45 +0900264 function rgba (color, alpha) {
265 // rgba not supported by IE8
266 return useExcanvas ? 'rgb(' + color.join(',') + ')' : 'rgba(' + color.concat(alpha).join(',') + ')';
Jian Lid7a5a742016-02-12 13:51:18 -0800267 }
268
269 // Credit: http://stackoverflow.com/a/11508164/1190235
270 function hexToRgb (hex) {
271 var bigint = parseInt(hex, 16),
272 r = (bigint >> 16) & 255,
273 g = (bigint >> 8) & 255,
274 b = bigint & 255;
275
276 return [r, g, b];
277 }
278
Jian Li46770fc2016-08-03 02:32:45 +0900279 function hasData (scope) {
280 return scope.chartData && scope.chartData.length;
281 }
282
283 function getChartColorFn (scope) {
284 return typeof scope.chartGetColor === 'function' ? scope.chartGetColor : getRandomColor;
285 }
286
287 function getChartData (type, scope) {
288 var colors = getColors(type, scope);
289 return Array.isArray(scope.chartData[0]) ?
290 getDataSets(scope.chartLabels, scope.chartData, scope.chartSeries || [], colors, scope.chartDatasetOverride) :
291 getData(scope.chartLabels, scope.chartData, colors, scope.chartDatasetOverride);
292 }
293
294 function getDataSets (labels, data, series, colors, datasetOverride) {
Jian Lid7a5a742016-02-12 13:51:18 -0800295 return {
296 labels: labels,
297 datasets: data.map(function (item, i) {
Jian Li46770fc2016-08-03 02:32:45 +0900298 var dataset = angular.extend({}, colors[i], {
Jian Lid7a5a742016-02-12 13:51:18 -0800299 label: series[i],
300 data: item
301 });
Jian Li46770fc2016-08-03 02:32:45 +0900302 if (datasetOverride && datasetOverride.length >= i) {
303 angular.merge(dataset, datasetOverride[i]);
304 }
305 return dataset;
Jian Lid7a5a742016-02-12 13:51:18 -0800306 })
307 };
308 }
309
Jian Li46770fc2016-08-03 02:32:45 +0900310 function getData (labels, data, colors, datasetOverride) {
311 var dataset = {
312 labels: labels,
313 datasets: [{
314 data: data,
315 backgroundColor: colors.map(function (color) {
316 return color.pointBackgroundColor;
317 }),
318 hoverBackgroundColor: colors.map(function (color) {
319 return color.backgroundColor;
320 })
321 }]
322 };
323 if (datasetOverride) {
324 angular.merge(dataset.datasets[0], datasetOverride);
325 }
326 return dataset;
Jian Lid7a5a742016-02-12 13:51:18 -0800327 }
328
Jian Li46770fc2016-08-03 02:32:45 +0900329 function getChartOptions (type, scope) {
330 return angular.extend({}, ChartJs.getOptions(type), scope.chartOptions);
Jian Lid7a5a742016-02-12 13:51:18 -0800331 }
332
Jian Li46770fc2016-08-03 02:32:45 +0900333 function bindEvents (cvs, scope) {
334 cvs.onclick = scope.chartClick ? getEventHandler(scope, 'chartClick', false) : angular.noop;
335 cvs.onmousemove = scope.chartHover ? getEventHandler(scope, 'chartHover', true) : angular.noop;
336 }
337
338 function updateChart (values, scope) {
339 if (Array.isArray(scope.chartData[0])) {
340 scope.chart.data.datasets.forEach(function (dataset, i) {
341 dataset.data = values[i];
Jian Lid7a5a742016-02-12 13:51:18 -0800342 });
343 } else {
Jian Li46770fc2016-08-03 02:32:45 +0900344 scope.chart.data.datasets[0].data = values;
Jian Lid7a5a742016-02-12 13:51:18 -0800345 }
Jian Li46770fc2016-08-03 02:32:45 +0900346
347 scope.chart.update();
348 scope.$emit('chart-update', scope.chart);
Jian Lid7a5a742016-02-12 13:51:18 -0800349 }
350
351 function isEmpty (value) {
352 return ! value ||
353 (Array.isArray(value) && ! value.length) ||
354 (typeof value === 'object' && ! Object.keys(value).length);
355 }
356
Jian Li46770fc2016-08-03 02:32:45 +0900357 function canDisplay (type, scope, elem, options) {
358 // TODO: check parent?
359 if (options.responsive && elem[0].clientHeight === 0) {
360 $timeout(function () {
361 createChart(type, scope, elem);
362 }, 50, false);
363 return false;
364 }
365 return true;
Jian Lid7a5a742016-02-12 13:51:18 -0800366 }
Jian Li82101d92016-05-04 12:00:46 -0700367
Jian Li46770fc2016-08-03 02:32:45 +0900368 function destroyChart(scope) {
369 if(! scope.chart) return;
370 scope.chart.destroy();
371 scope.$emit('chart-destroy', scope.chart);
Jian Li82101d92016-05-04 12:00:46 -0700372 }
Jian Lid7a5a742016-02-12 13:51:18 -0800373 }
374}));