1<script lang="ts">
2 import { Chart } from 'frappe-charts';
3 import { onMount } from 'svelte';
4 import type { HourlyDistribution } from '../types';
5
6 interface Props {
7 hourlyDistribution: HourlyDistribution[];
8 }
9
10 let { hourlyDistribution }: 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 (ensure all 24 hours)
21 const chartData = $derived.by(() => {
22 const visitsByHour = new Map<number, number>();
23 hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count));
24
25 const labels = Array.from({ length: 24 }, (_, i) =>
26 i % 6 === 0 ? `${String(i).padStart(2, '0')}h` : ''
27 );
28 const values = Array.from({ length: 24 }, (_, i) => visitsByHour.get(i) ?? 0);
29
30 return {
31 labels,
32 datasets: [{ name: 'Activity', values }]
33 };
34 });
35
36 // Peak hour for display
37 const peakHour = $derived.by(() => {
38 let maxVisits = 0;
39 let peak = 0;
40 hourlyDistribution.forEach(h => {
41 if (h.visit_count > maxVisits) {
42 maxVisits = h.visit_count;
43 peak = h.hour;
44 }
45 });
46 return `${String(peak).padStart(2, '0')}:00`;
47 });
48
49 // Initialize chart on mount
50 onMount(() => {
51 chart = new Chart(container, {
52 data: chartData,
53 type: 'bar',
54 height: 120,
55 colors: [getAccentColor()],
56 axisOptions: { xAxisMode: 'tick' },
57 barOptions: { spaceRatio: 0.3 },
58 tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` }
59 });
60
61 return () => {
62 chart?.destroy();
63 chart = null;
64 };
65 });
66
67 // Update chart when data changes (after initial mount)
68 $effect(() => {
69 if (!chart) return;
70
71 const data = chartData;
72 if (data.labels.length === 0) return;
73
74 chart.update(data);
75 });
76</script>
77
78<div class="chart-wrapper">
79 <div class="chart-header">
80 <span class="chart-label">Activity</span>
81 <span class="chart-value">peak at {peakHour}</span>
82 </div>
83 <div bind:this={container} class="chart-container"></div>
84</div>
85
86<style>
87 .chart-wrapper {
88 width: 100%;
89 display: flex;
90 flex-direction: column;
91 }
92
93 .chart-header {
94 display: flex;
95 align-items: center;
96 gap: 0.5rem;
97 padding: 0 0.25rem 0.5rem;
98 font-size: 0.65rem;
99 font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
100 }
101
102 .chart-label {
103 color: var(--primary, #4ade80);
104 font-weight: 600;
105 }
106
107 .chart-value {
108 color: var(--muted-foreground, #737373);
109 }
110
111 .chart-container {
112 width: 100%;
113 }
114
115 .chart-container :global(.frappe-chart) {
116 font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
117 }
118
119 .chart-container :global(.frappe-chart .axis text),
120 .chart-container :global(.frappe-chart .chart-label) {
121 fill: var(--muted-foreground, #737373);
122 font-size: 10px;
123 }
124
125 .chart-container :global(.frappe-chart .axis line),
126 .chart-container :global(.frappe-chart .chart-strokes) {
127 stroke: var(--border, #333);
128 }
129</style>