Розробка інтерактивних зведених таблиць для аналітики на сайті

Коли обсяги даних на сайті зростають, стандартні звіти перестають справлятися: менеджери чекають на вивантаження, база навантажується, а аналіз затягується. Ми розробляємо інтерактивні зведені таблиці із серверною агрегацією, які миттєво обробляють навіть мільйони записів. Наша команда реалізує проєкт під ключ — від підбору бібліотек до налаштування серверної логіки, забезпечуючи стабільну роботу та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка інтерактивних зведених таблиць для аналітики на сайті
Складний
~5 днів
Часті запитання

Пропонуємо розробку інтерактивних зведених таблиць під ключ: оцінимо ваш проєкт за 1 день. Наші зведені таблиці (pivot table) забезпечують гнучку аналітику на сайті з агрегацією даних. Використовуємо drag-and-drop таблиці, серверну агрегацію на ClickHouse pivot, експорт Excel та інтерактивні таблиці для бізнес-аналітики. Завдяки серверній агрегації аналітика реального часу стає можливою навіть для 10 млн рядків – це в 5 разів швидше, ніж клієнтська агрегація з React pivot.

Чому готові бібліотеки не підходять для великих даних

Уявіть: інтернет-магазин з 10 млн замовлень. Менеджеру потрібно сформувати звіт по продажах за квартал з групуванням по категоріях та місяцях. Звичайний експорт в Excel займає 30 хвилин і навантажує базу. Зведена таблиця з серверною агрегацією віддає дані за 200 мс і дозволяє змінювати зрізи в реальному часі. На практиці готові бібліотеки не завжди справляються з таким навантаженням: react-pivottable гальмує на 200k+ рядків, AG Grid вимагає enterprise-ліцензії для серверного режиму, а Flexmonster не підтримує ad-hoc запити до реляційних баз. Ми комбінуємо бібліотеки та власну серверну агрегацію, щоб закрити ці прогалини. Економія на ліцензіях робить такий підхід дешевшим, ніж enterprise-ліцензії на кожного розробника.

Wikipedia визначає зведену таблицю як інструмент аналізу даних з агрегацією по вимірах. В комерційних проєктах часто потрібна кастомізація, яку готові рішення не забезпечують.

Як ми будуємо серверну агрегацію для мільйонів рядків?

Для великих даних конфігурація pivot відправляється на сервер. Використовуємо ClickHouse або PostgreSQL з індексами, матеріалізованими представленнями та партиціонуванням. Кожна зміна осей надсилає запит, сервер повертає агрегати за 100-500 мс. Кешуємо результати за ключем конфігурації. Нижче — приклад генерації SQL з конфігу.

// Сервер — генерація SQL з конфігу
function buildPivotQuery(config: PivotConfig, dateRange: [Date, Date]): string {
  const rowsExpr = config.rows.map(r => `"${r}"`).join(', ');
  const colsExpr = config.cols.map(c => `"${c}"`).join(', ');
  const valExpr = config.values[0]; // спрощення
  const aggExpr = {
    sum: `SUM("${valExpr}")`,
    count: `COUNT(*)`,
    avg: `AVG("${valExpr}")::numeric(18,2)`,
    min: `MIN("${valExpr}")`,
    max: `MAX("${valExpr}")`,
  }[config.aggFn];
  return `
    SELECT ${rowsExpr}, ${colsExpr}, ${aggExpr} AS value
    FROM events
    WHERE created_at BETWEEN $1 AND $2
    GROUP BY ${rowsExpr}, ${colsExpr}
    ORDER BY ${rowsExpr}, ${colsExpr}
  `;
}

Покрокова інструкція

  1. Аналіз структури даних. Визначаємо поля для групування (рядки, стовпці) та міри (сума, середнє).
  2. Проектування SQL-запитів. Генеруємо динамічні запити з GROUP BY.
  3. Налаштування ClickHouse/PostgreSQL. Створюємо індекси, налаштовуємо кешування результатів.
  4. Інтеграція з клієнтом. По кожній зміні конфігу надсилаємо запит на сервер.

Порівняння клієнтської та серверної агрегації

Характеристика Клієнтська Серверна
Макс. записів ~200k Без обмежень
Час відгуку Миттєво 100–500 мс
Вартість інфраструктури Низька Вища (сервер + кеш)
Гнучкість Тільки попередньо завантажені дані Будь-які SQL-запити
Масштабованість Обмежена браузером До мільярда рядків

Серверна агрегація в 5 разів швидша для мільйонів рядків, але клієнтська в 5 разів швидша при малих даних. Вибір залежить від обсягу та вимог до інтерактивності.

Як реалізувати клієнтську агрегацію: покрокова інструкція

  1. Фільтрація даних — залишаємо лише записи, що відповідають встановленим фільтрам.
  2. Групування — визначаємо ключі для рядків та стовпців на основі значень вибраних полів.
  3. Агрегація — обчислюємо значення для кожної комірки (сума, кількість, середнє, мінімум, максимум).
  4. Виведення таблиці — рендеримо HTML-таблицю з підсумками та фіксованою шапкою.
Повний код функції агрегації (TypeScript)
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,
  };
}
Повний код компонента PivotTable (React+TypeScript)
function PivotTable({ result, config, format }: { result: PivotResult; config: PivotConfig; format?: (val: number) => string; }) {
  const fmt = format ?? (v => v.toLocaleString('uk-UA'));
  const rowTotals = result.rowKeys.map(rk => {
    const rowKey = rk.join('||');
    let total = 0;
    result.colKeys.forEach(ck => {
      total += result.data.get(rowKey)?.get(ck.join('||')) ?? 0;
    });
    return total;
  });
  const grandTotal = rowTotals.reduce((a, b) => a + b, 0);
  return (
    <div className="overflow-auto max-h-[600px]">
      <table className="text-sm border-collapse w-full">
        <thead className="sticky top-0 bg-white z-10">
          <tr>
            {config.rows.map(r => (
              <th key={r} className="border px-3 py-2 text-left bg-gray-50 font-medium">{r}</th>
            ))}
            {result.colKeys.map(ck => (
              <th key={ck.join('/')} className="border px-3 py-2 text-right bg-gray-50 font-medium whitespace-nowrap">
                {ck.join(' / ')}
              </th>
            ))}
            <th className="border px-3 py-2 text-right bg-blue-50 font-semibold">Разом</th>
          </tr>
        </thead>
        <tbody>
          {result.rowKeys.map((rk, ri) => {
            const rowKey = rk.join('||');
            return (
              <tr key={rowKey} className="hover:bg-gray-50">
                {rk.map((label, i) => (
                  <td key={i} className="border px-3 py-1.5 font-medium">{label}</td>
                ))}
                {result.colKeys.map(ck => {
                  const val = result.data.get(rowKey)?.get(ck.join('||'));
                  return (
                    <td key={ck.join('/')} className="border px-3 py-1.5 text-right tabular-nums">
                      {val != null ? fmt(val) : '—'}
                    </td>
                  );
                })}
                <td className="border px-3 py-1.5 text-right tabular-nums font-medium bg-blue-50">
                  {fmt(rowTotals[ri])}
                </td>
              </tr>
            );
          })}
        </tbody>
        <tfoot>
          <tr className="font-semibold bg-gray-100">
            <td colSpan={config.rows.length} className="border px-3 py-2">Разом</td>
            {result.colKeys.map(ck => {
              const colTotal = result.rowKeys.reduce((sum, rk) => {
                return sum + (result.data.get(rk.join('||'))?.get(ck.join('||')) ?? 0);
              }, 0);
              return (
                <td key={ck.join('/')} className="border px-3 py-2 text-right tabular-nums">{fmt(colTotal)}</td>
              );
            })}
            <td className="border px-3 py-2 text-right tabular-nums bg-blue-100">{fmt(grandTotal)}</td>
          </tr>
        </tfoot>
      </table>
    </div>
  );
}

Експорт в Excel

Використовуємо exceljs для генерації .xlsx на клієнті:

import ExcelJS from 'exceljs';
async function exportToExcel(result: PivotResult, config: PivotConfig) {
  const wb = new ExcelJS.Workbook();
  const ws = wb.addWorksheet('Зведена таблиця');
  const headers = [...config.rows, ...result.colKeys.map(k => k.join(' / ')), 'Разом'];
  ws.addRow(headers).font = { bold: true };
  result.rowKeys.forEach(rk => {
    const rowKey = rk.join('||');
    const row = [...rk];
    result.colKeys.forEach(ck => {
      row.push(String(result.data.get(rowKey)?.get(ck.join('||')) ?? ''));
    });
    ws.addRow(row);
  });
  const buffer = await wb.xlsx.writeBuffer();
  const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'pivot.xlsx';
  a.click();
}

Як вибрати між клієнтською та серверною агрегацією?

Якщо дані до 200k рядків і не вимагають складної фільтрації — клієнтський підхід швидший і дешевший. Для мільйонів рядків та ad-hoc запитів — серверна агрегація обов'язкова. Ми допомагаємо клієнтам підібрати оптимальну архітектуру.

Що входить у роботу

  • Розробка клієнтського UI з drag-and-drop (React, TypeScript, Tailwind)
  • Серверна агрегація на ClickHouse/PostgreSQL з кешуванням
  • Експорт в Excel/CSV, друк
  • Документація коду та архітектури
  • Доступ до репозиторію вихідних кодів
  • Навчання команди роботі з інструментом
  • Технічна підтримка 3 місяці після запуску

Етапи розробки та орієнтовні терміни

Етап Тривалість Опис
Аналіз даних 2-3 дні Вивчення структури даних, вимог до метрик та фільтрації
Проектування схеми агрегації 1-2 дні Визначення полів, мір, індексів для ClickHouse/PostgreSQL
Розробка клієнтського UI 1-2 тижні Реалізація drag-and-drop конфігуратора, таблиці з підсумками та sticky header
Серверна агрегація (якщо потрібна) 1-2 тижні Налаштування ClickHouse, кешування, генерація SQL
Експорт в Excel/CSV 2-3 дні Інтеграція з exceljs, перевірка форматування
Навантажувальне тестування 2-3 дні Перевірка на 10 млн рядків, оптимізація N+1 запитів

Клієнтський pivot до 50k рядків з drag-and-drop конфігуратором та експортом в Excel — 2–3 тижні. Серверний режим з ClickHouse або PostgreSQL, кешуванням результатів та підтримкою кількох значень — додатково 1–2 тижні.

Чому варто обрати нас?

Маємо 7+ років досвіду в розробці аналітичних систем, реалізовано 30+ проєктів аналітики, сертифіковані фахівці з ClickHouse та PostgreSQL. Гарантуємо якість та підтримку після запуску. Зв'яжіться з нами для оцінки проєкту — напишіть на пошту або в телеграм.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1393
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1368
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1139
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060