1
0
retrochart/retrochart.js
2024-10-09 23:19:36 +00:00

259 lines
8.9 KiB
JavaScript

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();
}
}