Оси
Типы осей: 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: [] } — сплошная сетка вдоль категорий.
Линия, тики и сетка
У линии оси те же три опции стиля, что и у сетки — цвет, толщина и пунктир, а у тиков рядом настраиваются длина и цвет:
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 на каждой оси скажет, какие серии она несёт:
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, %' } },
],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, %' } },
],
};
}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-строку; тики встают на календарные границы, формат подписи зависит от шага (часы → дни → месяцы → годы).
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 },
};
}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 так же, как на полосную, — разница в том, что ось расставляет их по настоящему расстоянию, и месяц без данных оставляет пустое место, а не исчезает:
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' },
};
}/** 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 перебивает это значение и задаётся в единицах оси — миллисекундах для временной, поэтому при зуме столбец продолжает накрывать свой период, чего ширина в пикселях не даёт:
axes: [
// часовые замеры с редкими пропусками: столбец шириной в час, что бы ни говорили пропуски
{ type: 'time', position: 'bottom', bandSpan: 60 * 60 * 1000 },
{ type: 'number', position: 'left' },
]Та же опция есть у оси number, в её собственных единицах. По одной точке шаг не вывести — такой столбец получает десятую часть ширины области построения.
Когда даты стоят ровно — торговые сессии, недели без выходных — второй ответ даёт ось ordinal-time: полосы равной ширины с календарными подписями, то есть пропуски закрываются и расстояние ни на что не влияет.
Логарифмическая ось
log — для данных, растущих на порядки; тики на степенях base (по умолчанию 10).
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 },
};
}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: значения данных — массивы [группа, элемент] (уровней может быть и больше); под подписями элементов появляется строка групп с разделителями:
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' },
],
};
}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 },
];
}В горизонтальных чартах ось категорий вертикальная, и колонка групп с разделителями появляется слева от подписей элементов:
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' },
],
};
}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 на один и тот же момент — одна группа.
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 },
};
}/** 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: свой шрифт, цвет и формат, потому что надпись группы отвечает не на тот вопрос, что деление под ней. Форматтеру приходит сырое значение своего уровня, номер яруса и диапазон делений, которые группа накрывает: у группы нет индекса деления — она стоит над их рядом:
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, заголовка с подзаголовком и легенды, — так что ваши отступы остаются вашими, подписи в них не залезают.
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 },
];
}Полярные графики вписываются так же: радиус сетки подбирается так, чтобы названия категорий по кругу остались внутри области, а длинное название с одной стороны сдвигает центр, а не ужимает всю паутину. Там, где спицы встают плотно, подписи, которые налезли бы друг на друга, снимаются — сама сетка остаётся целой (см. Радар). У круговой диаграммы и пончика радиус так же ограничивают выносные подписи.
Подписи, которые не влезают
На горизонтальной оси у подписи ровно один шаг места. Когда названия длиннее, по умолчанию они прореживаются: остаётся каждая вторая — или каждая третья, — и та рисуется целиком. Для дат и чисел это читается хорошо: пропущенные значения достраиваются в уме. Для категорий — плохо: пропущенное название значит пропущенную категорию.
Ось grouped-category прореживает не по оси целиком, а по прогонам: каждый прогон категорий оставляет столько подписей, сколько помещается между его собственными разделителями, и отбирает их от середины к краям. Подпись, которая вылезла бы за свой прогон, скрывается, а не рисуется поверх соседней группы, — и прогон уже собственной подписи остаётся без неё: за него говорит имя группы.
label.overflow: 'ellipsis' выбирает другой размен: на оси остаются все подписи, и каждая обрезается по месту между тиками, а на месте обрезки встаёт label.ellipsis — по умолчанию '..', можно задать '…'. Ничто больше не налезает на соседа и на линию тика между ними.
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 },
};
}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 — самостоятельное ограничение: действует независимо от тесноты, а на вертикальной оси заодно решает, сколько канвы подписи вправе отнять у графика — обрезанные длинные названия слева перестают двигать область построения вправо.
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 % шага, так что элемент виден всегда.
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 },
};
}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 уровни расходятся: колонка групп остаётся снаружи и занимает своё место, подписи элементов уходят внутрь и встают над барами, а разделитель групп сдвигается выше подписей.
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 },
};
}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
Опорные линии и диапазоны в координатах оси — с подписями:
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 },
};
}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 — кольца значений:
axes: {
angle: { title: { text: 'Месяц' }, gridLine: { lineDash: [3, 3] }, line: { stroke: '#64748b' } },
radius: { title: { text: 'Инциденты' }, min: 0, max: 60, ringCount: 3, label: { format: ',.0f' } },
},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 },
};
}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.gridLine | enabled, stroke, width, lineDash, opacity | тема, пунктир | спицы |
angle.line | enabled, stroke, width, lineDash | вкл. (обод) | обод вокруг паутины |
angle.label | enabled, шрифт, format, formatter | вкл. | названия категорий |
angle.title | enabled, text, шрифт | — | заголовок под графиком |
radius.gridLine | как выше | тема, пунктир | кольца |
radius.line | как выше | вкл. | вертикаль, вдоль которой читаются значения |
radius.label | enabled, шрифт, format, formatter | вкл. | значения колец |
radius.title | enabled, text, шрифт | — | заголовок вдоль левого края |
radius.min / max | number | по данным | границы шкалы значений |
radius.nice | boolean | true | округлять границы до целых шагов |
radius.ringCount | number | 4 | сколько колец у шкалы значений |
Все опции разом
Вся таблица, выписанная целиком, и каждая опция уведена от своего умолчания — один и тот же блок на розе и на радаре. Ничто в нём не про форму паутины: кольца выходят окружностями у одной и многоугольниками у другого из-за серии, а не из-за осей.
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',
},
},
},
};
}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 },
];
}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' },
},
},
};
}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/log | min, max, nice, base (log), bandSpan (число) |
| time | min, max (Date/timestamp), bandSpan (мс) |
| category | paddingInner, paddingOuter |
полярные angle / radius | см. Полярные оси; у radius ещё min, max, nice, ringCount |
Полный список опций
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
type | 'number' | 'category' | 'time' | 'log' | 'ordinal-time' | 'grouped-category' | по сериям | тип оси |
position | 'bottom' | 'left' | 'top' | 'right' | по типу | сторона оси |
title.enabled | boolean | true при text | заголовок оси |
title.text | string | — | текст заголовка |
title.fontSize | Pixels | 12 | шрифт заголовка |
title.color | ColorValue | foreground | цвет заголовка |
line.enabled | boolean | только ось категорий | линия оси |
line.stroke | ColorValue | axis темы (светло-серый) | цвет линии |
line.width | Pixels | 1 | толщина линии |
line.lineDash | Pixels[] | сплошная | пунктир линии оси ([] — принудительно сплошная) |
tick.enabled | boolean | false | тики |
tick.size | Pixels | 6 | длина тика |
tick.width | Pixels | 1 | толщина тика |
tick.stroke | ColorValue | axis темы (светло-серый) | цвет тика (tick.color — алиас) |
tick.lineDash | Pixels[] | сплошной | пунктир тика |
label.enabled | boolean | true | подписи делений |
label.fontSize | Pixels | 11 | шрифт подписей |
label.fontFamily | string | шрифт темы | гарнитура |
label.color | ColorValue | muted темы | цвет подписей |
label.spacing | Pixels | 8 | внешние подписи: отступ от тика или оси |
label.insideSpacing | Pixels | 4 | внутренние подписи: отступ от оси |
label.insideAlign | 'element' | 'gap' | 'element' | внутренние подписи: прижать к элементу или центрировать |
label.insideGap | Pixels | 4 | внутренние подписи: зазор до элемента |
label.placement | 'outside' | 'inside' | 'outside' | подписи у оси или внутри области построения |
label.format | string | — | format-строка (',.2f', '.0%', '%d %b') |
label.formatter | ({ value, index }) => string | — | программный формат |
label.avoidCollisions | boolean | true | пропуск пересекающихся подписей |
label.overflow | 'thin' | 'ellipsis' | 'thin' | тесные подписи: прореживать или оставить все и обрезать |
label.maxWidth | Pixels | — | наибольшая ширина подписи; длиннее — обрезается |
label.ellipsis | string | '..' | знак на месте обрезки |
gridLine.enabled | boolean | только ось значений | линии сетки |
gridLine.stroke | ColorValue | axis темы (светло-серый) | цвет сетки |
gridLine.width | Pixels | 1 | толщина |
gridLine.lineDash | Pixels[] | [4, 4] | пунктир сетки |
interval.values | unknown[] | авто | явные значения тиков |
interval.minSpacing | Pixels | 8 | мин. расстояние подписей |
crossLines[].type | 'line' | 'range' | 'line' | линия или диапазон |
crossLines[].value | значение | — | координата линии |
crossLines[].range | [от, до] | — | диапазон заливки |
crossLines[].stroke | ColorValue | muted темы | цвет линии |
crossLines[].strokeWidth | Pixels | 1 | толщина линии |
crossLines[].lineDash | Pixels[] | — | пунктир |
crossLines[].fill | ColorValue | muted темы | заливка диапазона |
crossLines[].fillOpacity | Fraction | 0.12 | прозрачность заливки |
crossLines[].label.text | string | — | текст подписи |
crossLines[].label.color | ColorValue | muted темы | цвет подписи |
crossLines[].label.fontSize | Pixels | 11 | шрифт подписи |
min (number, log) | number | домен данных | нижняя граница |
max (number, log) | number | домен данных | верхняя граница |
nice (number) | boolean | true | округление домена до «красивых» границ |
base (log) | number | 10 | основание логарифма |
paddingInner (category, ordinal-time, grouped-category) | Fraction | 0.2 (ordinal-time 0.25) | зазор между элементами, доля шага |
gap (category, ordinal-time, grouped-category) | Pixels | — | зазор между элементами в px; главнее paddingInner |
paddingOuter (category, ordinal-time, grouped-category) | Fraction | 0.1 | внешний band-отступ |
bandSpan (time, number) | number | шаг данных | ширина столбца в единицах оси (мс на временной оси) |
groupSpacing (grouped-category) | Pixels | 8 | отступ между подписями элементов и ярусом групп, и между ярусами |
groupLabel.enabled (grouped-category) | boolean | true | ярусы с надписями групп |
groupLabel.fontSize (grouped-category) | Pixels | 11 | размер шрифта надписи группы |
groupLabel.fontFamily (grouped-category) | string | шрифт темы | гарнитура |
groupLabel.fontWeight (grouped-category) | FontWeight | 'bold' | начертание надписи группы |
groupLabel.color (grouped-category) | ColorValue | foreground | цвет надписи группы |
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: true — overlays.loading.text.