411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
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 += '<div style="font-weight: bold; margin-bottom: 5px;">' + title + '</div>';
|
|
});
|
|
|
|
// Add body with color indicators
|
|
bodyLines.forEach(function(body, i) {
|
|
const colors = tooltip.labelColors[i];
|
|
const colorBox = '<span style="display: inline-block; width: 12px; height: 12px; background-color: ' +
|
|
colors.backgroundColor + '; border: 1px solid ' + colors.borderColor +
|
|
'; margin-right: 5px; vertical-align: middle;"></span>';
|
|
|
|
// 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 += '<div style="margin: 2px 0;">' + colorBox + label + '</div>';
|
|
});
|
|
|
|
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 {
|
|
}
|
|
}; |