Розробка Treemap-діаграм для візуалізації ієрархій на сайті

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

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка Treemap-діаграм для візуалізації ієрархій на сайті
Середня
від 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

Розробка Treemap для візуалізації ієрархій

Treemap робить одну річ добре: показує пропорції частин у ієрархічній структурі. Площа прямокутника пропорційна значенню. Колір—додатковий вимір: зростання/падіння, категорія, статус. Якщо потрібно зрозуміти "що займає більше всього місця" у бюджеті, дереві категорій, файловій системі або портфелі активів—treemap читається швидше за будь-яку таблицю.

D3 Treemap

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

interface TreeNode {
  name: string;
  value?: number;
  children?: TreeNode[];
  change?: number; // % зміни для кольорового кодування
}

interface TreemapProps {
  data: TreeNode;
  width?: number;
  height?: number;
  colorBy?: 'category' | 'change';
}

export function Treemap({ data, width = 800, height = 500, colorBy = 'category' }: TreemapProps) {
  const svgRef = useRef<SVGSVGElement>(null);

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

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

    // Ієрархія
    const root = d3.hierarchy(data)
      .sum(d => d.value ?? 0)
      .sort((a, b) => (b.value ?? 0) - (a.value ?? 0));

    // Layout
    d3.treemap()
      .size([width, height])
      .paddingOuter(3)
      .paddingInner(2)
      .paddingTop(18)
      .round(true)(root);

    const colorScale = colorBy === 'change'
      ? d3.scaleDiverging(d3.interpolateRdYlGn).domain([-30, 0, 30])
      : d3.scaleOrdinal(d3.schemeTableau10);

    const tooltip = d3.select('body').append('div')
      .style('position', 'absolute')
      .style('display', 'none')
      .style('background', 'rgba(15,23,42,0.9)')
      .style('color', '#f1f5f9')
      .style('padding', '8px 12px')
      .style('border-radius', '4px')
      .style('font-size', '13px')
      .style('pointer-events', 'none')
      .style('max-width', '220px');

    // Всі вузли з потомками (для групування)
    const leaves = root.leaves();
    const ancestors = root.descendants().filter(d => d.depth === 1);

    // Фонові прямокутники для груп
    svg.selectAll('.group-rect')
      .data(ancestors)
      .join('rect')
      .attr('class', 'group-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) => d.y1 - d.y0)
      .attr('fill', (d: any) => colorBy === 'change' ? '#e2e8f0' : d3.color(colorScale(d.data.name))!.brighter(0.7).toString())
      .attr('stroke', '#fff')
      .attr('stroke-width', 2);

    // Заголовки груп
    svg.selectAll('.group-label')
      .data(ancestors)
      .join('text')
      .attr('class', 'group-label')
      .attr('x', (d: any) => d.x0 + 6)
      .attr('y', (d: any) => d.y0 + 13)
      .attr('font-size', 11)
      .attr('font-weight', '600')
      .attr('fill', '#374151')
      .text((d: any) => d.data.name);

    // Листя
    const cell = svg.selectAll('.cell')
      .data(leaves)
      .join('g')
      .attr('class', 'cell')
      .attr('transform', (d: any) => `translate(${d.x0},${d.y0})`);

    cell.append('rect')
      .attr('width', (d: any) => d.x1 - d.x0)
      .attr('height', (d: any) => d.y1 - d.y0)
      .attr('fill', (d: any) => {
        if (colorBy === 'change') return colorScale(d.data.change ?? 0);
        return colorScale((d.parent?.data.name ?? '') as string);
      })
      .attr('fill-opacity', 0.85)
      .attr('stroke', '#fff')
      .attr('stroke-width', 1)
      .on('mouseover', (event, d: any) => {
        d3.select(event.currentTarget).attr('fill-opacity', 1);
        const pct = d.data.change != null
          ? `<br/>Зміна: ${d.data.change > 0 ? '+' : ''}${d.data.change.toFixed(1)}%`
          : '';
        tooltip
          .style('display', 'block')
          .style('left', `${event.pageX + 12}px`)
          .style('top', `${event.pageY - 28}px`)
          .html(`<strong>${d.data.name}</strong><br/>${d3.format(',.0f')(d.value ?? 0)}${pct}`);
      })
      .on('mouseout', (event) => {
        d3.select(event.currentTarget).attr('fill-opacity', 0.85);
        tooltip.style('display', 'none');
      });

    // Текст всередину ячеї (якщо місце дозволяє)
    cell.append('text')
      .attr('x', 4)
      .attr('y', 14)
      .attr('font-size', 11)
      .attr('fill', '#fff')
      .attr('font-weight', '500')
      .text((d: any) => {
        const w = d.x1 - d.x0;
        const h = d.y1 - d.y0;
        return w > 40 && h > 20 ? d.data.name : '';
      })
      .each(function(d: any) {
        const el = d3.select(this);
        const maxWidth = d.x1 - d.x0 - 8;
        // Скорочуйте, якщо не поміщається
        let text = d.data.name;
        while (this.getComputedTextLength() > maxWidth && text.length > 3) {
          text = text.slice(0, -1);
          el.text(text + '…');
        }
      });

    // Значення під назвою
    cell.append('text')
      .attr('x', 4)
      .attr('y', 26)
      .attr('font-size', 10)
      .attr('fill', 'rgba(255,255,255,0.8)')
      .text((d: any) => {
        const w = d.x1 - d.x0;
        const h = d.y1 - d.y0;
        return w > 50 && h > 35 ? d3.format(',.0f')(d.value ?? 0) : '';
      });

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

  return <svg ref={svgRef} width={width} height={height} style={{ display: 'block' }} />;
}

Drill-Down

Кліколаблий treemap з навігацією по рівнях ієрархії:

function DrilldownTreemap({ data }: { data: TreeNode }) {
  const [currentNode, setCurrentNode] = useState<TreeNode>(data);
  const [breadcrumb, setBreadcrumb] = useState<TreeNode[]>([data]);

  function drillDown(node: TreeNode) {
    if (!node.children?.length) return;
    setCurrentNode(node);
    setBreadcrumb(prev => [...prev, node]);
  }

  function drillUp(index: number) {
    const target = breadcrumb[index];
    setCurrentNode(target);
    setBreadcrumb(prev => prev.slice(0, index + 1));
  }

  return (
    <div>
      <nav className="flex gap-2 text-sm mb-3">
        {breadcrumb.map((node, i) => (
          <span key={i}>
            {i > 0 && <span className="text-gray-400 mx-1">/</span>}
            <button
              onClick={() => drillUp(i)}
              className={i === breadcrumb.length - 1 ? 'font-semibold' : 'text-blue-600 hover:underline'}
            >
              {node.name}
            </button>
          </span>
        ))}
      </nav>
      <Treemap data={currentNode} onCellClick={drillDown} />
    </div>
  );
}

Алгоритми раскладки

D3 надає кілька алгоритмів тайлінгу:

const layout = d3.treemap()
  .tile(d3.treemapSquarify)  // квадратні прямокутники (за замовчуванням)
  // .tile(d3.treemapSliceDice) // чередування горизонтальних та вертикальних зрізів
  // .tile(d3.treemapSlice)  // лише горизонтальні зрізи
  // .tile(d3.treemapResquarify) // переиспользует попередню раскладку при оновленні даних

treemapSquarify—найкращий aspect ratio, ячейки ближче до квадрата. treemapResquarify важний для анімованого оновлення—мінімізує переміщення елементів.

Структура даних з API

// Трансформуйте плоскі дані в ієрархію
function buildHierarchy(items: { category: string; subcategory: string; name: string; value: number }[]): TreeNode {
  const root: TreeNode = { name: 'root', children: [] };

  items.forEach(item => {
    let cat = root.children!.find(c => c.name === item.category);
    if (!cat) {
      cat = { name: item.category, children: [] };
      root.children!.push(cat);
    }
    let subcat = cat.children!.find(c => c.name === item.subcategory);
    if (!subcat) {
      subcat = { name: item.subcategory, children: [] };
      cat.children!.push(subcat);
    }
    subcat.children!.push({ name: item.name, value: item.value });
  });

  return root;
}

Часова шкала

Базова treemap із tooltip та drill-down—2–3 дні. З анімацією переходів, кольоровим кодуванням змін та експортом—4–6 днів.