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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

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

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

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1362
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    958
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    932
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    949

Почему готовые библиотеки не подходят для больших данных

Представьте: интернет-магазин с 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 раз быстрее при малых данных, но серверная масштабируется до миллиарда строк. Выбор зависит от объёма и требований к интерактивности.

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

  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('ru-RU'));

  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 запросов — серверная агрегация обязательна. Мы помогаем клиентам подобрать оптимальную архитектуру.

Этапы разработки и ориентировочные сроки

Этап Длительность Описание
Анализ данных 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 недели.

Состав работ

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

Оценим ваш проект за 1 день. Свяжитесь с нами — обсудим данные, метрики и подберём оптимальное решение. Получите консультацию уже сегодня.

Настройка веб-аналитики: GA4, GTM, Яндекс.Метрика и Amplitude

Мы часто видим: конверсия 1.2 %, трафик растёт, а конверсия стоит. Маркетолог смотрит в Google Analytics и говорит: «пользователи уходят с шага 2 оформления заказа». Разработчик открывает тот же шаг — ошибок нет, в Sentry тишина. Значит, дело не в JS-баге, а в UX или в кривых данных, которые показывает аналитика. Аналитика ломается незаметно: событие перестало трекаться после редеплоя — никто не заметил; GTM-тег стреляет дважды — данные задвоились; фильтр GA4 исключает бота, который на самом деле — реальный трафик с корпоративного прокси. Закажите аудит текущих тегов — мы найдём причину за неделю.

После правильной настройки экономия рекламного бюджета может достигать 150 000 ₽ в месяц — это реальный кейс интернет-магазина с 50 000 сессий в день, где дедупликация purchase вернула 20 % неверно приписанных конверсий.

Почему события GA4 дублируются и как это исправить?

Universal Analytics закрыт, его место заняла событийная модель GA4. В ней нет фиксированных хитов страниц и транзакций — только события с параметрами. Это гибче, но требует правильного дизайна событий.

Автоматические события GA4 собирает сам: page_view, scroll, click, session_start. Рекомендуемые события нужно реализовать самостоятельно: purchase, add_to_cart, begin_checkout, view_item. Google ожидает конкретную схему параметров — если передать product_id вместо item_id, данные попадут в GA4, но не в стандартные отчёты e-commerce. Кастомные события для специфики проекта: filter_applied, video_progress, form_step_completed. Кастомные параметры необходимо зарегистрировать в GA4 Admin → Custom definitions, иначе они не будут доступны в отчётах.

Частая ошибка — событие purchase с дублями. Причина: тег срабатывает на странице /thank-you, пользователь обновляет страницу — второй purchase уходит в GA4. Решение: на бэкенде генерируем уникальный transaction_id и передаём в событие. GA4 de-duplicates по нему (в теории — проверяйте через DebugView). Правильная атрибуция экономит до 20 % рекламного бюджета, который раньше уходил на неверно приписанные конверсии.

Как настроить data layer, чтобы не потерять данные?

GTM — инструмент для управления тегами без деплоя кода. Но «без кода» не значит «без архитектуры». Data Layer — основа всего. Передаём данные из приложения в GTM через dataLayer.push(). Структура: event + контекстные данные. Для e-commerce: перед открытием страницы продукта — push с данными товара. GTM-тег читает из dataLayer, не из DOM.

window.dataLayer = window.dataLayer || [];
dataLayer.push({
  event: 'view_item',
  ecommerce: {
    items: [{
      item_id: 'SKU-12345',
      item_name: 'Название товара',
      price: 1990.00,
      currency: 'RUB'
    }]
  }
});

Плохая практика: GTM-тег парсит DOM — ищет цену в span.price, название в h1. Это ломается при любом изменении верстки. Хорошая практика: всегда dataLayer. Используем Preview Mode для отладки и GTM Server-Side для чувствительных данных — отправка с сервера, не с браузера, обходит блокировщики рекламы, не теряет данные.

Как Яндекс.Метрика дополняет веб-аналитику?

Для российской аудитории Метрика обязательна — особенно Вебвизор. Запись сессии пользователя, который бросил корзину, часто даёт ответ быстрее, чем неделя анализа воронки. Цели в Метрике: событийные (через ym(COUNTER_ID, 'reachGoal', 'GOAL_NAME')) или автоматические (клик по кнопке, посещение страницы). Связка с CRM через Метрика Плюс — передача офлайн-конверсий. Наш опыт: в 8 из 10 проектов после настройки Метрики находили скрытые баги в UX, которые не показывали другие системы.

Что даёт product analytics в Amplitude?

Amplitude — продуктовый инструмент, в отличие от маркетинговых GA4 и Метрики. Он заточен под анализ поведения пользователей внутри продукта: воронки, ретеншн, user paths. Amplitude подходит для SaaS-продуктов, мобильных приложений и любых сервисов с зарегистрированными пользователями, где важно понять, как проходят онбординг, на каком шаге уходят, какие фичи используют чаще. Ключевые концепции: identify (связать анонимного пользователя с userId после авторизации), group (аккаунт в B2B SaaS), когорты для удержания. Amplitude Chart — воронка шагов за последние 30 дней с разбивкой по источнику.

Мониторинг качества данных

Аналитика без мониторинга — чёрный ящик. Настраиваем:

  • GA4 Realtime — проверяем после каждого деплоя, что ключевые события приходят
  • Alerting в GA4 — аномалия в количестве событий purchase (резкое падение = что-то сломалось)
  • GTM Preview в staging-окружении перед продакшеном
  • Ручные тесты воронок раз в неделю — просто пройти путь покупателя и проверить, что всё трекается

Что проверяем после каждого деплоя

  • Все ли рекомендуемые события присутствуют в DebugView
  • Нет ли задвоений (считаем количество purchase на 100 сессий)
  • Не изменилась ли структура dataLayer после обновления фронтенда

Что входит в работу

Компонент Описание
Аудит текущих тегов Проверка существующих GTM-тегов, dataLayer, дублей и ошибок
Дизайн событийной схемы Документация: список событий, параметры, триггеры
Настройка GA4 + GTM Создание конфигурации, тегов, Custom definitions
Яндекс.Метрика Установка счётчика, создание целей, настройка Вебвизора
Amplitude (опционально) Настройка клиентского и серверного SDK, когорты
QA и мониторинг Тестирование в Preview Mode, Alerting
Обучение и передача Доступы, инструкция по добавлению новых событий, консоль

Процесс и сроки

  1. Аудит текущих тегов и данных (2 дня)
  2. Дизайн событийной схемы (2 дня)
  3. Разработка Data Layer и настройка тегов (3–5 дней)
  4. QA в Preview Mode и на staging (2 дня)
  5. Деплой и настройка дашбордов (1 день)
Сценарий Срок
Базовая настройка GA4 + GTM 1 неделя
Полный e-commerce tracking + Метрика 2–3 недели
Server-side GTM + Amplitude 3–5 недель

Стоимость рассчитывается индивидуально. Получите консультацию по настройке веб-аналитики для вашего проекта — мы оценим объём работ за один день. Свяжитесь с нами, чтобы начать.

Wikipedia: Веб-аналитика — подробнее о методах и метриках. Официальная документация по событийной модели GA4 доступна в Google Analytics 4.