|
@@ -43,19 +43,42 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderChart(data) {
|
|
function renderChart(data) {
|
|
|
|
|
+ // Dynamic label formatting based on range and data length
|
|
|
const labels = data.map((item) => {
|
|
const labels = data.map((item) => {
|
|
|
const date = new Date(item.createdAt);
|
|
const date = new Date(item.createdAt);
|
|
|
- const dd = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
- const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
- const yyyy = date.getFullYear();
|
|
|
|
|
- return `${dd}/${mm}/${yyyy}`;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // For longer ranges, use more concise date formats
|
|
|
|
|
+ if (selectedRange === '6m' || selectedRange === '1y') {
|
|
|
|
|
+ const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
|
|
|
+ const year = date.getFullYear();
|
|
|
|
|
+ return `${month} ${year}`;
|
|
|
|
|
+ } else if (selectedRange === '30d') {
|
|
|
|
|
+ const day = date.getDate();
|
|
|
|
|
+ const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
|
|
|
+ return `${day} ${month}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 5d range - show day and month
|
|
|
|
|
+ const dd = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
+ const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
+ return `${dd}/${mm}`;
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
const prices = data.map((item) => item.price);
|
|
const prices = data.map((item) => item.price);
|
|
|
|
|
|
|
|
|
|
+ // Dynamic point visibility based on data density
|
|
|
|
|
+ const shouldShowPoints = data.length <= 30; // Hide points if more than 30 data points
|
|
|
|
|
+ const pointRadius = shouldShowPoints ? 5 : 0;
|
|
|
|
|
+ const pointHoverRadius = shouldShowPoints ? 7 : 5;
|
|
|
|
|
+
|
|
|
if (chartInstance) {
|
|
if (chartInstance) {
|
|
|
chartInstance.destroy();
|
|
chartInstance.destroy();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const gradientFill = chartCanvas.getContext('2d').createLinearGradient(0, 0, 0, 400);
|
|
|
|
|
+ gradientFill.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
|
|
|
|
|
+ gradientFill.addColorStop(1, 'rgba(59, 130, 246, 0.05)');
|
|
|
|
|
+
|
|
|
chartInstance = new Chart(chartCanvas, {
|
|
chartInstance = new Chart(chartCanvas, {
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
data: {
|
|
data: {
|
|
@@ -64,31 +87,84 @@
|
|
|
{
|
|
{
|
|
|
label: `Price (${data[0]?.currency || ''})`,
|
|
label: `Price (${data[0]?.currency || ''})`,
|
|
|
data: prices,
|
|
data: prices,
|
|
|
- fill: false,
|
|
|
|
|
|
|
+ fill: true,
|
|
|
|
|
+ backgroundColor: gradientFill,
|
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
|
borderWidth: 3,
|
|
borderWidth: 3,
|
|
|
pointBackgroundColor: 'rgb(59, 130, 246)',
|
|
pointBackgroundColor: 'rgb(59, 130, 246)',
|
|
|
pointBorderColor: 'white',
|
|
pointBorderColor: 'white',
|
|
|
pointBorderWidth: 2,
|
|
pointBorderWidth: 2,
|
|
|
- pointRadius: 5,
|
|
|
|
|
- pointHoverRadius: 7,
|
|
|
|
|
|
|
+ pointRadius: pointRadius,
|
|
|
|
|
+ pointHoverRadius: pointHoverRadius,
|
|
|
tension: 0.4
|
|
tension: 0.4
|
|
|
}
|
|
}
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
|
options: {
|
|
options: {
|
|
|
responsive: true,
|
|
responsive: true,
|
|
|
|
|
+ maintainAspectRatio: false,
|
|
|
|
|
+ interaction: {
|
|
|
|
|
+ intersect: false,
|
|
|
|
|
+ mode: 'index'
|
|
|
|
|
+ },
|
|
|
|
|
+ // Add decimation for performance with large datasets
|
|
|
|
|
+ datasets: {
|
|
|
|
|
+ line: {
|
|
|
|
|
+ pointRadius: pointRadius, // Apply to all line datasets
|
|
|
|
|
+ pointHoverRadius: pointHoverRadius
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ elements: {
|
|
|
|
|
+ line: {
|
|
|
|
|
+ tension: 0.4
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // Performance optimization for large datasets
|
|
|
|
|
+ animation: {
|
|
|
|
|
+ duration: data.length > 100 ? 0 : 750 // Disable animation for large datasets
|
|
|
|
|
+ },
|
|
|
scales: {
|
|
scales: {
|
|
|
x: {
|
|
x: {
|
|
|
title: {
|
|
title: {
|
|
|
display: true,
|
|
display: true,
|
|
|
- text: 'Date'
|
|
|
|
|
|
|
+ text: 'Date',
|
|
|
|
|
+ color: 'rgb(107, 114, 128)',
|
|
|
|
|
+ font: {
|
|
|
|
|
+ size: 12,
|
|
|
|
|
+ weight: 'bold'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(107, 114, 128, 0.1)'
|
|
|
|
|
+ },
|
|
|
|
|
+ ticks: {
|
|
|
|
|
+ color: 'rgb(107, 114, 128)',
|
|
|
|
|
+ // Smart label skipping based on data length
|
|
|
|
|
+ maxTicksLimit: data.length > 50 ? 8 : data.length > 20 ? 12 : undefined,
|
|
|
|
|
+ // Auto-skip for better readability
|
|
|
|
|
+ autoSkip: true,
|
|
|
|
|
+ maxRotation: 45,
|
|
|
|
|
+ minRotation: 0
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
y: {
|
|
y: {
|
|
|
title: {
|
|
title: {
|
|
|
display: true,
|
|
display: true,
|
|
|
- text: 'Price'
|
|
|
|
|
|
|
+ text: 'Price',
|
|
|
|
|
+ color: 'rgb(107, 114, 128)',
|
|
|
|
|
+ font: {
|
|
|
|
|
+ size: 12,
|
|
|
|
|
+ weight: 'bold'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(107, 114, 128, 0.1)'
|
|
|
|
|
+ },
|
|
|
|
|
+ ticks: {
|
|
|
|
|
+ color: 'rgb(107, 114, 128)',
|
|
|
|
|
+ callback: function (value) {
|
|
|
|
|
+ return value.toFixed(2);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -97,21 +173,39 @@
|
|
|
display: false
|
|
display: false
|
|
|
},
|
|
},
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
|
|
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
|
|
|
+ titleColor: 'white',
|
|
|
|
|
+ bodyColor: 'white',
|
|
|
|
|
+ borderColor: 'rgb(59, 130, 246)',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ cornerRadius: 8,
|
|
|
|
|
+ displayColors: false,
|
|
|
callbacks: {
|
|
callbacks: {
|
|
|
title: (context) => {
|
|
title: (context) => {
|
|
|
const idx = context[0].dataIndex;
|
|
const idx = context[0].dataIndex;
|
|
|
const raw = data[idx];
|
|
const raw = data[idx];
|
|
|
const dt = new Date(raw.createdAt);
|
|
const dt = new Date(raw.createdAt);
|
|
|
- const dd = String(dt.getDate()).padStart(2, '0');
|
|
|
|
|
- const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
- const yyyy = dt.getFullYear();
|
|
|
|
|
- return `${dd}/${mm}/${yyyy}`;
|
|
|
|
|
|
|
+ return dt.toLocaleDateString('en-US', {
|
|
|
|
|
+ year: 'numeric',
|
|
|
|
|
+ month: 'long',
|
|
|
|
|
+ day: 'numeric'
|
|
|
|
|
+ });
|
|
|
},
|
|
},
|
|
|
- label: (context) => context.parsed.y.toFixed(2)
|
|
|
|
|
|
|
+ label: (context) => {
|
|
|
|
|
+ const value = context.parsed.y;
|
|
|
|
|
+ const currency = data[context.dataIndex]?.currency || '';
|
|
|
|
|
+ return `Price: ${currency}${value.toFixed(2)}`;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
datalabels: {
|
|
datalabels: {
|
|
|
display: false
|
|
display: false
|
|
|
|
|
+ },
|
|
|
|
|
+ // Add decimation for large datasets
|
|
|
|
|
+ decimation: {
|
|
|
|
|
+ enabled: data.length > 100,
|
|
|
|
|
+ algorithm: 'lttb', // Largest Triangle Three Buckets algorithm
|
|
|
|
|
+ samples: 100 // Reduce to max 100 samples for performance
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -196,7 +290,9 @@
|
|
|
})}
|
|
})}
|
|
|
</p>
|
|
</p>
|
|
|
{/if}
|
|
{/if}
|
|
|
- <canvas bind:this={chartCanvas}></canvas>
|
|
|
|
|
|
|
+ <div class="relative h-80">
|
|
|
|
|
+ <canvas bind:this={chartCanvas}></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
<div class="flex justify-center gap-4 mt-6">
|
|
<div class="flex justify-center gap-4 mt-6">
|
|
|
{#each ranges as range}
|
|
{#each ranges as range}
|