Passed
Push — master ( 3c272a...518644 )
by Marcel
03:13
created

js/app.js   F

Complexity

Total Complexity 157
Complexity/F 2.71

Size

Lines of Code 943
Function Count 58

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 635
c 0
b 0
f 0
dl 0
loc 943
rs 1.965
wmc 157
mnd 99
bc 99
fnc 58
bpm 1.7068
cpm 2.7068
noi 10

How to fix   Complexity   

Complexity

Complex classes like js/app.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * Analytics
3
 *
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later. See the LICENSE.md file.
6
 *
7
 * @author Marcel Scherello <[email protected]>
8
 * @copyright 2021 Marcel Scherello
9
 */
10
/** global: OCA */
11
/** global: OCP */
12
/** global: OC */
13
/** global: table */
14
/** global: Chart */
15
/** global: cloner */
16
/** global: _ */
17
18
'use strict';
19
20
if (!OCA.Analytics) {
21
    /**
22
     * @namespace
23
     */
24
    OCA.Analytics = {
25
        TYPE_EMPTY_GROUP: 0,
26
        TYPE_INTERNAL_FILE: 1,
27
        TYPE_INTERNAL_DB: 2,
28
        TYPE_GIT: 3,
29
        TYPE_EXTERNAL_FILE: 4,
30
        TYPE_EXTERNAL_REGEX: 5,
31
        TYPE_EXCEL: 7,
32
        TYPE_SHARED: 99,
33
        SHARE_TYPE_USER: 0,
34
        SHARE_TYPE_GROUP: 1,
35
        SHARE_TYPE_LINK: 3,
36
        SHARE_TYPE_ROOM: 10,
37
        initialDocumentTitle: null,
38
        isAdvanced: false,
39
        currentReportData: {},
40
        chartObject: null,
41
        // flexible mapping depending on type requiered by the used chart library
42
        chartTypeMapping: {
43
            'datetime': 'line',
44
            'column': 'bar',
45
            'area': 'line',
46
            'line': 'line',
47
            'doughnut': 'doughnut'
48
        },
49
        datasources: [],
50
        datasourceOptions: [],
51
        datasets: [],
52
        reports: [],
53
        unsavedFilters: null,
54
        refreshTimer: null,
55
    };
56
}
57
/**
58
 * @namespace OCA.Analytics.Core
59
 */
60
OCA.Analytics.Core = {
61
    initApplication: function () {
62
        OCA.Analytics.Backend.getDatasourceDefinitions();
63
        OCA.Analytics.Backend.getDatasetDefinitions();
64
65
        const urlHash = decodeURI(location.hash);
66
        if (urlHash.length > 1) {
67
            if (urlHash[2] === 'f') {
68
                window.location.href = '#';
69
                OCA.Analytics.Navigation.createDataset(urlHash.substring(3));
70
            } else if (urlHash[2] === 'r') {
71
                OCA.Analytics.Navigation.init(urlHash.substring(4));
72
            }
73
        } else {
74
            OCA.Analytics.Navigation.init();
75
        }
76
    },
77
78
    getDistinctValues: function (array) {
79
        let unique = [];
80
        let distinct = [];
81
        if (array === undefined) {
82
            return distinct;
83
        }
84
        for (let i = 0; i < array.length; i++) {
85
            if (!unique[array[i][0]]) {
86
                distinct.push(array[i][0]);
87
                unique[array[i][0]] = 1;
88
            }
89
        }
90
        return distinct;
91
    },
92
93
    getInitialState: function (key) {
94
        const app = 'analytics';
95
        const elem = document.querySelector(`#initial-state-${app}-${key}`)
96
        if (elem === null) {
97
            return false;
98
        }
99
        return JSON.parse(atob(elem.value))
100
    }
101
};
102
103
OCA.Analytics.UI = {
104
105
    buildDataTable: function (jsondata) {
106
        OCA.Analytics.UI.showElement('tableContainer');
107
108
        let columns = [];
109
        let data, unit = '';
110
111
        let header = jsondata.header;
112
        let allDimensions = jsondata.dimensions;
113
        (jsondata.dimensions) ? allDimensions = jsondata.dimensions : allDimensions = jsondata.header;
114
        let headerKeys = Object.keys(header);
115
        for (let i = 0; i < headerKeys.length; i++) {
116
            columns[i] = {'title': (header[headerKeys[i]] !== null) ? header[headerKeys[i]] : ""};
117
            let columnType = Object.keys(allDimensions).find(key => allDimensions[key] === header[headerKeys[i]]);
118
119
            if (i === headerKeys.length - 1) {
120
                // prepare for later unit cloumn
121
                //columns[i]['render'] = function(data, type, row, meta) {
122
                //    return data + ' ' + row[row.length-2];
123
                //};
124
                if (header[headerKeys[i]] !== null && header[headerKeys[i]].length === 1) {
125
                    unit = header[headerKeys[i]];
126
                }
127
                columns[i]['render'] = $.fn.dataTable.render.number('.', ',', 2, unit + ' ');
128
                columns[i]['className'] = 'dt-right';
129
            } else if (columnType === 'timestamp') {
130
                columns[i]['render'] = function (data, type) {
131
                    // If display or filter data is requested, format the date
132
                    if (type === 'display' || type === 'filter') {
133
                        return new Date(data * 1000).toLocaleString();
134
                    }
135
                    // Otherwise the data type requested (`type`) is type detection or
136
                    // sorting data, for which we want to use the integer, so just return
137
                    // that, unaltered
138
                    return data;
139
                }
140
            } else if (columnType === 'unit') {
141
                columns[i]['visible'] = false;
142
                columns[i]['searchable'] = false;
143
            }
144
        }
145
        data = jsondata.data;
146
147
        const language = {
148
            search: t('analytics', 'Search'),
149
            lengthMenu: t('analytics', 'Show _MENU_ entries'),
150
            info: t('analytics', 'Showing _START_ to _END_ of _TOTAL_ entries'),
151
            infoEmpty: t('analytics', 'Showing 0 to 0 of 0 entries'),
152
            paginate: {
153
                first: t('analytics', 'first'),
154
                previous: t('analytics', 'previous'),
155
                next: t('analytics', 'next'),
156
                last: t('analytics', 'last')
157
            },
158
        };
159
160
        $('#tableContainer').DataTable({
161
            data: data,
162
            columns: columns,
163
            language: language,
164
            rowCallback: function (row, data, index) {
165
                OCA.Analytics.UI.dataTableRowCallback(row, data, index, jsondata.thresholds)
166
            },
167
            drawCallback: function () {
168
                var pagination = $(this).closest('.dataTables_wrapper').find('.dataTables_paginate');
169
                pagination.toggle(this.api().page.info().pages > 1);
170
                var info = $(this).closest('.dataTables_wrapper').find('.dataTables_info');
171
                info.toggle(this.api().page.info().pages > 1);
172
                var length = $(this).closest('.dataTables_wrapper').find('.dataTables_length');
173
                length.toggle(this.api().page.info().pages > 1);
174
                var filter = $(this).closest('.dataTables_wrapper').find('.dataTables_filter');
175
                filter.toggle(this.api().page.info().pages > 1);
176
            },
177
        });
178
    },
179
180
    dataTableRowCallback: function (row, data, index, thresholds) {
181
        const operators = {
182
            '=': function (a, b) {
183
                return a === b
184
            },
185
            '<': function (a, b) {
186
                return a < b
187
            },
188
            '>': function (a, b) {
189
                return a > b
190
            },
191
            '<=': function (a, b) {
192
                return a <= b
193
            },
194
            '>=': function (a, b) {
195
                return a >= b
196
            },
197
            '!=': function (a, b) {
198
                return a !== b
199
            },
200
        };
201
202
        thresholds = thresholds.filter(p => p.dimension1 === data[0] || p.dimension1 === '*');
203
204
        for (let threshold of thresholds) {
205
            const comparison = operators[threshold['option']](parseFloat(data[2]), parseFloat(threshold['value']));
206
            threshold['severity'] = parseInt(threshold['severity']);
207
            if (comparison === true) {
208
                if (threshold['severity'] === 2) {
209
                    $(row).find('td:eq(2)').css('color', 'red');
210
                } else if (threshold['severity'] === 3) {
211
                    $(row).find('td:eq(2)').css('color', 'orange');
212
                } else if (threshold['severity'] === 4) {
213
                    $(row).find('td:eq(2)').css('color', 'green');
214
                }
215
            }
216
        }
217
    },
218
219
    buildChart: function (jsondata) {
220
221
        OCA.Analytics.UI.showElement('tableSeparatorContainer');
222
        OCA.Analytics.UI.showElement('chartContainer');
223
        let ctx = document.getElementById('myChart').getContext('2d');
224
225
        let chartType;
226
        jsondata.options.chart === '' ? chartType = 'column' : chartType = jsondata.options.chart;
227
        let datasets = [], xAxisCategories = [];
228
        let lastObject = false;
229
        let dataSeries = -1;
230
        let targetDataseries = 0;
231
        let hidden = false;
232
233
        let header = jsondata.header;
234
        let headerKeys = Object.keys(header).length;
235
        let dataSeriesColumn = headerKeys - 3; //characteristic is taken from the second last column
236
        let characteristicColumn = headerKeys - 2; //characteristic is taken from the second last column
237
        let keyFigureColumn = headerKeys - 1; //key figures is taken from the last column
238
239
        Chart.defaults.elements.line.borderWidth = 2;
240
        Chart.defaults.elements.line.tension = 0.1;
241
        Chart.defaults.elements.line.fill = false;
242
        Chart.defaults.elements.point.radius = 1;
243
        Chart.defaults.plugins.legend.display = false;
244
        Chart.defaults.plugins.legend.position = 'bottom';
245
246
        var chartOptions = {
247
            maintainAspectRatio: false,
248
            responsive: true,
249
            scales: {
250
                'primary': {
251
                    type: 'linear',
252
                    stacked: false,
253
                    position: 'left',
254
                    display: true,
255
                    grid: {
256
                        display: true,
257
                    },
258
                    ticks: {
259
                        callback: function (value) {
260
                            return value.toLocaleString();
261
                        },
262
                    },
263
                },
264
                'secondary': {
265
                    type: 'linear',
266
                    stacked: false,
267
                    position: 'right',
268
                    display: false,
269
                    grid: {
270
                        display: false,
271
                    },
272
                    ticks: {
273
                        callback: function (value) {
274
                            return value.toLocaleString();
275
                        },
276
                    },
277
                },
278
                'xAxes': {
279
                    type: 'category',
280
                    time: {
281
                        parser: 'YYYY-MM-DD HH:mm',
282
                        tooltipFormat: 'LL',
283
                    },
284
                    distribution: 'linear',
285
                    grid: {
286
                        display: false
287
                    },
288
                    display: true,
289
                },
290
            },
291
            animation: {
292
                duration: 0 // general animation time
293
            },
294
295
            tooltips: {
296
                callbacks: {
297
                    label: function (tooltipItem, data) {
298
//                        let datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
299
                        let datasetLabel = data.datasets[tooltipItem.datasetIndex].label || data.labels[tooltipItem.index];
300
                        if (tooltipItem.yLabel !== '') {
301
                            return datasetLabel + ': ' + parseFloat(tooltipItem.yLabel).toLocaleString();
302
                        } else {
303
                            return datasetLabel;
304
                        }
305
                    }
306
                }
307
            },
308
309
            plugins: {
310
                datalabels: {
311
                    display: false,
312
                    formatter: (value, ctx) => {
313
                        let sum = 0;
314
                        let dataArr = ctx.chart.data.datasets[0].data;
315
                        dataArr.map(data => {
316
                            sum += data;
317
                        });
318
                        value = (value * 100 / sum).toFixed(0);
319
                        if (value > 5) {
320
                            return value + "%";
321
                        } else {
322
                            return '';
323
                        }
324
                    },
325
                }
326
            },
327
        };
328
329
        for (let values of jsondata.data) {
330
            // indexOf will search, if an exiting dataset existists already for that lable (first column)
331
            // internal data comes sorted from the database and the dataSeries can increment
332
            // external data like csv can be unsorted
333
            if (dataSeriesColumn >= 0 && datasets.indexOf(datasets.find(o => o.label === values[dataSeriesColumn])) === -1) {
334
                // create new dataseries for every new lable in dataSeriesColumn
335
                datasets.push({label: values[dataSeriesColumn], data: [], hidden: hidden});
336
                dataSeries++;
337
                // default hide > 4th series for better visibility
338
                if (dataSeries === 3) {
339
                    hidden = true;
340
                }
341
                lastObject = values[dataSeriesColumn];
342
                targetDataseries = dataSeries;
343
            } else if (lastObject === false) {
344
                // when only 2 columns are provided, no label will be set
345
                datasets.push({label: '', data: [], hidden: hidden});
346
                dataSeries++;
347
                targetDataseries = dataSeries;
348
                lastObject = true;
349
            } else if (lastObject !== values[dataSeriesColumn] && lastObject !== true) {
350
                // find the correct dataset, where the data needs to be added to
351
                targetDataseries = datasets.indexOf(datasets.find(o => o.label === values[dataSeriesColumn]));
352
                if (targetDataseries === -1) {
353
                    targetDataseries = 0;
354
                }
355
                lastObject = values[dataSeriesColumn];
356
            }
357
358
            if (chartType === 'datetime' || chartType === 'area') {
359
                datasets[targetDataseries]['data'].push({
360
                    y: parseFloat(values[keyFigureColumn]),
361
                    x: values[characteristicColumn]
362
363
                });
364
            } else {
365
                datasets[targetDataseries]['data'].push(parseFloat(values[keyFigureColumn]));
366
                if (targetDataseries === 0) {
367
                    // Add category lables only once and not for every data series.
368
                    // They have to be unique anyway
369
                    xAxisCategories.push(values[characteristicColumn]);
370
                }
371
            }
372
        }
373
374
        if (datasets.length > 1) {
375
            // show legend button only when usefull with >1 dataset
376
            OCA.Analytics.UI.showElement('chartLegendContainer');
377
        }
378
379
        // do the color magic
380
        // a predefined color array is used
381
        let colors = ["#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5", "#c49c94", "#f7b6d2", "#c7c7c7", "#dbdb8d", "#9edae5"];
382
        for (let i = 0; i < datasets.length; ++i) {
383
            let j = i - (Math.floor(i / colors.length) * colors.length)
384
385
            // in only one dataset is being shown, create a fancy gadient fill
386
            if (datasets.length === 1 && chartType !== 'column' && chartType !== 'doughnut') {
387
                const hexToRgb = colors[j].replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i
388
                    , (m, r, g, b) => '#' + r + r + g + g + b + b)
389
                    .substring(1).match(/.{2}/g)
390
                    .map(x => parseInt(x, 16));
391
392
                datasets[0].backgroundColor = function (context) {
393
                    const chart = context.chart;
394
                    const {ctx, chartArea} = chart;
395
                    let gradient = ctx.createLinearGradient(0, 0, 0, chart.height);
396
                    gradient.addColorStop(0, 'rgb(' + hexToRgb[0] + ',' + hexToRgb[1] + ',' + hexToRgb[2] + ')');
397
                    gradient.addColorStop(1, 'rgb(' + hexToRgb[0] + ',' + hexToRgb[1] + ',' + hexToRgb[2] + ',0)');
398
                    return gradient;
399
                }
400
                datasets[i].borderColor = colors[j];
401
                Chart.defaults.elements.line.fill = true;
402
            } else if (chartType === 'doughnut') {
403
                // special array handling for dougnuts
404
                datasets[i].backgroundColor = colors;
405
                datasets[i].borderColor = colors;
406
                Chart.defaults.elements.line.fill = false;
407
            } else {
408
                datasets[i].backgroundColor = colors[j];
409
                Chart.defaults.elements.line.fill = false;
410
                datasets[i].borderColor = colors[j];
411
            }
412
        }
413
414
        if (chartType === 'datetime') {
415
            chartOptions.scales['xAxes'].type = 'time';
416
            chartOptions.scales['xAxes'].distribution = 'linear';
417
        } else if (chartType === 'area') {
418
            chartOptions.scales['xAxes'].type = 'time';
419
            chartOptions.scales['xAxes'].distribution = 'linear';
420
            chartOptions.scales['primary'].stacked = true;
421
            Chart.defaults.elements.line.fill = true;
422
        } else if (chartType === 'doughnut') {
423
            chartOptions.scales['xAxes'].display = false;
424
            chartOptions.scales['primary'].display = false;
425
            chartOptions.scales['primary'].grid.display = false;
426
            chartOptions.scales['secondary'].display = false;
427
            chartOptions.scales['secondary'].grid.display = false;
428
            chartOptions.circumference = 180;
429
            chartOptions.rotation = -90;
430
            Chart.defaults.plugins.legend.display = true;
431
            chartOptions.plugins.datalabels.display = true;
432
        }
433
434
        // the user can add/overwrite chart options
435
        // the user can put the options in array-format into the report definition
436
        // these are merged with the standard report settings
437
        // e.g. the display unit for the x-axis can be overwritten '{"scales": {"xAxes": {"time": {"unit" : "month"}}}}'
438
        // e.g. add a secondary y-axis '{"scales":{"secondary":{"display":true}}}'
439
440
        // replace old settings from Chart.js 2
441
        // {"scales":{"yAxes":[{},{"display":true}]}} => {"scales":{"secondary":{"display":true}}}
442
        if (jsondata.options.chartoptions !== null) {
443
            jsondata.options.chartoptions = jsondata.options.chartoptions.replace('{"yAxes":[{},{"display":true}]}', '{"secondary":{"display":true}}');
444
        }
445
        OCA.Analytics.currentReportData.options.chartoptions = jsondata.options.chartoptions;
446
        let userChartOptions = jsondata.options.chartoptions;
447
        if (userChartOptions !== '' && userChartOptions !== null) {
448
            chartOptions = cloner.deep.merge(chartOptions, JSON.parse(userChartOptions));
449
        }
450
451
        // the user can modify dataset/series settings
452
        // these are merged with the data array coming from the backend
453
        // e.g. assign one series to the secondary y-axis: '[{"yAxisID":"B"},{},{"yAxisID":"B"},{}]'
454
        //let userDatasetOptions = document.getElementById('userDatasetOptions').value;
455
        let userDatasetOptions = jsondata.options.dataoptions;
456
        if (userDatasetOptions !== '' && userDatasetOptions !== null) {
457
            datasets = cloner.deep.merge({}, datasets);
458
            datasets = cloner.deep.merge(datasets, JSON.parse(userDatasetOptions));
459
            datasets = Object.values(datasets);
460
        }
461
462
        OCA.Analytics.chartObject = new Chart(ctx, {
463
            plugins: [ChartDataLabels],
464
            type: OCA.Analytics.chartTypeMapping[chartType],
465
            data: {
466
                labels: xAxisCategories,
467
                datasets: datasets
468
            },
469
            options: chartOptions,
470
        });
471
    },
472
473
    toggleChartLegend: function () {
474
        OCA.Analytics.chartObject.legend.options.display = !OCA.Analytics.chartObject.legend.options.display
475
        OCA.Analytics.chartObject.update();
476
    },
477
478
    downloadChart: function () {
479
        OCA.Analytics.UI.hideReportMenu();
480
        document.getElementById('downlaodChartLink').href = OCA.Analytics.chartObject.toBase64Image();
481
        document.getElementById('downlaodChartLink').setAttribute('download', OCA.Analytics.currentReportData.options.name + '.png');
482
        document.getElementById('downlaodChartLink').click();
483
    },
484
485
    resetContentArea: function () {
486
        if (OCA.Analytics.isAdvanced) {
487
            OCA.Analytics.UI.showElement('analytics-intro');
488
            document.getElementById('app-sidebar').classList.add('disappear');
489
        } else {
490
            if ($.fn.dataTable.isDataTable('#tableContainer')) {
491
                $('#tableContainer').DataTable().destroy();
492
            }
493
            OCA.Analytics.UI.hideElement('chartContainer');
494
            OCA.Analytics.UI.hideElement('chartLegendContainer');
495
            document.getElementById('chartContainer').innerHTML = '';
496
            document.getElementById('chartContainer').innerHTML = '<canvas id="myChart" ></canvas>';
497
            OCA.Analytics.UI.hideElement('tableContainer');
498
            OCA.Analytics.UI.hideElement('tableSeparatorContainer');
499
            document.getElementById('tableContainer').innerHTML = '';
500
            document.getElementById('reportHeader').innerHTML = '';
501
            document.getElementById('reportSubHeader').innerHTML = '';
502
            OCA.Analytics.UI.hideElement('reportSubHeader');
503
            OCA.Analytics.UI.hideElement('noDataContainer');
504
505
            OCA.Analytics.UI.showElement('reportMenuBar');
506
            OCA.Analytics.UI.hideReportMenu();
507
            document.getElementById('chartOptionsIcon').disabled = false;
508
            document.getElementById('analysisIcon').disabled = false;
509
            document.getElementById('drilldownIcon').disabled = false;
510
            document.getElementById('downlaodChartIcon').disabled = false;
511
            document.getElementById('analysisIcon').disabled = false;
512
        }
513
    },
514
515
    buildReportOptions: function () {
516
        let currentReport = OCA.Analytics.currentReportData;
517
        let canUpdate = parseInt(currentReport.options.permissions) === OC.PERMISSION_UPDATE;
518
        let isInternalShare = currentReport.options.isShare !== undefined;
519
        let isExternalShare = document.getElementById('sharingToken').value !== '';
520
521
        if (isExternalShare) {
522
            if (canUpdate) {
523
                OCA.Analytics.UI.hideElement('reportMenuIcon');
524
                OCA.Analytics.Filter.refreshFilterVisualisation();
525
            } else {
526
                //document.getElementById('reportMenuBar').remove();
527
                OCA.Analytics.UI.hideElement('reportMenuBar');
528
                //document.getElementById('reportMenuBar').id = 'reportMenuBarHidden';
529
            }
530
            return;
531
        }
532
533
        if (!canUpdate) {
534
            OCA.Analytics.UI.hideElement('reportMenuBar');
535
        }
536
537
        if (isInternalShare) {
538
            OCA.Analytics.UI.showElement('reportMenuIcon');
539
        }
540
541
        if (parseInt(currentReport.options.type) !== OCA.Analytics.TYPE_INTERNAL_DB) {
542
            document.getElementById('drilldownIcon').disabled = true;
543
        }
544
545
        if (currentReport.options.chart === 'doughnut') {
546
            document.getElementById('analysisIcon').disabled = true;
547
        }
548
549
        let visualization = currentReport.options.visualization;
550
        if (visualization === 'table') {
551
            document.getElementById('chartOptionsIcon').disabled = true;
552
            document.getElementById('analysisIcon').disabled = true;
553
            document.getElementById('downlaodChartIcon').disabled = true;
554
        }
555
556
        let refresh = parseInt(currentReport.options.refresh);
557
        isNaN(refresh) ? refresh = 0 : refresh = refresh;
558
        document.getElementById('refresh' + refresh).checked = true;
559
560
        OCA.Analytics.Filter.refreshFilterVisualisation();
561
    },
562
563
    reportOptionsEventlisteners: function () {
564
        document.getElementById('addFilterIcon').addEventListener('click', OCA.Analytics.Filter.openFilterDialog);
565
        document.getElementById('reportMenuIcon').addEventListener('click', OCA.Analytics.UI.toggleReportMenu);
566
        document.getElementById('saveIcon').addEventListener('click', OCA.Analytics.Filter.Backend.updateReport);
567
        document.getElementById('saveIconNew').addEventListener('click', OCA.Analytics.Filter.Backend.newReport);
568
        document.getElementById('drilldownIcon').addEventListener('click', OCA.Analytics.Filter.openDrilldownDialog);
569
        document.getElementById('chartOptionsIcon').addEventListener('click', OCA.Analytics.Filter.openChartOptionsDialog);
570
571
        document.getElementById('analysisIcon').addEventListener('click', OCA.Analytics.UI.showReportMenuAnalysis);
572
        document.getElementById('refreshIcon').addEventListener('click', OCA.Analytics.UI.showReportMenuRefresh);
573
        document.getElementById('trendIcon').addEventListener('click', OCA.Analytics.Functions.trend);
574
        //document.getElementById('linearRegressionIcon').addEventListener('click', OCA.Analytics.Functions.linearRegression);
575
        document.getElementById('backIcon').addEventListener('click', OCA.Analytics.UI.showReportMenuMain);
576
        document.getElementById('backIcon2').addEventListener('click', OCA.Analytics.UI.showReportMenuMain);
577
        document.getElementById('downlaodChartIcon').addEventListener('click', OCA.Analytics.UI.downloadChart);
578
        document.getElementById('chartLegend').addEventListener('click', OCA.Analytics.UI.toggleChartLegend);
579
580
        let refresh = document.getElementsByName('refresh');
581
        for (let i = 0; i < refresh.length; i++) {
582
            refresh[i].addEventListener('change', OCA.Analytics.Filter.Backend.saveRefresh);
583
        }
584
    },
585
586
    showElement: function (element) {
587
        if (document.getElementById(element)) {
588
            document.getElementById(element).hidden = false;
589
            //document.getElementById(element).style.removeProperty('display');
590
        }
591
    },
592
593
    hideElement: function (element) {
594
        if (document.getElementById(element)) {
595
            document.getElementById(element).hidden = true;
596
            //document.getElementById(element).style.display = 'none';
597
        }
598
    },
599
600
    hideReportMenu: function () {
601
        if (document.getElementById('reportMenu') !== null) {
602
            document.getElementById('reportMenu').classList.remove('open');
603
        }
604
    },
605
606
    toggleReportMenu: function () {
607
        document.getElementById('reportMenu').classList.toggle('open');
608
        document.getElementById('reportMenuMain').style.removeProperty('display');
609
        document.getElementById('reportMenuAnalysis').style.setProperty('display', 'none', 'important');
610
        document.getElementById('reportMenuRefresh').style.setProperty('display', 'none', 'important');
611
    },
612
613
    showReportMenuAnalysis: function () {
614
        document.getElementById('reportMenuMain').style.setProperty('display', 'none', 'important');
615
        document.getElementById('reportMenuAnalysis').style.removeProperty('display');
616
    },
617
618
    showReportMenuRefresh: function () {
619
        document.getElementById('reportMenuMain').style.setProperty('display', 'none', 'important');
620
        document.getElementById('reportMenuRefresh').style.removeProperty('display');
621
    },
622
623
    showReportMenuMain: function () {
624
        document.getElementById('reportMenuAnalysis').style.setProperty('display', 'none', 'important');
625
        document.getElementById('reportMenuRefresh').style.setProperty('display', 'none', 'important');
626
        document.getElementById('reportMenuMain').style.removeProperty('display');
627
    },
628
629
630
};
631
632
OCA.Analytics.Functions = {
633
634
    trend: function () {
635
        OCA.Analytics.UI.showElement('chartLegendContainer');
636
        OCA.Analytics.UI.hideReportMenu();
637
638
        let numberDatasets = OCA.Analytics.chartObject.data.datasets.length;
639
        let datasetType = 'time';
640
        for (let y = 0; y < numberDatasets; y++) {
641
            let dataset = OCA.Analytics.chartObject.data.datasets[y];
642
            let newLabel = dataset.label + " Trend";
643
644
            // generate trend only for visible data series
645
            if (OCA.Analytics.chartObject.isDatasetVisible(y) === false) continue;
646
            // dont add trend twice
647
            if (OCA.Analytics.chartObject.data.datasets.find(o => o.label === newLabel) !== undefined) continue;
648
            // dont add trend for a trend
649
            if (dataset.label.substr(dataset.label.length - 6) === " Trend") continue;
650
651
            let yValues = [];
652
            for (let i = 0; i < dataset.data.length; i++) {
653
                if (typeof (dataset.data[i]) === 'number') {
654
                    datasetType = 'bar';
655
                    yValues.push(parseInt(dataset.data[i]));
656
                } else {
657
                    yValues.push(parseInt(dataset.data[i]["y"]));
658
                }
659
            }
660
            let xValues = [];
661
            for (let i = 1; i <= dataset.data.length; i++) {
662
                xValues.push(i);
663
            }
664
665
            let regression = OCA.Analytics.Functions.regression(xValues, yValues);
666
            let ylast = ((dataset.data.length) * regression["slope"]) + regression["intercept"];
667
668
            let data = [];
669
            if (datasetType === 'time') {
670
                data = [
671
                    {x: dataset.data[0]["x"], y: regression["intercept"]},
672
                    {x: dataset.data[dataset.data.length - 1]["x"], y: ylast},
673
                ]
674
            } else {
675
                for (let i = 1; i < dataset.data.length + 1; i++) {
676
                    data.push(((i) * regression["slope"]) + regression["intercept"]);
677
                }
678
            }
679
            let newDataset = {
680
                label: newLabel,
681
                backgroundColor: dataset.backgroundColor,
682
                borderColor: dataset.borderColor,
683
                borderDash: [5, 5],
684
                type: 'line',
685
                yAxisID: dataset.yAxisID,
686
                data: data
687
            };
688
            OCA.Analytics.chartObject.data.datasets.push(newDataset);
689
690
        }
691
        OCA.Analytics.chartObject.update();
692
    },
693
694
    regression: function (x, y) {
695
        const n = y.length;
696
        let sx = 0;
697
        let sy = 0;
698
        let sxy = 0;
699
        let sxx = 0;
700
        let syy = 0;
701
        for (let i = 0; i < n; i++) {
702
            sx += x[i];
703
            sy += y[i];
704
            sxy += x[i] * y[i];
705
            sxx += x[i] * x[i];
706
            syy += y[i] * y[i];
707
        }
708
        const mx = sx / n;
709
        const my = sy / n;
710
        const yy = n * syy - sy * sy;
711
        const xx = n * sxx - sx * sx;
712
        const xy = n * sxy - sx * sy;
713
        const slope = xy / xx;
714
        const intercept = my - slope * mx;
715
716
        return {slope, intercept};
717
    }
718
719
};
720
721
OCA.Analytics.Datasource = {
722
    buildDropdown: function () {
723
        let options = document.createDocumentFragment();
724
        let sortedOptions = OCA.Analytics.Datasource.sortOptions(OCA.Analytics.datasources);
725
        sortedOptions.forEach((entry) => {
726
            let value = entry[1];
727
            let option = document.createElement('option');
728
            option.value = entry[0];
729
            option.innerText = value;
730
            options.appendChild(option);
731
        });
732
        return options;
733
    },
734
735
    sortOptions: function (obj) {
736
        var sortable = [];
737
        for (var key in obj)
738
            if (obj.hasOwnProperty(key))
739
                sortable.push([key, obj[key]]);
740
        sortable.sort(function (a, b) {
741
            var x = a[1].toLowerCase(),
742
                y = b[1].toLowerCase();
743
            return x < y ? -1 : x > y ? 1 : 0;
744
        });
745
        return sortable;
746
    },
747
748
    buildOptionsForm: function (datasource) {
749
        let template = OCA.Analytics.datasourceOptions[datasource];
750
        let form = document.createDocumentFragment();
751
752
        for (let templateOption of template) {
753
            // loop all options of the datasourcetemplate and create the input form
754
            let tablerow = document.createElement('div');
755
            tablerow.style.display = 'table-row';
756
            let label = document.createElement('div');
757
            label.style.display = 'table-cell';
758
            label.style.width = '100%';
759
            label.innerText = templateOption.name;
760
761
            let input;
762
            if (templateOption.type && templateOption.type === 'tf') {
763
                input = OCA.Analytics.Datasource.buildOptionsSelect(templateOption);
764
            } else {
765
                input = OCA.Analytics.Datasource.buildOptionsInput(templateOption);
766
            }
767
            input.style.display = 'table-cell';
768
            form.appendChild(tablerow);
769
            tablerow.appendChild(label);
770
            tablerow.appendChild(input);
771
        }
772
        return form;
773
    },
774
775
    buildOptionsInput: function (templateOption) {
776
        let input = document.createElement('input');
777
        input.style.display = 'inline-flex';
778
        input.classList.add('sidebarInput');
779
        input.placeholder = templateOption.placeholder;
780
        input.id = templateOption.id;
781
        return input;
782
    },
783
784
    buildOptionsSelect: function (templateOption) {
785
        let input = document.createElement('select');
786
        input.style.display = 'inline-flex';
787
        input.classList.add('sidebarInput');
788
        input.id = templateOption.id;
789
790
        let selectOptions = templateOption.placeholder.split("/")
791
        for (let selectOption of selectOptions) {
792
            let option = document.createElement('option');
793
            option.value = selectOption;
794
            option.innerText = selectOption;
795
            input.appendChild(option);
796
        }
797
        return input;
798
    },
799
800
};
801
802
OCA.Analytics.Backend = {
803
804
    getData: function () {
805
        OCA.Analytics.UI.resetContentArea();
806
        OCA.Analytics.UI.hideElement('analytics-intro');
807
        OCA.Analytics.UI.hideElement('analytics-content');
808
        OCA.Analytics.UI.showElement('analytics-loading');
809
810
        let url;
811
        if (document.getElementById('sharingToken').value === '') {
812
            const reportId = document.querySelector('#navigationDatasets .active').firstElementChild.dataset.id;
813
            if (parseInt(reportId) === OCA.Analytics.Navigation.newReportId) return; //don´t load for new reports
814
            url = OC.generateUrl('apps/analytics/data/') + reportId;
815
        } else {
816
            const token = document.getElementById('sharingToken').value;
817
            url = OC.generateUrl('apps/analytics/data/public/') + token;
818
        }
819
820
        // send user current filteroptions to the datarequest;
821
        // if nothing is changed by the user, the filter which is stored for the report, will be used
822
        let ajaxData = {};
823
        if (typeof (OCA.Analytics.currentReportData.options) !== 'undefined' && typeof (OCA.Analytics.currentReportData.options.filteroptions) !== 'undefined') {
824
            ajaxData.filteroptions = JSON.stringify(OCA.Analytics.currentReportData.options.filteroptions);
825
        }
826
827
        if (typeof (OCA.Analytics.currentReportData.options) !== 'undefined' && typeof (OCA.Analytics.currentReportData.options.dataoptions) !== 'undefined') {
828
            ajaxData.dataoptions = OCA.Analytics.currentReportData.options.dataoptions;
829
        }
830
831
        if (typeof (OCA.Analytics.currentReportData.options) !== 'undefined' && typeof (OCA.Analytics.currentReportData.options.chartoptions) !== 'undefined') {
832
            ajaxData.chartoptions = OCA.Analytics.currentReportData.options.chartoptions;
833
        }
834
835
        $.ajax({
836
            type: 'GET',
837
            url: url,
838
            data: ajaxData,
839
            success: function (data) {
840
                OCA.Analytics.UI.hideElement('analytics-loading');
841
                OCA.Analytics.UI.showElement('analytics-content');
842
                OCA.Analytics.currentReportData = data;
843
                try {
844
                    OCA.Analytics.currentReportData.options.filteroptions = JSON.parse(OCA.Analytics.currentReportData.options.filteroptions);
845
                } catch (e) {
846
                    OCA.Analytics.currentReportData.options.filteroptions = {};
847
                }
848
                if (OCA.Analytics.currentReportData.options.filteroptions === null) {
849
                    OCA.Analytics.currentReportData.options.filteroptions = {};
850
                }
851
852
                document.getElementById('reportHeader').innerText = data.options.name;
853
                if (data.options.subheader !== '') {
854
                    document.getElementById('reportSubHeader').innerText = data.options.subheader;
855
                    OCA.Analytics.UI.showElement('reportSubHeader');
856
                }
857
858
                document.title = data.options.name + ' @ ' + OCA.Analytics.initialDocumentTitle;
859
                if (data.status !== 'nodata' && parseInt(data.error) === 0) {
860
                    data.data = OCA.Analytics.Backend.formatDates(data.data);
861
                    let visualization = data.options.visualization;
862
                    if (visualization === 'chart') {
863
                        OCA.Analytics.UI.buildChart(data);
864
                    } else if (visualization === 'table') {
865
                        OCA.Analytics.UI.buildDataTable(data);
866
                    } else {
867
                        OCA.Analytics.UI.buildChart(data);
868
                        OCA.Analytics.UI.buildDataTable(data);
869
                    }
870
                } else {
871
                    OCA.Analytics.UI.showElement('noDataContainer');
872
                    if (parseInt(data.error) !== 0) {
873
                        OCA.Analytics.Notification.notification('error', data.error);
874
                    }
875
                }
876
                OCA.Analytics.UI.buildReportOptions();
877
878
                let refresh = parseInt(OCA.Analytics.currentReportData.options.refresh);
879
                OCA.Analytics.Backend.startRefreshTimer(refresh);
880
            }
881
        });
882
    },
883
884
    formatDates: function (data) {
885
        let firstrow = data[0];
886
        let now;
887
        for (let i = 0; i < firstrow.length; i++) {
888
            // loop columns and check for a valid date
889
            if (!isNaN(new Date(firstrow[i]).valueOf()) && firstrow[i] !== null && firstrow[i].length >= 19) {
890
                // column contains a valid date
891
                // then loop all rows for this column and convert to local time
892
                for (let j = 0; j < data.length; j++) {
893
                    if (data[j][i].length === 19) {
894
                        // values are assumed to have a timezone or are used as UTC
895
                        data[j][i] = data[j][i] + 'Z';
896
                    }
897
                    now = new Date(data[j][i]);
898
                    data[j][i] = now.getFullYear()
899
                        + "-" + (now.getMonth() < 10 ? '0' : '') + (now.getMonth() + 1)
900
                        + "-" + (now.getDate() < 10 ? '0' : '') + now.getDate()
901
                        + " " + (now.getHours() < 10 ? '0' : '') + now.getHours()
902
                        + ":" + (now.getMinutes() < 10 ? '0' : '') + now.getMinutes()
903
                        + ":" + (now.getSeconds() < 10 ? '0' : '') + now.getSeconds()
904
                }
905
            }
906
        }
907
        return data;
908
    },
909
910
    getDatasourceDefinitions: function () {
911
        $.ajax({
912
            type: 'GET',
913
            url: OC.generateUrl('apps/analytics/datasource'),
914
            success: function (data) {
915
                OCA.Analytics.datasourceOptions = data['options'];
916
                OCA.Analytics.datasources = data['datasources'];
917
            }
918
        });
919
    },
920
921
    getDatasetDefinitions: function () {
922
        $.ajax({
923
            type: 'GET',
924
            url: OC.generateUrl('apps/analytics/dataset'),
925
            success: function (data) {
926
                OCA.Analytics.datasets = data;
927
            }
928
        });
929
    },
930
931
    startRefreshTimer(minutes) {
932
        if (minutes !== 0 && !isNaN(minutes)) {
933
            if (OCA.Analytics.refreshTimer === null) {
934
                OCA.Analytics.refreshTimer = setTimeout(OCA.Analytics.Backend.getData, minutes * 60 * 1000)
935
            } else {
936
                clearTimeout(OCA.Analytics.refreshTimer)
937
                OCA.Analytics.refreshTimer = null
938
                OCA.Analytics.Backend.startRefreshTimer(minutes);
939
            }
940
        } else {
941
            if (OCA.Analytics.refreshTimer !== null) {
942
                clearTimeout(OCA.Analytics.refreshTimer)
943
                OCA.Analytics.refreshTimer = null
944
            }
945
        }
946
    },
947
};
948
949
document.addEventListener('DOMContentLoaded', function () {
950
    if (document.getElementById('advanced').value === 'true') {
951
        OCA.Analytics.isAdvanced = true;
952
    }
953
    OCA.Analytics.initialDocumentTitle = document.title;
954
    OCA.Analytics.UI.hideElement('analytics-warning');
955
956
    if (document.getElementById('sharingToken').value === '') {
957
        OCA.Analytics.UI.showElement('analytics-intro');
958
        OCA.Analytics.Core.initApplication();
959
    } else {
960
        OCA.Analytics.Backend.getData();
961
    }
962
    if (!OCA.Analytics.isAdvanced) {
963
        OCA.Analytics.UI.reportOptionsEventlisteners();
964
    }
965
966
    window.addEventListener("beforeprint", function () {
967
        document.getElementById('chartContainer').style.height = document.getElementById('myChart').style.height;
968
    });
969
});