Passed
Push — master ( b4e78c...02779c )
by MusikAnimal
05:56
created

web/static/js/editcounter.js   A

Complexity

Total Complexity 32
Complexity/F 1.45

Size

Lines of Code 272
Function Count 22

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 0
nc 2
dl 0
loc 272
rs 9.6
c 5
b 1
f 0
wmc 32
mnd 1
bc 30
fnc 22
bpm 1.3636
cpm 1.4544
noi 2

7 Functions

Rating   Name   Duplication   Size   Complexity  
A editcounter.js ➔ toggleNamespace 0 61 1
B editcounter.js ➔ $ 0 33 2
A editcounter.js ➔ getPercentage 0 5 1
A window.setupMonthYearChart 0 66 2
A editcounter.js ➔ getYAxisLabels 0 19 1
A editcounter.js ➔ loadLatestGlobal 0 11 2
A editcounter.js ➔ getMonthYearTotals 0 18 1
1
/**
2
 * Namespaces that have been excluded from view via namespace toggle table.
3
 * @type {Array}
4
 */
5
window.excludedNamespaces = [];
6
7
/**
8
 * Chart labels for the month/yearcount charts.
9
 * @type {Object} Keys are the chart IDs, values are arrays of strings.
10
 */
11
window.chartLabels = {};
12
13
/**
14
 * Number of digits of the max month/year total. We want to keep this consistent
15
 * for aesthetic reasons, even if the updated totals are fewer digits in size.
16
 * @type {Object} Keys are the chart IDs, values are integers.
17
 */
18
window.maxDigits = {};
19
20
$(function () {
21
    // Don't do anything if this isn't a Edit Counter page.
22
    if ($('body.editcounter').length === 0) {
23
        return;
24
    }
25
26
    // Set up charts.
27
    $('.chart-wrapper').each(function () {
28
        var chartType = $(this).data('chart-type');
29
        if ( chartType === undefined ) {
30
            return false;
31
        }
32
        var data = $(this).data('chart-data');
33
        var labels = $(this).data('chart-labels');
34
        var $ctx = $('canvas', $(this));
35
36
        /** global: Chart */
37
        new Chart($ctx, {
0 ignored issues
show
Unused Code Best Practice introduced by
The object created with new Chart($ctx, {Identif...e))))),false,false)))}) is not used but discarded. Consider invoking another function instead of a constructor if you are doing this purely for side effects.
Loading history...
38
            type: chartType,
39
            data: {
40
                labels: labels,
41
                datasets: [ { data: data } ]
42
            }
43
        });
44
45
        return undefined;
46
    });
47
48
    loadLatestGlobal();
49
50
    // Set up namespace toggle chart.
51
    setupToggleTable(window.namespaceTotals, window.namespaceChart, null, toggleNamespace);
52
});
53
54
/**
55
 * Callback for setupToggleTable(). This will show/hide a given namespace from
56
 * all charts, and update totals and percentages.
57
 * @param  {Object} newData New namespaces and totals, as returned by setupToggleTable.
58
 * @param  {String} key     Namespace ID of the toggled namespace.
59
 */
60
function toggleNamespace(newData, key)
61
{
62
    var total = 0, counts = [];
63
    Object.keys(newData).forEach(function (namespace) {
64
        var count = parseInt(newData[namespace], 10);
65
        counts.push(count);
66
        total += count;
67
    });
68
    var namespaceCount = Object.keys(newData).length;
69
70
    /** global: i18nLang */
71
    $('.namespaces--namespaces').text(
72
        namespaceCount.toLocaleString(i18nLang) + ' ' +
73
        $.i18n('num-namespaces', namespaceCount)
74
    );
75
    $('.namespaces--count').text(total.toLocaleString(i18nLang));
76
77
    // Now that we have the total, loop through once more time to update percentages.
78
    counts.forEach(function (count) {
79
        // Calculate percentage, rounded to tenths.
80
        var percentage = getPercentage(count, total);
81
82
        // Update text with new value and percentage.
83
        $('.namespaces-table .sort-entry--count[data-value='+count+']').text(
84
            count.toLocaleString(i18nLang) + ' (' + percentage + ')'
85
        );
86
    });
87
88
    // Loop through month and year charts, toggling the dataset for the newly excluded namespace.
89
    ['year', 'month'].forEach(function (id) {
90
        var chartObj = window[id + 'countsChart'],
91
            nsName = window.namespaces[key] || $.i18n('mainspace');
92
93
        // Figure out the index of the namespace we're toggling within this chart object.
94
        var datasetIndex;
95
        chartObj.data.datasets.forEach(function (dataset, i) {
96
            if (dataset.label === nsName) {
97
                datasetIndex = i;
98
            }
99
        });
100
101
        // Fetch the metadata and toggle the hidden property.
102
        var meta = chartObj.getDatasetMeta(datasetIndex);
103
        meta.hidden = meta.hidden === null ? !chartObj.data.datasets[datasetIndex].hidden : null;
104
105
        // Add this namespace to the list of excluded namespaces.
106
        if (meta.hidden) {
107
            window.excludedNamespaces.push(nsName);
108
        } else {
109
            window.excludedNamespaces = window.excludedNamespaces.filter(function (namespace) {
110
                return namespace !== nsName;
111
            });
112
        }
113
114
        // Update y-axis labels with the new totals.
115
        window[id + 'countsChart'].config.data.labels = getYAxisLabels(id, chartObj.data.datasets);
116
117
        // Refresh chart.
118
        chartObj.update();
119
    });
120
}
121
122
/**
123
 * Load recent global edits' HTML via AJAX, to not slow down the initial page load.
124
 * Only load if container is present, which is missing in subroutes, e.g. ec-namespacetotals, etc.
125
 */
126
function loadLatestGlobal()
127
{
128
    // Load the contributions browser, or set up the listeners if it is already present.
129
    var initFunc = $('.contributions-table').length ? 'setupContributionsNavListeners' : 'loadContributions';
130
    window[initFunc](
131
        function (params) {
132
            return params.target + '-contributions/' + params.project + '/' + params.username;
133
        },
134
        'latest-global-edits'
135
    );
136
}
137
138
/**
139
 * Build the labels for the y-axis of the year/monthcount charts,
140
 * which include the year/month and the total number of edits across
141
 * all namespaces in that year/month.
142
 * @param {String} id ID prefix of the chart, either 'month' or 'year'.
143
 * @param {Array} datasets Datasets making up the chart.
144
 * @return {Array} Labels for each year/month.
145
 */
146
function getYAxisLabels(id, datasets)
147
{
148
    var labelsAndTotals = getMonthYearTotals(id, datasets);
149
150
    // Format labels with totals next to them. This is a bit hacky,
151
    // but it works! We use tabs (\t) to make the labels/totals
152
    // for each namespace line up perfectly.
153
    // The caveat is that we can't localize the numbers because
154
    // the commas are not monospaced :(
155
    return Object.keys(labelsAndTotals).map(function (year) {
156
        var digitCount = labelsAndTotals[year].toString().length;
157
        var numTabs = (window.maxDigits[id] - digitCount) * 2;
158
159
        // +5 for a bit of extra spacing.
160
        /** global: i18nLang */
161
        return year + Array(numTabs + 5).join("\t") +
162
            labelsAndTotals[year].toLocaleString(i18nLang, {useGrouping: false});
163
    });
164
}
165
166
/**
167
 * Get the total number of edits for the given dataset (year or month).
168
 * @param {String} id ID prefix of the chart, either 'month' or 'year'.
169
 * @param {Array} datasets Datasets making up the chart.
170
 * @return {Object} Labels for each year/month as keys, totals as the values.
171
 */
172
function getMonthYearTotals(id, datasets)
173
{
174
    var labelsAndTotals = {};
175
    datasets.forEach(function (namespace) {
176
        if (window.excludedNamespaces.indexOf(namespace.label) !== -1) {
177
            return;
178
        }
179
180
        namespace.data.forEach(function (count, index) {
181
            if (!labelsAndTotals[window.chartLabels[id][index]]) {
182
                labelsAndTotals[window.chartLabels[id][index]] = 0;
183
            }
184
            labelsAndTotals[window.chartLabels[id][index]] += count;
185
        });
186
    });
187
188
    return labelsAndTotals;
189
}
190
191
/**
192
 * Calculate and format a percentage, rounded to the tenths place.
193
 * @param  {Number} numerator
194
 * @param  {Number} denominator
195
 * @return {Number}
196
 */
197
function getPercentage(numerator, denominator)
198
{
199
    /** global: i18nLang */
200
    return (numerator / denominator).toLocaleString(i18nLang, {style: 'percent'});
201
}
202
203
/**
204
 * Set up the monthcounts or yearcounts chart. This is set on the window
205
 * because it is called in the yearcounts/monthcounts view.
206
 * @param {String} id 'year' or 'month'.
207
 * @param {Array} datasets Datasets grouped by mainspace.
208
 * @param {Array} labels The bare labels for the y-axis (years or months).
209
 * @param {Number} maxTotal Maximum value of year/month totals.
210
 */
211
window.setupMonthYearChart = function (id, datasets, labels, maxTotal) {
212
    /** @type {Array} Labels for each namespace. */
213
    var namespaces = datasets.map(function (dataset) {
214
        return dataset.label;
215
    });
216
217
    window.maxDigits[id] = maxTotal.toString().length
218
    window.chartLabels[id] = labels;
219
220
    /** global: i18nRTL */
221
    /** global: i18nLang */
222
    window[id + 'countsChart'] = new Chart($('#' + id + 'counts-canvas'), {
223
        type: 'horizontalBar',
224
        data: {
225
            labels: getYAxisLabels(id, datasets),
226
            datasets: datasets
227
        },
228
        options: {
229
            tooltips: {
230
                mode: 'nearest',
231
                intersect: true,
232
                callbacks: {
233
                    label: function (tooltip) {
234
                        var labelsAndTotals = getMonthYearTotals(id, datasets),
235
                            totals = Object.keys(labelsAndTotals).map(function (label) {
236
                                return labelsAndTotals[label];
237
                            }),
238
                            total = totals[tooltip.index],
239
                            percentage = getPercentage(tooltip.xLabel, total);
240
241
                        return tooltip.xLabel.toLocaleString(i18nLang) + ' ' +
242
                            '(' + percentage + ')';
243
                    },
244
                    title: function (tooltip) {
245
                        var yLabel = tooltip[0].yLabel.replace(/\t.*/, '');
246
                        return yLabel + ' - ' + namespaces[tooltip[0].datasetIndex];
247
                    }
248
                }
249
            },
250
            responsive: true,
251
            maintainAspectRatio: false,
252
            scales: {
253
                xAxes: [{
254
                    stacked: true,
255
                    ticks: {
256
                        beginAtZero: true,
257
                        reverse: i18nRTL,
258
                        callback: function (value) {
259
                            if (Math.floor(value) === value) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if Math.floor(value) === value is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
260
                                return value.toLocaleString(i18nLang);
261
                            }
262
                        },
263
                    }
264
                }],
265
                yAxes: [{
266
                    stacked: true,
267
                    barThickness: 18,
268
                    position: i18nRTL ? 'right' : 'left'
269
                }]
270
            },
271
            legend: {
272
                display: false
273
            }
274
        }
275
    });
276
}
277