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
Last 12 weeks, by platform
- Web
- Mobile
View data
| Period | Web | Mobile |
|---|---|---|
| W1 | 4.2k | 2.1k |
| W2 | 4.6k | 2.4k |
| W3 | 4.4k | 2.6k |
| W4 | 5k | 2.5k |
| W5 | 5.4k | 3k |
| W6 | 5.2k | 3.3k |
| W7 | 5.9k | 3.2k |
| W8 | 6.1k | 3.6k |
| W9 | 6k | 3.9k |
| W10 | 6.7k | 4.2k |
| W11 | 7k | 4.1k |
| W12 | 7.4k | 4.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