Standalone TreeMap для Angular без лишних зависимостей: как я сделал stockchart-treemap

TreeMap — это визуализация, где площадь прямоугольника = вес, а цвет = метрика. Отлично подходит для market heatmap (карта рынка), портфелей, иерархий ресурсов и любых “взвешенных деревьев”.
Мне TreeMap понадобился в Angular-проекте под “тепловые карты” и разные иерархические отчёты. Казалось бы — задача стандартная, значит решение должно быть “в один npm install”. Но реальность оказалась неожиданной: готовых TreeMap-решений именно для Angular практически нет.
В итоге я сделал свой standalone компонент и оформил его в npm-пакет: stockchart-treemap.
Дисклеймер
Пакет бесплатный.
Это не реклама — мне никто не заплатил ни копейки.
Я делал компонент в первую очередь для своих задач, а потом упаковал так, чтобы можно было переиспользовать и не зависеть от внешних UI-комбайнов.
Почему не использовать готовое решение
Вот тут самое забавное: обычно в статьях пишут “не подошло потому что тяжелое/дорогое/не то API”. У меня было проще — мне просто не удалось найти нормальный готовый TreeMap под Angular.
Что я видел в процессе поисков:
Почти всё — под чистый JS, React или Vue
TreeMap как визуализация часто встречается в “JS-мире”: примеры на D3-подобных штуках, готовые виджеты, демки. Но это не Angular-компонент, а код, который сам рисует DOM/SVG/Canvas.
В Angular это превращается в вечную обвязку:
ElementRef,ngAfterViewInit, ручной lifecycle,чистка при destroy,
ручные обновления при изменении данных,
танцы с change detection,
и “почему оно перерисовалось два раза/не там/не тогда”.
То есть формально “решение есть”, но по ощущениям это уже не “встроил компонент”, а “интегрировал чужой движок”.
То, что называется “Angular TreeMap”, часто:
устаревшее (старые версии Angular, нет standalone),
или это тонкая обёртка над JS-ядром без нормального Angular UX (шаблоны, события, типизация, обновления данных — всё “на честном слове”).
Есть варианты в составе больших UI-комбайнов
Иногда это нормально, но лично мне не хотелось тащить “полплатформы” ради одного TreeMap. Особенно когда в проекте уже есть своя дизайн-система и свои компоненты.
Итог: формально решений много, но “поставил и получил нативный Angular-компонент с понятной интеграцией” — мне не попалось. Поэтому я решил сделать компонент так, как мне самому было бы удобно его использовать.
Что получилось: stockchart-treemap
stockchart-treemap — standalone TreeMap компонент для Angular:
без внешних runtime-зависимостей (никаких Kendo UI и т.п.),
с цветовой шкалой по значениям (min/center/max),
с кастомными шаблонами плиток и заголовков,
с несколькими режимами источника данных,
с событиями клика/ховера и “путём” до узла.
Демо
StackBlitz: https://stackblitz.com/edit/stackblitz-starters-drgync8z?file=README.md
Установка
npm install stockchart-treemap
Peer deps: @angular/core и @angular/common 20.x.
Быстрый старт
Пример “из коробки”: просто массив + настройки полей.
import { Component } from '@angular/core'; import { TreeMapComponent, TreeMapOptions } from 'stockchart-treemap'; @Component({ standalone: true, selector: 'app-treemap-demo', imports: [TreeMapComponent], template: ` <stockchart-treemap [data]="data" [options]="options"> </stockchart-treemap> ` }) export class TreemapDemoComponent { data = [ { name: 'Tech', value: 35, change: 4.2, items: [ { name: 'A', value: 20, change: 5.1 }, { name: 'B', value: 15, change: 2.0 } ]}, { name: 'Energy', value: 25, change: -3.4 }, { name: 'Health', value: 18, change: 1.6 } ]; options: Partial<TreeMapOptions> = { textField: 'name', valueField: 'value', childrenField: 'items', // цвет — по "change" colorValueField: 'change', colorScale: { min: '#E53935', center: '#F5F5F5', max: '#2E7D32' }, // оставляем пустым, чтобы полагаться на colorScale colors: [] }; }
Источники данных: три режима, потому что в жизни по-разному
Визуализация — это всегда “последний метр” после данных. И данные могут приходить как угодно, поэтому я сделал три режима.
1) Обычный массив
Самый простой: [data]="data".
2) Observable
Если данные живые (RxJS-пайплайн, сокеты, периодические обновления на вашей стороне) — можно отдать data$, компонент подпишется и будет перерисовываться при новых значениях.
3) Loader-функция + polling
Иногда удобнее дать компоненту “как загрузить данные”, а он сам будет обновляться раз в N миллисекунд:
loader = () => this.http.get<Item[]>('/api/treemap'); refreshMs = 5000; // обновление каждые 5 секунд
Плюс есть refreshNow() — полезно для “обновить по кнопке” или после смены фильтров.
Цвета: палитра или шкала по значению
Мне нужны были оба сценария:
Палитра
Когда цвет — это категория (например, разные отделы/типы). Можно задать:
colors: ['#...', '#...', ...]либо пары
[min,max], чтобы генерировать градиенты по уровням.
Шкала по значению (heatmap)
Классика для “карты рынка”: у элемента есть метрика (например, change), и цвет вычисляется по шкале:
min— красный,center— нейтральный,max— зелёный.
Работает так:
задаём
colorValueField,задаём
colorScale,и получаем прогнозируемый градиент.
Если у данных вообще нет цветового поля — оттенки для вложенных уровней считаются автоматически, чтобы дерево было читабельным.
Layout-ы: squarified и slice-and-dice
Я сделал два варианта разметки:
squarified(по умолчанию) — стремится к “квадратности” плиток, обычно выглядит лучше.horizontal/vertical— slice-and-dice, нарезка полосами с чередованием ориентации по уровням.
Это покрывает большую часть практических кейсов без превращения компонента в “комбайн настроек”.
Кастомные шаблоны: плитка и заголовок
TreeMap без нормальных шаблонов быстро превращается в “красивую мозаику”, из которой нельзя извлечь смысл.
Можно задать отдельно:
titleTemplate— заголовок контейнеров,tileTemplate— содержимое плитки (обычно лист).
Пример:
<stockchart-treemap [data]="data" [options]="options" [tileTemplate]="tile" [titleTemplate]="title" (tileClick)="onTile($event)"> </stockchart-treemap> <ng-template #title let-item let-node="node"> <div class="title"> {{ item.name }} · {{ node.value | number:'1.0-0' }} </div> </ng-template> <ng-template #tile let-item let-isLeaf="isLeaf"> <div class="tile"> <strong>{{ item.name }}</strong> <span *ngIf="isLeaf">{{ item.change }}%</span> </div> </ng-template>
События и “путь” до узла
В интерактивных сценариях важно не только “что кликнули”, но и “где это в дереве”.
Поэтому в событиях есть:
dataItem— исходный объект,path— цепочка элементов от корня до выбранной ноды.
import { TreeMapEvent } from 'stockchart-treemap'; onTile(ev: TreeMapEvent<MyNode>) { console.log(ev.dataItem); console.log(ev.path); // полезно для breadcrumbs / drilldown }
Ключевые опции
То, что реально используется чаще всего:
textField / valueField / childrenField / colorFieldcolorValueField— поле для расчётаcolorScalecolors— палитра (если пусто — можно полагаться наcolorScaleили fallback)titleSize,showTopLevelTitlesderiveParentValueFromChildren— если у контейнера нет value, можно вывести из детейroundDecimals— помогает стабилизировать координатыminTileSize— минимальный размер плитки для рекурсии
Немного про “как я делал”
Я сознательно старался избежать “магии”. Внутри всё раскладывается по шагам:
Нормализация данных (любой источник → массив нод).
Подсчёт значений (в т.ч. вычисление контейнеров из детей — опционально).
Layout (squarified / slice-and-dice) рекурсивно разбивает прямоугольник.
Цвет (палитра или интерполяция по шкале).
Рендер + шаблоны + события.
На практике это даёт главное: предсказуемость и возможность легко встроить компонент в проект, не подстраиваясь под чужие решения.
Где это полезно
Market heatmap: сектор → индустрия → компания (площадь = вес, цвет = % change).
Портфель: активы → позиции (площадь = доля, цвет = PnL).
Ресурсы: департаменты → проекты → сервисы (площадь = расходы/нагрузка, цвет = отклонение).
Любые метрики в дереве: storage usage, распределение задач, coverage, KPI и т.д.
Ограничения
Если попытаться отображать десятки тысяч нод одновременно — DOM начнёт страдать (как и у большинства UI-визуализаций). Этот компонент рассчитан на нормальные продуктовые сценарии, а не на экстремальные бенчмарки.
Если когда-нибудь понадобится “очень много элементов” — это уже территория Canvas/WebGL/виртуализации.
Заключение
stockchart-treemap появился из простой причины: TreeMap для Angular почему-то оказался редкостью, а интегрировать JS-виджеты “как есть” в Angular — это отдельный проект по обвязке.
Я сделал standalone компонент, который ставится из npm, нормально живёт в Angular, поддерживает разные источники данных, цветовые шкалы и кастомные шаблоны.
Демо: https://stackblitz.com/edit/stackblitz-starters-drgync8z?file=README.md
Пакет: npm install stockchart-treemap