Charts & Stats chartsvgzero-dependencyradarspidercomparisonattributes
Radar Chart
Pure SVG spider chart comparing up to three entities across 5–8 attributes on a shared scale.
Live Preview
Scored 0–10 by the team
- Astro
- Next
View data
| Axis | Astro | Next |
|---|---|---|
| Performance | 9.5 | 7.5 |
| DX | 8.5 | 8 |
| Ecosystem | 7 | 9.5 |
| SEO | 9.5 | 8 |
| Learning curve | 8.5 | 6.5 |
| Flexibility | 8 | 8.5 |
Usage
Copy and paste this code into your Astro page:
astro
---
import { RadarChart } from 'astro-component-kit/charts';
---
<RadarChart
axes={['Speed', 'DX', 'Ecosystem', 'SEO', 'Flexibility']}
series={[
{ name: 'Astro', data: [9.5, 8.5, 7, 9.5, 8] },
{ name: 'Next', data: [7.5, 8, 9.5, 8, 8.5] },
]}
max={10}
/> ---
import { RadarChart } from 'astro-component-kit/charts';
---
<RadarChart
axes={['Speed', 'DX', 'Ecosystem', 'SEO', 'Flexibility']}
series={[
{ name: 'Astro', data: [9.5, 8.5, 7, 9.5, 8] },
{ name: 'Next', data: [7.5, 8, 9.5, 8, 8.5] },
]}
max={10}
/> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/lib/RadarChart.astro:
astro
---
/**
* RadarChart — Pure SVG spider chart comparing up to 3 entities across 5–8
* axes on a shared 0–max scale, with value tooltips on every vertex.
*
* @param {string[]} axes - Attribute names (5–8 recommended).
* @param {Array<{name: string, data: number[]}>} series - Entities (max 3).
* @param {number} max - Scale maximum.
* @param {string[]} colors - Optional per-series colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { polar, linePath, colorAt, type Point } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
axes?: string[];
series?: Array<{ name: string; data: number[] }>;
max?: number;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Framework comparison',
subtitle = 'Scored 0–10 by the team',
axes = ['Performance', 'DX', 'Ecosystem', 'SEO', 'Learning curve', 'Flexibility'],
series = [
{ name: 'Astro', data: [9.5, 8.5, 7, 9.5, 8.5, 8] },
{ name: 'Next', data: [7.5, 8, 9.5, 8, 6.5, 8.5] },
],
max = 10,
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const S = 340;
const c = S / 2;
const R = S / 2 - 54;
const n = axes.length;
const ang = (i: number) => (i / n) * Math.PI * 2;
const levels = [0.25, 0.5, 0.75, 1];
const shapes = series.slice(0, 3).map((s, si) => {
const pts: Point[] = s.data.map((v, i) => polar(c, c, R * Math.max(0, Math.min(1, v / max)), ang(i)));
return { ...s, pts, d: `${linePath(pts)} Z`, color: colorAt(si, colors) };
});
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
legend={shapes.map((s) => ({ label: s.name, color: s.color }))}
table={{ headers: ['Axis', ...series.map((s) => s.name)], rows: axes.map((a, i) => [a, ...series.map((s) => s.data[i])]) }}
>
<div class="rd">
<svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: radar chart across ${n} axes`}>
{levels.map((l) => (
<path class="cf-grid" fill="none" d={`${linePath(axes.map((_, i) => polar(c, c, R * l, ang(i))))} Z`} />
))}
{axes.map((a, i) => {
const [x, y] = polar(c, c, R, ang(i));
const [lx, ly] = polar(c, c, R + 18, ang(i));
const anchor = 4 > Math.abs(lx - c) ? 'middle' : lx > c ? 'start' : 'end';
return (
<g>
<line class="cf-grid" x1={c} y1={c} x2={x} y2={y} />
<text class="cf-label" x={lx} y={ly + 4} text-anchor={anchor}>{a}</text>
</g>
);
})}
{shapes.map((s) => (
<path d={s.d} fill={s.color} fill-opacity="0.18" stroke={s.color} stroke-width="2" stroke-linejoin="round" aria-hidden="true" />
))}
{shapes.map((s) => s.pts.map(([x, y], i) => (
<circle class="cf-mark" cx={x} cy={y} r="4.5" fill={s.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2"
tabindex="0" role="img" aria-label={`${s.name}, ${axes[i]}: ${s.data[i]} of ${max}`}
data-tip-title={axes[i]} data-tip={shapes.map((o) => `${o.color}::${o.name}: ${o.data[i]}`).join('|')} />
)))}
</svg>
</div>
</ChartFrame>
<style>
.rd { max-width: 400px; margin: 0 auto; }
</style> ---
/**
* RadarChart — Pure SVG spider chart comparing up to 3 entities across 5–8
* axes on a shared 0–max scale, with value tooltips on every vertex.
*
* @param {string[]} axes - Attribute names (5–8 recommended).
* @param {Array<{name: string, data: number[]}>} series - Entities (max 3).
* @param {number} max - Scale maximum.
* @param {string[]} colors - Optional per-series colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { polar, linePath, colorAt, type Point } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
axes?: string[];
series?: Array<{ name: string; data: number[] }>;
max?: number;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Framework comparison',
subtitle = 'Scored 0–10 by the team',
axes = ['Performance', 'DX', 'Ecosystem', 'SEO', 'Learning curve', 'Flexibility'],
series = [
{ name: 'Astro', data: [9.5, 8.5, 7, 9.5, 8.5, 8] },
{ name: 'Next', data: [7.5, 8, 9.5, 8, 6.5, 8.5] },
],
max = 10,
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const S = 340;
const c = S / 2;
const R = S / 2 - 54;
const n = axes.length;
const ang = (i: number) => (i / n) * Math.PI * 2;
const levels = [0.25, 0.5, 0.75, 1];
const shapes = series.slice(0, 3).map((s, si) => {
const pts: Point[] = s.data.map((v, i) => polar(c, c, R * Math.max(0, Math.min(1, v / max)), ang(i)));
return { ...s, pts, d: `${linePath(pts)} Z`, color: colorAt(si, colors) };
});
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
legend={shapes.map((s) => ({ label: s.name, color: s.color }))}
table={{ headers: ['Axis', ...series.map((s) => s.name)], rows: axes.map((a, i) => [a, ...series.map((s) => s.data[i])]) }}
>
<div class="rd">
<svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: radar chart across ${n} axes`}>
{levels.map((l) => (
<path class="cf-grid" fill="none" d={`${linePath(axes.map((_, i) => polar(c, c, R * l, ang(i))))} Z`} />
))}
{axes.map((a, i) => {
const [x, y] = polar(c, c, R, ang(i));
const [lx, ly] = polar(c, c, R + 18, ang(i));
const anchor = 4 > Math.abs(lx - c) ? 'middle' : lx > c ? 'start' : 'end';
return (
<g>
<line class="cf-grid" x1={c} y1={c} x2={x} y2={y} />
<text class="cf-label" x={lx} y={ly + 4} text-anchor={anchor}>{a}</text>
</g>
);
})}
{shapes.map((s) => (
<path d={s.d} fill={s.color} fill-opacity="0.18" stroke={s.color} stroke-width="2" stroke-linejoin="round" aria-hidden="true" />
))}
{shapes.map((s) => s.pts.map(([x, y], i) => (
<circle class="cf-mark" cx={x} cy={y} r="4.5" fill={s.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2"
tabindex="0" role="img" aria-label={`${s.name}, ${axes[i]}: ${s.data[i]} of ${max}`}
data-tip-title={axes[i]} data-tip={shapes.map((o) => `${o.color}::${o.name}: ${o.data[i]}`).join('|')} />
)))}
</svg>
</div>
</ChartFrame>
<style>
.rd { max-width: 400px; margin: 0 auto; }
</style>
Quick Info
- Category
- Charts & Stats
- Filename
RadarChart.astro- Dependencies
- apexcharts (peer)
- Tags
- chartsvgzero-dependencyradarspidercomparisonattributes