/* global Highcharts, constantes, jQuery */
// =========================
// Init global Highcharts (solo 1 vez)
// =========================
(function initHighchartsGlobalOnce() {
if (!window.Highcharts) return;
if (!window.__hcComparativaOptionsSet) {
Highcharts.setOptions({
global: { useUTC: false },
lang: {
resetZoom: "Deshacer zoom",
resetZoomTitle: "Deshacer zoom",
months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
shortMonths: ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"],
weekdays: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
shortWeekdays: ["Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab"],
downloadJPEG: "Descargar JPEG",
downloadPDF: "Descargar PDF",
downloadPNG: "Descargar PNG",
downloadSVG: "Descargar SVG",
downloadCSV: "Descargar CSV",
downloadXLS: "Descargar XLS",
viewFullscreen: "Ver en pantalla completa",
printChart: "Imprimir gráfica",
viewData: "Ver tabla de datos",
contextButtonTitle: "Desplegar menú",
rangeSelectorZoom: "Rango"
}
});
window.__hcComparativaOptionsSet = true;
}
if (!window.__hcComparativaDataRowsWrapped) {
(function (H) {
if (!H || !H.Chart || !H.Chart.prototype) return;
H.wrap(H.Chart.prototype, 'getDataRows', function (proceed, multiLevelHeaders) {
var rows = proceed.call(this, multiLevelHeaders),
xMin = this.xAxis && this.xAxis[0] ? this.xAxis[0].min : null,
xMax = this.xAxis && this.xAxis[0] ? this.xAxis[0].max : null;
if (typeof xMin !== 'number' || typeof xMax !== 'number') return rows;
return rows.filter(function (row) {
return typeof row.x !== 'number' || (row.x >= xMin && row.x <= xMax);
});
});
}(Highcharts));
window.__hcComparativaDataRowsWrapped = true;
}
}());
// =========================
// Función principal (global)
// =========================
window.graficaFormulario = function graficaFormulario(urlRecibida) {
(function ($) {
$(document).ready(function () {
// -------------------------
// Estado
// -------------------------
var seriesCounter = 0;
var series = [{}];
var precipitacion = false;
var precipitacionAcumulada = false;
var data_precipitacion = [];
var data_precipitacion_acum = [];
var chart;
var series_graph = [{}];
var ultimo_elemento_pa;
var variablesTabla = [];
// Si existe una chart previa, destrúyela (evita acumulación/lag)
if (window.__chartComparativa && typeof window.__chartComparativa.destroy === 'function') {
try { window.__chartComparativa.destroy(); } catch (e) { /* noop */ }
}
window.__chartComparativa = null;
// -------------------------
// Helpers (mínimo cambio)
// -------------------------
function contieneUmbral(cadena, palabra) {
return String(cadena || '').toLowerCase().includes(String(palabra || '').toLowerCase());
}
function formatearCabeceraComparativa(nombre) {
if (!nombre) return '';
var partes = String(nombre).split(' - ');
if (partes.length === 1) return nombre;
if (partes.length === 2) return partes[0] + ' - ' + partes[1];
if (partes.length === 3) return partes[0] + ' - ' + partes[1] + '
' + partes[2];
var primera = partes[0] + ' - ' + partes[1];
var ultima = partes[partes.length - 1];
return primera + '
' + ultima;
}
function limpiarTituloComparativa(nombre) {
if (!nombre) return '';
return String(nombre).replace(/<[^>]*>/g, '');
}
function addYAxis(chartRef, yAxisOptions) {
chartRef.addAxis(yAxisOptions, false, false);
}
function getSeriesIndexByName(seriesArr, name) {
for (var k = 0; k < seriesArr.length; k++) {
if (seriesArr[k].name === name) return k;
}
return -1;
}
// -------------------------
// Tabla optimizada (Map + chunks)
// -------------------------
function drawTableComparativa(variables, onDone) {
var theadEl = document.querySelector('#tabla-grafica-comparativa thead #tabla-grafica-comparativa-header');
var tbodyEl = document.querySelector('#tabla-grafica-comparativa tbody');
if (!theadEl || !tbodyEl) {
if (typeof onDone === 'function') onDone();
return;
}
if (!variables || !variables.length) {
theadEl.innerHTML = '';
tbodyEl.innerHTML = '';
if (typeof onDone === 'function') onDone();
return;
}
// CABECERAS
var headerHtml = '';
headerHtml += '
FECHA | ';
headerHtml += 'HORA | ';
variables.forEach(function (v) {
var nombre = v[0] || '';
var nombreHTML = formatearCabeceraComparativa(nombre);
var titulo = limpiarTituloComparativa(nombre);
headerHtml +=
'' +
nombreHTML +
' | ';
});
theadEl.innerHTML = headerHtml;
// Serie -> Map(ts -> val) + unión timestamps
var seriesMaps = [];
var timestampsSet = new Set();
for (var i = 0; i < variables.length; i++) {
var arr = variables[i][1] || [];
var m = new Map();
for (var j = 0; j < arr.length; j++) {
var p = arr[j];
if (!p) continue;
m.set(p[0], p[1]);
timestampsSet.add(p[0]);
}
seriesMaps.push(m);
}
var timestamps = Array.from(timestampsSet).sort(function (a, b) { return b - a; });
tbodyEl.innerHTML = '';
var pad2 = function (n) { return (n < 10 ? '0' + n : '' + n); };
var CHUNK = 300;
var idx = 0;
var ultimoTimestampConDatos = null;
function renderChunk() {
var end = Math.min(idx + CHUNK, timestamps.length);
var html = '';
for (; idx < end; idx++) {
var ts = timestamps[idx];
var valores = new Array(seriesMaps.length);
var tieneDatos = false;
for (var s = 0; s < seriesMaps.length; s++) {
var v = seriesMaps[s].get(ts);
valores[s] = (v === null || typeof v === 'undefined') ? null : v;
if (valores[s] !== null) tieneDatos = true;
}
if (tieneDatos) {
ultimoTimestampConDatos = ts;
} else if (ultimoTimestampConDatos !== null && ts > ultimoTimestampConDatos) {
for (var t = 0; t < valores.length; t++) valores[t] = null;
}
var fechaHora = new Date(ts);
var hora =
pad2(fechaHora.getHours()) + ':' +
pad2(fechaHora.getMinutes());
var fecha =
pad2(fechaHora.getDate()) + '-' +
pad2(fechaHora.getMonth() + 1) + '-' +
fechaHora.getFullYear();
var row =
'' +
'| ' + fecha + ' | ' +
'' + hora + ' | ';
for (var c = 0; c < valores.length; c++) {
var out = '-';
var vv = valores[c];
if (vv !== null && typeof vv !== 'undefined') {
var num = Number(vv);
out = Number.isFinite(num) ? num.toFixed(2) : '-';
}
row += '' + out + ' | ';
}
row += '
';
html += row;
}
tbodyEl.insertAdjacentHTML('beforeend', html);
if (idx < timestamps.length) {
requestAnimationFrame(renderChunk);
} else if (typeof onDone === 'function') {
onDone();
}
}
requestAnimationFrame(renderChunk);
}
function hideSpinnersAsync() {
requestAnimationFrame(function () {
$('.datos-grafica-comparativa .spinner-tabla-overlay').hide();
$('.contenedor-spinner').hide();
});
}
function buildTableDeferred(variables) {
var run = function () {
drawTableComparativa(variables, hideSpinnersAsync);
};
if ('requestIdleCallback' in window) {
window.requestIdleCallback(run, { timeout: 1500 });
} else {
setTimeout(run, 0);
}
}
// -------------------------
// extractData optimizado (binary search, solo rango)
// -------------------------
function lowerBoundByX(data, x) {
var lo = 0, hi = data.length;
while (lo < hi) {
var mid = (lo + hi) >> 1;
if (data[mid][0] < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
function upperBoundByX(data, x) {
var lo = 0, hi = data.length;
while (lo < hi) {
var mid = (lo + hi) >> 1;
if (data[mid][0] <= x) lo = mid + 1;
else hi = mid;
}
return lo;
}
function extractData(fecha_inicio, fecha_fin, list_precipitacion, list_precipitacion_acumulada) {
if (!chart) return;
for (var j = 0; j < (list_precipitacion || []).length; j++) {
var elemento_lista_preci = list_precipitacion[j];
var elemento_lista_preci_ac = (list_precipitacion_acumulada || [])[j];
if (!elemento_lista_preci || !elemento_lista_preci_ac) continue;
var data = elemento_lista_preci.data || [];
var data_pa = elemento_lista_preci_ac.data || [];
if (!data.length || !data_pa.length) continue;
// último punto de la acumulada
ultimo_elemento_pa = data_pa[data_pa.length - 1];
var fecha_inicial = data[0][0];
// rango visible en "data"
var start = lowerBoundByX(data, fecha_inicio);
var endExclusive = upperBoundByX(data, fecha_fin);
var end = endExclusive - 1;
var suma = [];
suma.push([fecha_inicial, 0]);
if (start <= end) {
var primer_elemento = data[start];
suma.push([primer_elemento[0], 0]);
for (var idx = start + 1; idx <= end; idx++) {
// reproduce la lógica original: el 2º punto suma sobre el valor del primero
var prev = (idx === start + 1) ? primer_elemento[1] : suma[suma.length - 1][1];
suma.push([data[idx][0], data[idx][1] + prev]);
}
}
// cierre como tu código: pone el último timestamp de la serie acumulada y el acumulado final
var fecha_fin_calc = ultimo_elemento_pa[0];
var aux = suma[suma.length - 1];
suma.push([fecha_fin_calc, aux ? aux[1] : 0]);
var auxName = String(elemento_lista_preci.name || '').replace("horaria (mm/h)", "acumulada (mm)");
var indexAcum = getSeriesIndexByName(chart.series, auxName);
if (indexAcum >= 0 && chart.series[indexAcum]) {
chart.series[indexAcum].update({
data: suma,
selected: chart.series[indexAcum].selected
}, false);
}
}
chart.redraw();
}
// -------------------------
// Highcharts options
// -------------------------
var extremesTimer = null;
var options = {
chart: {
zoomType: 'x',
alignTicks: false,
boost: {
enabled: true,
useGPUTranslations: true,
usePreallocated: true
},
animation: false
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { day: '%e %b' },
title: { text: 'Fecha' },
events: {
afterSetExtremes: function (e) {
if (!precipitacionAcumulada) return;
clearTimeout(extremesTimer);
extremesTimer = setTimeout(function () {
extractData(e.min, e.max, data_precipitacion, data_precipitacion_acum);
}, 120);
}
},
ordinal: false
},
plotOptions: {
series: {
showInNavigator: true,
showCheckbox: true,
turboThreshold: 0,
boostThreshold: 2000,
animation: false,
events: {
checkboxClick: function () {
if (this.visible) this.hide();
else this.show();
},
legendItemClick: function (e) {
var chartRef = e.target.chart,
index = e.target.index;
if (chartRef.series[index] && chartRef.series[index].checkbox) {
chartRef.series[index].checkbox.checked = this.selected = !this.visible;
}
}
},
gapUnit: 'relative',
gapSize: 1
}
},
column: {
grouping: false,
shadow: false
},
tooltip: {
pointFormat: '{series.name}: {point.y}
',
headerFormat: '{point.key}
',
split: false,
shared: true
},
legend: {
enabled: true,
layout: 'horizontal',
align: 'center',
x: 0,
floating: false,
backgroundColor: '#ffffff',
useHTML: true,
itemStyle: { color: 'black', 'text-decoration': 'none' },
itemHoverStyle: { color: '#022a38' },
itemHiddenStyle: { 'text-decoration': 'none' }
},
rangeSelector: {
enabled: true,
buttonTheme: {
fill: 'none',
stroke: 'none',
'stroke-width': 0,
r: 8,
style: { color: '#0067b2', fontWeight: 'bold' },
states: {
hover: {},
select: { fill: '#0067b2', style: { color: 'white' } }
}
},
inputBoxWidth: 105,
inputBoxHeight: 18,
inputDateFormat: '%d/%m/%Y %H:%M',
inputEditDateFormat: '%d/%m/%Y %H:%M',
inputStyle: { color: '#0067b2', fontWeight: 'bold' },
inputPosition: { align: 'left', x: 90, y: 0 },
buttonPosition: { align: 'left', x: 0, y: 0 },
labelStyle: { color: '#212529', fontWeight: 'bold' },
buttons: [{
type: 'hour',
count: 1,
text: '1 hora',
title: 'Última hora'
}, {
type: 'day',
count: 1,
text: '1 día',
title: 'Último día'
}, {
type: 'week',
count: 1,
text: '1 semana',
title: 'Última semana'
}, {
type: 'all',
text: '1 Mes',
title: 'Último mes'
}]
},
exporting: {
buttons: {
contextButton: {
menuItems: ["viewFullscreen", "separator", "downloadCSV", "downloadXLS"]
}
},
filename: 'comparativa'
},
navigation: {
buttonOptions: {
height: 40,
width: 40,
symbolSize: 20,
symbolX: 19,
symbolY: 17,
symbolStrokeWidth: 3,
useHTML: true,
align: 'right',
x: -80,
y: -7
}
},
series: []
};
// -------------------------
// success
// -------------------------
function success(data) {
var series_name;
var series_color;
var series_opposite = true;
var indice_nivel;
var indice_nivel_piezos;
var indice_caudal;
var visible = false;
var label_format;
// reset tabla + precip
variablesTabla = [];
data_precipitacion = [];
data_precipitacion_acum = [];
series_graph = [{}];
options.series = [];
// Construimos series_graph y variablesTabla
$.each(data, function (i, item) {
var newseries = {};
newseries.name = item.name;
newseries.data = item.data;
newseries.type = item.type;
newseries.color = item.color;
newseries.tooltip = item.tooltip;
newseries.visible = item.visible;
newseries.selected = item.selected;
newseries.showInLegend = item.showInLegend;
newseries.lineWidth = 2;
newseries.pointWidth = 10;
newseries.pointPadding = 3;
newseries.groupPadding = 0.1;
series_graph.push(newseries);
// Series a usar en la tabla (sin umbrales / navigator / mínimo ecológico)
var nombreSerieLower = (item.name || '').toLowerCase();
if (
!nombreSerieLower.includes('umbral') &&
!nombreSerieLower.includes('navigator') &&
!nombreSerieLower.includes('mínimo ecológico')
) {
variablesTabla.push([item.name, item.data]);
}
});
// Prepara precipitaciones + series
precipitacion = false;
precipitacionAcumulada = false;
for (var i = 1; i < series_graph.length; i++) {
if (String(series_graph[i].name || '').includes('Precipitación horaria (mm/h)')) {
data_precipitacion.push(series_graph[i]);
}
if (String(series_graph[i].name || '').includes('Precipitación acumulada')) {
data_precipitacion_acum.push(series_graph[i]);
precipitacionAcumulada = true;
}
options.series.push(series_graph[i]);
}
// Crear chart
chart = new Highcharts.stockChart("contenedor-grafica-formu", options);
window.__chartComparativa = chart;
var contadorN = 0;
var contadorC = 0;
var contadorP = 0;
var contadorPA = 0;
var contadorT = 0;
var contadorNE = 0;
var contadorCP = 0;
var contadorHP = 0;
for (var j = 0; j < chart.series.length; j++) {
label_format = "{value:.1f}";
series_name = chart.series[j].name;
series_color = chart.series[j].color;
visible = chart.series[j].visible;
if (
!String(series_name).toLowerCase().includes("umbral") &&
!String(series_name).toLowerCase().includes("navigator") &&
!String(series_name).toLowerCase().includes("ecológico")
) {
switch (true) {
case series_name.includes("Nivel") &&
!series_name.includes("NEMBA") &&
!series_name.includes("Embalse") &&
!series_name.includes("HP"):
indice_nivel = 1;
series_opposite = false;
label_format = "{value:.2f}";
if (contadorN === 0) {
addYAxis(chart, {
id: 'y-axis-' + 1,
title: {
text: 'Nivel (m)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 1,
visible: true,
selected: true,
dataGrouping: { enabled: false }
}, false);
contadorN++;
break;
case series_name.includes("HP"):
indice_nivel_piezos = j;
series_opposite = false;
label_format = "{value:.2f}";
if (contadorHP === 0) {
addYAxis(chart, {
id: 'y-axis-' + 1,
title: {
text: 'Profundidad piezométrica (m)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 1,
visible: true,
selected: true,
gapUnit: 'relative',
gapSize: 20000,
dataGrouping: { enabled: false },
name: series_name.replace('- HP', '')
}, false);
contadorHP++;
break;
case series_name.includes("Caudal"):
indice_caudal = 2;
series_opposite = false;
label_format = "{value:.2f}";
if (contadorC === 0) {
addYAxis(chart, {
id: 'y-axis-' + 2,
title: {
text: 'Caudal (m3/s)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 2,
visible: true,
selected: true,
dataGrouping: { enabled: false }
}, false);
contadorC++;
break;
case series_name.includes("acumulada"):
precipitacionAcumulada = true;
if (contadorPA === 0) {
addYAxis(chart, {
id: 'y-axis-' + 5,
title: {
text: 'Precipìtación acumulada (mm/h)',
style: { color: series_color, fontWeight: 'bold' }
},
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 5,
visible: true,
selected: true,
dataGrouping: { enabled: false }
}, false);
contadorPA++;
break;
case series_name.includes("horaria"):
if (visible) {
precipitacion = true;
if (contadorP === 0) {
addYAxis(chart, {
id: 'y-axis-' + 3,
title: {
text: 'Precipìtación horaria (mm/h)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 3,
visible: visible,
selected: true,
dataGrouping: { enabled: false }
}, false);
contadorP++;
}
break;
case series_name.includes("Temperatura"):
if (contadorT === 0) {
addYAxis(chart, {
id: 'y-axis-' + 4,
title: {
text: 'Temperatura (ºC)',
style: { color: series_color, fontWeight: 'bold' }
},
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
if (!series_name.includes("WT")) {
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 4,
visible: true,
selected: true,
dataGrouping: { enabled: false }
}, false);
} else {
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 4,
visible: true,
selected: true,
gapUnit: 'relative',
gapSize: 20000,
dataGrouping: { enabled: false },
name: series_name.replace('- WT', '')
}, false);
}
contadorT++;
break;
case series_name.includes("Embalse") || series_name.includes("NEMBA"):
series_opposite = false;
if (contadorNE === 0) {
addYAxis(chart, {
id: 'y-axis-' + 6,
title: {
text: 'Nivel embalse (msnm)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 6,
visible: true,
selected: true,
dataGrouping: { enabled: false }
}, false);
contadorNE++;
break;
case series_name.includes("Cota"):
series_opposite = false;
label_format = "{value:.2f}";
if (contadorCP === 0) {
addYAxis(chart, {
id: 'y-axis-' + 6,
title: {
text: 'Cota piezométrica (msnm)',
style: { color: series_color, fontWeight: 'bold' }
},
opposite: series_opposite,
gridLineWidth: 0,
labels: {
style: { color: series_color, fontWeight: 'bold' },
format: label_format
},
showEmpty: false
});
}
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 6,
visible: true,
selected: true,
gapUnit: 'relative',
gapSize: 20000,
dataGrouping: { enabled: false },
name: series_name.replace('- CP', '')
}, false);
contadorCP++;
break;
}
} else {
// Series de umbral / mínimo ecológico
var low = String(series_name).toLowerCase();
if (low.includes("umbral")) {
if (low.includes("nivel")) {
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + indice_nivel,
visible: false,
selected: false,
gapUnit: 'relative',
gapSize: 200
}, false);
} else if (low.includes("caudal")) {
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + indice_caudal,
visible: false,
selected: false,
gapUnit: 'relative',
gapSize: 200
}, false);
}
} else if (low.includes("mínimo ecológico")) {
chart.series[j].update({
index: j,
yAxis: 'y-axis-' + 2,
visible: false,
selected: false,
dataGrouping: { enabled: false }
}, false);
}
}
series_opposite = true;
} // fin for series
// Redibuja todo de una vez
chart.redraw();
// Tabla (diferida) + spinners
buildTableDeferred(variablesTabla);
} // fin success
// -------------------------
// AJAX
// -------------------------
$.ajax({
url: constantes.ajax_url,
type: 'POST',
data: {
action: 'estaciones_grafica_comparativa',
cadena: urlRecibida
},
success: function (response) {
if (response && response.success) {
success(response.data);
} else {
console.log('Error en la petición: ', response ? response.data : response);
hideSpinnersAsync();
}
},
error: function (xhr, textStatus, errorThrown) {
console.log('Error en la comunicación con el servidor:', xhr.status, textStatus, errorThrown);
hideSpinnersAsync();
}
});
}); // fin ready
})(jQuery);
};