GrChartBar
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- how much every category has — orders by region, errors by service, revenue by product: columns are compared by eye more precisely than in any other type;
- two or three metrics per category side by side — the series stand as a group, and
groupPaddingseparates the groups; - the composition of every category —
stackedfolds the series into one column; - the shares inside a category —
stacked: '100%'normalises the column to one, and the proportions are compared rather than the quantities; - long names of categories —
orientation: 'horizontal'lays the bars sideways, and a label is read as a line rather than as a slanted tail; - there are few categories — the vertical layout holds fifteen or twenty, beyond that the labels of the axis run into one another; the horizontal one counts height rather than width and calmly takes thirty or forty rows.
When to take something else
| Need | Take |
|---|---|
| Show the course of a value over time | GrChartLine |
| Show the whole and the contribution of its parts over time | GrChartArea |
| Show the composition of one whole | GrChartPie |
| Compare the shape of a profile across several axes | GrChartRadar |
| Compare the rows of a table with one another | GrDataTable + GrSparkline |
Hundreds of categories are not a chart but a list: the room runs out before the reader’s patience does (in the vertical layout it is the width, in the horizontal one the height of the page). Such data is shown as a table with sorting, and the top of it with columns.
Horizontal: bars sideways, the axes named by the data
<GrChartBar :series="series" orientation="horizontal" aria-label="Revenue by product" />
Only the layout changes: the stack, the group, '100%', the references, the legend, the tooltip and
the hidden table work in exactly the same way.
The axes are named by the data rather than by the screen. yDomain, yTickFormat and
yTickCount are always the value axis, wherever it lies; xTickFormat is always the labels of the
categories. With showGrid: 'y' the value lines in the horizontal layout run vertically: the grid
follows its own axis rather than the direction on the screen.
The alternative — introducing paired horizontalTickFormat and the rest — would give two props with
one meaning and the question “and which of them is in charge right now” on every page. One imprecise
name is cheaper than two precise ones.
The keyboard follows the eyes: ↓/↑ walk the categories from top to bottom, and ←/→ switch
the series being read inside a category.
What the horizontal layout cannot do
dualAxis with orientation: 'horizontal' is switched off: the second value axis would become the
top one, and the layout has no top margin. The prop does not fail — it simply does not apply, all of
the series sit on one axis, and a warning is printed in dev. If two scales are needed, take the
vertical layout.
Zero on the axis is not switchable
There is nothing to switch off in includeZero for columns: a bar is drawn from the baseline, and if
the base is not zero, the length of the bar stops being the quantity. In a line chart there is a
choice — there the course is read rather than the length.
Dimming instead of hiding
dimInactive dims the inactive series under the cursor instead of removing them. A removed series
would shift its neighbours and change the width of the bars — the drawing would “move” from a single
movement of the mouse.
A threshold is drawn as a reference, not as a series
A plan, a norm and the limit of the acceptable are the references prop rather than a series made of a constant: a
constant series would enter the legend, would stretch the axis and would travel into the table as data. The details —
../model.md, the section “A reference is not a series”.
A stack and a group are different questions
A group answers “how much each has”, a stack “what each consists of”. Mixing them in one chart is not allowed: the reader does not know whether to compare the heights of the columns or the heights of the segments. If both answers are needed, that is two charts.
Two axes are a deliberate decision
Series of different orders (money and units) are readable on one chart only with two axes, and two
axes let any pair be fitted to a visible correlation. That is why the axis: 'right' of a series
does not work until dualAxis is switched on. The invariants —
../model.md, the section “The second value axis”.
Install
npm i @feugene/granularity-chartsImport
import { GrChartBar } from '@feugene/granularity-charts/components/GrChartBar'API
The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.
Examples 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>Accessibility
- APG pattern
Серия читается той же парой стрелок; активная категория подсвечивается приглушением соседей