| | <script lang="ts"> |
| | import { onMount, onDestroy } from 'svelte'; |
| | import { getContext } from 'svelte'; |
| | import Spinner from '$lib/components/common/Spinner.svelte'; |
| |
|
| | const i18n = getContext('i18n'); |
| |
|
| | export let history: Array<{ date: string; won: number; lost: number }> = []; |
| | export let loading = false; |
| | export let aggregateWeekly = false; |
| |
|
| | let chartCanvas: HTMLCanvasElement; |
| | let chartInstance: any = null; |
| | let Chart: any = null; |
| |
|
| | const createChart = async () => { |
| | if (!chartCanvas || !history.length) return; |
| |
|
| | |
| | if (!Chart) { |
| | const module = await import('chart.js/auto'); |
| | Chart = module.default; |
| | } |
| |
|
| | |
| | if (chartInstance) { |
| | chartInstance.destroy(); |
| | } |
| |
|
| | |
| | let chartData = history; |
| |
|
| | if (aggregateWeekly && history.length > 7) { |
| | |
| | const weeklyData: { [key: string]: { won: number; lost: number; startDate: string } } = {}; |
| | history.forEach((h) => { |
| | const date = new Date(h.date); |
| | |
| | const day = date.getDay(); |
| | const diff = date.getDate() - day + (day === 0 ? -6 : 1); |
| | const monday = new Date(date); |
| | monday.setDate(diff); |
| | const weekKey = monday.toISOString().split('T')[0]; |
| |
|
| | if (!weeklyData[weekKey]) { |
| | weeklyData[weekKey] = { won: 0, lost: 0, startDate: weekKey }; |
| | } |
| | weeklyData[weekKey].won += h.won; |
| | weeklyData[weekKey].lost += h.lost; |
| | }); |
| |
|
| | chartData = Object.values(weeklyData).sort( |
| | (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() |
| | ); |
| | } |
| |
|
| | const labels = chartData.map((h) => { |
| | const date = new Date('startDate' in h ? h.startDate : h.date); |
| | if (aggregateWeekly) { |
| | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); |
| | } |
| | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); |
| | }); |
| |
|
| | |
| | const wonData = chartData.map((h) => h.won); |
| | const lostData = chartData.map((h) => -h.lost); |
| |
|
| | |
| | const barPercentage = aggregateWeekly ? 0.95 : 0.9; |
| | const categoryPercentage = aggregateWeekly ? 1.0 : 0.95; |
| |
|
| | chartInstance = new Chart(chartCanvas, { |
| | type: 'bar', |
| | data: { |
| | labels, |
| | datasets: [ |
| | { |
| | label: 'Won', |
| | data: wonData, |
| | backgroundColor: '#5ba3c8', |
| | borderRadius: 2, |
| | barPercentage, |
| | categoryPercentage |
| | }, |
| | { |
| | label: 'Lost', |
| | data: lostData, |
| | backgroundColor: '#d97c5a', |
| | borderRadius: 2, |
| | barPercentage, |
| | categoryPercentage |
| | } |
| | ] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | interaction: { |
| | intersect: false, |
| | mode: 'index' |
| | }, |
| | plugins: { |
| | legend: { |
| | display: false |
| | }, |
| | tooltip: { |
| | backgroundColor: 'rgba(17, 24, 39, 0.9)', |
| | titleColor: '#f3f4f6', |
| | bodyColor: '#d1d5db', |
| | borderColor: 'rgba(75, 85, 99, 0.3)', |
| | borderWidth: 1, |
| | padding: 8, |
| | displayColors: true, |
| | boxWidth: 8, |
| | boxHeight: 8, |
| | callbacks: { |
| | label: function (context: any) { |
| | const value = Math.abs(context.raw); |
| | return `${context.dataset.label}: ${value}`; |
| | } |
| | } |
| | } |
| | }, |
| | scales: { |
| | x: { |
| | stacked: true, |
| | grid: { |
| | display: false |
| | }, |
| | ticks: { |
| | display: false |
| | }, |
| | border: { |
| | display: false |
| | } |
| | }, |
| | y: { |
| | stacked: true, |
| | grid: { |
| | color: 'rgba(107, 114, 128, 0.1)', |
| | drawTicks: false |
| | }, |
| | ticks: { |
| | color: '#6b7280', |
| | font: { |
| | size: 10 |
| | }, |
| | padding: 8, |
| | stepSize: 1, |
| | precision: 0, |
| | callback: function (value: number) { |
| | return Math.abs(value); |
| | } |
| | }, |
| | border: { |
| | display: false |
| | } |
| | } |
| | }, |
| | animation: { |
| | duration: 400, |
| | easing: 'easeOutQuart' |
| | } |
| | } |
| | }); |
| | }; |
| |
|
| | $: if (chartCanvas && history.length && !loading && aggregateWeekly !== undefined) { |
| | createChart(); |
| | } |
| |
|
| | onDestroy(() => { |
| | if (chartInstance) { |
| | chartInstance.destroy(); |
| | chartInstance = null; |
| | } |
| | }); |
| | </script> |
| |
|
| | <div class="w-full"> |
| | {#if loading} |
| | <div class="flex items-center justify-center h-40"> |
| | <Spinner className="size-5" /> |
| | </div> |
| | {:else if !history.length || history.every((h) => h.won === 0 && h.lost === 0)} |
| | <div class="flex items-center justify-center h-40 text-gray-500 text-sm"> |
| | {$i18n.t('No activity data')} |
| | </div> |
| | {:else} |
| | <div class="h-48"> |
| | <canvas bind:this={chartCanvas}></canvas> |
| | </div> |
| | {/if} |
| | </div> |
| |
|