#!/usr/bin/env php
<?php
// =============================================================================
// data_chart — a chart from a CSV file, written as SVG, for the Data tools (tools_builtins/data.json).
//
//   php data_chart bar|line|scatter|hist INPUT.csv OUTPUT.svg XCOLUMN [YCOLUMN] [TITLE words...]
//     bar      XCOLUMN = categories (up to 30; repeated values are added up), YCOLUMN = numbers (omit it to count rows)
//     line     XCOLUMN = x (numbers are sorted, text keeps its order), YCOLUMN = numbers
//     scatter  XCOLUMN and YCOLUMN = numbers
//     hist     XCOLUMN = numbers; YCOLUMN, if given and a whole number 5-40, is the number of bins
//   A column is a name, the name with _ for spaces, or a number (1 = the first). Comma, semicolon or tab separated; a BOM is fine.
//
// SVG opens in the Preview panel and in any browser, scales without loss, and ImageMagick can turn it into a PNG. No library is used: the
// numbers are laid out here (nice axis ticks, rotated labels when there are many categories) and every text is XML-escaped.
// =============================================================================
if (PHP_SAPI !== 'cli') { http_response_code(403); exit; }
function fail(string $m): void { fwrite(STDERR, "data_chart: $m\n"); exit(1); }
function nice_ticks(float $lo, float $hi, int $n = 5): array {
    if ($hi <= $lo) { $hi = $lo + 1; }
    $span = $hi - $lo; $raw = $span / max(1, $n); $mag = 10 ** floor(log10($raw)); $f = $raw / $mag;
    $step = ($f < 1.5 ? 1 : ($f < 3 ? 2 : ($f < 7 ? 5 : 10))) * $mag;
    $a = floor($lo / $step) * $step; $b = ceil($hi / $step) * $step; $tk = [];
    for ($v = $a; $v <= $b + $step / 2; $v += $step) $tk[] = round($v, 10);
    return [$tk, $a, $b];
}
function fmt(float $v): string {
    if (abs($v) >= 1e6 || (abs($v) < 1e-3 && $v != 0)) return rtrim(rtrim(sprintf('%.3g', $v), '0'), '.');
    $d = abs($v) >= 10000 ? 0 : (abs($v) >= 1000 ? 1 : 2);
    $s = number_format($v, $d, '.', ',');
    return strpos($s, '.') !== false ? rtrim(rtrim($s, '0'), '.') : $s;
}
function esc(string $s): string { return htmlspecialchars($s, ENT_XML1 | ENT_QUOTES, 'UTF-8'); }

$kind = $argv[1] ?? ''; $in = $argv[2] ?? ''; $out = $argv[3] ?? ''; $xc = $argv[4] ?? ''; $rest = array_slice($argv, 5);
if (!in_array($kind, ['bar', 'line', 'scatter', 'hist'], true) || $in === '' || $out === '' || $xc === '') fail('usage: data_chart bar|line|scatter|hist INPUT.csv OUTPUT.svg XCOLUMN [YCOLUMN] [TITLE...]');
if (!is_file($in)) fail("no such file: $in");
$raw = (string)file_get_contents($in); if (strncmp($raw, "\xEF\xBB\xBF", 3) === 0) $raw = substr($raw, 3);
$first = strtok($raw, "\n"); $c = substr_count($first, ','); $s = substr_count($first, ';'); $t = substr_count($first, "\t");
$sep = ($s > $c && $s >= $t) ? ';' : (($t > $c) ? "\t" : ',');
$fh = fopen('php://memory', 'w+'); fwrite($fh, $raw); rewind($fh);
$head = fgetcsv($fh, 0, $sep, '"', ''); if (!$head) fail('the file has no header row');
$rows = []; while (($r = fgetcsv($fh, 0, $sep, '"', '')) !== false) { if ($r === [null]) continue; $rows[] = $r; }
if (!$rows) fail('the file has no data rows');
$col = function (string $want) use ($head): int {
    foreach ($head as $i => $h) if ($h === $want) return $i;
    foreach ($head as $i => $h) if (str_replace(' ', '_', $h) === $want) return $i;
    if (ctype_digit($want) && (int)$want >= 1 && (int)$want <= count($head)) return (int)$want - 1;
    fail("no column '$want'. Columns: " . implode(', ', $head));
};
$xi = $col($xc); $yi = null; $yc = ''; $bins = null;
if ($rest && $kind !== 'hist') { $yc = array_shift($rest); $yi = $col($yc); }
elseif ($rest && $kind === 'hist' && ctype_digit($rest[0])) { $bins = max(5, min(40, (int)array_shift($rest))); }
if (in_array($kind, ['line', 'scatter'], true) && $yi === null) fail("$kind needs an x column and a y column");
$title = trim(implode(' ', $rest));
$num = fn($v) => ($v !== null && trim((string)$v) !== '' && is_numeric(trim((string)$v))) ? (float)trim((string)$v) : null;

$W = 820; $H = 500; $L = 70; $R = 24; $Tp = 56; $B = 92; $pw = $W - $L - $R; $ph = $H - $Tp - $B;
$pal = ['#4c78a8', '#f58518', '#54a24b', '#e45756', '#72b7b2', '#b279a2', '#9d755d', '#bab0ac'];
$skipped = 0; $cats = []; $xs = []; $ys = []; $pts = []; $note = ''; $ylab = ''; $numericX = false; $mn = 0; $w = 1;
if ($kind === 'bar') {
    $sum = [];
    foreach ($rows as $r) {
        $k = (string)($r[$xi] ?? ''); if ($k === '') { $skipped++; continue; }
        if ($yi === null) { $sum[$k] = ($sum[$k] ?? 0) + 1; continue; }
        $v = $num($r[$yi] ?? null); if ($v === null) { $skipped++; continue; }
        $sum[$k] = ($sum[$k] ?? 0) + $v;
    }
    if (!$sum) fail('nothing to draw: no usable rows');
    if (count($sum) > 30) { arsort($sum); $sum = array_slice($sum, 0, 30, true); $note = 'top 30 of the categories'; }
    $cats = array_keys($sum); $ys = array_values($sum); $ylab = $yi === null ? 'rows' : $head[$yi];
} elseif ($kind === 'hist') {
    foreach ($rows as $r) { $v = $num($r[$xi] ?? null); if ($v === null) $skipped++; else $xs[] = $v; }
    if (!$xs) fail('nothing to draw: the column has no numbers');
    sort($xs); $n = count($xs); $bins = $bins ?? max(5, min(40, (int)ceil(log($n, 2)) + 1)); $mn = $xs[0]; $mx = $xs[$n - 1]; if ($mx == $mn) $mx = $mn + 1;
    $w = ($mx - $mn) / $bins; $cnt = array_fill(0, $bins, 0);
    foreach ($xs as $v) { $b = min($bins - 1, (int)floor(($v - $mn) / $w)); $cnt[$b]++; }
    $ys = $cnt; $ylab = 'count'; $note = "$bins bins";
} else {
    foreach ($rows as $r) {
        $y = $num($r[$yi] ?? null); $xr = $r[$xi] ?? null; $x = $num($xr);
        if ($y === null || ($kind === 'scatter' && $x === null) || $xr === null || $xr === '') { $skipped++; continue; }
        $pts[] = [$x, (string)$xr, $y];
    }
    if (!$pts) fail('nothing to draw: no usable rows');
    $numericX = true; foreach ($pts as $p) if ($p[0] === null) { $numericX = false; break; }
    if ($kind === 'line' && $numericX) usort($pts, fn($a, $b) => $a[0] <=> $b[0]);
    $ylab = $head[$yi];
}

$svg = [];
$svg[] = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $W . ' ' . $H . '" width="' . $W . '" height="' . $H . '" font-family="DejaVu Sans, Arial, sans-serif" font-size="12">';
$svg[] = '<rect width="100%" height="100%" fill="#ffffff"/>';
$ttl = $title !== '' ? $title : ($kind === 'hist' ? 'Distribution of ' . $head[$xi] : ($ylab . ' by ' . $head[$xi]));
$svg[] = '<text x="' . ($W / 2) . '" y="26" text-anchor="middle" font-size="16" font-weight="bold" fill="#222">' . esc($ttl) . '</text>';
$sub = count($rows) . ' rows' . ($skipped ? ", $skipped skipped (empty or not a number)" : '') . ($note !== '' ? ', ' . $note : '');
$svg[] = '<text x="' . ($W / 2) . '" y="44" text-anchor="middle" font-size="11" fill="#777">' . esc($sub) . '</text>';
$px = fn($f) => $L + $f * $pw; $py = fn($f) => $Tp + $ph - $f * $ph;
$yvals = ($kind === 'bar' || $kind === 'hist') ? $ys : array_column($pts, 2);
[$yt, $ya, $yb] = nice_ticks(min(0, min($yvals)), max($yvals) <= 0 ? 1 : max($yvals));
$xt = []; $xa = 0; $xb = 1;
if ($kind === 'scatter' || ($kind === 'line' && $numericX)) { $xv = array_column($pts, 0); [$xt, $xa, $xb] = nice_ticks(min($xv), max($xv)); }
foreach ($yt as $v) {
    $f = ($v - $ya) / ($yb - $ya); $y = $py($f);
    $svg[] = '<line x1="' . $L . '" y1="' . round($y, 1) . '" x2="' . ($L + $pw) . '" y2="' . round($y, 1) . '" stroke="#e5e5e5"/>';
    $svg[] = '<text x="' . ($L - 8) . '" y="' . round($y + 4, 1) . '" text-anchor="end" fill="#555">' . esc(fmt($v)) . '</text>';
}
$svg[] = '<line x1="' . $L . '" y1="' . $Tp . '" x2="' . $L . '" y2="' . ($Tp + $ph) . '" stroke="#888"/>';
$svg[] = '<line x1="' . $L . '" y1="' . ($Tp + $ph) . '" x2="' . ($L + $pw) . '" y2="' . ($Tp + $ph) . '" stroke="#888"/>';
$svg[] = '<text transform="translate(16 ' . ($Tp + $ph / 2) . ') rotate(-90)" text-anchor="middle" fill="#555">' . esc($ylab) . '</text>';
if ($kind === 'bar' || $kind === 'hist') {
    $n = count($ys); $slot = $pw / $n; $bw = $slot * ($kind === 'hist' ? 0.96 : 0.7);
    foreach ($ys as $i => $v) {
        $f = ($v - $ya) / ($yb - $ya); $f0 = (0 - $ya) / ($yb - $ya); $y1 = $py(max($f, $f0)); $h = abs($py($f) - $py($f0)); $x = $L + $i * $slot + ($slot - $bw) / 2;
        $tip = $kind === 'bar' ? $cats[$i] : fmt($mn + $i * $w) . ' to ' . fmt($mn + ($i + 1) * $w);
        $svg[] = '<rect x="' . round($x, 1) . '" y="' . round($y1, 1) . '" width="' . round($bw, 1) . '" height="' . round($h, 1) . '" fill="' . $pal[0] . '"><title>' . esc($tip . ': ' . fmt((float)$v)) . '</title></rect>';
        if ($n <= 14) $svg[] = '<text x="' . round($x + $bw / 2, 1) . '" y="' . round($y1 - 4, 1) . '" text-anchor="middle" font-size="10" fill="#444">' . esc(fmt((float)$v)) . '</text>';
        if ($kind === 'bar') {
            $lab = mb_strlen($cats[$i]) > 18 ? mb_substr($cats[$i], 0, 17) . '…' : $cats[$i]; $cx = round($x + $bw / 2, 1); $cy = $Tp + $ph + 14;
            if ($n > 8) $svg[] = '<text transform="translate(' . $cx . ' ' . $cy . ') rotate(-40)" text-anchor="end" fill="#555">' . esc($lab) . '</text>';
            else $svg[] = '<text x="' . $cx . '" y="' . $cy . '" text-anchor="middle" fill="#555">' . esc($lab) . '</text>';
        }
    }
    if ($kind === 'hist') foreach ([0, $bins / 2, $bins] as $i) $svg[] = '<text x="' . round($L + $i * $slot, 1) . '" y="' . ($Tp + $ph + 16) . '" text-anchor="middle" fill="#555">' . esc(fmt($mn + $i * $w)) . '</text>';
    $svg[] = '<text x="' . ($L + $pw / 2) . '" y="' . ($H - 10) . '" text-anchor="middle" fill="#555">' . esc($head[$xi]) . '</text>';
} else {
    $numX = $kind === 'scatter' || $numericX; $n = count($pts);
    if ($numX) foreach ($xt as $v) {
        $x = $px(($v - $xa) / ($xb - $xa));
        $svg[] = '<line x1="' . round($x, 1) . '" y1="' . ($Tp + $ph) . '" x2="' . round($x, 1) . '" y2="' . ($Tp + $ph + 4) . '" stroke="#888"/><text x="' . round($x, 1) . '" y="' . ($Tp + $ph + 18) . '" text-anchor="middle" fill="#555">' . esc(fmt($v)) . '</text>';
    }
    $coords = [];
    foreach ($pts as $i => $p) {
        $fx = $numX ? ($p[0] - $xa) / ($xb - $xa) : ($n > 1 ? $i / ($n - 1) : 0.5); $fy = ($p[2] - $ya) / ($yb - $ya);
        $coords[] = [round($px($fx), 1), round($py($fy), 1), $p];
    }
    if ($kind === 'line') $svg[] = '<polyline fill="none" stroke="' . $pal[0] . '" stroke-width="2" points="' . implode(' ', array_map(fn($c2) => $c2[0] . ',' . $c2[1], $coords)) . '"/>';
    foreach ($coords as $c2) $svg[] = '<circle cx="' . $c2[0] . '" cy="' . $c2[1] . '" r="' . ($kind === 'scatter' ? 3.5 : 3) . '" fill="' . $pal[0] . '" fill-opacity="' . ($kind === 'scatter' ? '0.7' : '1') . '"><title>' . esc($c2[2][1] . ', ' . fmt((float)$c2[2][2])) . '</title></circle>';
    if (!$numX) {
        $step = max(1, (int)ceil($n / 12));
        foreach ($coords as $i => $c2) if ($i % $step === 0) {
            $lab = mb_strlen($c2[2][1]) > 14 ? mb_substr($c2[2][1], 0, 13) . '…' : $c2[2][1];
            $svg[] = '<text transform="translate(' . $c2[0] . ' ' . ($Tp + $ph + 14) . ') rotate(-40)" text-anchor="end" fill="#555">' . esc($lab) . '</text>';
        }
    }
    $svg[] = '<text x="' . ($L + $pw / 2) . '" y="' . ($H - 10) . '" text-anchor="middle" fill="#555">' . esc($head[$xi]) . '</text>';
}
$svg[] = '</svg>';
if (file_put_contents($out . '.tmp', implode("\n", $svg) . "\n") === false || !rename($out . '.tmp', $out)) fail("could not write $out");
echo "Wrote $out (" . count($rows) . " rows" . ($skipped ? ", $skipped skipped" : '') . ")\n";
