Commit | Line | Data |
---|---|---|
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 |
24 | define([ |
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 | }); |