MDL-59790 core: fix tooltip for pie chart
[moodle.git] / lib / amd / src / chart_output_chartjs.js
CommitLineData
357ec2d5
FM
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/>.
15
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
601da0e6 22 * @module core/chart_output_chartjs
357ec2d5 23 */
f5474e65
FM
24define([
25 'jquery',
26 'core/chartjs',
27 'core/chart_axis',
01440dcc 28 'core/chart_bar',
f5474e65 29 'core/chart_output_base',
f0f1e031 30 'core/chart_line',
ccaa2b34 31 'core/chart_pie',
f0f1e031 32 'core/chart_series'
01440dcc 33], function($, Chartjs, Axis, Bar, Base, Line, Pie, Series) {
357ec2d5 34
2b01f915
FM
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 };
45
357ec2d5
FM
46 /**
47 * Chart output for Chart.js.
601da0e6
FM
48 *
49 * @class
50 * @alias module:core/chart_output_chartjs
51 * @extends {module:core/chart_output_base}
357ec2d5
FM
52 */
53 function Output() {
54 Base.prototype.constructor.apply(this, arguments);
9b28bf0b
FM
55
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 }
62
357ec2d5
FM
63 this._build();
64 }
357ec2d5
FM
65 Output.prototype = Object.create(Base.prototype);
66
601da0e6
FM
67 /**
68 * Reference to the chart config object.
69 *
70 * @type {Object}
71 * @protected
72 */
357ec2d5 73 Output.prototype._config = null;
601da0e6
FM
74
75 /**
76 * Reference to the instance of chart.js.
77 *
78 * @type {Object}
79 * @protected
80 */
357ec2d5
FM
81 Output.prototype._chartjs = null;
82
601da0e6
FM
83 /**
84 * Reference to the canvas node.
85 *
86 * @type {Jquery}
87 * @protected
88 */
89 Output.prototype._canvas = null;
357ec2d5 90
601da0e6
FM
91 /**
92 * Builds the config and the chart.
93 *
94 * @protected
95 */
357ec2d5
FM
96 Output.prototype._build = function() {
97 this._config = this._makeConfig();
9b28bf0b 98 this._chartjs = new Chartjs(this._canvas[0], this._config);
357ec2d5
FM
99 };
100
cff1f90a
SL
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 };
117
01440dcc 118 /**
b02e738c 119 * Get the chart type and handles the Chart.js specific chart types.
01440dcc 120 *
b02e738c
SL
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.
01440dcc 123 *
b02e738c 124 * @method getChartType
01440dcc
SL
125 * @returns {String} the chart type.
126 * @protected
127 */
128 Output.prototype._getChartType = function() {
129 var type = this._chart.getType();
130
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';
b02e738c
SL
134 } else if (this._chart.getType() === Pie.prototype.TYPE && this._chart.getDoughnut() === true) {
135 // Pie chart can be displayed as doughnut.
136 type = 'doughnut';
01440dcc
SL
137 }
138
139 return type;
140 };
141
601da0e6
FM
142 /**
143 * Make the axis config.
144 *
145 * @protected
146 * @param {module:core/chart_axis} axis The axis.
2b01f915
FM
147 * @param {String} xy Accepts 'x' or 'y'.
148 * @param {Number} index The axis index.
601da0e6
FM
149 * @return {Object} The axis config.
150 */
2b01f915
FM
151 Output.prototype._makeAxisConfig = function(axis, xy, index) {
152 var scaleData = {
153 id: makeAxisId(xy, index)
154 };
f5474e65
FM
155
156 if (axis.getPosition() !== Axis.prototype.POS_DEFAULT) {
157 scaleData.position = axis.getPosition();
158 }
159
160 if (axis.getLabel() !== null) {
161 scaleData.scaleLabel = {
162 display: true,
cff1f90a 163 labelString: this._cleanData(axis.getLabel())
f5474e65
FM
164 };
165 }
166
909c5cf2
FM
167 if (axis.getStepSize() !== null) {
168 scaleData.ticks = scaleData.ticks || {};
169 scaleData.ticks.stepSize = axis.getStepSize();
170 }
171
46de49dc
FM
172 if (axis.getMax() !== null) {
173 scaleData.ticks = scaleData.ticks || {};
174 scaleData.ticks.max = axis.getMax();
175 }
176
177 if (axis.getMin() !== null) {
178 scaleData.ticks = scaleData.ticks || {};
179 scaleData.ticks.min = axis.getMin();
180 }
181
f5474e65
FM
182 return scaleData;
183 };
184
601da0e6
FM
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 */
357ec2d5
FM
192 Output.prototype._makeConfig = function() {
193 var config = {
01440dcc 194 type: this._getChartType(),
357ec2d5 195 data: {
cff1f90a 196 labels: this._cleanData(this._chart.getLabels()),
601da0e6 197 datasets: this._makeDatasetsConfig()
357ec2d5
FM
198 },
199 options: {
858cbfdf
SL
200 title: {
201 display: this._chart.getTitle() !== null,
cff1f90a 202 text: this._cleanData(this._chart.getTitle())
858cbfdf 203 }
357ec2d5
FM
204 }
205 };
f5474e65
FM
206
207 this._chart.getXAxes().forEach(function(axis, i) {
cc8438d1
FM
208 var axisLabels = axis.getLabels();
209
f5474e65
FM
210 config.options.scales = config.options.scales || {};
211 config.options.scales.xAxes = config.options.scales.xAxes || [];
2b01f915 212 config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);
cc8438d1
FM
213
214 if (axisLabels !== null) {
215 config.options.scales.xAxes[i].ticks.callback = function(value, index) {
216 return axisLabels[index] || '';
217 };
218 }
6f9f8b59 219 config.options.scales.xAxes[i].stacked = this._isStacked();
f5474e65
FM
220 }.bind(this));
221
222 this._chart.getYAxes().forEach(function(axis, i) {
681e1a76
FM
223 var axisLabels = axis.getLabels();
224
f5474e65
FM
225 config.options.scales = config.options.scales || {};
226 config.options.scales.yAxes = config.options.scales.yAxes || [];
2b01f915 227 config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);
681e1a76
FM
228
229 if (axisLabels !== null) {
230 config.options.scales.yAxes[i].ticks.callback = function(value) {
231 return axisLabels[parseInt(value, 10)] || '';
232 };
233 }
6f9f8b59 234 config.options.scales.yAxes[i].stacked = this._isStacked();
f5474e65
FM
235 }.bind(this));
236
08501958
SL
237 config.options.tooltips = {
238 callbacks: {
239 label: this._makeTooltip.bind(this)
240 }
241 };
242
357ec2d5
FM
243 return config;
244 };
245
601da0e6
FM
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) {
ccaa2b34 254 var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
2b01f915 255 var dataset = {
cff1f90a 256 label: this._cleanData(series.getLabel()),
601da0e6
FM
257 data: series.getValues(),
258 type: series.getType(),
259 fill: false,
ccaa2b34
FM
260 backgroundColor: colors,
261 // Pie charts look better without borders.
2b01f915 262 borderColor: this._chart.getType() == Pie.prototype.TYPE ? null : colors,
f0f1e031 263 lineTension: this._isSmooth(series) ? 0.3 : 0
601da0e6 264 };
2b01f915
FM
265
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 }
272
273 return dataset;
ccaa2b34 274 }.bind(this));
601da0e6
FM
275 return sets;
276 };
277
08501958
SL
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) {
287
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];
294
295 // Build default tooltip.
b45ca55d
SL
296 var tooltip = [];
297
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 }
08501958 303
cff1f90a 304 // Add series labels to the tooltip if any.
08501958 305 if (serieLabels !== null) {
b45ca55d
SL
306 tooltip.push(this._cleanData(serieLabels[tooltipItem.index]));
307 } else {
308 tooltip.push(this._cleanData(serieLabel) + ': ' + tooltipData);
08501958
SL
309 }
310
311 return tooltip;
312 };
313
f0f1e031
SL
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 }
331
332 return smooth;
333 };
334
c574995d
SL
335 /**
336 * Verify if the bar chart is stacked or not.
337 *
338 * @protected
c574995d
SL
339 * @returns {Bool}
340 */
6f9f8b59 341 Output.prototype._isStacked = function() {
c574995d 342 var stacked = false;
c574995d 343
6f9f8b59
MG
344 // Stacking is (currently) only supported for bar charts.
345 if (this._chart.getType() === Bar.prototype.TYPE) {
c574995d
SL
346 stacked = this._chart.getStacked();
347 }
348
349 return stacked;
350 };
351
601da0e6 352 /** @override */
357ec2d5
FM
353 Output.prototype.update = function() {
354 $.extend(true, this._config, this._makeConfig());
355 this._chartjs.update();
356 };
357
358 return Output;
359
360});