1 GitHub star = 1 new component.

Star the repo
Charts & Stats chartsvgzero-dependencyrankbumpleaderboardtime-series

Bump Chart

Rank-over-time chart tracking each entity's position across periods with labelled rank nodes.

Live Preview

Framework popularity rank

Developer survey position by year

  • Astro
  • Next
  • Svelte
  • Nuxt
  • Remix
2021 2022 2023 2024 2025 2026 5 4 3 2 1 1 Astro Astro 1 1 1 1 2 2 Next Next 3 2 2 3 3 4 Svelte Svelte 2 3 4 4 5 3 Nuxt Nuxt 4 5 5 5 4 5 Remix Remix
View data
Name202120222023202420252026
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