{
  "name": "chart",
  "title": {
    "es": "Gráfico",
    "en": "Chart"
  },
  "summary": {
    "es": "Línea y barras sin dependencias; los datos viven en una tabla HTML.",
    "en": "Line and bar charts with no dependencies; data lives in an HTML table."
  },
  "category": "dashboard",
  "status": "stable",
  "nivel": "componentes",
  "updated": "2026-09-30",
  "license": "MIT",
  "page": "https://componentes-lovat.vercel.app/c/chart/",
  "requires": [],
  "files": [
    {
      "type": "html",
      "url": "https://componentes-lovat.vercel.app/raw/chart/chart.html",
      "content": "<figure class=\"c-chart\" data-c-chart data-type=\"line\">\n  <figcaption class=\"c-chart__head\">\n    <span class=\"c-chart__title\">Visitas por semana</span>\n    <span class=\"c-chart__note\">Datos de ejemplo</span>\n  </figcaption>\n  <div class=\"c-chart__legend\" aria-hidden=\"true\"></div>\n  <div class=\"c-chart__plot\" tabindex=\"0\"></div>\n  <p class=\"c-chart__readout\" aria-live=\"polite\"></p>\n  <details class=\"c-chart__data\">\n    <summary>Ver datos</summary>\n    <table>\n      <thead><tr><th scope=\"col\">Semana</th><th scope=\"col\">2026</th><th scope=\"col\">2025</th></tr></thead>\n      <tbody>\n        <tr><th scope=\"row\">S1</th><td>1200</td><td>980</td></tr>\n        <tr><th scope=\"row\">S2</th><td>1350</td><td>1010</td></tr>\n        <tr><th scope=\"row\">S3</th><td>1280</td><td>1100</td></tr>\n        <tr><th scope=\"row\">S4</th><td>1620</td><td>1150</td></tr>\n        <tr><th scope=\"row\">S5</th><td>1780</td><td>1190</td></tr>\n        <tr><th scope=\"row\">S6</th><td>1710</td><td>1240</td></tr>\n        <tr><th scope=\"row\">S7</th><td>2050</td><td>1300</td></tr>\n        <tr><th scope=\"row\">S8</th><td>2240</td><td>1360</td></tr>\n      </tbody>\n    </table>\n  </details>\n</figure>\n\n<figure class=\"c-chart\" data-c-chart data-type=\"bar\">\n  <figcaption class=\"c-chart__head\">\n    <span class=\"c-chart__title\">Pedidos por canal</span>\n    <span class=\"c-chart__note\">Datos de ejemplo</span>\n  </figcaption>\n  <div class=\"c-chart__legend\" aria-hidden=\"true\"></div>\n  <div class=\"c-chart__plot\" tabindex=\"0\"></div>\n  <p class=\"c-chart__readout\" aria-live=\"polite\"></p>\n  <details class=\"c-chart__data\">\n    <summary>Ver datos</summary>\n    <table>\n      <thead><tr><th scope=\"col\">Canal</th><th scope=\"col\">Septiembre</th><th scope=\"col\">Agosto</th></tr></thead>\n      <tbody>\n        <tr><th scope=\"row\">Web</th><td>420</td><td>380</td></tr>\n        <tr><th scope=\"row\">Tienda</th><td>310</td><td>330</td></tr>\n        <tr><th scope=\"row\">WhatsApp</th><td>260</td><td>190</td></tr>\n        <tr><th scope=\"row\">Marketplace</th><td>140</td><td>150</td></tr>\n      </tbody>\n    </table>\n  </details>\n</figure>\n"
    },
    {
      "type": "css",
      "url": "https://componentes-lovat.vercel.app/raw/chart/chart.css",
      "content": "/* Gráfico de línea y barras sin dependencias. Los datos viven en la <table>; chart.js dibuja el SVG.\n   Colores solo de variables del tema (cambian con cada combinación de Wada): --primary, --muted-foreground,\n   --foreground, --border. Sin degradados ni animación. Necesita tokens.css. */\n.c-chart { margin: 0; width: 100%; max-width: 44rem; display: grid; gap: .5rem; font-family: inherit; color: var(--foreground); }\n.c-chart__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .25rem 1rem; }\n.c-chart__title { font-weight: 600; font-size: .9375rem; }\n.c-chart__note { font-size: .75rem; color: var(--muted-foreground); }\n.c-chart__legend { display: flex; flex-wrap: wrap; gap: .25rem 1rem; font-size: .75rem; color: var(--muted-foreground); }\n.c-chart__key { display: inline-flex; align-items: center; gap: .375rem; }\n.c-chart__key svg { width: 18px; height: 8px; overflow: visible; }\n.c-chart__plot { position: relative; height: var(--chart-h, 240px); border-radius: calc(var(--radius) - 2px); outline: none; }\n.c-chart__plot:focus-visible { box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring); }\n.c-chart__plot svg { display: block; width: 100%; height: 100%; overflow: visible; }\n.c-chart__grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }\n.c-chart__axis { fill: var(--muted-foreground); font-size: 11px; font-variant-numeric: tabular-nums; }\n.c-chart__line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }\n.c-chart__dot { stroke: var(--background); stroke-width: 2; }\n.c-chart__bar { shape-rendering: crispEdges; }\n.c-chart__cursor { stroke: var(--foreground); stroke-opacity: .25; stroke-width: 1; }\n.c-chart__s0 { stroke: var(--primary); fill: var(--primary); }\n.c-chart__s1 { stroke: var(--muted-foreground); fill: var(--muted-foreground); }\n.c-chart__s2 { stroke: var(--foreground); fill: var(--foreground); }\n.c-chart__line.c-chart__s1 { stroke-dasharray: 5 4; }\n.c-chart__line.c-chart__s2 { stroke-dasharray: 1 4; }\n.c-chart__line { fill: none; }\n.c-chart__bar.c-chart__s1 { fill-opacity: .55; }\n.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; }\n.c-chart__tip b { display: block; margin-bottom: .125rem; }\n.c-chart__tip span { display: flex; justify-content: space-between; gap: .75rem; font-variant-numeric: tabular-nums; }\n.c-chart__tip i { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; margin-right: .375rem; vertical-align: .05em; }\n.c-chart__readout { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; margin: 0; }\n.c-chart__data { font-size: .75rem; color: var(--muted-foreground); }\n.c-chart__data summary { cursor: pointer; width: fit-content; border-radius: 4px; }\n.c-chart__data summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n.c-chart__data table { margin-top: .5rem; border-collapse: collapse; font-variant-numeric: tabular-nums; color: var(--foreground); }\n.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; }\n.c-chart__data td { text-align: right; }\n.c-chart__data thead th { color: var(--muted-foreground); }\n@media (forced-colors: active) { .c-chart__s0, .c-chart__s1, .c-chart__s2 { stroke: CanvasText; fill: CanvasText; } }\n"
    },
    {
      "type": "js",
      "url": "https://componentes-lovat.vercel.app/raw/chart/chart.js",
      "content": "/* Gráfico de línea y barras sin dependencias. Lee los datos de la <table> del propio gráfico (primera\n   columna = etiquetas; cada columna siguiente = una serie) y dibuja un SVG que se adapta al ancho.\n   Teclado: enfoca el gráfico y usa ← → (Inicio/Fin) para recorrer los puntos; el valor se anuncia.\n   Uso: <figure data-c-chart data-type=\"line|bar\"> … <table> … </figure>; Componentes.chart.init(root) tras insertar. */\n(function () {\n  const C = (window.Componentes = window.Componentes || {});\n  const SVG = 'http://www.w3.org/2000/svg';\n  const fmt = new Intl.NumberFormat(document.documentElement.lang || 'es', { maximumFractionDigits: 1 });\n\n  function el(tag, attrs, parent) {\n    const n = document.createElementNS(SVG, tag);\n    for (const k in attrs) n.setAttribute(k, attrs[k]);\n    if (parent) parent.appendChild(n);\n    return n;\n  }\n\n  function read(fig) {\n    const t = fig.querySelector('table');\n    const series = [...t.tHead.rows[0].cells].slice(1).map((c) => ({ name: c.textContent.trim(), values: [] }));\n    const labels = [];\n    [...t.tBodies[0].rows].forEach((r) => {\n      labels.push(r.cells[0].textContent.trim());\n      series.forEach((s, i) => s.values.push(parseFloat(String(r.cells[i + 1]?.textContent).replace(/[^\\d.-]/g, '')) || 0));\n    });\n    return { labels, series: series.slice(0, 3) };\n  }\n\n  // Escala «redonda»: el máximo sube al siguiente 1, 2, 2.5 o 5 × 10^n.\n  function niceMax(v) {\n    if (v <= 0) return 1;\n    const p = Math.pow(10, Math.floor(Math.log10(v)));\n    for (const m of [1, 2, 2.5, 5, 10]) if (m * p >= v) return m * p;\n    return 10 * p;\n  }\n\n  function legend(fig, data, type) {\n    const box = fig.querySelector('.c-chart__legend');\n    if (!box) return;\n    box.innerHTML = '';\n    data.series.forEach((s, i) => {\n      const k = document.createElement('span');\n      k.className = 'c-chart__key';\n      const svg = el('svg', { viewBox: '0 0 18 8' }, k);\n      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);\n      else el('line', { x1: 0, y1: 4, x2: 18, y2: 4, class: `c-chart__line c-chart__s${i}` }, svg);\n      k.append(s.name);\n      box.appendChild(k);\n    });\n  }\n\n  function draw(fig) {\n    const plot = fig.querySelector('.c-chart__plot');\n    const data = fig._data;\n    const type = fig.dataset.type === 'bar' ? 'bar' : 'line';\n    const W = plot.clientWidth, H = plot.clientHeight;\n    if (!W || !H) return;\n    const pad = { t: 8, r: 8, b: 24, l: 40 };\n    const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b;\n    const max = niceMax(Math.max(...data.series.flatMap((s) => s.values)));\n    const y = (v) => pad.t + ih - (v / max) * ih;\n    const n = data.labels.length;\n    const step = type === 'bar' ? iw / n : iw / Math.max(1, n - 1);\n    const xAt = (i) => (type === 'bar' ? pad.l + step * i + step / 2 : pad.l + step * i);\n\n    plot.querySelector('svg')?.remove();\n    const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', focusable: 'false' });\n    plot.prepend(svg);\n    for (let k = 0; k <= 4; k++) {\n      const v = (max / 4) * k, yy = Math.round(y(v)) + .5;\n      el('line', { x1: pad.l, x2: W - pad.r, y1: yy, y2: yy, class: 'c-chart__grid' }, svg);\n      el('text', { x: pad.l - 8, y: yy + 4, 'text-anchor': 'end', class: 'c-chart__axis' }, svg).textContent = fmt.format(v);\n    }\n    const every = Math.ceil(n / Math.max(1, Math.floor(iw / 56)));\n    data.labels.forEach((lab, i) => {\n      if (i % every) return;\n      el('text', { x: xAt(i), y: H - 6, 'text-anchor': 'middle', class: 'c-chart__axis' }, svg).textContent = lab;\n    });\n    const cursor = el('line', { x1: 0, x2: 0, y1: pad.t, y2: pad.t + ih, class: 'c-chart__cursor', visibility: 'hidden' }, svg);\n\n    if (type === 'bar') {\n      const groupW = step * .7, bw = groupW / data.series.length;\n      data.series.forEach((s, si) => s.values.forEach((v, i) => {\n        const x = pad.l + step * i + (step - groupW) / 2 + bw * si;\n        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);\n      }));\n    } else {\n      data.series.forEach((s, si) => {\n        el('polyline', { points: s.values.map((v, i) => `${xAt(i)},${y(v)}`).join(' '), class: `c-chart__line c-chart__s${si}` }, svg);\n      });\n    }\n    const dots = data.series.map((s, si) => el('circle', { r: 4, class: `c-chart__dot c-chart__s${si}`, visibility: 'hidden' }, svg));\n    fig._geo = { xAt, y, step, pad, n, cursor, dots, type, W };\n  }\n\n  function show(fig, i) {\n    const g = fig._geo, data = fig._data, plot = fig.querySelector('.c-chart__plot');\n    if (!g) return;\n    let tip = plot.querySelector('.c-chart__tip');\n    if (i == null) { if (tip) tip.hidden = true; g.cursor.setAttribute('visibility', 'hidden'); g.dots.forEach((d) => d.setAttribute('visibility', 'hidden')); return; }\n    fig._i = i;\n    const x = g.xAt(i);\n    g.cursor.setAttribute('x1', x); g.cursor.setAttribute('x2', x); g.cursor.setAttribute('visibility', g.type === 'line' ? 'visible' : 'hidden');\n    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'); });\n    if (!tip) { tip = document.createElement('div'); tip.className = 'c-chart__tip'; tip.setAttribute('aria-hidden', 'true'); plot.appendChild(tip); }\n    tip.hidden = false;\n    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('');\n    tip.querySelector('b').textContent = data.labels[i];\n    const left = Math.min(Math.max(0, x + 12), g.W - tip.offsetWidth);\n    tip.style.left = (x + 12 + tip.offsetWidth > g.W ? x - 12 - tip.offsetWidth : left) + 'px';\n    const ro = fig.querySelector('.c-chart__readout');\n    if (ro) ro.textContent = `${data.labels[i]}: ` + data.series.map((s) => `${s.name} ${fmt.format(s.values[i])}`).join(', ');\n  }\n\n  function setup(fig) {\n    fig._data = read(fig);\n    const data = fig._data, plot = fig.querySelector('.c-chart__plot');\n    const type = fig.dataset.type === 'bar' ? 'bar' : 'line';\n    const title = fig.querySelector('.c-chart__title')?.textContent.trim() || 'Gráfico';\n    const s0 = data.series[0];\n    const last = s0.values.length - 1;\n    plot.setAttribute('role', 'img');\n    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».`);\n    legend(fig, data, type);\n    draw(fig);\n    plot.addEventListener('pointermove', (e) => {\n      const g = fig._geo; if (!g) return;\n      const r = plot.getBoundingClientRect(), px = e.clientX - r.left;\n      const i = g.type === 'bar' ? Math.floor((px - g.pad.l) / g.step) : Math.round((px - g.pad.l) / g.step);\n      show(fig, Math.max(0, Math.min(g.n - 1, i)));\n    });\n    plot.addEventListener('pointerleave', () => { if (document.activeElement !== plot) show(fig, null); });\n    plot.addEventListener('focus', () => show(fig, fig._i ?? 0));\n    plot.addEventListener('blur', () => show(fig, null));\n    plot.addEventListener('keydown', (e) => {\n      const n = data.labels.length, cur = fig._i ?? 0;\n      const next = { ArrowRight: cur + 1, ArrowLeft: cur - 1, Home: 0, End: n - 1 }[e.key];\n      if (next === undefined) return;\n      e.preventDefault();\n      show(fig, Math.max(0, Math.min(n - 1, next)));\n    });\n    new ResizeObserver(() => { draw(fig); if (document.activeElement === plot) show(fig, fig._i ?? 0); }).observe(plot);\n  }\n\n  function init(root) {\n    (root || document).querySelectorAll('[data-c-chart]:not([data-c-ready])').forEach((fig) => {\n      fig.dataset.cReady = '';\n      try { setup(fig); } catch (e) { console.warn('[chart]', e); }\n    });\n  }\n\n  C.chart = { init, redraw: (fig) => { fig._data = read(fig); draw(fig); } };\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());\n  else init();\n})();\n"
    },
    {
      "type": "standalone",
      "url": "https://componentes-lovat.vercel.app/r/chart.html"
    }
  ],
  "tokensFile": "https://componentes-lovat.vercel.app/tokens.css",
  "darkTheme": "add class 'dark' to <html>",
  "contexto": "herramienta",
  "densidad": "alta",
  "tono": "sobria",
  "states": [
    "rest",
    "hover",
    "focus"
  ],
  "use": {
    "es": [
      "Para mostrar una tendencia (línea) o comparar categorías (barras).",
      "Hasta 3 series."
    ],
    "en": [
      "To show a trend (line) or compare categories (bars).",
      "Up to 3 series."
    ]
  },
  "avoid": {
    "es": [
      "Para 2 o 3 números: escríbelos.",
      "Para partes de un todo con muchas categorías: usa una tabla."
    ],
    "en": [
      "For 2 or 3 numbers: write them.",
      "For parts of a whole with many categories: use a table."
    ]
  },
  "a11y": {
    "roles": [
      "img",
      "table"
    ],
    "keyboard": [
      {
        "keys": "← / →",
        "es": "Recorre los puntos y anuncia sus valores",
        "en": "Moves through points and announces values"
      },
      {
        "keys": "Inicio / Fin",
        "es": "Primer o último punto",
        "en": "First or last point"
      }
    ],
    "notes": {
      "es": [
        "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."
      ],
      "en": [
        "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."
      ]
    },
    "test": {
      "es": [
        "Enfoca el gráfico con Tab y pulsa →: se anuncia «S2: 2026 1350, 2025 1010»."
      ],
      "en": [
        "Tab to the chart and press →: “S2: 2026 1350, 2025 1010” is announced."
      ]
    }
  },
  "issues": {
    "es": [],
    "en": []
  },
  "credits": [
    {
      "source": "W3C WAI, Complex images",
      "url": "https://www.w3.org/WAI/tutorials/images/complex/"
    }
  ],
  "changes": [
    {
      "date": "2026-09-30",
      "es": "Primera versión",
      "en": "First version"
    }
  ],
  "partes": [],
  "pieza": "https://componentes-lovat.vercel.app/r/chart.html"
}
