Files
c-relay-pg/admin/lib/ascii_chart.php
T

148 lines
5.1 KiB
PHP

<?php
/**
* admin2/lib/ascii_chart.php — Server-side ASCII X-bar chart renderer.
*
* Produces a monospaced ASCII bar chart (using 'X' characters) from an
* array of bin counts. Mirrors the layout of the original text_graph.js
* ASCIIBarChart: Y-axis count labels, X-bar columns, X-axis with
* elapsed-time labels.
*
* The output is a plain text string suitable for both browser <div>
* injection (white-space: pre) and terminal display via curl.
*/
/**
* Render an ASCII bar chart from an array of bin counts.
*
* @param array $bins Array of integer counts (one per time bin, left=oldest)
* @param array $options {
* @var string $title Chart title (centered at top)
* @var int $max_height Chart height in rows (default 11)
* @var string $x_axis_label Label below the X-axis (default '')
* @var int $bin_duration Seconds per bin — for X-axis elapsed labels
* @var int $label_interval Label every N bins (default 5)
* }
* @return string The ASCII chart as a multi-line string.
*/
function render_ascii_chart(array $bins, array $options = []): string {
$title = $options['title'] ?? 'New Events';
$max_height = $options['max_height'] ?? 11;
$x_axis_label = $options['x_axis_label'] ?? '';
$bin_duration = $options['bin_duration'] ?? 10;
$label_interval = $options['label_interval'] ?? 5;
$num_bins = count($bins);
if ($num_bins === 0) {
return "No data available.\n";
}
$max_count = max($bins);
if ($max_count < 1) $max_count = 1; // Avoid division by zero for all-empty bins
// Scaling: each X represents scale_factor counts
$scale_factor = max(1, (int)ceil($max_count / $max_height));
$scaled_max = (int)ceil($max_count / $scale_factor) * $scale_factor;
$output = '';
// --- Title (centered) ---
$chart_width = 4 + $num_bins; // 4 = Y-axis number width (3) + separator (1)
if ($title !== '') {
$title_padding = (int)floor(($chart_width - strlen($title)) / 2);
if ($title_padding < 0) $title_padding = 0;
$output .= str_repeat(' ', $title_padding) . $title . "\n\n";
}
// --- Bar rows (top to bottom) ---
for ($row = $max_height; $row > 0; $row--) {
$row_count = ($row - 1) * $scale_factor + 1;
$line = str_pad((string)$row_count, 3, ' ', STR_PAD_LEFT) . ' |';
for ($i = 0; $i < $num_bins; $i++) {
$count = $bins[$i];
$scaled_height = ($count > 0) ? (int)ceil($count / $scale_factor) : 0;
$line .= ($scaled_height >= $row) ? 'X' : ' ';
}
$output .= $line . "\n";
}
// --- X-axis line ---
$output .= ' +' . str_repeat('-', $num_bins) . "\n";
// --- X-axis labels (elapsed time every label_interval bins) ---
$label_line = ' ';
$labels = [];
for ($i = 0; $i < $num_bins; $i++) {
if ($i % $label_interval === 0) {
$elapsed_sec = $i * $bin_duration;
$labels[] = format_elapsed_time($elapsed_sec);
}
}
// Build label line with spacing
for ($i = 0; $i < count($labels); $i++) {
$label_line .= $labels[$i];
if ($i < count($labels) - 1) {
$spacing = $label_interval - strlen($labels[$i]);
if ($spacing < 1) $spacing = 1;
$label_line .= str_repeat(' ', $spacing);
}
}
// Pad to match X-axis dash line length
$min_label_len = 4 + $num_bins;
if (strlen($label_line) < $min_label_len) {
$label_line .= str_repeat(' ', $min_label_len - strlen($label_line));
}
$output .= $label_line . "\n";
// --- X-axis label (if provided) ---
if ($x_axis_label !== '') {
$label_pad = (int)floor(($num_bins - strlen($x_axis_label)) / 2);
if ($label_pad < 0) $label_pad = 0;
$output .= "\n" . ' ' . str_repeat(' ', $label_pad) . $x_axis_label . "\n";
}
return $output;
}
/**
* Format an elapsed time in seconds as a compact label.
* < 60s → "Ns"
* < 1h → "Nm"
* < 1d → "Nh"
* else → "Nd"
*/
function format_elapsed_time(int $seconds): string {
if ($seconds < 60) {
return $seconds . 's';
} elseif ($seconds < 3600) {
return (int)floor($seconds / 60) . 'm';
} elseif ($seconds < 86400) {
return (int)floor($seconds / 3600) . 'h';
} else {
return (int)floor($seconds / 86400) . 'd';
}
}
/**
* Build a fixed-length bin array from SQL query rows.
*
* The query returns only non-empty bins. This function creates a
* zero-filled array of $num_bins length and overlays the counts
* at the correct positions, so empty time slots show as blank
* columns — the chart always advances in time.
*
* @param array $rows Query rows with 'bin' (int) and 'cnt' (int)
* @param int $num_bins Total number of bins (fixed length)
* @return array Zero-filled array of counts
*/
function build_bin_array(array $rows, int $num_bins): array {
$bins = array_fill(0, $num_bins, 0);
foreach ($rows as $r) {
$idx = (int)$r['bin'];
if ($idx >= 0 && $idx < $num_bins) {
$bins[$idx] = (int)$r['cnt'];
}
}
return $bins;
}