Розробка теплових карт даних (Heatmap) для сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка теплових карт даних (Heatmap) для сайту
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

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

Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка теплових карт даних (Heatmap)

Теплова карта—один з небагатьох типів візуалізацій, який працює одразу для кількох завдань: активність користувачів у часі, матриця кореляцій, географічне розподілення, частота подій. Спільна ідея одна—колір кодує числове значення, й паттерни видні миттєво там, де таблиця з цифрами потребувала б хвилини.

Типи завдань

Часова активність—рядки це дні тижня, стовпці—години. Класика для показу коли відбуваються события (замовлення, візити, інциденти). GitHub contribution graph—саме такого типу карта.

Матриця кореляцій—N×N ячеї, значення від -1 до 1. Використовується у фінансах та ML для аналізу залежностей між змінними.

Географічна heatmap—накладення щільності точок на карту. Окрема тема, зазвичай вирішується через Leaflet + leaflet.heat або Mapbox.

Cohort retention—рядки це когорти (місяць реєстрації), стовпці—періоди утримання. Один з ключових інструментів product analytics.

Реалізація через D3

import { useEffect, useRef } from 'react';
import * as d3 from 'd3';

interface HeatmapCell {
  row: string;
  col: string;
  value: number;
}

interface HeatmapProps {
  data: HeatmapCell[];
  rows: string[];
  cols: string[];
  colorScheme?: 'blues' | 'reds' | 'greens' | 'rdylgn';
  width?: number;
  height?: number;
}

export function Heatmap({ data, rows, cols, colorScheme = 'blues', width = 700, height = 400 }: HeatmapProps) {
  const svgRef = useRef<SVGSVGElement>(null);
  const margin = { top: 30, right: 20, bottom: 60, left: 80 };
  const iw = width - margin.left - margin.right;
  const ih = height - margin.top - margin.bottom;

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

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

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

    const xScale = d3.scaleBand().domain(cols).range([0, iw]).padding(0.05);
    const yScale = d3.scaleBand().domain(rows).range([0, ih]).padding(0.05);

    const colorInterpolators = {
      blues: d3.interpolateBlues,
      reds: d3.interpolateReds,
      greens: d3.interpolateGreens,
      rdylgn: d3.interpolateRdYlGn,
    };

    const extent = d3.extent(data, d => d.value) as [number, number];
    const colorScale = d3.scaleSequential()
      .domain(extent)
      .interpolator(colorInterpolators[colorScheme]);

    // Axes
    g.append('g')
      .attr('transform', `translate(0,${ih})`)
      .call(d3.axisBottom(xScale).tickSize(0))
      .select('.domain').remove();

    g.append('g')
      .call(d3.axisLeft(yScale).tickSize(0))
      .select('.domain').remove();

    // 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', '6px 10px')
      .style('border-radius', '4px')
      .style('font-size', '12px')
      .style('pointer-events', 'none');

    // Cells
    g.selectAll('.cell')
      .data(data)
      .join('rect')
      .attr('class', 'cell')
      .attr('x', d => xScale(d.col)!)
      .attr('y', d => yScale(d.row)!)
      .attr('width', xScale.bandwidth())
      .attr('height', yScale.bandwidth())
      .attr('fill', d => d.value == null ? '#f0f0f0' : colorScale(d.value))
      .attr('rx', 2)
      .on('mouseover', (event, d) => {
        tooltip
          .style('display', 'block')
          .style('left', `${event.pageX + 12}px`)
          .style('top', `${event.pageY - 28}px`)
          .html(`<strong>${d.row} / ${d.col}</strong><br/>${d3.format(',.2f')(d.value)}`);
      })
      .on('mouseout', () => tooltip.style('display', 'none'));

    return () => { tooltip.remove(); };
  }, [data, rows, cols, colorScheme]);

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

Легенда з градієнтом

function addColorLegend(
  svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
  colorScale: d3.ScaleSequential<string>,
  x: number,
  y: number,
  width = 200,
  height = 12
) {
  const defs = svg.append('defs');
  const gradientId = `legend-gradient-${Math.random().toString(36).slice(2)}`;

  const gradient = defs.append('linearGradient').attr('id', gradientId);
  gradient.append('stop').attr('offset', '0%').attr('stop-color', colorScale(colorScale.domain()[0]));
  gradient.append('stop').attr('offset', '100%').attr('stop-color', colorScale(colorScale.domain()[1]));

  const legendG = svg.append('g').attr('transform', `translate(${x},${y})`);

  legendG.append('rect')
    .attr('width', width)
    .attr('height', height)
    .style('fill', `url(#${gradientId})`);

  const legendScale = d3.scaleLinear()
    .domain(colorScale.domain())
    .range([0, width]);

  legendG.append('g')
    .attr('transform', `translate(0,${height})`)
    .call(d3.axisBottom(legendScale).ticks(4).tickFormat(d3.format(',.0f')));
}

Cohort retention heatmap

Приватний випадок з особливою логікою—значення по діагоналі від 0% до 100%:

interface CohortRow {
  cohort: string;    // "Jan 2024"
  periods: (number | null)[];  // retention % за періодами
}

function prepareCohortData(cohorts: CohortRow[]): HeatmapCell[] {
  return cohorts.flatMap((row, rowIdx) =>
    row.periods.map((value, colIdx) => ({
      row: row.cohort,
      col: `Period ${colIdx}`,
      value: value ?? 0,
      // Ячейки за межами життя когорти—null
      isEmpty: value === null,
    }))
  ).filter(d => !d.isEmpty);
}

Для retention краще використовувати d3.interpolateRdYlGn colorScale—червоний для низьких значень, зелений для високих. Фіксуйте домен [0, 100], а не від min до max, інакше візуалізація вводить в оману.

Продуктивність

SVG-heatmap із 10 000+ ячеї (наприклад, 365 днів × 24 години × кілька метрик) працює повільно. Canvas вирішує:

useEffect(() => {
  const canvas = canvasRef.current!;
  const ctx = canvas.getContext('2d')!;
  const dpr = window.devicePixelRatio;

  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  ctx.scale(dpr, dpr);

  const cellW = iw / cols.length;
  const cellH = ih / rows.length;

  data.forEach(d => {
    const x = margin.left + cols.indexOf(d.col) * cellW;
    const y = margin.top + rows.indexOf(d.row) * cellH;
    ctx.fillStyle = colorScale(d.value);
    ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2);
  });
}, [data]);

Tooltip на Canvas-реалізації вимагає ручного hit-testing: у обробнику mousemove вичислюйте col та row із координат миші.

Інтеграція з бекендом

Для великих часових діапазонів дані агрегуються на сервері:

-- Активність по дням тижня та годинам
SELECT
  EXTRACT(DOW FROM created_at)::int AS dow,
  EXTRACT(HOUR FROM created_at)::int AS hour,
  COUNT(*) AS events
FROM user_events
WHERE created_at > NOW() - INTERVAL '90 days'
  AND user_id = $1
GROUP BY 1, 2
ORDER BY 1, 2;
// API endpoint
app.get('/api/heatmap/activity', async (req, res) => {
  const rows = await db.query(`...`);
  // Заповніть порожні ячейки нулями
  const grid: number[][] = Array.from({ length: 7 }, () => new Array(24).fill(0));
  rows.forEach(r => { grid[r.dow][r.hour] = r.events; });
  res.json({ grid });
});

Часова шкала

Базова heatmap із tooltip та легендою—1–2 дні. Cohort retention з правильною підготовкою даних та drill-down—3–5 днів. Географічна heatmap на карті—окрема оцінка.