MDL-59790 core: fix tooltip for pie chart
[moodle.git] / lib / amd / src / chart_output_chartjs.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Chart output for chart.js.
18  *
19  * @package    core
20  * @copyright  2016 Frédéric Massart - FMCorz.net
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  * @module     core/chart_output_chartjs
23  */
24 define([
25     'jquery',
26     'core/chartjs',
27     'core/chart_axis',
28     'core/chart_bar',
29     'core/chart_output_base',
30     'core/chart_line',
31     'core/chart_pie',
32     'core/chart_series'
33 ], function($, Chartjs, Axis, Bar, Base, Line, Pie, Series) {
35     /**
36      * Makes an axis ID.
37      *
38      * @param {String} xy Accepts 'x' and 'y'.
39      * @param {Number} index The axis index.
40      * @return {String}
41      */
42     var makeAxisId = function(xy, index) {
43         return 'axis-' + xy + '-' + index;
44     };
46     /**
47      * Chart output for Chart.js.
48      *
49      * @class
50      * @alias module:core/chart_output_chartjs
51      * @extends {module:core/chart_output_base}
52      */
53     function Output() {
54         Base.prototype.constructor.apply(this, arguments);
56         // Make sure that we've got a canvas tag.
57         this._canvas = this._node;
58         if (this._canvas.prop('tagName') != 'CANVAS') {
59             this._canvas = $('<canvas>');
60             this._node.append(this._canvas);
61         }
63         this._build();
64     }
65     Output.prototype = Object.create(Base.prototype);
67     /**
68      * Reference to the chart config object.
69      *
70      * @type {Object}
71      * @protected
72      */
73     Output.prototype._config = null;
75     /**
76      * Reference to the instance of chart.js.
77      *
78      * @type {Object}
79      * @protected
80      */
81     Output.prototype._chartjs = null;
83     /**
84      * Reference to the canvas node.
85      *
86      * @type {Jquery}
87      * @protected
88      */
89     Output.prototype._canvas = null;
91     /**
92      * Builds the config and the chart.
93      *
94      * @protected
95      */
96     Output.prototype._build = function() {
97         this._config = this._makeConfig();
98         this._chartjs = new Chartjs(this._canvas[0], this._config);
99     };
101     /**
102      * Clean data.
103      *
104      * @param {(String|String[])} data A single string or an array of strings.
105      * @returns {(String|String[])}
106      * @protected
107      */
108     Output.prototype._cleanData = function(data) {
109         if (data instanceof Array) {
110             return data.map(function(value) {
111                 return $('<span>').html(value).text();
112             });
113         } else {
114             return $('<span>').html(data).text();
115         }
116     };
118     /**
119      * Get the chart type and handles the Chart.js specific chart types.
120      *
121      * By default returns the current chart TYPE value. Also does the handling of specific chart types, for example
122      * check if the bar chart should be horizontal and the pie chart should be displayed as a doughnut.
123      *
124      * @method getChartType
125      * @returns {String} the chart type.
126      * @protected
127      */
128     Output.prototype._getChartType = function() {
129         var type = this._chart.getType();
131         // Bars can be displayed vertically and horizontally, defining horizontalBar type.
132         if (this._chart.getType() === Bar.prototype.TYPE && this._chart.getHorizontal() === true) {
133             type = 'horizontalBar';
134         } else if (this._chart.getType() === Pie.prototype.TYPE && this._chart.getDoughnut() === true) {
135             // Pie chart can be displayed as doughnut.
136             type = 'doughnut';
137         }
139         return type;
140     };
142     /**
143      * Make the axis config.
144      *
145      * @protected
146      * @param {module:core/chart_axis} axis The axis.
147      * @param {String} xy Accepts 'x' or 'y'.
148      * @param {Number} index The axis index.
149      * @return {Object} The axis config.
150      */
151     Output.prototype._makeAxisConfig = function(axis, xy, index) {
152         var scaleData = {
153             id: makeAxisId(xy, index)
154         };
156         if (axis.getPosition() !== Axis.prototype.POS_DEFAULT) {
157             scaleData.position = axis.getPosition();
158         }
160         if (axis.getLabel() !== null) {
161             scaleData.scaleLabel = {
162                 display: true,
163                 labelString: this._cleanData(axis.getLabel())
164             };
165         }
167         if (axis.getStepSize() !== null) {
168             scaleData.ticks = scaleData.ticks || {};
169             scaleData.ticks.stepSize = axis.getStepSize();
170         }
172         if (axis.getMax() !== null) {
173             scaleData.ticks = scaleData.ticks || {};
174             scaleData.ticks.max = axis.getMax();
175         }
177         if (axis.getMin() !== null) {
178             scaleData.ticks = scaleData.ticks || {};
179             scaleData.ticks.min = axis.getMin();
180         }
182         return scaleData;
183     };
185     /**
186      * Make the config config.
187      *
188      * @protected
189      * @param {module:core/chart_axis} axis The axis.
190      * @return {Object} The axis config.
191      */
192     Output.prototype._makeConfig = function() {
193         var config = {
194             type: this._getChartType(),
195             data: {
196                 labels: this._cleanData(this._chart.getLabels()),
197                 datasets: this._makeDatasetsConfig()
198             },
199             options: {
200                 title: {
201                     display: this._chart.getTitle() !== null,
202                     text: this._cleanData(this._chart.getTitle())
203                 }
204             }
205         };
207         this._chart.getXAxes().forEach(function(axis, i) {
208             var axisLabels = axis.getLabels();
210             config.options.scales = config.options.scales || {};
211             config.options.scales.xAxes = config.options.scales.xAxes || [];
212             config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);
214             if (axisLabels !== null) {
215                 config.options.scales.xAxes[i].ticks.callback = function(value, index) {
216                     return axisLabels[index] || '';
217                 };
218             }
219             config.options.scales.xAxes[i].stacked = this._isStacked();
220         }.bind(this));
222         this._chart.getYAxes().forEach(function(axis, i) {
223             var axisLabels = axis.getLabels();
225             config.options.scales = config.options.scales || {};
226             config.options.scales.yAxes = config.options.scales.yAxes || [];
227             config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);
229             if (axisLabels !== null) {
230                 config.options.scales.yAxes[i].ticks.callback = function(value) {
231                     return axisLabels[parseInt(value, 10)] || '';
232                 };
233             }
234             config.options.scales.yAxes[i].stacked = this._isStacked();
235         }.bind(this));
237         config.options.tooltips = {
238             callbacks: {
239                 label: this._makeTooltip.bind(this)
240             }
241         };
243         return config;
244     };
246     /**
247      * Get the datasets configurations.
248      *
249      * @protected
250      * @return {Object[]}
251      */
252     Output.prototype._makeDatasetsConfig = function() {
253         var sets = this._chart.getSeries().map(function(series) {
254             var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
255             var dataset = {
256                 label: this._cleanData(series.getLabel()),
257                 data: series.getValues(),
258                 type: series.getType(),
259                 fill: false,
260                 backgroundColor: colors,
261                 // Pie charts look better without borders.
262                 borderColor: this._chart.getType() == Pie.prototype.TYPE ? null : colors,
263                 lineTension: this._isSmooth(series) ? 0.3 : 0
264             };
266             if (series.getXAxis() !== null) {
267                 dataset.xAxisID = makeAxisId('x', series.getXAxis());
268             }
269             if (series.getYAxis() !== null) {
270                 dataset.yAxisID = makeAxisId('y', series.getYAxis());
271             }
273             return dataset;
274         }.bind(this));
275         return sets;
276     };
278     /**
279      * Get the chart data, add labels and rebuild the tooltip.
280      *
281      * @param {Object[]} tooltipItem The tooltip item data.
282      * @param {Object[]} data The chart data.
283      * @returns {String}
284      * @protected
285      */
286     Output.prototype._makeTooltip = function(tooltipItem, data) {
288         // Get series and chart data to rebuild the tooltip and add labels.
289         var series = this._chart.getSeries()[tooltipItem.datasetIndex];
290         var serieLabel = series.getLabel();
291         var serieLabels = series.getLabels();
292         var chartData = data.datasets[tooltipItem.datasetIndex].data;
293         var tooltipData = chartData[tooltipItem.index];
295         // Build default tooltip.
296         var tooltip = [];
298         // Pie and doughnut charts does not have axis.
299         if (tooltipItem.xLabel == '' && tooltipItem.yLabel == '') {
300             var chartLabels = this._cleanData(this._chart.getLabels());
301             tooltip.push(chartLabels[tooltipItem.index]);
302         }
304         // Add series labels to the tooltip if any.
305         if (serieLabels !== null) {
306             tooltip.push(this._cleanData(serieLabels[tooltipItem.index]));
307         } else {
308             tooltip.push(this._cleanData(serieLabel) + ': ' + tooltipData);
309         }
311         return tooltip;
312     };
314     /**
315      * Verify if the chart line is smooth or not.
316      *
317      * @protected
318      * @param {module:core/chart_series} series The series.
319      * @returns {Bool}
320      */
321     Output.prototype._isSmooth = function(series) {
322         var smooth = false;
323         if (this._chart.getType() === Line.prototype.TYPE) {
324             smooth = series.getSmooth();
325             if (smooth === null) {
326                 smooth = this._chart.getSmooth();
327             }
328         } else if (series.getType() === Series.prototype.TYPE_LINE) {
329             smooth = series.getSmooth();
330         }
332         return smooth;
333     };
335     /**
336      * Verify if the bar chart is stacked or not.
337      *
338      * @protected
339      * @returns {Bool}
340      */
341     Output.prototype._isStacked = function() {
342         var stacked = false;
344         // Stacking is (currently) only supported for bar charts.
345         if (this._chart.getType() === Bar.prototype.TYPE) {
346             stacked = this._chart.getStacked();
347         }
349         return stacked;
350     };
352     /** @override */
353     Output.prototype.update = function() {
354         $.extend(true, this._config, this._makeConfig());
355         this._chartjs.update();
356     };
358     return Output;
360 });