Edeka commited on
Commit
43c79d7
·
verified ·
1 Parent(s): bc70014

Upload 3 files

Browse files
Files changed (4) hide show
  1. .gitattributes +1 -0
  2. README.md +126 -0
  3. index.html +413 -0
  4. sensorik.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ sensorik.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sensor Data Analyzer
3
+ emoji: 📡
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: static
7
+ app_file: index.html
8
+ pinned: false
9
+ license: mit
10
+ short_description: Sensordaten lokal analysieren und Anomalien erkennen.
11
+ ---
12
+
13
+ # Sensor Data Analyzer
14
+
15
+ **Sensordaten direkt im Browser analysieren, visualisieren und Auffälligkeiten erkennen.**
16
+
17
+ Sensor Data Analyzer ist ein kostenloses Tool der unabhängigen Hugging-Face-Organisation **Sensorik**.
18
+
19
+ Das Tool eignet sich für CSV-basierte Messreihen aus Bereichen wie:
20
+
21
+ - Temperatur
22
+ - Luftfeuchtigkeit
23
+ - Druck
24
+ - Strom und Spannung
25
+ - Vibration
26
+ - Maschinenzustände
27
+ - IoT
28
+ - Umweltmessung
29
+ - Energie
30
+ - Labor- und Versuchsdaten
31
+
32
+ ## Funktionen
33
+
34
+ - CSV-Dateien lokal im Browser öffnen
35
+ - Trennzeichen automatisch erkennen
36
+ - numerische Messspalten automatisch erkennen
37
+ - Zeit-/Indexspalte auswählen
38
+ - Messwerte visualisieren
39
+ - Mittelwert, Median, Minimum, Maximum und Standardabweichung berechnen
40
+ - gleitenden Mittelwert anzeigen
41
+ - Anomalien über robuste MAD-Analyse erkennen
42
+ - Empfindlichkeit der Erkennung einstellen
43
+ - auffällige Messwerte tabellarisch anzeigen
44
+ - erkannte Anomalien als CSV exportieren
45
+ - Beispieldatensatz direkt im Tool laden
46
+ - responsive Oberfläche für Desktop und Mobilgeräte
47
+
48
+ ## Datenschutz
49
+
50
+ **Die Verarbeitung erfolgt vollständig lokal im Browser.**
51
+
52
+ Dieser Static Space überträgt hochgeladene CSV-Dateien oder Messwerte nicht an einen Server.
53
+
54
+ ## Warum MAD?
55
+
56
+ Für die automatische Auffälligkeitserkennung verwendet das Tool eine robuste Methode auf Basis von
57
+ **Median Absolute Deviation (MAD)**.
58
+
59
+ Sie ist gegenüber einzelnen Extremwerten häufig robuster als ein klassischer Z-Score.
60
+
61
+ Eine erkannte Anomalie bedeutet jedoch **nicht automatisch**, dass ein Sensor oder eine Maschine defekt ist.
62
+
63
+ ## Beispiel
64
+
65
+ Eine Messreihe wie:
66
+
67
+ ```text
68
+ timestamp,temperature
69
+ 10:00,68.2
70
+ 10:01,68.5
71
+ 10:02,68.1
72
+ 10:03,92.7
73
+ 10:04,68.4
74
+ ```
75
+
76
+ kann den Messwert `92.7` als auffällig markieren.
77
+
78
+ Die Ursache muss anschließend fachlich untersucht werden.
79
+
80
+ ## Mögliche Einsatzbereiche
81
+
82
+ - schnelle Erstprüfung von Sensordaten
83
+ - Datenqualität
84
+ - IoT-Debugging
85
+ - Maschinenüberwachung
86
+ - Predictive-Maintenance-Prototyping
87
+ - Umwelt- und Energiedaten
88
+ - Lehre und Demonstration
89
+ - Vorbereitung von Machine-Learning-Datasets
90
+
91
+ ## Einschränkungen
92
+
93
+ Das Tool ist bewusst leichtgewichtig.
94
+
95
+ Es berücksichtigt unter anderem nicht automatisch:
96
+
97
+ - physikalische Grenzwerte
98
+ - Sensor-Kalibrierung
99
+ - saisonale Effekte
100
+ - komplexe Trendbrüche
101
+ - mehrere gekoppelte Sensorsignale
102
+ - Maschinen- oder Prozesskontext
103
+
104
+ Für sicherheitskritische oder industrielle Entscheidungen ist eine fachliche Validierung erforderlich.
105
+
106
+ ## Wichtiger Hinweis
107
+
108
+ Sensor Data Analyzer ist ein **Analyse-, Lern- und Prototyping-Werkzeug**.
109
+
110
+ Er ist kein zertifiziertes:
111
+
112
+ - Messsystem
113
+ - Diagnosesystem
114
+ - Sicherheitssystem
115
+ - Steuerungssystem
116
+ - Predictive-Maintenance-System
117
+
118
+ Ergebnisse können unvollständig, fehlerhaft oder kontextabhängig sein.
119
+
120
+ ## Unabhängigkeit
121
+
122
+ **Sensorik** ist eine unabhängige Hugging-Face-Community-Organisation und nicht mit einem Sensorhersteller, Industrieunternehmen, Forschungsinstitut oder Hugging Face offiziell verbunden.
123
+
124
+ ---
125
+
126
+ **Messen. Verstehen. Erkennen. Intelligent reagieren.**
index.html ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}
15
+ body{
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}
31
+ h1{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}
34
+ button,.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:7px
37
+ }
38
+ button: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)}
42
+ input[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}
52
+ select,input[type=range],input[type=number]{
53
+ width:100%;border:1px solid var(--line);border-radius:10px;background:#071a28;color:var(--text);padding:10px
54
+ }
55
+ input[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}
64
+ canvas{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}
69
+ table{width:100%;border-collapse:collapse;font-size:12px}
70
+ th,td{padding:9px 10px;border-bottom:1px solid rgba(36,72,94,.7);text-align:left;white-space:nowrap}
71
+ th{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}
75
+ footer{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>
187
+ let rows=[], headers=[], anomalies=[], currentValues=[], currentLabels=[];
188
+
189
+ const $=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
+
197
+ function 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
+ }
201
+ function 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
+ }
225
+ function 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
+ }
233
+ function 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
+ }
238
+ function 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
+ }
246
+ function esc(s){return String(s).replace(/[&<>"']/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[m]))}
247
+
248
+ async 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
+
261
+ function 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
+
284
+ function 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
+ }
289
+ function 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
+ }
296
+ function 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
+ }
306
+ function 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
+ }
316
+ function fmt(n){
317
+ if(!Number.isFinite(n))return "–";
318
+ return new Intl.NumberFormat("de-DE",{maximumFractionDigits:3}).format(n);
319
+ }
320
+
321
+ function 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
+
355
+ function renderEmpty(){
356
+ $("chartNote").textContent="Lade eine CSV-Datei oder Beispieldaten.";
357
+ $("anomalyTable").innerHTML='<div class="empty">Noch keine Analyse vorhanden.</div>';
358
+ }
359
+
360
+ function 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
+
370
+ function 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
+
401
+ function 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
+ }
410
+ window.addEventListener("resize",()=>{if(currentValues.length) analyze()});
411
+ </script>
412
+ </body>
413
+ </html>
sensorik.png ADDED

Git LFS Details

  • SHA256: 924fe06b99542e2915407c61f78cbfc93f21fa74240674296402f7dd427a9700
  • Pointer size: 132 Bytes
  • Size of remote file: 1.8 MB