sensor-data-analyzer / index.html
Edeka's picture
Upload 3 files
43c79d7 verified
Raw
History Blame Contribute Delete
19 kB
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Sensor Data Analyzer: CSV-Sensordaten lokal im Browser analysieren, visualisieren und Anomalien erkennen.">
<title>Sensor Data Analyzer</title>
<style>
:root{
--bg:#06131f;--panel:#0b2233;--panel2:#0f2c40;--line:#24485e;
--text:#eef7fb;--muted:#9db5c2;--cyan:#35d7e7;--blue:#4b8cff;
--green:#65d69b;--yellow:#ffd166;--red:#ff7e8a;--shadow:0 18px 60px rgba(0,0,0,.28);
}
*{box-sizing:border-box}
body{
margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
background:
radial-gradient(circle at 8% 0%,rgba(53,215,231,.13),transparent 29rem),
radial-gradient(circle at 92% 8%,rgba(75,140,255,.13),transparent 30rem),
var(--bg);
color:var(--text);line-height:1.5;
}
.wrap{max-width:1200px;margin:auto;padding:24px 18px 60px}
.hero,.panel{
border:1px solid var(--line);background:rgba(11,34,51,.95);border-radius:22px;
box-shadow:var(--shadow)
}
.hero{padding:28px;margin-bottom:18px;background:linear-gradient(135deg,rgba(15,44,64,.96),rgba(6,19,31,.97))}
.badge{display:inline-flex;gap:8px;align-items:center;padding:7px 11px;border-radius:999px;
background:rgba(53,215,231,.09);border:1px solid rgba(53,215,231,.25);color:#c3fbff;font-size:13px;font-weight:800}
h1{font-size:clamp(33px,5vw,58px);margin:15px 0 9px;letter-spacing:-.04em;line-height:1.02}
.hero p{color:var(--muted);max-width:820px;font-size:17px;margin:0}
.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}
button,.file-label{
border:0;border-radius:12px;padding:11px 15px;font-weight:800;cursor:pointer;font-size:14px;
transition:.15s transform,.15s opacity;display:inline-flex;align-items:center;gap:7px
}
button:hover,.file-label:hover{transform:translateY(-1px)}
.primary{background:linear-gradient(135deg,var(--cyan),var(--blue));color:#04111c}
.secondary{background:#16384e;color:var(--text);border:1px solid var(--line)}
.ghost{background:transparent;color:var(--muted);border:1px solid var(--line)}
input[type=file]{display:none}
.privacy{display:flex;gap:10px;align-items:flex-start;margin-top:18px;border:1px solid rgba(101,214,155,.25);
background:rgba(101,214,155,.06);padding:13px 15px;border-radius:15px;font-size:13px}
.privacy strong{color:#baffd4}
.grid{display:grid;grid-template-columns:330px 1fr;gap:18px}
.panel{padding:19px}
.panel h2{margin:0 0 5px;font-size:19px}
.sub{font-size:13px;color:var(--muted);margin-bottom:15px}
.control{margin-bottom:14px}
.control label{display:block;font-size:12px;font-weight:800;color:#cfe0e8;margin-bottom:6px}
select,input[type=range],input[type=number]{
width:100%;border:1px solid var(--line);border-radius:10px;background:#071a28;color:var(--text);padding:10px
}
input[type=range]{padding:0}
.range-row{display:grid;grid-template-columns:1fr 52px;gap:9px;align-items:center}
.range-value{background:#071a28;border:1px solid var(--line);border-radius:9px;padding:7px;text-align:center;font-size:12px}
.fileinfo{padding:11px;border:1px solid var(--line);background:#071a28;border-radius:12px;color:var(--muted);font-size:12px;margin:12px 0}
.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:16px}
.metric{border:1px solid var(--line);background:#081d2b;border-radius:14px;padding:13px;min-width:0}
.metric span{display:block;color:var(--muted);font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.metric strong{font-size:20px;letter-spacing:-.03em}
.chart-wrap{position:relative;height:420px;border:1px solid var(--line);border-radius:16px;background:#061925;overflow:hidden}
canvas{width:100%;height:100%;display:block}
.chart-note{font-size:12px;color:var(--muted);margin-top:9px}
.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--muted);margin-top:8px}
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:5px}
.table-wrap{overflow:auto;max-height:330px;border:1px solid var(--line);border-radius:13px;margin-top:12px}
table{width:100%;border-collapse:collapse;font-size:12px}
th,td{padding:9px 10px;border-bottom:1px solid rgba(36,72,94,.7);text-align:left;white-space:nowrap}
th{position:sticky;top:0;background:#102c40;z-index:1}
.empty{padding:26px;color:var(--muted);text-align:center}
.notice{margin-top:18px;padding:15px 16px;border-radius:15px;border:1px solid rgba(255,209,102,.27);
background:rgba(255,209,102,.06);font-size:13px;color:#dbe7ec}
footer{text-align:center;color:#6f8998;font-size:12px;margin-top:24px}
.status-good{color:var(--green)} .status-warn{color:var(--yellow)}
@media(max-width:900px){
.grid{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(3,1fr)}
}
@media(max-width:560px){
.wrap{padding:12px 10px 40px}.hero,.panel{padding:16px;border-radius:16px}
.metrics{grid-template-columns:repeat(2,1fr)}.chart-wrap{height:330px}
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<span class="badge">📡 Sensorik · Local-first Sensor Analysis</span>
<h1>Sensor Data Analyzer</h1>
<p>CSV-Messdaten öffnen, Sensorverläufe visualisieren, statistisch zusammenfassen und auffällige Messwerte mit robuster MAD-Analyse erkennen.</p>
<div class="actions">
<label class="file-label primary" for="fileInput">📂 CSV öffnen</label>
<input id="fileInput" type="file" accept=".csv,text/csv">
<button class="secondary" onclick="loadDemo()">Beispieldaten laden</button>
<button class="ghost" onclick="downloadAnomalies()">Anomalien exportieren</button>
</div>
<div class="privacy">
<div>🔒</div>
<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>
</div>
</section>
<div class="grid">
<aside class="panel">
<h2>Analyse konfigurieren</h2>
<div class="sub">Messspalte und Erkennung anpassen</div>
<div class="fileinfo" id="fileInfo">Noch keine Daten geladen.</div>
<div class="control">
<label for="xColumn">Zeit / X-Achse</label>
<select id="xColumn"></select>
</div>
<div class="control">
<label for="yColumn">Messwert</label>
<select id="yColumn"></select>
</div>
<div class="control">
<label for="threshold">Anomalie-Empfindlichkeit (MAD)</label>
<div class="range-row">
<input id="threshold" type="range" min="1.5" max="6" step=".1" value="3.5">
<div class="range-value" id="thresholdValue">3.5</div>
</div>
</div>
<div class="control">
<label for="smooth">Gleitender Mittelwert</label>
<div class="range-row">
<input id="smooth" type="range" min="1" max="30" step="1" value="5">
<div class="range-value" id="smoothValue">5</div>
</div>
</div>
<div class="control">
<label for="limit">Max. Punkte im Diagramm</label>
<select id="limit">
<option value="500">500</option>
<option value="1000" selected>1.000</option>
<option value="2500">2.500</option>
<option value="5000">5.000</option>
</select>
</div>
<button class="secondary" style="width:100%;justify-content:center" onclick="analyze()">Analyse aktualisieren</button>
</aside>
<main>
<section class="panel">
<h2>Messreihe</h2>
<div class="sub">Statistik und Verlauf der ausgewählten Sensorgröße</div>
<div class="metrics" id="metrics">
<div class="metric"><span>Messwerte</span><strong>–</strong></div>
<div class="metric"><span>Mittelwert</span><strong>–</strong></div>
<div class="metric"><span>Median</span><strong>–</strong></div>
<div class="metric"><span>Minimum</span><strong>–</strong></div>
<div class="metric"><span>Maximum</span><strong>–</strong></div>
<div class="metric"><span>Anomalien</span><strong>–</strong></div>
</div>
<div class="chart-wrap"><canvas id="chart"></canvas></div>
<div class="legend">
<span><i class="dot" style="background:#35d7e7"></i>Messwert</span>
<span><i class="dot" style="background:#ffd166"></i>Gleitender Mittelwert</span>
<span><i class="dot" style="background:#ff7e8a"></i>Erkannte Anomalie</span>
</div>
<div class="chart-note" id="chartNote">Lade eine CSV-Datei oder Beispieldaten.</div>
</section>
<section class="panel" style="margin-top:18px">
<h2>Auffällige Messwerte</h2>
<div class="sub">Robuste MAD-Erkennung – fachliche Interpretation bleibt erforderlich</div>
<div class="table-wrap" id="anomalyTable"><div class="empty">Noch keine Analyse vorhanden.</div></div>
</section>
</main>
</div>
<div class="notice">
<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.
</div>
<footer>Sensorik · Independent Hugging Face Community Project</footer>
</div>
<script>
let rows=[], headers=[], anomalies=[], currentValues=[], currentLabels=[];
const $=id=>document.getElementById(id);
$("threshold").addEventListener("input",()=>{$("thresholdValue").textContent=$("threshold").value; analyze();});
$("smooth").addEventListener("input",()=>{$("smoothValue").textContent=$("smooth").value; analyze();});
$("xColumn").addEventListener("change",analyze);
$("yColumn").addEventListener("change",analyze);
$("limit").addEventListener("change",analyze);
$("fileInput").addEventListener("change",handleFile);
function detectDelimiter(line){
const options=[",",";","\t","|"];
return options.map(d=>[d,(line.split(d).length-1)]).sort((a,b)=>b[1]-a[1])[0][0];
}
function parseCSV(text){
const lines=text.replace(/\r/g,"").split("\n").filter(x=>x.trim().length);
if(lines.length<2) throw new Error("CSV enthält zu wenige Zeilen.");
const delim=detectDelimiter(lines[0]);
const parseLine=line=>{
let out=[],cur="",quote=false;
for(let i=0;i<line.length;i++){
const c=line[i];
if(c==='"'){
if(quote && line[i+1]==='"'){cur+='"';i++}
else quote=!quote;
} else if(c===delim && !quote){out.push(cur.trim());cur=""}
else cur+=c;
}
out.push(cur.trim()); return out;
};
const h=parseLine(lines[0]).map((x,i)=>x||("column_"+(i+1)));
const data=lines.slice(1).map(line=>{
const vals=parseLine(line),o={};
h.forEach((k,i)=>o[k]=vals[i]??"");
return o;
});
return {headers:h,rows:data,delimiter:delim};
}
function toNum(v){
if(v===null||v===undefined||v==="") return NaN;
let s=String(v).trim().replace(/\s/g,"");
if(/^[-+]?\d{1,3}(\.\d{3})*,\d+$/.test(s)) s=s.replace(/\./g,"").replace(",",".");
else if(/^[-+]?\d+,\d+$/.test(s)) s=s.replace(",",".");
const n=Number(s);
return Number.isFinite(n)?n:NaN;
}
function numericShare(col){
if(!rows.length)return 0;
const vals=rows.slice(0,Math.min(300,rows.length));
return vals.filter(r=>Number.isFinite(toNum(r[col]))).length/vals.length;
}
function setupSelectors(){
$("xColumn").innerHTML=`<option value="__index__">Zeilenindex</option>`+headers.map(h=>`<option value="${esc(h)}">${esc(h)}</option>`).join("");
const nums=headers.filter(h=>numericShare(h)>.6);
$("yColumn").innerHTML=nums.map(h=>`<option value="${esc(h)}">${esc(h)}</option>`).join("");
if(nums.length) $("yColumn").value=nums[0];
const likelyTime=headers.find(h=>/time|date|datum|zeit|timestamp/i.test(h));
if(likelyTime) $("xColumn").value=likelyTime;
}
function esc(s){return String(s).replace(/[&<>"']/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[m]))}
async function handleFile(e){
const f=e.target.files[0]; if(!f)return;
try{
const text=await f.text();
const p=parseCSV(text);
headers=p.headers; rows=p.rows;
$("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>`;
setupSelectors(); analyze();
}catch(err){
$("fileInfo").textContent="Fehler: "+err.message;
}
}
function loadDemo(){
headers=["timestamp","temperature","vibration","pressure"];
rows=[];
const start=new Date("2026-09-16T08:00:00");
for(let i=0;i<360;i++){
const t=new Date(start.getTime()+i*60000);
let temp=68 + Math.sin(i/25)*1.8 + Math.sin(i/7)*.25;
let vib=2.1 + Math.sin(i/12)*.18 + Math.cos(i/5)*.08;
let pressure=5.2 + Math.sin(i/50)*.12;
if([91,92,93].includes(i)) temp+=12;
if([218,219].includes(i)) vib+=1.7;
if(i===305) pressure-=1.1;
rows.push({
timestamp:t.toISOString().slice(0,16).replace("T"," "),
temperature:temp.toFixed(2),
vibration:vib.toFixed(3),
pressure:pressure.toFixed(3)
});
}
$("fileInfo").innerHTML="<strong>Beispieldaten</strong><br>360 Zeilen · 3 Sensorsignale · künstliche Anomalien";
setupSelectors(); $("xColumn").value="timestamp"; $("yColumn").value="temperature"; analyze();
}
function median(arr){
if(!arr.length)return NaN;
const a=[...arr].sort((x,y)=>x-y),m=Math.floor(a.length/2);
return a.length%2?a[m]:(a[m-1]+a[m])/2;
}
function stats(v){
if(!v.length)return {};
const mean=v.reduce((a,b)=>a+b,0)/v.length;
const med=median(v);
const sd=Math.sqrt(v.reduce((s,x)=>s+(x-mean)**2,0)/Math.max(1,v.length-1));
return {n:v.length,mean,median:med,min:Math.min(...v),max:Math.max(...v),sd};
}
function madAnomalies(vals,threshold){
const med=median(vals);
const abs=vals.map(x=>Math.abs(x-med));
const mad=median(abs);
if(!Number.isFinite(mad)||mad===0) return vals.map(()=>({score:0,is:false}));
return vals.map(x=>{
const score=.6745*(x-med)/mad;
return {score,is:Math.abs(score)>threshold};
});
}
function movingAverage(vals,w){
if(w<=1)return [...vals];
const out=[],q=[];let sum=0;
for(const v of vals){
q.push(v);sum+=v;
if(q.length>w)sum-=q.shift();
out.push(sum/q.length);
}
return out;
}
function fmt(n){
if(!Number.isFinite(n))return "–";
return new Intl.NumberFormat("de-DE",{maximumFractionDigits:3}).format(n);
}
function analyze(){
if(!rows.length || !$("yColumn").value){renderEmpty();return}
const xcol=$("xColumn").value,ycol=$("yColumn").value,lim=parseInt($("limit").value);
let temp=[];
rows.forEach((r,i)=>{
const y=toNum(r[ycol]);
if(Number.isFinite(y)) temp.push({label:xcol==="__index__"?(i+1):r[xcol],value:y,row:i});
});
if(!temp.length){renderEmpty();return}
const step=Math.max(1,Math.ceil(temp.length/lim));
const sampled=temp.filter((_,i)=>i%step===0 || i===temp.length-1);
currentValues=sampled.map(x=>x.value);
currentLabels=sampled.map(x=>x.label);
const s=stats(currentValues), threshold=parseFloat($("threshold").value);
const flags=madAnomalies(currentValues,threshold);
const smooth=movingAverage(currentValues,parseInt($("smooth").value));
anomalies=sampled.map((x,i)=>({...x,score:flags[i].score,is:flags[i].is})).filter(x=>x.is);
$("metrics").innerHTML=[
["Messwerte",s.n.toLocaleString("de-DE")],
["Mittelwert",fmt(s.mean)],
["Median",fmt(s.median)],
["Minimum",fmt(s.min)],
["Maximum",fmt(s.max)],
["Anomalien",anomalies.length.toLocaleString("de-DE")]
].map(x=>`<div class="metric"><span>${x[0]}</span><strong>${x[1]}</strong></div>`).join("");
drawChart(currentValues,smooth,flags);
renderAnomalies();
$("chartNote").innerHTML=`Spalte <strong>${esc(ycol)}</strong> · ${temp.length.toLocaleString("de-DE")} gültige Werte`+
(step>1?` · Diagramm auf ca. ${sampled.length.toLocaleString("de-DE")} Punkte reduziert`:"")+
` · MAD-Schwelle ${threshold}`;
}
function renderEmpty(){
$("chartNote").textContent="Lade eine CSV-Datei oder Beispieldaten.";
$("anomalyTable").innerHTML='<div class="empty">Noch keine Analyse vorhanden.</div>';
}
function renderAnomalies(){
if(!anomalies.length){
$("anomalyTable").innerHTML='<div class="empty status-good">Keine Auffälligkeiten mit der aktuellen MAD-Schwelle erkannt.</div>';
return;
}
$("anomalyTable").innerHTML=`<table><thead><tr><th>#</th><th>X / Zeit</th><th>Messwert</th><th>Robuster Score</th></tr></thead><tbody>`+
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("")+
`</tbody></table>`+(anomalies.length>500?`<div class="empty">${anomalies.length-500} weitere Anomalien nicht angezeigt.</div>`:"");
}
function drawChart(vals,smooth,flags){
const c=$("chart"),rect=c.parentElement.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
c.width=Math.max(300,Math.floor(rect.width*dpr)); c.height=Math.max(260,Math.floor(rect.height*dpr));
const ctx=c.getContext("2d");ctx.scale(dpr,dpr);
const W=rect.width,H=rect.height,p={l:54,r:18,t:18,b:34};
ctx.clearRect(0,0,W,H);
const min=Math.min(...vals),max=Math.max(...vals),pad=(max-min||1)*.08,ymin=min-pad,ymax=max+pad;
const X=i=>p.l+(W-p.l-p.r)*(i/Math.max(1,vals.length-1));
const Y=v=>p.t+(H-p.t-p.b)*(1-(v-ymin)/(ymax-ymin));
ctx.strokeStyle="rgba(157,181,194,.16)";ctx.lineWidth=1;ctx.font="11px system-ui";ctx.fillStyle="#8ca6b5";
for(let j=0;j<=4;j++){
const y=p.t+(H-p.t-p.b)*j/4;
ctx.beginPath();ctx.moveTo(p.l,y);ctx.lineTo(W-p.r,y);ctx.stroke();
const val=ymax-(ymax-ymin)*j/4;ctx.fillText(fmt(val),6,y+4);
}
function line(data,color,width){
ctx.strokeStyle=color;ctx.lineWidth=width;ctx.beginPath();
data.forEach((v,i)=>{const x=X(i),y=Y(v);i?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.stroke();
}
line(vals,"#35d7e7",1.7); line(smooth,"#ffd166",1.5);
ctx.fillStyle="#ff7e8a";
flags.forEach((f,i)=>{if(f.is){ctx.beginPath();ctx.arc(X(i),Y(vals[i]),4.3,0,Math.PI*2);ctx.fill()}});
ctx.fillStyle="#8ca6b5";
const ticks=Math.min(5,vals.length);
for(let j=0;j<ticks;j++){
const i=Math.round((vals.length-1)*j/Math.max(1,ticks-1)),txt=String(currentLabels[i]??i+1);
const short=txt.length>18?txt.slice(0,17)+"…":txt;
ctx.fillText(short,X(i)-Math.min(45,short.length*3),H-10);
}
}
function downloadAnomalies(){
if(!anomalies.length){alert("Keine Anomalien zum Exportieren.");return}
const x=$("xColumn").value,y=$("yColumn").value;
let csv=`${x==="__index__"?"index":x},${y},robust_score\n`;
anomalies.forEach(a=>csv+=`"${String(a.label).replace(/"/g,'""')}",${a.value},${a.score}\n`);
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const url=URL.createObjectURL(blob),a=document.createElement("a");
a.href=url;a.download="sensorik-anomalien.csv";a.click();URL.revokeObjectURL(url);
}
window.addEventListener("resize",()=>{if(currentValues.length) analyze()});
</script>
</body>
</html>