v0.26.2 3cd9d2e

HourlyActivityChart.svelte RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close