Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 47 additions & 3 deletions js/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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')
Expand All @@ -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;
}
Expand Down
67 changes: 48 additions & 19 deletions js/visualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1167,6 +1182,7 @@ OCA.Analytics.Visualization = {

const sums = {};
const counts = {};
const keyParts = {};

data.data.forEach(row => {
const original = row[dimension];
Expand All @@ -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;
Expand Down
26 changes: 12 additions & 14 deletions templates/part.templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,27 +160,25 @@ class="sidebarPointer"><?php p($l->t('Visualization')); ?></h3>
<template id="templateTimeAggregationOptions">
<div class="table" style="display: table;" id="timeGroupingOptionsTable">
<div style="display: table-row;">
<div style="display: table-cell; width: 150px;">
<label for="timeGroupingDimension"><?php p($l->t('Time dimension')); ?></label>
</div>
<div style="display: table-cell; width: 150px;">
<label for="timeGroupingGrouping"><?php p($l->t('Grouping')); ?></label>
</div>
<div style="display: table-cell; width: 150px;">
<label for="timeGroupingMode"><?php p($l->t('Mode')); ?></label>
</div>
</div>
<div style="display: table-row;">
<div style="display: table-cell; width: 150px;">
<div style="display: table-cell; width: 150px; vertical-align: middle;">
<label for="timeGroupingDimension"><?php p($l->t('Time dimension')); ?></label><br>
<select id="timeGroupingDimension" class="optionsInput"></select>
</div>
<div style="display: table-cell; width: 150px;">
<div style="display: table-cell; width: 150px; vertical-align: middle;">
<label for="timeGroupingGrouping"><?php p($l->t('Grouping')); ?></label><br>
<select id="timeGroupingGrouping" class="optionsInput"></select>
</div>
<div style="display: table-cell; width: 150px;">
<div style="display: table-cell; width: 150px; vertical-align: middle;">
<label for="timeGroupingMode"><?php p($l->t('Mode')); ?></label><br>
<select id="timeGroupingMode" class="optionsInput"></select>
</div>
</div>
<div style="display: table-row;">
<div style="display: table-cell; vertical-align: top;">
<span><?php p($l->t('Aggregated Columns')); ?></span>
<div id="timeGroupingColumns" style="display: inline-flex; flex-wrap: wrap; gap: 10px; margin-left: 10px;"></div>
</div>
</div>
</div>
</template>

Expand Down