web/src/lib/aggregation.ts 2.4 K raw
1
import type { RawDigraph, MetricStats, DigraphAggregation } from './types';
2
import { std, filterOutliers } from './utils';
3
4
function normalizeKey(key: string): string {
5
  if (key === ' ') return '␣';
6
  if (key === 'Backspace') return '⌫';
7
  if (key === 'Enter') return '⏎';
8
  return key.toLowerCase();
9
}
10
11
export function normalizeDigraphKey(key1: string, key2: string): string {
12
  return `${normalizeKey(key1)} → ${normalizeKey(key2)}`;
13
}
14
15
export function computeMetricStats(values: number[]): MetricStats {
16
  if (values.length === 0) {
17
    return { mean: 0, std: 0, min: 0, max: 0, count: 0 };
18
  }
19
  const count = values.length;
20
  const mean = values.reduce((s, v) => s + v, 0) / count;
21
  return {
22
    mean: Math.round(mean * 10) / 10,
23
    std: Math.round(std(values) * 10) / 10,
24
    min: Math.round(Math.min(...values) * 10) / 10,
25
    max: Math.round(Math.max(...values) * 10) / 10,
26
    count,
27
  };
28
}
29
30
const EXCLUDED_KEYS = new Set([
31
  'ArrowUp',
32
  'ArrowDown',
33
  'ArrowLeft',
34
  'ArrowRight',
35
  'Home',
36
  'End',
37
  'PageUp',
38
  'PageDown',
39
  'Insert',
40
  'Delete',
41
  'Escape',
42
  'Tab',
43
  'F1', 'F2', 'F3', 'F4', 'F5', 'F6',
44
  'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
45
]);
46
47
function shouldExclude(key: string): boolean {
48
  return EXCLUDED_KEYS.has(key);
49
}
50
51
export function aggregateDigraphs(digraphs: RawDigraph[]): DigraphAggregation[] {
52
  const groups = new Map<string, RawDigraph[]>();
53
54
  for (const d of digraphs) {
55
    if (shouldExclude(d.key1) || shouldExclude(d.key2)) continue;
56
    const nk = normalizeDigraphKey(d.key1, d.key2);
57
    const group = groups.get(nk);
58
    if (group) {
59
      group.push(d);
60
    } else {
61
      groups.set(nk, [d]);
62
    }
63
  }
64
65
  const aggregations: DigraphAggregation[] = [];
66
67
  for (const [normalizedKeys, group] of groups) {
68
    const agg: DigraphAggregation = {
69
      normalizedKeys,
70
      count: group.length,
71
      holdTime1: computeMetricStats(filterOutliers(group.map((d) => d.holdTime1))),
72
      holdTime2: computeMetricStats(filterOutliers(group.map((d) => d.holdTime2))),
73
      pressPress: computeMetricStats(filterOutliers(group.map((d) => d.pressPress))),
74
      releaseRelease: computeMetricStats(filterOutliers(group.map((d) => d.releaseRelease))),
75
      pressRelease: computeMetricStats(filterOutliers(group.map((d) => d.pressRelease))),
76
      releasePress: computeMetricStats(filterOutliers(group.map((d) => d.releasePress))),
77
    };
78
    aggregations.push(agg);
79
  }
80
81
  aggregations.sort((a, b) => b.count - a.count);
82
  return aggregations;
83
}