Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> JavaScript

Cách xóa biểu đồ khỏi HTML5 Canvas để hover events không còn kích hoạt


Vấn đề

Khi vẽ chart (biểu đồ) lên canvas, các sự kiện hover/click thường được gắn trên canvas element. Để "xóa" chart và vô hiệu hóa events, cần xóa hoàn toàn canvas element cũ và tạo canvas mới.

Tại sao không dùng clearRect()?

ctx.clearRect() chỉ xóa pixel, **không xóa event listeners** gắn trên canvas. Các handler hover vẫn chạy, có thể gây lỗi logic hoặc memory leak.

Giải pháp: Replace Canvas Element

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>Reset Canvas Chart</title>
  <style>
    .chart-container { position: relative; width: 600px; height: 400px; margin: 2rem auto; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
    canvas { display: block; width: 100%; height: 100%; }
    .controls { text-align: center; margin: 1rem; }
    button { padding: 0.75rem 1.5rem; margin: 0 0.5rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
    button.danger { background: #e74c3c; }
    button:hover { opacity: 0.9; }
  </style>
</head>
<body>
  <div class="chart-container" id="chartContainer">
    <canvas id="chartCanvas"></canvas>
  </div>
  
  <div class="controls">
    <button onclick="drawChart()">Vẽ biểu đồ</button>
    <button class="danger" onclick="resetCanvas()">Xóa hoàn toàn (Reset Canvas)</button>
    <button onclick="clearOnly()">Chỉ clearRect (giữ events)</button>
  </div>
  
  <div id="log" style="max-width: 600px; margin: 0 auto; padding: 1rem; background: #f8f9fa; border-radius: 4px; font-family: monospace; font-size: 0.875rem; height: 150px; overflow-y: auto;"></div>

  <script>
    const container = document.getElementById('chartContainer');
    let canvas = document.getElementById('chartCanvas');
    let ctx = canvas.getContext('2d');
    let hoverHandler = null;
    let chartData = null;
    
    function log(msg) {
      const el = document.getElementById('log');
      const time = new Date().toLocaleTimeString();
      el.innerHTML += `
[${time}] ${msg}
`; el.scrollTop = el.scrollHeight; } // Tạo chart mẫu function drawChart() { if (!chartData) { chartData = generateChartData(); } // Resize canvas to container resizeCanvas(); // Draw chart drawBarChart(ctx, chartData); // Attach hover handler (giả lập chart library) attachHoverEvents(); log('Đã vẽ biểu đồ. Hover vào cột để xem tooltip.'); } function generateChartData() { return [ { label: 'Tháng 1', value: 65, color: '#3498db' }, { label: 'Tháng 2', value: 89, color: '#2ecc71' }, { label: 'Tháng 3', value: 42, color: '#f39c12' }, { label: 'Tháng 4', value: 110, color: '#e74c3c' }, { label: 'Tháng 5', value: 78, color: '#9b59b6' }, { label: 'Tháng 6', value: 95, color: '#1abc9c' }, ]; } function resizeCanvas() { const rect = container.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; } function drawBarChart(ctx, data) { const { width, height } = canvas; const padding = 50; const chartWidth = width - 2 * padding; const chartHeight = height - 2 * padding; const maxValue = Math.max(...data.map(d => d.value)); const barWidth = chartWidth / data.length * 0.7; const gap = chartWidth / data.length * 0.3; // Axes ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, height - padding); ctx.lineTo(width - padding, height - padding); ctx.stroke(); // Bars data.forEach((item, i) => { const x = padding + gap/2 + i * (barWidth + gap); const barHeight = (item.value / maxValue) * chartHeight; const y = height - padding - barHeight; // Bar ctx.fillStyle = item.color; ctx.fillRect(x, y, barWidth, barHeight); // Label ctx.fillStyle = '#333'; ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.fillText(item.label, x + barWidth/2, height - padding + 20); // Value on top ctx.fillText(item.value, x + barWidth/2, y - 5); // Store hit region for hover (simplified) item.hitRegion = { x, y, width: barWidth, height: barHeight }; }); } // Gắn hover events (giả lập chart library) function attachHoverEvents() { // Remove old handler if (hoverHandler) { canvas.removeEventListener('mousemove', hoverHandler); } hoverHandler = (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (!chartData) return; let found = false; for (const item of chartData) { const r = item.hitRegion; if (r && x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) { canvas.style.cursor = 'pointer'; canvas.title = `${item.label}: ${item.value}`; found = true; break; } } if (!found) { canvas.style.cursor = 'default'; canvas.title = ''; } }; canvas.addEventListener('mousemove', hoverHandler); log('Đã gắn hover event listener.'); } // Chỉ clear pixel - GARD EVENTS VẪN CÒN function clearOnly() { ctx.clearRect(0, 0, canvas.width, canvas.height); log('Đã clearRect(). Hover events VẪN HOẠT ĐỘNG (thử hover vào vị trí cột cũ).'); } // XÓA HOÀN TOÀN: Replace canvas element function resetCanvas() { // 1. Remove old canvas const oldCanvas = canvas; oldCanvas.remove(); // 2. Create new canvas canvas = document.createElement('canvas'); canvas.id = 'chartCanvas'; container.appendChild(canvas); // 3. Get new context ctx = canvas.getContext('2d'); // 4. Reset state chartData = null; hoverHandler = null; canvas.style.cursor = 'default'; canvas.title = ''; log('Đã REPLACE canvas element. Hover events ĐÃ BỊ XÓA HOÀN TOÀN.'); } </script> </body> </html>

Pattern khuyên dùng cho Chart Library (Chart.js, D3, v.v.)

class ChartManager {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.canvas = null;
    this.ctx = null;
    this.chartInstance = null; // Chart.js instance
    this.resizeObserver = null;
  }
  
  init() {
    this.createCanvas();
    this.setupResizeObserver();
  }
  
  createCanvas() {
    // Remove old
    if (this.canvas) {
      this.destroyChart();
      this.canvas.remove();
    }
    
    // Create new
    this.canvas = document.createElement('canvas');
    this.canvas.id = 'chart-canvas';
    this.container.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d');
    this.resizeCanvas();
  }
  
  resizeCanvas() {
    const rect = this.container.getBoundingClientRect();
    this.canvas.width = rect.width;
    this.canvas.height = rect.height;
  }
  
  setupResizeObserver() {
    this.resizeObserver = new ResizeObserver(() => {
      this.resizeCanvas();
      this.redraw();
    });
    this.resizeObserver.observe(this.container);
  }
  
  drawChart(data, options) {
    this.destroyChart(); // Destroy old chart instance (removes its listeners)
    
    // Example with Chart.js
    this.chartInstance = new Chart(this.ctx, {
      type: 'bar',
      data: data,
      options: { ...options, responsive: false, maintainAspectRatio: false }
    });
  }
  
  destroyChart() {
    if (this.chartInstance) {
      this.chartInstance.destroy(); // Chart.js destroys its own event listeners
      this.chartInstance = null;
    }
  }
  
  reset() {
    this.destroyChart();
    this.createCanvas(); // Fresh canvas = fresh event surface
  }
  
  redraw() {
    if (this.chartInstance) {
      this.chartInstance.resize();
      this.chartInstance.update();
    }
  }
}

Tóm tắt

Phương phápXóa pixelXóa event listenersDùng khi
ctx.clearRect()✓✗Redraw cùng chart, cùng data structure
chartInstance.destroy()✓ (lib lo)✓ (lib lo)Dùng Chart.js, D3, ApexCharts...
Replace <canvas> element✓✓ (hoàn toàn)Không dùng library, hoặc library không có destroy()