class RetroBarChart { constructor(canvasId, data, options = {}) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.data = data; this.options = { barColor: '#d4be98', barHoverColor: '#d4be98', textColor: 'white', tooltipBgColor: 'rgba(0, 0, 0, 0.8)', tooltipTextColor: '#d4be98', barWidth: 0, spacing: 0, title: '', titleColor: 'white', titleFont: '16px Mix', yAxisColor: 'white', yAxisFont: '12px Mix', formatTooltip: (label, value) => `${label}: ${value}`, ...options }; this.setupCanvas(); this.setupEventListeners(); this.setupMutationObserver(); // Initial draw attempt this.tryDraw(); } setupCanvas() { if (this.isCanvasVisible()) { const parentWidth = this.canvas.parentElement.clientWidth; const parentHeight = Math.min(300, parentWidth * 0.5); this.canvas.width = parentWidth; this.canvas.height = parentHeight; this.ctx.imageSmoothingEnabled = false; const titleHeight = this.options.title ? 30 : 0; const titleSpacing = this.options.title ? 20 : 0; this.chartArea = { top: 10 + titleHeight + titleSpacing, right: this.canvas.width - 10, bottom: this.canvas.height - 30, left: 40 }; if (!this.options.barWidth) { this.options.barWidth = Math.floor((this.chartArea.right - this.chartArea.left) / (this.data.length + 1)); } if (!this.options.spacing) { this.options.spacing = Math.floor((this.chartArea.right - this.chartArea.left - this.options.barWidth * this.data.length) / (this.data.length + 1)); } } } setupEventListeners() { this.canvas.addEventListener('mousemove', this.handleHover.bind(this)); this.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); window.addEventListener('resize', this.handleResize.bind(this)); } setupMutationObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { this.handleVisibilityChange(); } }); }); // Observe the canvas and all its ancestor elements let element = this.canvas; while (element && element !== document.body) { observer.observe(element, { attributes: true, attributeFilter: ['style'] }); element = element.parentElement; } } handleVisibilityChange() { if (this.isCanvasVisible()) { this.setupCanvas(); this.draw(); } } isCanvasVisible() { let element = this.canvas; while (element) { const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden') { return false; } element = element.parentElement; } return true; } handleResize() { this.setupCanvas(); this.tryDraw(); } tryDraw() { if (this.isCanvasVisible()) { this.draw(); } else { // If not visible, set up a periodic check if (!this.drawAttemptInterval) { this.drawAttemptInterval = setInterval(() => { if (this.isCanvasVisible()) { this.draw(); clearInterval(this.drawAttemptInterval); this.drawAttemptInterval = null; } }, 100); // Check every 100ms } } } draw() { if (!this.isCanvasVisible()) return; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.drawTitle(); this.drawYAxis(); this.drawBars(); } drawTitle() { if (this.options.title) { this.ctx.fillStyle = this.options.titleColor; this.ctx.font = this.options.titleFont; this.ctx.textAlign = 'center'; this.ctx.fillText(this.options.title, this.canvas.width / 2, 30); } } drawYAxis() { const maxValue = Math.max(...this.data.map(item => item.value)); const yAxisSteps = 5; const stepValue = maxValue / yAxisSteps; this.ctx.strokeStyle = this.options.yAxisColor; this.ctx.fillStyle = this.options.yAxisColor; this.ctx.font = this.options.yAxisFont; this.ctx.textAlign = 'right'; for (let i = 0; i <= yAxisSteps; i++) { const y = this.chartArea.bottom - (i / yAxisSteps) * (this.chartArea.bottom - this.chartArea.top); const value = Math.round(i * stepValue); // Draw tick this.ctx.beginPath(); this.ctx.moveTo(this.chartArea.left - 5, y); this.ctx.lineTo(this.chartArea.left, y); this.ctx.stroke(); // Draw value this.ctx.fillText(value.toString(), this.chartArea.left - 8, y + 4); } } drawBars() { const maxValue = Math.max(...this.data.map(item => item.value)); this.data.forEach((item, index) => { const x = this.chartArea.left + this.options.spacing + (this.options.barWidth + this.options.spacing) * index; const barHeight = Math.floor((item.value / maxValue) * (this.chartArea.bottom - this.chartArea.top)); const y = this.chartArea.bottom - barHeight; // Draw bar this.ctx.fillStyle = this.options.barColor; for (let i = 0; i < barHeight; i += 2) { this.ctx.fillRect(x, y + i, this.options.barWidth, 1); } // Draw value this.ctx.fillStyle = this.options.textColor; this.ctx.font = `bold ${Math.max(12, Math.floor(this.options.barWidth / 4))}px Mix`; this.ctx.textAlign = 'center'; this.ctx.fillText(item.value.toString(), x + this.options.barWidth / 2, this.chartArea.bottom + 15); // Draw label this.ctx.font = `${Math.max(10, Math.floor(this.options.barWidth / 5))}px Mix`; this.ctx.fillText(item.label, x + this.options.barWidth / 2, this.chartArea.bottom + 28); }); } handleHover(event) { const rect = this.canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; this.draw(); let hovered = false; this.data.forEach((item, index) => { const barX = this.chartArea.left + this.options.spacing + (this.options.barWidth + this.options.spacing) * index; const barHeight = Math.floor((item.value / Math.max(...this.data.map(d => d.value))) * (this.chartArea.bottom - this.chartArea.top)); const barY = this.chartArea.bottom - barHeight; if (x >= barX && x <= barX + this.options.barWidth && y >= barY && y <= this.chartArea.bottom) { hovered = true; // Highlight hovered bar this.ctx.fillStyle = this.options.barHoverColor; this.ctx.fillRect(barX, barY, this.options.barWidth, barHeight); // Show tooltip const tooltipContent = this.options.formatTooltip(item.label, item.value); this.drawTooltip(x, y, tooltipContent); } }); this.canvas.style.cursor = hovered ? 'pointer' : 'default'; } drawTooltip(x, y, content) { this.ctx.font = 'bold 14px Mix'; const metrics = this.ctx.measureText(content); const tooltipWidth = metrics.width + 20; // Add padding const tooltipHeight = 30; const tooltipX = Math.max(0, Math.min(x - tooltipWidth / 2, this.canvas.width - tooltipWidth)); const tooltipY = Math.max(0, y - tooltipHeight - 5); // Draw tooltip background this.ctx.fillStyle = this.options.tooltipBgColor; this.ctx.beginPath(); this.ctx.roundRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight, 5); this.ctx.fill(); // Draw tooltip text this.ctx.fillStyle = this.options.tooltipTextColor; this.ctx.textAlign = 'center'; this.ctx.fillText(content, tooltipX + tooltipWidth / 2, tooltipY + 20); } handleMouseLeave() { this.draw(); this.canvas.style.cursor = 'default'; } updateData(newData) { this.data = newData; this.setupCanvas(); this.tryDraw(); } updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; this.setupCanvas(); this.tryDraw(); } }