first
This commit is contained in:
301
README.md
Normal file
301
README.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# ASCII Bar Chart
|
||||||
|
|
||||||
|
A dynamic, real-time ASCII-based vertical bar chart library that renders beautiful terminal-style visualizations using monospaced characters. Perfect for dashboards, monitoring tools, or any application that needs lightweight, text-based data visualization.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📊 **Real-time Updates**: Add data points dynamically and watch the chart animate
|
||||||
|
- 📏 **Auto-scaling**: Y-axis automatically adjusts to show min/max values
|
||||||
|
- 🔤 **Monospaced Rendering**: Uses 'X' characters in Courier font for clean visualization
|
||||||
|
- 📱 **Responsive**: Automatically adjusts font size to fit container width
|
||||||
|
- 🏷️ **Customizable Labels**: Add title, X-axis, and vertical Y-axis labels
|
||||||
|
- 🎯 **Scrolling Data**: Old data automatically scrolls off as new data arrives
|
||||||
|
- 🎨 **Terminal Theme**: Retro green-on-black aesthetic
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
```
|
||||||
|
Real-Time Data Visualization
|
||||||
|
|
||||||
|
C 20 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
o 19 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
u 18 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
n 17 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
t 16 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
15 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
14 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
13 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
12 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
11 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
10 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
9 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
8 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
7 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
6 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
5 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
4 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
3 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
2 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
1 | X X X X X X X X X X X X X X X X X X X X
|
||||||
|
+----------------------------------------
|
||||||
|
1 6 11 16 21 26 31 36 41
|
||||||
|
|
||||||
|
Time (seconds)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Simply include the JavaScript file in your HTML:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="ascii-bar-chart.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
#chart {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: #000;
|
||||||
|
color: #0f0;
|
||||||
|
padding: 20px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="chart"></div>
|
||||||
|
|
||||||
|
<script src="ascii-bar-chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Create chart
|
||||||
|
const chart = new ASCIIBarChart('chart', {
|
||||||
|
maxHeight: 20,
|
||||||
|
maxDataPoints: 30,
|
||||||
|
title: 'My Chart',
|
||||||
|
xAxisLabel: 'Time',
|
||||||
|
yAxisLabel: 'Value'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add data
|
||||||
|
chart.addValue(15);
|
||||||
|
chart.addValue(23);
|
||||||
|
chart.addValue(18);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Constructor
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
new ASCIIBarChart(containerId, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a new ASCII bar chart instance.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|------|----------|-------------|
|
||||||
|
| `containerId` | string | Yes | ID of the HTML element to render the chart in |
|
||||||
|
| `options` | object | No | Configuration options (see below) |
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `maxHeight` | number | `20` | Maximum height of the chart in rows |
|
||||||
|
| `maxDataPoints` | number | `30` | Maximum number of columns before old data scrolls off |
|
||||||
|
| `title` | string | `''` | Chart title (displayed centered at top) |
|
||||||
|
| `xAxisLabel` | string | `''` | X-axis label (displayed centered at bottom) |
|
||||||
|
| `yAxisLabel` | string | `''` | Y-axis label (displayed vertically on left side) |
|
||||||
|
| `autoFitWidth` | boolean | `true` | Automatically adjust font size to fit container width |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const chart = new ASCIIBarChart('my-chart', {
|
||||||
|
maxHeight: 25,
|
||||||
|
maxDataPoints: 50,
|
||||||
|
title: 'Server Response Times',
|
||||||
|
xAxisLabel: 'Request Number',
|
||||||
|
yAxisLabel: 'Milliseconds',
|
||||||
|
autoFitWidth: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
#### `addValue(value)`
|
||||||
|
|
||||||
|
Adds a new data point to the chart.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `value` (number): The numeric value to add to the chart
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
chart.addValue(42);
|
||||||
|
chart.addValue(38);
|
||||||
|
chart.addValue(45);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `clear()`
|
||||||
|
|
||||||
|
Clears all data from the chart and resets it to initial state.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
chart.clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features in Detail
|
||||||
|
|
||||||
|
### Auto-Scaling Y-Axis
|
||||||
|
|
||||||
|
The Y-axis automatically scales to show the full range of your data:
|
||||||
|
- Minimum value always displays as one 'X' at the bottom
|
||||||
|
- Maximum value uses the full chart height
|
||||||
|
- Y-axis labels show actual data values, not just row numbers
|
||||||
|
- Dynamically adjusts as new data arrives
|
||||||
|
|
||||||
|
### Scrolling Data Window
|
||||||
|
|
||||||
|
When the number of data points exceeds `maxDataPoints`:
|
||||||
|
- Old data automatically scrolls off the left side
|
||||||
|
- X-axis labels update to show actual data point numbers
|
||||||
|
- Maintains a sliding window of the most recent data
|
||||||
|
|
||||||
|
### Responsive Font Sizing
|
||||||
|
|
||||||
|
When `autoFitWidth` is enabled:
|
||||||
|
- Font size automatically adjusts to fill container width
|
||||||
|
- Responds to window resizing in real-time
|
||||||
|
- Maintains readability with min/max font size bounds (4px-20px)
|
||||||
|
- Uses ResizeObserver for efficient updates
|
||||||
|
|
||||||
|
### Vertical Y-Axis Label
|
||||||
|
|
||||||
|
The Y-axis label is rendered vertically, one character per row:
|
||||||
|
```
|
||||||
|
V
|
||||||
|
a
|
||||||
|
l
|
||||||
|
u
|
||||||
|
e
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Real-Time Data Streaming
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const chart = new ASCIIBarChart('chart', {
|
||||||
|
maxHeight: 20,
|
||||||
|
maxDataPoints: 60,
|
||||||
|
title: 'Live Metrics'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate real-time data
|
||||||
|
setInterval(() => {
|
||||||
|
const value = Math.random() * 100;
|
||||||
|
chart.addValue(value);
|
||||||
|
}, 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with Data Sources
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// WebSocket example
|
||||||
|
const ws = new WebSocket('ws://your-server.com/data');
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
chart.addValue(data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// API polling example
|
||||||
|
async function fetchAndUpdate() {
|
||||||
|
const response = await fetch('/api/metrics');
|
||||||
|
const data = await response.json();
|
||||||
|
chart.addValue(data.currentValue);
|
||||||
|
}
|
||||||
|
setInterval(fetchAndUpdate, 5000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Charts
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const cpuChart = new ASCIIBarChart('cpu-chart', {
|
||||||
|
title: 'CPU Usage %',
|
||||||
|
yAxisLabel: 'Percent'
|
||||||
|
});
|
||||||
|
|
||||||
|
const memChart = new ASCIIBarChart('mem-chart', {
|
||||||
|
title: 'Memory Usage MB',
|
||||||
|
yAxisLabel: 'Megabytes'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update both charts
|
||||||
|
cpuChart.addValue(45);
|
||||||
|
memChart.addValue(2048);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
The chart uses monospaced fonts and renders as plain text. Style the container element:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#chart-container {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background-color: #000;
|
||||||
|
color: #0f0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #0f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
- Modern browsers with ES6 support
|
||||||
|
- ResizeObserver API (for auto-fit width feature)
|
||||||
|
- All major browsers: Chrome, Firefox, Safari, Edge
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - feel free to use in your projects!
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See [`ascii-bar-chart.html`](ascii-bar-chart.html) for a complete working example with:
|
||||||
|
- Interactive controls (Start/Stop/Reset)
|
||||||
|
- Configurable settings (update interval, max columns, chart height)
|
||||||
|
- Test data generator with increasing trend
|
||||||
|
- Full styling and layout
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions welcome! This is a simple, focused library for ASCII-based charting.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- Initial release
|
||||||
|
- Dynamic vertical bar charts
|
||||||
|
- Auto-scaling Y-axis
|
||||||
|
- Scrolling data window
|
||||||
|
- Responsive font sizing
|
||||||
|
- Customizable labels and titles
|
||||||
273
ascii-bar-chart.html
Normal file
273
ascii-bar-chart.html
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ASCII Bar Chart</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #00ff00;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart-container {
|
||||||
|
background-color: #000;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #00ff00;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 1.0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #00ff00;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #00cc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart-info div {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings input {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #00ff00;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
|
padding: 3px 5px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>ASCII Vertical Bar Chart</h1>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<button onclick="dataGenerator.start()">Start</button>
|
||||||
|
<button onclick="dataGenerator.stop()">Stop</button>
|
||||||
|
<button onclick="dataGenerator.reset()">Reset</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings" style="margin-bottom: 15px; font-size: 12px;">
|
||||||
|
<label>
|
||||||
|
Update Interval (ms):
|
||||||
|
<input type="number" id="update-interval" value="1000" min="100" max="10000" step="100" style="width: 80px;">
|
||||||
|
</label>
|
||||||
|
<label style="margin-left: 15px;">
|
||||||
|
Max Columns:
|
||||||
|
<input type="number" id="max-columns" value="30" min="10" max="100" step="5" style="width: 60px;">
|
||||||
|
</label>
|
||||||
|
<label style="margin-left: 15px;">
|
||||||
|
Chart Height:
|
||||||
|
<input type="number" id="chart-height" value="20" min="10" max="50" step="5" style="width: 60px;">
|
||||||
|
</label>
|
||||||
|
<button onclick="applySettings()" style="margin-left: 15px; padding: 5px 15px;">Apply Settings</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="info">
|
||||||
|
<span>Data points: <span id="count">0</span></span> |
|
||||||
|
<span>Status: <span id="status">Stopped</span></span> |
|
||||||
|
<span>Next update in: <span id="countdown">--</span>s</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chart-container"></div>
|
||||||
|
|
||||||
|
<div id="chart-info">
|
||||||
|
<div><strong>Legend:</strong> Each X represents a count unit</div>
|
||||||
|
<div><strong>Values:</strong> <span id="values">--</span></div>
|
||||||
|
<div><strong>Max value:</strong> <span id="max-value">--</span>, <strong>Scale:</strong> <span id="scale">--</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Include the ASCII Bar Chart library -->
|
||||||
|
<script src="ascii-bar-chart.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// Initialize the chart
|
||||||
|
let chart = new ASCIIBarChart('chart-container', {
|
||||||
|
maxHeight: 20,
|
||||||
|
maxDataPoints: 30,
|
||||||
|
title: 'Real-Time Data Visualization',
|
||||||
|
xAxisLabel: 'Time (seconds)',
|
||||||
|
yAxisLabel: 'Count',
|
||||||
|
autoFitWidth: true // Automatically adjust font size to fit container width
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
chart.render();
|
||||||
|
chart.updateInfo();
|
||||||
|
|
||||||
|
// Test data generator (separate from chart API)
|
||||||
|
class DataGenerator {
|
||||||
|
constructor(updateInterval = 1000) {
|
||||||
|
this.baseValue = 10;
|
||||||
|
this.intervalId = null;
|
||||||
|
this.countdownId = null;
|
||||||
|
this.nextUpdateTime = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
this.updateInterval = updateInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpdateInterval(interval) {
|
||||||
|
this.updateInterval = interval;
|
||||||
|
// If running, restart with new interval
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.stop();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateValue() {
|
||||||
|
// Add some randomness around the base value (±3)
|
||||||
|
const variation = Math.floor(Math.random() * 7) - 3;
|
||||||
|
const value = this.baseValue + variation;
|
||||||
|
|
||||||
|
// Increase the base value slightly for next time (0.5 to 1.5 increase)
|
||||||
|
this.baseValue += 0.5 + Math.random();
|
||||||
|
|
||||||
|
return Math.max(1, Math.round(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountdown() {
|
||||||
|
if (!this.isRunning || !this.nextUpdateTime) {
|
||||||
|
document.getElementById('countdown').textContent = '--';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const remaining = Math.max(0, Math.ceil((this.nextUpdateTime - now) / 1000));
|
||||||
|
document.getElementById('countdown').textContent = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
document.getElementById('status').textContent = 'Running';
|
||||||
|
|
||||||
|
// Add first data point immediately
|
||||||
|
chart.addValue(this.generateValue());
|
||||||
|
|
||||||
|
// Set up interval for subsequent updates
|
||||||
|
this.nextUpdateTime = Date.now() + this.updateInterval;
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
chart.addValue(this.generateValue());
|
||||||
|
this.nextUpdateTime = Date.now() + this.updateInterval;
|
||||||
|
}, this.updateInterval);
|
||||||
|
|
||||||
|
// Update countdown every second
|
||||||
|
this.countdownId = setInterval(() => {
|
||||||
|
this.updateCountdown();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this.updateCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
document.getElementById('status').textContent = 'Stopped';
|
||||||
|
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.countdownId) {
|
||||||
|
clearInterval(this.countdownId);
|
||||||
|
this.countdownId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextUpdateTime = null;
|
||||||
|
this.updateCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.stop();
|
||||||
|
this.baseValue = 10;
|
||||||
|
chart.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create data generator for testing
|
||||||
|
let dataGenerator = new DataGenerator(1000);
|
||||||
|
|
||||||
|
// Function to apply settings
|
||||||
|
function applySettings() {
|
||||||
|
const wasRunning = dataGenerator.isRunning;
|
||||||
|
|
||||||
|
// Stop current generator
|
||||||
|
dataGenerator.stop();
|
||||||
|
|
||||||
|
// Get new settings
|
||||||
|
const updateInterval = parseInt(document.getElementById('update-interval').value);
|
||||||
|
const maxColumns = parseInt(document.getElementById('max-columns').value);
|
||||||
|
const chartHeight = parseInt(document.getElementById('chart-height').value);
|
||||||
|
|
||||||
|
// Recreate chart with new settings
|
||||||
|
chart = new ASCIIBarChart('chart-container', {
|
||||||
|
maxHeight: chartHeight,
|
||||||
|
maxDataPoints: maxColumns,
|
||||||
|
title: 'Real-Time Data Visualization',
|
||||||
|
xAxisLabel: 'Time (seconds)',
|
||||||
|
yAxisLabel: 'Count',
|
||||||
|
autoFitWidth: true
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.render();
|
||||||
|
chart.updateInfo();
|
||||||
|
|
||||||
|
// Recreate data generator with new interval
|
||||||
|
dataGenerator = new DataGenerator(updateInterval);
|
||||||
|
|
||||||
|
// Restart if it was running
|
||||||
|
if (wasRunning) {
|
||||||
|
dataGenerator.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
221
ascii-bar-chart.js
Normal file
221
ascii-bar-chart.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* ASCIIBarChart - A dynamic ASCII-based vertical bar chart renderer
|
||||||
|
*
|
||||||
|
* Creates real-time animated bar charts using monospaced characters (X)
|
||||||
|
* with automatic scaling, labels, and responsive font sizing.
|
||||||
|
*/
|
||||||
|
class ASCIIBarChart {
|
||||||
|
/**
|
||||||
|
* Create a new ASCII bar chart
|
||||||
|
* @param {string} containerId - The ID of the HTML element to render the chart in
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {number} [options.maxHeight=20] - Maximum height of the chart in rows
|
||||||
|
* @param {number} [options.maxDataPoints=30] - Maximum number of data columns before scrolling
|
||||||
|
* @param {string} [options.title=''] - Chart title (displayed centered at top)
|
||||||
|
* @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
|
||||||
|
* @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
|
||||||
|
* @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
|
||||||
|
*/
|
||||||
|
constructor(containerId, options = {}) {
|
||||||
|
this.container = document.getElementById(containerId);
|
||||||
|
this.data = [];
|
||||||
|
this.maxHeight = options.maxHeight || 20;
|
||||||
|
this.maxDataPoints = options.maxDataPoints || 30;
|
||||||
|
this.totalDataPoints = 0; // Track total number of data points added
|
||||||
|
this.title = options.title || '';
|
||||||
|
this.xAxisLabel = options.xAxisLabel || '';
|
||||||
|
this.yAxisLabel = options.yAxisLabel || '';
|
||||||
|
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
||||||
|
|
||||||
|
// Set up resize observer if auto-fit is enabled
|
||||||
|
if (this.autoFitWidth) {
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.adjustFontSize();
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(this.container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new data point to the chart
|
||||||
|
* @param {number} value - The numeric value to add
|
||||||
|
*/
|
||||||
|
addValue(value) {
|
||||||
|
this.data.push(value);
|
||||||
|
this.totalDataPoints++;
|
||||||
|
|
||||||
|
// Keep only the most recent data points
|
||||||
|
if (this.data.length > this.maxDataPoints) {
|
||||||
|
this.data.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.updateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all data from the chart
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.data = [];
|
||||||
|
this.totalDataPoints = 0;
|
||||||
|
this.render();
|
||||||
|
this.updateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the width of the chart in characters
|
||||||
|
* @returns {number} The chart width in characters
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getChartWidth() {
|
||||||
|
if (this.data.length === 0) return 50; // Default width for empty chart
|
||||||
|
|
||||||
|
const yAxisPadding = this.yAxisLabel ? 2 : 0;
|
||||||
|
const yAxisNumbers = 3; // Width of Y-axis numbers
|
||||||
|
const separator = 1; // The '|' character
|
||||||
|
const dataWidth = this.data.length * 2; // Each column is 2 characters wide
|
||||||
|
const padding = 1; // Extra padding
|
||||||
|
|
||||||
|
return yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust font size to fit container width
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
adjustFontSize() {
|
||||||
|
if (!this.autoFitWidth) return;
|
||||||
|
|
||||||
|
const containerWidth = this.container.clientWidth;
|
||||||
|
const chartWidth = this.getChartWidth();
|
||||||
|
|
||||||
|
if (chartWidth === 0) return;
|
||||||
|
|
||||||
|
// Calculate optimal font size
|
||||||
|
// For monospace fonts, character width is approximately 0.6 * font size
|
||||||
|
const charWidthRatio = 0.6;
|
||||||
|
const padding = 40; // Account for container padding
|
||||||
|
const availableWidth = containerWidth - padding;
|
||||||
|
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
||||||
|
|
||||||
|
// Set reasonable bounds (min 4px, max 20px)
|
||||||
|
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
||||||
|
|
||||||
|
this.container.style.fontSize = fontSize + 'px';
|
||||||
|
this.container.style.lineHeight = '1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the chart to the container
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (this.data.length === 0) {
|
||||||
|
this.container.textContent = 'No data yet. Click Start to begin.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
const maxValue = Math.max(...this.data);
|
||||||
|
const minValue = Math.min(...this.data);
|
||||||
|
const valueRange = maxValue - minValue;
|
||||||
|
const scale = this.maxHeight;
|
||||||
|
|
||||||
|
// Calculate Y-axis label width (for vertical text)
|
||||||
|
const yLabelWidth = this.yAxisLabel ? 2 : 0;
|
||||||
|
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
||||||
|
|
||||||
|
// Add title if provided (centered)
|
||||||
|
if (this.title) {
|
||||||
|
const chartWidth = 4 + this.data.length * 2; // Y-axis numbers + data columns
|
||||||
|
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
||||||
|
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw from top to bottom
|
||||||
|
for (let row = scale; row > 0; row--) {
|
||||||
|
let line = '';
|
||||||
|
|
||||||
|
// Add vertical Y-axis label character
|
||||||
|
if (this.yAxisLabel) {
|
||||||
|
const labelIndex = Math.floor((scale - row) / scale * this.yAxisLabel.length);
|
||||||
|
if (labelIndex < this.yAxisLabel.length) {
|
||||||
|
line += this.yAxisLabel[labelIndex] + ' ';
|
||||||
|
} else {
|
||||||
|
line += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the actual value this row represents
|
||||||
|
const rowValue = minValue + (valueRange * (row - 1) / (scale - 1));
|
||||||
|
|
||||||
|
// Add Y-axis label (show actual values)
|
||||||
|
line += String(Math.round(rowValue)).padStart(3, ' ') + ' |';
|
||||||
|
|
||||||
|
// Draw each column
|
||||||
|
for (let i = 0; i < this.data.length; i++) {
|
||||||
|
const value = this.data[i];
|
||||||
|
|
||||||
|
// Scale the value to fit between 1 and scale
|
||||||
|
let scaledValue;
|
||||||
|
if (valueRange === 0) {
|
||||||
|
// All values are the same
|
||||||
|
scaledValue = 1;
|
||||||
|
} else {
|
||||||
|
scaledValue = 1 + Math.round(((value - minValue) / valueRange) * (scale - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scaledValue >= row) {
|
||||||
|
line += ' X';
|
||||||
|
} else {
|
||||||
|
line += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output += line + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw X-axis
|
||||||
|
output += yAxisPadding + ' +' + '-'.repeat(this.data.length * 2) + '\n';
|
||||||
|
|
||||||
|
// Draw X-axis labels (column numbers that move with the data)
|
||||||
|
let xAxisLabels = yAxisPadding + ' ';
|
||||||
|
const startIndex = this.totalDataPoints - this.data.length + 1;
|
||||||
|
for (let i = 0; i < this.data.length; i++) {
|
||||||
|
const dataPointNumber = startIndex + i;
|
||||||
|
if (i % 5 === 0) {
|
||||||
|
xAxisLabels += String(dataPointNumber).padStart(2, ' ');
|
||||||
|
} else {
|
||||||
|
xAxisLabels += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += xAxisLabels + '\n';
|
||||||
|
|
||||||
|
// Add X-axis label if provided
|
||||||
|
if (this.xAxisLabel) {
|
||||||
|
const labelPadding = Math.floor((this.data.length * 2 - this.xAxisLabel.length) / 2);
|
||||||
|
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.textContent = output;
|
||||||
|
|
||||||
|
// Adjust font size to fit width
|
||||||
|
if (this.autoFitWidth) {
|
||||||
|
this.adjustFontSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the external info display
|
||||||
|
document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
|
||||||
|
document.getElementById('max-value').textContent = maxValue;
|
||||||
|
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the info display
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
updateInfo() {
|
||||||
|
document.getElementById('count').textContent = this.data.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user