PageSourceSearch

https://goldesel.de/custom/widgets/dividends-chart-widget.js?v=oNL2pvjNG3JMfWdM0s1cvFfUaPNwW82S4w6zGUEujqA

js goldesel.de collected 2026-10-01 16:05:28 UTC 48,904 bytes, 1,034 lines download raw bytes

1// Definiere die Vue-Komponente global oder exportiere sie, wenn du ein einfaches Modul-System hast
2const DividendsChartWidget = {
3    // Der Template-String enthält das HTML der Komponente
4    template: `
5      <div class="revenue-widget">
6          <template v-if="chartApiData?.instrument?.logo">
7          <div class="logo-container">
8            <img :src="chartApiData?.instrument?.logo" alt="Logo" class="logo-image">
9            <div class="logo-text">
10               <h4>{{ chartApiData?.instrument?.name }}</h4>
11            </div>
12             <div class="dates-widget-content-details-value-link show-more-dividends-link" v-if="showMoreLink">
13                <a href="#" @click.prevent="showMoreDividends">Mehr anzeigen <i class="fa-solid fa-arrow-right"></i></a>
14            </div>
15          </div>
16          </template>
17        <div class="widget-header">
18           <h3>{{ chartApiData?.title || title || 'Unternehmens Umsatz' }}</h3>
19          <div class="info-icon-container">
20            <i class="fa-duotone fa-solid fa-circle-info" @click.stop="toggleTooltip"></i>
21            <div class="tooltip" v-if="showTooltip" v-html="infoText">
22             
23            </div>
24          </div>
25        </div>
26
27        <div class="stock-navigation">
28          <div @click="switchPeriod('yearly')"
29            :class="{ active: selectedPeriod === 'yearly', 'disabled-chart-switch': !chartAnimationComplete }">
30            Jährlich ({{dividendFrequency}}x pro Jahr)
31          </div>
32          <div  @click="switchPeriod('quarterly')"
33            :class="{ active: selectedPeriod === 'quarterly', 'disabled-chart-switch': !chartAnimationComplete }">
34                Detailliert
35          </div>
36        </div>
37
38        <div class="current-value-section" :class="{ 'current-value-section--no-dividends': !loading && !error && !hasActualDividends }">
39            <div v-if="loading" class="loading-indicator" style="color: #fff;">Laden...</div>
40            <div v-else-if="error" class="error-message" style="color: #ff4444;">{{ error }}</div>
41            <div v-else-if="!hasActualDividends" class="info-message dividends-empty-message">Keine Dividende vorhanden</div>
42            <div class="revenue-widget-formatted-display-value" v-else v-show="!chartTooltipActive" style="margin-bottom: -50px;   z-index: 9;border-radius: 6px;" :style="{marginLeft: selectedPeriod === 'yearly' ? '50px' : '20px'}">
43                <div style="font-size: 28px; color: #fff; font-weight: 700; line-height: 28px;" v-html="formattedDisplayValue"></div>
44            </div>
45        </div>
46
47        <div v-if="hasActualDividends" class="chart-container" style="height: 300px; position: relative;">
48          <canvas  ref="divCanvas"></canvas>
49        </div>
50      </div>
51    `,
52    // Props definition
53    props: {
54        isin: {
55            type: String,
56            required: true
57        },
58        title: {
59            type: String,
60            required: false,
61            default: 'Dividende pro Aktie (DPS)'
62        },
63        chartDataFromParent: {
64            type: Object,
65            required: false
66        },
67        barColorActual: {
68            type: String,
69            required: false,
70            default: '#AB6CFA'
71        },
72        barColorEstimate: {
73            type: String,
74            required: false,
75            default: 'rgba(171, 108, 250, 0.3)'
76        },
77        overideEndpoint: {
78            type: String,
79            required: false,
80            default: null
81        },
82        formattedDisplayBackgroundColor: {
83            type: String,
84            required: false,
85            default: null
86        },
87        showMoreLink: {
88            type: Boolean,
89            required: false,
90            default: false
91        },
92        currentPrice: {
93            type: Number,
94            required: false,
95            default: 0
96        },
97        userLoggedIn: {
98            type: Boolean,
99            required: false,
100            default: false
101        },
102    },
103    // Daten der Komponente
104    data() {
105        return {
106            chartApiData: null,
107            selectedPeriod: 'yearly',
108            currency: 'USD',
109            loading: true,
110            dataFromAPI: false,
111            uniqueLabels: [],
112            dividendFrequency: 0,
113            field: 'amount',
114            error: null,
115            API_ENDPOINT: '/Aktien/Api/GetFundamentals/V2',
116            GRID_COLOR: 'rgba(138, 135, 176, 0.50)',
117            TEXT_COLOR: 'rgba(255, 255, 255, 0.50)',
118            isVisible: true,
119            showTooltip: false,
120            chartAnimationComplete: false,
121            infoText: `Dieser Chart zeigt den ${this.title} über die Zeit. Die vollen Balken zeigen tatsächliche Werte, während die abgeschwächten Balken Schätzungen darstellen.`,
122            estimateOverlayEl: null,
123            chartTooltipActive: false
124        };
125    },
126
127
128    // Berechnete Eigenschaften
129    computed: {
130        hasActualDividends() {
131            if (!this.chartApiData) return false;
132            if (this.chartApiData.stockDividends?.length > 0) return true;
133            const annual = this.chartApiData.annualDividends?.dividends;
134            return annual?.some(d => d && d.type !== 'Estimates' && (d.amount ?? 0) > 0) ?? false;
135        },
136        currentChartData() {
137            if (!this.chartApiData?.stockDividends && !this.chartApiData?.annualDividends?.dividends) return [];
138
139            if (!this.hasActualDividends) {
140                this.dividendFrequency = 0;
141                return [];
142            }
143
144            const data = this.selectedPeriod === 'yearly'
145                ? this.chartApiData.annualDividends?.dividends
146                : this.chartApiData.stockDividends;
147
148            if (!data) return [];
149
150
151
152            this.currency = this.chartApiData.stockDividends[0]?.currency || this.chartApiData.mixedFundamentals.currency;
153            this.dividendFrequency = this.chartApiData.annualDividends?.dividendFrequency;
154
155            const currentYear = new Date().getFullYear();
156            const effectiveCurrentPrice = this.resolvedCurrentPrice;
157            let returnData = data
158                .filter(item => item && (this.selectedPeriod === 'yearly' ? item.year : item.date) && item[this.field] != null)
159                .sort((a, b) => {
160                    const aValue = this.selectedPeriod === 'yearly' ? a.year : a.date;
161                    const bValue = this.selectedPeriod === 'yearly' ? b.year : b.date;
162                    if (!aValue || !bValue) return 0;
163                    //return aValue.toString().localeCompare(bValue.toString());
164                    if (this.selectedPeriod === 'yearly') {
165                        return parseInt(aValue) - parseInt(bValue);
166                    } else {
167                        return new Date(aValue).getTime() - new Date(bValue).getTime();
168                    }
169                })
170                .map(item => {
171                    let dividendYield = item.dividendYield || null;
172                    //window.log("item current year",  currentYear, dividendYield, item.year, this.dividend, item);
173                    if (item.year == currentYear && dividendYield == null) {
174                      //  window.log("item current year",  currentYear, dividendYield, this.dividend);
175                        dividendYield = this.dividend;
176                    }
177                    if (item.type === 'Estimates') {
178                        if (effectiveCurrentPrice > 0) {
179                            dividendYield = (item.amount / effectiveCurrentPrice);
180                           
181                        } else {
182                            dividendYield = null;
183                        }
184                    }
185                    return {
186                        period: this.selectedPeriod === 'yearly' ? item.year : item.date,
187                        calendarYear: this.selectedPeriod === 'yearly' ? item.year : new Date(item.date).getFullYear(),
188                        value: (item[this.field] || 0),
189                        dividendYield: dividendYield,
190                        isEstimate: item.type === 'Estimates' || false
191                    };
192                });
193
194            if (this.selectedPeriod === 'yearly') {
195                // reomove Date before current year - 5 
196                returnData = returnData.filter(item => item.period >= new Date().getFullYear() - 5);
197
198
199                // filter estimate data with calendarYear < currentyear   
200                returnData = returnData.filter(item => (item.calendarYear >= new Date().getFullYear() && item.isEstimate) || !item.isEstimate);
201                if (this.dividendFrequency == 1) {
202                    // if dividend for current year is available, remove estimate data for current year
203                    const hasData = returnData.some(item => item.period === new Date().getFullYear() && !item.isEstimate);
204                    if (hasData) {
205                        returnData = returnData.filter(item => (item.calendarYear > new Date().getFullYear() && item.isEstimate) || !item.isEstimate);
206                    }
207                }
208
209            }
210            if (this.selectedPeriod === 'quarterly') {
211                // reomove Date before current year - 5 
212                returnData = returnData.filter(item => item.calendarYear >= new Date().getFullYear() - 2);
213                returnData = returnData.filter(item => item.calendarYear <= new Date().getFullYear() + 1);
214                if (this.dividendFrequency != 4) {
215                    // remove estimate data
216                    returnData = returnData.filter(item => !item.isEstimate);
217                }
218            }
219
220            // filter out estimate data if year is also in actual data
221            const uniqueNonEstimates = {};
222            for (const item of returnData) {
223                const period = item.period; // Using 'period' as the key
224
225                if (item.isEstimate && uniqueNonEstimates[period] && !uniqueNonEstimates[period].isEstimate) {
226                    continue;
227                }
228                uniqueNonEstimates[period] = item;
229            }
230            const finalResult = Object.values(uniqueNonEstimates)
231                .sort((a, b) => {
232                    if (this.selectedPeriod === 'yearly') {
233                        return parseInt(a.period, 10) - parseInt(b.period, 10);
234                    }
235                    return new Date(a.period).getTime() - new Date(b.period).getTime();
236                });
237
238            this.uniqueLabels = finalResult.map(item => item.period + (item.isEstimate ? 'e' : ''));
239
240            // window.log("computed currentChartData()", returnData, this.isin, this.chartApiData.stockDividends[0].currency, this.uniqueLabels);
241            return finalResult;
242        },
243        resolvedCurrentPrice() {
244            if (this.currentPrice > 0) return this.currentPrice;
245            const fromInstrument = this.chartApiData?.instrument?.priceLast;
246            if (fromInstrument > 0) return fromInstrument;
247            const fromLatest = this.chartApiData?.instrument?.prices?.[0]?.latest?.price;
248            if (fromLatest > 0) return fromLatest;
249            return 0;
250        },
251        dividend() {
252            let dividendBasis = this.chartApiData?.profile.price;
253            let lastDiv = this.chartApiData?.profile?.lastDiv;
254            let dividend = 0;
255            if (lastDiv == 0) {
256                return 0;
257            }
258
259            const instrumentPrice = this.resolvedCurrentPrice;
260            // Prefer home-market instrument price over OTC FMP profile.price (often USD ADR).
261            if (instrumentPrice > 0) {
262                dividendBasis = instrumentPrice;
263            }
264
265            if (dividendBasis && dividendBasis > 0 && lastDiv) {
266                let _openPrice = this.chartApiData?.instrument?.prices
267                    && this.chartApiData?.instrument?.prices.length > 0
268                    && this.chartApiData?.instrument?.prices && this.chartApiData?.instrument?.prices[0] && this.chartApiData?.instrument?.prices[0].first
269                    ? this.chartApiData?.instrument?.prices[0].first.price
270                    : 0;
271                if (!_openPrice ||_openPrice == 0) {
272                    _openPrice = dividendBasis;
273                }
274    
275                if (this.chartApiData?.profile.currency == "EUR" || instrumentPrice > 0) {
276                    if (_openPrice == 0) {
277                        dividend = (this.chartApiData?.profile.lastDiv / dividendBasis );
278                    } else {
279                        dividendBasis = _openPrice;
280                        dividend = (this.chartApiData?.profile.lastDiv / _openPrice );
281                    }
282                } else {
283                    dividend = (this.chartApiData?.profile.lastDiv / this.chartApiData?.profile.price);
284                }
285            }
286            return dividend;
287        },
288        formattedDisplayValue() {
289            if (!this.chartApiData?.annualDividends) return '0,00';
290
291            const data = this.selectedPeriod === 'yearly'
292                ? this.chartApiData.annualDividends?.dividends
293                : this.chartApiData.stockDividends;
294
295            if (!data || data.length === 0) return '0,00';
296
297            // Get the most recent actual (non-estimate) value, shold come from 
298            let actualData = data.filter(d => d && d.type != 'Estimates');
299            if (actualData.length === 0) return '0,00';
300
301            actualData = actualData.sort((a, b) => {
302                const aValue = this.selectedPeriod === 'yearly' ? a.year : a.date;
303                const bValue = this.selectedPeriod === 'yearly' ? b.year : b.date;
304                if (!aValue || !bValue) return 0;
305
306                // this.selectedPeriod === 'yearly'  -  sort by year as number
307                // this.selectedPeriod === 'quarterly'  -  sort by date as date
308                if (this.selectedPeriod === 'yearly') {
309                    return parseInt(aValue) - parseInt(bValue);
310                } else {
311                    return new Date(aValue).getTime() - new Date(bValue).getTime();
312                }
313            })
314            const latestValue = actualData[actualData.length - 1][this.field];
315            let returnValue = `${this.formatPriceComact(latestValue, this.currency)} ${this.currency}`;
316            if (this.selectedPeriod === 'yearly') {
317                returnValue += `<br> <small style="font-size: 13px; color: rgba(255, 255, 255,0.6); font-weight: 400;">bestätigte Dividende für ${new Date().getFullYear()}</small>`;
318            }
319            return returnValue;
320        },
321
322        estimatePeriods() {
323            const estimatePeriods = new Set();
324            if (this.currentChartData) {
325                this.currentChartData.forEach(item => {
326                    if (item.isEstimate) {
327                        estimatePeriods.add(String(item.period));
328                    }
329                });
330            }
331            return estimatePeriods;
332        },
333    },
334    // Methoden der Komponente
335    methods: {
336        async fetchData() {
337            if (this.chartDataFromParent) {
338                this.chartApiData = this.chartDataFromParent;
339                this.loading = false;
340                this.error = null;
341                return; // Skip API call if we have parent data
342            }
343
344            this.loading = true;
345            this.error = null;
346            try {
347                const response = await fetch(`${this.API_ENDPOINT}?isin=${this.isin}`);
348                if (!response.ok) {
349                    throw new Error(`HTTP error! status: ${response.status}`);
350                }
351                const data = await response.json();
352                this.dataFromAPI = true;
353                this.chartApiData = data;
354            } catch (err) {
355                console.error("Error fetching chart data:", err);
356                this.error = "Daten konnten nicht geladen werden.";
357                this.chartApiData = null;
358            } finally {
359                this.loading = false;
360            }
361        },
362        formatPriceComact(value, currency = 'USD', format = 'de-DE', minimumFractionDigits = 2, maximumFractionDigits = 2) {
363            if (typeof value !== "number") {
364                return value;
365            }
366            var formatter = new Intl.NumberFormat(format, {
367                notation: 'compact', currency: currency, minimumFractionDigits: minimumFractionDigits, maximumFractionDigits: maximumFractionDigits, compactDisplay: 'short'
368            });
369            return formatter.format(value);
370        },
371        showMoreDividends() {
372            // send info to parent component
373            //this.$emit('showMoreEvents', this.chartApiData, );
374            this.$emit('showMoreDividends', true);
375        },
376        getSegmentStyle(ctx, solidColor, transparentColor) {
377            const x0 = ctx.p0.parsed.x;
378            const x1 = ctx.p1.parsed.x;
379
380            const allLabels = this.chartInstance.data.labels;
381            if (x0 < 0 || x0 >= allLabels.length || x1 < 0 || x1 >= allLabels.length) {
382                // Return a default style if indices are out of bounds
383                return {
384                    borderColor: solidColor,
385                    borderDash: undefined
386                };
387            }
388            const label0 = allLabels[x0].replace('e', '');
389            const label1 = allLabels[x1].replace('e', '');
390
391            const isEstimateSegment = this.estimatePeriods.has(label0) || this.estimatePeriods.has(label1);
392
393            return {
394                borderColor: isEstimateSegment ? transparentColor : solidColor,
395                borderDash: isEstimateSegment ? [5, 5] : undefined
396            };
397
398        },
399        renderChart() {
400            // Only render if the widget is visible
401            if (!this.isVisible) {
402                return;
403            }
404
405            const canvas = this.$refs.divCanvas;
406            if (!canvas) {
407                setTimeout(() => this.renderChart(), 50);
408                return;
409            }
410
411            if (!this.currentChartData?.length) {
412                return;
413            }
414            const labels = this.uniqueLabels;
415            let dataValues = this.currentChartData.filter(d => !d.isEstimate).map(d => d.value);
416            dataValues = dataValues.concat(Array(labels.length - dataValues.length).fill(null));
417            let dataValuesEstimate = this.currentChartData.filter(d => d.isEstimate).map(d => d.value);
418            dataValuesEstimate = Array(labels.length - dataValuesEstimate.length).fill(null).concat(dataValuesEstimate);
419
420            // Choose the highest dividend yield per year and align to labels; values in percent
421            let dividendYieldData = [];
422            {
423                const maxByYear = {};
424                for (const d of this.currentChartData) {
425                    if (typeof d.dividendYield === 'number' && !isNaN(d.dividendYield)) {
426                        const y = d.calendarYear;
427                        const current = maxByYear[y];
428                        maxByYear[y] = (typeof current === 'number') ? Math.max(current, d.dividendYield) : d.dividendYield;
429                    }
430                }
431                dividendYieldData = labels.map(l => {
432                    const year = parseInt(String(l).replace('e', ''), 10);
433                    const v = maxByYear[year];
434                    return (typeof v === 'number') ? v * 100 : null;
435                });
436            }
437
438
439            if (this.chartInstance) {
440                this.chartInstance.data.labels = labels;
441                this.chartInstance.data.datasets[0].data = dataValues;
442                this.chartInstance.data.datasets[1].data = dataValuesEstimate;
443                this.chartInstance.data.datasets[2].data = dividendYieldData;
444
445                this.chartInstance.options.scales.x.ticks.callback = (value, index, ticks) => {
446                    if (labels && labels[index]) {
447                        if (this.selectedPeriod === 'yearly') {
448                            return labels[index];
449                        } else {
450                            return new Date(labels[index].replace('e', '')).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: 'numeric' });
451                        }
452                    }
453                    return '';
454                };
455
456                this.chartAnimationComplete = false;
457                this.chartInstance.update();
458                setTimeout(() => {
459                    this.chartAnimationComplete = true;
460                    this.updateEstimateOverlay();
461                    this.notifyHostHeight();
462                }, 700);
463            } else {
464                const ctx = canvas.getContext('2d');
465                if (!ctx) {
466                    console.error("Failed to get canvas context");
467                    return;
468                }
469
470                const vm = this;
471                const tooltipActivityPlugin = {
472                    id: 'tooltipActivityPlugin',
473                    afterEvent(chart, args) {
474                        const hasActiveTooltip = !!(chart.tooltip && chart.tooltip.getActiveElements && chart.tooltip.getActiveElements().length > 0);
475                        if (vm.chartTooltipActive !== hasActiveTooltip) {
476                            vm.chartTooltipActive = hasActiveTooltip;
477                        }
478                    }
479                };
480
481                this.chartAnimationComplete = false;
482                this.chartInstance = new Chart(ctx, {
483                    type: 'bar',
484                    plugins: [tooltipActivityPlugin],
485                    data: {
486                        labels: labels,
487                        datasets: [{
488                            label: 'Tatsächliche Dividende',
489                            data: dataValues,
490                            backgroundColor: this.barColorActual,
491                            borderColor: 'transparent',
492                            borderWidth: 0,
493                            borderRadius: 3,
494
495                            categoryPercentage: 1.0,
496                            order: 1,
497                            grouped: false
498                        },
499                        {
500                            label: 'Geschätzte Dividende',
501                            data: dataValuesEstimate,
502                            backgroundColor: this.barColorEstimate,
503                            borderColor: 'transparent',
504                            borderWidth: 0,
505                            borderRadius: 3,
506                            categoryPercentage: 1.0,
507                            order: 2,
508                            grouped: false
509                        },
510                        {
511                            type: 'line',
512                            label: 'Dividendenrendite',
513                            data: dividendYieldData,
514                            backgroundColor: '#63FFDA',
515                            borderColor: '#63FFDA',
516                            pointRadius: 0,
517                            tension: 0.4,
518                            borderWidth: this.selectedPeriod === 'yearly' ? 3 : 0,
519                            yAxisID: 'y1',
520                            order: 0,
521                            visible: this.selectedPeriod === 'yearly',
522                            segment: {
523                                borderColor: ctx => this.getSegmentStyle(ctx, '#63FFDA', 'rgba(99, 255, 218, 1)').borderColor,
524                                borderDash: ctx => this.getSegmentStyle(ctx, '#63FFDA', 'rgba(99, 255, 218, 1)').borderDash
525                            }
526                        }
527                        ]
528                    },
529                    options: {
530                        animation: {
531                            duration: 700,
532                            easing: 'easeInOutQuart',
533                            onComplete: () => {
534                                this.chartAnimationComplete = true;
535                                this.updateEstimateOverlay();
536                                this.notifyHostHeight();
537                            },
538                        },
539                        responsive: true,
540                        maintainAspectRatio: false,
541                        layout: {
542                            padding: {
543                                left: 0,
544                                right: 0,
545                                top: 0,
546                                bottom: 0
547                            }
548                        },
549                        plugins: {
550                            legend: { display: false },
551                            tooltip: {
552                                enabled: true,
553                                backgroundColor: '#1a1f37',
554                                titleColor: '#fff',
555                                bodyColor: '#fff',
556                                padding: 12,
557                                displayColors: false,
558                                callbacks: {
559                                    label: (context) => {
560                                        let label = [];
561                                        const dataIndex = context.dataIndex;
562                                        const datasets = context.chart.data.datasets;
563
564                                        datasets.forEach((dataset, i) => {
565                                            const meta = context.chart.getDatasetMeta(i);
566                                            if (!meta.hidden) {
567                                                const value = dataset.data[dataIndex];
568                                                if (value !== null && value !== undefined) {
569                                                    const isEstimateLabel = String(context.label || '').endsWith('e');
570                                                    if (!this.userLoggedIn && (dataset.label === 'Geschätzte Dividende' || (dataset.yAxisID === 'y1' && isEstimateLabel))) {
571                                                        return;
572                                                    }
573                                                    if (dataset.yAxisID === 'y1' && this.selectedPeriod === 'yearly') {
574                                                        label.push(`Divi
574dendenrendite: ${parseFloat(value).toFixed(2)}%`);
575                                                    } else if (dataset.label === 'Tatsächliche Dividende' || dataset.label === 'Geschätzte Dividende') {
576                                                        label.push(`${dataset.label}: ${this.formatPriceComact(value, this.currency)}`);
577                                                    }
578                                                }
579                                            }
580                                        });
581                                        return label;
582                                    }
583                                }
584                            }
585                        },
586                        scales: {
587                            x: {
588                                stacked: false,
589                                grid: {
590                                    display: false,
591                                    drawBorder: false
592                                },
593                                ticks: {
594                                    color: this.TEXT_COLOR,
595                                    font: {
596                                        size: 9,
597                                        family: "Inter, Roboto, 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
598                                    },
599                                    callback: (value, index, ticks) => {
600                                        if (this.selectedPeriod === 'yearly') {
601                                            return labels[index];
602                                        } else {
603                                            if (labels && labels[index]) {
604                                                return new Date(labels[index].replace('e', '')).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: 'numeric' });
605                                            }
606                                            return '';
607                                        }
608                                    }
609                                },
610                                border: {
611                                    display: false
612                                }
613                            },
614                            y: {
615                                position: 'right',
616                                align: 'end',
617                                grid: {
618                                    color: this.GRID_COLOR,
619                                    drawBorder: false,
620                                    borderDash: [5, 5],
621                                },
622                                border: {
623                                    display: false,
624                                    dash: [2, 2]
625                                },
626                                ticks: {
627                                    color: this.TEXT_COLOR,
628                                    padding: 10,
629                                    font: {
630                                        size: 10,
631                                        align: 'end',
632                                        family: "Inter, Roboto,'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"
633                                    },
634                                    callback: (value, index, ticks) => {
635                                        if (index === ticks.length - 1) {
636                                            return this.currency;
637                                        }
638                                        if (typeof value === 'number') {
639                                            return `${this.formatPriceComact(value, this.currency, 'de-DE', 1, 1)}`;
640                                        }
641                                        return value;
642                                    },
643                                    beginAtZero: false
644                                }
645                            },
646                            y1: {
647                                type: 'linear',
648                                display: this.selectedPeriod === 'yearly',
649                                position: 'left',
650                                grid: {
651                                    drawOnChartArea: false, // only want the grid lines for one axis to show up
652                                },
653                                ticks: {
654                                    color:  this.TEXT_COLOR,
655                                    callback: function (value, index, values) {
656                                        return value + '%';
657                                    }
658                                },
659                            },
660                        }
661                    }
662                });
663                this.$nextTick(() => {
664                    this.updateEstimateOverlay();
665                    this.notifyHostHeight();
666                });
667            }
668        },
669        notifyHostHeight() {
670            try {
671                if (typeof window === 'undefined' || !window.ReactNativeWebView || typeof window.ReactNativeWebView.postMessage !== 'function') {
672                    return;
673                }
674                const height = Math.max(
675                    document.body?.scrollHeight || 0,
676                    document.documentElement?.scrollHeight || 0,
677                    document.body?.offsetHeight || 0,
678                    document.documentElement?.offsetHeight || 0
679                );
680                if (height > 0) {
681                    window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'height', height }));
682                }
683            } catch (e) {
684                // ignore host notify errors in browser
685            }
686        },
687        updateEstimateOverlay() {
688            try {
689                const container = this.$el && this.$el.querySelector && this.$el.querySelector('.chart-container');
690                if (!container || !this.chartInstance) return;
691
692                const labels = (this.chartInstance.data && this.chartInstance.data.labels) || [];
693                const firstEstimateIndex = labels.findIndex(l => String(l).endsWith('e'));
694                const hasEstimates = firstEstimateIndex !== -1;
695
696                if (this.userLoggedIn || !hasEstimates) {
697                    if (this.estimateOverlayEl) {
698                        this.estimateOverlayEl.remove();
699                        this.estimateOverlayEl = null;
700                    }
701                    return;
702                }
703
704                const xScale = this.chartInstance.scales && this.chartInstance.scales.x;
705                const chartArea = this.chartInstance && this.chartInstance.chartArea;
706                if (!xScale || !chartArea) return;
707
708                let centerPixel = null;
709                if (typeof xScale.getPixelForValue === 'function') {
710                    centerPixel = xScale.getPixelForValue(firstEstimateIndex);
711                } else if (typeof xScale.getPixelForTick === 'function') {
712                    centerPixel = xScale.getPixelForTick(firstEstimateIndex);
713                }
714                if (centerPixel == null || isNaN(centerPixel)) {
715                    const meta = this.chartInstance && this.chartInstance.getDatasetMeta && this.chartInstance.getDatasetMeta(0);
716                    const elem = meta && meta.data && meta.data[firstEstimateIndex];
717                    if (elem && typeof elem.x === 'number' && !isNaN(elem.x)) {
718                        centerPixel = elem.x;
719                    }
720                }
721                if (centerPixel == null || isNaN(centerPixel)) return;
722                let halfStep = 0;
723                if (firstEstimateIndex > 0) {
724                    const prev = xScale.getPixelForTick(firstEstimateIndex - 1);
725                    if (prev != null && !isNaN(prev)) halfStep = Math.abs(centerPixel - prev) / 2;
726                } else if (labels.length > 1) {
727                    const next = xScale.getPixelForTick(1);
728                    if (next != null && !isNaN(next)) halfStep = Math.abs(next - centerPixel) / 2;
729                }
730                const startPixel = Math.max(0, Math.round(centerPixel - halfStep - 2));
731
732                if (!this.estimateOverlayEl) {
733                    const overlay = document.createElement('div');
734                    overlay.className = 'estimate-overlay';
735                    overlay.innerHTML = `
736                        <div class="estimate-overlay-box">
737                            <div class="estimate-overlay-lock">
738                                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
739                                <path d="M17.7818 9.48865C17.5165 9.22348 17.1947 9.09099 16.8157 9.09099H16.3615V6.36356C16.3615 4.62117 15.7366 3.12509 14.4866 1.87508C13.2366 0.625028 11.7407 0 9.99804 0C8.25544 0 6.75922 0.625028 5.50936 1.87503C4.2593 3.12509 3.63448 4.62112 3.63448 6.36356V9.09099H3.18C2.80134 9.09099 2.4793 9.22348 2.21412 9.48865C1.94894 9.75363 1.81641 10.0757 1.81641 10.4547V18.6364C1.81641 19.0151 1.94899 19.3372 2.21412 19.6024C2.4793 19.8674 2.80134 20 3.18 20H16.8161C17.195 20 17.5168 19.8675 17.7821 19.6024C18.047 19.3372 18.1798 19.0151 18.1798 18.6364V10.4545C18.18 10.0758 18.047 9.75383 17.7818 9.48865ZM13.6343 9.09099H6.36175V6.36356C6.36175 5.35983 6.71693 4.50276 7.42713 3.79266C8.13743 3.08245 8.99435 2.72743 9.99819 2.72743C11.0021 2.72743 11.8588 3.0824 12.5692 3.79266C13.2792 4.50271 13.6343 5.35983 13.6343 6.36356V9.09099Z" fill="#63FFDA"/>
740                                </svg>
741                            </div>
742                            <div class="estimate-overlay-text">Logge dich ein,<br/>um alle Daten zu sehen</div>
743                            <a href="#" class="estimate-overlay-login">Login</a>
744                        </div>`;
745                        
746                    container.appendChild(overlay);
747                                       //should prevent default and call 
748                    const loginButton = container.querySelector('.estimate-overlay-login');
749                    if (loginButton) {
750                        loginButton.addEventListener('click', (event) => {
751                            event.preventDefault();
752                            this.loginEvent();
753                        });
754                    }
755
756                    this.estimateOverlayEl = overlay;
757                }
758
759                // Constrain overlay to the chart drawing area so axes remain visible
760                const areaLeft = Math.round(chartArea.left);
761                const areaRight = Math.round(chartArea.right);
762                const areaTop = Math.round(chartArea.top);
763                const areaBottom = Math.round(chartArea.bottom);
764                const left = Math.max(areaLeft, startPixel);
765                const width = Math.max(0, areaRight - left);
766                const height = Math.max(0, areaBottom - areaTop);
767                this.estimateOverlayEl.style.position = 'absolute';
768                this.estimateOverlayEl.style.left = left + 'px';
769                this.estimateOverlayEl.style.width = width + 'px';
770                this.estimateOverlayEl.style.top = areaTop + 'px';
771                this.estimateOverlayEl.style.height = height + 'px';
772
773                // Adjust inner box height to 2px above highest estimate bar
774                try {
775                    const yScale = this.chartInstance.scales && this.chartInstance.scales.y;
776                    const overlayBox = this.estimateOverlayEl && this.estimateOverlayEl.querySelector && this.estimateOverlayEl.querySelector('.estimate-overlay-box');
777                    if (yScale && overlayBox) {
778                        // dataset[1] holds estimates in this widget
779                        const ds = (this.chartInstance.data && this.chartInstance.data.datasets && this.chartInstance.data.datasets[1]) || null;
780                        const labelsAll = this.chartInstance.data && this.chartInstance.data.labels || [];
781                        let maxEstimate = null;
782                        for (let i = 0; i < labelsAll.length; i++) {
783                            if (String(labelsAll[i]).endsWith('e')) {
784                                const v = ds && Array.isArray(ds.data) ? ds.data[i] : null;
785                                if (typeof v === 'number') {
786                                    maxEstimate = (maxEstimate == null) ? v : Math.max(maxEstimate, v);
787                                }
788                            }
789                        }
790                        if (maxEstimate != null) {
791                            const yPixel = yScale.getPixelForValue(maxEstimate);
792                            const boxTopAbs = Math.max(areaTop, Math.round(yPixel) - 2);
793                            const boxHeight = Math.max(0, areaBottom - boxTopAbs);
794                            overlayBox.style.position = 'absolute';
795                            overlayBox.style.top = (boxTopAbs - areaTop) + 'px';
796                            overlayBox.style.height = boxHeight + 'px';
797                        } else {
798                            overlayBox.style.position = 'absolute';
799                            overlayBox.style.top = '0px';
800                            overlayBox.style.height = height + 'px';
801                        }
802                    }
803                } catch (err) { }
804            } catch (e) { }
805        },
806        switchPeriod(period) {
807            if (this.selectedPeriod !== period && this.chartAnimationComplete) {
808                this.selectedPeriod = period;
809                if (this.chartInstance) {
810                    this.chartInstance.options.scales.y1.display = period === 'yearly';
811                    this.chartInstance.data.datasets[2].visible = period === 'yearly';
812                    this.chartInstance.data.datasets[2].borderWidth = period === 'yearly' ? 3 : 0;
813                    this.chartInstance.update();
814                }
815            }
816        },
817        toggleTooltip() {
818            this.showTooltip = !this.showTooltip;
819        },
820        loginEvent() {
821            this.$emit('loginEvent', "");
822        },
823        handleClickOutside(event) {
824            const tooltipContainer = this.$el.querySelector('.info-icon-container');
825            if (tooltipContainer && !tooltipContainer.contains(event.target)) {
826                this.showTooltip = false;
827            }
828        },
829        addCustomStyles() {
830            // Erstelle ein Style-Element, falls es noch nicht existiert
831            let styleElement = document.getElementById('dividends-chart-widget-styles');
832            if (!styleElement) {
833                styleElement = document.createElement('style');
834                styleElement.id = 'dividends-chart-widget-styles';
835                document.head.appendChild(styleElement);
836            }
837            // Füge die CSS-Styles hinzu
838            styleElement.textContent = `
839               .info-icon-container {
840                position: relative;
841                display: inline-block;
842                cursor: pointer;
843            }
844            .estimate-overlay {
845                position: absolute;
846                top: 0;
847                right: 0;
848                height: 100%;
849     
850                backdrop-filter: blur(1px);
851                display: flex;
852                align-items: flex-end;
853                justify-content: center;
854                z-index: 5;
855                pointer-events: all;
856            }
857            .estimate-overlay-box {
858
859                border-radius: 10px;
860                padding: 32px 14px;
861                text-align: center;
862                color: #FFFFFF;
863                min-width: 90px;
864                border:  1px solid #3B3661;;
865            }
866            .estimate-overlay-lock { font-size: 18px; margin-bottom: 6px; }
867            .estimate-overlay-text { font-size: 12px; color: rgba(255,255,255,0.8); margin-bottom: 8px; line-height: 16px;font-style: italic; margin-top: 12px; margin-bottom: 12px; }
868            .estimate-overlay-login { display:inline-block; padding: 6px 14px; background:#63FFDA; color:#121029; border-radius: 8px; 
869                        font-weight: 800; font-size: 12px; text-decoration:none; }
870             .estimate-overlay-box  a{ color: #161431 !important;font-size: 12px;font-weight: 700; }
871            .logo-container {
872                display: flex;
873                align-items: center;
874                gap: 10px;
875                margin-bottom: 0px;
876            }   
877            .logo-image {
878                width: 24px;
879                height: 24px;
880                border-radius: 3px;
881            }   
882            .logo-text {
883                display: flex;
884                flex-direction: column;
885            }   
886            .logo-text h3 {
887                padding-top: 0px !important; 
888                padding-bottom: 0px !important;
889                margin-top: 0px !important;
890                margin-bottom: 0px !important;
891                line-height: 16px !important;
892            } 
893            .tooltip {
894                position: absolute;
895                z-index: 99;
896                background-color: #2B274B;
897                color: white;
898                padding: 10px;
899                border-radius: 4px;
900                width: 250px;
901                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
902                top: 100%;
903                right: -60px;
904                margin-top: 5px;
905                font-size: 12px;
906                line-height: 1.4;
907                display: block !important;
908                visibility: visible !important;
909                opacity: 1 !important;
910            }
911            .tooltip:before {
912                content: '';
913                position: absolute;
914                top: -5px;
915                right: 62px;
916                width: 0;
917                height: 0;
918                border-left: 5px solid transparent;
919                border-right: 5px solid transparent;
920                border-bottom: 5px solid #2B274B;
921            }
922            .revenue-widget-formatted-display-value {
923               background-color: ${this.formattedDisplayBackgroun
923dColor || 'rgba(22, 20, 49, 0.7)'}; 
924            }
925            .current-value-section {
926                height: 0;
927            }
928            .current-value-section--no-dividends {
929                height: auto;
930                padding-bottom: 16px;
931            }
932            .dividends-empty-message {
933                display: flex;
934                align-items: center;
935                justify-content: center;
936                width: 100%;
937                min-height: 80px;
938                margin: 24px 0 0;
939                color: rgba(255, 255, 255, 0.75);
940                font-size: 15px;
941            }
942            .tooltip-close {
943                position: absolute;
944                top: 5px;
945                right: 5px;
946                width: 16px;
947                height: 16px;
948                line-height: 16px;
949                text-align: center;
950                cursor: pointer;
951                font-size: 16px;
952                color: #aaa;
953                border-radius: 50%;
954            }
955            .tooltip-close:hover {
956                color: white;
957                background-color: rgba(255, 255, 255, 0.1);
958            }
959            `;
960        },
961    },
962    // Lifecycle Hooks
963    mounted() {
964        this.chartInstance = null;
965        // this.fetchData(); is over watcher in vue 3
966
967        // Füge die benutzerdefinierten Styles hinzu
968        this.addCustomStyles();
969
970        // Add click outside listener
971        document.addEventListener('click', this.handleClickOutside);
972    },
973    beforeDestroy() {
974        if (this.chartInstance) {
975            this.chartInstance.destroy();
976            this.chartInstance = null;
977        }
978        this.chartTooltipActive = false;
979        // Remove click outside listener
980        document.removeEventListener('click', this.handleClickOutside);
981    },
982    // Watchers
983    watch: {
984        isin: {
985            handler(newIsin) {
986                if (newIsin && !this.chartDataFromParent) {
987                    this.fetchData();
988                }
989            },
990            immediate: false
991        },
992        overideEndpoint: {
993            handler(newEndpoint) {
994                if (newEndpoint) {
995                    this.API_ENDPOINT = newEndpoint;
996                }
997            },
998            immediate: true
999        },
1000        chartDataFromParent: {
1001            handler(newData) {
1002
1003                if (newData) {
1004                    this.chartApiData = newData
1005                    this.loading = false;
1006                    this.error = null;
1007
1008                } else if (this.isin) {
1009                    this.fetchData();
1010                }
1011            },
1012            immediate: true
1013        },
1014        currentChartData: {
1015            handler(newData) {
1016                if (newData && newData.length > 0 && !this.loading && this.isVisible && this.hasActualDividends) {
1017                    this.$nextTick(() => {
1018                        this.renderChart();
1019                    });
1020                } else if (!this.loading && this.chartInstance) {
1021                    this.chartInstance.destroy();
1022                    this.chartInstance = null;
1023                }
1024            },
1025            immediate: true
1026        },
1027        hasActualDividends(hasDividends) {
1028            if (!hasDividends && this.chartInstance) {
1029                this.chartInstance.destroy();
1030                this.chartInstance = null;
1031            }
1032        }
1033    }
1034};

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.