github-actions[bot]
Sync: publish Space API fix
5e0b58b
Raw History Blame Contribute Delete
9.05 kB
/**
* Brain-5D Dashboard — Dynamics Visualizations (Raster, Histogram, Layer Explorer)
*
* @version 1.0.0
* @license MIT
*/
"use strict";
const $ = (id) => document.getElementById(id);
function formatNumber(value) {
if (value === null || value === undefined) return '—';
return Number(value).toLocaleString();
}
function formatFloat(value, decimals = 3) {
if (value === null || value === undefined) return '—';
return Number(value).toFixed(decimals);
}
function setText(id, value) {
const el = $(id);
if (el) el.textContent = String(value);
}
export async function refreshSpikeRaster() {
try {
const r = await fetch('/api/live/raster', { cache: 'no-store' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
drawSpikeRaster(data);
const badge = $('raster-badge');
if (badge) {
badge.textContent = `${data.sample_count}/${data.total_neurons} Neuronen`;
badge.className = 'gate-badge passed';
}
const meta = $('raster-meta');
if (meta) meta.textContent = `Tick ${data.tick} · Fenster: ${data.window_ticks} Ticks · ${data.sample_count} aktive Neuronen von ${data.total_neurons}`;
} catch {
const badge = $('raster-badge');
if (badge) { badge.textContent = 'offline'; badge.className = 'gate-badge failed'; }
}
}
function drawSpikeRaster(data) {
const canvas = $('spike-raster');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const nids = data.neuron_ids || [];
const ticks = data.spike_ticks || [];
if (!nids.length || !ticks.length) {
ctx.fillStyle = '#4a6a8a';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('Keine Spike-Daten im aktuellen Fenster', canvas.width/2, canvas.height/2);
return;
}
const windowTicks = data.window_ticks || 100;
const tick = data.tick || 0;
const tickMin = tick - windowTicks;
const tickMax = tick;
const neuronCount = nids.length;
const idToRow = {};
nids.forEach((id, i) => { idToRow[id] = i; });
const margin = { top: 20, right: 20, bottom: 30, left: 50 };
const plotW = canvas.width - margin.left - margin.right;
const plotH = canvas.height - margin.top - margin.bottom;
ctx.fillStyle = '#050d14';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(142, 178, 193, 0.06)';
ctx.lineWidth = 0.5;
for (let i = 0; i < 10; i++) {
const y = margin.top + (plotH / 10) * i;
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + plotW, y);
ctx.stroke();
}
ctx.fillStyle = 'rgba(64, 224, 208, 0.6)';
for (let i = 0; i < ticks.length; i++) {
const t = ticks[i];
const nid = nids[i % nids.length];
const row = idToRow[nid];
if (row === undefined) continue;
const x = margin.left + ((t - tickMin) / Math.max(1, tickMax - tickMin)) * plotW;
const y = margin.top + (row / Math.max(1, neuronCount)) * plotH;
ctx.fillRect(x - 0.5, y - 1, 2, 2);
}
ctx.fillStyle = 'rgba(142, 178, 193, 0.5)';
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.fillText('Tick →', margin.left + plotW/2, canvas.height - 5);
ctx.textAlign = 'right';
ctx.fillText('Neuron ↑', margin.left - 5, margin.top + 12);
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(142, 178, 193, 0.3)';
ctx.font = '9px monospace';
ctx.fillText(tickMin, margin.left, canvas.height - 8);
ctx.textAlign = 'right';
ctx.fillText(tickMax, margin.left + plotW, canvas.height - 8);
}
export async function refreshRateHistogram() {
try {
const r = await fetch('/api/live/histogram?bins=30', { cache: 'no-store' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
drawRateHistogram(data);
const stats = $('histogram-stats');
if (stats) {
stats.textContent = `μ=${data.mean_rate.toFixed(4)} σ=${data.std_rate.toFixed(4)}`;
stats.className = 'gate-badge passed';
}
const meta = $('histogram-meta');
if (meta) meta.textContent = `Tick ${data.tick} · ${data.active_count} aktiv / ${data.silent_count} stumm · Median ${data.median_rate.toFixed(4)}`;
} catch {
const stats = $('histogram-stats');
if (stats) { stats.textContent = 'offline'; stats.className = 'gate-badge failed'; }
}
}
function drawRateHistogram(data) {
const canvas = $('rate-histogram');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const bins = data.bins || [];
const counts = data.counts || [];
if (!counts.length) {
ctx.fillStyle = '#4a6a8a';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('Keine Histogramm-Daten', canvas.width/2, canvas.height/2);
return;
}
const margin = { top: 20, right: 20, bottom: 35, left: 60 };
const plotW = canvas.width - margin.left - margin.right;
const plotH = canvas.height - margin.top - margin.bottom;
ctx.fillStyle = '#050d14';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const maxCount = Math.max(...counts, 1);
const barW = plotW / counts.length;
for (let i = 0; i < counts.length; i++) {
const x = margin.left + i * barW;
const h = (counts[i] / maxCount) * plotH;
const y = margin.top + plotH - h;
const normalized = counts[i] / maxCount;
const hue = 210 - normalized * 195;
ctx.fillStyle = `hsl(${hue}, 85%, ${30 + normalized * 40}%)`;
ctx.fillRect(x + 1, y, barW - 2, h);
}
if (data.mean_rate > 0 && bins.length > 1) {
const maxRate = bins[bins.length - 1];
const meanX = margin.left + (data.mean_rate / maxRate) * plotW;
ctx.strokeStyle = 'rgba(255, 159, 28, 0.7)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(meanX, margin.top);
ctx.lineTo(meanX, margin.top + plotH);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(255, 159, 28, 0.7)';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('μ=' + data.mean_rate.toFixed(3), meanX, margin.top - 4);
}
ctx.fillStyle = 'rgba(142, 178, 193, 0.5)';
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.fillText('Feuerrate (spikes/tick) →', margin.left + plotW/2, canvas.height - 5);
ctx.textAlign = 'right';
ctx.fillText('Anzahl ↑', margin.left - 5, margin.top + 12);
}
export async function refreshLayerExplorer() {
const dimSelect = $('layer-dim');
const slider = $('layer-slider');
const kindSelect = $('layer-kind');
if (!dimSelect || !slider) return;
const dim = dimSelect.value;
const layerVal = parseInt(slider.value, 10);
const kind = kindSelect ? kindSelect.value : 'activity';
try {
const r = await fetch(`/api/live/projection?kind=${encodeURIComponent(kind)}&resolution=40`, { cache: 'no-store' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
drawLayerSlice(data, dim, layerVal);
const badge = $('layer-badge');
if (badge) badge.textContent = dim.toUpperCase();
const meta = $('layer-meta');
if (meta) meta.textContent = `Tick ${data.tick} · ${dim.toUpperCase()}=${layerVal} · ${data.kind || kind} · ${data.source}`;
} catch {
const meta = $('layer-meta');
if (meta) meta.textContent = '⚠️ Layer-Daten nicht verfügbar';
}
}
function drawLayerSlice(payload, dim, layerValue) {
const canvas = $('layer-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const values = payload.values || [];
if (!values.length) return;
const rows = values.length;
const cols = values[0]?.length || 1;
const flat = values.flat().filter(v => v !== null && v !== undefined && Number.isFinite(v));
if (!flat.length) {
ctx.fillStyle = '#4a6a8a';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('Keine Daten', canvas.width/2, canvas.height/2);
return;
}
const min = Math.min(...flat);
const max = Math.max(...flat);
const range = max - min || 1;
const cellW = canvas.width / cols;
const cellH = canvas.height / rows;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const val = values[y]?.[x];
if (val === null || val === undefined) {
ctx.fillStyle = 'rgba(40, 50, 65, 0.4)';
ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH));
} else {
const normalized = (val - min) / range;
const hue = 210 - normalized * 195;
const light = 16 + normalized * 50;
ctx.fillStyle = `hsl(${hue}, 88%, ${light}%)`;
ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH));
}
}
}
ctx.fillStyle = 'rgba(142, 178, 193, 0.4)';
ctx.font = '10px monospace';
ctx.textAlign = 'left';
ctx.fillText(`${dim.toUpperCase()}=${layerValue}`, 8, 16);
ctx.textAlign = 'right';
ctx.fillText(`${min.toFixed(2)} … ${max.toFixed(2)}`, canvas.width - 8, 16);
}