.
This commit is contained in:
47
README.md
47
README.md
@@ -11,6 +11,9 @@ A dynamic, real-time ASCII-based vertical bar chart library that renders beautif
|
|||||||
- 🏷️ **Customizable Labels**: Add title, X-axis, and vertical Y-axis labels
|
- 🏷️ **Customizable Labels**: Add title, X-axis, and vertical Y-axis labels
|
||||||
- 🎯 **Scrolling Data**: Old data automatically scrolls off as new data arrives
|
- 🎯 **Scrolling Data**: Old data automatically scrolls off as new data arrives
|
||||||
- 🎨 **Terminal Theme**: Retro green-on-black aesthetic
|
- 🎨 **Terminal Theme**: Retro green-on-black aesthetic
|
||||||
|
- ⏱️ **Time Bin Mode**: Aggregate data into configurable time bins (e.g., count events per 10-second window)
|
||||||
|
- 📈 **Multiple X-Axis Formats**: Display elapsed time, bin numbers, timestamps, or time ranges
|
||||||
|
- 👁️ **Active Bin Indicator**: Visual marker shows which bin is currently accumulating data
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
@@ -117,6 +120,9 @@ Creates a new ASCII bar chart instance.
|
|||||||
| `xAxisLabel` | string | `''` | X-axis label (displayed centered at bottom) |
|
| `xAxisLabel` | string | `''` | X-axis label (displayed centered at bottom) |
|
||||||
| `yAxisLabel` | string | `''` | Y-axis label (displayed vertically on left side) |
|
| `yAxisLabel` | string | `''` | Y-axis label (displayed vertically on left side) |
|
||||||
| `autoFitWidth` | boolean | `true` | Automatically adjust font size to fit container width |
|
| `autoFitWidth` | boolean | `true` | Automatically adjust font size to fit container width |
|
||||||
|
| `useBinMode` | boolean | `false` | Enable time bin mode for data aggregation |
|
||||||
|
| `binDuration` | number | `4000` | Duration of each time bin in milliseconds (4 seconds default) |
|
||||||
|
| `xAxisLabelFormat` | string | `'elapsed'` | X-axis label format: `'elapsed'`, `'bins'`, `'timestamps'`, `'ranges'` |
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -249,6 +255,47 @@ cpuChart.addValue(45);
|
|||||||
memChart.addValue(2048);
|
memChart.addValue(2048);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Time Bin Mode
|
||||||
|
|
||||||
|
Time bin mode aggregates data points into time-based bins, showing the count of events within each time window. This is useful for visualizing event frequency over time.
|
||||||
|
|
||||||
|
### Basic Time Bin Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const chart = new ASCIIBarChart('chart-container', {
|
||||||
|
useBinMode: true,
|
||||||
|
binDuration: 10000, // 10-second bins
|
||||||
|
xAxisLabelFormat: 'elapsed', // Show elapsed time
|
||||||
|
title: 'Event Counter',
|
||||||
|
yAxisLabel: 'Count'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Each addValue() increments the count in the current active bin
|
||||||
|
chart.addValue(1); // Bin 1: count = 1
|
||||||
|
chart.addValue(1); // Bin 1: count = 2
|
||||||
|
// ... after 10 seconds, automatically creates Bin 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### X-Axis Label Formats
|
||||||
|
|
||||||
|
- **`'elapsed'`** (default): Shows elapsed seconds since chart start ("0s", "10s", "20s")
|
||||||
|
- **`'bins'`**: Shows bin numbers ("Bin 1", "Bin 2", "Bin 3")
|
||||||
|
- **`'timestamps'`**: Shows actual timestamps ("103000", "103010", "103020")
|
||||||
|
- **`'ranges'`**: Shows time ranges ("0-10s", "10-20s", "20-30s")
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
- **X**: Regular bin data
|
||||||
|
- **O**: Active bin currently accumulating data
|
||||||
|
- Chart automatically scales when bin counts exceed chart height
|
||||||
|
|
||||||
|
### Manual Bin Rotation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Force rotation to new bin (useful for testing)
|
||||||
|
chart.rotateBin();
|
||||||
|
```
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
The chart uses monospaced fonts and renders as plain text. Style the container element:
|
The chart uses monospaced fonts and renders as plain text. Style the container element:
|
||||||
|
|||||||
156
text_graph.html
156
text_graph.html
@@ -18,7 +18,7 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 2px solid #00ff00;
|
border: 2px solid #00ff00;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
line-height: 1.0;
|
line-height: 1.0;
|
||||||
@@ -103,7 +103,25 @@
|
|||||||
Chart Height:
|
Chart Height:
|
||||||
<input type="number" id="chart-height" value="20" min="10" max="50" step="5" style="width: 60px;">
|
<input type="number" id="chart-height" value="20" min="10" max="50" step="5" style="width: 60px;">
|
||||||
</label>
|
</label>
|
||||||
|
<label style="margin-left: 15px;">
|
||||||
|
<input type="checkbox" id="use-bin-mode" checked onchange="toggleBinMode()">
|
||||||
|
Time Bin Mode
|
||||||
|
</label>
|
||||||
|
<label style="margin-left: 15px;">
|
||||||
|
Bin Duration (s):
|
||||||
|
<input type="number" id="bin-duration" value="4" min="0.1" max="300" step="0.1" style="width: 60px;">
|
||||||
|
</label>
|
||||||
|
<label style="margin-left: 15px;">
|
||||||
|
X-Axis Format:
|
||||||
|
<select id="x-axis-format" style="width: 100px;">
|
||||||
|
<option value="elapsed">Elapsed</option>
|
||||||
|
<option value="bins">Bin Numbers</option>
|
||||||
|
<option value="timestamps">Timestamps</option>
|
||||||
|
<option value="ranges">Time Ranges</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<button onclick="applySettings()" style="margin-left: 15px; padding: 5px 15px;">Apply Settings</button>
|
<button onclick="applySettings()" style="margin-left: 15px; padding: 5px 15px;">Apply Settings</button>
|
||||||
|
<button onclick="rotateBin()" id="rotate-bin-btn" style="margin-left: 10px; padding: 5px 15px; display: none;">Rotate Bin</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="info">
|
<div id="info">
|
||||||
@@ -115,9 +133,10 @@
|
|||||||
<div id="chart-container"></div>
|
<div id="chart-container"></div>
|
||||||
|
|
||||||
<div id="chart-info">
|
<div id="chart-info">
|
||||||
<div><strong>Legend:</strong> Each X represents a count unit</div>
|
<div><strong>Legend:</strong> <span id="legend-text">Each X represents a count of events</span></div>
|
||||||
<div><strong>Values:</strong> <span id="values">--</span></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><strong>Max value:</strong> <span id="max-value">--</span>, <strong>Scale:</strong> <span id="scale">--</span></div>
|
||||||
|
<div id="bin-info" style="display: none;"><strong>Current Bin:</strong> <span id="current-bin">--</span>, <strong>Time Remaining:</strong> <span id="time-remaining">--</span>s</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Include the ASCII Bar Chart library -->
|
<!-- Include the ASCII Bar Chart library -->
|
||||||
@@ -132,7 +151,10 @@
|
|||||||
title: 'Real-Time Data Visualization',
|
title: 'Real-Time Data Visualization',
|
||||||
xAxisLabel: 'Time (seconds)',
|
xAxisLabel: 'Time (seconds)',
|
||||||
yAxisLabel: 'Count',
|
yAxisLabel: 'Count',
|
||||||
autoFitWidth: true // Automatically adjust font size to fit container width
|
autoFitWidth: true, // Automatically adjust font size to fit container width
|
||||||
|
useBinMode: true, // Start in time bin mode
|
||||||
|
binDuration: 4000, // 4 seconds
|
||||||
|
xAxisLabelFormat: 'elapsed'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
@@ -163,12 +185,21 @@
|
|||||||
// Add some randomness around the base value (±3)
|
// Add some randomness around the base value (±3)
|
||||||
const variation = Math.floor(Math.random() * 7) - 3;
|
const variation = Math.floor(Math.random() * 7) - 3;
|
||||||
const value = this.baseValue + variation;
|
const value = this.baseValue + variation;
|
||||||
|
|
||||||
// Increase the base value slightly for next time (0.5 to 1.5 increase)
|
// Increase the base value slightly for next time (0.5 to 1.5 increase)
|
||||||
this.baseValue += 0.5 + Math.random();
|
this.baseValue += 0.5 + Math.random();
|
||||||
|
|
||||||
return Math.max(1, Math.round(value));
|
return Math.max(1, Math.round(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateRandomInterval() {
|
||||||
|
// Generate random interval based on update interval setting
|
||||||
|
// Random between 10% and 200% of the base update interval
|
||||||
|
const baseInterval = parseInt(document.getElementById('update-interval').value);
|
||||||
|
const minInterval = Math.max(50, baseInterval * 0.1); // Minimum 50ms
|
||||||
|
const maxInterval = baseInterval * 2; // Maximum 2x the base interval
|
||||||
|
return Math.floor(Math.random() * (maxInterval - minInterval)) + minInterval;
|
||||||
|
}
|
||||||
|
|
||||||
updateCountdown() {
|
updateCountdown() {
|
||||||
if (!this.isRunning || !this.nextUpdateTime) {
|
if (!this.isRunning || !this.nextUpdateTime) {
|
||||||
@@ -183,46 +214,60 @@
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.isRunning) return;
|
if (this.isRunning) return;
|
||||||
|
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
document.getElementById('status').textContent = 'Running';
|
document.getElementById('status').textContent = 'Running';
|
||||||
|
|
||||||
// Add first data point immediately
|
// Add first data point immediately
|
||||||
chart.addValue(this.generateValue());
|
chart.addValue(this.generateValue());
|
||||||
|
|
||||||
// Set up interval for subsequent updates
|
// Set up interval for subsequent updates with random timing
|
||||||
this.nextUpdateTime = Date.now() + this.updateInterval;
|
this.scheduleNextUpdate();
|
||||||
this.intervalId = setInterval(() => {
|
|
||||||
chart.addValue(this.generateValue());
|
|
||||||
this.nextUpdateTime = Date.now() + this.updateInterval;
|
|
||||||
}, this.updateInterval);
|
|
||||||
|
|
||||||
// Update countdown every second
|
|
||||||
this.countdownId = setInterval(() => {
|
this.countdownId = setInterval(() => {
|
||||||
this.updateCountdown();
|
this.updateCountdown();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
this.updateCountdown();
|
this.updateCountdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleNextUpdate() {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
const randomInterval = this.generateRandomInterval();
|
||||||
|
this.nextUpdateTime = Date.now() + randomInterval;
|
||||||
|
|
||||||
|
this.intervalId = setTimeout(() => {
|
||||||
|
if (this.isRunning) {
|
||||||
|
chart.addValue(this.generateValue());
|
||||||
|
this.scheduleNextUpdate();
|
||||||
|
}
|
||||||
|
}, randomInterval);
|
||||||
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (!this.isRunning) return;
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
document.getElementById('status').textContent = 'Stopped';
|
document.getElementById('status').textContent = 'Stopped';
|
||||||
|
|
||||||
if (this.intervalId) {
|
if (this.intervalId) {
|
||||||
clearInterval(this.intervalId);
|
clearTimeout(this.intervalId);
|
||||||
this.intervalId = null;
|
this.intervalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.countdownId) {
|
if (this.countdownId) {
|
||||||
clearInterval(this.countdownId);
|
clearInterval(this.countdownId);
|
||||||
this.countdownId = null;
|
this.countdownId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nextUpdateTime = null;
|
this.nextUpdateTime = null;
|
||||||
this.updateCountdown();
|
this.updateCountdown();
|
||||||
|
|
||||||
|
// Stop bin rotation timer when stopping data generation
|
||||||
|
if (chart.binCheckInterval) {
|
||||||
|
clearInterval(chart.binCheckInterval);
|
||||||
|
chart.binCheckInterval = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@@ -235,18 +280,48 @@
|
|||||||
// Create data generator for testing
|
// Create data generator for testing
|
||||||
let dataGenerator = new DataGenerator(1000);
|
let dataGenerator = new DataGenerator(1000);
|
||||||
|
|
||||||
|
// Function to toggle bin mode
|
||||||
|
function toggleBinMode() {
|
||||||
|
const useBinMode = document.getElementById('use-bin-mode').checked;
|
||||||
|
const binDurationInput = document.getElementById('bin-duration');
|
||||||
|
const xAxisFormatSelect = document.getElementById('x-axis-format');
|
||||||
|
const rotateBinBtn = document.getElementById('rotate-bin-btn');
|
||||||
|
const binInfo = document.getElementById('bin-info');
|
||||||
|
const legendText = document.getElementById('legend-text');
|
||||||
|
|
||||||
|
if (useBinMode) {
|
||||||
|
rotateBinBtn.style.display = 'inline-block';
|
||||||
|
binInfo.style.display = 'block';
|
||||||
|
legendText.textContent = 'Each X represents a count of events. O marks the active bin.';
|
||||||
|
} else {
|
||||||
|
rotateBinBtn.style.display = 'none';
|
||||||
|
binInfo.style.display = 'none';
|
||||||
|
legendText.textContent = 'Each X represents a count of events';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to manually rotate bin
|
||||||
|
function rotateBin() {
|
||||||
|
if (chart && chart.useBinMode) {
|
||||||
|
chart.rotateBin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to apply settings
|
// Function to apply settings
|
||||||
function applySettings() {
|
function applySettings() {
|
||||||
const wasRunning = dataGenerator.isRunning;
|
const wasRunning = dataGenerator.isRunning;
|
||||||
|
|
||||||
// Stop current generator
|
// Stop current generator
|
||||||
dataGenerator.stop();
|
dataGenerator.stop();
|
||||||
|
|
||||||
// Get new settings
|
// Get new settings
|
||||||
const updateInterval = parseInt(document.getElementById('update-interval').value);
|
const updateInterval = parseInt(document.getElementById('update-interval').value);
|
||||||
const maxColumns = parseInt(document.getElementById('max-columns').value);
|
const maxColumns = parseInt(document.getElementById('max-columns').value);
|
||||||
const chartHeight = parseInt(document.getElementById('chart-height').value);
|
const chartHeight = parseInt(document.getElementById('chart-height').value);
|
||||||
|
const useBinMode = document.getElementById('use-bin-mode').checked;
|
||||||
|
const binDuration = parseFloat(document.getElementById('bin-duration').value) * 1000; // Convert to milliseconds
|
||||||
|
const xAxisLabelFormat = document.getElementById('x-axis-format').value;
|
||||||
|
|
||||||
// Recreate chart with new settings
|
// Recreate chart with new settings
|
||||||
chart = new ASCIIBarChart('chart-container', {
|
chart = new ASCIIBarChart('chart-container', {
|
||||||
maxHeight: chartHeight,
|
maxHeight: chartHeight,
|
||||||
@@ -254,20 +329,43 @@
|
|||||||
title: 'Real-Time Data Visualization',
|
title: 'Real-Time Data Visualization',
|
||||||
xAxisLabel: 'Time (seconds)',
|
xAxisLabel: 'Time (seconds)',
|
||||||
yAxisLabel: 'Count',
|
yAxisLabel: 'Count',
|
||||||
autoFitWidth: true
|
autoFitWidth: true,
|
||||||
|
useBinMode: useBinMode,
|
||||||
|
binDuration: binDuration,
|
||||||
|
xAxisLabelFormat: xAxisLabelFormat
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Force font size adjustment for new settings
|
||||||
|
chart.fontSizeAdjusted = false;
|
||||||
chart.render();
|
chart.render();
|
||||||
chart.updateInfo();
|
chart.updateInfo();
|
||||||
|
|
||||||
// Recreate data generator with new interval
|
// Recreate data generator with new interval
|
||||||
dataGenerator = new DataGenerator(updateInterval);
|
dataGenerator = new DataGenerator(updateInterval);
|
||||||
|
|
||||||
// Restart if it was running
|
// Restart if it was running
|
||||||
if (wasRunning) {
|
if (wasRunning) {
|
||||||
dataGenerator.start();
|
dataGenerator.start();
|
||||||
|
} else {
|
||||||
|
// If not restarting, make sure bin timer is also stopped
|
||||||
|
if (chart.binCheckInterval) {
|
||||||
|
clearInterval(chart.binCheckInterval);
|
||||||
|
chart.binCheckInterval = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update bin info display
|
||||||
|
setInterval(() => {
|
||||||
|
if (chart && chart.useBinMode && chart.bins.length > 0) {
|
||||||
|
const currentBin = chart.currentBinIndex + 1;
|
||||||
|
const timeRemaining = chart.binStartTime ?
|
||||||
|
Math.max(0, Math.ceil((chart.binStartTime + chart.binDuration - Date.now()) / 1000)) : 0;
|
||||||
|
|
||||||
|
document.getElementById('current-bin').textContent = currentBin;
|
||||||
|
document.getElementById('time-remaining').textContent = timeRemaining;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
368
text_graph.js
368
text_graph.js
@@ -15,6 +15,9 @@ class ASCIIBarChart {
|
|||||||
* @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
|
* @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
|
||||||
* @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
|
* @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
|
||||||
* @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
|
* @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
|
||||||
|
* @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
|
||||||
|
* @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
|
||||||
|
* @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
|
||||||
*/
|
*/
|
||||||
constructor(containerId, options = {}) {
|
constructor(containerId, options = {}) {
|
||||||
this.container = document.getElementById(containerId);
|
this.container = document.getElementById(containerId);
|
||||||
@@ -26,7 +29,19 @@ class ASCIIBarChart {
|
|||||||
this.xAxisLabel = options.xAxisLabel || '';
|
this.xAxisLabel = options.xAxisLabel || '';
|
||||||
this.yAxisLabel = options.yAxisLabel || '';
|
this.yAxisLabel = options.yAxisLabel || '';
|
||||||
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
||||||
|
|
||||||
|
// Time bin configuration
|
||||||
|
this.useBinMode = options.useBinMode !== false; // Default to true
|
||||||
|
this.binDuration = options.binDuration || 4000; // 4 seconds default
|
||||||
|
this.xAxisLabelFormat = options.xAxisLabelFormat || 'elapsed';
|
||||||
|
|
||||||
|
// Time bin data structures
|
||||||
|
this.bins = [];
|
||||||
|
this.currentBinIndex = -1;
|
||||||
|
this.binStartTime = null;
|
||||||
|
this.binCheckInterval = null;
|
||||||
|
this.chartStartTime = Date.now();
|
||||||
|
|
||||||
// Set up resize observer if auto-fit is enabled
|
// Set up resize observer if auto-fit is enabled
|
||||||
if (this.autoFitWidth) {
|
if (this.autoFitWidth) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
@@ -34,6 +49,11 @@ class ASCIIBarChart {
|
|||||||
});
|
});
|
||||||
this.resizeObserver.observe(this.container);
|
this.resizeObserver.observe(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize first bin if bin mode is enabled
|
||||||
|
if (this.useBinMode) {
|
||||||
|
this.initializeBins();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,14 +61,22 @@ class ASCIIBarChart {
|
|||||||
* @param {number} value - The numeric value to add
|
* @param {number} value - The numeric value to add
|
||||||
*/
|
*/
|
||||||
addValue(value) {
|
addValue(value) {
|
||||||
this.data.push(value);
|
if (this.useBinMode) {
|
||||||
this.totalDataPoints++;
|
// Time bin mode: increment count in current active bin
|
||||||
|
this.checkBinRotation(); // Ensure we have an active bin
|
||||||
// Keep only the most recent data points
|
this.bins[this.currentBinIndex].count++;
|
||||||
if (this.data.length > this.maxDataPoints) {
|
this.totalDataPoints++;
|
||||||
this.data.shift();
|
} else {
|
||||||
|
// Legacy mode: add individual values
|
||||||
|
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.render();
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
}
|
}
|
||||||
@@ -59,6 +87,14 @@ class ASCIIBarChart {
|
|||||||
clear() {
|
clear() {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
this.totalDataPoints = 0;
|
this.totalDataPoints = 0;
|
||||||
|
|
||||||
|
if (this.useBinMode) {
|
||||||
|
this.bins = [];
|
||||||
|
this.currentBinIndex = -1;
|
||||||
|
this.binStartTime = null;
|
||||||
|
this.initializeBins();
|
||||||
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
}
|
}
|
||||||
@@ -69,15 +105,25 @@ class ASCIIBarChart {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getChartWidth() {
|
getChartWidth() {
|
||||||
if (this.data.length === 0) return 50; // Default width for empty chart
|
let dataLength = this.maxDataPoints; // Always use maxDataPoints for consistent width
|
||||||
|
|
||||||
|
if (dataLength === 0) return 50; // Default width for empty chart
|
||||||
|
|
||||||
const yAxisPadding = this.yAxisLabel ? 2 : 0;
|
const yAxisPadding = this.yAxisLabel ? 2 : 0;
|
||||||
const yAxisNumbers = 3; // Width of Y-axis numbers
|
const yAxisNumbers = 3; // Width of Y-axis numbers
|
||||||
const separator = 1; // The '|' character
|
const separator = 1; // The '|' character
|
||||||
const dataWidth = this.data.length * 2; // Each column is 2 characters wide
|
const dataWidth = dataLength * 2; // Each column is 2 characters wide
|
||||||
const padding = 1; // Extra padding
|
const padding = 1; // Extra padding
|
||||||
|
|
||||||
return yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
||||||
|
|
||||||
|
// Only log when width changes
|
||||||
|
if (this.lastChartWidth !== totalWidth) {
|
||||||
|
console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
|
||||||
|
this.lastChartWidth = totalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,22 +132,28 @@ class ASCIIBarChart {
|
|||||||
*/
|
*/
|
||||||
adjustFontSize() {
|
adjustFontSize() {
|
||||||
if (!this.autoFitWidth) return;
|
if (!this.autoFitWidth) return;
|
||||||
|
|
||||||
const containerWidth = this.container.clientWidth;
|
const containerWidth = this.container.clientWidth;
|
||||||
const chartWidth = this.getChartWidth();
|
const chartWidth = this.getChartWidth();
|
||||||
|
|
||||||
if (chartWidth === 0) return;
|
if (chartWidth === 0) return;
|
||||||
|
|
||||||
// Calculate optimal font size
|
// Calculate optimal font size
|
||||||
// For monospace fonts, character width is approximately 0.6 * font size
|
// For monospace fonts, character width is approximately 0.6 * font size
|
||||||
const charWidthRatio = 0.6;
|
const charWidthRatio = 0.6;
|
||||||
const padding = 40; // Account for container padding
|
const padding = 40; // Account for container padding
|
||||||
const availableWidth = containerWidth - padding;
|
const availableWidth = containerWidth - padding;
|
||||||
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
||||||
|
|
||||||
// Set reasonable bounds (min 4px, max 20px)
|
// Set reasonable bounds (min 4px, max 20px)
|
||||||
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
||||||
|
|
||||||
|
// Only log when font size changes
|
||||||
|
if (this.lastFontSize !== fontSize) {
|
||||||
|
console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
|
||||||
|
this.lastFontSize = fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
this.container.style.fontSize = fontSize + 'px';
|
this.container.style.fontSize = fontSize + 'px';
|
||||||
this.container.style.lineHeight = '1.0';
|
this.container.style.lineHeight = '1.0';
|
||||||
}
|
}
|
||||||
@@ -111,32 +163,60 @@ class ASCIIBarChart {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
if (this.data.length === 0) {
|
let dataToRender = [];
|
||||||
this.container.textContent = 'No data yet. Click Start to begin.';
|
let maxValue = 0;
|
||||||
return;
|
let minValue = 0;
|
||||||
|
let valueRange = 0;
|
||||||
|
|
||||||
|
if (this.useBinMode) {
|
||||||
|
// Bin mode: render bin counts
|
||||||
|
if (this.bins.length === 0) {
|
||||||
|
this.container.textContent = 'No data yet. Click Start to begin.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only render the most recent bins up to maxDataPoints
|
||||||
|
// Reverse the order so the most recent (active) bin is on the left
|
||||||
|
const startIndex = Math.max(0, this.bins.length - this.maxDataPoints);
|
||||||
|
const recentBins = this.bins.slice(startIndex);
|
||||||
|
dataToRender = recentBins.reverse().map(bin => bin.count); // Reverse so active bin is first (leftmost)
|
||||||
|
maxValue = Math.max(...dataToRender);
|
||||||
|
minValue = Math.min(...dataToRender);
|
||||||
|
valueRange = maxValue - minValue;
|
||||||
|
} else {
|
||||||
|
// Legacy mode: render individual values
|
||||||
|
if (this.data.length === 0) {
|
||||||
|
this.container.textContent = 'No data yet. Click Start to begin.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataToRender = this.data;
|
||||||
|
maxValue = Math.max(...this.data);
|
||||||
|
minValue = Math.min(...this.data);
|
||||||
|
valueRange = maxValue - minValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
const maxValue = Math.max(...this.data);
|
|
||||||
const minValue = Math.min(...this.data);
|
|
||||||
const valueRange = maxValue - minValue;
|
|
||||||
const scale = this.maxHeight;
|
const scale = this.maxHeight;
|
||||||
|
|
||||||
|
// Calculate scaling factor: each X represents at least 1 count
|
||||||
|
const maxCount = Math.max(...dataToRender);
|
||||||
|
const scaleFactor = Math.max(1, Math.ceil(maxCount / scale)); // 1 X = scaleFactor counts
|
||||||
|
const scaledMax = Math.ceil(maxCount / scaleFactor) * scaleFactor;
|
||||||
|
|
||||||
// Calculate Y-axis label width (for vertical text)
|
// Calculate Y-axis label width (for vertical text)
|
||||||
const yLabelWidth = this.yAxisLabel ? 2 : 0;
|
const yLabelWidth = this.yAxisLabel ? 2 : 0;
|
||||||
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
||||||
|
|
||||||
// Add title if provided (centered)
|
// Add title if provided (centered)
|
||||||
if (this.title) {
|
if (this.title) {
|
||||||
const chartWidth = 4 + this.data.length * 2; // Y-axis numbers + data columns
|
const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns
|
||||||
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
||||||
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw from top to bottom
|
// Draw from top to bottom
|
||||||
for (let row = scale; row > 0; row--) {
|
for (let row = scale; row > 0; row--) {
|
||||||
let line = '';
|
let line = '';
|
||||||
|
|
||||||
// Add vertical Y-axis label character
|
// Add vertical Y-axis label character
|
||||||
if (this.yAxisLabel) {
|
if (this.yAxisLabel) {
|
||||||
const L = this.yAxisLabel.length;
|
const L = this.yAxisLabel.length;
|
||||||
@@ -149,69 +229,78 @@ class ASCIIBarChart {
|
|||||||
line += ' ';
|
line += ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the actual value this row represents
|
// Calculate the actual count value this row represents (0 at bottom, increasing upward)
|
||||||
const rowValue = minValue + (valueRange * (row - 1) / (scale - 1));
|
const rowCount = (row - 1) * scaleFactor;
|
||||||
|
|
||||||
// Add Y-axis label (show actual values)
|
// Add Y-axis label (show actual count values)
|
||||||
line += String(Math.round(rowValue)).padStart(3, ' ') + ' |';
|
line += String(rowCount).padStart(3, ' ') + ' |';
|
||||||
|
|
||||||
// Draw each column
|
// Draw each column
|
||||||
for (let i = 0; i < this.data.length; i++) {
|
for (let i = 0; i < dataToRender.length; i++) {
|
||||||
const value = this.data[i];
|
const count = dataToRender[i];
|
||||||
|
const scaledHeight = Math.ceil(count / scaleFactor);
|
||||||
// Scale the value to fit between 1 and scale
|
|
||||||
let scaledValue;
|
if (scaledHeight >= row) {
|
||||||
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';
|
line += ' X';
|
||||||
} else {
|
} else {
|
||||||
line += ' ';
|
line += ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output += line + '\n';
|
output += line + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw X-axis
|
// Draw X-axis
|
||||||
output += yAxisPadding + ' +' + '-'.repeat(this.data.length * 2) + '\n';
|
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n';
|
||||||
|
|
||||||
// Draw X-axis labels (column numbers that move with the data)
|
// Draw X-axis labels based on mode and format
|
||||||
let xAxisLabels = yAxisPadding + ' ';
|
let xAxisLabels = yAxisPadding + ' ';
|
||||||
const startIndex = this.totalDataPoints - this.data.length + 1;
|
for (let i = 0; i < this.maxDataPoints; i++) {
|
||||||
for (let i = 0; i < this.data.length; i++) {
|
if (i % 5 === 0) {
|
||||||
const dataPointNumber = startIndex + i;
|
let label = '';
|
||||||
if (i % 5 === 0) {
|
if (this.useBinMode) {
|
||||||
xAxisLabels += String(dataPointNumber).padStart(2, ' ');
|
// For bin mode, show labels for all possible positions
|
||||||
} else {
|
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
|
||||||
xAxisLabels += ' ';
|
const elapsedSec = i * Math.floor(this.binDuration / 1000);
|
||||||
}
|
label = String(elapsedSec).padStart(2, ' ') + 's';
|
||||||
}
|
} else {
|
||||||
|
// For legacy mode, show data point numbers
|
||||||
|
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
|
||||||
|
label = String(startIndex + i).padStart(2, ' ');
|
||||||
|
}
|
||||||
|
xAxisLabels += label;
|
||||||
|
} else {
|
||||||
|
xAxisLabels += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
output += xAxisLabels + '\n';
|
output += xAxisLabels + '\n';
|
||||||
|
|
||||||
// Add X-axis label if provided
|
// Add X-axis label if provided
|
||||||
if (this.xAxisLabel) {
|
if (this.xAxisLabel) {
|
||||||
const labelPadding = Math.floor((this.data.length * 2 - this.xAxisLabel.length) / 2);
|
const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2);
|
||||||
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.textContent = output;
|
this.container.textContent = output;
|
||||||
|
|
||||||
// Adjust font size to fit width
|
// Adjust font size to fit width (only once at initialization)
|
||||||
if (this.autoFitWidth) {
|
if (this.autoFitWidth) {
|
||||||
this.adjustFontSize();
|
this.adjustFontSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the external info display
|
// Update the external info display
|
||||||
document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
|
if (this.useBinMode) {
|
||||||
document.getElementById('max-value').textContent = maxValue;
|
const binCounts = this.bins.map(bin => bin.count);
|
||||||
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
const scaleFactor = Math.max(1, Math.ceil(maxValue / scale));
|
||||||
|
document.getElementById('values').textContent = `[${dataToRender.join(', ')}]`;
|
||||||
|
document.getElementById('max-value').textContent = maxValue;
|
||||||
|
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, 1X=${scaleFactor} counts`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
|
||||||
|
document.getElementById('max-value').textContent = maxValue;
|
||||||
|
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,6 +308,115 @@ class ASCIIBarChart {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
updateInfo() {
|
updateInfo() {
|
||||||
document.getElementById('count').textContent = this.data.length;
|
if (this.useBinMode) {
|
||||||
|
const totalCount = this.bins.reduce((sum, bin) => sum + bin.count, 0);
|
||||||
|
document.getElementById('count').textContent = totalCount;
|
||||||
|
} else {
|
||||||
|
document.getElementById('count').textContent = this.data.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the bin system
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
initializeBins() {
|
||||||
|
this.bins = [];
|
||||||
|
this.currentBinIndex = -1;
|
||||||
|
this.binStartTime = null;
|
||||||
|
this.chartStartTime = Date.now();
|
||||||
|
|
||||||
|
// Create first bin
|
||||||
|
this.rotateBin();
|
||||||
|
|
||||||
|
// Set up automatic bin rotation check
|
||||||
|
this.binCheckInterval = setInterval(() => {
|
||||||
|
this.checkBinRotation();
|
||||||
|
}, 100); // Check every 100ms for responsiveness
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current bin should rotate and create new bin if needed
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
checkBinRotation() {
|
||||||
|
if (!this.useBinMode || !this.binStartTime) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if ((now - this.binStartTime) >= this.binDuration) {
|
||||||
|
this.rotateBin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate to a new bin, finalizing the current one
|
||||||
|
*/
|
||||||
|
rotateBin() {
|
||||||
|
// Finalize current bin if it exists
|
||||||
|
if (this.currentBinIndex >= 0) {
|
||||||
|
this.bins[this.currentBinIndex].isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new bin
|
||||||
|
const newBin = {
|
||||||
|
startTime: Date.now(),
|
||||||
|
count: 0,
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bins.push(newBin);
|
||||||
|
this.currentBinIndex = this.bins.length - 1;
|
||||||
|
this.binStartTime = newBin.startTime;
|
||||||
|
|
||||||
|
// Keep only the most recent bins
|
||||||
|
if (this.bins.length > this.maxDataPoints) {
|
||||||
|
this.bins.shift();
|
||||||
|
this.currentBinIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure currentBinIndex points to the last bin (the active one)
|
||||||
|
this.currentBinIndex = this.bins.length - 1;
|
||||||
|
|
||||||
|
// Force a render to update the display immediately
|
||||||
|
this.render();
|
||||||
|
this.updateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format X-axis label for a bin based on the configured format
|
||||||
|
* @param {number} binIndex - Index of the bin
|
||||||
|
* @returns {string} Formatted label
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
formatBinLabel(binIndex) {
|
||||||
|
const bin = this.bins[binIndex];
|
||||||
|
if (!bin) return ' ';
|
||||||
|
|
||||||
|
switch (this.xAxisLabelFormat) {
|
||||||
|
case 'bins':
|
||||||
|
return String(binIndex + 1).padStart(2, ' ');
|
||||||
|
|
||||||
|
case 'timestamps':
|
||||||
|
const time = new Date(bin.startTime);
|
||||||
|
return time.toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}).replace(/:/g, '');
|
||||||
|
|
||||||
|
case 'ranges':
|
||||||
|
const startSec = Math.floor((bin.startTime - this.chartStartTime) / 1000);
|
||||||
|
const endSec = startSec + Math.floor(this.binDuration / 1000);
|
||||||
|
return `${startSec}-${endSec}`;
|
||||||
|
|
||||||
|
case 'elapsed':
|
||||||
|
default:
|
||||||
|
// For elapsed time, always show time relative to the first bin (index 0)
|
||||||
|
// This keeps the leftmost label as 0s and increases to the right
|
||||||
|
const firstBinTime = this.bins[0] ? this.bins[0].startTime : this.chartStartTime;
|
||||||
|
const elapsedSec = Math.floor((bin.startTime - firstBinTime) / 1000);
|
||||||
|
return String(elapsedSec).padStart(2, ' ') + 's';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user