Spaces:
Sleeping
Sleeping
| (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(); | |
| })(); | |