sensorik/sensor-data-analyzer
0
1<!doctype html>2<html lang="de">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width,initial-scale=1">6<meta name="description" content="Sensor Data Analyzer: CSV-Sensordaten lokal im Browser analysieren, visualisieren und Anomalien erkennen.">7<title>Sensor Data Analyzer</title>8<style>9:root{10 --bg:#06131f;--panel:#0b2233;--panel2:#0f2c40;--line:#24485e;11 --text:#eef7fb;--muted:#9db5c2;--cyan:#35d7e7;--blue:#4b8cff;12 --green:#65d69b;--yellow:#ffd166;--red:#ff7e8a;--shadow:0 18px 60px rgba(0,0,0,.28);13}14*{box-sizing:border-box}15body{16 margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;17 background:18 radial-gradient(circle at 8% 0%,rgba(53,215,231,.13),transparent 29rem),19 radial-gradient(circle at 92% 8%,rgba(75,140,255,.13),transparent 30rem),20 var(--bg);21 color:var(--text);line-height:1.5;22}23.wrap{max-width:1200px;margin:auto;padding:24px 18px 60px}24.hero,.panel{25 border:1px solid var(--line);background:rgba(11,34,51,.95);border-radius:22px;26 box-shadow:var(--shadow)27}28.hero{padding:28px;margin-bottom:18px;background:linear-gradient(135deg,rgba(15,44,64,.96),rgba(6,19,31,.97))}29.badge{display:inline-flex;gap:8px;align-items:center;padding:7px 11px;border-radius:999px;30 background:rgba(53,215,231,.09);border:1px solid rgba(53,215,231,.25);color:#c3fbff;font-size:13px;font-weight:800}31h1{font-size:clamp(33px,5vw,58px);margin:15px 0 9px;letter-spacing:-.04em;line-height:1.02}32.hero p{color:var(--muted);max-width:820px;font-size:17px;margin:0}33.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}34button,.file-label{35 border:0;border-radius:12px;padding:11px 15px;font-weight:800;cursor:pointer;font-size:14px;36 transition:.15s transform,.15s opacity;display:inline-flex;align-items:center;gap:7px37}38button:hover,.file-label:hover{transform:translateY(-1px)}39.primary{background:linear-gradient(135deg,var(--cyan),var(--blue));color:#04111c}40.secondary{background:#16384e;color:var(--text);border:1px solid var(--line)}41.ghost{background:transparent;color:var(--muted);border:1px solid var(--line)}42input[type=file]{display:none}43.privacy{display:flex;gap:10px;align-items:flex-start;margin-top:18px;border:1px solid rgba(101,214,155,.25);44 background:rgba(101,214,155,.06);padding:13px 15px;border-radius:15px;font-size:13px}45.privacy strong{color:#baffd4}46.grid{display:grid;grid-template-columns:330px 1fr;gap:18px}47.panel{padding:19px}48.panel h2{margin:0 0 5px;font-size:19px}49.sub{font-size:13px;color:var(--muted);margin-bottom:15px}50.control{margin-bottom:14px}51.control label{display:block;font-size:12px;font-weight:800;color:#cfe0e8;margin-bottom:6px}52select,input[type=range],input[type=number]{53 width:100%;border:1px solid var(--line);border-radius:10px;background:#071a28;color:var(--text);padding:10px54}55input[type=range]{padding:0}56.range-row{display:grid;grid-template-columns:1fr 52px;gap:9px;align-items:center}57.range-value{background:#071a28;border:1px solid var(--line);border-radius:9px;padding:7px;text-align:center;font-size:12px}58.fileinfo{padding:11px;border:1px solid var(--line);background:#071a28;border-radius:12px;color:var(--muted);font-size:12px;margin:12px 0}59.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:16px}60.metric{border:1px solid var(--line);background:#081d2b;border-radius:14px;padding:13px;min-width:0}61.metric span{display:block;color:var(--muted);font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}62.metric strong{font-size:20px;letter-spacing:-.03em}63.chart-wrap{position:relative;height:420px;border:1px solid var(--line);border-radius:16px;background:#061925;overflow:hidden}64canvas{width:100%;height:100%;display:block}65.chart-note{font-size:12px;color:var(--muted);margin-top:9px}66.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--muted);margin-top:8px}67.dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:5px}68.table-wrap{overflow:auto;max-height:330px;border:1px solid var(--line);border-radius:13px;margin-top:12px}69table{width:100%;border-collapse:collapse;font-size:12px}70th,td{padding:9px 10px;border-bottom:1px solid rgba(36,72,94,.7);text-align:left;white-space:nowrap}71th{position:sticky;top:0;background:#102c40;z-index:1}72.empty{padding:26px;color:var(--muted);text-align:center}73.notice{margin-top:18px;padding:15px 16px;border-radius:15px;border:1px solid rgba(255,209,102,.27);74 background:rgba(255,209,102,.06);font-size:13px;color:#dbe7ec}75footer{text-align:center;color:#6f8998;font-size:12px;margin-top:24px}76.status-good{color:var(--green)} .status-warn{color:var(--yellow)}77@media(max-width:900px){78 .grid{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(3,1fr)}79}80@media(max-width:560px){81 .wrap{padding:12px 10px 40px}.hero,.panel{padding:16px;border-radius:16px}82 .metrics{grid-template-columns:repeat(2,1fr)}.chart-wrap{height:330px}83}84</style>85</head>86<body>87<div class="wrap">88<section class="hero">89 <span class="badge">📡 Sensorik · Local-first Sensor Analysis</span>90 <h1>Sensor Data Analyzer</h1>91 <p>CSV-Messdaten öffnen, Sensorverläufe visualisieren, statistisch zusammenfassen und auffällige Messwerte mit robuster MAD-Analyse erkennen.</p>92 <div class="actions">93 <label class="file-label primary" for="fileInput">📂 CSV öffnen</label>94 <input id="fileInput" type="file" accept=".csv,text/csv">95 <button class="secondary" onclick="loadDemo()">Beispieldaten laden</button>96 <button class="ghost" onclick="downloadAnomalies()">Anomalien exportieren</button>97 </div>98 <div class="privacy">99 <div>🔒</div>100 <div><strong>Lokale Verarbeitung:</strong> CSV-Dateien und Messwerte bleiben in deinem Browser. Dieser Static Space überträgt keine hochgeladenen Sensordaten an einen Server.</div>101 </div>102</section>103 104<div class="grid">105 <aside class="panel">106 <h2>Analyse konfigurieren</h2>107 <div class="sub">Messspalte und Erkennung anpassen</div>108 109 <div class="fileinfo" id="fileInfo">Noch keine Daten geladen.</div>110 111 <div class="control">112 <label for="xColumn">Zeit / X-Achse</label>113 <select id="xColumn"></select>114 </div>115 116 <div class="control">117 <label for="yColumn">Messwert</label>118 <select id="yColumn"></select>119 </div>120 121 <div class="control">122 <label for="threshold">Anomalie-Empfindlichkeit (MAD)</label>123 <div class="range-row">124 <input id="threshold" type="range" min="1.5" max="6" step=".1" value="3.5">125 <div class="range-value" id="thresholdValue">3.5</div>126 </div>127 </div>128 129 <div class="control">130 <label for="smooth">Gleitender Mittelwert</label>131 <div class="range-row">132 <input id="smooth" type="range" min="1" max="30" step="1" value="5">133 <div class="range-value" id="smoothValue">5</div>134 </div>135 </div>136 137 <div class="control">138 <label for="limit">Max. Punkte im Diagramm</label>139 <select id="limit">140 <option value="500">500</option>141 <option value="1000" selected>1.000</option>142 <option value="2500">2.500</option>143 <option value="5000">5.000</option>144 </select>145 </div>146 147 <button class="secondary" style="width:100%;justify-content:center" onclick="analyze()">Analyse aktualisieren</button>148 </aside>149 150 <main>151 <section class="panel">152 <h2>Messreihe</h2>153 <div class="sub">Statistik und Verlauf der ausgewählten Sensorgröße</div>154 <div class="metrics" id="metrics">155 <div class="metric"><span>Messwerte</span><strong>–</strong></div>156 <div class="metric"><span>Mittelwert</span><strong>–</strong></div>157 <div class="metric"><span>Median</span><strong>–</strong></div>158 <div class="metric"><span>Minimum</span><strong>–</strong></div>159 <div class="metric"><span>Maximum</span><strong>–</strong></div>160 <div class="metric"><span>Anomalien</span><strong>–</strong></div>161 </div>162 163 <div class="chart-wrap"><canvas id="chart"></canvas></div>164 <div class="legend">165 <span><i class="dot" style="background:#35d7e7"></i>Messwert</span>166 <span><i class="dot" style="background:#ffd166"></i>Gleitender Mittelwert</span>167 <span><i class="dot" style="background:#ff7e8a"></i>Erkannte Anomalie</span>168 </div>169 <div class="chart-note" id="chartNote">Lade eine CSV-Datei oder Beispieldaten.</div>170 </section>171 172 <section class="panel" style="margin-top:18px">173 <h2>Auffällige Messwerte</h2>174 <div class="sub">Robuste MAD-Erkennung – fachliche Interpretation bleibt erforderlich</div>175 <div class="table-wrap" id="anomalyTable"><div class="empty">Noch keine Analyse vorhanden.</div></div>176 </section>177 </main>178</div>179 180<div class="notice">181<strong>Hinweis:</strong> Eine statistische Anomalie ist nicht automatisch ein Sensorfehler, Defekt oder Sicherheitsereignis. Prozesszustand, Kalibrierung, physikalische Grenzwerte und Messbedingungen müssen berücksichtigt werden. Das Tool ist nicht für sicherheitskritische Entscheidungen zertifiziert.182</div>183<footer>Sensorik · Independent Hugging Face Community Project</footer>184</div>185 186<script>187let rows=[], headers=[], anomalies=[], currentValues=[], currentLabels=[];188 189const $=id=>document.getElementById(id);190$("threshold").addEventListener("input",()=>{$("thresholdValue").textContent=$("threshold").value; analyze();});191$("smooth").addEventListener("input",()=>{$("smoothValue").textContent=$("smooth").value; analyze();});192$("xColumn").addEventListener("change",analyze);193$("yColumn").addEventListener("change",analyze);194$("limit").addEventListener("change",analyze);195$("fileInput").addEventListener("change",handleFile);196 197function detectDelimiter(line){198 const options=[",",";","\t","|"];199 return options.map(d=>[d,(line.split(d).length-1)]).sort((a,b)=>b[1]-a[1])[0][0];200}201function parseCSV(text){202 const lines=text.replace(/\r/g,"").split("\n").filter(x=>x.trim().length);203 if(lines.length<2) throw new Error("CSV enthält zu wenige Zeilen.");204 const delim=detectDelimiter(lines[0]);205 const parseLine=line=>{206 let out=[],cur="",quote=false;207 for(let i=0;i<line.length;i++){208 const c=line[i];209 if(c==='"'){210 if(quote && line[i+1]==='"'){cur+='"';i++}211 else quote=!quote;212 } else if(c===delim && !quote){out.push(cur.trim());cur=""}213 else cur+=c;214 }215 out.push(cur.trim()); return out;216 };217 const h=parseLine(lines[0]).map((x,i)=>x||("column_"+(i+1)));218 const data=lines.slice(1).map(line=>{219 const vals=parseLine(line),o={};220 h.forEach((k,i)=>o[k]=vals[i]??"");221 return o;222 });223 return {headers:h,rows:data,delimiter:delim};224}225function toNum(v){226 if(v===null||v===undefined||v==="") return NaN;227 let s=String(v).trim().replace(/\s/g,"");228 if(/^[-+]?\d{1,3}(\.\d{3})*,\d+$/.test(s)) s=s.replace(/\./g,"").replace(",",".");229 else if(/^[-+]?\d+,\d+$/.test(s)) s=s.replace(",",".");230 const n=Number(s);231 return Number.isFinite(n)?n:NaN;232}233function numericShare(col){234 if(!rows.length)return 0;235 const vals=rows.slice(0,Math.min(300,rows.length));236 return vals.filter(r=>Number.isFinite(toNum(r[col]))).length/vals.length;237}238function setupSelectors(){239 $("xColumn").innerHTML=`<option value="__index__">Zeilenindex</option>`+headers.map(h=>`<option value="${esc(h)}">${esc(h)}</option>`).join("");240 const nums=headers.filter(h=>numericShare(h)>.6);241 $("yColumn").innerHTML=nums.map(h=>`<option value="${esc(h)}">${esc(h)}</option>`).join("");242 if(nums.length) $("yColumn").value=nums[0];243 const likelyTime=headers.find(h=>/time|date|datum|zeit|timestamp/i.test(h));244 if(likelyTime) $("xColumn").value=likelyTime;245}246function esc(s){return String(s).replace(/[&<>"']/g,m=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[m]))}247 248async function handleFile(e){249 const f=e.target.files[0]; if(!f)return;250 try{251 const text=await f.text();252 const p=parseCSV(text);253 headers=p.headers; rows=p.rows;254 $("fileInfo").innerHTML=`<strong>${esc(f.name)}</strong><br>${rows.length.toLocaleString("de-DE")} Zeilen · ${headers.length} Spalten · Trennzeichen: <code>${p.delimiter===" "?"Tab":esc(p.delimiter)}</code>`;255 setupSelectors(); analyze();256 }catch(err){257 $("fileInfo").textContent="Fehler: "+err.message;258 }259}260 261function loadDemo(){262 headers=["timestamp","temperature","vibration","pressure"];263 rows=[];264 const start=new Date("2026-09-16T08:00:00");265 for(let i=0;i<360;i++){266 const t=new Date(start.getTime()+i*60000);267 let temp=68 + Math.sin(i/25)*1.8 + Math.sin(i/7)*.25;268 let vib=2.1 + Math.sin(i/12)*.18 + Math.cos(i/5)*.08;269 let pressure=5.2 + Math.sin(i/50)*.12;270 if([91,92,93].includes(i)) temp+=12;271 if([218,219].includes(i)) vib+=1.7;272 if(i===305) pressure-=1.1;273 rows.push({274 timestamp:t.toISOString().slice(0,16).replace("T"," "),275 temperature:temp.toFixed(2),276 vibration:vib.toFixed(3),277 pressure:pressure.toFixed(3)278 });279 }280 $("fileInfo").innerHTML="<strong>Beispieldaten</strong><br>360 Zeilen · 3 Sensorsignale · künstliche Anomalien";281 setupSelectors(); $("xColumn").value="timestamp"; $("yColumn").value="temperature"; analyze();282}283 284function median(arr){285 if(!arr.length)return NaN;286 const a=[...arr].sort((x,y)=>x-y),m=Math.floor(a.length/2);287 return a.length%2?a[m]:(a[m-1]+a[m])/2;288}289function stats(v){290 if(!v.length)return {};291 const mean=v.reduce((a,b)=>a+b,0)/v.length;292 const med=median(v);293 const sd=Math.sqrt(v.reduce((s,x)=>s+(x-mean)**2,0)/Math.max(1,v.length-1));294 return {n:v.length,mean,median:med,min:Math.min(...v),max:Math.max(...v),sd};295}296function madAnomalies(vals,threshold){297 const med=median(vals);298 const abs=vals.map(x=>Math.abs(x-med));299 const mad=median(abs);300 if(!Number.isFinite(mad)||mad===0) return vals.map(()=>({score:0,is:false}));301 return vals.map(x=>{302 const score=.6745*(x-med)/mad;303 return {score,is:Math.abs(score)>threshold};304 });305}306function movingAverage(vals,w){307 if(w<=1)return [...vals];308 const out=[],q=[];let sum=0;309 for(const v of vals){310 q.push(v);sum+=v;311 if(q.length>w)sum-=q.shift();312 out.push(sum/q.length);313 }314 return out;315}316function fmt(n){317 if(!Number.isFinite(n))return "–";318 return new Intl.NumberFormat("de-DE",{maximumFractionDigits:3}).format(n);319}320 321function analyze(){322 if(!rows.length || !$("yColumn").value){renderEmpty();return}323 const xcol=$("xColumn").value,ycol=$("yColumn").value,lim=parseInt($("limit").value);324 let temp=[];325 rows.forEach((r,i)=>{326 const y=toNum(r[ycol]);327 if(Number.isFinite(y)) temp.push({label:xcol==="__index__"?(i+1):r[xcol],value:y,row:i});328 });329 if(!temp.length){renderEmpty();return}330 const step=Math.max(1,Math.ceil(temp.length/lim));331 const sampled=temp.filter((_,i)=>i%step===0 || i===temp.length-1);332 currentValues=sampled.map(x=>x.value);333 currentLabels=sampled.map(x=>x.label);334 const s=stats(currentValues), threshold=parseFloat($("threshold").value);335 const flags=madAnomalies(currentValues,threshold);336 const smooth=movingAverage(currentValues,parseInt($("smooth").value));337 anomalies=sampled.map((x,i)=>({...x,score:flags[i].score,is:flags[i].is})).filter(x=>x.is);338 339 $("metrics").innerHTML=[340 ["Messwerte",s.n.toLocaleString("de-DE")],341 ["Mittelwert",fmt(s.mean)],342 ["Median",fmt(s.median)],343 ["Minimum",fmt(s.min)],344 ["Maximum",fmt(s.max)],345 ["Anomalien",anomalies.length.toLocaleString("de-DE")]346 ].map(x=>`<div class="metric"><span>${x[0]}</span><strong>${x[1]}</strong></div>`).join("");347 348 drawChart(currentValues,smooth,flags);349 renderAnomalies();350 $("chartNote").innerHTML=`Spalte <strong>${esc(ycol)}</strong> · ${temp.length.toLocaleString("de-DE")} gültige Werte`+351 (step>1?` · Diagramm auf ca. ${sampled.length.toLocaleString("de-DE")} Punkte reduziert`:"")+352 ` · MAD-Schwelle ${threshold}`;353}354 355function renderEmpty(){356 $("chartNote").textContent="Lade eine CSV-Datei oder Beispieldaten.";357 $("anomalyTable").innerHTML='<div class="empty">Noch keine Analyse vorhanden.</div>';358}359 360function renderAnomalies(){361 if(!anomalies.length){362 $("anomalyTable").innerHTML='<div class="empty status-good">Keine Auffälligkeiten mit der aktuellen MAD-Schwelle erkannt.</div>';363 return;364 }365 $("anomalyTable").innerHTML=`<table><thead><tr><th>#</th><th>X / Zeit</th><th>Messwert</th><th>Robuster Score</th></tr></thead><tbody>`+366 anomalies.slice(0,500).map((a,i)=>`<tr><td>${i+1}</td><td>${esc(a.label)}</td><td>${fmt(a.value)}</td><td>${fmt(a.score)}</td></tr>`).join("")+367 `</tbody></table>`+(anomalies.length>500?`<div class="empty">${anomalies.length-500} weitere Anomalien nicht angezeigt.</div>`:"");368}369 370function drawChart(vals,smooth,flags){371 const c=$("chart"),rect=c.parentElement.getBoundingClientRect(),dpr=window.devicePixelRatio||1;372 c.width=Math.max(300,Math.floor(rect.width*dpr)); c.height=Math.max(260,Math.floor(rect.height*dpr));373 const ctx=c.getContext("2d");ctx.scale(dpr,dpr);374 const W=rect.width,H=rect.height,p={l:54,r:18,t:18,b:34};375 ctx.clearRect(0,0,W,H);376 const min=Math.min(...vals),max=Math.max(...vals),pad=(max-min||1)*.08,ymin=min-pad,ymax=max+pad;377 const X=i=>p.l+(W-p.l-p.r)*(i/Math.max(1,vals.length-1));378 const Y=v=>p.t+(H-p.t-p.b)*(1-(v-ymin)/(ymax-ymin));379 ctx.strokeStyle="rgba(157,181,194,.16)";ctx.lineWidth=1;ctx.font="11px system-ui";ctx.fillStyle="#8ca6b5";380 for(let j=0;j<=4;j++){381 const y=p.t+(H-p.t-p.b)*j/4;382 ctx.beginPath();ctx.moveTo(p.l,y);ctx.lineTo(W-p.r,y);ctx.stroke();383 const val=ymax-(ymax-ymin)*j/4;ctx.fillText(fmt(val),6,y+4);384 }385 function line(data,color,width){386 ctx.strokeStyle=color;ctx.lineWidth=width;ctx.beginPath();387 data.forEach((v,i)=>{const x=X(i),y=Y(v);i?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.stroke();388 }389 line(vals,"#35d7e7",1.7); line(smooth,"#ffd166",1.5);390 ctx.fillStyle="#ff7e8a";391 flags.forEach((f,i)=>{if(f.is){ctx.beginPath();ctx.arc(X(i),Y(vals[i]),4.3,0,Math.PI*2);ctx.fill()}});392 ctx.fillStyle="#8ca6b5";393 const ticks=Math.min(5,vals.length);394 for(let j=0;j<ticks;j++){395 const i=Math.round((vals.length-1)*j/Math.max(1,ticks-1)),txt=String(currentLabels[i]??i+1);396 const short=txt.length>18?txt.slice(0,17)+"…":txt;397 ctx.fillText(short,X(i)-Math.min(45,short.length*3),H-10);398 }399}400 401function downloadAnomalies(){402 if(!anomalies.length){alert("Keine Anomalien zum Exportieren.");return}403 const x=$("xColumn").value,y=$("yColumn").value;404 let csv=`${x==="__index__"?"index":x},${y},robust_score\n`;405 anomalies.forEach(a=>csv+=`"${String(a.label).replace(/"/g,'""')}",${a.value},${a.score}\n`);406 const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});407 const url=URL.createObjectURL(blob),a=document.createElement("a");408 a.href=url;a.download="sensorik-anomalien.csv";a.click();URL.revokeObjectURL(url);409}410window.addEventListener("resize",()=>{if(currentValues.length) analyze()});411</script>412</body>413</html>414 