GrChartBar
Берут, когда сколько у каждой категории.
Когда брать
- сколько у каждой категории — заказы по регионам, ошибки по сервисам, выручка по товарам: столбцы сравниваются глазом точнее любого другого типа;
- две-три метрики на категорию рядом — серии встают группой,
groupPaddingразводит группы; - состав каждой категории —
stackedскладывает серии в один столбец; - доли внутри категории —
stacked: '100%'нормирует столбец к единице, и сравниваются пропорции, а не величины; - длинные названия категорий —
orientation: 'horizontal'кладёт полосы вбок, и подпись читается строкой, а не наклонным хвостом; - категорий немного — вертикаль держит полтора-два десятка, дальше подписи оси налезают друг на друга; горизонталь считает не ширину, а высоту, и спокойно берёт три-четыре десятка строк.
Когда взять другое
| Нужно | Берите |
|---|---|
| Показать ход значения во времени | GrChartLine |
| Показать целое и вклад частей во времени | GrChartArea |
| Показать состав одного целого | GrChartPie |
| Сравнить форму профиля по нескольким осям | GrChartRadar |
| Сравнить строки таблицы между собой | GrDataTable + GrSparkline |
Категорий сотни — это не график, а список: место кончается раньше, чем у читателя терпение (у вертикали — ширина, у горизонтали — высота страницы). Такие данные показывают таблицей с сортировкой, а верхушку — столбцами.
Горизонталь: полосы вбок, имена осей по данным
<GrChartBar :series="series" orientation="horizontal" aria-label="Выручка по товарам" />
Меняется только раскладка: стек, группа, '100%', опоры, легенда, тултип и
скрытая таблица работают ровно так же.
Оси называются по данным, а не по экрану. yDomain, yTickFormat,
yTickCount — всегда ось значений, где бы она ни лежала; xTickFormat — всегда
подписи категорий. При showGrid: 'y' линии значений на горизонтали идут
вертикально: сетка следует за своей осью, а не за направлением на экране.
Альтернатива — завести парные horizontalTickFormat и прочее — дала бы два
пропа с одним смыслом и вопрос «а какой из них сейчас главный» на каждой
странице. Одно неточное имя дешевле двух точных.
Клавиатура следует за глазами: ↓/↑ идут по категориям сверху вниз,
←/→ переключают читаемую серию внутри категории.
Чего горизонталь не умеет
dualAxis при orientation: 'horizontal' выключен: вторая ось значений стала
бы верхней, а верхнего поля у раскладки нет. Проп не падает — он просто не
действует, все серии садятся на одну ось, и в dev выводится предупреждение.
Нужны две шкалы — берите вертикаль.
Ноль на оси не отключается
includeZero у столбцов нечего выключать: полоса рисуется от базовой линии, и
если база не ноль, длина полосы перестаёт быть величиной. У линии выбор есть —
там читают ход, а не длину.
Приглушение вместо скрытия
dimInactive гасит неактивные серии под курсором вместо того, чтобы убирать
их. Убранная серия сдвинула бы соседей и изменила ширину полос — рисунок
«поехал» бы от одного движения мыши.
Порог рисуется опорой, а не серией
План, норма и граница допустимого — это проп references, а не ряд из константы: серия-константа попала бы в
легенду, растянула бы ось и уехала бы в таблицу как данные. Разбор — ../model.md, раздел
«Опора — не серия».
Стопка и группа — разные вопросы
Группа отвечает «сколько у каждого», стопка — «из чего состоит каждый». Смешивать их в одном графике нельзя: читатель не знает, сравнивать ему высоты столбцов или высоты сегментов. Нужны оба ответа — это два графика.
Две оси — осознанное решение
Ряды разного порядка (деньги и штуки) читаются на одном графике только с двумя
осями, а две оси позволяют подогнать любую пару под видимую корреляцию. Поэтому
axis: 'right' у серии не работает, пока не включён dualAxis. Инварианты —
../model.md, раздел «Вторая ось значений».
Установка
npm i @feugene/granularity-chartsИмпорт
import { GrChartBar } from '@feugene/granularity-charts/components/GrChartBar'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 4
Basic
<script setup lang="ts">
import { ref } from 'vue'
// `GrChartBar` подставляется авто-импортом (`unplugin-vue-components`).
/**
* Столбцы отвечают на вопрос «сколько», а не «куда движется»: величину
* читают высотой полосы, и потому ось у них **всегда** от нуля. Обрежь её —
* и разница в три процента нарисуется разницей в три раза.
*/
const categories = ['Q1', 'Q2', 'Q3', 'Q4']
const series = [
{ id: 'plan', label: 'План', x: categories, y: [4.2, 4.8, 5.1, 6.0] },
{ id: 'fact', label: 'Факт', x: categories, y: [3.9, 5.2, 4.7, 6.6] },
]
/**
* Наведённая категория остаётся в полном цвете, соседние гаснут. Выключается,
* когда график стоит рядом с таблицей и лишнее движение цвета мешает читать
* соседей — тогда о выделенной категории говорит только тултип.
*/
const dimInactive = ref(true)
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap items-center justify-between gap-4">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
План и факт, млн ₽
</span>
<GrSwitch v-model="dimInactive" size="sm">Гасить остальные при наведении</GrSwitch>
</div>
<GrChartBar
:series="series"
:dim-inactive="dimInactive"
:height="240"
show-legend
aria-label="План и факт по кварталам"
/>
</div>
</template>Horizontal
<script setup lang="ts">
import { computed, ref } from 'vue'
/**
* Длинные названия категорий — единственный настоящий довод за горизонталь.
*
* Переключатель здесь не украшение: у вертикали те же подписи встают наклонным
* хвостом и обрезаются, а у горизонтали читаются строкой. Данные, порог и
* легенда при этом не меняются ни на байт.
*/
const departments = [
'Клиентское обслуживание',
'Разработка платформы',
'Логистика и склад',
'Финансы и отчётность',
'Маркетинг и коммуникации',
'Юридическая поддержка',
]
const series = [
{ id: 'closed', label: 'Закрыто', x: departments, y: [412, 388, 297, 214, 186, 92] },
{ id: 'open', label: 'В работе', x: departments, y: [64, 121, 48, 39, 57, 28] },
]
const orientation = ref<'horizontal' | 'vertical'>('horizontal')
const hint = computed(() => (
orientation.value === 'horizontal'
? 'Подпись читается строкой, и категорий помещается втрое больше: горизонталь тратит высоту страницы, а её всегда можно прокрутить.'
: 'Те же подписи по нижней оси: место кончается раньше названий, и читатель разбирает их по обрезкам.'
))
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap items-baseline justify-between gap-4">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Заявки по отделам за квартал
</span>
<GrSegmented
v-model="orientation"
size="sm"
:options="[
{ value: 'horizontal', label: 'Вбок' },
{ value: 'vertical', label: 'Вверх' },
]"
aria-label="Раскладка столбцов"
/>
</div>
<GrChartBar
:series="series"
:orientation="orientation"
stacked
:height="320"
:references="[{ axis: 'y', value: 350, label: 'План отдела', color: 'var(--gr-warning)' }]"
show-legend
aria-label="Заявки по отделам"
/>
<p class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
{{ hint }} Оси называются по данным, а не по экрану: порог задан как
<code>axis: 'y'</code> в обеих раскладках — это всегда ось значений, и при
горизонтали она рисует <strong>вертикальный</strong> пунктир.
</p>
</div>
</template>Stacked
<script setup lang="ts">
import { computed, ref } from 'vue'
/**
* Три режима одного набора данных — и три разных вопроса.
*
* Рядом сравнивают **сегменты между собой**: у кого больше обращений. Стопка
* показывает **целое и вклад**: сколько всего и из чего. Сто процентов
* показывает **только структуру**: как менялись доли, когда абсолютные числа
* растут у всех сразу и потому ничего не объясняют.
*/
const months = ['Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт']
const series = [
{ id: 'bug', label: 'Баги', x: months, y: [120, 138, 129, 142, 118, 96] },
{ id: 'howto', label: 'Как сделать', x: months, y: [86, 92, 104, 121, 148, 173] },
{ id: 'billing', label: 'Оплата', x: months, y: [40, 44, 39, 52, 61, 74] },
]
const mode = ref<'group' | 'stack' | 'share'>('stack')
const stacked = computed(() => (
mode.value === 'group' ? false : mode.value === 'share' ? '100%' as const : true
))
const hint = computed(() => ({
group: 'Сегменты сравниваются между собой: видно, какой тип обращений крупнее в каждом месяце.',
stack: 'Верх столбца — все обращения за месяц. Высота сегмента — вклад типа.',
share: 'Абсолютные числа убраны: остаётся структура. Видно, как «как сделать» отъедает долю у багов.',
}[mode.value]))
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap items-baseline justify-between gap-4">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Обращения в поддержку
</span>
<GrSegmented
v-model="mode"
size="sm"
:options="[
{ value: 'group', label: 'Рядом' },
{ value: 'stack', label: 'Стопкой' },
{ value: 'share', label: '100%' },
]"
aria-label="Режим столбцов"
/>
</div>
<GrChartBar
:series="series"
:stacked="stacked"
:height="260"
show-legend
aria-label="Обращения в поддержку по типам"
/>
<p class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
{{ hint }} Тултип и скрытая таблица во всех трёх режимах показывают
<strong>исходное число обращений</strong>, а не долю и не сумму под сегментом.
</p>
</div>
</template>Zero
<script setup lang="ts">
/**
* Полоса вниз от нуля — это минус, а не «столбец пониже».
*
* Скругляется только дальний от базовой линии конец, поэтому у отрицательной
* полосы он снизу: столбец остаётся приклеенным к оси, а не висит над ней.
*/
const weeks = ['W40', 'W41', 'W42', 'W43', 'W44', 'W45', 'W46', 'W47']
const series = [{
id: 'delta',
label: 'Изменение к прошлой неделе',
x: weeks,
y: [-320, -140, 90, 210, -60, 340, 520, 410],
}]
</script>
<template>
<div class="grid gap-3">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Прирост активных пользователей, неделя к неделе
</span>
<GrChartBar
:series="series"
:height="220"
aria-label="Прирост активных пользователей по неделям"
/>
</div>
</template>Доступность
- Паттерн APG
Серия читается той же парой стрелок; активная категория подсвечивается приглушением соседей- Клавиши
- категория