First commit

This commit is contained in:
2025-11-14 08:05:43 +00:00
commit 01fc486db7
3 changed files with 792 additions and 0 deletions

548
feinstaub.html Normal file
View File

@@ -0,0 +1,548 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Joachim Hummel">
<meta name="creator" content="Joachim Hummel">
<meta name="publisher" content="Next8AI">
<meta name="copyright" content="© 2025 Joachim Hummel">
<link rel="author" href="https://www.joachimhummel.de/impressum/">
<meta name="description" content="Interaktive Visualisierung von SDS/Feinstaub-Daten. Entwicklung & Umsetzung: Joachim Hummel (Next8AI).">
<meta property="og:type" content="website">
<meta property="og:locale" content="de_DE">
<meta property="og:title" content="SDS Daten Visualisierung Joachim Hummel">
<meta property="og:description" content="Interaktive Visualisierung von SDS/Feinstaub-Daten. Entwicklung & Umsetzung: Joachim Hummel (Next8AI).">
<meta property="og:url" content="https://next8ai.de/sds-visualisierung/">
<meta property="og:site_name" content="Next8AI">
<meta property="og:image" content="https://web.unixweb.home64.de/assets/sds-preview.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="SDS Daten Visualisierung Joachim Hummel">
<meta name="twitter:description" content="Interaktive Visualisierung von SDS/Feinstaub-Daten.">
<meta name="twitter:image" content="https://web.unixweb.home64.de/assets/sds-preview.png">
<title>SDS/Feinstaubbelastung Daten Visualisierung</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
margin-bottom: 10px;
}
.controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.filter-section {
width: 100%;
margin-top: 20px;
padding: 20px;
background: #f7fafc;
border-radius: 5px;
border-left: 4px solid #f6ad55;
}
.filter-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-item label {
font-size: 13px;
font-weight: 600;
color: #4a5568;
}
.filter-item input[type="range"] {
width: 100%;
}
.filter-value {
font-size: 14px;
color: #2d3748;
font-weight: bold;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 10px;
}
.toggle-switch input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: -9999px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #48bb78;
color: white;
}
.btn-secondary:hover {
background: #38a169;
}
.chart-container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
position: relative;
height: 600px;
}
.status {
margin-top: 15px;
padding: 10px 15px;
border-radius: 5px;
font-size: 14px;
}
.status.success {
background: #c6f6d5;
color: #22543d;
}
.status.error {
background: #fed7d7;
color: #742a2a;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: #f7fafc;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.stat-label {
font-size: 12px;
color: #718096;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2d3748;
margin-top: 5px;
}
#apiUrl {
flex: 1;
padding: 10px 15px;
border: 2px solid #e2e8f0;
border-radius: 5px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>SDS-Feinstaubbelastung Datenvisualisierung</h1>
<div class="controls">
<div class="file-input-wrapper">
<label class="btn btn-primary" for="jsonFile">JSON Datei laden</label>
<input type="file" id="jsonFile" accept=".json">
</div>
<input type="text" id="apiUrl" placeholder="Oder API-Endpunkt eingeben (z.B. https://api.example.com/data)">
<button class="btn btn-secondary" id="loadApi">Von API laden</button>
</div>
<div id="status"></div>
<div class="filter-section">
<h3 style="margin-bottom: 10px; color: #2d3748;">Ausreißer-Filter</h3>
<div class="toggle-switch">
<input type="checkbox" id="enableFilter" checked>
<label for="enableFilter" style="margin: 0;">Automatische Ausreißer-Filterung aktivieren</label>
</div>
<div class="filter-controls" id="filterControls">
<div class="filter-item">
<label for="p1MaxSlider">SDS_P1 Maximum</label>
<input type="range" id="p1MaxSlider" min="0" max="1000" value="100" step="10">
<span class="filter-value" id="p1MaxValue">100</span>
</div>
<div class="filter-item">
<label for="p2MaxSlider">SDS_P2 Maximum</label>
<input type="range" id="p2MaxSlider" min="0" max="1000" value="100" step="10">
<span class="filter-value" id="p2MaxValue">100</span>
</div>
<div class="filter-item">
<label>Gefilterte Werte</label>
<span class="filter-value" id="filteredCount">0 Datenpunkte entfernt</span>
</div>
</div>
</div>
<div id="status"></div>
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-label">Datenpunkte</div>
<div class="stat-value" id="statCount">0</div>
</div>
<div class="stat-card">
<div class="stat-label">SDS_P1 Durchschnitt</div>
<div class="stat-value" id="statP1Avg">0</div>
</div>
<div class="stat-card">
<div class="stat-label">SDS_P2 Durchschnitt</div>
<div class="stat-value" id="statP2Avg">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Zeitraum</div>
<div class="stat-value" id="statTimeRange">-</div>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="sdsChart"></canvas>
</div>
</div>
<script>
let chart = null;
let rawData = null;
let filterSettings = {
enabled: true,
p1Max: 100,
p2Max: 100
};
function calculateOutlierThreshold(values) {
const sorted = [...values].sort((a, b) => a - b);
const q1Index = Math.floor(sorted.length * 0.25);
const q3Index = Math.floor(sorted.length * 0.75);
const q1 = sorted[q1Index];
const q3 = sorted[q3Index];
const iqr = q3 - q1;
const upperBound = q3 + (1.5 * iqr);
return Math.min(upperBound, sorted[Math.floor(sorted.length * 0.95)]);
}
function applyFilters(data) {
if (!filterSettings.enabled) {
document.getElementById('filteredCount').textContent = '0 Datenpunkte entfernt';
return data;
}
const filtered = data.filter(item => {
const p1 = parseFloat(item.SDS_P1) || 0;
const p2 = parseFloat(item.SDS_P2) || 0;
return p1 <= filterSettings.p1Max && p2 <= filterSettings.p2Max;
});
const removed = data.length - filtered.length;
document.getElementById('filteredCount').textContent = `${removed} Datenpunkte entfernt`;
return filtered;
}
function autoDetectThresholds(data) {
const p1Values = data.map(item => parseFloat(item.SDS_P1) || 0);
const p2Values = data.map(item => parseFloat(item.SDS_P2) || 0);
const p1Threshold = calculateOutlierThreshold(p1Values);
const p2Threshold = calculateOutlierThreshold(p2Values);
filterSettings.p1Max = Math.ceil(p1Threshold);
filterSettings.p2Max = Math.ceil(p2Threshold);
document.getElementById('p1MaxSlider').value = filterSettings.p1Max;
document.getElementById('p2MaxSlider').value = filterSettings.p2Max;
document.getElementById('p1MaxSlider').max = Math.max(1000, filterSettings.p1Max * 2);
document.getElementById('p2MaxSlider').max = Math.max(1000, filterSettings.p2Max * 2);
document.getElementById('p1MaxValue').textContent = filterSettings.p1Max;
document.getElementById('p2MaxValue').textContent = filterSettings.p2Max;
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type}`;
status.style.display = 'block';
}
function updateStats(data) {
const statsDiv = document.getElementById('stats');
statsDiv.style.display = 'grid';
document.getElementById('statCount').textContent = data.length;
const p1Avg = (data.reduce((sum, item) => sum + (parseFloat(item.SDS_P1) || 0), 0) / data.length).toFixed(2);
const p2Avg = (data.reduce((sum, item) => sum + (parseFloat(item.SDS_P2) || 0), 0) / data.length).toFixed(2);
document.getElementById('statP1Avg').textContent = p1Avg;
document.getElementById('statP2Avg').textContent = p2Avg;
if (data.length > 0) {
const times = data.map(item => item.Uhrzeit || item.timestamp || '');
const minTime = times[0];
const maxTime = times[times.length - 1];
const timeRange = `${minTime} - ${maxTime}`;
document.getElementById('statTimeRange').textContent = timeRange;
}
}
function processData(jsonData) {
let data = Array.isArray(jsonData) ? jsonData : (jsonData.data || jsonData.records || []);
if (data.length === 0) {
showStatus('Keine Daten gefunden. Überprüfen Sie das JSON-Format.', 'error');
return;
}
rawData = data;
autoDetectThresholds(rawData);
renderChart();
}
function renderChart() {
if (!rawData) return;
const data = [...rawData];
data.sort((a, b) => {
const timeA = a.Uhrzeit || a.timestamp || a.Zeitstempel || a.created_at || '';
const timeB = b.Uhrzeit || b.timestamp || b.Zeitstempel || b.created_at || '';
return timeA.localeCompare(timeB);
});
const filteredData = applyFilters(data);
const labels = filteredData.map(item => {
return item.Uhrzeit || item.timestamp || item.Zeitstempel || item.created_at || 'Unbekannt';
});
const sds_p1_data = filteredData.map(item => parseFloat(item.SDS_P1) || 0);
const sds_p2_data = filteredData.map(item => parseFloat(item.SDS_P2) || 0);
updateChart(labels, sds_p1_data, sds_p2_data);
updateStats(filteredData);
showStatus(`Erfolgreich ${filteredData.length} von ${data.length} Datenpunkten geladen.`, 'success');
}
function updateChart(labels, p1Data, p2Data) {
const ctx = document.getElementById('sdsChart').getContext('2d');
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'SDS_P1',
data: p1Data,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 3,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
fill: true
},
{
label: 'SDS_P2',
data: p2Data,
borderColor: '#48bb78',
backgroundColor: 'rgba(72, 187, 120, 0.1)',
borderWidth: 3,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 14
},
padding: 20
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: {
size: 14
},
bodyFont: {
size: 13
}
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: 'Zeitpunkt',
font: {
size: 14,
weight: 'bold'
}
},
ticks: {
maxRotation: 45,
minRotation: 45
}
},
y: {
display: true,
title: {
display: true,
text: 'Wert',
font: {
size: 14,
weight: 'bold'
}
},
beginAtZero: true
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
}
});
}
document.getElementById('jsonFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
try {
const jsonData = JSON.parse(event.target.result);
processData(jsonData);
} catch (error) {
showStatus('Fehler beim Lesen der JSON-Datei: ' + error.message, 'error');
}
};
reader.readAsText(file);
});
document.getElementById('loadApi').addEventListener('click', async function() {
const url = document.getElementById('apiUrl').value.trim();
if (!url) {
showStatus('Bitte geben Sie eine API-URL ein.', 'error');
return;
}
try {
showStatus('Daten werden geladen...', 'success');
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-Fehler: ${response.status}`);
}
const jsonData = await response.json();
processData(jsonData);
} catch (error) {
showStatus('Fehler beim Laden der API-Daten: ' + error.message, 'error');
}
});
// Filter Event Listeners
document.getElementById('enableFilter').addEventListener('change', function(e) {
filterSettings.enabled = e.target.checked;
document.getElementById('filterControls').style.opacity = e.target.checked ? '1' : '0.5';
renderChart();
});
document.getElementById('p1MaxSlider').addEventListener('input', function(e) {
filterSettings.p1Max = parseInt(e.target.value);
document.getElementById('p1MaxValue').textContent = filterSettings.p1Max;
renderChart();
});
document.getElementById('p2MaxSlider').addEventListener('input', function(e) {
filterSettings.p2Max = parseInt(e.target.value);
document.getElementById('p2MaxValue').textContent = filterSettings.p2Max;
renderChart();
});
// Beispieldaten für Demo
const exampleData = [
{ Uhrzeit: '09:45:36', SDS_P1: 17.53, SDS_P2: 96272.72 },
{ Uhrzeit: '10:00:38', SDS_P1: 9.15, SDS_P2: 6.22 },
{ Uhrzeit: '10:15:36', SDS_P1: 9.75, SDS_P2: 5.97 },
{ Uhrzeit: '10:30:36', SDS_P1: 9.93, SDS_P2: 6.35 },
{ Uhrzeit: '10:45:36', SDS_P1: 9.55, SDS_P2: 5.88 },
{ Uhrzeit: '11:00:36', SDS_P1: 12.15, SDS_P2: 6.93 }
];
// Demo-Daten initial laden
processData(exampleData);
</script>
<footer style="margin:2rem 0; font-size:.9rem; opacity:.8">
Entwickelt von <a href="https://joachimhummel.de/">Joachim Hummel</a> · © 2025 ·
<a href="https://next8ai.de/impressum/">Impressum</a> ·
<a href="https://next8ai.de/datenschutz/">Datenschutz</a>
</footer>
</body>
</html>