/* 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); };