Розробка зведених таблиць (Pivot Table) для аналітики
Зведена таблиця—це UI для групування, агрегації та порівняння даних у реальному часі. Користувачі перетягують поля між осями, вибирають метрики та отримують потрібні зрізи без звертання до розробника. Excel та Google Sheets популярні саме завдяки цій можливості—вбудовування аналогічного інструменту в веб-застосунок усуває потребу експортувати дані.
Архітектура
Два принципово різних підходи:
Клієнтська агрегація—всі дані завантажені в браузер, pivot розраховується у JavaScript. Працює до ~100–200k рядків. Швидкий відклик при маніпуляціях з осями, не потребує round-trip до сервера.
Серверна агрегація—кожна зміна конфігурації відправляє запит на сервер, база даних розраховує агрегати. Обов'язково для великих обсягів. ClickHouse або PostgreSQL з правильними індексами повертають агрегати з мільйонів рядків за сотні мілісекунд.
Готові бібліотеки
Перед написанням з нуля, оцініть:
- react-pivottable—відкритий код, drag-and-drop, кілька рендерерів (таблиця, bar chart, heatmap). Обмежена клієнтською обробкою
- AG Grid (з row grouping)—enterprise-рівень, серверний режим, величезна екосистема. Платна для передових функцій
- Flexmonster—спеціалізований pivot, підключається до OLAP-кубів, комерційна ліцензія
Клієнтська реалізація
Ядро pivot—функція агрегації:
type AggregateFunction = 'sum' | 'count' | 'avg' | 'min' | 'max';
interface PivotConfig {
rows: string[]; // поля для рядків
cols: string[]; // поля для стовпців
values: string[]; // числові поля
aggFn: AggregateFunction;
filters: Record<string, string[]>; // поле -> дозволені значення
}
interface PivotResult {
rowKeys: string[][];
colKeys: string[][];
data: Map<string, Map<string, number>>;
}
function computePivot(rawData: Record<string, any>[], config: PivotConfig): PivotResult {
const { rows, cols, values, aggFn, filters } = config;
// Застосуйте фільтри
const filtered = rawData.filter(row =>
Object.entries(filters).every(([field, allowed]) =>
!allowed.length || allowed.includes(String(row[field]))
)
);
// Зберіть унікальні ключі рядків та стовпців
const rowKeySet = new Set<string>();
const colKeySet = new Set<string>();
const accumulator = new Map<string, Map<string, number[]>>();
filtered.forEach(row => {
const rowKey = rows.map(r => String(row[r] ?? '(порожньо)')).join('||');
const colKey = cols.map(c => String(row[c] ?? '(порожньо)')).join('||');
rowKeySet.add(rowKey);
colKeySet.add(colKey);
const numVal = values.reduce((sum, v) => sum + (Number(row[v]) || 0), 0);
if (!accumulator.has(rowKey)) accumulator.set(rowKey, new Map());
const colMap = accumulator.get(rowKey)!;
if (!colMap.has(colKey)) colMap.set(colKey, []);
colMap.get(colKey)!.push(numVal);
});
// Агрегуйте
const aggregated = new Map<string, Map<string, number>>();
accumulator.forEach((colMap, rowKey) => {
const row = new Map<string, number>();
colMap.forEach((vals, colKey) => {
let result: number;
switch (aggFn) {
case 'sum': result = vals.reduce((a, b) => a + b, 0); break;
case 'count': result = vals.length; break;
case 'avg': result = vals.reduce((a, b) => a + b, 0) / vals.length; break;
case 'min': result = Math.min(...vals); break;
case 'max': result = Math.max(...vals); break;
}
row.set(colKey, result);
});
aggregated.set(rowKey, row);
});
return {
rowKeys: Array.from(rowKeySet).sort().map(k => k.split('||')),
colKeys: Array.from(colKeySet).sort().map(k => k.split('||')),
data: aggregated,
};
}
Компонент таблиці
Користувальницький UI pivot рендерить ефективно з віртуалізованими рядками:
Ключові функції:
- Drag-and-drop конфігурація осі
- Агрегація в реальному часі
- Проміжні підсумки та загальний підсумок
- Експорт у CSV/Excel
- Умовне форматування для значень
Серверна реалізація
Для мільйонів рядків використовуйте вікнові функції:
SELECT
date_trunc('month', created_at)::date AS month,
category,
SUM(amount) AS total,
COUNT(*) AS cnt,
AVG(amount) AS avg_amount
FROM orders
WHERE created_at > NOW() - INTERVAL '12 months'
GROUP BY 1, 2
ORDER BY 1, 2;
Індексування на (category, created_at) або композитні індекси різко прискорюють агрегацію.
Часова шкала
Базова клієнтська pivot з drag-drop UI—3–5 днів. Серверна реалізація з фільтруванням, drill-down та експортом—7–10 днів.







