1<script lang="ts">
2 import { Chart } from 'frappe-charts';
3 import { onMount } from 'svelte';
4 import type { DailyVisitCount } from '../types';
5
6 interface Props {
7 dailyVisits: DailyVisitCount[];
8 }
9
10 let { dailyVisits }: Props = $props();
11
12 let container: HTMLDivElement;
13 let chart: Chart | null = null;
14
15 // Get accent color from CSS variable
16 function getAccentColor(): string {
17 return getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4ade80';
18 }
19
20 // Convert data to Frappe Charts format
21 const chartData = $derived.by(() => {
22 if (!dailyVisits || dailyVisits.length === 0) {
23 return { labels: [], datasets: [{ values: [] }] };
24 }
25
26 const labels = dailyVisits.map(d => {
27 const date = new Date(d.day + 'T12:00:00');
28 return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
29 });
30 const values = dailyVisits.map(d => d.visits);
31
32 return {
33 labels,
34 datasets: [{ name: 'Visits', values }]
35 };
36 });
37
38 const totalVisits = $derived(dailyVisits.reduce((sum, d) => sum + d.visits, 0));
39
40 // Initialize chart on mount
41 onMount(() => {
42 if (chartData.labels.length === 0) return;
43
44 chart = new Chart(container, {
45 data: chartData,
46 type: 'line',
47 height: 160,
48 colors: [getAccentColor()],
49 axisOptions: { xAxisMode: 'tick', xIsSeries: true },
50 lineOptions: { regionFill: 1, hideDots: 0, dotSize: 4 },
51 tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` }
52 });
53
54 return () => {
55 chart?.destroy();
56 chart = null;
57 };
58 });
59
60 // Update chart when data changes (after initial mount)
61 $effect(() => {
62 // Skip if chart not initialized yet or no data
63 if (!chart) return;
64
65 // Read chartData to track changes
66 const data = chartData;
67 if (data.labels.length === 0) return;
68
69 chart.update(data);
70 });
71</script>
72
73<div class="chart-wrapper">
74 <div class="chart-header">
75 <span class="chart-label">Visits</span>
76 <span class="chart-value">{totalVisits} total</span>
77 </div>
78 <div bind:this={container} class="chart-container"></div>
79</div>
80
81<style>
82 .chart-wrapper {
83 width: 100%;
84 display: flex;
85 flex-direction: column;
86 }
87
88 .chart-header {
89 display: flex;
90 align-items: center;
91 gap: 0.5rem;
92 padding: 0 0.25rem 0.5rem;
93 font-size: 0.65rem;
94 font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
95 }
96
97 .chart-label {
98 color: var(--primary, #4ade80);
99 font-weight: 600;
100 }
101
102 .chart-value {
103 color: var(--muted-foreground, #737373);
104 }
105
106 .chart-container {
107 width: 100%;
108 }
109
110 .chart-container :global(.frappe-chart) {
111 font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
112 }
113
114 .chart-container :global(.frappe-chart .axis text),
115 .chart-container :global(.frappe-chart .chart-label) {
116 fill: var(--muted-foreground, #737373);
117 font-size: 10px;
118 }
119
120 .chart-container :global(.frappe-chart .axis line),
121 .chart-container :global(.frappe-chart .chart-strokes) {
122 stroke: var(--border, #333);
123 }
124</style>