Skip to content

Sankey и Chord

Потоковые серии: рёбра fromField → toField с весом sizeField.

Sankey

Узлы раскладываются по колонкам топологической глубины, толщина связей пропорциональна потоку.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'User journey' },
    series: [{ type: 'sankey', fromField: 'from', toField: 'to', sizeField: 'value' }],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { from: 'Traffic', to: 'Organic', value: 620 },
    { from: 'Traffic', to: 'Ads', value: 380 },
    { from: 'Organic', to: 'Sign-up', value: 240 },
    { from: 'Ads', to: 'Sign-up', value: 190 },
    { from: 'Organic', to: 'Bounce', value: 380 },
    { from: 'Ads', to: 'Bounce', value: 190 },
    { from: 'Sign-up', to: 'Subscription', value: 160 },
    { from: 'Sign-up', to: 'Freemium', value: 270 },
  ];
}

Подписи и настройка узлов

Подпись узла — его имя и то, что через него течёт, нарисованные одним блоком: label.category — имя, label.value — число, у каждого свои шрифт, цвет и формат; ровно та же форма, что у подписи сектора круговой. Имя печатается само по себе, пока value.enabled не попросит и число; layout ставит половины в одну строку вместо двух, а сам label несёт шрифт, к которому обе половины откатываются. Рядом с ними — node.width / node.spacing и linkOpacity:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Budget flow' },
    subtitle: { text: 'the name and the total of a node, each with its own font' },
    series: [
      {
        type: 'sankey',
        fromField: 'from',
        toField: 'to',
        sizeField: 'amount',
        node: { width: 14, spacing: 24 },
        linkOpacity: 0.5,
        label: {
          fontSize: 12,
          // the name and the number are one label, styled apart
          category: { fontWeight: 'bold' },
          value: { enabled: true, format: ',.0f', fontSize: 11, color: '#8892a4' },
        },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { from: 'Salary', to: 'Budget', amount: 220 },
    { from: 'Freelance', to: 'Budget', amount: 60 },
    { from: 'Budget', to: 'Rent', amount: 90 },
    { from: 'Budget', to: 'Food', amount: 70 },
    { from: 'Budget', to: 'Transport', amount: 30 },
    { from: 'Budget', to: 'Savings', amount: 90 },
    { from: 'Savings', to: 'Investments', amount: 60 },
    { from: 'Savings', to: 'Emergency fund', amount: 30 },
  ];
}

Много узлов в колонке

Масштаб «значение → пиксели» задаёт та колонка, у которой раньше кончается место, а не самая тяжёлая: промежутки между узлами — фиксированная плата, и колонке из двенадцати узлов нужно сначала оплатить одиннадцать промежутков, а уж потом значения получат высоту. Если и одни промежутки не влезают, они сжимаются ниже node.spacing — каждому узлу остаётся хотя бы полоска, а колонка целиком остаётся внутри графика.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue by year and month' },
    subtitle: { text: 'a column of twelve nodes still fits the height' },
    series: [{ type: 'sankey', fromField: 'year', toField: 'month', sizeField: 'revenue', node: { spacing: 8 } }],
  };
}
ts
const YEARS = ['2019', '2020', '2021'];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

/** Revenue per year split across the months it landed in. */
export function getData() {
  return YEARS.flatMap((year, yearIndex) =>
    MONTHS.map((month, monthIndex) => ({
      year,
      month,
      revenue: 40 + ((yearIndex * 7 + monthIndex * 5) % 55),
    })),
  );
}

Chord

Узлы по кругу, ленты — взаимные потоки.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Calls between services' },
    series: [{ type: 'chord', fromField: 'from', toField: 'to', sizeField: 'calls' }],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { from: 'API', to: 'Auth', calls: 320 },
    { from: 'API', to: 'Billing', calls: 180 },
    { from: 'Web', to: 'API', calls: 540 },
    { from: 'Mobile', to: 'API', calls: 410 },
    { from: 'Billing', to: 'Auth', calls: 90 },
    { from: 'Web', to: 'Auth', calls: 130 },
  ];
}

Отступы и подписи

nodeSpacing — зазор между дугами (px по внутреннему радиусу), linkOpacity — плотность лент. Блок подписи тот же, что у санкея: здесь его числовая половина читается как доля кольца (value.type: 'percent'), а не как сам поток:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'User migration between platforms' },
    subtitle: { text: 'nodeSpacing: 28, dense ribbons, a share under every name' },
    series: [
      {
        type: 'chord',
        fromField: 'from',
        toField: 'to',
        sizeField: 'users',
        nodeSpacing: 28,
        linkOpacity: 0.55,
        label: {
          fontSize: 12,
          // the value half reads as a share of the ring, on a line of its own
          value: { enabled: true, type: 'percent', fontSize: 10, color: '#8892a4' },
        },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { from: 'Web', to: 'iOS', users: 18 },
    { from: 'Web', to: 'Android', users: 22 },
    { from: 'iOS', to: 'Web', users: 9 },
    { from: 'Android', to: 'Web', users: 12 },
    { from: 'iOS', to: 'Android', users: 6 },
    { from: 'Android', to: 'iOS', users: 7 },
    { from: 'Web', to: 'Desktop', users: 10 },
    { from: 'Desktop', to: 'Web', users: 5 },
  ];
}

Опции

Общие опции всех серий (name, showInLegend, tooltip.renderer, …) — в разделе Общие опции серий.

ОпцияСерииОписание
fromFieldоберёбра графа потоков
toFieldоберёбра графа потоков
sizeFieldоберёбра графа потоков
fillsобецвета узлов по кругу палитры

Полный список опций

ОпцияТипПо умолчаниюОписание
linkOpacityобе0.35прозрачность лент потоков
nodeSpacingchord12зазор между дугами узлов, px
label.enabledbooleantrueподписи узлов
label.formatter({ name, total, share }) => stringвся подпись сразу; сильнее category/value
label.fontSizePixels11шрифт, к которому откатываются обе половины
label.fontWeightstring | numbernormalнасыщенность
label.fontFamilystringшрифт темыгарнитура
label.colorColorValueforegroundцвет
label.layout'stacked' | 'inline''stacked'половины в две строки или в одну
label.separatorstring' · 'между половинами подписи в одну строку
label.categoryenabled, шрифт, format, formatterвкл.имя узла
label.valueenabled, type, шрифт, format, formatterвыкл.то, что течёт через узел
label.value.type'value' | 'percent''value'сам поток или его доля от целого
label.minShareFraction0доля, начиная с которой узел стоит подписи
label.avoidOverlapbooleanfalseснимать подпись, которой не хватило места
node.widthPixels14ширина узла sankey
node.spacingPixels14вертикальный зазор узлов sankey

Целое, от которого считается доля, — то, среди чего стоит узел: своя колонка у санкея, всё кольцо у хорды. minShare считает от того же целого, поэтому 0.02 оставляет без подписей мелочь переполненной колонки.

Подсказка и имя величины

Узел — не строка данных, а имя и то, во что оно складывается, поэтому tooltip.renderer получает NodeTooltipRendererParams: { datum?, label, value, share, color }. datum — строка, из которой прочитан узел; узел потока сложен из нескольких строк, и своей у него нет.

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

Без рендерера строка подсказки называется по ключу поля, откуда взята величина, — это имя колонки, а не имя меры. name у серии говорит, как её называть:

js
series: [{ type: 'treemap', sizeField: 'revenue', name: 'Выручка' }],