1
0

initial commit

This commit is contained in:
anon 2024-10-09 23:19:36 +00:00
commit 39ce55c9c4
2 changed files with 371 additions and 0 deletions

112
readme.md Normal file
View File

@ -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
<script src="retrochart.js"></script>
```
add a canvas element:
```html
<canvas id="myChart"></canvas>
```
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
<!DOCTYPE html>
<html>
<head>
<title>retrochart.js example</title>
<script src="retrochart.js"></script>
<style>
body {
background-color: #2e3440;
color: #d8dee9;
font-family: sans-serif;
text-align: center;
}
canvas {
margin-top: 50px;
}
</style>
</head>
<body>
<canvas id="myChart"></canvas>
<script>
const data = [
{ label: 'january', value: 20 },
{ label: 'february', value: 35 },
{ label: 'march', value: 25 },
{ label: 'april', value: 40 },
];
const options = {
barColor: '#88c0d0',
barHoverColor: '#81a1c1',
title: 'monthly revenue',
titleColor: '#eceff4',
titleFont: '24px Arial',
textColor: '#eceff4',
tooltipBgColor: 'rgba(46, 52, 64, 0.8)',
tooltipTextColor: '#88c0d0',
formatTooltip: (label, value) => `${label}: $${value}k`,
};
const chart = new RetroBarChart('myChart', data, options);
</script>
</body>
</html>
```
## notes
- no external libraries required.
- handles canvas resizing and visibility changes automatically.
- supports tooltips and hover effects.
## license
do whatever you want with it.

259
retrochart.js Normal file
View File

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