You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
705 lines
29 KiB
HTML
705 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Micro Air Quality Station</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
|
<style>
|
|
/* Estilo general con gradiente sutil de fondo */
|
|
body {
|
|
background: linear-gradient(135deg, #f5f5f7 0%, #e8e8ed 100%);
|
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
color: #1d1d1f;
|
|
margin: 0;
|
|
padding: 40px 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
h1 {
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
font-size: 2.5rem;
|
|
margin-bottom: 5px;
|
|
background: linear-gradient(90deg, #1d1d1f, #434344);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
h2 {
|
|
font-weight: 600;
|
|
letter-spacing: -0.5px;
|
|
color: #1d1d1f;
|
|
}
|
|
|
|
.contenedor {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Franja de Color NOM-172 con efecto premium */
|
|
#franja-ica {
|
|
padding: 24px;
|
|
border-radius: 24px;
|
|
margin-bottom: 40px;
|
|
text-align: center;
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 24px;
|
|
background: linear-gradient(135deg, #86868b, #6e6e73);
|
|
transition: background 0.5s ease, box-shadow 0.3s ease;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* Diseño de las Tarjetas (Cards) con profundidad multicapa */
|
|
.grid-tarjetas {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
gap: 24px;
|
|
margin-bottom: 50px;
|
|
}
|
|
|
|
.grid-graficas {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 40px;
|
|
margin-bottom: 50px;
|
|
}
|
|
|
|
.tarjeta, .tarjeta-grafica {
|
|
background: linear-gradient(145deg, #ffffff, #fdfdfd);
|
|
border-radius: 24px;
|
|
padding: 28px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.02);
|
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tarjeta:hover {
|
|
transform: translateY(-6px);
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), 0 2px 5px rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
/* Acento decorativo sutil en las tarjetas */
|
|
.tarjeta::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 4px;
|
|
background: linear-gradient(90deg, transparent, rgba(0, 113, 227, 0.1), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.tarjeta:hover::before { opacity: 1; }
|
|
|
|
.titulo-tarjeta {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #86868b;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.valor-tarjeta {
|
|
font-size: 46px;
|
|
font-weight: 700;
|
|
color: #1d1d1f;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
letter-spacing: -1.5px;
|
|
}
|
|
|
|
.unidad {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #a1a1a6;
|
|
letter-spacing: normal;
|
|
}
|
|
|
|
.error-desconectado {
|
|
font-size: 16px;
|
|
color: #ff3b30;
|
|
font-weight: 600;
|
|
background: rgba(255, 59, 48, 0.1);
|
|
padding: 8px 12px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
/* Elementos de formulario y botones modernizados */
|
|
.controles-superiores {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
background: rgba(255, 255, 255, 0.6);
|
|
padding: 15px 25px;
|
|
border-radius: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
select, input[type="date"] {
|
|
padding: 10px 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid #d2d2d7;
|
|
background-color: #ffffff;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
outline: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
|
}
|
|
|
|
select:focus, input[type="date"]:focus {
|
|
border-color: #0071e3;
|
|
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.2);
|
|
}
|
|
|
|
button {
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
border: none;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.btn-buscar { background: linear-gradient(135deg, #0071e3, #005bb5); color: white; font-weight: 600; }
|
|
.btn-csv { background: linear-gradient(135deg, #34c759, #28a745); color: white; font-weight: 600; }
|
|
.btn-excel { background: linear-gradient(135deg, #107c41, #0c5e31); color: white; font-weight: 600; }
|
|
|
|
/* Estilos de la Miniterminal estilo macOS */
|
|
.tarjeta-terminal {
|
|
background-color: #1e1e1e;
|
|
border-radius: 12px;
|
|
padding: 0;
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
margin-bottom: 50px;
|
|
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', Courier, monospace;
|
|
overflow: hidden;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.terminal-header {
|
|
background: linear-gradient(180deg, #3a3a3a 0%, #2a2a2a 100%);
|
|
padding: 10px 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
border-bottom: 1px solid #111;
|
|
}
|
|
|
|
.terminal-dots {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
}
|
|
.dot.red { background-color: #ff5f56; box-shadow: inset 0 0 4px rgba(0,0,0,0.2); }
|
|
.dot.yellow { background-color: #ffbd2e; box-shadow: inset 0 0 4px rgba(0,0,0,0.2); }
|
|
.dot.green { background-color: #27c93f; box-shadow: inset 0 0 4px rgba(0,0,0,0.2); }
|
|
|
|
.terminal-title {
|
|
color: #999;
|
|
font-size: 13px;
|
|
margin: 0 auto;
|
|
font-weight: 500;
|
|
transform: translateX(-20px); /* Ajuste visual por los puntos */
|
|
}
|
|
|
|
.terminal-cuerpo { padding: 20px; }
|
|
|
|
.terminal-salida {
|
|
height: 250px;
|
|
overflow-y: auto;
|
|
color: #4af626;
|
|
margin-bottom: 15px;
|
|
white-space: pre-wrap;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
text-shadow: 0 0 2px rgba(74, 246, 38, 0.4);
|
|
}
|
|
.terminal-salida::-webkit-scrollbar { width: 8px; }
|
|
.terminal-salida::-webkit-scrollbar-thumb { background-color: #444; border-radius: 4px; }
|
|
|
|
.terminal-input-container {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.terminal-input {
|
|
flex-grow: 1;
|
|
background-color: transparent;
|
|
color: #fff;
|
|
border: none;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
|
|
.terminal-btn {
|
|
background: #333;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
box-shadow: none;
|
|
}
|
|
.terminal-btn:hover { background: #555; transform: none; box-shadow: none; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="contenedor">
|
|
<h1>CENTRO DE MONITOREO DE CALIDAD DEL AIRE</h1>
|
|
<p style="color: #86868b; margin-bottom: 30px; font-size: 1.1rem; font-weight: 500;">Adquisición de Datos de Calidad del Aire en Tiempo Real</p>
|
|
|
|
<!-- FRANJA NOM-172 -->
|
|
<div id="franja-ica">Calculando Índice de Calidad del Aire (ICA)...</div>
|
|
|
|
<!-- TARJETAS DE LECTURA EN TIEMPO REAL -->
|
|
<div class="grid-tarjetas">
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>☁️</span> Monóxido de Carbono (CO)</div>
|
|
<div class="valor-tarjeta" id="valor-co">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>🛡️</span> Ozono (O3)</div>
|
|
<div class="valor-tarjeta" id="valor-o3">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>🚗</span> Dióxido de Nitrógeno (NO2)</div>
|
|
<div class="valor-tarjeta" id="valor-no2">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>🏭</span> Dióxido de Azufre (SO2)</div>
|
|
<div class="valor-tarjeta" id="valor-so2">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>🌡️</span> Temperatura</div>
|
|
<div class="valor-tarjeta" id="valor-temp">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>💧</span> Humedad</div>
|
|
<div class="valor-tarjeta" id="valor-hum">--</div>
|
|
</div>
|
|
<div class="tarjeta">
|
|
<div class="titulo-tarjeta"><span>🧭</span> Presión Atmosférica</div>
|
|
<div class="valor-tarjeta" id="valor-pres">--</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GRÁFICA COMPARATIVA ICA -->
|
|
<div class="tarjeta-grafica" style="margin-bottom: 50px;">
|
|
<h2>Comparativa de Índices de Calidad (ICA)</h2>
|
|
<canvas id="graficaComparativaICA" height="80"></canvas>
|
|
</div>
|
|
|
|
<!-- SECCIÓN DE GRÁFICAS INDIVIDUALES -->
|
|
<div class="controles-superiores">
|
|
<h2 style="margin: 0;">Tendencias Históricas Individuales</h2>
|
|
|
|
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
|
<select id="selector-rango" onchange="gestionarSelector()">
|
|
<option value="1_mes">Último Mes</option>
|
|
<option value="2_meses">Últimos 2 Meses</option>
|
|
<option value="1_anio">Último Año</option>
|
|
<option value="personalizado">Personalizado...</option>
|
|
</select>
|
|
|
|
<div id="rango-personalizado" style="display: none; gap: 10px; align-items: center;">
|
|
<input type="date" id="fecha-inicio">
|
|
<span style="font-weight: 600; color: #86868b;">a</span>
|
|
<input type="date" id="fecha-fin">
|
|
<button class="btn-buscar" onclick="actualizarGrafica()">🔍 Buscar</button>
|
|
</div>
|
|
|
|
<button class="btn-csv" onclick="exportarCSV()">📄 Exportar CSV</button>
|
|
<button class="btn-excel" onclick="exportarExcel()">📊 Exportar Excel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-graficas">
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Monóxido de Carbono (CO)</h2>
|
|
<canvas id="graficaCO"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Ozono (O3)</h2>
|
|
<canvas id="graficaO3"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Dióxido de Nitrógeno (NO2)</h2>
|
|
<canvas id="graficaNO2"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Dióxido de Azufre (SO2)</h2>
|
|
<canvas id="graficaSO2"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Temperatura</h2>
|
|
<canvas id="graficaTemp"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Humedad</h2>
|
|
<canvas id="graficaHum"></canvas>
|
|
</div>
|
|
<div class="tarjeta-grafica">
|
|
<h2>Gráfica de Presión Atmosférica</h2>
|
|
<canvas id="graficaPres"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MINITERMINAL REMOTA ESTILO MACOS -->
|
|
<div class="tarjeta-terminal">
|
|
<div class="terminal-header">
|
|
<div class="terminal-dots">
|
|
<div class="dot red"></div>
|
|
<div class="dot yellow"></div>
|
|
<div class="dot green"></div>
|
|
</div>
|
|
<div class="terminal-title">pi@microestacion:~ (Bash)</div>
|
|
</div>
|
|
<div class="terminal-cuerpo">
|
|
<div class="terminal-salida" id="terminal-salida">Bienvenido a la Raspberry Pi. Sistema listo.
|
|
Para consultar parámetros manuales utilice los comandos locales (ej. aql clima, aql CO, aql values)...
|
|
</div>
|
|
<div class="terminal-input-container">
|
|
<span style="color: #4af626; font-weight: bold;">$</span>
|
|
<input type="text" id="terminal-input" class="terminal-input" placeholder="Escribe un comando..." onkeypress="manejarEnterTerminal(event)">
|
|
<button class="terminal-btn" onclick="ejecutarComando()">Enviar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
let datosGlobalesParaExportar = [];
|
|
|
|
// --- 1. LÓGICA NOM-172 ---
|
|
function calcularICA(cp, gas) {
|
|
const breakpoints = {
|
|
"O3": [[0.0, 0.051, 0, 50], [0.052, 0.070, 51, 100], [0.071, 0.092, 101, 150], [0.093, 0.114, 151, 200], [0.115, 10.0, 201, 500]],
|
|
"CO": [[0.0, 8.74, 0, 50], [8.75, 11.00, 51, 100], [11.01, 13.30, 101, 150], [13.31, 15.50, 151, 200], [15.51, 100.0, 201, 500]],
|
|
"SO2": [[0.0, 0.008, 0, 50], [0.009, 0.110, 51, 100], [0.111, 0.165, 101, 150], [0.166, 0.220, 151, 200], [0.221, 10.0, 201, 500]],
|
|
"NO2": [[0.0, 0.107, 0, 50], [0.108, 0.210, 51, 100], [0.211, 0.230, 101, 150], [0.231, 0.250, 151, 200], [0.251, 10.0, 201, 500]]
|
|
};
|
|
if (!breakpoints[gas]) return 0;
|
|
for (let bp of breakpoints[gas]) {
|
|
if (cp >= bp[0] && cp <= bp[1]) {
|
|
return Math.round(((bp[3] - bp[2]) / (bp[1] - bp[0])) * (cp - bp[0]) + bp[2]);
|
|
}
|
|
}
|
|
return 500;
|
|
}
|
|
|
|
// Modificado para usar gradientes modernos en la franja
|
|
function obtenerColorBanda(ica) {
|
|
if (ica <= 50) return { texto: "Buena", color: "linear-gradient(135deg, #34c759, #28a745)" };
|
|
if (ica <= 100) return { texto: "Aceptable", color: "linear-gradient(135deg, #ffcc00, #e5b700)" };
|
|
if (ica <= 150) return { texto: "Mala", color: "linear-gradient(135deg, #ff9500, #e58600)" };
|
|
if (ica <= 200) return { texto: "Muy Mala", color: "linear-gradient(135deg, #ff3b30, #d93128)" };
|
|
return { texto: "Extremadamente Mala", color: "linear-gradient(135deg, #af52de, #9948c2)" };
|
|
}
|
|
|
|
let graficaComparativa;
|
|
|
|
// --- 2. TIEMPO REAL ---
|
|
async function obtenerDatosActuales() {
|
|
try {
|
|
const respuesta = await fetch('/api/actual');
|
|
const datos = await respuesta.json();
|
|
|
|
if (datos.estado === "desconectado" || datos.estado === "error") {
|
|
const mensajeError = `<span class="error-desconectado">⚠️ Desconectado</span>`;
|
|
document.querySelectorAll('.valor-tarjeta').forEach(el => el.innerHTML = mensajeError);
|
|
document.getElementById('franja-ica').innerText = "Error: Sensores Desconectados";
|
|
document.getElementById('franja-ica').style.background = "linear-gradient(135deg, #86868b, #6e6e73)";
|
|
return;
|
|
}
|
|
|
|
document.getElementById('valor-co').innerHTML = `${datos.CO} <span class="unidad">ppm</span>`;
|
|
document.getElementById('valor-o3').innerHTML = `${datos.O3} <span class="unidad">ppm</span>`;
|
|
document.getElementById('valor-no2').innerHTML = `${datos.NO2} <span class="unidad">ppm</span>`;
|
|
document.getElementById('valor-so2').innerHTML = `${datos.SO2} <span class="unidad">ppm</span>`;
|
|
document.getElementById('valor-temp').innerHTML = `${datos.TEMP} <span class="unidad">°C</span>`;
|
|
document.getElementById('valor-hum').innerHTML = `${datos.HUM} <span class="unidad">%</span>`;
|
|
document.getElementById('valor-pres').innerHTML = `${datos.PRES} <span class="unidad">hPa</span>`;
|
|
|
|
let icaCO = calcularICA(parseFloat(datos.CO), "CO");
|
|
let icaO3 = calcularICA(parseFloat(datos.O3), "O3");
|
|
let icaNO2 = calcularICA(parseFloat(datos.NO2), "NO2");
|
|
let icaSO2 = calcularICA(parseFloat(datos.SO2), "SO2");
|
|
|
|
let icaMaximo = Math.max(icaCO, icaO3, icaNO2, icaSO2);
|
|
let banda = obtenerColorBanda(icaMaximo);
|
|
|
|
let franja = document.getElementById('franja-ica');
|
|
franja.style.background = banda.color;
|
|
franja.innerText = `Calidad del Aire: ${banda.texto} (ICA: ${icaMaximo})`;
|
|
|
|
if(graficaComparativa) graficaComparativa.destroy();
|
|
const ctxComp = document.getElementById('graficaComparativaICA').getContext('2d');
|
|
graficaComparativa = new Chart(ctxComp, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Monóxido de Carbono', 'Ozono', 'Dióxido de Nitrógeno', 'Dióxido de Azufre'],
|
|
datasets: [{
|
|
label: 'Índice ICA Individual',
|
|
data: [icaCO, icaO3, icaNO2, icaSO2],
|
|
backgroundColor: ['#0071e3', '#34c759', '#ff9500', '#ff3b30'],
|
|
borderRadius: 12, // Bordes más redondeados
|
|
borderSkipped: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: true, suggestedMax: 150, grid: { color: '#f0f0f5' } },
|
|
x: { grid: { display: false } }
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) { console.error("Error de conexión:", error); }
|
|
}
|
|
|
|
setInterval(obtenerDatosActuales, 10000);
|
|
obtenerDatosActuales();
|
|
|
|
// --- 3. GRÁFICAS HISTÓRICAS INDIVIDUALES ---
|
|
let historicas = {};
|
|
|
|
function renderizarGraficaIndividual(idCanvas, etiquetas, datos, label, colorHex) {
|
|
if (historicas[idCanvas]) { historicas[idCanvas].destroy(); }
|
|
const ctx = document.getElementById(idCanvas).getContext('2d');
|
|
|
|
// Crear gradiente para el relleno debajo de la línea
|
|
let gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
gradient.addColorStop(0, colorHex + '66'); // 40% opacidad
|
|
gradient.addColorStop(1, colorHex + '00'); // Transparente
|
|
|
|
historicas[idCanvas] = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: etiquetas.length > 0 ? etiquetas : ['Sin datos'],
|
|
datasets: [{
|
|
label: label,
|
|
data: datos.length > 0 ? datos : [0],
|
|
borderColor: colorHex,
|
|
backgroundColor: gradient, // Relleno degradado moderno
|
|
borderWidth: 3, // Línea un poco más gruesa
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointBackgroundColor: '#ffffff',
|
|
pointBorderColor: colorHex,
|
|
pointBorderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
aspectRatio: 2.5,
|
|
plugins: {
|
|
legend: { display: false },
|
|
title: { display: false },
|
|
tooltip: {
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
titleColor: '#1d1d1f',
|
|
bodyColor: '#1d1d1f',
|
|
borderColor: 'rgba(0,0,0,0.1)',
|
|
borderWidth: 1,
|
|
padding: 12,
|
|
boxPadding: 6,
|
|
usePointStyle: true
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
title: { display: true, text: 'Fecha y Hora', color: '#86868b', font: { weight: '600', size: 13 } },
|
|
grid: { display: false }, // Limpieza visual en eje X
|
|
border: { display: true, color: '#e5e5ea', width: 2 },
|
|
ticks: { display: true, color: '#86868b', maxTicksLimit: 10, maxRotation: 45, minRotation: 45, font: {size: 11} }
|
|
},
|
|
y: {
|
|
display: true,
|
|
title: { display: true, text: label, color: '#86868b', font: { weight: '600', size: 13 } },
|
|
grid: { display: true, color: '#f0f0f5', drawOnChartArea: true, drawTicks: false },
|
|
border: { display: false }, // Ocultar borde Y para un look más abierto
|
|
ticks: { display: true, color: '#86868b', font: {size: 11}, padding: 10 },
|
|
beginAtZero: true
|
|
}
|
|
},
|
|
elements: { point: { radius: 0, hitRadius: 15, hoverRadius: 6 } }, // Puntos invisibles hasta hacer hover
|
|
interaction: { mode: 'index', intersect: false }
|
|
}
|
|
});
|
|
}
|
|
|
|
function gestionarSelector() {
|
|
const rango = document.getElementById('selector-rango').value;
|
|
const divPersonalizado = document.getElementById('rango-personalizado');
|
|
|
|
if (rango === 'personalizado') {
|
|
divPersonalizado.style.display = 'flex';
|
|
} else {
|
|
divPersonalizado.style.display = 'none';
|
|
actualizarGrafica();
|
|
}
|
|
}
|
|
|
|
async function actualizarGrafica() {
|
|
const rango = document.getElementById('selector-rango').value;
|
|
let urlAPI = `/api/historial?rango=${rango}`;
|
|
|
|
if (rango === 'personalizado') {
|
|
const inicio = document.getElementById('fecha-inicio').value;
|
|
const fin = document.getElementById('fecha-fin').value;
|
|
if (!inicio || !fin) {
|
|
alert("Por favor, selecciona una fecha de inicio y una de fin.");
|
|
return;
|
|
}
|
|
urlAPI += `&inicio=${inicio}&fin=${fin}`;
|
|
}
|
|
|
|
const respuesta = await fetch(urlAPI);
|
|
const datosHistorial = await respuesta.json();
|
|
|
|
datosGlobalesParaExportar = datosHistorial;
|
|
|
|
if (datosHistorial.length === 0) {
|
|
alert("La base de datos no tiene registros para este rango de fechas.");
|
|
}
|
|
|
|
const fechas = datosHistorial.map(f => f.fecha_hora);
|
|
|
|
renderizarGraficaIndividual('graficaCO', fechas, datosHistorial.map(f => f.co), 'Monóxido de Carbono (ppm)', '#0071e3');
|
|
renderizarGraficaIndividual('graficaO3', fechas, datosHistorial.map(f => f.ozono), 'Ozono (ppm)', '#34c759');
|
|
renderizarGraficaIndividual('graficaNO2', fechas, datosHistorial.map(f => f.no2), 'Dióxido de Nitrógeno (ppm)', '#ff9500');
|
|
renderizarGraficaIndividual('graficaSO2', fechas, datosHistorial.map(f => f.so2), 'Dióxido de Azufre (ppm)', '#ff3b30');
|
|
renderizarGraficaIndividual('graficaTemp', fechas, datosHistorial.map(f => f.temperatura), 'Temperatura (°C)', '#ffcc00');
|
|
renderizarGraficaIndividual('graficaHum', fechas, datosHistorial.map(f => f.humedad), 'Humedad (%)', '#5ac8fa');
|
|
renderizarGraficaIndividual('graficaPres', fechas, datosHistorial.map(f => f.presion), 'Presión Atmosférica (hPa)', '#5856d6');
|
|
}
|
|
|
|
// --- 4. FUNCIÓN PARA EXPORTAR A CSV (EXCEL) ---
|
|
function exportarCSV() {
|
|
if (datosGlobalesParaExportar.length === 0) {
|
|
alert("No hay datos disponibles para exportar en este rango.");
|
|
return;
|
|
}
|
|
let contenidoCSV = "Fecha y Hora,CO (ppm),O3 (ppm),NO2 (ppm),SO2 (ppm),Temperatura (C),Humedad (%),Presion Atmosferica (hPa)\n";
|
|
datosGlobalesParaExportar.forEach(fila => {
|
|
contenidoCSV += `${fila.fecha_hora},${fila.co},${fila.ozono},${fila.no2},${fila.so2},${fila.temperatura},${fila.humedad},${fila.presion}\n`;
|
|
});
|
|
const blob = new Blob([contenidoCSV], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const enlaceVirtual = document.createElement("a");
|
|
enlaceVirtual.setAttribute("href", url);
|
|
enlaceVirtual.setAttribute("download", "Historial_Calidad_Aire.csv");
|
|
document.body.appendChild(enlaceVirtual);
|
|
enlaceVirtual.click();
|
|
document.body.removeChild(enlaceVirtual);
|
|
}
|
|
|
|
// --- 5. FUNCIÓN PARA EXPORTAR A EXCEL (.XLSX) ---
|
|
function exportarExcel() {
|
|
if (datosGlobalesParaExportar.length === 0) {
|
|
alert("No hay datos disponibles para exportar en este rango.");
|
|
return;
|
|
}
|
|
const datosFormateados = datosGlobalesParaExportar.map(fila => ({
|
|
"Fecha y Hora": fila.fecha_hora,
|
|
"CO (ppm)": fila.co,
|
|
"Ozono - O3 (ppm)": fila.ozono,
|
|
"Dióxido de Nitrógeno - NO2 (ppm)": fila.no2,
|
|
"Dióxido de Azufre - SO2 (ppm)": fila.so2,
|
|
"Temperatura (°C)": fila.temperatura,
|
|
"Humedad (%)": fila.humedad,
|
|
"Presión Atmosférica (hPa)": fila.presion
|
|
}));
|
|
const hoja = XLSX.utils.json_to_sheet(datosFormateados);
|
|
const libro = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(libro, hoja, "Calidad del Aire");
|
|
XLSX.writeFile(libro, "Historial_Calidad_Aire.xlsx");
|
|
}
|
|
|
|
// --- 6. LÓGICA DE LA MINITERMINAL ---
|
|
function manejarEnterTerminal(event) {
|
|
if (event.key === "Enter") {
|
|
ejecutarComando();
|
|
}
|
|
}
|
|
|
|
async function ejecutarComando() {
|
|
const inputElement = document.getElementById('terminal-input');
|
|
const salidaElement = document.getElementById('terminal-salida');
|
|
const comando = inputElement.value.trim();
|
|
|
|
if (!comando) return;
|
|
|
|
salidaElement.innerHTML += `\n<span style="color: #ffffff;">$ ${comando}</span>\n`;
|
|
inputElement.value = "";
|
|
salidaElement.scrollTop = salidaElement.scrollHeight;
|
|
|
|
try {
|
|
const respuesta = await fetch('/api/ejecutar', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ comando: comando })
|
|
});
|
|
|
|
const datos = await respuesta.json();
|
|
|
|
if (datos.error && datos.error !== "") {
|
|
salidaElement.innerHTML += `<span style="color: #ff5f56;">${datos.error}</span>`;
|
|
} else if (datos.salida && datos.salida !== "") {
|
|
salidaElement.innerHTML += `${datos.salida}`;
|
|
}
|
|
|
|
} catch (error) {
|
|
salidaElement.innerHTML += `<span style="color: #ff5f56;">Error de comunicación con el servidor.</span>\n`;
|
|
}
|
|
|
|
salidaElement.scrollTop = salidaElement.scrollHeight;
|
|
}
|
|
|
|
actualizarGrafica();
|
|
</script>
|
|
</body>
|
|
</html> |