GrChartRadar

  • charts
  • charts
  • API · 27 props
import { GrChartRadar } from '@feugene/granularity-charts/components/GrChartRadar'

Overview

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

NeedTake
Compare particular quantities with one anotherGrChartBar
Show what a whole consists ofGrChartPie
Show the course of a value over timeGrChartLine
Show one value with a labelGrStatistic

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.

Examples 4 each with a live demo and its code

  • Three providers, six criteria, one shape each

    Every spoke is a score out of 10, so one shared scale is honest and the areas compare: the shape says NorthPay is the API pick and Globex the coverage pick before any number is read. fill makes each profile a figure, and v-model:hidden-series lets the legend drop a provider from the ranking beside the chart too.

    Payment provider shortlistReview panel scores out of 10 · toggle providers in the legend
    Chart data
    XNorthPayPayLaneGlobex Pay
    Fees695
    Payout speed967
    Coverage7510
    API quality976
    Fraud tools869
    Support687
    1. 1. NorthPay45 / 60
    2. 2. Globex Pay44 / 60
    3. 3. PayLane41 / 60
  • A candidate against the frame of the role

    The requirement is drawn as an outline, not a figure: a series dash and a transparent fill-color turn it into a frame the candidate is read against. Where the filled shape falls inside the frame is where the interview found a gap, and the list beside the chart says by how much.

    Dana Whitfield · Senior backend engineerInterview loop, 2 October · levels 1–5
    Chart data
    XRoleDana Whitfield
    System design45
    Go43
    SQL34
    On-call32
    Mentoring34
    • System design5 / 4
    • Go3 / 4
    • SQL4 / 3
    • On-call2 / 3
    • Mentoring4 / 3
  • Five metrics, five units

    axis-scale="per-axis" normalises each spoke and axis-max names its ceiling: on one shared scale 142 orders would squash a 4.7 rating into the centre. The price is that only the shape compares here, not the area — so the real values stand in the table beside it, as well as in the tooltip and the hidden table.

    Lisbon store against the networkSeptember · each spoke has its own scale
    Chart data
    AxisLisbonNetwork averageAxis maximum
    Orders142118200
    Basket384460
    NPS6148100
    Repeat342950
    Rating4.74.35
    MetricLisbonNetwork
    Orders a day142118
    Average basket, €3844
    NPS6148
    Repeat customers, %3429
    Rating, of 54.74.3
  • A week that closes on itself

    shape="circle" draws rings, which suits a cycle where Sunday meets Monday; start-angle puts Monday at twelve o’clock. v-model:active-index is the spoke under the cursor or the arrow keys, and the panel beside the chart reads it.

    Support week, chats a dayWeekends grew by half since August · hover a day or use the arrow keys
    Chart data
    XSeptemberAugust
    Mon412356
    Tue368342
    Wed351330
    Thu340335
    Fri296301
    Sat164102
    Sun14188

    Hover a day, or focus the chart and use the arrow keys.

API generated from the package · cannot drift

Props

PropTypedefaultDescription
fillboolean | undefinedundefinedFill the polygon of a series; `true` by default. Without the fill only the outline remains — easier to read when there are many series.
heightnumber | undefinedundefinedThe canvas height in pixels; 280 by default. The data, the loading state and the empty state all take up the same height.
widthnumber | undefined640Declared width: the first render starts from it, after that the width is measured.
yDomainreadonly [number | null, number | null] | undefinedundefinedThe bounds of the value axis with `shared`. Zero always stays in the domain.
valueFormatGrChartNumberFormat | undefinedundefinedThe value format in the tooltip, the hidden table and announcements: decimal places and separators.
showPoints"auto" | "never" | "always" | undefinedundefinedVertex markers: `auto` (the default), `always` or `never`.
showLegendboolean | "auto" | undefinedundefinedThe series legend: `auto` (the default) — when there is more than one series; `true` and `false` — always and never.
legendPosition"bottom" | "top" | undefinedundefinedWhere the legend sits: `bottom` (the default) or `top`.
tooltipboolean | undefinedundefinedThe panel with the values under the cursor; `true` by default. `false` hides only the panel: the cursor, the active marks, `update:activeIndex` and `pointHover` keep working while the chart is interactive.
hiddenSeriesv-modelreadonly string[] | undefinedundefinedHidden series by id — `v-model:hiddenSeries`. The axes do not disappear with them.
activeIndexv-modelnumber | null | undefinedundefinedThe cursor — `v-model:activeIndex`. The axis index.
loadingboolean | undefinedfalseLoading: a skeleton of the same height stands in place of the plot area, and the space for the axes is already reserved.
emptyboolean | undefinedundefinedThe empty state, set explicitly. Unset — the chart is empty when there is nothing to draw.
emptyTextstring | undefinedundefinedThe placeholder text of an empty chart. Unset — "No data" from the package translations.

Slots

SlotTypeDescription
tooltip{ active: GrChartActivePoint; formatValue: (value: number | null) => string; }Custom content for the tooltip panel instead of the standard one; the data of the active position comes in the slot props.
legend{ series: readonly NormalizedSeries[]; toggle: (id: string) => void; }A custom legend instead of the standard one. It is not drawn for an empty chart.
emptyanyA custom placeholder for an empty chart instead of "No data"; it takes up the same `height`.
headeranyA header above the chart, inside its root — above a legend placed on top.

Events

EventSignatureDescription
update:hiddenSeries(event: "update:hiddenSeries", value: string[]): void—
update:activeIndex(event: "update:activeIndex", value: number | null): void—
pointClick(event: "pointClick", value: GrChartActivePoint): void—
pointHover(event: "pointHover", value: GrChartActivePoint | null): void—
legendToggle(event: "legendToggle", value: { seriesId: string; hidden: boolean; }): void—

Methods

MethodsSignatureDescription
elementHTMLElement | nullThe root element — for measuring and scrolling in the consumer.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrChartRadarConfigurableProps, GrChartSize, GrChartRadarProps } from '@feugene/granularity-charts/components/GrChartRadar'

Filedist/types/components/GrChartRadar/config.d.ts

grChartRadarConfigconst
const grChartRadarConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartRadar">;

Filedist/types/components/GrChartRadar/defaults.d.ts

GrChartRadarConfigurablePropsinterface
interface GrChartRadarConfigurableProps {
    size: GrChartSize;
    height: number;
    axisScale: 'shared' | 'per-axis';
    rings: number;
    fill: boolean;
    shape: 'polygon' | 'circle';
    showPoints: 'auto' | 'always' | 'never';
    showLegend: boolean | 'auto';
    legendPosition: 'top' | 'bottom';
    tooltip: boolean;
    dataTable: 'hidden' | 'visible' | 'off';
    dataTableMaxRows: number | 'auto';
}

Filedist/types/components/GrChartFrame/chartFrameStyles.d.ts

GrChartSizetype
type GrChartSize = GrComponentSize;

Filedist/types/components/GrChartRadar/safelist.d.ts

grChartRadarSafelistconst
const grChartRadarSafelist: string[];

Filedist/types/components/GrChartRadar/GrChartRadar.vue.d.ts

GrChartRadarPropsinterface
interface GrChartRadarProps {
    series: readonly GrChartSeries[] | readonly (number | null)[];
    axisScale?: 'shared' | 'per-axis';
    axisMax?: Record<string, number>;
    rings?: number;
    shape?: 'polygon' | 'circle';
    startAngle?: number;
    fill?: boolean;
    showPoints?: 'auto' | 'always' | 'never';
    height?: number;
    width?: number;
    yDomain?: readonly [number | null, number | null];
    showLegend?: boolean | 'auto';
    legendPosition?: 'top' | 'bottom';
    tooltip?: boolean;
    hiddenSeries?: readonly string[];
    activeIndex?: number | null;
    loading?: boolean;
    empty?: boolean;
    emptyText?: string;
    dataTable?: 'hidden' | 'visible' | 'off';
    interactive?: boolean;
    size?: GrChartSize;
    valueFormat?: GrChartNumberFormat;
    locale?: string;
    ariaLabel?: string;
    ariaDescription?: string;
    dataTableMaxRows?: number | 'auto';
}
GrChartRadarEmitsinterface
interface GrChartRadarEmits {
    (e: 'update:hiddenSeries', value: string[]): void;
    (e: 'update:activeIndex', value: number | null): void;
    (e: 'pointClick', value: GrChartActivePoint): void;
    (e: 'pointHover', value: GrChartActivePoint | null): void;
    (e: 'legendToggle', value: {
        seriesId: string;
        hidden: boolean;
    }): void;
}

Usage from the library prose

Two radius scales — and both have a price
ModeThe domainWhat is paid
shared (the default)one for all of the axesheterogeneous metrics cannot be brought together: the largest will take the whole scale
per-axisone of its own per axisthe 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.

With per-axis the ring has no single number, so the ceiling of each axis stands as a second, small line under its name (max 200); in full, as one line “Orders a day · 200”, it stays in the label’s <title>, in the tooltip and in the table.

The labels give way to the web, not the other way round

The radius is the largest at which every label fits into the canvas on the side its axis faces, but no less than half of the possible one: in a narrow card the labels make room for the drawing rather than eating it. A name wider than a quarter of the canvas wraps at a space onto two lines; if it still does not fit, it ends with an ellipsis, with the full text in <title>. The axis ceiling with per-axis is never truncated: “max 5,…” would lie about the number.

The ring labels stand on the bisector between the first and the second spoke rather than on the first: there the outer ring ended up in the same column as the name of the top axis.

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.