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áp | Xóa pixel | Xóa event listeners | Dù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() |