diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf1c069..82cdd326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,13 @@ ### 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 checkboxes ### 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 +- 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 10c18dea..7a48502a 100644 --- a/js/filter.js +++ b/js/filter.js @@ -388,11 +388,17 @@ 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) + .map(Number) + .filter(idx => drilldown[idx] === undefined); const dimSelect = container.getElementById('timeGroupingDimension'); dimSelect.innerHTML = ''; - Object.keys(dimensions).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'); @@ -405,13 +411,46 @@ 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]) + .filter(i => i < header.length); + header.forEach((name, index) => { + 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)) { + checkbox.checked = true; + } + label.appendChild(checkbox); + label.appendChild(document.createTextNode(' ' + name)); + columnsContainer.appendChild(label); + }); + if (filterOptions && filterOptions.timeAggregation) { dimSelect.value = filterOptions.timeAggregation.dimension; groupingSelect.value = filterOptions.timeAggregation.grouping; modeSelect.value = filterOptions.timeAggregation.mode; } + const updateColumnOptions = () => { + const dimIdx = visibleDims.indexOf(parseInt(dimSelect.value, 10)); + columnsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { + if (parseInt(cb.value, 10) === dimIdx) { + cb.checked = false; + cb.disabled = true; + } else { + cb.disabled = false; + } + }); + }; + updateColumnOptions(); + dimSelect.addEventListener('change', updateColumnOptions); + OCA.Analytics.Notification.htmlDialogUpdate( container, t('analytics', 'Aggregate daily data into weeks, months, or years') @@ -434,6 +473,11 @@ OCA.Analytics.Filter = { filterOptions.timeAggregation.grouping = grouping; filterOptions.timeAggregation.mode = document.getElementById('timeGroupingMode').value; + const selected = Array.from(document.getElementsByName('timeGroupingColumn')) + .filter(cb => cb.checked) + .map(cb => parseInt(cb.value, 10)); + filterOptions.timeAggregation.columns = selected; + if (grouping === 'none') { delete filterOptions.timeAggregation; } diff --git a/js/visualization.js b/js/visualization.js index 74dd966d..1bbb8c2a 100644 --- a/js/visualization.js +++ b/js/visualization.js @@ -1113,19 +1113,34 @@ 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) + .map(Number) + .filter(idx => drilldown[idx] === undefined); + const dimension = visibleDims.indexOf(parseInt(tg.dimension, 10)); + if (dimension === -1) { return data; } - const grouping = tg.grouping; - const mode = tg.mode || 'summation'; - const valueIndex = 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++) { + 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 +1182,7 @@ OCA.Analytics.Visualization = { const sums = {}; const counts = {}; + const keyParts = {}; data.data.forEach(row => { const original = row[dimension]; @@ -1183,25 +1199,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..193a7737 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -160,27 +160,25 @@ class="sidebarPointer">t('Visualization')); ?>