1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticas chartsvgzero-dependencylinetrendtime-seriesarea

Line Chart

Gráfico de líneas SVG multiserie con curvas suaves, lineales o escalonadas, relleno degradado y tooltip con cruceta.

Vista Previa en Vivo

Weekly active users

Last 12 weeks, by platform

  • Web
  • Mobile
0 2k 4k 6k 8k W1W3W5W7W9W11
View data
PeriodWebMobile
W14.2k2.1k
W24.6k2.4k
W34.4k2.6k
W45k2.5k
W55.4k3k
W65.2k3.3k
W75.9k3.2k
W86.1k3.6k
W96k3.9k
W106.7k4.2k
W117k4.1k
W127.4k4.6k

Uso

Copia y pega este código directamente en tu página de Astro:

astro
---
import { LineChart } from 'astro-component-kit/charts';
---

<LineChart
  title="Revenue"
  categories={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}
  series={[{ name: 'Revenue', data: [12, 19, 15, 24, 28, 33] }]}
  prefix="$"
  curve="smooth"
/>
--- import { LineChart } from 'astro-component-kit/charts'; --- <LineChart title="Revenue" categories={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']} series={[{ name: 'Revenue', data: [12, 19, 15, 24, 28, 33] }]} prefix="$" curve="smooth" />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/lib/LineChart.astro:

astro
---
/**
 * LineChart — Zero-dependency SVG line chart with smooth, linear or step curves,
 * optional area fill, crosshair tooltip and a data-table fallback.
 *
 * @param {string[]} categories - X-axis labels.
 * @param {Array<{name: string, data: number[]}>} series - One or more series (max 8).
 * @param {string} curve - Interpolation (smooth|linear|step).
 * @param {boolean} area - Fill a soft gradient under each line.
 * @param {boolean} dots - Draw point markers.
 * @param {string} prefix - Value prefix (e.g. '$').
 * @param {string} suffix - Value suffix (e.g. '%').
 * @param {number} height - SVG height in viewBox units (width is 640).
 * @param {string[]} colors - Optional color overrides per series.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
import ChartFrame from './ChartFrame.astro';
import { colorAt, scaleLinear, niceTicks, curvePath, areaPath, formatValue, chartId, type Point } from './chart-utils';

interface Props {
  title?: string;
  subtitle?: string;
  categories?: string[];
  series?: Array<{ name: string; data: number[] }>;
  curve?: 'smooth' | 'linear' | 'step';
  area?: boolean;
  dots?: boolean;
  prefix?: string;
  suffix?: string;
  height?: number;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  title = 'Weekly active users',
  subtitle = 'Last 12 weeks, by platform',
  categories = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12'],
  series = [
    { name: 'Web', data: [4200, 4580, 4410, 5020, 5390, 5210, 5870, 6120, 6050, 6680, 7010, 7420] },
    { name: 'Mobile', data: [2100, 2350, 2600, 2550, 2980, 3310, 3240, 3620, 3900, 4150, 4080, 4560] },
  ],
  curve = 'smooth',
  area = true,
  dots = false,
  prefix = '',
  suffix = '',
  height = 280,
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
} = Astro.props;

const W = 640;
const H = height;
const m = { t: 12, r: 16, b: 28, l: 44 };
const all = series.flatMap((s) => s.data);
const ticks = niceTicks(Math.min(0, ...all), Math.max(...all), 4);
const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t);
const n = categories.length;
const x = (i: number) => m.l + (n <= 1 ? 0 : (i / (n - 1)) * (W - m.l - m.r));
const gid = chartId('line');
const step = n > 1 ? (W - m.l - m.r) / (n - 1) : W;
const labelEvery = Math.ceil(n / 8);

const lines = series.slice(0, 8).map((s, si) => {
  const pts: Point[] = s.data.map((v, i) => [x(i), y(v)]);
  return {
    name: s.name,
    color: colorAt(si, colors),
    pts,
    d: curvePath(pts, curve),
    area: areaPath(pts, H - m.b, curve),
  };
});

const fmt = (v: number) => formatValue(v, prefix, suffix);
---

<ChartFrame
  title={title}
  subtitle={subtitle}
  variant={variant}
  size={size}
  className={className}
  legend={lines.map((l) => ({ label: l.name, color: l.color }))}
  table={{
    headers: ['Period', ...series.map((s) => s.name)],
    rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i]))]),
  }}
>
  <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: line chart with ${series.length} series over ${n} periods`}>
    <defs>
      {lines.map((l, i) => (
        <linearGradient id={`${gid}-g${i}`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color={l.color} stop-opacity="0.22" />
          <stop offset="100%" stop-color={l.color} stop-opacity="0" />
        </linearGradient>
      ))}
    </defs>

    {ticks.map((t) => (
      <g>
        <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
        <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
      </g>
    ))}

    {categories.map((c, i) => i % labelEvery === 0 && (
      <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{c}</text>
    ))}

    {area && lines.map((l, i) => <path d={l.area} fill={`url(#${gid}-g${i})`} aria-hidden="true" />)}
    {lines.map((l) => (
      <path class="lc__line" d={l.d} fill="none" stroke={l.color} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true" />
    ))}
    {dots && lines.map((l) => l.pts.map(([px, py]) => (
      <circle cx={px} cy={py} r="4" fill={l.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" aria-hidden="true" />
    )))}

    {categories.map((c, i) => (
      <g class="cf-hit-group">
        <line class="cf-cross" x1={x(i)} x2={x(i)} y1={m.t} y2={H - m.b} />
        {lines.map((l) => (
          <circle class="cf-cross" cx={l.pts[i]?.[0]} cy={l.pts[i]?.[1]} r="4.5" fill={l.color} stroke="#fff" stroke-width="1.5" />
        ))}
        <rect
          class="cf-hit"
          x={x(i) - step / 2}
          y={m.t}
          width={step}
          height={H - m.t - m.b}
          tabindex="0"
          role="img"
          aria-label={`${c}: ${series.map((s) => `${s.name} ${fmt(s.data[i])}`).join(', ')}`}
          data-tip-title={c}
          data-tip={lines.map((l, si) => `${l.color}::${l.name}: ${fmt(series[si].data[i])}`).join('|')}
        />
      </g>
    ))}
  </svg>
</ChartFrame>

<style>
  .lc__line {
    stroke-dasharray: 2400;
    stroke-dashoffset: 2400;
    animation: lc-draw 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
  }
  @keyframes lc-draw {
    to { stroke-dashoffset: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .lc__line { animation: none; stroke-dashoffset: 0; }
  }
</style>
--- /** * LineChart — Zero-dependency SVG line chart with smooth, linear or step curves, * optional area fill, crosshair tooltip and a data-table fallback. * * @param {string[]} categories - X-axis labels. * @param {Array<{name: string, data: number[]}>} series - One or more series (max 8). * @param {string} curve - Interpolation (smooth|linear|step). * @param {boolean} area - Fill a soft gradient under each line. * @param {boolean} dots - Draw point markers. * @param {string} prefix - Value prefix (e.g. '$'). * @param {string} suffix - Value suffix (e.g. '%'). * @param {number} height - SVG height in viewBox units (width is 640). * @param {string[]} colors - Optional color overrides per series. * @param {string} variant - Card style (glass|solid|ghost). * @param {string} size - Density (sm|md|lg). */ import ChartFrame from './ChartFrame.astro'; import { colorAt, scaleLinear, niceTicks, curvePath, areaPath, formatValue, chartId, type Point } from './chart-utils'; interface Props { title?: string; subtitle?: string; categories?: string[]; series?: Array<{ name: string; data: number[] }>; curve?: 'smooth' | 'linear' | 'step'; area?: boolean; dots?: boolean; prefix?: string; suffix?: string; height?: number; colors?: string[]; variant?: 'glass' | 'solid' | 'ghost'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { title = 'Weekly active users', subtitle = 'Last 12 weeks, by platform', categories = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12'], series = [ { name: 'Web', data: [4200, 4580, 4410, 5020, 5390, 5210, 5870, 6120, 6050, 6680, 7010, 7420] }, { name: 'Mobile', data: [2100, 2350, 2600, 2550, 2980, 3310, 3240, 3620, 3900, 4150, 4080, 4560] }, ], curve = 'smooth', area = true, dots = false, prefix = '', suffix = '', height = 280, colors, variant = 'glass', size = 'md', className = '', } = Astro.props; const W = 640; const H = height; const m = { t: 12, r: 16, b: 28, l: 44 }; const all = series.flatMap((s) => s.data); const ticks = niceTicks(Math.min(0, ...all), Math.max(...all), 4); const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t); const n = categories.length; const x = (i: number) => m.l + (n <= 1 ? 0 : (i / (n - 1)) * (W - m.l - m.r)); const gid = chartId('line'); const step = n > 1 ? (W - m.l - m.r) / (n - 1) : W; const labelEvery = Math.ceil(n / 8); const lines = series.slice(0, 8).map((s, si) => { const pts: Point[] = s.data.map((v, i) => [x(i), y(v)]); return { name: s.name, color: colorAt(si, colors), pts, d: curvePath(pts, curve), area: areaPath(pts, H - m.b, curve), }; }); const fmt = (v: number) => formatValue(v, prefix, suffix); --- <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} legend={lines.map((l) => ({ label: l.name, color: l.color }))} table={{ headers: ['Period', ...series.map((s) => s.name)], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i]))]), }} > <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: line chart with ${series.length} series over ${n} periods`}> <defs> {lines.map((l, i) => ( <linearGradient id={`${gid}-g${i}`} x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color={l.color} stop-opacity="0.22" /> <stop offset="100%" stop-color={l.color} stop-opacity="0" /> </linearGradient> ))} </defs> {ticks.map((t) => ( <g> <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} /> <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text> </g> ))} {categories.map((c, i) => i % labelEvery === 0 && ( <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{c}</text> ))} {area && lines.map((l, i) => <path d={l.area} fill={`url(#${gid}-g${i})`} aria-hidden="true" />)} {lines.map((l) => ( <path class="lc__line" d={l.d} fill="none" stroke={l.color} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true" /> ))} {dots && lines.map((l) => l.pts.map(([px, py]) => ( <circle cx={px} cy={py} r="4" fill={l.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" aria-hidden="true" /> )))} {categories.map((c, i) => ( <g class="cf-hit-group"> <line class="cf-cross" x1={x(i)} x2={x(i)} y1={m.t} y2={H - m.b} /> {lines.map((l) => ( <circle class="cf-cross" cx={l.pts[i]?.[0]} cy={l.pts[i]?.[1]} r="4.5" fill={l.color} stroke="#fff" stroke-width="1.5" /> ))} <rect class="cf-hit" x={x(i) - step / 2} y={m.t} width={step} height={H - m.t - m.b} tabindex="0" role="img" aria-label={`${c}: ${series.map((s) => `${s.name} ${fmt(s.data[i])}`).join(', ')}`} data-tip-title={c} data-tip={lines.map((l, si) => `${l.color}::${l.name}: ${fmt(series[si].data[i])}`).join('|')} /> </g> ))} </svg> </ChartFrame> <style> .lc__line { stroke-dasharray: 2400; stroke-dashoffset: 2400; animation: lc-draw 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; } @keyframes lc-draw { to { stroke-dashoffset: 0; } } @media (prefers-reduced-motion: reduce) { .lc__line { animation: none; stroke-dashoffset: 0; } } </style>

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
LineChart.astro
Dependencias
apexcharts (peer)
Etiquetas
chartsvgzero-dependencylinetrendtime-seriesarea