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
| 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.
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.
fillmakes each profile a figure, andv-model:hidden-serieslets 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 X NorthPay PayLane Globex Pay Fees 6 9 5 Payout speed 9 6 7 Coverage 7 5 10 API quality 9 7 6 Fraud tools 8 6 9 Support 6 8 7 - 1. NorthPay45 / 60
- 2. Globex Pay44 / 60
- 3. PayLane41 / 60
A candidate against the frame of the role
The requirement is drawn as an outline, not a figure: a series
dashand a transparentfill-colorturn 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 X Role Dana Whitfield System design 4 5 Go 4 3 SQL 3 4 On-call 3 2 Mentoring 3 4 - System design5 / 4
- Go3 / 4
- SQL4 / 3
- On-call2 / 3
- Mentoring4 / 3
Five metrics, five units
axis-scale="per-axis"normalises each spoke andaxis-maxnames 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 Axis Lisbon Network average Axis maximum Orders 142 118 200 Basket 38 44 60 NPS 61 48 100 Repeat 34 29 50 Rating 4.7 4.3 5 Metric Lisbon Network Orders a day 142 118 Average basket, € 38 44 NPS 61 48 Repeat customers, % 34 29 Rating, of 5 4.7 4.3 A week that closes on itself
shape="circle"draws rings, which suits a cycle where Sunday meets Monday;start-angleputs Monday at twelve o’clock.v-model:active-indexis 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 X September August Mon 412 356 Tue 368 342 Wed 351 330 Thu 340 335 Fri 296 301 Sat 164 102 Sun 141 88 Hover a day, or focus the chart and use the arrow keys.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
fill | boolean | undefined | undefined | Fill the polygon of a series; `true` by default. Without the fill only the outline remains — easier to read when there are many series. |
height | number | undefined | undefined | The canvas height in pixels; 280 by default. The data, the loading state and the empty state all take up the same height. |
width | number | undefined | 640 | Declared width: the first render starts from it, after that the width is measured. |
yDomain | readonly [number | null, number | null] | undefined | undefined | The bounds of the value axis with `shared`. Zero always stays in the domain. |
valueFormat | GrChartNumberFormat | undefined | undefined | The value format in the tooltip, the hidden table and announcements: decimal places and separators. |
showPoints | "auto" | "never" | "always" | undefined | undefined | Vertex markers: `auto` (the default), `always` or `never`. |
showLegend | boolean | "auto" | undefined | undefined | The series legend: `auto` (the default) — when there is more than one series; `true` and `false` — always and never. |
legendPosition | "bottom" | "top" | undefined | undefined | Where the legend sits: `bottom` (the default) or `top`. |
tooltip | boolean | undefined | undefined | The 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. |
activeIndexv-model | number | null | undefined | undefined | The cursor — `v-model:activeIndex`. The axis index. |
loading | boolean | undefined | false | Loading: a skeleton of the same height stands in place of the plot area, and the space for the axes is already reserved. |
empty | boolean | undefined | undefined | The empty state, set explicitly. Unset — the chart is empty when there is nothing to draw. |
emptyText | string | undefined | undefined | The placeholder text of an empty chart. Unset — "No data" from the package translations. |
dataTable | "hidden" | "visible" | "off" | undefined | undefined | The data table: `hidden` (the default) — for the screen reader only, `visible` — under the chart, `off` — no table. |
interactive | boolean | undefined | true | `false` — the chart becomes a picture: no focus, no tooltip, no keyboard. |
size | "md" | "xs" | "sm" | "lg" | undefined | undefined | The type size of the axis names, the rings and the legend on the core control scale; by default — the size from `GrConfigProvider`. |
locale | string | undefined | undefined | The BCP-47 locale for numbers and dates. Unset — the language of the application translations, otherwise `en`. |
ariaLabel | string | undefined | undefined | The accessible name of the chart. Unset — a summary the component assembles: the chart type and the amount of data. |
ariaDescription | string | undefined | undefined | A description for the screen reader — the conclusion the chart is shown for; it adds to the summary rather than replacing it. |
dataTableMaxRows | number | "auto" | undefined | undefined | The row cap of the hidden data table. `'auto'` (the default) — as many rows as can be read; above the cap the table takes an even sample and says so with a note, while point-by-point completeness is left to the keyboard. A number sets its own cap, `Infinity` lifts it entirely, `dataTable: 'off'` removes the table altogether — which of these is needed is up to the application. |
shape | "polygon" | "circle" | undefined | undefined | The shape of the grid rings. |
startAngle | number | undefined | 0 | The angle of the first axis in degrees from twelve o'clock, clockwise. |
axisScale | "shared" | "per-axis" | undefined | undefined | How to scale the spokes. `shared` — one scale for every axis: the area of the shape is comparable, the shape is honest. `per-axis` — each axis is normalized by its own maximum; this is the only way to show unlike metrics on one web, but the areas can no longer be compared. |
axisMax | Record<string, number> | undefined | undefined | The upper bounds of the axes with `per-axis`, keyed by axis name. Without them the maximum is taken from the data. |
rings | number | undefined | undefined | The desired number of grid rings — the same as the number of value axis ticks. The actual number follows the ladder of "nice" numbers and may differ: a ring has to sit on a round value, otherwise the label under it reads as random. Under per-axis normalization there are no round values, and there are exactly as many rings as requested. |
seriesrequired | readonly GrChartSeries[] | readonly (number | null)[] | — | Series over a common set of axes. The X axis is always categorical — the radar has no `xScale` of its own. |
Slots
| Slot | Type | Description |
|---|---|---|
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. |
empty | any | A custom placeholder for an empty chart instead of "No data"; it takes up the same `height`. |
header | any | A header above the chart, inside its root — above a legend placed on top. |
Events
| Event | Signature | Description |
|---|---|---|
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
| Methods | Signature | Description |
|---|---|---|
element | HTMLElement | null | The 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 type { GrChartRadarConfigurableProps, GrChartSize, GrChartRadarProps } from '@feugene/granularity-charts/components/GrChartRadar'Filedist/types/components/GrChartRadar/config.d.ts
const grChartRadarConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartRadar">;Filedist/types/components/GrChartRadar/defaults.d.ts
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
type GrChartSize = GrComponentSize;Filedist/types/components/GrChartRadar/safelist.d.ts
const grChartRadarSafelist: string[];Filedist/types/components/GrChartRadar/GrChartRadar.vue.d.ts
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';
}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
| 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.
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.
Props in this example