From e45f6aecf481d55f768105caaffb9f13da136217 Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 25 Aug 2025 21:11:00 +0700 Subject: [PATCH 1/6] Allow selecting columns for time aggregation --- CHANGELOG.md | 1 + js/filter.js | 45 ++++++++++++++++++++++++++++++ js/visualization.js | 53 ++++++++++++++++++++++++++---------- templates/part.templates.php | 7 +++++ 4 files changed, 92 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf1c069..2328d7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report +- Select specific columns for time aggregation ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/js/filter.js b/js/filter.js index 10c18dea..75f4c35d 100644 --- a/js/filter.js +++ b/js/filter.js @@ -406,12 +406,48 @@ OCA.Analytics.Filter = { }); const filterOptions = OCA.Analytics.currentReportData.options.filteroptions; + + const columnsContainer = container.getElementById('timeGroupingColumns'); + const header = OCA.Analytics.currentReportData.header || []; + const selectedCols = filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]; + header.forEach((name, index) => { + const div = document.createElement('div'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = 'timeGroupingColumn' + index; + checkbox.name = 'timeGroupingColumn'; + checkbox.value = index; + if (selectedCols.includes(index)) { + checkbox.checked = true; + } + const label = document.createElement('label'); + label.setAttribute('for', checkbox.id); + label.textContent = name; + div.appendChild(checkbox); + div.appendChild(label); + columnsContainer.appendChild(div); + }); + if (filterOptions && filterOptions.timeAggregation) { dimSelect.value = filterOptions.timeAggregation.dimension; groupingSelect.value = filterOptions.timeAggregation.grouping; modeSelect.value = filterOptions.timeAggregation.mode; } + const updateColumnCheckboxes = () => { + const dimIdx = parseInt(dimSelect.value.match(/\d+$/)?.[0], 10) - 1; + columnsContainer.querySelectorAll('input[name="timeGroupingColumn"]').forEach(cb => { + if (parseInt(cb.value, 10) === dimIdx) { + cb.checked = false; + cb.disabled = true; + } else { + cb.disabled = false; + } + }); + }; + updateColumnCheckboxes(); + dimSelect.addEventListener('change', updateColumnCheckboxes); + OCA.Analytics.Notification.htmlDialogUpdate( container, t('analytics', 'Aggregate daily data into weeks, months, or years') @@ -434,6 +470,15 @@ OCA.Analytics.Filter = { filterOptions.timeAggregation.grouping = grouping; filterOptions.timeAggregation.mode = document.getElementById('timeGroupingMode').value; + const selected = []; + const colBoxes = document.getElementsByName('timeGroupingColumn'); + for (let i = 0; i < colBoxes.length; i++) { + if (colBoxes[i].checked) { + selected.push(parseInt(colBoxes[i].value, 10)); + } + } + filterOptions.timeAggregation.columns = selected; + if (grouping === 'none') { delete filterOptions.timeAggregation; } diff --git a/js/visualization.js b/js/visualization.js index 74dd966d..e9bea03a 100644 --- a/js/visualization.js +++ b/js/visualization.js @@ -1120,12 +1120,23 @@ OCA.Analytics.Visualization = { const grouping = tg.grouping; const mode = tg.mode || 'summation'; - const valueIndex = data.data[0].length - 1; + const valueIndices = (tg.columns && tg.columns.length) + ? tg.columns.map(c => parseInt(c, 10)) + : [data.data[0].length - 1]; if (data.data.length === 0) { return data; } + const rowLength = data.data[0].length; + const valueSet = new Set(valueIndices); + const keyIndices = []; + for (let i = 0; i < rowLength; i++) { + if (!valueSet.has(i)) { + keyIndices.push(i); + } + } + const sample = data.data[0][dimension]; const isTimestamp = !isNaN(sample) && sample !== ''; const tsLength = isTimestamp ? String(sample).length : 0; @@ -1167,6 +1178,7 @@ OCA.Analytics.Visualization = { const sums = {}; const counts = {}; + const keyParts = {}; data.data.forEach(row => { const original = row[dimension]; @@ -1183,25 +1195,38 @@ OCA.Analytics.Visualization = { } const newTime = formatDate(date, original); - const newRow = row.slice(); - newRow[dimension] = newTime; - - const key = newRow.slice(0, valueIndex).join('\u0001'); - const val = parseFloat(row[valueIndex]) || 0; + const keyArr = keyIndices.map(i => (i === dimension ? newTime : row[i])); + const key = keyArr.join('\u0001'); if (!sums[key]) { - sums[key] = val; - counts[key] = 1; - } else { - sums[key] += val; - counts[key] += 1; + sums[key] = Array(valueIndices.length).fill(0); + counts[key] = Array(valueIndices.length).fill(0); + keyParts[key] = keyArr; } + + valueIndices.forEach((vIdx, j) => { + const val = parseFloat(row[vIdx]) || 0; + sums[key][j] += val; + counts[key][j] += 1; + }); }); data.data = Object.keys(sums).map(key => { - const parts = key.split('\u0001'); - const value = mode === 'average' ? sums[key] / counts[key] : sums[key]; - return [...parts, value.toString()]; + const parts = keyParts[key]; + const values = sums[key].map((sum, j) => { + return (mode === 'average' ? sum / counts[key][j] : sum).toString(); + }); + const row = []; + let p = 0; + let v = 0; + for (let i = 0; i < rowLength; i++) { + if (valueSet.has(i)) { + row.push(values[v++]); + } else { + row.push(parts[p++]); + } + } + return row; }); return data; diff --git a/templates/part.templates.php b/templates/part.templates.php index 20b093b4..0938f125 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -181,6 +181,13 @@ class="sidebarPointer">t('Visualization')); ?> +
+
+ +
+
+
+
From e12063fdd4288fbc03873a2b35f43e4dd7676420 Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 25 Aug 2025 21:45:21 +0700 Subject: [PATCH 2/6] Align time aggregation column selector --- CHANGELOG.md | 2 +- js/filter.js | 16 +++++++++------- templates/part.templates.php | 9 +++------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2328d7a3..3794352e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report -- Select specific columns for time aggregation +- Select specific columns for time aggregation via in-row checkboxes ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/js/filter.js b/js/filter.js index 75f4c35d..988193b4 100644 --- a/js/filter.js +++ b/js/filter.js @@ -411,7 +411,11 @@ OCA.Analytics.Filter = { const header = OCA.Analytics.currentReportData.header || []; const selectedCols = filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]; header.forEach((name, index) => { - const div = document.createElement('div'); + const label = document.createElement('label'); + label.style.whiteSpace = 'nowrap'; + label.style.marginRight = '10px'; + label.style.display = 'inline-block'; + const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'timeGroupingColumn' + index; @@ -420,12 +424,10 @@ OCA.Analytics.Filter = { if (selectedCols.includes(index)) { checkbox.checked = true; } - const label = document.createElement('label'); - label.setAttribute('for', checkbox.id); - label.textContent = name; - div.appendChild(checkbox); - div.appendChild(label); - columnsContainer.appendChild(div); + + label.appendChild(checkbox); + label.appendChild(document.createTextNode(name)); + columnsContainer.appendChild(label); }); if (filterOptions && filterOptions.timeAggregation) { diff --git a/templates/part.templates.php b/templates/part.templates.php index 0938f125..f1910437 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -169,6 +169,9 @@ class="sidebarPointer">t('Visualization')); ?>
+
+ +
@@ -180,13 +183,7 @@ class="sidebarPointer">t('Visualization')); ?>
-
-
-
- -
-
From a686de93eae8c6a1f274a6623ba5eaf5277fd789 Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 25 Aug 2025 22:35:38 +0700 Subject: [PATCH 3/6] Use multiselect for time aggregation columns --- CHANGELOG.md | 2 +- js/filter.js | 49 +++++++++++++----------------------- templates/part.templates.php | 18 +++++++------ 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3794352e..b600fbd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report -- Select specific columns for time aggregation via in-row checkboxes +- Select specific columns for time aggregation via multi-select list ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/js/filter.js b/js/filter.js index 988193b4..c979abf9 100644 --- a/js/filter.js +++ b/js/filter.js @@ -407,27 +407,17 @@ OCA.Analytics.Filter = { const filterOptions = OCA.Analytics.currentReportData.options.filteroptions; - const columnsContainer = container.getElementById('timeGroupingColumns'); + const columnsSelect = container.getElementById('timeGroupingColumns'); const header = OCA.Analytics.currentReportData.header || []; const selectedCols = filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]; header.forEach((name, index) => { - const label = document.createElement('label'); - label.style.whiteSpace = 'nowrap'; - label.style.marginRight = '10px'; - label.style.display = 'inline-block'; - - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = 'timeGroupingColumn' + index; - checkbox.name = 'timeGroupingColumn'; - checkbox.value = index; + const option = document.createElement('option'); + option.value = index; + option.textContent = name; if (selectedCols.includes(index)) { - checkbox.checked = true; + option.selected = true; } - - label.appendChild(checkbox); - label.appendChild(document.createTextNode(name)); - columnsContainer.appendChild(label); + columnsSelect.appendChild(option); }); if (filterOptions && filterOptions.timeAggregation) { @@ -436,19 +426,19 @@ OCA.Analytics.Filter = { modeSelect.value = filterOptions.timeAggregation.mode; } - const updateColumnCheckboxes = () => { + const updateColumnOptions = () => { const dimIdx = parseInt(dimSelect.value.match(/\d+$/)?.[0], 10) - 1; - columnsContainer.querySelectorAll('input[name="timeGroupingColumn"]').forEach(cb => { - if (parseInt(cb.value, 10) === dimIdx) { - cb.checked = false; - cb.disabled = true; + Array.from(columnsSelect.options).forEach(opt => { + if (parseInt(opt.value, 10) === dimIdx) { + opt.selected = false; + opt.disabled = true; } else { - cb.disabled = false; + opt.disabled = false; } }); }; - updateColumnCheckboxes(); - dimSelect.addEventListener('change', updateColumnCheckboxes); + updateColumnOptions(); + dimSelect.addEventListener('change', updateColumnOptions); OCA.Analytics.Notification.htmlDialogUpdate( container, @@ -472,13 +462,10 @@ OCA.Analytics.Filter = { filterOptions.timeAggregation.grouping = grouping; filterOptions.timeAggregation.mode = document.getElementById('timeGroupingMode').value; - const selected = []; - const colBoxes = document.getElementsByName('timeGroupingColumn'); - for (let i = 0; i < colBoxes.length; i++) { - if (colBoxes[i].checked) { - selected.push(parseInt(colBoxes[i].value, 10)); - } - } + const columnsSelect = document.getElementById('timeGroupingColumns'); + const selected = Array.from(columnsSelect.options) + .filter(opt => opt.selected) + .map(opt => parseInt(opt.value, 10)); filterOptions.timeAggregation.columns = selected; if (grouping === 'none') { diff --git a/templates/part.templates.php b/templates/part.templates.php index f1910437..54f14959 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -160,30 +160,32 @@ class="sidebarPointer">t('Visualization')); ?> From 18eb27d7d4f83a07146ba4ad8a13d0e0019d6399 Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 26 Aug 2025 20:44:18 +0700 Subject: [PATCH 4/6] Document checkbox column selection --- CHANGELOG.md | 2 +- js/filter.js | 35 ++++++++++++++++++++--------------- templates/part.templates.php | 24 ++++++++---------------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b600fbd6..14ddd795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report -- Select specific columns for time aggregation via multi-select list +- Select specific columns for time aggregation via checkboxes ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/js/filter.js b/js/filter.js index c979abf9..60cacef9 100644 --- a/js/filter.js +++ b/js/filter.js @@ -407,17 +407,23 @@ OCA.Analytics.Filter = { const filterOptions = OCA.Analytics.currentReportData.options.filteroptions; - const columnsSelect = container.getElementById('timeGroupingColumns'); + const columnsContainer = container.getElementById('timeGroupingColumns'); const header = OCA.Analytics.currentReportData.header || []; const selectedCols = filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]; header.forEach((name, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = name; + const label = document.createElement('label'); + label.style.whiteSpace = 'nowrap'; + label.style.marginRight = '10px'; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'timeGroupingColumn'; + checkbox.value = index; if (selectedCols.includes(index)) { - option.selected = true; + checkbox.checked = true; } - columnsSelect.appendChild(option); + label.appendChild(checkbox); + label.appendChild(document.createTextNode(' ' + name)); + columnsContainer.appendChild(label); }); if (filterOptions && filterOptions.timeAggregation) { @@ -428,12 +434,12 @@ OCA.Analytics.Filter = { const updateColumnOptions = () => { const dimIdx = parseInt(dimSelect.value.match(/\d+$/)?.[0], 10) - 1; - Array.from(columnsSelect.options).forEach(opt => { - if (parseInt(opt.value, 10) === dimIdx) { - opt.selected = false; - opt.disabled = true; + columnsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { + if (parseInt(cb.value, 10) === dimIdx) { + cb.checked = false; + cb.disabled = true; } else { - opt.disabled = false; + cb.disabled = false; } }); }; @@ -462,10 +468,9 @@ OCA.Analytics.Filter = { filterOptions.timeAggregation.grouping = grouping; filterOptions.timeAggregation.mode = document.getElementById('timeGroupingMode').value; - const columnsSelect = document.getElementById('timeGroupingColumns'); - const selected = Array.from(columnsSelect.options) - .filter(opt => opt.selected) - .map(opt => parseInt(opt.value, 10)); + const selected = Array.from(document.getElementsByName('timeGroupingColumn')) + .filter(cb => cb.checked) + .map(cb => parseInt(cb.value, 10)); filterOptions.timeAggregation.columns = selected; if (grouping === 'none') { diff --git a/templates/part.templates.php b/templates/part.templates.php index 54f14959..193a7737 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -161,30 +161,22 @@ class="sidebarPointer">t('Visualization')); ?>
- -
-
- -
-
- -
-
- -
-
-
-
+
+
+
-
- +
+
+
+ t('Aggregated Columns')); ?> +
From 63b31ad8f186f4ed36095a3dca87d8e105a8db66 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 27 Aug 2025 08:29:47 +0700 Subject: [PATCH 5/6] Hide hidden columns in time aggregation --- CHANGELOG.md | 1 + js/filter.js | 13 ++++++++----- js/visualization.js | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ddd795..40a85c73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) - date formatting on all rows +- Exclude hidden columns from time aggregation options ## 5.8.0 - 2025-07-29 ### Added diff --git a/js/filter.js b/js/filter.js index 60cacef9..645f0666 100644 --- a/js/filter.js +++ b/js/filter.js @@ -388,10 +388,14 @@ OCA.Analytics.Filter = { const container = document.importNode(document.getElementById('templateTimeAggregationOptions').content, true); + const filterOptions = OCA.Analytics.currentReportData.options.filteroptions || {}; + const dimensions = OCA.Analytics.currentReportData.dimensions; + const drilldown = filterOptions.drilldown || {}; + const visibleDims = Object.keys(dimensions).filter(key => !drilldown[key]); const dimSelect = container.getElementById('timeGroupingDimension'); dimSelect.innerHTML = ''; - Object.keys(dimensions).forEach(key => { + visibleDims.forEach(key => { dimSelect.options.add(new Option(dimensions[key], key)); }); @@ -405,11 +409,10 @@ OCA.Analytics.Filter = { modeSelect.options.add(new Option(text, value)); }); - const filterOptions = OCA.Analytics.currentReportData.options.filteroptions; - const columnsContainer = container.getElementById('timeGroupingColumns'); const header = OCA.Analytics.currentReportData.header || []; - const selectedCols = filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]; + const selectedCols = (filterOptions?.timeAggregation?.columns?.map(Number) || [header.length - 1]) + .filter(i => i < header.length); header.forEach((name, index) => { const label = document.createElement('label'); label.style.whiteSpace = 'nowrap'; @@ -433,7 +436,7 @@ OCA.Analytics.Filter = { } const updateColumnOptions = () => { - const dimIdx = parseInt(dimSelect.value.match(/\d+$/)?.[0], 10) - 1; + const dimIdx = visibleDims.indexOf(dimSelect.value); columnsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { if (parseInt(cb.value, 10) === dimIdx) { cb.checked = false; diff --git a/js/visualization.js b/js/visualization.js index e9bea03a..1b1f2e77 100644 --- a/js/visualization.js +++ b/js/visualization.js @@ -1113,22 +1113,24 @@ OCA.Analytics.Visualization = { return data; } - const dimension = parseInt(tg.dimension.match(/\d+$/)?.[0], 10) - 1; - if (isNaN(dimension)) { + const drilldown = data.options.filteroptions?.drilldown || {}; + const dimensions = data.dimensions || {}; + const visibleDims = Object.keys(dimensions).filter(key => !drilldown[key]); + const dimension = visibleDims.indexOf(tg.dimension); + if (dimension === -1) { return data; } - const grouping = tg.grouping; - const mode = tg.mode || 'summation'; - const valueIndices = (tg.columns && tg.columns.length) - ? tg.columns.map(c => parseInt(c, 10)) - : [data.data[0].length - 1]; - if (data.data.length === 0) { return data; } const rowLength = data.data[0].length; + const grouping = tg.grouping; + const mode = tg.mode || 'summation'; + const valueIndices = (tg.columns && tg.columns.length) + ? tg.columns.map(c => parseInt(c, 10)).filter(i => i < rowLength) + : [rowLength - 1]; const valueSet = new Set(valueIndices); const keyIndices = []; for (let i = 0; i < rowLength; i++) { From 1708a9b283f1ae8303a2b651e08b3beb4328ec5d Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 27 Aug 2025 08:49:45 +0700 Subject: [PATCH 6/6] Fix visible dimension detection for time aggregation --- CHANGELOG.md | 1 + js/filter.js | 10 ++++++---- js/visualization.js | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a85c73..82cdd326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) - date formatting on all rows - Exclude hidden columns from time aggregation options +- Correct visible dimension mapping for time aggregation ## 5.8.0 - 2025-07-29 ### Added diff --git a/js/filter.js b/js/filter.js index 645f0666..7a48502a 100644 --- a/js/filter.js +++ b/js/filter.js @@ -392,11 +392,13 @@ OCA.Analytics.Filter = { const dimensions = OCA.Analytics.currentReportData.dimensions; const drilldown = filterOptions.drilldown || {}; - const visibleDims = Object.keys(dimensions).filter(key => !drilldown[key]); + const visibleDims = Object.keys(dimensions) + .map(Number) + .filter(idx => drilldown[idx] === undefined); const dimSelect = container.getElementById('timeGroupingDimension'); dimSelect.innerHTML = ''; - visibleDims.forEach(key => { - dimSelect.options.add(new Option(dimensions[key], key)); + visibleDims.forEach(idx => { + dimSelect.options.add(new Option(dimensions[idx], idx)); }); const groupingSelect = container.getElementById('timeGroupingGrouping'); @@ -436,7 +438,7 @@ OCA.Analytics.Filter = { } const updateColumnOptions = () => { - const dimIdx = visibleDims.indexOf(dimSelect.value); + const dimIdx = visibleDims.indexOf(parseInt(dimSelect.value, 10)); columnsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { if (parseInt(cb.value, 10) === dimIdx) { cb.checked = false; diff --git a/js/visualization.js b/js/visualization.js index 1b1f2e77..1bbb8c2a 100644 --- a/js/visualization.js +++ b/js/visualization.js @@ -1115,8 +1115,10 @@ OCA.Analytics.Visualization = { const drilldown = data.options.filteroptions?.drilldown || {}; const dimensions = data.dimensions || {}; - const visibleDims = Object.keys(dimensions).filter(key => !drilldown[key]); - const dimension = visibleDims.indexOf(tg.dimension); + const visibleDims = Object.keys(dimensions) + .map(Number) + .filter(idx => drilldown[idx] === undefined); + const dimension = visibleDims.indexOf(parseInt(tg.dimension, 10)); if (dimension === -1) { return data; }