Skip to content

Treemap

Hierarchical series without axes. Data is nested via children; a node's value is the leaf's sizeField or the sum of its descendants.

Treemap

Squarify layout: nested rectangles, groups with headers.

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Lines of code by module' },
    series: [{ type: 'treemap', labelField: 'label', sizeField: 'size' }],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    {
      label: 'Frontend',
      children: [
        { label: 'app', size: 420 },
        { label: 'widgets', size: 180 },
        { label: 'shared', size: 310 },
      ],
    },
    {
      label: 'Backend',
      children: [
        { label: 'api', size: 540 },
        { label: 'workers', size: 230 },
        { label: 'db', size: 160 },
      ],
    },
    {
      label: 'Infra',
      children: [
        { label: 'ci', size: 90 },
        { label: 'deploy', size: 140 },
      ],
    },
  ];
}

Tile labels and gaps

A tile label is put together the way a pie sector's is: the name of the node and its value are one label of two halves, each with its own font and format. label.value.enabled turns the number on, layout puts it on its own line (default) or behind a separator in the same row, and placement moves the whole block to one of 9 spots in the tile. The color is chosen by auto-contrast against the tile.

itemGap is the gap between neighbouring tiles and groupGap the gap between neighbouring groups — between them only: a tile on the edge of its group, or of the chart, keeps that edge, so the padding of the plot stays the padding of the plot. groupGap falls back to itemGap when it is not given:

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Tile labels and gaps' },
    series: [
      {
        type: 'treemap',
        labelField: 'label',
        sizeField: 'size',
        itemGap: 3,
        groupGap: 10,
        groupHeader: { height: 22, fontSize: 13 },
        label: {
          enabled: true,
          placement: 'top-left',
          layout: 'inline',
          fontSize: 12,
          fontWeight: 'bold',
          value: { enabled: true, fontWeight: 'normal' },
        },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    {
      label: 'Frontend',
      children: [
        { label: 'App', size: 34 },
        { label: 'Widgets', size: 22 },
        { label: 'UI Kit', size: 18 },
        { label: 'Utils', size: 8 },
      ],
    },
    {
      label: 'Backend',
      children: [
        { label: 'API', size: 28 },
        { label: 'Jobs', size: 14 },
        { label: 'Auth', size: 9 },
      ],
    },
    {
      label: 'Infra',
      children: [
        { label: 'CI', size: 12 },
        { label: 'IaC', size: 10 },
      ],
    },
  ];
}

Group headers read the same two halves, always in one row, so a group states its total the same way its tiles state theirs. A header is a heading over its group: the strip is unpainted until groupHeader.background asks for a fill, and the name is written in the color of the group — or in auto-contrast against the fill once there is one. groupHeader also carries the height of the strip and its font, falling back to label's. A label that does not fit its tile — or a heading that does not fit its strip — is not drawn; label.minShare decides earlier which nodes are worth a label at all.

js
series: [
  {
    type: 'treemap',
    itemGap: 3,
    groupGap: 10,
    groupHeader: { height: 22, fontSize: 13, background: '#f2f0ed' },
  },
],

Options

Options common to all series (name, showInLegend, tooltip.renderer, …) are covered in Common series options.

OptionSeriesDefaultDescription
groupHeader.heighttreemap18group header height
groupHeader.backgroundtreemapnonefill behind the heading
groupHeader.fontSizetreemaplabel.fontSize, then 11heading font size
groupHeader.fontWeighttreemaplabel.fontWeight, then boldheading font weight
groupHeader.fontFamilytreemaplabel.fontFamily, then theme fontheading font family
groupHeader.colortreemapthe color of the group; auto-contrast over a fillheading text color
fillsallpalettebranch/layer colors
itemGaptreemap2gap between neighbouring tiles
groupGaptreemapitemGapgap between neighbouring groups
labelFieldtreemaplabel/size/childrenhierarchy keys
sizeFieldtreemaplabel/size/childrenhierarchy keys
childrenFieldtreemaplabel/size/childrenhierarchy keys
labelNameFormattable<PartNameParams>the raw field valuehow the name of a node reads everywhere: legend, tooltip, header, label
label.enabledbooleantrueshow value labels
label.placementcenter, edges and corners (9 positions)'center'label position
label.layout'stacked' | 'inline''stacked'the value on its own line or behind a separator
label.separatorstring' · 'between the halves of an inline label
label.minShareFraction0share of the total a node needs to be labelled
label.categorySwitchable & FontOptions & Formattableonthe name half: its own font and format
label.valuePartValueLabelOptionsoffthe value half: type: 'percent' | 'value', format, formatter, its own font
label.formatter({ datum, label, value, share }) => stringthe whole label at once; wins over category/value
label.fontSizePixels11label font size
label.fontWeightstring | numbernormalfont weight
label.fontFamilystringtheme fontfont family
label.colorColorValueforeground; auto-contrast when insidetext color

Tooltip and the name of a value

A node is not a row of the data — it is a name and what it adds up to — so tooltip.renderer receives NodeTooltipRendererParams: { datum?, label, value, share, color }. datum is the row the node was read from; a flow node is summed from several rows and has none.

js
tooltip: { renderer: ({ label, value, share }) => `${label}: ${value} (${Math.round(share * 100)}%)` },

Without a renderer the row of the tooltip is named after the data key the value came from — a column name, not the name of a measure. name on the series says what it should be called instead:

js
series: [{ type: 'treemap', sizeField: 'revenue', name: 'Revenue' }],