Skip to content

Оси

Типы осей: category (бэнды), number, time, log. Связка по position: bottom/top — ось X, left/right — ось Y.

Вид по умолчанию

Из коробки оси не шумят, и два направления делят работу: у оси категорий остаётся линия и нет сетки, ось значений линию не рисует — её читают по пунктирной сетке. Тики выключены на обеих. В горизонтальном графике ось значений — горизонтальная, поэтому пунктир вместе с ней разворачивается вертикально.

Вся эта обвязка светло-серая — токен темы axisColor, переопределяется на каждой оси через line.stroke, tick.stroke и gridLine.stroke. Дефолты лежат под вашими опциями, так что всё возвращается по запросу: tick: { enabled: true }, line: { enabled: true }, gridLine: { enabled: true, lineDash: [] } — сплошная сетка вдоль категорий.

Линия, тики и сетка

У линии оси те же три опции стиля, что и у сетки — цвет, толщина и пунктир, а у тиков рядом настраиваются длина и цвет:

js
axes: [
  {
    position: 'bottom',
    line: { stroke: '#0f766e', width: 2, lineDash: [6, 3] },
    tick: { enabled: true, size: 10, width: 2, color: '#0f766e' },
    gridLine: { stroke: '#e2e8f0', width: 1, lineDash: [2, 4] },
  },
],

line.lineDash и gridLine.lineDash записываются как массивы штрихов: [штрих, пробел] в пикселях; пустой массив [] рисует сплошную линию, даже если тема задала пунктир. tick.size — длина засечки, она всегда рисуется наружу от области построения, а tick.color — алиас tick.stroke: любой из них задаёт цвет засечек и выигрывает у темы (tickColor).

Две оси значений

Величины разного масштаба — евро и проценты, запросы и задержка — плохо уживаются на одной шкале: меньшая прижимается к нулевой линии. Объявите вторую ось значений на противоположной стороне, а keys на каждой оси скажет, какие серии она несёт:

js
series: [
  { type: 'bar', xField: 'month', yField: 'revenue', name: 'Revenue' },
  { type: 'line', xField: 'month', yField: 'margin', name: 'Margin' },
],
axes: [
  { type: 'category', position: 'bottom' },
  { type: 'number', position: 'left', keys: ['revenue'], title: { text: 'Revenue, k€' } },
  { type: 'number', position: 'right', keys: ['margin'], title: { text: 'Margin, %' } },
],
ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue and Margin' },
    subtitle: { text: 'thousands of euro and per cent, on their own scales' },
    series: [
      { type: 'bar', xField: 'month', yField: 'revenue', name: 'Revenue' },
      { type: 'line', xField: 'month', yField: 'margin', name: 'Margin' },
    ],
    axes: [
      { type: 'category', position: 'bottom' },
      { type: 'number', position: 'left', keys: ['revenue'], title: { text: 'Revenue, k€' } },
      { type: 'number', position: 'right', keys: ['margin'], label: { format: ',.1f' }, title: { text: 'Margin, %' } },
    ],
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', revenue: 420, margin: 12.4 },
    { month: 'Feb', revenue: 465, margin: 13.1 },
    { month: 'Mar', revenue: 510, margin: 12.8 },
    { month: 'Apr', revenue: 495, margin: 14.6 },
    { month: 'May', revenue: 580, margin: 15.2 },
    { month: 'Jun', revenue: 640, margin: 16.9 },
    { month: 'Jul', revenue: 705, margin: 16.1 },
    { month: 'Aug', revenue: 690, margin: 17.4 },
  ];
}

В keys перечисляются поля значений — yField, а у многополевых серий их low/high и OHLC-поля; id серии тоже подходит, и так две серии по одному полю расходятся на разные оси. Всё, что не забрала ни одна ось, достаётся первой оси без keys — поэтому график с одной осью значений ведёт себя как раньше.

Дальше каждая ось масштабируется только по своим сериям и сама подбирает круглые границы; скрытие серии через легенду пересчитывает только её ось. Две сетки никогда не совпали бы, поэтому сетку сохраняет только первая ось значений — второй её можно вернуть через gridLine: { enabled: true }.

В горизонтальном графике всё то же самое, только оси значений — bottom и top. По первой оси значений по-прежнему читаются аннотации, подпись значения у перекрестия и окно зума по Y.

Временная ось

time принимает Date, timestamp или ISO-строку; тики встают на календарные границы, формат подписи зависит от шага (часы → дни → месяцы → годы).

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Metric over Time' },
    series: [{ type: 'line', xField: 'date', yField: 'value', name: 'Value', marker: { enabled: false } }],
    axes: [
      { type: 'time', position: 'bottom' },
      { type: 'number', position: 'left' },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  const start = Date.UTC(2025, 0, 1);
  const day = 24 * 60 * 60 * 1000;
  const values = [41, 43, 47, 45, 49, 53, 51, 56, 58, 55, 61, 64, 62, 67, 70, 68, 73, 71, 76, 79, 77, 82, 85, 83, 88, 86, 91, 94, 92, 97];
  return values.map((value, index) => ({ date: new Date(start + index * 3 * day), value }));
}

Столбцы на временной оси

Столбцы, диапазоны, ящики и свечи встают на ось time так же, как на полосную, — разница в том, что ось расставляет их по настоящему расстоянию, и месяц без данных оставляет пустое место, а не исчезает:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Sales by month' },
    subtitle: { text: 'February has no data — a continuous axis keeps its place' },
    series: [
      { type: 'bar', xField: 'date', yField: 'plan', name: 'Plan' },
      { type: 'bar', xField: 'date', yField: 'actual', name: 'Actual' },
    ],
    axes: [
      { type: 'time', position: 'bottom' },
      { type: 'number', position: 'left', title: { text: 'M₽' } },
    ],
    legend: { position: 'bottom' },
  };
}
ts
/** Monthly sales — February is missing, and a continuous axis leaves its place empty. */
export function getData() {
  const month = (index: number) => new Date(Date.UTC(2025, index, 1));
  return [
    { date: month(0), plan: 120, actual: 108 },
    { date: month(2), plan: 130, actual: 141 },
    { date: month(3), plan: 135, actual: 129 },
    { date: month(4), plan: 140, actual: 152 },
    { date: month(5), plan: 145, actual: 138 },
    { date: month(6), plan: 150, actual: 163 },
  ];
}

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

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

ts
axes: [
  // часовые замеры с редкими пропусками: столбец шириной в час, что бы ни говорили пропуски
  { type: 'time', position: 'bottom', bandSpan: 60 * 60 * 1000 },
  { type: 'number', position: 'left' },
]

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

Когда даты стоят ровно — торговые сессии, недели без выходных — второй ответ даёт ось ordinal-time: полосы равной ширины с календарными подписями, то есть пропуски закрываются и расстояние ни на что не влияет.

Логарифмическая ось

log — для данных, растущих на порядки; тики на степенях base (по умолчанию 10).

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Exponential Growth' },
    subtitle: { text: 'logarithmic Y axis' },
    series: [{ type: 'line', xField: 'year', yField: 'users', name: 'Users' }],
    axes: [
      { type: 'category', position: 'bottom' },
      { type: 'log', position: 'left' },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { year: '2018', users: 120 },
    { year: '2019', users: 540 },
    { year: '2020', users: 2400 },
    { year: '2021', users: 9800 },
    { year: '2022', users: 41000 },
    { year: '2023', users: 165000 },
    { year: '2024', users: 720000 },
    { year: '2025', users: 2900000 },
  ];
}

Иерархические категории

grouped-category: значения данных — массивы [группа, элемент] (уровней может быть и больше); под подписями элементов появляется строка групп с разделителями:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue by Product Category' },
    subtitle: { text: '$B, grouped categories year → quarter' },
    series: [
      { type: 'bar', xField: 'period', yField: 'iphone', name: 'iPhone' },
      { type: 'bar', xField: 'period', yField: 'mac', name: 'Mac' },
      { type: 'bar', xField: 'period', yField: 'services', name: 'Services' },
    ],
    axes: [
      { type: 'grouped-category', position: 'bottom' },
      { type: 'number', position: 'left' },
    ],
  };
}
ts
export function getData() {
  return [
    { period: ['2018', 'Q1'], iphone: 140, mac: 16, services: 20 },
    { period: ['2018', 'Q2'], iphone: 124, mac: 20, services: 30 },
    { period: ['2018', 'Q3'], iphone: 112, mac: 20, services: 36 },
    { period: ['2018', 'Q4'], iphone: 118, mac: 24, services: 36 },
    { period: ['2019', 'Q1'], iphone: 124, mac: 18, services: 26 },
    { period: ['2019', 'Q2'], iphone: 108, mac: 20, services: 40 },
    { period: ['2019', 'Q3'], iphone: 96, mac: 22, services: 42 },
    { period: ['2019', 'Q4'], iphone: 104, mac: 22, services: 40 },
  ];
}

В горизонтальных чартах ось категорий вертикальная, и колонка групп с разделителями появляется слева от подписей элементов:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue by Product Category' },
    subtitle: { text: '$B, grouped categories year → quarter' },
    series: [
      { type: 'bar', xField: 'period', yField: 'iphone', name: 'iPhone', direction: 'horizontal' },
      { type: 'bar', xField: 'period', yField: 'mac', name: 'Mac', direction: 'horizontal' },
      { type: 'bar', xField: 'period', yField: 'services', name: 'Services', direction: 'horizontal' },
    ],
    axes: [
      { type: 'grouped-category', position: 'left' },
      { type: 'number', position: 'bottom' },
    ],
  };
}
ts
export function getData() {
  return [
    { period: ['2018', 'Q1'], iphone: 140, mac: 16, services: 20 },
    { period: ['2018', 'Q2'], iphone: 124, mac: 20, services: 30 },
    { period: ['2018', 'Q3'], iphone: 112, mac: 20, services: 36 },
    { period: ['2018', 'Q4'], iphone: 118, mac: 24, services: 36 },
    { period: ['2019', 'Q1'], iphone: 124, mac: 18, services: 26 },
    { period: ['2019', 'Q2'], iphone: 108, mac: 20, services: 40 },
    { period: ['2019', 'Q3'], iphone: 96, mac: 22, services: 42 },
    { period: ['2019', 'Q4'], iphone: 104, mac: 22, services: 40 },
  ];
}

Столько ярусов, сколько уровней в кортеже

Кортеж не ограничен двумя элементами: каждый, кроме последнего, получает свой ярус. ['2024', 1, 'Q1'] подписывает деления кварталами, над ними ставит ряд полугодий, а над ними — ряд годов: самый верхний уровень дальше всех от графика, как в шапке сводной таблицы. Разделитель принадлежит самому верхнему уровню, на котором он есть, — граница года рисуется один раз и проходит через все ярусы.

Группа — это соседние категории с равными значениями, а не с равным текстом: null и 'null', 1 и '1' остаются двумя группами ровно так же, как они остаются двумя категориями. Два объекта Date на один и тот же момент — одна группа.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue by quarter' },
    subtitle: { text: '$B, three levels on the axis: year → half → quarter' },
    series: [{ type: 'bar', xField: 'period', yField: 'revenue', name: 'Revenue', cornerRadius: 2 }],
    axes: [
      {
        type: 'grouped-category',
        position: 'bottom',
        // the formatter gets the raw level value: the halves arrive as 1 and 2
        groupLabel: { formatter: ({ value, level }) => (level === 0 ? `FY ${String(value)}` : `H${String(value)}`) },
      },
      { type: 'number', position: 'left', label: { format: '$,d' } },
    ],
    legend: { enabled: false },
  };
}
ts
/** Three levels on the axis: year → half (raw number) → quarter. */
export function getData() {
  return [
    { period: ['2023', 1, 'Q1'], revenue: 117 },
    { period: ['2023', 1, 'Q2'], revenue: 94 },
    { period: ['2023', 2, 'Q3'], revenue: 82 },
    { period: ['2023', 2, 'Q4'], revenue: 120 },
    { period: ['2024', 1, 'Q1'], revenue: 126 },
    { period: ['2024', 1, 'Q2'], revenue: 91 },
    { period: ['2024', 2, 'Q3'], revenue: 88 },
    { period: ['2024', 2, 'Q4'], revenue: 134 },
  ];
}

Оформление и формат ярусов групп

У ярусов свой блок опций — groupLabel: свой шрифт, цвет и формат, потому что надпись группы отвечает не на тот вопрос, что деление под ней. Форматтеру приходит сырое значение своего уровня, номер яруса и диапазон делений, которые группа накрывает: у группы нет индекса деления — она стоит над их рядом:

js
axes: [
  {
    type: 'grouped-category',
    groupLabel: {
      fontSize: 12,
      color: '#334155',
      // уровень 0 — самый верхний ярус
      formatter: ({ value, level }) => (level === 0 ? `FY ${value}` : `H${value}`),
    },
  },
];

groupLabel.format — сериализуемая половина того же самого ('%b %Y', ',.0f'), применяется к значению уровня. Без того и другого группа печатается так же, как число на делении, — с сокращением миллионов и тысяч.

label.formatter остаётся за подписями элементов: ему приходит весь кортеж и индекс деления, так что каждый ярус форматируется под свой вопрос. groupLabel: { enabled: false } убирает ярусы совсем — подписи элементов остаются, а ось перестаёт резервировать место под группы.

Подписи всегда влезают

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

Это место делится с зонами осей, а не прибавляется к ним: вылет подписи на 12 px влево ничего не стоит, если ось Y и так занимает там 40 px. Поэтому эффект виден только там, где он нужен: процентная ось, последний тик которой стоит у правого края, или горизонтальные столбцы, у которых подписи значений уходят вслед за столбцами.

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

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

Полярные графики вписываются так же: радиус сетки подбирается так, чтобы названия категорий по кругу остались внутри области, а длинное название с одной стороны сдвигает центр, а не ужимает всю паутину. Там, где спицы встают плотно, подписи, которые налезли бы друг на друга, снимаются — сама сетка остаётся целой (см. Радар). У круговой диаграммы и пончика радиус так же ограничивают выносные подписи.

Подписи, которые не влезают

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

Ось grouped-category прореживает не по оси целиком, а по прогонам: каждый прогон категорий оставляет столько подписей, сколько помещается между его собственными разделителями, и отбирает их от середины к краям. Подпись, которая вылезла бы за свой прогон, скрывается, а не рисуется поверх соседней группы, — и прогон уже собственной подписи остаётся без неё: за него говорит имя группы.

label.overflow: 'ellipsis' выбирает другой размен: на оси остаются все подписи, и каждая обрезается по месту между тиками, а на месте обрезки встаёт label.ellipsis — по умолчанию '..', можно задать '…'. Ничто больше не налезает на соседа и на линию тика между ними.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue by department' },
    subtitle: { text: '$M, long category names cut to the room between the ticks' },
    series: [{ type: 'bar', xField: 'department', yField: 'revenue', name: 'Revenue', cornerRadius: 2 }],
    axes: [
      {
        type: 'grouped-category',
        position: 'bottom',
        // every name stays on the axis: the ones that do not fit are cut instead of dropped
        label: { overflow: 'ellipsis' },
      },
      { type: 'number', position: 'left', label: { format: '$,d' } },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { department: ['Retail', 'Kitchen appliances'], revenue: 48 },
    { department: ['Retail', 'Home entertainment'], revenue: 36 },
    { department: ['Retail', 'Personal computing'], revenue: 29 },
    { department: ['Wholesale', 'Building materials'], revenue: 41 },
    { department: ['Wholesale', 'Industrial fittings'], revenue: 33 },
    { department: ['Wholesale', 'Agricultural supplies'], revenue: 22 },
  ];
}

На оси grouped-category ярусы групп живут по тому же правилу: надпись держат в границах прогона категорий, над которым она стоит, — и она не доходит до разделителей по краям. groupLabel.maxWidth сужает её ещё, знак обрезки берётся из label.ellipsis; имя, от которого обрезка оставила бы один знак, скрывается. Имена групп живут внутри своих прогонов так же, как подписи: имя шире прогона скрывается, а не перешагивает разделитель к соседу.

label.maxWidth — самостоятельное ограничение: действует независимо от тесноты, а на вертикальной оси заодно решает, сколько канвы подписи вправе отнять у графика — обрезанные длинные названия слева перестают двигать область построения вправо.

js
axes: [
  { type: 'category', position: 'bottom', label: { overflow: 'ellipsis' } },
  // у левой оси нет шага, в который надо вписаться: границу задаёт maxWidth
  { type: 'category', position: 'left', label: { maxWidth: 90, ellipsis: '…' } },
];

Подписи внутри графика

label.placement: 'inside' переносит подписи тиков внутрь области построения, и ось не резервирует под них толщину. На вертикальной категорийной оси каждая подпись получает свою строку: строка прибавляется к зазору между бэндами (и одна резервируется над первым), поэтому paddingInner — или gap — продолжает означать ровно то же, что и везде: расстояние между элементами. Внутренние подписи рисуются поверх серий.

У внутренней подписи два своих расстояния, по 4 px: label.insideSpacing — отступ от оси внутрь области построения, и label.insideGap — зазор до своего элемента и до предыдущего (именно он задаёт высоту строки). label.spacing относится только к внешним подписям и сюда не достаёт.

label.insideAlign выбирает, где подпись стоит внутри этой строки: 'element' (по умолчанию) прижимает её к своему элементу на insideGap, 'gap' центрирует между двумя элементами.

Когда строки перестают влезать — много категорий на низкой оси — подписи прореживаются так же, как тесные подписи на горизонтальной оси: остаётся каждая k-я. label.avoidCollisions: false это отключает и рисует все. Под зазор бэнд отдаёт не больше 80 % шага, так что элемент виден всегда.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Traffic by channel' },
    subtitle: { text: 'category labels sit above their bars' },
    series: [
      {
        type: 'bar',
        xField: 'channel',
        yField: 'share',
        name: 'Share',
        direction: 'horizontal',
        cornerRadius: 2,
        label: { enabled: true, placement: 'right', fontWeight: 'bold', formatter: ({ value }) => `${Math.round(value * 100)}%` },
      },
    ],
    axes: [
      {
        type: 'category',
        position: 'left',
        label: { placement: 'inside', fontWeight: 'bold', insideSpacing: 0, insideGap: 4 },
        line: { enabled: false },
      },
      {
        type: 'number',
        position: 'bottom',
        label: { format: '.0%' },
        gridLine: { lineDash: [2, 3] },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { channel: 'Organic search', share: 0.6 },
    { channel: 'Direct', share: 0.2 },
    { channel: 'Referral', share: 0.3 },
    { channel: 'Email', share: 0.1 },
    { channel: 'Social', share: 0.05 },
    { channel: 'Paid ads', share: 0.4 },
  ];
}

У grouped-category уровни расходятся: колонка групп остаётся снаружи и занимает своё место, подписи элементов уходят внутрь и встают над барами, а разделитель групп сдвигается выше подписей.

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Revenue share by channel' },
    subtitle: { text: 'group column outside, item labels inside' },
    series: [
      {
        type: 'bar',
        xField: 'channel',
        yField: 'revenue',
        name: 'Share',
        direction: 'horizontal',
        cornerRadius: 2,
        label: { enabled: true, placement: 'right', fontWeight: 'bold', formatter: ({ value }) => `${Math.round(value * 100)}%` },
      },
    ],
    axes: [
      {
        type: 'grouped-category',
        position: 'left',
        label: { placement: 'inside', fontWeight: 'bold' },
        line: { enabled: false },
      },
      {
        type: 'number',
        position: 'bottom',
        max: 1,
        label: { format: '.0%' },
        gridLine: { lineDash: [2, 3] },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { channel: ['Europe', 'Web'], revenue: 0.48 },
    { channel: ['Europe', 'Retail'], revenue: 0.31 },
    { channel: ['Europe', 'Partners'], revenue: 0.17 },
    { channel: ['Americas', 'Web'], revenue: 0.62 },
    { channel: ['Americas', 'Retail'], revenue: 0.24 },
    { channel: ['Americas', 'Partners'], revenue: 0.09 },
  ];
}

На горизонтальной оси подписи идут вдоль внутреннего края области построения, а на оси значений — над своей линией сетки.

CrossLines

Опорные линии и диапазоны в координатах оси — с подписями:

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Latency p95' },
    series: [{ type: 'line', xField: 'month', yField: 'latency', name: 'p95, ms' }],
    axes: [
      {
        type: 'category',
        position: 'bottom',
        crossLines: [{ type: 'range', range: ['Apr', 'May'], label: { text: 'incident' } }],
      },
      {
        type: 'number',
        position: 'left',
        crossLines: [{ value: 200, stroke: '#e5484d', label: { text: 'SLO 200 ms', color: '#e5484d' } }],
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', latency: 182 },
    { month: 'Feb', latency: 174 },
    { month: 'Mar', latency: 196 },
    { month: 'Apr', latency: 230 },
    { month: 'May', latency: 218 },
    { month: 'Jun', latency: 187 },
    { month: 'Jul', latency: 171 },
    { month: 'Aug', latency: 165 },
  ];
}

Полярные оси

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

js
axes: {
  angle: { title: { text: 'Месяц' }, gridLine: { lineDash: [3, 3] }, line: { stroke: '#64748b' } },
  radius: { title: { text: 'Инциденты' }, min: 0, max: 60, ringCount: 3, label: { format: ',.0f' } },
},
ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Polar axes' },
    series: [{ type: 'nightingale', angleField: 'month', radiusField: 'incidents', name: 'Incidents', fillOpacity: 0.7 }],
    axes: {
      angle: {
        title: { text: 'Month' },
        gridLine: { lineDash: [3, 3], opacity: 0.5 },
        line: { stroke: '#64748b' },
      },
      radius: {
        title: { text: 'Incidents' },
        min: 0,
        max: 60,
        ringCount: 3,
        label: { format: ',.0f', fontWeight: 'bold' },
      },
    },
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', incidents: 14 },
    { month: 'Feb', incidents: 11 },
    { month: 'Mar', incidents: 17 },
    { month: 'Apr', incidents: 9 },
    { month: 'May', incidents: 13 },
    { month: 'Jun', incidents: 21 },
    { month: 'Jul', incidents: 18 },
    { month: 'Aug', incidents: 12 },
  ];
}

Сетка внутри паутины и линии осей вокруг неё — разные вещи, и настраиваются они по-разному. angle.gridLine — спицы, radius.gridLine — кольца: это фон, и выглядят они как сетка в теме — пунктиром, приглушённо, позади данных. Линии осей — это angle.line, обод, замыкающий паутину, и radius.line, вертикаль, вдоль которой читаются значения колец: у каждой свои цвет, толщина и штрих, обе сплошные и обе рисуются по умолчанию — там, где тема вообще рисует линии осей. Внешнее кольцо уступает ободу, так что по одной окружности они не проходят. Пустой lineDash делает линию сетки сплошной там, где тема ставит пунктир, а enabled: false убирает любую из четырёх линий.

Шкала значений подписана от центра наружу, вместе с самим центром: там шкала и начинается — нулём или порогом min, если он задан. Подписи принимают format или formatter, а заголовки стоят снаружи графика: заголовок категорий — под ним, заголовок значений — вдоль левого края. Место под них отнимается до подгонки сетки, поэтому заголовок никогда не перекрывает подпись.

Радиальные столбцы переворачивают раскладку — их категории становятся кольцами, а значения спицами, — но опции следуют смыслу, а не форме: angle по-прежнему про категории, radius — про значения. Столбцы проходят только часть круга, поэтому обод там — решение автора: его рисует angle.line: { enabled: true }; radius.line же — та линия, от которой столбцы растут, и она на месте, как и везде.

ОпцияТипПо умолчаниюОписание
angle.gridLineenabled, stroke, width, lineDash, opacityтема, пунктирспицы
angle.lineenabled, stroke, width, lineDashвкл. (обод)обод вокруг паутины
angle.labelenabled, шрифт, format, formatterвкл.названия категорий
angle.titleenabled, text, шрифтзаголовок под графиком
radius.gridLineкак вышетема, пунктиркольца
radius.lineкак вышевкл.вертикаль, вдоль которой читаются значения
radius.labelenabled, шрифт, format, formatterвкл.значения колец
radius.titleenabled, text, шрифтзаголовок вдоль левого края
radius.min / maxnumberпо даннымграницы шкалы значений
radius.nicebooleantrueокруглять границы до целых шагов
radius.ringCountnumber4сколько колец у шкалы значений

Все опции разом

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

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

/** Every polar axis option on a rose, each one set away from its default. */
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Every axis option — rose' },
    series: [{ type: 'nightingale', angleField: 'month', radiusField: 'incidents', name: 'Incidents', fillOpacity: 0.55 }],
    legend: { enabled: false },
    axes: {
      angle: {
        // the spokes
        gridLine: { enabled: true, stroke: '#0ea5e9', width: 1, lineDash: [1, 3], opacity: 0.7 },
        // the rim around the web
        line: { enabled: true, stroke: '#0f172a', width: 2, lineDash: [8, 3] },
        // the category names
        label: {
          enabled: true,
          fontSize: 12,
          fontFamily: 'system-ui, sans-serif',
          fontWeight: 'bold',
          color: '#0f172a',
          formatter: ({ value }) => String(value).toUpperCase(),
        },
        title: { enabled: true, text: 'Month', fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 'normal', color: '#0ea5e9' },
      },
      radius: {
        // bounds of the value scale, taken from the options rather than the data
        min: 0,
        max: 45,
        nice: false,
        ringCount: 3,
        // the rings
        gridLine: { enabled: true, stroke: '#f43f5e', width: 1, lineDash: [], opacity: 0.25 },
        // the vertical the ring values are read along
        line: { enabled: true, stroke: '#f43f5e', width: 2, lineDash: [4, 2] },
        // the ring values
        label: {
          enabled: true,
          fontSize: 11,
          fontFamily: 'system-ui, sans-serif',
          fontWeight: 'bold',
          color: '#f43f5e',
          format: ',.0f',
        },
        title: {
          enabled: true,
          text: 'Incidents',
          fontSize: 14,
          fontFamily: 'system-ui, sans-serif',
          fontWeight: 'normal',
          color: '#f43f5e',
        },
      },
    },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', incidents: 12 },
    { month: 'Feb', incidents: 7 },
    { month: 'Mar', incidents: 19 },
    { month: 'Apr', incidents: 25 },
    { month: 'May', incidents: 34 },
    { month: 'Jun', incidents: 41 },
    { month: 'Jul', incidents: 28 },
    { month: 'Aug', incidents: 16 },
  ];
}
ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

/** Every polar axis option on a radar, each one set away from its default. */
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Every axis option — radar' },
    series: [{ type: 'radar-area', angleField: 'skill', radiusField: 'score', name: 'Score', fillOpacity: 0.15 }],
    legend: { enabled: false },
    axes: {
      angle: {
        // the spokes
        gridLine: { enabled: true, stroke: '#0ea5e9', width: 1, lineDash: [1, 3], opacity: 0.7 },
        // the rim around the web
        line: { enabled: true, stroke: '#0f172a', width: 2, lineDash: [8, 3] },
        // the category names
        label: {
          enabled: true,
          fontSize: 12,
          fontFamily: 'system-ui, sans-serif',
          fontWeight: 'bold',
          color: '#0f172a',
          formatter: ({ value, index }) => `${index + 1}. ${String(value)}`,
        },
        title: { enabled: true, text: 'Skill', fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 'normal', color: '#0ea5e9' },
      },
      radius: {
        // bounds of the value scale, taken from the options rather than the data
        min: 0,
        max: 12,
        nice: false,
        ringCount: 6,
        // the rings
        gridLine: { enabled: true, stroke: '#f43f5e', width: 1, lineDash: [], opacity: 0.25 },
        // the vertical the ring values are read along
        line: { enabled: true, stroke: '#f43f5e', width: 2, lineDash: [4, 2] },
        // the ring values
        label: {
          enabled: true,
          fontSize: 11,
          fontFamily: 'system-ui, sans-serif',
          fontWeight: 'bold',
          color: '#f43f5e',
          format: ',.1f',
        },
        title: { enabled: true, text: 'Score', fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 'normal', color: '#f43f5e' },
      },
    },
  };
}
ts
export function getData() {
  return [
    { skill: 'Speed', score: 7.5 },
    { skill: 'Quality', score: 8.2 },
    { skill: 'Reliability', score: 6.4 },
    { skill: 'Support', score: 5.1 },
    { skill: 'Price', score: 8.8 },
    { skill: 'Docs', score: 4.6 },
  ];
}

Что стоит вычитать из этой пары: nice: false оставляет max ровно там, куда его поставили, поэтому внешнее кольцо у розы не доходит до обода, а у радара ложится на него — и там, где ложится, линию оставляет за собой обод, а кольцо уступает. format на одной оси и formatter на другой: там, где заданы оба, побеждает formatter.

Опции оси

БлокОпции
number/logmin, max, nice, base (log), bandSpan (число)
timemin, max (Date/timestamp), bandSpan (мс)
categorypaddingInner, paddingOuter
полярные angle / radiusсм. Полярные оси; у radius ещё min, max, nice, ringCount

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

ОпцияТипПо умолчаниюОписание
type'number' | 'category' | 'time' | 'log' | 'ordinal-time' | 'grouped-category'по сериямтип оси
position'bottom' | 'left' | 'top' | 'right'по типусторона оси
title.enabledbooleantrue при textзаголовок оси
title.textstringтекст заголовка
title.fontSizePixels12шрифт заголовка
title.colorColorValueforegroundцвет заголовка
line.enabledbooleanтолько ось категорийлиния оси
line.strokeColorValueaxis темы (светло-серый)цвет линии
line.widthPixels1толщина линии
line.lineDashPixels[]сплошнаяпунктир линии оси ([] — принудительно сплошная)
tick.enabledbooleanfalseтики
tick.sizePixels6длина тика
tick.widthPixels1толщина тика
tick.strokeColorValueaxis темы (светло-серый)цвет тика (tick.color — алиас)
tick.lineDashPixels[]сплошнойпунктир тика
label.enabledbooleantrueподписи делений
label.fontSizePixels11шрифт подписей
label.fontFamilystringшрифт темыгарнитура
label.colorColorValuemuted темыцвет подписей
label.spacingPixels8внешние подписи: отступ от тика или оси
label.insideSpacingPixels4внутренние подписи: отступ от оси
label.insideAlign'element' | 'gap''element'внутренние подписи: прижать к элементу или центрировать
label.insideGapPixels4внутренние подписи: зазор до элемента
label.placement'outside' | 'inside''outside'подписи у оси или внутри области построения
label.formatstringformat-строка (',.2f', '.0%', '%d %b')
label.formatter({ value, index }) => stringпрограммный формат
label.avoidCollisionsbooleantrueпропуск пересекающихся подписей
label.overflow'thin' | 'ellipsis''thin'тесные подписи: прореживать или оставить все и обрезать
label.maxWidthPixelsнаибольшая ширина подписи; длиннее — обрезается
label.ellipsisstring'..'знак на месте обрезки
gridLine.enabledbooleanтолько ось значенийлинии сетки
gridLine.strokeColorValueaxis темы (светло-серый)цвет сетки
gridLine.widthPixels1толщина
gridLine.lineDashPixels[][4, 4]пунктир сетки
interval.valuesunknown[]автоявные значения тиков
interval.minSpacingPixels8мин. расстояние подписей
crossLines[].type'line' | 'range''line'линия или диапазон
crossLines[].valueзначениекоордината линии
crossLines[].range[от, до]диапазон заливки
crossLines[].strokeColorValuemuted темыцвет линии
crossLines[].strokeWidthPixels1толщина линии
crossLines[].lineDashPixels[]пунктир
crossLines[].fillColorValuemuted темызаливка диапазона
crossLines[].fillOpacityFraction0.12прозрачность заливки
crossLines[].label.textstringтекст подписи
crossLines[].label.colorColorValuemuted темыцвет подписи
crossLines[].label.fontSizePixels11шрифт подписи
min (number, log)numberдомен данныхнижняя граница
max (number, log)numberдомен данныхверхняя граница
nice (number)booleantrueокругление домена до «красивых» границ
base (log)number10основание логарифма
paddingInner (category, ordinal-time, grouped-category)Fraction0.2 (ordinal-time 0.25)зазор между элементами, доля шага
gap (category, ordinal-time, grouped-category)Pixelsзазор между элементами в px; главнее paddingInner
paddingOuter (category, ordinal-time, grouped-category)Fraction0.1внешний band-отступ
bandSpan (time, number)numberшаг данныхширина столбца в единицах оси (мс на временной оси)
groupSpacing (grouped-category)Pixels8отступ между подписями элементов и ярусом групп, и между ярусами
groupLabel.enabled (grouped-category)booleantrueярусы с надписями групп
groupLabel.fontSize (grouped-category)Pixels11размер шрифта надписи группы
groupLabel.fontFamily (grouped-category)stringшрифт темыгарнитура
groupLabel.fontWeight (grouped-category)FontWeight'bold'начертание надписи группы
groupLabel.color (grouped-category)ColorValueforegroundцвет надписи группы
groupLabel.format (grouped-category)stringстрока формата для значения уровня
groupLabel.formatter (grouped-category)({ value, level, start, end }) => stringпрограммное форматирование надписи группы
groupLabel.maxWidth (grouped-category)Pixelsширина прогона группынаибольшая ширина надписи группы

Подписи горизонтальных осей автоматически прореживаются при тесноте (label.avoidCollisions: false отключает) — либо обрезаются, см. Подписи, которые не влезают.

Overlays

Состояния «нет данных» и «загрузка» включены по умолчанию: пустой data показывает overlays.noData.text, а loading: trueoverlays.loading.text.