v0.23.3 fc8669a

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    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>

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