1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
17 * Chart output for chart.js.
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
29 'core/chart_output_base',
33 ], function($, Chartjs, Axis, Bar, Base, Line, Pie, Series) {
38 * @param {String} xy Accepts 'x' and 'y'.
39 * @param {Number} index The axis index.
42 var makeAxisId = function(xy, index) {
43 return 'axis-' + xy + '-' + index;
47 * Chart output for Chart.js.
50 * @alias module:core/chart_output_chartjs
51 * @extends {module:core/chart_output_base}
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);
65 Output.prototype = Object.create(Base.prototype);
68 * Reference to the chart config object.
73 Output.prototype._config = null;
76 * Reference to the instance of chart.js.
81 Output.prototype._chartjs = null;
84 * Reference to the canvas node.
89 Output.prototype._canvas = null;
92 * Builds the config and the chart.
96 Output.prototype._build = function() {
97 this._config = this._makeConfig();
98 this._chartjs = new Chartjs(this._canvas[0], this._config);
104 * @param {(String|String[])} data A single string or an array of strings.
105 * @returns {(String|String[])}
108 Output.prototype._cleanData = function(data) {
109 if (data instanceof Array) {
110 return data.map(function(value) {
111 return $('<span>').html(value).text();
114 return $('<span>').html(data).text();
119 * Get the chart type and handles the Chart.js specific chart types.
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.
124 * @method getChartType
125 * @returns {String} the chart type.
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.
143 * Make the axis config.
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.
151 Output.prototype._makeAxisConfig = function(axis, xy, index) {
153 id: makeAxisId(xy, index)
156 if (axis.getPosition() !== Axis.prototype.POS_DEFAULT) {
157 scaleData.position = axis.getPosition();
160 if (axis.getLabel() !== null) {
161 scaleData.scaleLabel = {
163 labelString: this._cleanData(axis.getLabel())
167 if (axis.getStepSize() !== null) {
168 scaleData.ticks = scaleData.ticks || {};
169 scaleData.ticks.stepSize = axis.getStepSize();
172 if (axis.getMax() !== null) {
173 scaleData.ticks = scaleData.ticks || {};
174 scaleData.ticks.max = axis.getMax();
177 if (axis.getMin() !== null) {
178 scaleData.ticks = scaleData.ticks || {};
179 scaleData.ticks.min = axis.getMin();
186 * Make the config config.
189 * @param {module:core/chart_axis} axis The axis.
190 * @return {Object} The axis config.
192 Output.prototype._makeConfig = function() {
194 type: this._getChartType(),
196 labels: this._cleanData(this._chart.getLabels()),
197 datasets: this._makeDatasetsConfig()
201 display: this._chart.getTitle() !== null,
202 text: this._cleanData(this._chart.getTitle())
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] || '';
219 config.options.scales.xAxes[i].stacked = this._isStacked();
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)] || '';
234 config.options.scales.yAxes[i].stacked = this._isStacked();
237 config.options.tooltips = {
239 label: this._makeTooltip.bind(this)
247 * Get the datasets configurations.
252 Output.prototype._makeDatasetsConfig = function() {
253 var sets = this._chart.getSeries().map(function(series) {
254 var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
256 label: this._cleanData(series.getLabel()),
257 data: series.getValues(),
258 type: series.getType(),
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
266 if (series.getXAxis() !== null) {
267 dataset.xAxisID = makeAxisId('x', series.getXAxis());
269 if (series.getYAxis() !== null) {
270 dataset.yAxisID = makeAxisId('y', series.getYAxis());
279 * Get the chart data, add labels and rebuild the tooltip.
281 * @param {Object[]} tooltipItem The tooltip item data.
282 * @param {Object[]} data The chart data.
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.
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]);
304 // Add series labels to the tooltip if any.
305 if (serieLabels !== null) {
306 tooltip.push(this._cleanData(serieLabels[tooltipItem.index]));
308 tooltip.push(this._cleanData(serieLabel) + ': ' + tooltipData);
315 * Verify if the chart line is smooth or not.
318 * @param {module:core/chart_series} series The series.
321 Output.prototype._isSmooth = function(series) {
323 if (this._chart.getType() === Line.prototype.TYPE) {
324 smooth = series.getSmooth();
325 if (smooth === null) {
326 smooth = this._chart.getSmooth();
328 } else if (series.getType() === Series.prototype.TYPE_LINE) {
329 smooth = series.getSmooth();
336 * Verify if the bar chart is stacked or not.
341 Output.prototype._isStacked = function() {
344 // Stacking is (currently) only supported for bar charts.
345 if (this._chart.getType() === Bar.prototype.TYPE) {
346 stacked = this._chart.getStacked();
353 Output.prototype.update = function() {
354 $.extend(true, this._config, this._makeConfig());
355 this._chartjs.update();