initial commit
This commit is contained in:
commit
39ce55c9c4
112
readme.md
Normal file
112
readme.md
Normal 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
259
retrochart.js
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user