Подписи значений
Подпись значения — это число рядом с элементом серии. Их умеют печатать все серии, у элементов которых есть значение: line, area, scatter, bubble, bar, histogram, range bar, waterfall, heatmap, funnel и pyramid, treemap, sunburst, sankey и chord, pie и donut, radar.
По умолчанию подписи выключены (исключения — funnel, pyramid и treemap: без подписей они нечитаемы) и включаются на серии:
series: [{ type: 'bar', xField: 'month', yField: 'revenue', label: { enabled: true } }],import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Value Labels on Bars' },
subtitle: { text: "outer ('top') and inner ('inner-top') placements" },
series: [
{
type: 'bar',
xField: 'team',
yField: 'done',
name: 'Done',
label: { enabled: true, placement: 'inner-top', fontWeight: 'bold' },
},
{
type: 'bar',
xField: 'team',
yField: 'planned',
name: 'Plan',
label: { enabled: true, placement: 'top' },
},
],
};
}export function getData() {
return [
{ team: 'Alpha', done: 34, planned: 42 },
{ team: 'Beta', done: 27, planned: 30 },
{ team: 'Gamma', done: 41, planned: 38 },
{ team: 'Delta', done: 22, planned: 35 },
];
}Опции
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | boolean | false (funnel/pyramid/treemap — true) | показать подписи |
placement | зависит от серии | top (heatmap/treemap — center) | положение подписи относительно элемента |
formatter | (params) => string | значение как есть | текст подписи; params зависят от серии |
avoidOverlap | boolean | false | скрывать подпись, налезающую на уже нарисованную (ниже) |
minShare | Fraction | 0 | только для серий-частей целого: доля от суммы, начиная с которой часть достойна подписи |
layout | 'inline' | 'stacked' | 'inline' (pie — 'stacked') | подписи «имя и значение»: значение через разделитель или на своей строке |
fontSize | Pixels | 11 (funnel/pyramid — 12) | размер шрифта |
fontWeight | string | number | normal | насыщенность |
fontFamily | string | шрифт темы | гарнитура |
color | ColorValue | foreground; внутри элемента — автоконтраст | цвет текста |
Положение
Прямоугольные элементы — bar, histogram, range bar, waterfall, ячейки heatmap — принимают полный набор: четыре внешние стороны top, bottom, left, right, внешние углы top-left, top-right, bottom-left, bottom-right, center и внутренние варианты тех же восьми позиций с префиксом inner- (inner-top, inner-bottom-right, …).
Точечные элементы — line, area, scatter — принимают top, bottom, left, right; у bubble добавляется inside — текст по центру маркера. У funnel и pyramid — inside и outside, причём внешняя подпись соединяется с сегментом выносной линией.
Вершина радара принимает те же пять плюс outward — своё значение по умолчанию. Где на паутине «наружу», зависит от того, в каком месте круга стоит вершина, поэтому outward отодвигает подпись вдоль её собственной спицы и оставляет внутренность многоугольнику.
Подпись внутри элемента получает ореол цвета элемента, а цвет текста подбирается по контрасту с заливкой — она остаётся читаемой и на тёмном, и на светлом столбце.
Подписи секторов
Pie и donut подписывают сектор, а не элемент, поэтому и подпись устроена иначе: имя и значение — одна подпись из двух частей, у каждой свой шрифт и цвет, а label.placement двигает её целиком. 'inside' ставит её по центру сектора, поверх ореола цвета сектора:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Rotation and sector labels' },
subtitle: { text: 'rotation: -90, labels inside the sectors' },
series: [
{
type: 'pie',
angleField: 'amount',
angleName: 'Share, %',
labelField: 'source',
rotation: -90,
fills: ['#1d4fd7', '#27c08d', '#f4a236', '#9a7bff'],
// the whole label sits in the sector: the name above, the share under it
label: {
placement: 'inside',
category: { fontWeight: 'bold' },
value: { enabled: true },
},
},
],
legend: { position: 'right' },
};
}export function getData() {
return [
{ source: 'Organization', amount: 46 },
{ source: 'Grants', amount: 28 },
{ source: 'Partners', amount: 16 },
{ source: 'Other', amount: 10 },
];
}'outside' — значение по умолчанию — выносит подпись за круг на выноску из двух сегментов. Выносные подписи теснятся сверху и снизу круга, где соседние секторы смотрят почти в одну сторону, поэтому диаграмма разводит их по своим сторонам и отдаёт радиус, пока самая длинная не поместится в область чарта.
Какие секторы вообще получают подпись, решает label.minShare: ниже этой доли от суммы сектор рисуется, но без подписи — так длинный хвост мелочи не хоронит числа, ради которых чарт и сделан.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Labels for what carries the chart' },
subtitle: { text: 'minShare leaves the tail of slivers unlabelled' },
legend: { enabled: false },
series: [
{
type: 'donut',
angleField: 'revenue',
angleName: 'Revenue',
labelField: 'brand',
sectorSpacing: 4,
cornerRadius: 6,
// under four percent of the total a sector is drawn but not labelled
label: { minShare: 0.04, value: { enabled: true, type: 'value', format: ',.0f' } },
},
],
};
}export function getData() {
return [
{ brand: 'JeansMar', revenue: 569566 },
{ brand: 'Cashmerehous', revenue: 274719 },
{ brand: 'Inside Out', revenue: 250483 },
{ brand: 'Cashmere & Silk', revenue: 216170 },
{ brand: 'Fifoshka', revenue: 214121 },
{ brand: 'Larne dress', revenue: 183819 },
{ brand: 'Fashion House', revenue: 159575 },
{ brand: 'Be Very', revenue: 149460 },
{ brand: 'Blues & Jeans', revenue: 121152 },
{ brand: 'Bat Norton', revenue: 113117 },
{ brand: 'Kurtkin Mir', revenue: 111099 },
{ brand: 'Thing!', revenue: 109086 },
{ brand: 'Stella', revenue: 105043 },
{ brand: 'Be Trandy', revenue: 94960 },
{ brand: 'Jeans club', revenue: 90908 },
{ brand: 'Velvet Season', revenue: 90904 },
{ brand: 'Egoista', revenue: 14139 },
{ brand: 'Mon Ami', revenue: 12820 },
{ brand: 'Pretty Wool', revenue: 11304 },
{ brand: 'Lace Age', revenue: 9877 },
{ brand: 'Silk Way', revenue: 8461 },
{ brand: 'Nordwind', revenue: 7215 },
{ brand: 'Bella Riva', revenue: 6088 },
{ brand: 'Trikotage', revenue: 4930 },
{ brand: 'Zima', revenue: 3742 },
{ brand: 'Mimi', revenue: 2611 },
];
}Полный набор — на странице Pie и Donut: layout для однострочной подписи, separator, positionRatio, шрифты частей и опции выносной линии.
Порядок отрисовки
Подписи рисуются поверх всех элементов чарта, а не только своей серии. Столбец, нарисованный позже, не перекроет число предыдущего, а вторая серия не закроет подписи первой.
Место для подписи, свисающей за край области построения, резервируется на этапе раскладки: плот сжимается так, чтобы текст внешней подписи помещался в область чарта, а не обрезался краем канваса.
Борьба с перекрытием
Подписи начинают мешать друг другу задолго до того, как на чарте кончается место: элементы близкой высоты выстраивают числа в один ряд, и текст сталкивается. С avoidOverlap: true подпись, чей прямоугольник налезает на уже нарисованную, не выводится:
label: { enabled: true, avoidOverlap: true },import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Labels That Fight for Room' },
subtitle: { text: 'avoidOverlap keeps the first label of every crowded spot' },
series: [
{
type: 'line',
xField: 'day',
yField: 'visits',
name: 'Visits',
label: {
enabled: true,
avoidOverlap: true,
formatter: ({ value }) => `${value} visits`,
},
},
],
};
}export function getData() {
const visits = [
118, 126, 131, 129, 140, 152, 149, 143, 138, 146, 158, 167, 171, 169, 160, 152, 147, 151, 163, 176, 181, 174, 168, 172,
];
return visits.map((value, index) => ({ day: `${index + 1}`, visits: value }));
}Что стоит знать перед включением:
- Место достаётся первой подписи. Серии опрашиваются в порядке объявления, внутри серии данные идут по порядку — значит более ранняя точка выигрывает у более поздней. Ставьте первой ту серию, чьи числа важнее. Серии, где элементы читаются как части целого — pie/donut, funnel/cone-funnel, pyramid, bubble, — просят в порядке величины: место достаётся самой крупной части, а в тесноте скрываются подписи самых мелких.
- Учёт общий на весь чарт. Две серии избегают подписей друг друга, а не только собственных, но участвуют только те, у кого опция включена: серия без
avoidOverlapрисует свои подписи всегда и ничего не резервирует. - Считаются только подписи. Подпись скрывается при столкновении с другой подписью, но не тогда, когда она просто лежит поверх столбца или линии. Такую подпись стоит увести внутрь элемента (
placement: 'inner-top') или укоротить черезformatter. - Между прямоугольниками нужен зазор 2px. Пока подписи ближе, они считаются соприкасающимися.
- Опцию принимают декартовы серии, funnel/pyramid и pie/donut. Pie разводит тесные выносные подписи вдоль своей стороны круга и без опции; опция добавляет к этому скрытие тех, кому места так и не нашлось, начиная с самых узких секторов. Вторая половина ответа —
label.minShare, и её принимают pie/donut, funnel/cone-funnel, pyramid и scatter/bubble: она вовсе оставляет без подписей длинный хвост мелких секторов, тонких стадий, тонких слоёв и мелких точек. Treemap печатает подпись только там, где плитка достаточно велика; sunburst, sankey и chord рисуют все подписи.
На круговых опция работает с выносными подписями, которые не удалось развести: порог подбирать не нужно — стороны круга держат столько подписей, на сколько у них хватает строк:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'A long tail of small sectors' },
subtitle: { text: 'avoidOverlap hands out labels until the sides run out of rows' },
legend: { enabled: false },
series: [
{
type: 'donut',
angleField: 'revenue',
angleName: 'Revenue',
labelField: 'brand',
sectorSpacing: 4,
cornerRadius: 6,
// the sectors of the tail are slivers, and slivers still get drawn
label: { avoidOverlap: true, value: { enabled: true, type: 'value', format: ',.0f' } },
},
],
};
}export function getData() {
return [
{ brand: 'JeansMar', revenue: 569566 },
{ brand: 'Cashmerehous', revenue: 274719 },
{ brand: 'Inside Out', revenue: 250483 },
{ brand: 'Cashmere & Silk', revenue: 216170 },
{ brand: 'Fifoshka', revenue: 214121 },
{ brand: 'Larne dress', revenue: 183819 },
{ brand: 'Fashion House', revenue: 159575 },
{ brand: 'Be Very', revenue: 149460 },
{ brand: 'Blues & Jeans', revenue: 121152 },
{ brand: 'Bat Norton', revenue: 113117 },
{ brand: 'Kurtkin Mir', revenue: 111099 },
{ brand: 'Thing!', revenue: 109086 },
{ brand: 'Stella', revenue: 105043 },
{ brand: 'Be Trandy', revenue: 94960 },
{ brand: 'Jeans club', revenue: 90908 },
{ brand: 'Velvet Season', revenue: 90904 },
{ brand: 'Egoista', revenue: 14139 },
{ brand: 'Mon Ami', revenue: 12820 },
{ brand: 'Pretty Wool', revenue: 11304 },
{ brand: 'Lace Age', revenue: 9877 },
{ brand: 'Silk Way', revenue: 8461 },
{ brand: 'Nordwind', revenue: 7215 },
{ brand: 'Bella Riva', revenue: 6088 },
{ brand: 'Trikotage', revenue: 4930 },
{ brand: 'Zima', revenue: 3742 },
{ brand: 'Mimi', revenue: 2611 },
];
}Часто лучше не скрывать, а укоротить текст — formatter даёт полный контроль:
label: {
enabled: true,
avoidOverlap: true,
formatter: ({ value }) => `${(value / 1_000_000).toFixed(1)} млн`,
},