GráficoChart
Línea y barras sin dependencias; los datos viven en una tabla HTML.Line and bar charts with no dependencies; data lives in an HTML table.
DiseñoDesign: W3C WAI, Complex imagesMITherramienta · densidaddensity alta · sobriaMarkdownJSON
CódigoCode
<figure class="c-chart" data-c-chart data-type="line">
<figcaption class="c-chart__head">
<span class="c-chart__title">Visitas por semana</span>
<span class="c-chart__note">Datos de ejemplo</span>
</figcaption>
<div class="c-chart__legend" aria-hidden="true"></div>
<div class="c-chart__plot" tabindex="0"></div>
<p class="c-chart__readout" aria-live="polite"></p>
<details class="c-chart__data">
<summary>Ver datos</summary>
<table>
<thead><tr><th scope="col">Semana</th><th scope="col">2026</th><th scope="col">2025</th></tr></thead>
<tbody>
<tr><th scope="row">S1</th><td>1200</td><td>980</td></tr>
<tr><th scope="row">S2</th><td>1350</td><td>1010</td></tr>
<tr><th scope="row">S3</th><td>1280</td><td>1100</td></tr>
<tr><th scope="row">S4</th><td>1620</td><td>1150</td></tr>
<tr><th scope="row">S5</th><td>1780</td><td>1190</td></tr>
<tr><th scope="row">S6</th><td>1710</td><td>1240</td></tr>
<tr><th scope="row">S7</th><td>2050</td><td>1300</td></tr>
<tr><th scope="row">S8</th><td>2240</td><td>1360</td></tr>
</tbody>
</table>
</details>
</figure>
<figure class="c-chart" data-c-chart data-type="bar">
<figcaption class="c-chart__head">
<span class="c-chart__title">Pedidos por canal</span>
<span class="c-chart__note">Datos de ejemplo</span>
</figcaption>
<div class="c-chart__legend" aria-hidden="true"></div>
<div class="c-chart__plot" tabindex="0"></div>
<p class="c-chart__readout" aria-live="polite"></p>
<details class="c-chart__data">
<summary>Ver datos</summary>
<table>
<thead><tr><th scope="col">Canal</th><th scope="col">Septiembre</th><th scope="col">Agosto</th></tr></thead>
<tbody>
<tr><th scope="row">Web</th><td>420</td><td>380</td></tr>
<tr><th scope="row">Tienda</th><td>310</td><td>330</td></tr>
<tr><th scope="row">WhatsApp</th><td>260</td><td>190</td></tr>
<tr><th scope="row">Marketplace</th><td>140</td><td>150</td></tr>
</tbody>
</table>
</details>
</figure>/* Gráfico de línea y barras sin dependencias. Los datos viven en la <table>; chart.js dibuja el SVG.
Colores solo de variables del tema (cambian con cada combinación de Wada): --primary, --muted-foreground,
--foreground, --border. Sin degradados ni animación. Necesita tokens.css. */
.c-chart { margin: 0; width: 100%; max-width: 44rem; display: grid; gap: .5rem; font-family: inherit; color: var(--foreground); }
.c-chart__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .25rem 1rem; }
.c-chart__title { font-weight: 600; font-size: .9375rem; }
.c-chart__note { font-size: .75rem; color: var(--muted-foreground); }
.c-chart__legend { display: flex; flex-wrap: wrap; gap: .25rem 1rem; font-size: .75rem; color: var(--muted-foreground); }
.c-chart__key { display: inline-flex; align-items: center; gap: .375rem; }
.c-chart__key svg { width: 18px; height: 8px; overflow: visible; }
.c-chart__plot { position: relative; height: var(--chart-h, 240px); border-radius: calc(var(--radius) - 2px); outline: none; }
.c-chart__plot:focus-visible { box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring); }
.c-chart__plot svg { display: block; width: 100%; height: 100%; overflow: visible; }
.c-chart__grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }
.c-chart__axis { fill: var(--muted-foreground); font-size: 11px; font-variant-numeric: tabular-nums; }
.c-chart__line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.c-chart__dot { stroke: var(--background); stroke-width: 2; }
.c-chart__bar { shape-rendering: crispEdges; }
.c-chart__cursor { stroke: var(--foreground); stroke-opacity: .25; stroke-width: 1; }
.c-chart__s0 { stroke: var(--primary); fill: var(--primary); }
.c-chart__s1 { stroke: var(--muted-foreground); fill: var(--muted-foreground); }
.c-chart__s2 { stroke: var(--foreground); fill: var(--foreground); }
.c-chart__line.c-chart__s1 { stroke-dasharray: 5 4; }
.c-chart__line.c-chart__s2 { stroke-dasharray: 1 4; }
.c-chart__line { fill: none; }
.c-chart__bar.c-chart__s1 { fill-opacity: .55; }
.c-chart__tip { position: absolute; top: 0; pointer-events: none; z-index: 2; min-width: 7rem; padding: .375rem .5rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px); background: var(--popover); color: var(--popover-foreground); font-size: .75rem; line-height: 1.4; box-shadow: 0 4px 12px -4px #0000002e; white-space: nowrap; }
.c-chart__tip b { display: block; margin-bottom: .125rem; }
.c-chart__tip span { display: flex; justify-content: space-between; gap: .75rem; font-variant-numeric: tabular-nums; }
.c-chart__tip i { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; margin-right: .375rem; vertical-align: .05em; }
.c-chart__readout { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; margin: 0; }
.c-chart__data { font-size: .75rem; color: var(--muted-foreground); }
.c-chart__data summary { cursor: pointer; width: fit-content; border-radius: 4px; }
.c-chart__data summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.c-chart__data table { margin-top: .5rem; border-collapse: collapse; font-variant-numeric: tabular-nums; color: var(--foreground); }
.c-chart__data th, .c-chart__data td { padding: .25rem .75rem .25rem 0; text-align: left; border-bottom: 1px solid var(--border); font-weight: 400; }
.c-chart__data td { text-align: right; }
.c-chart__data thead th { color: var(--muted-foreground); }
@media (forced-colors: active) { .c-chart__s0, .c-chart__s1, .c-chart__s2 { stroke: CanvasText; fill: CanvasText; } }/* Gráfico de línea y barras sin dependencias. Lee los datos de la <table> del propio gráfico (primera
columna = etiquetas; cada columna siguiente = una serie) y dibuja un SVG que se adapta al ancho.
Teclado: enfoca el gráfico y usa ← → (Inicio/Fin) para recorrer los puntos; el valor se anuncia.
Uso: <figure data-c-chart data-type="line|bar"> … <table> … </figure>; Componentes.chart.init(root) tras insertar. */
(function () {
const C = (window.Componentes = window.Componentes || {});
const SVG = 'http://www.w3.org/2000/svg';
const fmt = new Intl.NumberFormat(document.documentElement.lang || 'es', { maximumFractionDigits: 1 });
function el(tag, attrs, parent) {
const n = document.createElementNS(SVG, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
function read(fig) {
const t = fig.querySelector('table');
const series = [...t.tHead.rows[0].cells].slice(1).map((c) => ({ name: c.textContent.trim(), values: [] }));
const labels = [];
[...t.tBodies[0].rows].forEach((r) => {
labels.push(r.cells[0].textContent.trim());
series.forEach((s, i) => s.values.push(parseFloat(String(r.cells[i + 1]?.textContent).replace(/[^\d.-]/g, '')) || 0));
});
return { labels, series: series.slice(0, 3) };
}
// Escala «redonda»: el máximo sube al siguiente 1, 2, 2.5 o 5 × 10^n.
function niceMax(v) {
if (v <= 0) return 1;
const p = Math.pow(10, Math.floor(Math.log10(v)));
for (const m of [1, 2, 2.5, 5, 10]) if (m * p >= v) return m * p;
return 10 * p;
}
function legend(fig, data, type) {
const box = fig.querySelector('.c-chart__legend');
if (!box) return;
box.innerHTML = '';
data.series.forEach((s, i) => {
const k = document.createElement('span');
k.className = 'c-chart__key';
const svg = el('svg', { viewBox: '0 0 18 8' }, k);
if (type === 'bar') el('rect', { x: 5, y: 0, width: 8, height: 8, rx: 1.5, class: `c-chart__bar c-chart__s${i}` }, svg);
else el('line', { x1: 0, y1: 4, x2: 18, y2: 4, class: `c-chart__line c-chart__s${i}` }, svg);
k.append(s.name);
box.appendChild(k);
});
}
function draw(fig) {
const plot = fig.querySelector('.c-chart__plot');
const data = fig._data;
const type = fig.dataset.type === 'bar' ? 'bar' : 'line';
const W = plot.clientWidth, H = plot.clientHeight;
if (!W || !H) return;
const pad = { t: 8, r: 8, b: 24, l: 40 };
const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b;
const max = niceMax(Math.max(...data.series.flatMap((s) => s.values)));
const y = (v) => pad.t + ih - (v / max) * ih;
const n = data.labels.length;
const step = type === 'bar' ? iw / n : iw / Math.max(1, n - 1);
const xAt = (i) => (type === 'bar' ? pad.l + step * i + step / 2 : pad.l + step * i);
plot.querySelector('svg')?.remove();
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', focusable: 'false' });
plot.prepend(svg);
for (let k = 0; k <= 4; k++) {
const v = (max / 4) * k, yy = Math.round(y(v)) + .5;
el('line', { x1: pad.l, x2: W - pad.r, y1: yy, y2: yy, class: 'c-chart__grid' }, svg);
el('text', { x: pad.l - 8, y: yy + 4, 'text-anchor': 'end', class: 'c-chart__axis' }, svg).textContent = fmt.format(v);
}
const every = Math.ceil(n / Math.max(1, Math.floor(iw / 56)));
data.labels.forEach((lab, i) => {
if (i % every) return;
el('text', { x: xAt(i), y: H - 6, 'text-anchor': 'middle', class: 'c-chart__axis' }, svg).textContent = lab;
});
const cursor = el('line', { x1: 0, x2: 0, y1: pad.t, y2: pad.t + ih, class: 'c-chart__cursor', visibility: 'hidden' }, svg);
if (type === 'bar') {
const groupW = step * .7, bw = groupW / data.series.length;
data.series.forEach((s, si) => s.values.forEach((v, i) => {
const x = pad.l + step * i + (step - groupW) / 2 + bw * si;
el('rect', { x: x + 1, y: y(v), width: Math.max(1, bw - 2), height: pad.t + ih - y(v), rx: 3, class: `c-chart__bar c-chart__s${si}` }, svg);
}));
} else {
data.series.forEach((s, si) => {
el('polyline', { points: s.values.map((v, i) => `${xAt(i)},${y(v)}`).join(' '), class: `c-chart__line c-chart__s${si}` }, svg);
});
}
const dots = data.series.map((s, si) => el('circle', { r: 4, class: `c-chart__dot c-chart__s${si}`, visibility: 'hidden' }, svg));
fig._geo = { xAt, y, step, pad, n, cursor, dots, type, W };
}
function show(fig, i) {
const g = fig._geo, data = fig._data, plot = fig.querySelector('.c-chart__plot');
if (!g) return;
let tip = plot.querySelector('.c-chart__tip');
if (i == null) { if (tip) tip.hidden = true; g.cursor.setAttribute('visibility', 'hidden'); g.dots.forEach((d) => d.setAttribute('visibility', 'hidden')); return; }
fig._i = i;
const x = g.xAt(i);
g.cursor.setAttribute('x1', x); g.cursor.setAttribute('x2', x); g.cursor.setAttribute('visibility', g.type === 'line' ? 'visible' : 'hidden');
g.dots.forEach((d, si) => { d.setAttribute('cx', x); d.setAttribute('cy', g.y(data.series[si].values[i])); d.setAttribute('visibility', g.type === 'line' ? 'visible' : 'hidden'); });
if (!tip) { tip = document.createElement('div'); tip.className = 'c-chart__tip'; tip.setAttribute('aria-hidden', 'true'); plot.appendChild(tip); }
tip.hidden = false;
tip.innerHTML = `<b></b>` + data.series.map((s, si) => `<span><span><i class="c-chart__s${si}" style="background:currentColor;color:var(--${['primary', 'muted-foreground', 'foreground'][si]})"></i>${s.name}</span><strong>${fmt.format(s.values[i])}</strong></span>`).join('');
tip.querySelector('b').textContent = data.labels[i];
const left = Math.min(Math.max(0, x + 12), g.W - tip.offsetWidth);
tip.style.left = (x + 12 + tip.offsetWidth > g.W ? x - 12 - tip.offsetWidth : left) + 'px';
const ro = fig.querySelector('.c-chart__readout');
if (ro) ro.textContent = `${data.labels[i]}: ` + data.series.map((s) => `${s.name} ${fmt.format(s.values[i])}`).join(', ');
}
function setup(fig) {
fig._data = read(fig);
const data = fig._data, plot = fig.querySelector('.c-chart__plot');
const type = fig.dataset.type === 'bar' ? 'bar' : 'line';
const title = fig.querySelector('.c-chart__title')?.textContent.trim() || 'Gráfico';
const s0 = data.series[0];
const last = s0.values.length - 1;
plot.setAttribute('role', 'img');
plot.setAttribute('aria-label', `${title}. ${type === 'bar' ? 'Barras' : 'Línea'} con ${data.series.length} series y ${data.labels.length} puntos; ${s0.name} va de ${fmt.format(s0.values[0])} en ${data.labels[0]} a ${fmt.format(s0.values[last])} en ${data.labels[last]}. Usa las flechas para recorrerlo; los datos están en «Ver datos».`);
legend(fig, data, type);
draw(fig);
plot.addEventListener('pointermove', (e) => {
const g = fig._geo; if (!g) return;
const r = plot.getBoundingClientRect(), px = e.clientX - r.left;
const i = g.type === 'bar' ? Math.floor((px - g.pad.l) / g.step) : Math.round((px - g.pad.l) / g.step);
show(fig, Math.max(0, Math.min(g.n - 1, i)));
});
plot.addEventListener('pointerleave', () => { if (document.activeElement !== plot) show(fig, null); });
plot.addEventListener('focus', () => show(fig, fig._i ?? 0));
plot.addEventListener('blur', () => show(fig, null));
plot.addEventListener('keydown', (e) => {
const n = data.labels.length, cur = fig._i ?? 0;
const next = { ArrowRight: cur + 1, ArrowLeft: cur - 1, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
show(fig, Math.max(0, Math.min(n - 1, next)));
});
new ResizeObserver(() => { draw(fig); if (document.activeElement === plot) show(fig, fig._i ?? 0); }).observe(plot);
}
function init(root) {
(root || document).querySelectorAll('[data-c-chart]:not([data-c-ready])').forEach((fig) => {
fig.dataset.cReady = '';
try { setup(fig); } catch (e) { console.warn('[chart]', e); }
});
}
C.chart = { init, redraw: (fig) => { fig._data = read(fig); draw(fig); } };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());
else init();
})();<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gráfico · Componentes</title>
<!-- Gráfico · Componentes (MIRÓ Labs) · MIT · https://componentes-lovat.vercel.app/c/chart/
Diseño: W3C WAI, Complex images -->
<script>(function(){var d=document.documentElement,p=new URLSearchParams(location.search),q=p.get('theme');d.classList.toggle('dark',q?q==='dark':matchMedia('(prefers-color-scheme: dark)').matches);if(self!==top)d.classList.add('embed');addEventListener('message',function(e){if(e.data&&e.data.type==='componentes:theme')d.classList.toggle('dark',!!e.data.dark)});})();</script>
<style>
/* Componentes (MIRÓ Labs) · tokens. Tema de coss ui (MIT, © coss.com, antes Origin UI).
Generado por tools/build_css.py. Tema oscuro: clase .dark en <html> (los componentes usan esa clase).
Letra: la del sistema; cambia --font-sans para usar otra. */
:root{--animate-spin:spin 1s linear infinite;--aspect-video:16/9;--color-amber-400:lab(80.1641% 16.6016 99.2089);--color-amber-500:lab(72.7183% 31.8672 97.9407);--color-amber-700:lab(47.2709% 42.9082 69.2966);--color-background:var(--background);--color-black:#000;--color-blue-400:lab(65.0361% -1.42065 -56.9802);--color-blue-500:lab(54.1736% 13.3369 -74.6839);--color-blue-700:lab(36.9089% 35.0961 -85.6872);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-cyan-900:lab(30.372% -13.1853 -18.7887);--color-emerald-400:lab(75.0771% -60.7313 19.4147);--color-emerald-500:lab(66.9756% -58.27 19.5419);--color-emerald-700:lab(44.4871% -41.0396 11.0361);--color-foreground:var(--foreground);--color-muted:var(--muted);--color-muted-foreground:var(--muted-foreground);--color-neutral-100:lab(96.52% -.0000298023 .0000119209);--color-neutral-400:lab(66.128% -.0000298023 .0000119209);--color-neutral-50:lab(98.26% 0 0);--color-neutral-500:lab(48.496% 0 0);--color-neutral-800:lab(15.204% 0 -.00000596046);--color-neutral-950:lab(2.75381% 0 0);--color-orange-600:lab(57.1026% 64.2584 89.8886);--color-primary:var(--primary);--color-purple-500:lab(52.0183% 66.11 -78.2316);--color-red-400:lab(63.7053% 60.745 31.3109);--color-red-500:lab(55.4814% 75.0732 48.8528);--color-red-700:lab(40.4273% 67.2623 53.7441);--color-rose-500:lab(56.101% 79.4328 31.4532);--color-teal-600:lab(55.0223% -41.0774 -3.90277);--color-white:#fff;--container-2xl:42rem;--container-3xl:48rem;--container-5xl:64rem;--container-lg:32rem;--container-md:28rem;--container-sm:24rem;--container-xl:36rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--font-weight-bold:700;--font-weight-medium:500;--font-weight-normal:400;--font-weight-semibold:600;--radius-2xl:1rem;--radius-lg:var(--radius);--radius-md:calc(var(--radius) - 2px);--radius-xl:calc(var(--radius) + 4px);--spacing:.25rem;--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--tracking-tight:-.025em;--tracking-widest:.1em;--tw-shadow-color:#000}
:root{--font-sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
:root{--radius:.625rem;--background:var(--color-white);--foreground:var(--color-neutral-800);--card:var(--color-white);--card-foreground:var(--color-neutral-800);--popover:var(--color-white);--popover-foreground:var(--color-neutral-800);--primary:var(--color-neutral-800);--primary-foreground:var(--color-neutral-50);--secondary:#0000000a}
@supports (color:color-mix(in lab, red, red)){:root{--secondary:color-mix(in oklab,var(--color-black)4%,transparent)}}
:root{--secondary-foreground:var(--color-neutral-800);--muted:#0000000a}
@supports (color:color-mix(in lab, red, red)){:root{--muted:color-mix(in oklab,var(--color-black)4%,transparent)}}
:root{--muted-foreground:#686868}
@supports (color:color-mix(in lab, red, red)){:root{--muted-foreground:color-mix(in srgb,var(--color-neutral-500)90%,var(--color-black))}}
:root{--accent:#0000000a}
@supports (color:color-mix(in lab, red, red)){:root{--accent:color-mix(in oklab,var(--color-black)4%,transparent)}}
:root{--accent-foreground:var(--color-neutral-800);--destructive:var(--color-red-500);--destructive-foreground:var(--color-red-700);--info:var(--color-blue-500);--info-foreground:var(--color-blue-700);--success:var(--color-emerald-500);--success-foreground:var(--color-emerald-700);--warning:var(--color-amber-500);--warning-foreground:var(--color-amber-700);--border:#00000014}
@supports (color:color-mix(in lab, red, red)){:root{--border:color-mix(in oklab,var(--color-black)8%,transparent)}}
:root{--input:#0000001a}
@supports (color:color-mix(in lab, red, red)){:root{--input:color-mix(in oklab,var(--color-black)10%,transparent)}}
:root{--ring:var(--color-neutral-400);--chart-1:var(--color-orange-600);--chart-2:var(--color-teal-600);--chart-3:var(--color-cyan-900);--chart-4:var(--color-amber-400);--chart-5:var(--color-amber-500);--sidebar:var(--color-neutral-50);--sidebar-foreground:#262626}
@supports (color:lab(0% 0 0)){:root{--sidebar-foreground:lab(15.204% 0 -.00000596046)}}
@supports (color:color-mix(in lab, red, red)){:root{--sidebar-foreground:color-mix(in srgb,var(--color-neutral-800)64%,var(--sidebar))}}
:root{--sidebar-primary:var(--color-neutral-800);--sidebar-primary-foreground:var(--color-neutral-50);--sidebar-accent:#0000000a}
@supports (color:color-mix(in lab, red, red)){:root{--sidebar-accent:color-mix(in oklab,var(--color-black)4%,transparent)}}
:root{--sidebar-accent-foreground:var(--color-neutral-800);--sidebar-border:#0000000f}
@supports (color:color-mix(in lab, red, red)){:root{--sidebar-border:color-mix(in oklab,var(--color-black)6%,transparent)}}
:root{--sidebar-ring:var(--color-neutral-400);--code:var(--color-white);--code-foreground:var(--foreground);--code-highlight:#0000000a}
@supports (color:color-mix(in lab, red, red)){:root{--code-highlight:color-mix(in oklab,var(--color-black)4%,transparent)}}
.dark{--background:#141414}
@supports (color:color-mix(in lab, red, red)){.dark{--background:color-mix(in srgb,var(--color-neutral-950)96%,var(--color-white))}}
.dark{--foreground:var(--color-neutral-100);--card:var(--background)}
@supports (color:color-mix(in lab, red, red)){.dark{--card:color-mix(in srgb,var(--background)98%,var(--color-white))}}
.dark{--card-foreground:var(--color-neutral-100);--popover:var(--background)}
@supports (color:color-mix(in lab, red, red)){.dark{--popover:color-mix(in srgb,var(--background)96%,var(--color-white))}}
.dark{--popover-foreground:var(--color-neutral-100);--primary:var(--color-neutral-100);--primary-foreground:var(--color-neutral-800);--secondary:#ffffff0a}
@supports (color:color-mix(in lab, red, red)){.dark{--secondary:color-mix(in oklab,var(--color-white)4%,transparent)}}
.dark{--secondary-foreground:var(--color-neutral-100);--muted:#ffffff0a}
@supports (color:color-mix(in lab, red, red)){.dark{--muted:color-mix(in oklab,var(--color-white)4%,transparent)}}
.dark{--muted-foreground:#818181}
@supports (color:color-mix(in lab, red, red)){.dark{--muted-foreground:color-mix(in srgb,var(--color-neutral-500)90%,var(--color-white))}}
.dark{--accent:#ffffff0a}
@supports (color:color-mix(in lab, red, red)){.dark{--accent:color-mix(in oklab,var(--color-white)4%,transparent)}}
.dark{--accent-foreground:var(--color-neutral-100);--destructive:#fb414a}
@supports (color:color-mix(in lab, red, red)){.dark{--destructive:color-mix(in srgb,var(--color-red-500)90%,var(--color-white))}}
.dark{--destructive-foreground:var(--color-red-400);--info:var(--color-blue-500);--info-foreground:var(--color-blue-400);--success:var(--color-emerald-500);--success-foreground:var(--color-emerald-400);--warning:var(--color-amber-500);--warning-foreground:var(--color-amber-400);--border:#ffffff0f}
@supports (color:color-mix(in lab, red, red)){.dark{--border:color-mix(in oklab,var(--color-white)6%,transparent)}}
.dark{--input:#ffffff14}
@supports (color:color-mix(in lab, red, red)){.dark{--input:color-mix(in oklab,var(--color-white)8%,transparent)}}
.dark{--ring:var(--color-neutral-500);--chart-1:var(--color-blue-700);--chart-2:var(--color-emerald-500);--chart-3:var(--color-amber-500);--chart-4:var(--color-purple-500);--chart-5:var(--color-rose-500);--sidebar:#111}
@supports (color:color-mix(in lab, red, red)){.dark{--sidebar:color-mix(in srgb,var(--color-neutral-950)97%,var(--color-white))}}
.dark{--sidebar-foreground:#f5f5f5}
@supports (color:lab(0% 0 0)){.dark{--sidebar-foreground:lab(96.52% -.0000298023 .0000119209)}}
@supports (color:color-mix(in lab, red, red)){.dark{--sidebar-foreground:color-mix(in srgb,var(--color-neutral-100)64%,var(--sidebar))}}
.dark{--sidebar-primary:var(--color-neutral-100);--sidebar-primary-foreground:var(--color-neutral-800);--sidebar-accent:#ffffff0a}
@supports (color:color-mix(in lab, red, red)){.dark{--sidebar-accent:color-mix(in oklab,var(--color-white)4%,transparent)}}
.dark{--sidebar-accent-foreground:var(--color-neutral-100);--sidebar-border:#ffffff0d}
@supports (color:color-mix(in lab, red, red)){.dark{--sidebar-border:color-mix(in oklab,var(--color-white)5%,transparent)}}
.dark{--sidebar-ring:var(--color-neutral-400);--code:var(--background)}
@supports (color:color-mix(in lab, red, red)){.dark{--code:color-mix(in srgb,var(--background)98%,var(--color-white))}}
.dark{--code-foreground:var(--foreground);--code-highlight:#ffffff0a}
@supports (color:color-mix(in lab, red, red)){.dark{--code-highlight:color-mix(in oklab,var(--color-white)4%,transparent)}}
:where([class*="c-"], [class*="c-"] *, [class*="c-"]::before, [class*="c-"]::after, [class*="c-"] *::before, [class*="c-"] *::after){box-sizing:border-box;border:0 solid;margin:0;padding:0}
:where([class*="c-"]) :where(button,input,select,textarea,optgroup){font:inherit;font-feature-settings:inherit;letter-spacing:inherit;color:inherit;background-color:transparent;border-radius:0}
:where([class*="c-"]) :where(button,[type=button],[type=reset],[type=submit]){appearance:button}
:where([class*="c-"]) :where(img,svg,video,canvas){display:block;vertical-align:middle}
:where([class*="c-"]) :where(a){color:inherit;text-decoration:inherit}
:where([class*="c-"]) :where(ol,ul,menu){list-style:none}
:where([class*="c-"]) :where(table){text-indent:0;border-color:inherit;border-collapse:collapse}
:where([class*="c-"]) :where([hidden]:not([hidden=until-found])){display:none!important}
:where([class*="c-"]){border-color:var(--border);outline-color:var(--ring)}
:where([class*="c-"], [class*="c-"] *, [class*="c-"]::before, [class*="c-"]::after, [class*="c-"] *::before, [class*="c-"] *::after){--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-mask-linear:linear-gradient(#fff,#fff);--tw-mask-radial:linear-gradient(#fff,#fff);--tw-mask-conic:linear-gradient(#fff,#fff);--tw-mask-left:linear-gradient(#fff,#fff);--tw-mask-right:linear-gradient(#fff,#fff);--tw-mask-bottom:linear-gradient(#fff,#fff);--tw-mask-top:linear-gradient(#fff,#fff);--tw-mask-top-from-position:0%;--tw-mask-top-to-position:100%;--tw-mask-top-from-color:black;--tw-mask-top-to-color:transparent;--tw-mask-right-from-position:0%;--tw-mask-right-to-position:100%;--tw-mask-right-from-color:black;--tw-mask-right-to-color:transparent;--tw-mask-bottom-from-position:0%;--tw-mask-bottom-to-position:100%;--tw-mask-bottom-from-color:black;--tw-mask-bottom-to-color:transparent;--tw-mask-left-from-position:0%;--tw-mask-left-to-position:100%;--tw-mask-left-from-color:black;--tw-mask-left-to-color:transparent;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-border-spacing-x:0;--tw-border-spacing-y:0}
@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}
@property --tw-content{syntax:"*";inherits:false;initial-value:""}
@property --tw-duration{syntax:"*";inherits:false}
@property --tw-ease{syntax:"*";inherits:false}
@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-inset-shadow-color{syntax:"*";inherits:false}
@property --tw-leading{syntax:"*";inherits:false}
@property --tw-ring-color{syntax:"*";inherits:false}
@property --tw-ring-inset{syntax:"*";inherits:false}
@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}
@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}
@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}
@property --tw-shadow-color{syntax:"*";inherits:false}
@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}
@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}
@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){:where([class*="c-"], [class*="c-"] *){animation-duration:1ms!important;animation-iteration-count:1!important;transition-duration:1ms!important}}
/* Gráfico de línea y barras sin dependencias. Los datos viven en la <table>; chart.js dibuja el SVG.
Colores solo de variables del tema (cambian con cada combinación de Wada): --primary, --muted-foreground,
--foreground, --border. Sin degradados ni animación. Necesita tokens.css. */
.c-chart { margin: 0; width: 100%; max-width: 44rem; display: grid; gap: .5rem; font-family: inherit; color: var(--foreground); }
.c-chart__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .25rem 1rem; }
.c-chart__title { font-weight: 600; font-size: .9375rem; }
.c-chart__note { font-size: .75rem; color: var(--muted-foreground); }
.c-chart__legend { display: flex; flex-wrap: wrap; gap: .25rem 1rem; font-size: .75rem; color: var(--muted-foreground); }
.c-chart__key { display: inline-flex; align-items: center; gap: .375rem; }
.c-chart__key svg { width: 18px; height: 8px; overflow: visible; }
.c-chart__plot { position: relative; height: var(--chart-h, 240px); border-radius: calc(var(--radius) - 2px); outline: none; }
.c-chart__plot:focus-visible { box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring); }
.c-chart__plot svg { display: block; width: 100%; height: 100%; overflow: visible; }
.c-chart__grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }
.c-chart__axis { fill: var(--muted-foreground); font-size: 11px; font-variant-numeric: tabular-nums; }
.c-chart__line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.c-chart__dot { stroke: var(--background); stroke-width: 2; }
.c-chart__bar { shape-rendering: crispEdges; }
.c-chart__cursor { stroke: var(--foreground); stroke-opacity: .25; stroke-width: 1; }
.c-chart__s0 { stroke: var(--primary); fill: var(--primary); }
.c-chart__s1 { stroke: var(--muted-foreground); fill: var(--muted-foreground); }
.c-chart__s2 { stroke: var(--foreground); fill: var(--foreground); }
.c-chart__line.c-chart__s1 { stroke-dasharray: 5 4; }
.c-chart__line.c-chart__s2 { stroke-dasharray: 1 4; }
.c-chart__line { fill: none; }
.c-chart__bar.c-chart__s1 { fill-opacity: .55; }
.c-chart__tip { position: absolute; top: 0; pointer-events: none; z-index: 2; min-width: 7rem; padding: .375rem .5rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px); background: var(--popover); color: var(--popover-foreground); font-size: .75rem; line-height: 1.4; box-shadow: 0 4px 12px -4px #0000002e; white-space: nowrap; }
.c-chart__tip b { display: block; margin-bottom: .125rem; }
.c-chart__tip span { display: flex; justify-content: space-between; gap: .75rem; font-variant-numeric: tabular-nums; }
.c-chart__tip i { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; margin-right: .375rem; vertical-align: .05em; }
.c-chart__readout { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; margin: 0; }
.c-chart__data { font-size: .75rem; color: var(--muted-foreground); }
.c-chart__data summary { cursor: pointer; width: fit-content; border-radius: 4px; }
.c-chart__data summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.c-chart__data table { margin-top: .5rem; border-collapse: collapse; font-variant-numeric: tabular-nums; color: var(--foreground); }
.c-chart__data th, .c-chart__data td { padding: .25rem .75rem .25rem 0; text-align: left; border-bottom: 1px solid var(--border); font-weight: 400; }
.c-chart__data td { text-align: right; }
.c-chart__data thead th { color: var(--muted-foreground); }
@media (forced-colors: active) { .c-chart__s0, .c-chart__s1, .c-chart__s2 { stroke: CanvasText; fill: CanvasText; } }
/* página de prueba */
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:40px 24px;background:var(--background);color:var(--foreground);font-family:var(--font-sans);font-size:14px;line-height:1.5}
.c-demo{display:grid;gap:1rem;justify-items:center;width:auto;max-width:100%;min-width:0;justify-self:center;grid-template-columns:minmax(0,auto)}
body{grid-template-columns:minmax(0,1fr)}
.embed body{background:transparent}
</style>
</head>
<body>
<div class="c-demo">
<figure class="c-chart" data-c-chart data-type="line">
<figcaption class="c-chart__head">
<span class="c-chart__title">Visitas por semana</span>
<span class="c-chart__note">Datos de ejemplo</span>
</figcaption>
<div class="c-chart__legend" aria-hidden="true"></div>
<div class="c-chart__plot" tabindex="0"></div>
<p class="c-chart__readout" aria-live="polite"></p>
<details class="c-chart__data">
<summary>Ver datos</summary>
<table>
<thead><tr><th scope="col">Semana</th><th scope="col">2026</th><th scope="col">2025</th></tr></thead>
<tbody>
<tr><th scope="row">S1</th><td>1200</td><td>980</td></tr>
<tr><th scope="row">S2</th><td>1350</td><td>1010</td></tr>
<tr><th scope="row">S3</th><td>1280</td><td>1100</td></tr>
<tr><th scope="row">S4</th><td>1620</td><td>1150</td></tr>
<tr><th scope="row">S5</th><td>1780</td><td>1190</td></tr>
<tr><th scope="row">S6</th><td>1710</td><td>1240</td></tr>
<tr><th scope="row">S7</th><td>2050</td><td>1300</td></tr>
<tr><th scope="row">S8</th><td>2240</td><td>1360</td></tr>
</tbody>
</table>
</details>
</figure>
<figure class="c-chart" data-c-chart data-type="bar">
<figcaption class="c-chart__head">
<span class="c-chart__title">Pedidos por canal</span>
<span class="c-chart__note">Datos de ejemplo</span>
</figcaption>
<div class="c-chart__legend" aria-hidden="true"></div>
<div class="c-chart__plot" tabindex="0"></div>
<p class="c-chart__readout" aria-live="polite"></p>
<details class="c-chart__data">
<summary>Ver datos</summary>
<table>
<thead><tr><th scope="col">Canal</th><th scope="col">Septiembre</th><th scope="col">Agosto</th></tr></thead>
<tbody>
<tr><th scope="row">Web</th><td>420</td><td>380</td></tr>
<tr><th scope="row">Tienda</th><td>310</td><td>330</td></tr>
<tr><th scope="row">WhatsApp</th><td>260</td><td>190</td></tr>
<tr><th scope="row">Marketplace</th><td>140</td><td>150</td></tr>
</tbody>
</table>
</details>
</figure>
</div>
<script>
/* Gráfico de línea y barras sin dependencias. Lee los datos de la <table> del propio gráfico (primera
columna = etiquetas; cada columna siguiente = una serie) y dibuja un SVG que se adapta al ancho.
Teclado: enfoca el gráfico y usa ← → (Inicio/Fin) para recorrer los puntos; el valor se anuncia.
Uso: <figure data-c-chart data-type="line|bar"> … <table> … </figure>; Componentes.chart.init(root) tras insertar. */
(function () {
const C = (window.Componentes = window.Componentes || {});
const SVG = 'http://www.w3.org/2000/svg';
const fmt = new Intl.NumberFormat(document.documentElement.lang || 'es', { maximumFractionDigits: 1 });
function el(tag, attrs, parent) {
const n = document.createElementNS(SVG, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
function read(fig) {
const t = fig.querySelector('table');
const series = [...t.tHead.rows[0].cells].slice(1).map((c) => ({ name: c.textContent.trim(), values: [] }));
const labels = [];
[...t.tBodies[0].rows].forEach((r) => {
labels.push(r.cells[0].textContent.trim());
series.forEach((s, i) => s.values.push(parseFloat(String(r.cells[i + 1]?.textContent).replace(/[^\d.-]/g, '')) || 0));
});
return { labels, series: series.slice(0, 3) };
}
// Escala «redonda»: el máximo sube al siguiente 1, 2, 2.5 o 5 × 10^n.
function niceMax(v) {
if (v <= 0) return 1;
const p = Math.pow(10, Math.floor(Math.log10(v)));
for (const m of [1, 2, 2.5, 5, 10]) if (m * p >= v) return m * p;
return 10 * p;
}
function legend(fig, data, type) {
const box = fig.querySelector('.c-chart__legend');
if (!box) return;
box.innerHTML = '';
data.series.forEach((s, i) => {
const k = document.createElement('span');
k.className = 'c-chart__key';
const svg = el('svg', { viewBox: '0 0 18 8' }, k);
if (type === 'bar') el('rect', { x: 5, y: 0, width: 8, height: 8, rx: 1.5, class: `c-chart__bar c-chart__s${i}` }, svg);
else el('line', { x1: 0, y1: 4, x2: 18, y2: 4, class: `c-chart__line c-chart__s${i}` }, svg);
k.append(s.name);
box.appendChild(k);
});
}
function draw(fig) {
const plot = fig.querySelector('.c-chart__plot');
const data = fig._data;
const type = fig.dataset.type === 'bar' ? 'bar' : 'line';
const W = plot.clientWidth, H = plot.clientHeight;
if (!W || !H) return;
const pad = { t: 8, r: 8, b: 24, l: 40 };
const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b;
const max = niceMax(Math.max(...data.series.flatMap((s) => s.values)));
const y = (v) => pad.t + ih - (v / max) * ih;
const n = data.labels.length;
const step = type === 'bar' ? iw / n : iw / Math.max(1, n - 1);
const xAt = (i) => (type === 'bar' ? pad.l + step * i + step / 2 : pad.l + step * i);
plot.querySelector('svg')?.remove();
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', focusable: 'false' });
plot.prepend(svg);
for (let k = 0; k <= 4; k++) {
const v = (max / 4) * k, yy = Math.round(y(v)) + .5;
el('line', { x1: pad.l, x2: W - pad.r, y1: yy, y2: yy, class: 'c-chart__grid' }, svg);
el('text', { x: pad.l - 8, y: yy + 4, 'text-anchor': 'end', class: 'c-chart__axis' }, svg).textContent = fmt.format(v);
}
const every = Math.ceil(n / Math.max(1, Math.floor(iw / 56)));
data.labels.forEach((lab, i) => {
if (i % every) return;
el('text', { x: xAt(i), y: H - 6, 'text-anchor': 'middle', class: 'c-chart__axis' }, svg).textContent = lab;
});
const cursor = el('line', { x1: 0, x2: 0, y1: pad.t, y2: pad.t + ih, class: 'c-chart__cursor', visibility: 'hidden' }, svg);
if (type === 'bar') {
const groupW = step * .7, bw = groupW / data.series.length;
data.series.forEach((s, si) => s.values.forEach((v, i) => {
const x = pad.l + step * i + (step - groupW) / 2 + bw * si;
el('rect', { x: x + 1, y: y(v), width: Math.max(1, bw - 2), height: pad.t + ih - y(v), rx: 3, class: `c-chart__bar c-chart__s${si}` }, svg);
}));
} else {
data.series.forEach((s, si) => {
el('polyline', { points: s.values.map((v, i) => `${xAt(i)},${y(v)}`).join(' '), class: `c-chart__line c-chart__s${si}` }, svg);
});
}
const dots = data.series.map((s, si) => el('circle', { r: 4, class: `c-chart__dot c-chart__s${si}`, visibility: 'hidden' }, svg));
fig._geo = { xAt, y, step, pad, n, cursor, dots, type, W };
}
function show(fig, i) {
const g = fig._geo, data = fig._data, plot = fig.querySelector('.c-chart__plot');
if (!g) return;
let tip = plot.querySelector('.c-chart__tip');
if (i == null) { if (tip) tip.hidden = true; g.cursor.setAttribute('visibility', 'hidden'); g.dots.forEach((d) => d.setAttribute('visibility', 'hidden')); return; }
fig._i = i;
const x = g.xAt(i);
g.cursor.setAttribute('x1', x); g.cursor.setAttribute('x2', x); g.cursor.setAttribute('visibility', g.type === 'line' ? 'visible' : 'hidden');
g.dots.forEach((d, si) => { d.setAttribute('cx', x); d.setAttribute('cy', g.y(data.series[si].values[i])); d.setAttribute('visibility', g.type === 'line' ? 'visible' : 'hidden'); });
if (!tip) { tip = document.createElement('div'); tip.className = 'c-chart__tip'; tip.setAttribute('aria-hidden', 'true'); plot.appendChild(tip); }
tip.hidden = false;
tip.innerHTML = `<b></b>` + data.series.map((s, si) => `<span><span><i class="c-chart__s${si}" style="background:currentColor;color:var(--${['primary', 'muted-foreground', 'foreground'][si]})"></i>${s.name}</span><strong>${fmt.format(s.values[i])}</strong></span>`).join('');
tip.querySelector('b').textContent = data.labels[i];
const left = Math.min(Math.max(0, x + 12), g.W - tip.offsetWidth);
tip.style.left = (x + 12 + tip.offsetWidth > g.W ? x - 12 - tip.offsetWidth : left) + 'px';
const ro = fig.querySelector('.c-chart__readout');
if (ro) ro.textContent = `${data.labels[i]}: ` + data.series.map((s) => `${s.name} ${fmt.format(s.values[i])}`).join(', ');
}
function setup(fig) {
fig._data = read(fig);
const data = fig._data, plot = fig.querySelector('.c-chart__plot');
const type = fig.dataset.type === 'bar' ? 'bar' : 'line';
const title = fig.querySelector('.c-chart__title')?.textContent.trim() || 'Gráfico';
const s0 = data.series[0];
const last = s0.values.length - 1;
plot.setAttribute('role', 'img');
plot.setAttribute('aria-label', `${title}. ${type === 'bar' ? 'Barras' : 'Línea'} con ${data.series.length} series y ${data.labels.length} puntos; ${s0.name} va de ${fmt.format(s0.values[0])} en ${data.labels[0]} a ${fmt.format(s0.values[last])} en ${data.labels[last]}. Usa las flechas para recorrerlo; los datos están en «Ver datos».`);
legend(fig, data, type);
draw(fig);
plot.addEventListener('pointermove', (e) => {
const g = fig._geo; if (!g) return;
const r = plot.getBoundingClientRect(), px = e.clientX - r.left;
const i = g.type === 'bar' ? Math.floor((px - g.pad.l) / g.step) : Math.round((px - g.pad.l) / g.step);
show(fig, Math.max(0, Math.min(g.n - 1, i)));
});
plot.addEventListener('pointerleave', () => { if (document.activeElement !== plot) show(fig, null); });
plot.addEventListener('focus', () => show(fig, fig._i ?? 0));
plot.addEventListener('blur', () => show(fig, null));
plot.addEventListener('keydown', (e) => {
const n = data.labels.length, cur = fig._i ?? 0;
const next = { ArrowRight: cur + 1, ArrowLeft: cur - 1, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
show(fig, Math.max(0, Math.min(n - 1, next)));
});
new ResizeObserver(() => { draw(fig); if (document.activeElement === plot) show(fig, fig._i ?? 0); }).observe(plot);
}
function init(root) {
(root || document).querySelectorAll('[data-c-chart]:not([data-c-ready])').forEach((fig) => {
fig.dataset.cReady = '';
try { setup(fig); } catch (e) { console.warn('[chart]', e); }
});
}
C.chart = { init, redraw: (fig) => { fig._data = read(fig); draw(fig); } };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());
else init();
})();
</script>
</body>
</html>«Todo» trae tokens, dependencias y componente en un solo archivo, listo para abrir o dárselo a un agente.“All” bundles tokens, dependencies and the component in one file, ready to open or hand to an agent.
InstalaciónInstallation
- Agrega una vezAdd once
tokens.css(colores, radios, tema oscuro).(colors, radii, dark theme). - Copia los archivos:Copy the files:
chart.html,chart.css,chart.js. - Para el tema oscuro, pon la claseFor the dark theme, add the class
darkento<html>.
Cuándo usarloWhen to use it
ÚsaloUse it
- Para mostrar una tendencia (línea) o comparar categorías (barras).
- Hasta 3 series.
- To show a trend (line) or compare categories (bars).
- Up to 3 series.
Mejor noAvoid
- Para 2 o 3 números: escríbelos.
- Para partes de un todo con muchas categorías: usa una tabla.
- For 2 or 3 numbers: write them.
- For parts of a whole with many categories: use a table.
AccesibilidadAccessibility
| ← → | Recorre los puntos y anuncia sus valoresMoves through points and announces values |
| Inicio Fin | Primer o último puntoFirst or last point |
- El SVG tiene un resumen en aria-label; los valores exactos están en «Ver datos».
- Las series se distinguen por color y por trazo (continuo, discontinuo, punteado).
- Colores solo del tema: --primary y --muted-foreground; sin degradados ni animación.
- The SVG has a summary in aria-label; exact values are under “View data”.
- Series differ by color and stroke (solid, dashed, dotted).
- Theme colors only: --primary and --muted-foreground; no gradients or animation.
Cómo probarloHow to test it
- Enfoca el gráfico con Tab y pulsa →: se anuncia «S2: 2026 1350, 2025 1010».
- Tab to the chart and press →: “S2: 2026 1350, 2025 1010” is announced.