1(function($) { 2 function waitForHighcharts(callback, timeout = 100) { 3 if (typeof Highcharts !== 'undefined' && typeof Highcharts.stockChart === 'function') { 4 callback(); 5 } else { 6 setTimeout(() => waitForHighcharts(callback, timeout), timeout); 7 } 8 } 9 10 $(window).on('load', function() { 11 waitForHighcharts(function() { 12 const $riskSelector = $('#stats_mutual_risk'); 13 const $sizeSelector = $('#stats_mutual_size'); 14 const $chartContainers = $('.risk-chart'); 15 16 function parseDateString(dateStr) { 17 // For YYYYMMDD 18 if (/^\d{8}$/.test(dateStr)) { 19 const year = dateStr.substring(0, 4); 20 const month = dateStr.substring(4, 6); 21 const day = dateStr.substring(6, 8); 22 return Date.UTC(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10)); 23 } 24 // For YYYY-MM-DD 25 if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { 26 const [year, month, day] = dateStr.split('-'); 27 return Date.UTC(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10)); 28 } 29 return null; 30 } 31 32 function initializeCharts() { 33 $chartContainers.each(function() { 34 const $container = $(this); 35 36 // Destroy any existing chart instance in THIS specific container 37 const existingChart = $container.data('chart'); 38 if (existingChart) { 39 existingChart.destroy(); 40 $container.removeData('chart'); 41 } 42 43 const chartType = $container.data('chartType'); 44 let chartTitle = ''; 45 if (chartType === 'volume') { 46 chartTitle = 'Volumen gestionado Indexa Capital'; 47 } 48 const chart = Highcharts.stockChart(this, { 49 chart: { 50 type: 'line', 51 height: 500, 52 style: { 53 fontFamily: 'Nunito Sans, sans-serif', 54 fontSize: '14px' 55 }, 56 scrollbar: { enabled: false } 57 }, 58 title: { text: chartTitle }, 59 rangeSelector: { 60 enabled: false, 61 }, 62 navigator: { 63 enabled: false 64 }, 65 scrollbar: { 66 enabled: false 67 }, 68 xAxis: { 69 type: 'datetime', 70 labels: { 71 rotation: -45, 72 step: 1, 73 autoRotation: false, 74 overflow: 'justify', 75 crop: false, 76 style: { 77 color: 'rgb(119, 119, 119)', 78 fontSize: '11px' 79 }, 80 formatter: function() { 81 return Highcharts.dateFormat('%d/%m/%Y', this.value); 82 } 83 }, 84 tickInterval: 90 * 24 * 3600 * 1000, // default: 90 days 85 minTickInterval: 60 * 24 * 3600 * 1000, // minimum: 60 days 86 gridLineWidth: 0, 87 lineColor: '#ffffff', 88 tickColor: '#ffffff', 89 events: { 90 afterSetExtremes: function(e) { 91 const range = e.max - e.min; 92 let newTickInterval; 93 94 if (range < 120 * 24 * 3600 * 1000) { 95 newTickInterval = 15 * 24 * 3600 * 1000; 96 } else if (range < 365 * 24 * 3600 * 1000) { 97 newTickInterval = 45 * 24 * 3600 * 1000; 98 } else { 99 newTickInterval = 120 * 24 * 3600 * 1000; 100 } 101 102 if (this.options.tickInterval !== newTickInterval) { 103 this.update({ tickInterval: newTickInterval }); 104 } 105 } 106 } 107 }, 108 yAxis: { 109 title: { text: '' }, 110 labels: { 111 align: 'left', 112 x: 0, 113 style: { 114 color: 'rgb(119, 119, 119)', 115 fontSize: '11px' 116 }, 117 formatter: function() { 118 return this.value.toFixed(0); 119 } 120 }, 121 gridLineColor: '#ffffff', 122 gridLineWidth: 0 123 }, 124 legend: { 125 enabled: true, 126 align: 'center', 127 verticalAlign: 'bottom', 128 layout: 'horizontal', 129 itemStyle: { 130 fontWeight: '400', 131 fontSize: '15px', 132 color: '#222' 133 } 134 }, 135 credits: { enabled: false }, 136 tooltip: { 137 useHTML: true, 138 shared: true, 139 xDateFormat: '%d/%m/%Y', 140 backgroundColor: '#ffffff', 141 borderColor: '#ccc', 142 borderRadius: 8, 143 shadow: true, 144 style: { 145 color: '#333', 146 fontSize: '14px' 147 }, 148 formatter: function() { 149 let s = `<span style="font-size:14px;">${Highcharts.dateFormat('%d/%m/%Y', this.x)}</span><br/>`;
150 this.points.forEach(point => { 151 let color = point.series.name === 'Cartera' ? 'rgb(35, 111, 199)' : 'rgb(237, 125, 49)'; 152 s += `<span style="color:${color};font-size:18px;">â</span> <span style="color:#666;">${point.series.name}:</span> <b style="color:#222;">${Highcharts.numberFormat(point.y, 1, ',', '.')}</b><br/>`; 153 }); 154 return s; 155 }, 156 crosshairs: false 157 }, 158 responsive: { 159 rules: [{ 160 condition: { 161 maxWidth: 600 162 }, 163 chartOptions: { 164 legend: { 165 layout: 'horizontal', 166 align: 'center', 167 verticalAlign: 'bottom' 168 }, 169 xAxis: { 170 labels: { 171 rotation: -45 172 } 173 } 174 } 175 }] 176 }, 177 series: [] 178 }); 179 $container.data('chart', chart); 180 }); 181 } 182 183 async function updateCharts(riskLevel, sizeValue) { 184 for (const container of $chartContainers.toArray()) { 185 const $container = $(container); 186 const chartType = $container.data('chartType'); 187 const chart = $container.data('chart'); 188 189 if (chart) chart.showLoading('Cargando datos...'); 190 191 try { 192 let apiUrl; 193 if (chartType === 'volume') { 194 apiUrl = `https://carlosgalan.net/wp-json/ic-api/v1/stats-volume`; 195 } else { 196 apiUrl = `https://carlosgalan.net/wp-json/ic-api/v1/stats-performance/${riskLevel}?size=${sizeValue}`; 197 } 198 const response = await fetch(apiUrl); 199 if (!response.ok) throw new Error('API error'); 200 201 const apiData = await response.json(); 202 let data, annualizedReturn, annualizedBenchmark, acumuladaReturn, acumuladaBenchmark; 203 if (chartType === 'performance') { 204 // New API structure: { series: [...], annualized_return, annualized_benchmark, acumulada_return, acumulada_benchmark } 205 data = apiData.series || []; 206 annualizedReturn = apiData.annualized_return; 207 annualizedBenchmark = apiData.annualized_benchmark; 208 acumuladaReturn = apiData.acumulada_return; 209 acumuladaBenchmark = apiData.acumulada_benchmark; 210 } else { 211 data = Array.isArray(apiData) ? apiData : []; 212 } 213 if (!Array.isArray(data)) { 214 if (chart) chart.hideLoading(); 215 return; 216 } 217 218 // Remove any previous badges 219 $container.prev('.risk-annualized-badges').remove(); 220 221 // Add badges for annualized and acumulada returns (performance only) 222 if (chartType === 'performance') { 223 const badgeRow = $(` 224 <div class="risk-annualized-badges minimalist-badges"> 225 <table class="risk-badges-table" style="width:100%; border-collapse:separate; border-spacing:0 4px;"> 226 <thead> 227 <tr> 228 <th style="text-align:left;"></th> 229 <th class="badge-metric-label" style="text-align:right;">Rentabilidad neta Anualizada (%)</th> 230 <th class="badge-metric-label" style="text-align:right;">Rentabilidad neta Acumulada (%)</th> 231 </tr> 232 </thead> 233 <tbody> 234 <tr> 235 <td class="badge-label cartera" style="font-weight:600; color:#236FC7;">Cartera</td> 236 <td class="badge-value cartera" style="color:#236FC7; font-weight:600; border-radius:4px; text-align:right;"> 237 ${annualizedReturn.toFixed(1).replace('.', ',')} 238 </td> 239 <td class="badge-value cartera" style="color:#236FC7; font-weight:600; border-radius:4px; text-align:right;"> 240 ${acumuladaReturn.toFixed(1).replace('.', ',')} 241 </td> 242 </tr> 243 <tr> 244 <td class="badge-label benchmark" style="font-weight:600; color:#ED7D31;">Benchmark</td> 245 <td class="badge-value benchmark" style="color:#ED7D31; font-weight:600; border-radius:4px; text-align:right;"> 246 ${annualizedBenchmark.toFixed(1).replace('.', ',')} 247 </td> 248 <td class="badge-value benchmark" style="color:#ED7D31; font-weight:600; border-radius:4px; text-align:right;"> 249 ${acumuladaBenchmark.toFixed(1).replace('.', ',')} 250 </td> 251 </tr> 252 </tbody> 253 </table> 254 </div> 255 `); 256 $container.before(badgeRow); 257 } 258 259 let seriesData = []; 260 261 if (chartType === 'performance') { 262 seriesData = [ 263 { 264 name: 'Cartera', 265 color: '#236FC7', 266 lineWidth: 2, 267 marker: { enabled: false }, 268 data: data.map(p => [ 269 parseDateString(p.date), 270 typeof p.return === 'string' 271 ? parseFloat(p.return.replace(',', '.')) 272 : (typeof p.return === 'number' ? p.return : 0) 273 ]) 274 }, 275 { 276 name: 'Benchmark', 277 color: '#ED7D31', 278 lineWidth: 2, 279 marker: { enabled: false }, 280 data: data.map(p => [ 281 parseDateString(p.date), 282 typeof p.benchmark === 'string' 283 ? parseFloat(p.benchmark.replace(',', '.')) 284 : (typeof p.benchmark === 'number' ? p.benchmark : 0) 285 ]) 286 } 287 ]; 288 } else if (chartType === 'volume') { 289 seriesData = [{ 290 name: 'Volumen', 291 color: '#236FC7', 292 lineWidth: 3, 293 marker: { enabled: false }, 294 data: data.map(p => [parseDateString(p.date), parseFloat((p.volume || '0').replace(',', '.'))]) 295 }]; 296 297 // Calculate min and max values from series data 298 let minValue = null; 299 let maxValue = null; 300 seriesData.forEach(function(series) { 301 if (series.data && series.data.length > 0) { 302 series.data.forEach(function(point) { 303 const value = Array.isArray(point) ? point[1] : (typeof point === 'object' ? point.y : point); 304 if (value !== null && !isNaN(value)) { 305 if (minValue === null || value < minValue) minValue = value; 306 if (maxValue === null || value > maxValue) maxValue = value; 307 } 308 }); 309 } 310 }); 311 const minPadding = minValue !== null ? minValue * 0.97 : null; 312 const maxPadding = maxValue !== null ? maxValue * 1.01 : null; 313 314 // Configure tooltip with enhanced formatting 315 chart.update({ 316 tooltip: { 317 formatter: function() { 318 var tooltipHTML = ''; 319 var points = this.points || [this.point];
320 points.forEach(function(point) { 321 if (point && point.y !== undefined) { 322 // Format the date for each point 323 tooltipHTML += '<span>' + Highcharts.dateFormat('%d/%m/%Y', point.x) + '</span><br/>'; 324 // Convert value to millions 325 var valueInMillions = point.y / 1000000; 326 // Format the value using Highcharts.numberFormat 327 const formattedValue = Highcharts.numberFormat(valueInMillions, 0, ',', '.'); 328 // Append formatted data to tooltipHTML 329 tooltipHTML += '<span style="color:' + point.color + '">â</span> ' + 330 point.series.name + ': <b>' + formattedValue + ' Mâ¬' + '</b><br/>'; 331 } 332 }); 333 return tooltipHTML; 334 }, 335 shared: true, 336 enabled: true, 337 useHTML: true 338 }, 339 colors: ['#236FC7', '#ED7D31'], 340 yAxis: { 341 title: { text: null }, 342 labels: { 343 formatter: function () { 344 return (this.value / 1000000).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ".") + ' Mâ¬'; 345 } 346 }, 347 startOnTick: false, 348 endOnTick: false, 349 min: minPadding, 350 max: maxPadding, 351 tickAmount: 7 352 }, 353 plotOptions: { 354 series: { 355 marker: { enabled: false }, 356 events: { 357 legendItemClick: function() { return false; } 358 } 359 } 360 } 361 }, false); 362 } 363 364 // Start of user-provided logic adaptation 365 let minValue = null; 366 let maxValue = null; 367 368 seriesData.forEach(function(series) { 369 if (series.data && series.data.length > 0) { 370 series.data.forEach(function(point) { // point is [x, y] 371 const value = point[1]; // Y-value 372 if (value !== null && !isNaN(value)) { 373 if (minValue === null || value < minValue) { 374 minValue = value; 375 } 376 if (maxValue === null || value > maxValue) { 377 maxValue = value; 378 } 379 } 380 }); 381 } 382 }); 383 384 // Apply padding to min/max values (using your variable names for clarity) 385 const yAxisPaddedMin = minValue !== null ? minValue * 0.98 : null; 386 const yAxisPaddedMax = maxValue !== null ? maxValue * 1.01 : null;
387 // End of user-provided logic adaptation 388 389 // Clear old series 390 while (chart.series.length > 0) { 391 chart.series[0].remove(false); 392 } 393 394 // Update Y-axis extremes based on calculated padded values 395 if (typeof yAxisPaddedMin === 'number' && typeof yAxisPaddedMax === 'number' && !isNaN(yAxisPaddedMin) && !isNaN(yAxisPaddedMax)) { 396 chart.yAxis[0].setExtremes(yAxisPaddedMin, yAxisPaddedMax, false); 397 } else { 398 // Fallback to auto-scaling if calculated values are not valid 399 chart.yAxis[0].setExtremes(null, null, false); 400 } 401 402 // Add new series 403 seriesData.forEach(s => chart.addSeries(s, false)); 404 chart.redraw(); 405 406 } catch (err) { 407 console.error('Error updating chart:', err); 408 } 409 410 if (chart) chart.hideLoading(); 411 } 412 } 413 414 function setupSelector() { 415 function triggerUpdate() { 416 updateCharts($riskSelector.val(), $sizeSelector.val()); 417 } 418 if ($riskSelector.length && $sizeSelector.length) { 419 $riskSelector.on('change', triggerUpdate); 420 $sizeSelector.on('change', triggerUpdate); 421 triggerUpdate(); 422 } 423 } 424 425 function updateChartsRange() { 426 var from = $('#stats_mutual_from').val(); 427 var to = $('#stats_mutual_to').val(); 428 429 function parseDate(str) { 430 var parts = str.split('/'); 431 return Date.UTC(parts[2], parts[1] - 1, parts[0]); 432 } 433 434 var min = parseDate(from); 435 var max = parseDate(to); 436 437 $chartContainers.each(function() { 438 var chart = $(this).data('chart'); 439 if (chart) { 440 chart.xAxis[0].setExtremes(min, max); 441 } 442 }); 443 } 444 445 if ($chartContainers.length && $riskSelector.length && $sizeSelector.length) { 446 initializeCharts(); 447 setupSelector(); 448 } 449 450 $('#stats_mutual_from, #stats_mutual_to').on('change', updateChartsRange); 451 }); 452 }); 453})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.