Un primer dibujo completo: rectángulos y limpieza
Escribir el primer canvas útil de principio a fin con los tres métodos de rectángulo, entender las diferencias entre ellos y ver el estado del contexto en acción.
Los tres métodos de rectángulo
ctx.fillRect(x, y, ancho, alto); // rellena con fillStylectx.strokeRect(x, y, ancho, alto); // traza el contorno con strokeStyle y lineWidthctx.clearRect(x, y, ancho, alto); // borra a negro transparenteSon los únicos métodos de dibujo que no tocan la ruta actual. Todos los demás —arc, lineTo, rect— construyen una ruta que hay que pintar después. Estos tres actúan de inmediato y no dejan rastro en el estado de rutas, lo que los hace seguros de usar en medio de la construcción de otra ruta sin efectos raros.
Sí respetan el resto del estado: la matriz de transformación, el recorte, globalAlpha, el modo de composición y las sombras. Un fillRect dentro de un contexto rotado sale rotado.
clearRect tiene una peculiaridad que hay que fijar: no pinta blanco, borra. Deja la zona en negro transparente, con alfa 0, de modo que se ve lo que haya detrás en la página. Si el contexto se creó con alpha: false, no hay transparencia posible y el resultado es negro opaco.
Y una segunda peculiaridad, mucho menos conocida: clearRect ignora el modo de composición, pero sí respeta el recorte. Es decir, si tienes una región de recorte activa, clearRect solo borra dentro de ella. Es un comportamiento útil que se usa poco.
// Limpiar todo el canvas, ignorando cualquier transformacion activafunction limpiarTodo(ctx) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); // matriz identidad ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.restore();}Esa función es de las que acaban en todos los proyectos. Sin el setTransform, un clearRect(0, 0, w, h) en un contexto escalado o trasladado deja zonas sin borrar, y el resultado son estelas fantasma que aparecen sin explicación aparente.
El estado se lee al dibujar, no al configurar
Un detalle conceptual que ordena mucho: las propiedades del contexto se consultan en el momento del dibujo, no se asocian a nada.
ctx.fillStyle = '#89b4fa';ctx.fillRect(10, 10, 50, 50);ctx.fillStyle = '#f38ba8'; // el rectangulo anterior NO cambiactx.fillRect(70, 10, 50, 50);Esto parece obvio escrito así, y sin embargo es la fuente del error más frecuente en el aprendizaje: asignar un estilo esperando que se aplique a algo ya dibujado. En el canvas el estado es un conjunto de variables globales del contexto, y cada llamada de dibujo toma una foto de ellas.
De ahí sale también la primera regla de rendimiento del canvas: cambiar de estilo tiene coste, y agrupar los dibujos por estilo es más rápido que alternar.
// Peor: 200 cambios de estadofor (const b of barras) { ctx.fillStyle = b.color; ctx.fillRect(...); }// Mejor cuando hay pocos colores: agrupar por colorfor (const [color, grupo] of porColor) { ctx.fillStyle = color; for (const b of grupo) ctx.fillRect(b.x, b.y, b.w, b.h);}Un gráfico de barras completo
Con esto ya se puede construir algo real. Este ejemplo funciona pegado tal cual y contiene el patrón que se repite en toda visualización: datos, escala, dibujo, en tres bloques separados.
<!doctype html><meta charset="utf-8"><style> body { background: #1e1e2e; color: #cdd6f4; font: 14px system-ui; margin: 2rem; } canvas { display: block; background: #181825; border-radius: 8px; }</style><canvas id="g" width="640" height="360"> <table> <caption>Ventas mensuales del primer semestre</caption> <tr><th>Mes</th><th>Ventas</th></tr> <tr><td>Ene</td><td>320</td></tr><tr><td>Feb</td><td>412</td></tr> <tr><td>Mar</td><td>288</td></tr><tr><td>Abr</td><td>501</td></tr> <tr><td>May</td><td>466</td></tr><tr><td>Jun</td><td>390</td></tr> </table></canvas><script> const ctx = document.getElementById('g').getContext('2d'); // 1. DATOS const datos = [ { mes: 'Ene', valor: 320 }, { mes: 'Feb', valor: 412 }, { mes: 'Mar', valor: 288 }, { mes: 'Abr', valor: 501 }, { mes: 'May', valor: 466 }, { mes: 'Jun', valor: 390 }, ]; // 2. ESCALA: del dominio de los datos al rango en pixeles const margen = { arriba: 24, derecha: 24, abajo: 40, izquierda: 52 }; const W = ctx.canvas.width, H = ctx.canvas.height; const anchoTrama = W - margen.izquierda - margen.derecha; const altoTrama = H - margen.arriba - margen.abajo; const maximo = Math.max(...datos.map(d => d.valor)); const techo = Math.ceil(maximo / 100) * 100; const escalaY = v => margen.arriba + altoTrama * (1 - v / techo); const paso = anchoTrama / datos.length; const anchoBarra = paso * 0.62; // 3. DIBUJO function pintar() { ctx.clearRect(0, 0, W, H); // Rejilla horizontal y etiquetas del eje ctx.strokeStyle = '#313244'; ctx.fillStyle = '#7f849c'; ctx.lineWidth = 1; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.font = '12px system-ui'; for (let v = 0; v <= techo; v += techo / 5) { const y = Math.round(escalaY(v)) + 0.5; // +0.5 para una linea nitida ctx.beginPath(); ctx.moveTo(margen.izquierda, y); ctx.lineTo(W - margen.derecha, y); ctx.stroke(); ctx.fillText(String(v), margen.izquierda - 10, y); } // Barras ctx.fillStyle = '#89b4fa'; datos.forEach((d, i) => { const x = margen.izquierda + paso * i + (paso - anchoBarra) / 2; const y = escalaY(d.valor); ctx.fillRect(x, y, anchoBarra, margen.arriba + altoTrama - y); }); // Etiquetas del eje X ctx.fillStyle = '#a6adc8'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; datos.forEach((d, i) => { ctx.fillText(d.mes, margen.izquierda + paso * (i + 0.5), H - margen.abajo + 10); }); } pintar();</script>Fíjate en tres decisiones. La primera, que hay un objeto margen en lugar de números sueltos: el margen es lo que separa el área de trazado del borde del canvas, y necesita nombre porque aparece en seis sitios. La segunda, que existe una función escalaY que convierte un valor del dominio en una coordenada: es la abstracción central de cualquier visualización. Y la tercera, que la rejilla usa Math.round(...) + 0.5 para que las líneas de un píxel salgan nítidas en lugar de grises.
Qué falta
Ese gráfico funciona, pero tiene tres carencias que el resto de la guía va a ir cubriendo, y conviene nombrarlas ahora para que sepas que no se te han pasado por alto.
Se ve borroso en pantallas de alta densidad, porque el búfer tiene 640 por 360 píxeles y en un portátil moderno ese elemento ocupa el doble de píxeles físicos. Es el problema del que se ocupa el nivel siguiente.
No se adapta al tamaño del contenedor: los números están fijos. Adaptarlo bien exige observar el tamaño real del elemento y reconstruir el búfer.
No tiene interacción: no hay tooltip al pasar el ratón, porque para eso hace falta hit testing, y el hit testing en canvas es un tema con entidad propia.
Ninguna de las tres es un fallo del código: son exactamente las tres cosas que el modo inmediato deja en tus manos.