v0.7.26 - Tidy up api
This commit is contained in:
132
api/index.css
132
api/index.css
@@ -85,6 +85,8 @@ body {
|
||||
.relay-info {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
max-width: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.relay-name {
|
||||
@@ -102,6 +104,8 @@ body {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
background-color: var(--secondary-color);
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.relay-pubkey-container:hover {
|
||||
@@ -132,6 +136,10 @@ body {
|
||||
font-size: 10px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
@@ -180,7 +188,7 @@ body {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
transition: background-color 0.2s ease;
|
||||
margin-left: auto;
|
||||
/* margin-left: auto; */
|
||||
}
|
||||
|
||||
.admin-label {
|
||||
@@ -545,6 +553,7 @@ button:disabled {
|
||||
.inline-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.inline-buttons button {
|
||||
@@ -655,9 +664,9 @@ button:disabled {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: var(--border-width) solid var(--border-color);
|
||||
padding-bottom: 10px;
|
||||
/* margin-bottom: 15px; */
|
||||
/* border-bottom: var(--border-width) solid var(--border-color); */
|
||||
/* padding-bottom: 10px; */
|
||||
}
|
||||
|
||||
.countdown-btn {
|
||||
@@ -1002,34 +1011,99 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.inline-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
/* Config Toggle Button Styles */
|
||||
.config-toggle-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.query-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
/* Toggle Button Styles */
|
||||
.toggle-btn {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
padding: 8px 12px;
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border: var(--border-width) solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.toggle-btn:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.toggle-btn:active {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.sql-results-table {
|
||||
font-size: 10px;
|
||||
}
|
||||
.config-toggle-btn:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.sql-results-table th,
|
||||
.sql-results-table td {
|
||||
padding: 4px 6px;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
.config-toggle-btn:active {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.config-toggle-btn[data-state="true"] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.config-toggle-btn[data-state="false"] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.config-toggle-btn[data-state="indeterminate"] {
|
||||
background-color: var(--muted-color);
|
||||
color: var(--primary-color);
|
||||
cursor: not-allowed;
|
||||
border-color: var(--muted-color);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ================================
|
||||
REAL-TIME EVENT RATE CHART
|
||||
================================ */
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
#event-rate-chart {
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: var(--primary-color);
|
||||
background: var(--secondary-color);
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
border: var(--border-width) solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,13 @@
|
||||
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>DATABASE STATISTICS</h2>
|
||||
<button type="button" id="refresh-stats-btn" class="countdown-btn"></button>
|
||||
<!-- Monitoring toggle button will be inserted here by JavaScript -->
|
||||
<!-- Temporarily disable auto-refresh button for real-time monitoring -->
|
||||
<!-- <button type="button" id="refresh-stats-btn" class="countdown-btn"></button> -->
|
||||
</div>
|
||||
|
||||
<!-- Event Rate Graph Container -->
|
||||
<div id="event-rate-chart"></div>
|
||||
|
||||
<!-- Database Overview Table -->
|
||||
<div class="input-group">
|
||||
@@ -77,6 +81,10 @@
|
||||
<td>Total Events</td>
|
||||
<td id="total-events">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active Subscriptions</td>
|
||||
<td id="active-subscriptions">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oldest Event</td>
|
||||
<td id="oldest-event">-</td>
|
||||
@@ -164,6 +172,35 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SUBSCRIPTION DETAILS Section (Admin Only) -->
|
||||
<div class="section flex-section" id="subscriptionDetailsSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>ACTIVE SUBSCRIPTION DETAILS</h2>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="subscription-details-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subscription ID</th>
|
||||
<th>Client IP</th>
|
||||
<th>Duration</th>
|
||||
<th>Events Sent</th>
|
||||
<th>Status</th>
|
||||
<th>Filters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subscription-details-table-body">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testing Section -->
|
||||
<div id="div_config" class="section flex-section" style="display: none;">
|
||||
<h2>RELAY CONFIGURATION</h2>
|
||||
@@ -307,9 +344,11 @@
|
||||
|
||||
<!-- Query Actions -->
|
||||
<div class="input-group">
|
||||
<button type="button" id="execute-sql-btn" class="primary-button">EXECUTE QUERY</button>
|
||||
<button type="button" id="clear-sql-btn">CLEAR</button>
|
||||
<button type="button" id="clear-history-btn" class="danger-button">CLEAR HISTORY</button>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="execute-sql-btn">EXECUTE QUERY</button>
|
||||
<button type="button" id="clear-sql-btn">CLEAR</button>
|
||||
<button type="button" id="clear-history-btn">CLEAR HISTORY</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Results -->
|
||||
@@ -327,6 +366,8 @@
|
||||
<!-- Load NOSTR_LOGIN_LITE main library -->
|
||||
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> -->
|
||||
<script src="/api/nostr-lite.js"></script>
|
||||
<!-- Load text_graph library -->
|
||||
<script src="/api/text_graph.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
896
api/index.js
896
api/index.js
File diff suppressed because it is too large
Load Diff
470
api/text_graph.js
Normal file
470
api/text_graph.js
Normal file
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 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
|
||||
* @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 = {}) {
|
||||
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
|
||||
|
||||
// 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
|
||||
if (this.autoFitWidth) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.adjustFontSize();
|
||||
});
|
||||
this.resizeObserver.observe(this.container);
|
||||
}
|
||||
|
||||
// Initialize first bin if bin mode is enabled
|
||||
if (this.useBinMode) {
|
||||
this.initializeBins();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new data point to the chart
|
||||
* @param {number} value - The numeric value to add
|
||||
*/
|
||||
addValue(value) {
|
||||
if (this.useBinMode) {
|
||||
// Time bin mode: increment count in current active bin
|
||||
this.checkBinRotation(); // Ensure we have an active bin
|
||||
this.bins[this.currentBinIndex].count++;
|
||||
this.totalDataPoints++;
|
||||
} 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.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data from the chart
|
||||
*/
|
||||
clear() {
|
||||
this.data = [];
|
||||
this.totalDataPoints = 0;
|
||||
|
||||
if (this.useBinMode) {
|
||||
this.bins = [];
|
||||
this.currentBinIndex = -1;
|
||||
this.binStartTime = null;
|
||||
this.initializeBins();
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the width of the chart in characters
|
||||
* @returns {number} The chart width in characters
|
||||
* @private
|
||||
*/
|
||||
getChartWidth() {
|
||||
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 yAxisNumbers = 3; // Width of Y-axis numbers
|
||||
const separator = 1; // The '|' character
|
||||
// const dataWidth = dataLength * 2; // Each column is 2 characters wide // TEMP: commented for no-space test
|
||||
const dataWidth = dataLength; // Each column is 1 character wide // TEMP: adjusted for no-space columns
|
||||
const padding = 1; // Extra 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// Use a slightly smaller ratio to fit more content
|
||||
const charWidthRatio = 0.6;
|
||||
const padding = 30; // Reduce padding to fit more content
|
||||
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));
|
||||
|
||||
// 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.lineHeight = '1.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart to the container
|
||||
* @private
|
||||
*/
|
||||
render() {
|
||||
let dataToRender = [];
|
||||
let maxValue = 0;
|
||||
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;
|
||||
}
|
||||
// Always create a fixed-length array filled with 0s, then overlay actual bin data
|
||||
dataToRender = new Array(this.maxDataPoints).fill(0);
|
||||
|
||||
// Overlay actual bin data (most recent bins, reversed for left-to-right display)
|
||||
const startIndex = Math.max(0, this.bins.length - this.maxDataPoints);
|
||||
const recentBins = this.bins.slice(startIndex);
|
||||
|
||||
// Reverse the bins so most recent is on the left, and overlay onto the fixed array
|
||||
recentBins.reverse().forEach((bin, index) => {
|
||||
if (index < this.maxDataPoints) {
|
||||
dataToRender[index] = bin.count;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
|
||||
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 = '';
|
||||
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)
|
||||
const yLabelWidth = this.yAxisLabel ? 2 : 0;
|
||||
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
||||
|
||||
// Add title if provided (centered)
|
||||
if (this.title) {
|
||||
// const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test
|
||||
const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space 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 L = this.yAxisLabel.length;
|
||||
const startRow = Math.floor((scale - L) / 2) + 1;
|
||||
const relativeRow = scale - row + 1; // 1 at top, scale at bottom
|
||||
if (relativeRow >= startRow && relativeRow < startRow + L) {
|
||||
const labelIndex = relativeRow - startRow;
|
||||
line += this.yAxisLabel[labelIndex] + ' ';
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the actual count value this row represents (0 at bottom, increasing upward)
|
||||
const rowCount = (row - 1) * scaleFactor;
|
||||
|
||||
// Add Y-axis label (show actual count values)
|
||||
line += String(rowCount).padStart(3, ' ') + ' |';
|
||||
|
||||
// Draw each column
|
||||
for (let i = 0; i < dataToRender.length; i++) {
|
||||
const count = dataToRender[i];
|
||||
const scaledHeight = Math.ceil(count / scaleFactor);
|
||||
|
||||
if (scaledHeight >= row) {
|
||||
// line += ' X'; // TEMP: commented out space between columns
|
||||
line += 'X'; // TEMP: no space between columns
|
||||
} else {
|
||||
// line += ' '; // TEMP: commented out space between columns
|
||||
line += ' '; // TEMP: single space for empty columns
|
||||
}
|
||||
}
|
||||
|
||||
output += line + '\n';
|
||||
}
|
||||
|
||||
// Draw X-axis
|
||||
// output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test
|
||||
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length
|
||||
|
||||
// Draw X-axis labels based on mode and format
|
||||
let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis
|
||||
|
||||
// Determine label interval (every 5 columns)
|
||||
const labelInterval = 5;
|
||||
|
||||
// Generate all labels first and store in array
|
||||
let labels = [];
|
||||
for (let i = 0; i < this.maxDataPoints; i++) {
|
||||
if (i % labelInterval === 0) {
|
||||
let label = '';
|
||||
if (this.useBinMode) {
|
||||
// For bin mode, show labels for all possible positions
|
||||
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
|
||||
const elapsedSec = (i * this.binDuration) / 1000;
|
||||
// Format with appropriate precision for sub-second bins
|
||||
if (this.binDuration < 1000) {
|
||||
// Show decimal seconds for sub-second bins
|
||||
label = elapsedSec.toFixed(1) + 's';
|
||||
} else {
|
||||
// Show whole seconds for 1+ second bins
|
||||
label = String(Math.round(elapsedSec)) + 's';
|
||||
}
|
||||
} else {
|
||||
// For legacy mode, show data point numbers
|
||||
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
|
||||
label = String(startIndex + i);
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the label string with calculated spacing
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const label = labels[i];
|
||||
xAxisLabels += label;
|
||||
|
||||
// Add spacing: labelInterval - label.length (except for last label)
|
||||
if (i < labels.length - 1) {
|
||||
const spacing = labelInterval - label.length;
|
||||
xAxisLabels += ' '.repeat(spacing);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the label line extends to match the X-axis dash line length
|
||||
// The dash line is this.maxDataPoints characters long, starting after " +"
|
||||
const dashLineLength = this.maxDataPoints;
|
||||
const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " "
|
||||
if (xAxisLabels.length < minLabelLineLength) {
|
||||
xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
|
||||
}
|
||||
output += xAxisLabels + '\n';
|
||||
|
||||
// Add X-axis label if provided
|
||||
if (this.xAxisLabel) {
|
||||
// const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test
|
||||
const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns
|
||||
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
||||
}
|
||||
|
||||
this.container.textContent = output;
|
||||
|
||||
// Adjust font size to fit width (only once at initialization)
|
||||
if (this.autoFitWidth) {
|
||||
this.adjustFontSize();
|
||||
}
|
||||
|
||||
// Update the external info display
|
||||
if (this.useBinMode) {
|
||||
const binCounts = this.bins.map(bin => bin.count);
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the info display
|
||||
* @private
|
||||
*/
|
||||
updateInfo() {
|
||||
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