anomaly-detection / src /server /static /dashboard.js
zeetay's picture
sync with main
25e7a66 verified
Raw
History Blame Contribute Delete
10.7 kB
(function () {
'use strict';
const HF_SPACE = 'wss://zeetay-anomaly-detection.hf.space';
const useSameHost = location.hostname.endsWith('.hf.space') || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
const WS_URL = (useSameHost ? (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host : HF_SPACE) + '/ws';
const MAX_POINTS = 200;
const THRESHOLD = 0.60;
// DOM refs
const statusEl = document.getElementById('status');
const phaseBadgeEl = document.getElementById('phase-badge');
const phaseDescEl = document.getElementById('phase-desc');
const obsCountEl = document.getElementById('obs-count');
const totalObsEl = document.getElementById('total-obs');
const totalAnomaliesEl = document.getElementById('total-anomalies');
const totalDriftEl = document.getElementById('total-drift');
const precisionEl = document.getElementById('precision');
const recallEl = document.getElementById('recall');
const f1El = document.getElementById('f1');
// Rolling buffer of the last MAX_POINTS messages
const buffer = [];
// ── helpers ─────────────────────────────────────────────────────────────────
function setConnected(ok) {
statusEl.textContent = ok ? 'Connected' : 'Disconnected';
statusEl.className = ok ? 'connected' : 'disconnected';
}
function updatePhase(phase) {
const labels = { A: 'Phase A', B: 'Phase B', C: 'Phase C' };
const descs = { A: 'building baseline', B: 'sensors drifting', C: 'anomalies active' };
phaseBadgeEl.textContent = labels[phase] || phase;
phaseBadgeEl.className = 'phase-badge phase-' + phase;
if (phaseDescEl) phaseDescEl.textContent = descs[phase] || '';
}
function updateCounters(msg) {
if (msg.total_anomalies_detected != null) totalAnomaliesEl.textContent = msg.total_anomalies_detected;
if (msg.total_drift_events != null) totalDriftEl.textContent = msg.total_drift_events;
const obs = (msg.observation_index != null) ? msg.observation_index + 1 : 0;
totalObsEl.textContent = obs;
obsCountEl.textContent = obs + ' observations';
precisionEl.textContent = (msg.running_precision ?? 0).toFixed(2);
recallEl.textContent = (msg.running_recall ?? 0).toFixed(2);
f1El.textContent = (msg.running_f1 ?? 0).toFixed(2);
if (msg.phase) updatePhase(msg.phase);
}
// ── shared drift-line plugin ─────────────────────────────────────────────────
function makeDriftPlugin() {
return {
id: 'driftLines',
afterDraw: function (chart) {
if (!chart.chartArea) return;
var driftPts = buffer.filter(function (p) { return p.drift_event; });
if (!driftPts.length) return;
var xScale = chart.scales && chart.scales.x;
if (!xScale) return;
var ctx = chart.ctx;
var area = chart.chartArea;
ctx.save();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = '#eab308';
ctx.lineWidth = 1.5;
driftPts.forEach(function (p) {
var x = xScale.getPixelForValue(p.index);
if (x >= area.left && x <= area.right) {
ctx.beginPath();
ctx.moveTo(x, area.top);
ctx.lineTo(x, area.bottom);
ctx.stroke();
}
});
ctx.restore();
}
};
}
// ── sensor chart ─────────────────────────────────────────────────────────────
var sensorChart = new Chart(document.getElementById('chart-sensors').getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Temperature', data: [], borderColor: '#38bdf8', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 },
{ label: 'Pressure', data: [], borderColor: '#a78bfa', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 },
{ label: 'Vibration', data: [], borderColor: '#34d399', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
animation: false,
scales: {
x: {
display: true,
title: { display: true, text: 'Observation index', color: '#a1a1aa', font: { family: "'DM Sans', system-ui", size: 11 } },
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#71717a', maxTicksLimit: 12 }
},
y: {
display: true,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#71717a' }
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
afterBody: function (items) {
var idx = items[0] && items[0].dataIndex;
var p = buffer[idx];
if (!p) return '';
var lines = [];
if (p.alert) lines.push('Alert: YES');
if (p.label && p.label !== 'normal') lines.push('Ground truth: ' + p.label.replace('_', ' '));
return lines;
}
}
}
}
},
plugins: [makeDriftPlugin()]
});
// ── anomaly score chart ───────────────────────────────────────────────────────
var thresholdPlugin = {
id: 'thresholdLine',
afterDraw: function (chart) {
if (!chart.chartArea) return;
var yScale = chart.scales && chart.scales.y;
if (!yScale) return;
var y = yScale.getPixelForValue(THRESHOLD);
var ctx = chart.ctx;
var area = chart.chartArea;
ctx.save();
ctx.setLineDash([6, 3]);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(area.left, y);
ctx.lineTo(area.right, y);
ctx.stroke();
ctx.restore();
}
};
var scoreChart = new Chart(document.getElementById('chart-score').getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Anomaly score',
data: [],
borderColor: '#6366f1',
backgroundColor: 'rgba(99,102,241,0.08)',
fill: true,
tension: 0.2,
pointRadius: 0,
borderWidth: 1.5
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
animation: false,
scales: {
x: {
display: true,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#71717a', maxTicksLimit: 12 }
},
y: {
display: true,
min: 0,
max: 1,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#71717a', stepSize: 0.25 }
}
},
plugins: {
legend: { display: false }
}
},
plugins: [makeDriftPlugin(), thresholdPlugin]
});
// ── buffer + redraw ───────────────────────────────────────────────────────────
function pushPoint(msg) {
buffer.push({
index: msg.observation_index,
temperature: msg.temperature,
pressure: msg.pressure,
vibration: msg.vibration,
anomaly_score: msg.anomaly_score,
alert: msg.alert,
drift_event: msg.drift_event,
label: msg.label,
phase: msg.phase
});
if (buffer.length > MAX_POINTS) buffer.shift();
}
function redrawCharts() {
if (!buffer.length) return;
var labels = buffer.map(function (p) { return p.index; });
var tempData = buffer.map(function (p) { return p.temperature; });
var pressData = buffer.map(function (p) { return p.pressure; });
var vibeData = buffer.map(function (p) { return p.vibration; });
var scoreData = buffer.map(function (p) { return p.anomaly_score; });
var alertRadius = buffer.map(function (p) { return p.alert ? 5 : 0; });
var alertColor = buffer.map(function (p) { return p.alert ? '#ef4444' : 'transparent'; });
// Sensor chart
sensorChart.data.labels = labels;
sensorChart.data.datasets[0].data = tempData;
sensorChart.data.datasets[1].data = pressData;
sensorChart.data.datasets[2].data = vibeData;
[0, 1, 2].forEach(function (i) {
sensorChart.data.datasets[i].pointRadius = alertRadius;
sensorChart.data.datasets[i].pointBackgroundColor = alertColor;
sensorChart.data.datasets[i].pointBorderColor = alertColor;
});
sensorChart.update('none');
// Score chart
scoreChart.data.labels = labels;
scoreChart.data.datasets[0].data = scoreData;
scoreChart.data.datasets[0].pointRadius = alertRadius;
scoreChart.data.datasets[0].pointBackgroundColor = alertColor;
scoreChart.update('none');
}
// ── WebSocket ─────────────────────────────────────────────────────────────────
function connect() {
var ws = new WebSocket(WS_URL);
ws.onopen = function () { setConnected(true); };
ws.onclose = function () { setConnected(false); setTimeout(connect, 2000); };
ws.onerror = function () { setConnected(false); };
ws.onmessage = function (ev) {
try {
var msg = JSON.parse(ev.data);
updateCounters(msg);
pushPoint(msg);
redrawCharts();
} catch (e) { /* ignore parse errors */ }
};
}
// ── accordion ────────────────────────────────────────────────────────────────
(function () {
var section = document.getElementById('explainer');
var toggle = document.getElementById('explainer-toggle');
if (section && toggle) {
toggle.addEventListener('click', function () { section.classList.toggle('open'); });
}
})();
connect();
})();