Charts & Stats chartsvgzero-dependencyrankbumpleaderboardtime-series
Bump Chart
Rank-over-time chart tracking each entity's position across periods with labelled rank nodes.
Live Preview
Developer survey position by year
- Astro
- Next
- Svelte
- Nuxt
- Remix
View data
| Name | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
|---|---|---|---|---|---|---|
| Astro | #5 | #4 | #3 | #2 | #1 | #1 |
| Next | #1 | #1 | #1 | #1 | #2 | #2 |
| Svelte | #3 | #2 | #2 | #3 | #3 | #4 |
| Nuxt | #2 | #3 | #4 | #4 | #5 | #3 |
| Remix | #4 | #5 | #5 | #5 | #4 | #5 |
Usage
Copy and paste this code into your Astro page:
astro
---
import { BumpChart } from 'astro-component-kit/charts';
---
<BumpChart
periods={['2024', '2025', '2026']}
series={[
{ name: 'Astro', ranks: [3, 2, 1] },
{ name: 'Next', ranks: [1, 1, 2] },
{ name: 'Svelte', ranks: [2, 3, 3] },
]}
/> ---
import { BumpChart } from 'astro-component-kit/charts';
---
<BumpChart
periods={['2024', '2025', '2026']}
series={[
{ name: 'Astro', ranks: [3, 2, 1] },
{ name: 'Next', ranks: [1, 1, 2] },
{ name: 'Svelte', ranks: [2, 3, 3] },
]}
/> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/lib/BumpChart.astro:
astro
---
/**
* BumpChart — Rank-over-time chart. Each line tracks one entity's position
* (1 = top) across periods, with labels at both ends.
*
* @param {string[]} periods - X-axis labels.
* @param {Array<{name: string, ranks: number[]}>} series - Rank per period (1-based), max 8.
* @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 { colorAt, smoothPath, type Point } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
periods?: string[];
series?: Array<{ name: string; ranks: number[] }>;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Framework popularity rank',
subtitle = 'Developer survey position by year',
periods = ['2021', '2022', '2023', '2024', '2025', '2026'],
series = [
{ name: 'Astro', ranks: [5, 4, 3, 2, 1, 1] },
{ name: 'Next', ranks: [1, 1, 1, 1, 2, 2] },
{ name: 'Svelte', ranks: [3, 2, 2, 3, 3, 4] },
{ name: 'Nuxt', ranks: [2, 3, 4, 4, 5, 3] },
{ name: 'Remix', ranks: [4, 5, 5, 5, 4, 5] },
],
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const W = 640;
const maxRank = Math.max(...series.flatMap((s) => s.ranks));
const rowH = 44;
const H = maxRank * rowH + 36;
const m = { t: 30, l: 80, r: 80 };
const n = periods.length;
const x = (i: number) => m.l + (i / Math.max(1, n - 1)) * (W - m.l - m.r);
const y = (r: number) => m.t + (r - 1) * rowH + 8;
const lines = series.slice(0, 8).map((s, si) => ({
...s,
color: colorAt(si, colors),
pts: s.ranks.map((r, i): Point => [x(i), y(r)]),
}));
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
legend={lines.map((l) => ({ label: l.name, color: l.color }))}
table={{ headers: ['Name', ...periods], rows: series.map((s) => [s.name, ...s.ranks.map((r) => `#${r}`)]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: bump chart of ranks over ${n} periods`}>
{periods.map((p, i) => (
<g>
<line class="cf-grid" x1={x(i)} x2={x(i)} y1={m.t - 6} y2={H - 10} />
<text class="cf-label" x={x(i)} y={14} text-anchor="middle">{p}</text>
</g>
))}
{lines.map((l) => (
<g class="cf-mark cf-dim" tabindex="0" role="img"
aria-label={`${l.name}: ${l.ranks.map((r, i) => `${periods[i]} #${r}`).join(', ')}`}
data-tip-title={l.name}
data-tip={l.ranks.map((r, i) => `${periods[i]}: #${r}`).join('|')}>
<path d={smoothPath(l.pts)} fill="none" stroke={l.color} stroke-width="3" stroke-linecap="round" />
<path d={smoothPath(l.pts)} fill="none" stroke="transparent" stroke-width="16" />
{l.pts.map(([px, py], i) => (
<g>
<circle cx={px} cy={py} r="11" fill={l.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
<text x={px} y={py + 4} text-anchor="middle" font-size="10" font-weight="700" fill="#fff">{l.ranks[i]}</text>
</g>
))}
<text class="cf-label cf-label--strong" x={m.l - 18} y={l.pts[0][1] + 4} text-anchor="end">{l.name}</text>
<text class="cf-label cf-label--strong" x={W - m.r + 18} y={l.pts[n - 1][1] + 4}>{l.name}</text>
</g>
))}
</svg>
</ChartFrame> ---
/**
* BumpChart — Rank-over-time chart. Each line tracks one entity's position
* (1 = top) across periods, with labels at both ends.
*
* @param {string[]} periods - X-axis labels.
* @param {Array<{name: string, ranks: number[]}>} series - Rank per period (1-based), max 8.
* @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 { colorAt, smoothPath, type Point } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
periods?: string[];
series?: Array<{ name: string; ranks: number[] }>;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Framework popularity rank',
subtitle = 'Developer survey position by year',
periods = ['2021', '2022', '2023', '2024', '2025', '2026'],
series = [
{ name: 'Astro', ranks: [5, 4, 3, 2, 1, 1] },
{ name: 'Next', ranks: [1, 1, 1, 1, 2, 2] },
{ name: 'Svelte', ranks: [3, 2, 2, 3, 3, 4] },
{ name: 'Nuxt', ranks: [2, 3, 4, 4, 5, 3] },
{ name: 'Remix', ranks: [4, 5, 5, 5, 4, 5] },
],
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const W = 640;
const maxRank = Math.max(...series.flatMap((s) => s.ranks));
const rowH = 44;
const H = maxRank * rowH + 36;
const m = { t: 30, l: 80, r: 80 };
const n = periods.length;
const x = (i: number) => m.l + (i / Math.max(1, n - 1)) * (W - m.l - m.r);
const y = (r: number) => m.t + (r - 1) * rowH + 8;
const lines = series.slice(0, 8).map((s, si) => ({
...s,
color: colorAt(si, colors),
pts: s.ranks.map((r, i): Point => [x(i), y(r)]),
}));
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
legend={lines.map((l) => ({ label: l.name, color: l.color }))}
table={{ headers: ['Name', ...periods], rows: series.map((s) => [s.name, ...s.ranks.map((r) => `#${r}`)]) }}
>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: bump chart of ranks over ${n} periods`}>
{periods.map((p, i) => (
<g>
<line class="cf-grid" x1={x(i)} x2={x(i)} y1={m.t - 6} y2={H - 10} />
<text class="cf-label" x={x(i)} y={14} text-anchor="middle">{p}</text>
</g>
))}
{lines.map((l) => (
<g class="cf-mark cf-dim" tabindex="0" role="img"
aria-label={`${l.name}: ${l.ranks.map((r, i) => `${periods[i]} #${r}`).join(', ')}`}
data-tip-title={l.name}
data-tip={l.ranks.map((r, i) => `${periods[i]}: #${r}`).join('|')}>
<path d={smoothPath(l.pts)} fill="none" stroke={l.color} stroke-width="3" stroke-linecap="round" />
<path d={smoothPath(l.pts)} fill="none" stroke="transparent" stroke-width="16" />
{l.pts.map(([px, py], i) => (
<g>
<circle cx={px} cy={py} r="11" fill={l.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
<text x={px} y={py + 4} text-anchor="middle" font-size="10" font-weight="700" fill="#fff">{l.ranks[i]}</text>
</g>
))}
<text class="cf-label cf-label--strong" x={m.l - 18} y={l.pts[0][1] + 4} text-anchor="end">{l.name}</text>
<text class="cf-label cf-label--strong" x={W - m.r + 18} y={l.pts[n - 1][1] + 4}>{l.name}</text>
</g>
))}
</svg>
</ChartFrame>
Quick Info
- Category
- Charts & Stats
- Filename
BumpChart.astro- Dependencies
- apexcharts (peer)
- Tags
- chartsvgzero-dependencyrankbumpleaderboardtime-series