GrChartRadar
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the shape of a profile across several metrics — competencies, scores by criterion, the characteristics of a model: the silhouette as a whole matters rather than an individual value;
- a comparison of two or three profiles — “before and after”, “us and a competitor”, “a candidate and a vacancy”: overlaid contours are compared at a glance;
- there are three to eight axes — with fewer than three no figure comes out, and with more than ten the spokes merge and the labels run into one another;
- the metrics are heterogeneous —
axisScale: 'per-axis'gives every axis a domain of its own, and only the shape is compared.
When to take something else
| Need | Take |
|---|---|
| Compare particular quantities with one another | GrChartBar |
| Show what a whole consists of | GrChartPie |
| Show the course of a value over time | GrChartLine |
| Show one value with a label | GrStatistic |
Exact values are not read off a radar: the area grows as the square of the radius, and the eye systematically exaggerates the difference. If quantities are needed, take columns, and leave the radar where the question really is about shape.
Two radius scales — and both have a price
| Mode | The domain | What is paid |
|---|---|---|
shared (the default) | one for all of the axes | heterogeneous metrics cannot be brought together: the largest will take the whole scale |
per-axis | one of its own per axis | the areas of the figures cannot be compared — only the shape is |
The share lives only in the computation of the radius. The value of a point stays the original one in every mode: were the normalisation to leak into the data, the tooltip and the hidden table would start showing percentages instead of quantities.
The axes are aligned before the normalisation
The number of spokes has to be constant. In Cartesian charts the positions are computed from the visible series, and that is right; in polar ones it breaks the drawing — hide with the legend the only series that knew about an axis, and the web will turn. Every series is therefore padded with gaps up to the shared set of axes, and a gap holds its position.
A break tears the contour and removes the fill of that series entirely: closing the contour through a gap means drawing an edge that is not there, and filling a torn contour an area that is not there.
Install
npm i @feugene/granularity-chartsImport
import { GrChartRadar } from '@feugene/granularity-charts/components/GrChartRadar'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">
// `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>