GrChartRadar
Берут, когда форма профиля по нескольким метрикам.
Когда брать
- форма профиля по нескольким метрикам — компетенции, оценки по критериям, характеристики модели: важен силуэт целиком, а не отдельное значение;
- сравнение двух-трёх профилей — «было и стало», «мы и конкурент», «кандидат и вакансия»: наложенные контуры сравниваются одним взглядом;
- осей от трёх до восьми — меньше трёх фигуры не получается, больше десяти спицы сливаются и подписи налезают;
- метрики разнородные —
axisScale: 'per-axis'даёт каждой оси свой домен, и сравнивается только форма.
Когда взять другое
| Нужно | Берите |
|---|---|
| Сравнить конкретные величины между собой | GrChartBar |
| Показать, из чего состоит целое | GrChartPie |
| Показать ход значения во времени | GrChartLine |
| Показать одно значение с подписью | GrStatistic |
Точные значения по радару не читаются: площадь растёт как квадрат радиуса, и глаз систематически преувеличивает разницу. Нужны величины — берите столбцы, а радар оставьте там, где вопрос и правда про форму.
Две шкалы радиуса — и обе с ценой
| Режим | Домен | Чем платим |
|---|---|---|
shared (по умолчанию) | один на все оси | разнородные метрики свести нельзя: та, что крупнее, займёт шкалу целиком |
per-axis | свой на каждую ось | площади фигур сравнивать нельзя — сравнивается только форма |
Доля живёт только в расчёте радиуса. Собственное значение точки остаётся исходным во всех режимах: протеки нормировка в данные — тултип и скрытая таблица начали бы показывать проценты вместо величин.
Оси выравниваются до нормализации
Число спиц обязано быть постоянным. У декартовых графиков позиции считаются по видимым сериям, и это верно; на полярных ломает рисунок — спрячь легендой единственную серию, знавшую про ось, и паутина провернётся. Поэтому каждая серия дополняется пропусками до общего набора осей, а пропуск позицию удерживает.
Разрыв рвёт контур и снимает заливку этой серии целиком: замкнуть контур через пропуск значит нарисовать ребро, которого нет, а залить рваный контур — площадь, которой нет.
Установка
npm i @feugene/granularity-chartsИмпорт
import { GrChartRadar } from '@feugene/granularity-charts/components/GrChartRadar'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 4
Basic
<script setup lang="ts">
// `GrChartRadar` подставляется авто-импортом (`unplugin-vue-components`).
/**
* Радар отвечает на вопрос, которого не закрывают ни столбцы, ни круг: не
* «сколько» и не «из чего», а **какой формы**. Два профиля на одной паутине
* сравниваются одним взглядом — там, где две таблицы пришлось бы читать.
*/
const axes = ['Скорость', 'Цена', 'Поддержка', 'Надёжность', 'Документация']
const series = [
{ id: 'ours', label: 'Наш продукт', x: axes, y: [8, 6, 9, 8, 7] },
{ id: 'rival', label: 'Конкурент', x: axes, y: [6, 9, 5, 7, 4] },
]
</script>
<template>
<div class="grid gap-3">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Оценка по десятибалльной шкале
</span>
<GrChartRadar
:series="series"
:height="300"
show-legend
aria-label="Сравнение продуктов по пяти критериям"
/>
</div>
</template>Gaps
<script setup lang="ts">
/**
* Пропуск на радаре — не ноль и не «соединить по прямой».
*
* Замкнуть контур через него значит нарисовать ребро, которого нет; залить
* рваный контур — площадь, которой нет. Поэтому кольцо рвётся, а заливка у
* этой серии снимается целиком: полупустая паутина честнее ложной.
*/
const axes = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь']
const series = [
{ id: 'full', label: 'Отдел А', x: axes, y: [7, 8, 6, 9, 7, 8] },
// Два месяца опрос не проводили — значений за них не существует.
{ id: 'partial', label: 'Отдел Б', x: axes, y: [6, null, null, 8, 9, 7] },
]
</script>
<template>
<div class="grid gap-3">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Индекс вовлечённости по месяцам
</span>
<GrChartRadar
:series="series"
:height="300"
show-legend
aria-label="Индекс вовлечённости по отделам"
/>
<p class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
У отдела Б два месяца без опроса. Контур там <strong>разорван</strong>, а заливка снята — иначе график
показал бы объём за время, которого не измеряли. В таблице данных на этих осях стоит «нет значения»,
а стрелки по ним всё равно проходят: значение отсутствует, ось существует.
</p>
</div>
</template>Per Axis
<script setup lang="ts">
import { computed, ref } from 'vue'
/**
* Разнородные метрики на одной паутине.
*
* Выручка в миллионах, NPS в баллах и число сделок общей шкалы не имеют: на
* ней всё, кроме сделок, схлопнется в точку у центра.
* `axis-scale="per-axis"` нормирует каждую спицу своим максимумом — и это
* единственный способ показать такой набор, но за него платят: **площади фигур
* сравнивать больше нельзя**, сравнивается только форма.
*/
/**
* Все метрики здесь — «больше значит лучше», и это не случайность.
*
* Радар читают по площади и по вылету луча: ось, где меньше значит лучше
* (время отклика, отток), на нём читается наоборот — худший результат вылетает
* дальше всех. Такую метрику либо переворачивают до передачи (100 − отток),
* либо не ставят на паутину вовсе.
*/
const axes = ['Выручка, млн', 'NPS', 'Конверсия, %', 'Удержание, %', 'Сделок']
const series = [
{ id: 'q3', label: 'III квартал', x: axes, y: [4.2, 62, 3.1, 78, 240] },
{ id: 'q4', label: 'IV квартал', x: axes, y: [5.8, 71, 4.4, 83, 310] },
]
/** Известные пределы шкал: без них 71 сел бы на внешнее кольцо и прочитался как максимум. */
const axisMax = { 'NPS': 100, 'Удержание, %': 100 }
const mode = ref<'per-axis' | 'shared'>('per-axis')
const hint = computed(() => (mode.value === 'per-axis'
? 'Каждая спица нормирована своим максимумом. Форма читается, площадь — нет.'
: 'Общая шкала на все оси: сделки заняли её целиком, остальное схлопнулось у центра.'))
</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: 'per-axis', label: 'На ось' }, { value: 'shared', label: 'Общая' }]"
aria-label="Шкала осей"
/>
</div>
<GrChartRadar
:series="series"
:axis-scale="mode"
:axis-max="axisMax"
:height="300"
show-legend
aria-label="Метрики квартала к кварталу"
/>
<p class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
{{ hint }} Подписи колец при нормировке исчезают намеренно: единственного верного числа для кольца там нет —
у каждой оси свой максимум, и он уезжает в её имя. Тултип и скрытая таблица при этом показывают
<strong>исходное значение</strong>, а не долю.
</p>
</div>
</template>Shape
<script setup lang="ts">
import { computed, ref } from 'vue'
/**
* Оформление и плотность.
*
* На четырёх сериях заливки наслаиваются и рисунок мутнеет — тогда её
* выключают, и различителем остаётся контур: у каждой серии свой цвет, своя
* форма марки и своя штриховка.
*/
const axes = [
'Vue',
'React',
'Svelte',
'Angular',
'Solid',
'Qwik',
'Astro',
'Nuxt',
'Next',
'Remix',
'Vite',
'Webpack',
]
const series = [
{ id: 'usage', label: 'Используют', x: axes, y: [82, 74, 41, 38, 22, 14, 31, 46, 52, 19, 88, 44] },
{ id: 'interest', label: 'Хотят попробовать', x: axes, y: [61, 55, 68, 24, 49, 43, 57, 51, 44, 33, 71, 12] },
{ id: 'retention', label: 'Продолжат', x: axes, y: [88, 69, 76, 42, 58, 47, 64, 73, 61, 38, 92, 29] },
]
const shape = ref<'polygon' | 'circle'>('polygon')
const fill = ref(false)
const rings = ref(4)
const hint = computed(() => (fill.value
? 'Три заливки поверх двенадцати осей: рисунок мутнеет, контуры теряются.'
: 'Без заливки различителями остаются цвет, форма марки и штриховка контура.'))
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<span class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
Опрос по фреймворкам, % ответов
</span>
<div class="flex flex-wrap items-center gap-3">
<GrSegmented
v-model="shape"
size="sm"
:options="[{ value: 'polygon', label: 'Многоугольник' }, { value: 'circle', label: 'Окружности' }]"
aria-label="Форма сетки"
/>
<GrSwitch v-model="fill" size="sm">Заливка</GrSwitch>
</div>
</div>
<GrChartRadar
:series="series"
:shape="shape"
:fill="fill"
:rings="rings"
:height="340"
show-legend
aria-label="Опрос по фреймворкам"
/>
<p class="text-[length:var(--gr-control-text-sm)] text-[var(--gr-muted-fg)]">
{{ hint }}
</p>
</div>
</template>