Sankey и Chord
Потоковые серии: рёбра fromField → toField с весом sizeField.
Sankey
Узлы раскладываются по колонкам топологической глубины, толщина связей пропорциональна потоку.
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 },
};
}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:
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 },
};
}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 — каждому узлу остаётся хотя бы полоска, а колонка целиком остаётся внутри графика.
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 } }],
};
}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
Узлы по кругу, ленты — взаимные потоки.
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 },
};
}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'), а не как сам поток:
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 },
};
}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 | прозрачность лент потоков |
nodeSpacing | chord | 12 | зазор между дугами узлов, px |
label.enabled | boolean | true | подписи узлов |
label.formatter | ({ name, total, share }) => string | — | вся подпись сразу; сильнее category/value |
label.fontSize | Pixels | 11 | шрифт, к которому откатываются обе половины |
label.fontWeight | string | number | normal | насыщенность |
label.fontFamily | string | шрифт темы | гарнитура |
label.color | ColorValue | foreground | цвет |
label.layout | 'stacked' | 'inline' | 'stacked' | половины в две строки или в одну |
label.separator | string | ' · ' | между половинами подписи в одну строку |
label.category | enabled, шрифт, format, formatter | вкл. | имя узла |
label.value | enabled, type, шрифт, format, formatter | выкл. | то, что течёт через узел |
label.value.type | 'value' | 'percent' | 'value' | сам поток или его доля от целого |
label.minShare | Fraction | 0 | доля, начиная с которой узел стоит подписи |
label.avoidOverlap | boolean | false | снимать подпись, которой не хватило места |
node.width | Pixels | 14 | ширина узла sankey |
node.spacing | Pixels | 14 | вертикальный зазор узлов sankey |
Целое, от которого считается доля, — то, среди чего стоит узел: своя колонка у санкея, всё кольцо у хорды. minShare считает от того же целого, поэтому 0.02 оставляет без подписей мелочь переполненной колонки.
Подсказка и имя величины
Узел — не строка данных, а имя и то, во что оно складывается, поэтому tooltip.renderer получает NodeTooltipRendererParams: { datum?, label, value, share, color }. datum — строка, из которой прочитан узел; узел потока сложен из нескольких строк, и своей у него нет.
tooltip: { renderer: ({ label, value, share }) => `${label}: ${value} (${Math.round(share * 100)}%)` },Без рендерера строка подсказки называется по ключу поля, откуда взята величина, — это имя колонки, а не имя меры. name у серии говорит, как её называть:
series: [{ type: 'treemap', sizeField: 'revenue', name: 'Выручка' }],