PageSourceSearch

https://carlosgalan.net/wp-content/plugins/idx-graficos-plugin/js/risk-chart.js?ver=1.0.0

js carlosgalan.net collected 2026-10-04 01:51:01 UTC 23,886 bytes, 453 lines download raw bytes

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.