GrChartBar

Пакет: @feugene/granularity-chartsспутникГруппа: Прочее

Берут, когда сколько у каждой категории.

Когда брать

  • сколько у каждой категории — заказы по регионам, ошибки по сервисам, выручка по товарам: столбцы сравниваются глазом точнее любого другого типа;
  • две-три метрики на категорию рядом — серии встают группой, 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

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

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

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

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
Серия читается той же парой стрелок; активная категория подсвечивается приглушением соседей
Клавиши
категория

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты