dauabi / src /lib /components /admin /Evaluations /ModelActivityChart.svelte
github-actions[bot]
GitHub deploy: 7e224e4a536b07ec008613f06592e34050e7067c
f1f8449
<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;
// Dynamically import Chart.js
if (!Chart) {
const module = await import('chart.js/auto');
Chart = module.default;
}
// Destroy previous chart instance
if (chartInstance) {
chartInstance.destroy();
}
// For year/all views, aggregate by week
let chartData = history;
if (aggregateWeekly && history.length > 7) {
// Aggregate daily data into weekly buckets
const weeklyData: { [key: string]: { won: number; lost: number; startDate: string } } = {};
history.forEach((h) => {
const date = new Date(h.date);
// Get the Monday of this week as the bucket key
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' });
});
// Diverging chart: wins go UP (positive), losses go DOWN (negative)
const wonData = chartData.map((h) => h.won);
const lostData = chartData.map((h) => -h.lost); // Negative for below-zero bars
// Thicker bars for weekly aggregated data
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 // Hide legend for cleaner look
},
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 // Hide x-axis labels for cleaner look
},
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); // Show absolute values on y-axis
}
},
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>