Team Ai
Apppublic

sensorik/sensor-data-analyzer

sourceHugging Facemitupdated 24d agoView on Hugging Face
0likes
index.html414 linesDownload Raw Back to root
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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[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