import { Chart, registerables } from 'chart.js'; Chart.register(...registerables); window.initReturnRatioChart = function(timelineData, trendData = []) { const chartCanvas = document.getElementById('returnRatioChart'); if (!chartCanvas || !timelineData || timelineData.length === 0) { return; } // Get data range for better debugging const values = timelineData.map(item => item.return_ratio); const minValue = Math.min(...values); const maxValue = Math.max(...values); const ctx = chartCanvas.getContext('2d'); if (!ctx) { return; } // Destroy existing chart if it exists if (chartCanvas.chart) { chartCanvas.chart.destroy(); } try { // Prepare datasets - trend line first to ensure it renders behind the main line const datasets = []; // Add trend line first (renders behind) if (trendData && trendData.length > 0) { datasets.push({ label: 'Trend Line', data: trendData.map(item => item.trend_value), borderColor: '#DC2626', // red-600 for better visibility backgroundColor: 'transparent', borderWidth: 3, // Increased width for better visibility borderDash: [8, 4], // Adjusted dash pattern for better visibility tension: 0, // Linear regression line (straight) fill: false, pointRadius: 0, pointHoverRadius: 0, order: 1, // Render behind main line }); } // Add main return ratio line (renders on top) datasets.push({ label: 'Reciprocity Rate %', data: timelineData.map(item => item.return_ratio), borderColor: '#000', // black backgroundColor: 'rgba(107, 114, 128, 0.1)', // Reduced opacity for less interference borderWidth: 3, fill: true, tension: 0.4, pointBackgroundColor: '#000', // black pointBorderColor: '#fff', pointBorderWidth: 2, pointRadius: 6, pointHoverRadius: 8, order: 0, // Render on top }); // Get translated labels from the first data point const translations = timelineData[0]?.translations || { period: 'Period', return_ratio: 'Reciprocity Rate' }; const chart = new Chart(ctx, { type: 'line', data: { labels: timelineData.map(item => item.label), datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, plugins: { legend: { display: true, position: 'top', align: 'end', labels: { usePointStyle: true, color: '#1F2937' } }, tooltip: { enabled: false, external: function(context) { const tooltip = context.tooltip; let tooltipEl = document.getElementById('custom-tooltip'); if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.id = 'custom-tooltip'; tooltipEl.style.position = 'absolute'; tooltipEl.style.background = 'rgba(0, 0, 0, 0.8)'; tooltipEl.style.color = 'white'; tooltipEl.style.border = '2px solid white'; tooltipEl.style.borderRadius = '8px'; tooltipEl.style.pointerEvents = 'none'; tooltipEl.style.transform = 'translate(-50%, 0)'; tooltipEl.style.transition = 'all .1s ease'; tooltipEl.style.padding = '10px'; tooltipEl.style.fontSize = '12px'; tooltipEl.style.fontFamily = 'Arial, sans-serif'; tooltipEl.style.zIndex = '9999'; tooltipEl.style.boxShadow = 'none'; tooltipEl.style.outline = 'none'; document.body.appendChild(tooltipEl); } if (tooltip.opacity === 0) { tooltipEl.style.opacity = '0'; return; } if (tooltip.body) { const titleLines = tooltip.title || []; const bodyLines = tooltip.body.map(b => b.lines); let innerHtml = ''; // Add title titleLines.forEach(function(title) { innerHtml += '
' + title + '
'; }); // Add body with color indicators bodyLines.forEach(function(body, i) { const colors = tooltip.labelColors[i]; const colorBox = ''; // Fix the label based on dataset order (trend first, return ratio second) let label = body[0]; if (i === 0) { // First dataset = Trend Line (red) label = 'Trend: ' + label.split(': ')[1]; } else { // Second dataset = Reciprocity Rate (gray) label = 'Reciprocity Rate: ' + label.split(': ')[1]; } innerHtml += '
' + colorBox + label + '
'; }); tooltipEl.innerHTML = innerHtml; } const position = context.chart.canvas.getBoundingClientRect(); tooltipEl.style.opacity = '1'; tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px'; tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px'; } } }, scales: { x: { display: true, title: { display: true, text: translations.period, color: '#6B7280' }, grid: { color: 'rgba(0, 0, 0, 0.1)' }, ticks: { color: '#6B7280' } }, y: { display: true, title: { display: true, text: translations.return_ratio, color: '#6B7280' }, beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.1)' }, ticks: { color: '#6B7280', callback: function(value) { return Math.round(value) + '%'; } } } } } }); // Store chart reference for cleanup chartCanvas.chart = chart; // Store chart instance globally for PDF export window.returnRatioChart = chart; // Set canvas height chartCanvas.style.height = '300px'; // Hide loading indicator const loadingIndicator = document.getElementById('chartLoadingIndicator'); if (loadingIndicator) { loadingIndicator.style.display = 'none'; } return chart; } catch (error) { return null; } }; // Livewire integration for chart initialization and PDF handling document.addEventListener('livewire:init', () => { Livewire.on('openPdf', (url) => { if (url) { window.open(url, '_blank'); } else { } }); // Function to initialize chart function initializeChart() { // Find chart container const chartContainer = document.querySelector('[data-chart-data]'); if (!chartContainer) { return; } // Get chart data const timelineDataAttr = chartContainer.getAttribute('data-chart-data'); const trendDataAttr = chartContainer.getAttribute('data-trend-data'); if (!timelineDataAttr) { return; } try { const timelineData = JSON.parse(timelineDataAttr); const trendData = trendDataAttr ? JSON.parse(trendDataAttr) : []; // Initialize the chart if (typeof window.initReturnRatioChart === 'function') { window.initReturnRatioChart(timelineData, trendData); } else { } } catch (error) { } } // Initialize chart on page load document.addEventListener('DOMContentLoaded', () => { setTimeout(initializeChart, 100); }); // Listen for Livewire updates - debounced let morphTimer = null; Livewire.hook('morph.updated', () => { clearTimeout(morphTimer); morphTimer = setTimeout(() => { initializeChart(); }, 300); }); }); // Function to export Return Ratio chart as base64 image for PDF window.exportReturnRatioChartForPdf = async function() { if (!window.returnRatioChart) { return null; } try { const originalChart = window.returnRatioChart; // Render into an offscreen canvas to avoid touching the live DOM canvas const pdfWidth = 900; const pdfHeight = 400; const offscreen = document.createElement('canvas'); offscreen.width = pdfWidth; offscreen.height = pdfHeight; const ctx = offscreen.getContext('2d'); const offscreenChart = new Chart(ctx, { type: originalChart.config.type, data: JSON.parse(JSON.stringify(originalChart.config.data)), options: { ...JSON.parse(JSON.stringify(originalChart.config.options || {})), responsive: false, maintainAspectRatio: false, animation: false, } }); // Wait for render await new Promise(resolve => setTimeout(resolve, 300)); const chartImage = offscreen.toDataURL('image/png', 1.0); offscreenChart.destroy(); return chartImage; } catch (error) { return null; } }; // Find the SingleReport Livewire component by looking for its unique DOM marker window.findSingleReportComponent = function() { if (!window.Livewire) return null; // Primary: use the dedicated id marker on the single-report root div const marker = document.getElementById('single-report-component'); if (marker) { const wireElement = marker.closest('[wire\\:id]'); if (wireElement) { return window.Livewire.find(wireElement.getAttribute('wire:id')); } } // Fallback: scan for unique child elements const elements = document.querySelectorAll('[wire\\:id]'); for (let element of elements) { if ( element.querySelector('[data-chart-data]') || element.querySelector('[data-balance-chart-data]') || element.querySelector('#returnRatioChart') || element.querySelector('#accountBalancesChart') || element.querySelector('#account-balances') || element.querySelector('#transaction-types') ) { return window.Livewire.find(element.getAttribute('wire:id')); } } return null; }; // Function to export both charts and send to Livewire for PDF generation window.exportPdfWithChart = async function() { // Export Return Ratio Chart const returnRatioChartImage = await window.exportReturnRatioChartForPdf(); // Export Account Balances Chart (if available) let accountBalancesChartImage = null; if (window.accountBalancesChart) { accountBalancesChartImage = await window.exportAccountBalancesChartForPdf(); } // If neither chart is available, fall back to regular PDF export if (!returnRatioChartImage && !accountBalancesChartImage) { if (window.Livewire) { const component = window.findSingleReportComponent(); if (component) { component.call('exportPdf'); } else { } } return; } // Read date range and decimal format from DOM const chartContainer = document.querySelector('[data-balance-chart-data]') || document.querySelector('[data-chart-data]'); let fromDate = null; let toDate = null; let decimalFormat = false; if (chartContainer) { const dateRange = chartContainer.getAttribute('data-date-range'); if (dateRange) { const dates = dateRange.split(' to '); if (dates.length === 2) { fromDate = dates[0].trim(); toDate = dates[1].trim(); } } decimalFormat = chartContainer.getAttribute('data-decimal-format') === '1'; } if (window.Livewire) { try { const component = window.findSingleReportComponent(); if (!component) { throw new Error('SingleReport Livewire component not found'); } await component.call('exportPdfWithCharts', returnRatioChartImage, accountBalancesChartImage, fromDate, toDate, decimalFormat); } catch (error) { try { const comp = window.findSingleReportComponent(); if (comp) { await comp.call('exportPdf'); } } catch (fallbackError) { } } } else { } };