From 39ce55c9c49baf6c5dc32970f153d04b8e50b015 Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 9 Oct 2024 23:19:36 +0000 Subject: [PATCH] initial commit --- readme.md | 112 ++++++++++++++++++++++ retrochart.js | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 readme.md create mode 100644 retrochart.js diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..92b4d26 --- /dev/null +++ b/readme.md @@ -0,0 +1,112 @@ +# retrochart + +sometimes you just want a simple bar chart without the hassle. `retrochart.js` is a minimalist library for drawing bar charts on an HTML5 canvas. no dependencies, no bloat—just straightforward charting. + +## usage + +include the script in your html: + +```html + +``` + +add a canvas element: + +```html + +``` + +initialize the chart with your data: + +```javascript +const data = [ + { label: 'apples', value: 30 }, + { label: 'bananas', value: 15 }, + { label: 'cherries', value: 25 }, +]; + +const chart = new RetroBarChart('myChart', data); +``` + +## customization + +you can customize the chart with options: + +```javascript +const options = { + barColor: '#ff6347', + barHoverColor: '#ffa07a', + title: 'fruit sales', + titleColor: '#ffffff', + titleFont: '20px Arial', + textColor: '#ffffff', + tooltipBgColor: 'rgba(0, 0, 0, 0.7)', + tooltipTextColor: '#ff6347', + formatTooltip: (label, value) => `${label}: ${value} sold`, +}; + +const chart = new RetroBarChart('myChart', data, options); +``` + +## methods + +- `updateData(newData)` - update the chart with new data. +- `updateOptions(newOptions)` - update chart options. + +## full example + +```html + + + + retrochart.js example + + + + + + + + +``` + +## notes + +- no external libraries required. +- handles canvas resizing and visibility changes automatically. +- supports tooltips and hover effects. + +## license + +do whatever you want with it. \ No newline at end of file diff --git a/retrochart.js b/retrochart.js new file mode 100644 index 0000000..48d09ac --- /dev/null +++ b/retrochart.js @@ -0,0 +1,259 @@ +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(); + } +} \ No newline at end of file