Skip to content

Подписи значений

Подпись значения — это число рядом с элементом серии. Их умеют печатать все серии, у элементов которых есть значение: line, area, scatter, bubble, bar, histogram, range bar, waterfall, heatmap, funnel и pyramid, treemap, sunburst, sankey и chord, pie и donut, radar.

По умолчанию подписи выключены (исключения — funnel, pyramid и treemap: без подписей они нечитаемы) и включаются на серии:

js
series: [{ type: 'bar', xField: 'month', yField: 'revenue', label: { enabled: true } }],
ts
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' },
      },
    ],
  };
}
ts
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 },
  ];
}

Опции

ОпцияТипПо умолчаниюОписание
enabledbooleanfalse (funnel/pyramid/treemap — true)показать подписи
placementзависит от серииtop (heatmap/treemap — center)положение подписи относительно элемента
formatter(params) => stringзначение как естьтекст подписи; params зависят от серии
avoidOverlapbooleanfalseскрывать подпись, налезающую на уже нарисованную (ниже)
minShareFraction0только для серий-частей целого: доля от суммы, начиная с которой часть достойна подписи
layout'inline' | 'stacked''inline' (pie — 'stacked')подписи «имя и значение»: значение через разделитель или на своей строке
fontSizePixels11 (funnel/pyramid — 12)размер шрифта
fontWeightstring | numbernormalнасыщенность
fontFamilystringшрифт темыгарнитура
colorColorValueforeground; внутри элемента — автоконтрастцвет текста

Положение

Прямоугольные элементы — 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' ставит её по центру сектора, поверх ореола цвета сектора:

ts
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' },
  };
}
ts
export function getData() {
  return [
    { source: 'Organization', amount: 46 },
    { source: 'Grants', amount: 28 },
    { source: 'Partners', amount: 16 },
    { source: 'Other', amount: 10 },
  ];
}

'outside' — значение по умолчанию — выносит подпись за круг на выноску из двух сегментов. Выносные подписи теснятся сверху и снизу круга, где соседние секторы смотрят почти в одну сторону, поэтому диаграмма разводит их по своим сторонам и отдаёт радиус, пока самая длинная не поместится в область чарта.

Какие секторы вообще получают подпись, решает label.minShare: ниже этой доли от суммы сектор рисуется, но без подписи — так длинный хвост мелочи не хоронит числа, ради которых чарт и сделан.

ts
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' } },
      },
    ],
  };
}
ts
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 подпись, чей прямоугольник налезает на уже нарисованную, не выводится:

js
label: { enabled: true, avoidOverlap: true },
ts
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`,
        },
      },
    ],
  };
}
ts
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 рисуют все подписи.

На круговых опция работает с выносными подписями, которые не удалось развести: порог подбирать не нужно — стороны круга держат столько подписей, на сколько у них хватает строк:

ts
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' } },
      },
    ],
  };
}
ts
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 даёт полный контроль:

js
label: {
  enabled: true,
  avoidOverlap: true,
  formatter: ({ value }) => `${(value / 1_000_000).toFixed(1)} млн`,
},