Skip to content

Pie и Donut

Полярные серии долей. angleField задаёт значение сектора, labelField — имя (легенда, подписи секторов, тултип).

Подпись сектора — одна подпись из двух частей, имени и значения, у каждой свой шрифт, цвет и формат. label.placement решает, где ей быть целиком: 'outside' — на выноске рядом с кругом, 'inside' — внутри сектора. Подписи внутри получают ореол цвета сектора и автоматический контрастный цвет текста.

Имя приходит из labelField, и у этого поля формат такой же, как у любого другого. labelName задаёт, как его значение превращается в текст, один раз на всю серию: легенда, заголовок тултипа и половина подписи с именем читаются одинаково. Принимаются format (сериализуемая строка) и formatter (функция — для того, что строкой не выразить):

js
labelName: { format: '%d.%m.%Y' },

Там, где подписи нужно что-то короче легенды, у label.category есть свой формат, и он перекрывает labelName; за имя он отвечает ровно так же, как value.format/value.formatter — за число:

js
label: { category: { format: '%d.%m' }, value: { enabled: true } },
ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Browser share' },
    series: [
      {
        type: 'pie',
        angleField: 'share',
        angleName: 'Share, %',
        labelField: 'browser',
        // name and share read as one label, the share in its own smaller line
        label: { value: { enabled: true } },
      },
    ],
  };
}
ts
export function getData() {
  return [
    { browser: 'Chrome', share: 64 },
    { browser: 'Safari', share: 19 },
    { browser: 'Edge', share: 6 },
    { browser: 'Firefox', share: 5 },
    { browser: 'Other', share: 6 },
  ];
}

Отступы и скругление у pie

Те же sectorSpacing и cornerRadius работают и без кольца:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Pie with spacing and rounded corners' },
    series: [
      {
        type: 'pie',
        angleField: 'share',
        angleName: 'Share, %',
        labelField: 'device',
        sectorSpacing: 3,
        cornerRadius: 7,
        // one-line label: the name, the separator and the value in a row
        label: { layout: 'inline', value: { enabled: true, type: 'value', format: '.0f' } },
      },
    ],
  };
}
ts
export function getData() {
  return [
    { device: 'Smartphones', share: 48 },
    { device: 'Laptops', share: 24 },
    { device: 'Tablets', share: 14 },
    { device: 'Desktops', share: 9 },
    { device: 'Other', share: 5 },
  ];
}

Donut

innerRadiusRatio создаёт кольцо; innerLabels — текст в центре.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Order statuses' },
    series: [
      {
        type: 'donut',
        angleField: 'count',
        angleName: 'Orders',
        labelField: 'status',
        innerRadiusRatio: 0.62,
        // callout lines: length and style of each segment are configured separately
        calloutLine: {
          radial: { length: 10, strokeWidth: 1 },
          horizontal: { length: 14, strokeWidth: 1 },
        },
        innerLabels: [
          { text: '2193', fontSize: 22, fontWeight: 'bold' },
          { text: 'orders', fontSize: 12 },
        ],
      },
    ],
    legend: { position: 'right' },
  };
}
ts
export function getData() {
  return [
    { status: 'Completed', count: 1840 },
    { status: 'Cancelled', count: 215 },
    { status: 'Refunded', count: 96 },
    { status: 'Failed', count: 42 },
  ];
}

Поворот, цвета, подписи внутри секторов

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 },
  ];
}

Кастомный тултип

tooltip.renderer серии получает datum целиком — в тултип можно выводить любые поля:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Custom sector tooltip' },
    series: [
      {
        type: 'donut',
        angleField: 'visits',
        labelField: 'channel',
        innerRadiusRatio: 0.55,
        tooltip: {
          renderer: ({ datum, color }) => ({
            heading: String(datum.channel),
            rows: [
              { label: 'Visits', value: String(datum.visits), color },
              { label: 'Conversion', value: `${datum.conversion}%` },
            ],
          }),
        },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { channel: 'Organic', visits: 18200, conversion: 4.1 },
    { channel: 'Ads', visits: 9400, conversion: 2.6 },
    { channel: 'Social', visits: 6100, conversion: 1.9 },
    { channel: 'Email', visits: 2800, conversion: 6.4 },
  ];
}

Отступы и скругление

sectorSpacing — зазор между секторами (px), cornerRadius — скругление углов:

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

const money = new Intl.NumberFormat('en-US');

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Sector spacing and corner radius' },
    series: [
      {
        type: 'donut',
        angleField: 'amount',
        labelField: 'fund',
        innerRadiusRatio: 0.55,
        sectorSpacing: 6,
        cornerRadius: 8,
        label: { placement: 'outside' },
        calloutLine: {
          radial: { length: 12, strokeWidth: 1 },
          horizontal: { length: 16, stroke: '#9aa1ad', strokeWidth: 1 },
        },
        tooltip: {
          renderer: ({ label, value, color }) => ({
            heading: label,
            rows: [{ label: 'Amount', value: money.format(Number(value)), color }],
          }),
        },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { fund: 'Stocks', amount: 32129675 },
    { fund: 'Bonds', amount: 9196461 },
    { fund: 'Funds', amount: 5248310 },
    { fund: 'Deposits', amount: 3933726 },
    { fund: 'Currency', amount: 3354246 },
  ];
}

Длинный хвост мелких секторов

Узкий сектор рисуется, каким бы узким он ни был: уступает зазор sectorSpacing, а не сектор, который он должен был отделить.

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

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 },
  ];
}

label.avoidOverlap решает, есть ли для подписи место. Подписи выстраиваются в строки по сторонам круга, и когда у стороны кончаются строки, подпись теряют самые узкие секторы на ней: порог подбирать не нужно, но набор уцелевших подписей зависит от размера чарта.

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 },
  ];
}

Опции складываются: minShare отбирает секторы, достойные подписи, avoidOverlap гарантирует, что оставшиеся не столкнутся.

Значения в легенде

legendValue выводит значение сектора справа от подписи; тултип здесь привязан к курсору (tooltip.position.anchorTo: 'pointer'):

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Values in the legend' },
    series: [
      {
        type: 'donut',
        angleField: 'amount',
        labelField: 'source',
        innerRadiusRatio: 0.62,
        sectorSpacing: 3,
        cornerRadius: 4,
        label: { enabled: false },
        innerLabels: [
          { text: 'Total', fontSize: 13 },
          { text: '$86K', fontSize: 24, fontWeight: 'bold' },
        ],
        legendValue: {
          enabled: true,
          formatter: ({ value }) => `$${value}K`,
        },
      },
    ],
    legend: { position: 'right' },
    tooltip: { position: { anchorTo: 'pointer', yOffset: -8 } },
  };
}
ts
export function getData() {
  return [
    { source: 'Equipment sales', amount: 35 },
    { source: 'Online training', amount: 12 },
    { source: 'Athlete contracts', amount: 5 },
    { source: 'Merchandise', amount: 21 },
    { source: 'Sponsored events', amount: 13 },
  ];
}

Donut-прогресс

Тонкое кольцо + innerLabels — компактный индикатор:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Year progress' },
    series: [
      {
        type: 'donut',
        angleField: 'value',
        labelField: 'state',
        innerRadiusRatio: 0.78,
        rotation: 0,
        fills: ['#21a06c', '#e8eaee'],
        label: { enabled: false },
        innerLabels: [
          { text: '68%', fontSize: 26, fontWeight: 'bold' },
          { text: 'of plan complete', fontSize: 12 },
        ],
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { state: 'Done', value: 68 },
    { state: 'Remaining', value: 32 },
  ];
}

Опции

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

ОпцияТипПо умолчаниюОписание
angleFieldstringзначение сектора (обязательно)
labelFieldstringимя сектора
fillsColorValue[]палитра темыцвета секторов по кругу
strokesColorValue[]палитра темыцвета секторов по кругу
rotationDegrees0начальный угол
outerRadiusRatioFraction0.85доля свободного радиуса под чарт (подписи выносятся в оставшееся место)
innerRadiusRatio (donut)Fraction0.6внутренний радиус
angleNamestringимя angleFieldподпись значения в тултипе
labelName.formatstringкак значение labelField становится текстом: легенда, заголовок тултипа, подпись
labelName.formatter({ datum, value }) => stringто же, когда строкой формата не выразить
sectorSpacingPixels0зазор постоянной ширины между секторами
cornerRadiusPixels0скругление углов секторов
label.enabledbooleanвкл. при labelFieldподписи секторов
label.placement'outside' | 'inside''outside'снаружи на выноске или внутри сектора
label.layout'stacked' | 'inline''stacked'значение отдельной строкой под именем или в одну строку с ним
label.separatorstring' · 'разделитель частей однострочной подписи
label.positionRatioFraction0.7позиция вдоль радиуса (при placement: 'inside')
label.minShareFraction0доля от суммы, начиная с которой сектор достоин подписи
label.avoidOverlapbooleanfalseскрывать подписи, которым не хватило места, вместо наложения
label.category.enabledbooleanвкл. при labelFieldимя сектора в подписи
label.category.formatstringстрока формата для поля имени ('%d.%m.%Y', ',.0f')
label.category.formatter({ datum, label, value, share }) => …текст половины с именем
label.category.fontSizePixels11шрифт имени
label.category.fontFamilystringшрифт темыгарнитура
label.category.fontWeightstring | numbernormalнасыщенность
label.category.colorColorValueforeground / автоконтрастцвет текста
label.value.enabledbooleanfalseзначение сектора в подписи
label.value.type'percent' | 'value''percent'доля от суммы или значение angleField
label.value.formatstringстрока формата (',.2f', '.1%')
label.value.formatter({ datum, label, value, share }) => stringполный контроль над текстом
label.value.fontSizePixels11шрифт значения
label.value.colorColorValueforeground / автоконтрастцвет текста
calloutLine.radial.lengthPixels20длина радиального отрезка
calloutLine.radial.strokeColorValueцвет секторацвет радиального отрезка
calloutLine.radial.strokeWidthPixels1толщина
calloutLine.horizontal.lengthPixels20длина хвоста к подписи
calloutLine.horizontal.strokeColorValueкак radialцвет хвоста
calloutLine.horizontal.strokeWidthPixelsкак radialтолщина хвоста
legendValue.enabledbooleanfalseзначение сектора в легенде
legendValue.formatter({ datum, label, value, color }) => stringзначениеформат значения
tooltip.renderer({ datum, label, value, color }) => …кастомный тултип
innerLabels[]{ text, fontSize?, fontWeight?, color? }строки в центре donut
innerCircle.fillColorValueзаливка центра donut
innerRadiusRatioFraction0.6внутренний радиус donut

Клик по элементу легенды скрывает сектор (доли пересчитываются); непоместившиеся элементы легенды пагинируются стрелками.