GrChartRadar

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

Берут, когда форма профиля по нескольким метрикам.

Когда брать

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

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

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

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

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>

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