Разработка Sankey-диаграмм для визуализации потоков на сайте

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка Sankey-диаграмм для визуализации потоков на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка Sankey-диаграмм для визуализации потоков на сайте

Sankey-диаграмма показывает потоки между узлами: ширина полосы пропорциональна объёму потока. Это один из лучших инструментов для задач типа «откуда пришли деньги и куда ушли», «как пользователи переходят между этапами воронки», «какие страницы источники трафика для каких конверсий».

Без специального инструмента построить Sankey вручную трудно — нужен layout-алгоритм, который правильно расставляет узлы и рисует кривые Безье. Библиотека d3-sankey берёт на себя эту часть.

Установка

npm install d3-sankey d3
npm install --save-dev @types/d3-sankey

Структура данных

interface SankeyNode {
  id: string;
  label: string;
  color?: string;
}

interface SankeyLink {
  source: string;  // id узла-источника
  target: string;  // id узла-цели
  value: number;   // объём потока
}

interface SankeyData {
  nodes: SankeyNode[];
  links: SankeyLink[];
}

// Пример: воронка e-commerce
const data: SankeyData = {
  nodes: [
    { id: 'organic', label: 'Органика' },
    { id: 'paid', label: 'Платная реклама' },
    { id: 'direct', label: 'Прямые' },
    { id: 'catalog', label: 'Каталог' },
    { id: 'product', label: 'Карточка товара' },
    { id: 'cart', label: 'Корзина' },
    { id: 'checkout', label: 'Оформление' },
    { id: 'purchase', label: 'Покупка' },
    { id: 'exit', label: 'Выход' },
  ],
  links: [
    { source: 'organic', target: 'catalog', value: 4200 },
    { source: 'organic', target: 'product', value: 1800 },
    { source: 'paid', target: 'catalog', value: 2100 },
    { source: 'paid', target: 'product', value: 3400 },
    { source: 'direct', target: 'catalog', value: 900 },
    { source: 'catalog', target: 'product', value: 5600 },
    { source: 'catalog', target: 'exit', value: 3100 },
    { source: 'product', target: 'cart', value: 2900 },
    { source: 'product', target: 'exit', value: 4800 },
    { source: 'cart', target: 'checkout', value: 1600 },
    { source: 'cart', target: 'exit', value: 1300 },
    { source: 'checkout', target: 'purchase', value: 1100 },
    { source: 'checkout', target: 'exit', value: 500 },
  ],
};

Компонент

import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
import { sankey, sankeyLinkHorizontal, sankeyLeft } from 'd3-sankey';

export function SankeyDiagram({ data, width = 800, height = 500 }: { data: SankeyData; width?: number; height?: number }) {
  const svgRef = useRef<SVGSVGElement>(null);
  const margin = { top: 20, right: 20, bottom: 20, left: 20 };

  useEffect(() => {
    if (!svgRef.current) return;

    const svg = d3.select(svgRef.current);
    svg.selectAll('*').remove();

    const iw = width - margin.left - margin.right;
    const ih = height - margin.top - margin.bottom;

    // Подготовка данных для d3-sankey
    const nodeMap = new Map(data.nodes.map((n, i) => [n.id, { ...n, index: i }]));

    const sankeyData = {
      nodes: data.nodes.map(n => ({ ...n })),
      links: data.links.map(l => ({
        source: data.nodes.findIndex(n => n.id === l.source),
        target: data.nodes.findIndex(n => n.id === l.target),
        value: l.value,
      })),
    };

    const sankeyLayout = sankey()
      .nodeWidth(20)
      .nodePadding(12)
      .nodeAlign(sankeyLeft)
      .extent([[0, 0], [iw, ih]]);

    const { nodes, links } = sankeyLayout(sankeyData as any);

    const colorScale = d3.scaleOrdinal(d3.schemeTableau10);

    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);

    // Tooltip
    const tooltip = d3.select('body').append('div')
      .style('position', 'absolute')
      .style('display', 'none')
      .style('background', 'rgba(0,0,0,0.8)')
      .style('color', '#fff')
      .style('padding', '8px 12px')
      .style('border-radius', '4px')
      .style('font-size', '13px')
      .style('pointer-events', 'none');

    // Links
    g.append('g')
      .selectAll('.link')
      .data(links)
      .join('path')
      .attr('class', 'link')
      .attr('d', sankeyLinkHorizontal())
      .attr('fill', 'none')
      .attr('stroke', (d: any) => colorScale(String(d.source.index)))
      .attr('stroke-width', (d: any) => Math.max(1, d.width))
      .attr('stroke-opacity', 0.4)
      .on('mouseover', (event, d: any) => {
        d3.select(event.currentTarget).attr('stroke-opacity', 0.7);
        tooltip
          .style('display', 'block')
          .style('left', `${event.pageX + 12}px`)
          .style('top', `${event.pageY - 28}px`)
          .html(`<strong>${d.source.label} → ${d.target.label}</strong><br/>${d3.format(',.0f')(d.value)} пользователей`);
      })
      .on('mouseout', (event) => {
        d3.select(event.currentTarget).attr('stroke-opacity', 0.4);
        tooltip.style('display', 'none');
      });

    // Nodes
    const nodeG = g.append('g')
      .selectAll('.node')
      .data(nodes)
      .join('g')
      .attr('class', 'node');

    nodeG.append('rect')
      .attr('x', (d: any) => d.x0)
      .attr('y', (d: any) => d.y0)
      .attr('width', (d: any) => d.x1 - d.x0)
      .attr('height', (d: any) => Math.max(1, d.y1 - d.y0))
      .attr('fill', (d: any) => colorScale(String(d.index)))
      .attr('rx', 3)
      .on('mouseover', (event, d: any) => {
        tooltip
          .style('display', 'block')
          .style('left', `${event.pageX + 12}px`)
          .style('top', `${event.pageY - 28}px`)
          .html(`<strong>${d.label}</strong><br/>Объём: ${d3.format(',.0f')(d.value)}`);
      })
      .on('mouseout', () => tooltip.style('display', 'none'));

    // Labels
    nodeG.append('text')
      .attr('x', (d: any) => d.x0 < iw / 2 ? d.x1 + 6 : d.x0 - 6)
      .attr('y', (d: any) => (d.y0 + d.y1) / 2)
      .attr('dy', '0.35em')
      .attr('text-anchor', (d: any) => d.x0 < iw / 2 ? 'start' : 'end')
      .attr('font-size', 12)
      .attr('fill', '#374151')
      .text((d: any) => d.label);

    return () => { tooltip.remove(); };
  }, [data, width, height]);

  return <svg ref={svgRef} width={width} height={height} />;
}

Подготовка данных на сервере

Данные для Sankey обычно агрегируются из событийного потока. Пример для воронки сайта:

-- Последовательные переходы между страницами в рамках сессии
WITH ranked_events AS (
  SELECT
    session_id,
    page_type,
    LAG(page_type) OVER (PARTITION BY session_id ORDER BY created_at) AS prev_page_type,
    ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at) AS step
  FROM page_views
  WHERE created_at > NOW() - INTERVAL '30 days'
)
SELECT
  COALESCE(prev_page_type, 'entry') AS source,
  page_type AS target,
  COUNT(*) AS value
FROM ranked_events
WHERE prev_page_type IS NOT NULL OR step = 1
GROUP BY 1, 2
HAVING COUNT(*) > 50  -- фильтруем редкие переходы
ORDER BY value DESC;

Нюансы раскладки

d3-sankey поддерживает несколько алгоритмов выравнивания узлов:

  • sankeyLeft — узлы выравниваются по левому краю уровня. Подходит для воронок
  • sankeyRight — по правому краю
  • sankeyCenter — по центру графа (для ациклических графов)
  • sankeyJustify (по умолчанию) — листовые узлы прижимаются к правому краю

Для циклических данных (A → B → A) стандартный d3-sankey не работает — нужна предобработка или библиотека d3-sankey-circular.

Сроки

Sankey-диаграмма с tooltip и базовыми взаимодействиями — 2–3 дня. С drill-down (клик по узлу раскрывает детали), фильтрацией по периоду и экспортом — 5–7 дней.