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.