This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sqlite3
import json
from datetime import datetime, timedelta
from collections import defaultdict, Counter
import sys
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class TikkerVisualizer:
def __init__(self, database_path='tikker.db'):
self.database_path = database_path
self.connection = sqlite3.connect(database_path)
self.cursor = self.connection.cursor()
def get_total_events(self):
start_time = time.time()
logging.info("Retrieving total events...")
self.cursor.execute("SELECT COUNT(*) FROM kevent")
result = self.cursor.fetchone()[0]
duration = time.time() - start_time
logging.info(f"Total events retrieved in {duration:.2f} seconds")
return result, duration
def get_pressed_events(self):
start_time = time.time()
logging.info("Retrieving pressed events...")
self.cursor.execute("SELECT COUNT(*) FROM kevent WHERE event='PRESSED'")
result = self.cursor.fetchone()[0]
duration = time.time() - start_time
logging.info(f"Pressed events retrieved in {duration:.2f} seconds")
return result, duration
def get_released_events(self):
start_time = time.time()
logging.info("Retrieving released events...")
self.cursor.execute("SELECT COUNT(*) FROM kevent WHERE event='RELEASED'")
result = self.cursor.fetchone()[0]
duration = time.time() - start_time
logging.info(f"Released events retrieved in {duration:.2f} seconds")
return result, duration
def get_date_range(self):
start_time = time.time()
logging.info("Retrieving date range...")
self.cursor.execute("SELECT MIN(timestamp), MAX(timestamp) FROM kevent")
result = self.cursor.fetchone()
duration = time.time() - start_time
logging.info(f"Date range retrieved in {duration:.2f} seconds")
return result[0], result[1], duration
def get_hourly_activity(self, limit=1000):
start_time = time.time()
logging.info("Retrieving hourly activity...")
self.cursor.execute("""
SELECT strftime('%H', timestamp) as hour, COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
GROUP BY hour
ORDER BY hour
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Hourly activity retrieved in {duration:.2f} seconds")
return result, duration
def get_daily_activity(self, limit=365):
start_time = time.time()
logging.info("Retrieving daily activity...")
self.cursor.execute(f"""
SELECT strftime('%Y-%m-%d', timestamp) as day, COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
GROUP BY day
ORDER BY day DESC
LIMIT {limit}
""")
result = list(reversed(self.cursor.fetchall()))
duration = time.time() - start_time
logging.info(f"Daily activity retrieved in {duration:.2f} seconds")
return result, duration
def get_weekday_activity(self):
start_time = time.time()
logging.info("Retrieving weekday activity...")
self.cursor.execute("""
SELECT
CASE CAST(strftime('%w', timestamp) AS INTEGER)
WHEN 0 THEN 'Sunday'
WHEN 1 THEN 'Monday'
WHEN 2 THEN 'Tuesday'
WHEN 3 THEN 'Wednesday'
WHEN 4 THEN 'Thursday'
WHEN 5 THEN 'Friday'
WHEN 6 THEN 'Saturday'
END as weekday,
COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
GROUP BY strftime('%w', timestamp)
ORDER BY strftime('%w', timestamp)
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Weekday activity retrieved in {duration:.2f} seconds")
return result, duration
def get_top_keys(self, limit=30):
start_time = time.time()
logging.info("Retrieving top keys...")
self.cursor.execute(f"""
SELECT char, COUNT(*) as count
FROM kevent
WHERE event='PRESSED' AND char IS NOT NULL AND char != ''
GROUP BY char
ORDER BY count DESC
LIMIT {limit}
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Top keys retrieved in {duration:.2f} seconds")
return result, duration
def get_keyboard_heatmap_data(self):
start_time = time.time()
logging.info("Retrieving keyboard heatmap data...")
self.cursor.execute("""
SELECT char, COUNT(*) as count
FROM kevent
WHERE event='PRESSED' AND char IS NOT NULL AND char != ''
GROUP BY char
ORDER BY count DESC
""")
data = {}
for char, count in self.cursor.fetchall():
data[char] = count
duration = time.time() - start_time
logging.info(f"Keyboard heatmap data retrieved in {duration:.2f} seconds")
return data, duration
def get_hour_day_heatmap(self, days_back=30):
start_time = time.time()
logging.info("Retrieving hour-day heatmap...")
self.cursor.execute(f"""
SELECT
strftime('%Y-%m-%d', timestamp) as day,
strftime('%H', timestamp) as hour,
COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
AND timestamp >= datetime('now', '-{days_back} days')
GROUP BY day, hour
ORDER BY day, hour
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Hour-day heatmap retrieved in {duration:.2f} seconds")
return result, duration
def get_typing_speed_data(self, sample_size=10000):
start_time = time.time()
logging.info("Retrieving typing speed data...")
self.cursor.execute(f"""
SELECT
strftime('%Y-%m-%d %H:00:00', timestamp) as hour_bucket,
COUNT(*) as keypresses
FROM kevent
WHERE event='PRESSED'
GROUP BY hour_bucket
ORDER BY hour_bucket DESC
LIMIT {sample_size}
""")
result = list(reversed(self.cursor.fetchall()))
duration = time.time() - start_time
logging.info(f"Typing speed data retrieved in {duration:.2f} seconds")
return result, duration
def get_character_frequency(self):
start_time = time.time()
logging.info("Retrieving character frequency...")
letters = Counter()
numbers = Counter()
special = Counter()
self.cursor.execute("""
SELECT char, COUNT(*) as count
FROM kevent
WHERE event='PRESSED' AND char IS NOT NULL AND char != ''
GROUP BY char
""")
for char, count in self.cursor.fetchall():
if len(char) == 1:
if char.isalpha():
letters[char.lower()] += count
elif char.isdigit():
numbers[char] += count
else:
special[char] += count
result = {
'letters': dict(letters.most_common(26)),
'numbers': dict(numbers.most_common(10)),
'special': dict(special.most_common(20))
}
duration = time.time() - start_time
logging.info(f"Character frequency retrieved in {duration:.2f} seconds")
return result, duration
def get_monthly_stats(self):
start_time = time.time()
logging.info("Retrieving monthly stats...")
self.cursor.execute("""
SELECT
strftime('%Y-%m', timestamp) as month,
COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
GROUP BY month
ORDER BY month
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Monthly stats retrieved in {duration:.2f} seconds")
return result, duration
def get_peak_activity_times(self):
start_time = time.time()
logging.info("Retrieving peak activity times...")
self.cursor.execute("""
SELECT
strftime('%Y-%m-%d %H:00:00', timestamp) as hour_block,
COUNT(*) as count
FROM kevent
WHERE event='PRESSED'
GROUP BY hour_block
ORDER BY count DESC
LIMIT 10
""")
result = self.cursor.fetchall()
duration = time.time() - start_time
logging.info(f"Peak activity times retrieved in {duration:.2f} seconds")
return result, duration
# NEW: Words Per Minute (WPM) and Performance Metrics
def get_wpm_stats(self):
start_time = time.time()
logging.info("Calculating WPM and performance metrics...")
# Get total key presses and time span
self.cursor.execute("SELECT COUNT(*) FROM kevent WHERE event='PRESSED'")
total_presses = self.cursor.fetchone()[0]
self.cursor.execute("SELECT MIN(timestamp), MAX(timestamp) FROM kevent WHERE event='PRESSED'")
min_time, max_time = self.cursor.fetchone()
if not min_time or not max_time or total_presses == 0:
return {
'wpm_average': 0,
'wpm_peak_hour': 0,
'wpm_peak_day': 0,
'words_typed': 0,
'characters_typed': 0,
'total_typing_time_minutes': 0,
'typing_efficiency': 0,
'peak_hour': None,
'peak_day': None
}, 0.0
min_dt = datetime.fromisoformat(min_time)
max_dt = datetime.fromisoformat(max_time)
total_minutes = (max_dt - min_dt).total_seconds() / 60.0
# Estimate words: 5 characters per word (standard WPM formula)
characters_typed = total_presses
words_typed = characters_typed / 5.0
wpm_average = words_typed / total_minutes if total_minutes > 0 else 0
# Peak hour WPM
self.cursor.execute("""
SELECT strftime('%Y-%m-%d %H:00:00', timestamp) as hour, COUNT(*) as presses
FROM kevent WHERE event='PRESSED'
GROUP BY hour
ORDER BY presses DESC
LIMIT 1
""")
peak_hour_row = self.cursor.fetchone()
peak_hour_wpm = (peak_hour_row[1] / 5.0) if peak_hour_row else 0
# Peak day WPM
self.cursor.execute("""
SELECT strftime('%Y-%m-%d', timestamp) as day, COUNT(*) as presses
FROM kevent WHERE event='PRESSED'
GROUP BY day
ORDER BY presses DESC
LIMIT 1
""")
peak_day_row = self.cursor.fetchone()
peak_day_wpm = (peak_day_row[1] / 5.0 / 60.0) if peak_day_row else 0 # per minute
# Typing efficiency: presses per minute during active time
# Approximate active time as sum of intervals with activity
self.cursor.execute("""
SELECT COUNT(DISTINCT strftime('%Y-%m-%d %H', timestamp)) as active_hours
FROM kevent WHERE event='PRESSED'
""")
active_hours = self.cursor.fetchone()[0]
typing_efficiency = total_presses / (active_hours * 60) if active_hours > 0 else 0
result = {
'wpm_average': round(wpm_average, 2),
'wpm_peak_hour': round(peak_hour_wpm, 2),
'wpm_peak_day': round(peak_day_wpm, 2),
'words_typed': int(words_typed),
'characters_typed': characters_typed,
'total_typing_time_minutes': round(total_minutes, 1),
'typing_efficiency': round(typing_efficiency, 2),
'peak_hour': peak_hour_row[0] if peak_hour_row else None,
'peak_day': peak_day_row[0] if peak_day_row else None
}
duration = time.time() - start_time
logging.info(f"WPM stats calculated in {duration:.2f} seconds")
return result, duration
def get_performance_insights(self):
start_time = time.time()
logging.info("Generating performance insights...")
# === 1. Key type distribution and corrections ===
self.cursor.execute("""
SELECT
SUM(CASE WHEN char IS NOT NULL AND length(char) = 1 AND char GLOB '[a-zA-Z]' THEN 1 ELSE 0 END) as letters,
SUM(CASE WHEN char IS NOT NULL AND length(char) = 1 AND char GLOB '[0-9]' THEN 1 ELSE 0 END) as numbers,
SUM(CASE WHEN char IS NOT NULL AND length(char) = 1 AND char NOT GLOB '[a-zA-Z0-9]' THEN 1 ELSE 0 END) as special,
SUM(CASE WHEN char = ' ' THEN 1 ELSE 0 END) as spaces,
SUM(CASE WHEN char = 'Backspace' THEN 1 ELSE 0 END) as backspaces,
SUM(CASE WHEN char = 'Delete' THEN 1 ELSE 0 END) as deletes
FROM kevent WHERE event='PRESSED'
""")
row = self.cursor.fetchone()
letters = row[0] or 0
numbers = row[1] or 0
special = row[2] or 0
spaces = row[3] or 0
backspaces = row[4] or 0
deletes = row[5] or 0
total_edits = backspaces + deletes
valid_keystrokes = letters + numbers + special + spaces
error_rate = (total_edits / valid_keystrokes * 100) if valid_keystrokes > 0 else 0
# === 2. Daily activity consistency (manual STDDEV) ===
self.cursor.execute("""
SELECT COUNT(*) as count
FROM kevent WHERE event='PRESSED'
GROUP BY strftime('%Y-%m-%d', timestamp)
""")
daily_counts = [row[0] for row in self.cursor.fetchall()]
if daily_counts:
avg_daily = sum(daily_counts) / len(daily_counts)
variance = sum((x - avg_daily) ** 2 for x in daily_counts) / len(daily_counts)
std_daily = variance ** 0.5
consistency_score = 100 - (std_daily / avg_daily * 100) if avg_daily > 0 else 0
consistency_score = max(0, min(100, consistency_score))
else:
avg_daily = std_daily = 0
consistency_score = 0
result = {
'error_rate_percent': round(error_rate, 2),
'backspace_count': backspaces,
'delete_count': deletes,
'total_corrections': total_edits,
'consistency_score': round(consistency_score, 1),
'letters_typed': letters,
'numbers_typed': numbers,
'special_typed': special,
'spaces_typed': spaces
}
duration = time.time() - start_time
logging.info(f"Performance insights generated in {duration:.2f} seconds")
return result, duration
def close(self):
self.connection.close()
def generate_html(self, output_file='tikker_report.html'):
logging.info("Starting HTML generation...")
overall_start = time.time()
total_events, total_events_duration = self.get_total_events()
pressed_events, pressed_events_duration = self.get_pressed_events()
released_events, released_events_duration = self.get_released_events()
date_range_min, date_range_max, date_range_duration = self.get_date_range()
date_range = (date_range_min, date_range_max)
hourly_data, hourly_duration = self.get_hourly_activity()
daily_data, daily_duration = self.get_daily_activity()
weekday_data, weekday_duration = self.get_weekday_activity()
top_keys, top_keys_duration = self.get_top_keys()
keyboard_heatmap, keyboard_heatmap_duration = self.get_keyboard_heatmap_data()
hour_day_heatmap, hour_day_heatmap_duration = self.get_hour_day_heatmap()
typing_speed, typing_speed_duration = self.get_typing_speed_data()
char_frequency, char_frequency_duration = self.get_character_frequency()
monthly_stats, monthly_stats_duration = self.get_monthly_stats()
peak_times, peak_times_duration = self.get_peak_activity_times()
wpm_stats, wpm_duration = self.get_wpm_stats()
perf_insights, perf_duration = self.get_performance_insights()
durations = {
'total_events': total_events_duration,
'pressed_events': pressed_events_duration,
'released_events': released_events_duration,
'date_range': date_range_duration,
'hourly_activity': hourly_duration,
'daily_activity': daily_duration,
'weekday_activity': weekday_duration,
'top_keys': top_keys_duration,
'keyboard_heatmap': keyboard_heatmap_duration,
'hour_day_heatmap': hour_day_heatmap_duration,
'typing_speed': typing_speed_duration,
'character_frequency': char_frequency_duration,
'monthly_stats': monthly_stats_duration,
'peak_activity_times': peak_times_duration,
'wpm_calculation': wpm_duration,
'performance_insights': perf_duration
}
logging.info("Generating HTML content...")
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tikker Keyboard Performance Report</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {{
--primary: #4f46e5;
--primary-light: #6366f1;
--primary-dark: #4338ca;
--secondary: #64748b;
--background: #f8fafc;
--surface: #ffffff;
--text-primary: #1e293b;
--text-secondary: #475569;
--border: #e2e8f0;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #0ea5e9;
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius-sm: 0.375rem;
--radius: 0.5rem;
--radius-lg: 0.75rem;
}}
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--background);
color: var(--text-primary);
line-height: 1.6;
padding: 2rem;
min-height: 100vh;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}}
header {{
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
padding: 3rem 2.5rem;
text-align: center;
}}
header h1 {{
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.025em;
}}
header p {{
font-size: 1.125rem;
opacity: 0.9;
max-width: 700px;
margin: 0 auto;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 2.5rem;
background: #f1f5f9;
}}
.stat-card {{
background: var(--surface);
padding: 1.75rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--border);
transition: all 0.2s ease;
}}
.stat-card:hover {{
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--primary);
}}
.stat-card h3 {{
color: var(--secondary);
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}}
.stat-card .value {{
font-size: 2.25rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.25rem;
}}
.stat-card .label {{
color: var(--text-secondary);
font-size: 0.875rem;
}}
.content {{
padding: 2.5rem;
}}
.section {{
margin-bottom: 3.5rem;
}}
.section h2 {{
color: var(--text-primary);
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--primary);
display: inline-block;
}}
.section p {{
color: var(--text-secondary);
margin-bottom: 1.5rem;
max-width: 800px;
}}
.chart-container {{
background: var(--surface);
padding: 1.75rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--border);
}}
canvas {{
max-width: 100%;
height: auto;
}}
.bar-horizontal {{
height: 2.5rem;
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
margin-bottom: 0.75rem;
border-radius: var(--radius-sm);
position: relative;
transition: all 0.3s ease;
}}
.bar-horizontal:hover {{
transform: scaleX(1.02);
box-shadow: var(--shadow);
}}
.bar-label {{
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: white;
font-weight: 600;
font-size: 0.875rem;
}}
.bar-value {{
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: white;
font-weight: 600;
font-size: 0.875rem;
}}
.keyboard-grid {{
display: grid;
grid-template-columns: repeat(13, 1fr);
gap: 0.5rem;
margin-top: 1.5rem;
max-width: 900px;
}}
.key {{
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-weight: 600;
font-size: 1rem;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
background: #f1f5f9;
color: var(--text-primary);
}}
.key:hover {{
transform: scale(1.1);
z-index: 10;
box-shadow: var(--shadow);
}}
.heatmap-grid {{
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: 0.125rem;
margin-top: 1.5rem;
max-width: 1200px;
}}
.heatmap-cell {{
aspect-ratio: 1;
border-radius: 0.125rem;
transition: transform 0.2s ease;
background: #e2e8f0;
}}
.heatmap-cell:hover {{
transform: scale(1.5);
z-index: 10;
box-shadow: var(--shadow);
}}
.legend {{
display: flex;
align-items: center;
margin-top: 1.5rem;
gap: 0.75rem;
font-size: 0.875rem;
color: var(--text-secondary);
}}
.legend-gradient {{
height: 1rem;
flex: 1;
max-width: 200px;
border-radius: var(--radius-sm);
background: linear-gradient(90deg,
hsl(240, 100%, 90%) 0%,
hsl(200, 100%, 80%) 25%,
hsl(160, 100%, 70%) 50%,
hsl(80, 100%, 60%) 75%,
hsl(0, 100%, 50%) 100%);
}}
.legend-labels {{
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
}}
.grid-2col {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
}}
table {{
width: 100%;
border-collapse: collapse;
margin-top: 1.5rem;
font-size: 0.875rem;
}}
th {{
background: var(--primary);
color: white;
font-weight: 600;
text-align: left;
padding: 0.75rem 1rem;
font-size: 0.875rem;
}}
td {{
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
}}
tr:hover {{
background: #f8fafc;
}}
.tooltip {{
position: absolute;
background: rgba(15, 23, 42, 0.95);
color: white;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 1000;
box-shadow: var(--shadow);
}}
.durations {{
background: #f1f5f9;
padding: 1.75rem;
border-radius: var(--radius);
margin-bottom: 2.5rem;
border: 1px solid var(--border);
}}
.durations h2 {{
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--primary);
display: inline-block;
}}
.durations ul {{
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.75rem;
}}
.durations li {{
padding: 0.5rem 0;
color: var(--text-secondary);
font-size: 0.875rem;
display: flex;
justify-content: space-between;
}}
.durations li span:first-child {{
font-weight: 500;
color: var(--text-primary);
}}
.performance-metrics {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}}
.metric-card {{
background: #f8fafc;
padding: 1.25rem;
border-radius: var(--radius);
border: 1px solid var(--border);
}}
.metric-card h4 {{
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.metric-card .metric-value {{
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}}
.metric-card .metric-sub {{
font-size: 0.875rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}}
footer {{
background: #f1f5f9;
color: var(--text-secondary);
text-align: center;
padding: 2rem;
margin-top: 3rem;
border-top: 1px solid var(--border);
font-size: 0.875rem;
}}
@media (max-width: 768px) {{
body {{
padding: 1rem;
}}
header h1 {{
font-size: 2rem;
}}
.stats-grid {{
grid-template-columns: 1fr;
padding: 1.5rem;
}}
.content {{
padding: 1.5rem;
}}
.grid-2col {{
grid-template-columns: 1fr;
}}
.keyboard-grid {{
grid-template-columns: repeat(10, 1fr);
}}
.heatmap-grid {{
grid-template-columns: repeat(12, 1fr);
}}
.durations ul {{
grid-template-columns: 1fr;
}}
}}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Tikker Keyboard Performance Report</h1>
<p>Comprehensive Analysis of Typing Speed, Accuracy, and Productivity Metrics</p>
</header>
<div class="stats-grid">
<div class="stat-card" title="Total number of keyboard events recorded (presses and releases)">
<h3>Total Events</h3>
<div class="value">{total_events:,}</div>
<div class="label">Recorded Actions</div>
</div>
<div class="stat-card" title="Number of key press events, primary metric for activity analysis">
<h3>Key Presses</h3>
<div class="value">{pressed_events:,}</div>
<div class="label">Keys Pressed</div>
</div>
<div class="stat-card" title="Average words per minute across entire tracking period">
<h3>Average WPM</h3>
<div class="value">{wpm_stats['wpm_average']}</div>
<div class="label">Words Per Minute</div>
</div>
<div class="stat-card" title="Peak words per minute in a single hour">
<h3>Peak Hour WPM</h3>
<div class="value">{wpm_stats['wpm_peak_hour']}</div>
<div class="label">Max Speed</div>
</div>
<div class="stat-card" title="Duration between first and last recorded event">
<h3>Tracking Period</h3>
<div class="value">{(datetime.fromisoformat(date_range[1]) - datetime.fromisoformat(date_range[0])).days if date_range[0] and date_range[1] else 0}</div>
<div class="label">Days of Data</div>
</div>
<div class="stat-card" title="Estimated total words typed (5 characters per word)">
<h3>Words Typed</h3>
<div class="value">{wpm_stats['words_typed']:,}</div>
<div class="label">Total Output</div>
</div>
</div>
<div class="content">
<div class="durations">
<h2>Data Retrieval Performance</h2>
<ul>
{''.join(f'<li><span>{key.replace("_", " ").title()}:</span> <span>{value:.3f}s</span></li>' for key, value in durations.items())}
</ul>
</div>
<!-- WPM & Performance Section -->
<div class="section">
<h2>Typing Speed & Performance</h2>
<p>Detailed breakdown of words per minute, efficiency, and accuracy metrics.</p>
<div class="performance-metrics">
<div class="metric-card">
<h4>Average WPM</h4>
<div class="metric-value">{wpm_stats['wpm_average']}</div>
<div class="metric-sub">Over {wpm_stats['total_typing_time_minutes']} minutes</div>
</div>
<div class="metric-card">
<h4>Peak Hour WPM</h4>
<div class="metric-value">{wpm_stats['wpm_peak_hour']}</div>
<div class="metric-sub">{wpm_stats['peak_hour'] or 'N/A'}</div>
</div>
<div class="metric-card">
<h4>Typing Efficiency</h4>
<div class="metric-value">{wpm_stats['typing_efficiency']}</div>
<div class="metric-sub">Presses per active minute</div>
</div>
<div class="metric-card">
<h4>Consistency Score</h4>
<div class="metric-value">{perf_insights['consistency_score']}%</div>
<div class="metric-sub">Daily activity stability</div>
</div>
<div class="metric-card">
<h4>Error Rate</h4>
<div class="metric-value">{perf_insights['error_rate_percent']}%</div>
<div class="metric-sub">{perf_insights['total_corrections']:,} corrections</div>
</div>
<div class="metric-card">
<h4>Characters Typed</h4>
<div class="metric-value">{wpm_stats['characters_typed']:,}</div>
<div class="metric-sub">{perf_insights['letters_typed']:,} letters • {perf_insights['spaces_typed']:,} spaces</div>
</div>
</div>
</div>
<div class="section">
<h2>Hourly Activity Distribution</h2>
<p>Average key press volume by hour of day (00:0023:00) across the complete dataset.</p>
<div class="chart-container">
<canvas id="hourlyChart"></canvas>
</div>
</div>
<div class="section">
<h2>Daily Activity Timeline (Last 365 Days)</h2>
<p>Daily key press totals over the past year, illustrating long-term usage patterns.</p>
<div class="chart-container">
<canvas id="dailyChart"></canvas>
</div>
</div>
<div class="grid-2col">
<div class="section">
<h2>Activity by Day of Week</h2>
<p>Distribution of typing activity across weekdays, highlighting weekly patterns.</p>
<div class="chart-container">
<canvas id="weekdayChart"></canvas>
</div>
</div>
<div class="section">
<h2>Monthly Activity Trends</h2>
<p>Monthly key press totals showing seasonal and long-term productivity trends.</p>
<div class="chart-container">
<canvas id="monthlyChart"></canvas>
</div>
</div>
</div>
<div class="section">
<h2>Top 30 Most Frequently Used Keys</h2>
<p>Ranking of the most pressed keys by total count, excluding system keys.</p>
<div class="chart-container">
<div id="topKeysChart"></div>
</div>
</div>
<div class="section">
<h2>Keyboard Usage Heatmap</h2>
<p>Visual intensity map of key usage. Color intensity corresponds to press frequency.</p>
<div class="chart-container">
<div class="keyboard-grid" id="keyboardHeatmap"></div>
<div class="legend">
<span>Low Activity</span>
<div class="legend-gradient"></div>
<span>High Activity</span>
</div>
</div>
</div>
<div class="section">
<h2>Hour × Day Activity Matrix (Last 30 Days)</h2>
<p>Detailed view of typing patterns by hour and day. Each cell represents one hour of activity.</p>
<div class="chart-container">
<div class="heatmap-grid" id="hourDayHeatmap"></div>
<div class="legend-labels">
<span>00:00</span>
<span>06:00</span>
<span>12:00</span>
<span>18:00</span>
<span>23:00</span>
</div>
</div>
</div>
<div class="section">
<h2>Keypresses Per Hour Timeline</h2>
<p>Hourly key press volume over time, highlighting peak productivity periods.</p>
<div class="chart-container">
<canvas id="typingSpeedChart"></canvas>
</div>
</div>
<div class="grid-2col">
<div class="section">
<h2>Letter Frequency Analysis</h2>
<p>Relative frequency of letter usage (A-Z), case-insensitive.</p>
<div class="chart-container">
<canvas id="letterChart"></canvas>
</div>
</div>
<div class="section">
<h2>Number Key Usage</h2>
<p>Frequency distribution of numeric key usage (0-9).</p>
<div class="chart-container">
<canvas id="numberChart"></canvas>
</div>
</div>
</div>
<div class="section">
<h2>Peak Activity Periods</h2>
<p>Top 10 one-hour periods with highest key press volume.</p>
<div class="chart-container">
<table>
<thead>
<tr>
<th>Rank</th>
<th>Time Period</th>
<th>Key Presses</th>
</tr>
</thead>
<tbody>
{''.join(f'<tr><td>{i+1}</td><td>{time}</td><td>{count:,}</td></tr>' for i, (time, count) in enumerate(peak_times))}
</tbody>
</table>
</div>
</div>
</div>
<footer>
<p>Report generated on {datetime.now().strftime('%Y-%m-%d at %H:%M:%S')}</p>
<p>Tikker Keyboard Performance Analytics © {datetime.now().year} • Enterprise Edition</p>
</footer>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
const hourlyData = {json.dumps([[h, c] for h, c in hourly_data])};
const dailyData = {json.dumps([[d, c] for d, c in daily_data])};
const weekdayData = {json.dumps([[w, c] for w, c in weekday_data])};
const topKeysData = {json.dumps([[k, c] for k, c in top_keys])};
const keyboardHeatmapData = {json.dumps(keyboard_heatmap)};
const hourDayHeatmapData = {json.dumps([[d, h, c] for d, h, c in hour_day_heatmap])};
const typingSpeedData = {json.dumps([[t, c] for t, c in typing_speed])};
const letterFrequency = {json.dumps(char_frequency['letters'])};
const numberFrequency = {json.dumps(char_frequency['numbers'])};
const monthlyData = {json.dumps([[m, c] for m, c in monthly_stats])};
const tooltip = document.getElementById('tooltip');
function createBarChart(canvasId, labels, data, label, color) {{
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const width = canvas.parentElement.clientWidth;
const height = 400;
canvas.width = width;
canvas.height = height;
const maxValue = Math.max(...data);
const padding = 60;
const barWidth = (width - padding * 2) / data.length;
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {{
const y = padding + (height - padding * 2) * i / 5;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.fillStyle = '#64748b';
ctx.font = '12px Inter';
ctx.textAlign = 'right';
ctx.fillText(Math.round(maxValue * (1 - i / 5)).toLocaleString(), padding - 10, y + 4);
}}
const chartRects = [];
data.forEach((value, index) => {{
const barHeight = (value / maxValue) * (height - padding * 2);
const x = padding + index * barWidth;
const y = height - padding - barHeight;
const gradient = ctx.createLinearGradient(x, y, x, height - padding);
gradient.addColorStop(0, color);
gradient.addColorStop(1, color + 'cc');
ctx.fillStyle = gradient;
ctx.fillRect(x + barWidth * 0.1, y, barWidth * 0.8, barHeight);
chartRects.push({{
x: x + barWidth * 0.1,
y: y,
width: barWidth * 0.8,
height: barHeight,
label: labels[index],
value: value.toLocaleString()
}});
if (data.length <= 24) {{
ctx.save();
ctx.translate(x + barWidth * 0.5, height - padding + 20);
ctx.rotate(-Math.PI / 3);
ctx.fillStyle = '#64748b';
ctx.font = '11px Inter';
ctx.textAlign = 'right';
ctx.fillText(labels[index], 0, 0);
ctx.restore();
}}
}});
canvas.addEventListener('mousemove', (e) => {{
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let found = false;
for (const bar of chartRects) {{
if (x >= bar.x && x <= bar.x + bar.width && y >= bar.y && y <= bar.y + bar.height) {{
tooltip.innerHTML = `<strong>${{bar.label}}</strong><br>${{bar.value}} key presses`;
tooltip.style.opacity = '1';
tooltip.style.left = `${{e.pageX + 12}}px`;
tooltip.style.top = `${{e.pageY + 12}}px`;
found = true;
break;
}}
}}
if (!found) {{ tooltip.style.opacity = '0'; }}
}});
canvas.addEventListener('mouseout', () => {{ tooltip.style.opacity = '0'; }});
}}
function createLineChart(canvasId, labels, data, label, color) {{
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const width = canvas.parentElement.clientWidth;
const height = 400;
canvas.width = width;
canvas.height = height;
const maxValue = Math.max(...data);
const padding = 60;
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {{
const y = padding + (height - padding * 2) * i / 5;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.fillStyle = '#64748b';
ctx.font = '12px Inter';
ctx.textAlign = 'right';
ctx.fillText(Math.round(maxValue * (1 - i / 5)).toLocaleString(), padding - 10, y + 4);
}}
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
const chartPoints = [];
data.forEach((value, index) => {{
const x = padding + (index / (data.length - 1)) * (width - padding * 2);
const y = height - padding - (value / maxValue) * (height - padding * 2);
if (index === 0) {{
ctx.moveTo(x, y);
}} else {{
ctx.lineTo(x, y);
}}
chartPoints.push({{
x: x,
y: y,
radius: 8,
label: labels[index],
value: value.toLocaleString()
}});
}});
ctx.stroke();
ctx.fillStyle = color + '15';
ctx.lineTo(width - padding, height - padding);
ctx.lineTo(padding, height - padding);
ctx.closePath();
ctx.fill();
ctx.fillStyle = color;
chartPoints.forEach(point => {{
ctx.beginPath();
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
ctx.fill();
}});
canvas.addEventListener('mousemove', (e) => {{
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let found = false;
for (const point of chartPoints) {{
const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2));
if (distance <= point.radius) {{
tooltip.innerHTML = `<strong>${{point.label}}</strong><br>${{point.value}} key presses`;
tooltip.style.opacity = '1';
tooltip.style.left = `${{e.pageX + 12}}px`;
tooltip.style.top = `${{e.pageY + 12}}px`;
found = true;
break;
}}
}}
if (!found) {{ tooltip.style.opacity = '0'; }}
}});
canvas.addEventListener('mouseout', () => {{ tooltip.style.opacity = '0'; }});
}}
function createPieChart(canvasId, labels, data, colors) {{
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const width = canvas.parentElement.clientWidth;
const height = 400;
canvas.width = width;
canvas.height = height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 60;
const total = data.reduce((sum, val) => sum + val, 0);
let currentAngle = -Math.PI / 2;
const chartSlices = [];
data.forEach((value, index) => {{
const sliceAngle = (value / total) * 2 * Math.PI;
ctx.fillStyle = colors[index % colors.length];
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
chartSlices.push({{
startAngle: currentAngle,
endAngle: currentAngle + sliceAngle,
label: labels[index],
value: value.toLocaleString(),
percent: (value / total * 100).toFixed(1)
}});
const labelAngle = currentAngle + sliceAngle / 2;
const labelX = centerX + Math.cos(labelAngle) * (radius * 0.65);
const labelY = centerY + Math.sin(labelAngle) * (radius * 0.65);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 13px Inter';
ctx.textAlign = 'center';
ctx.fillText(labels[index].substring(0, 3), labelX, labelY);
currentAngle += sliceAngle;
}});
canvas.addEventListener('mousemove', (e) => {{
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left - centerX;
const y = e.clientY - rect.top - centerY;
const distance = Math.sqrt(x*x + y*y);
let angle = Math.atan2(y, x);
if (angle < -Math.PI / 2) {{
angle += 2 * Math.PI;
}}
let found = false;
if (distance <= radius) {{
for (const slice of chartSlices) {{
if (angle >= slice.startAngle && angle <= slice.endAngle) {{
tooltip.innerHTML = `<strong>${{slice.label}}</strong><br>${{slice.value}} presses (${{slice.percent}}%)`;
tooltip.style.opacity = '1';
tooltip.style.left = `${{e.pageX + 12}}px`;
tooltip.style.top = `${{e.pageY + 12}}px`;
found = true;
break;
}}
}}
}}
if (!found) {{ tooltip.style.opacity = '0'; }}
}});
canvas.addEventListener('mouseout', () => {{ tooltip.style.opacity = '0'; }});
}}
createBarChart('hourlyChart',
hourlyData.map(d => d[0].padStart(2, '0') + ':00'),
hourlyData.map(d => d[1]),
'Key Presses',
'#4f46e5');
createLineChart('dailyChart',
dailyData.map(d => d[0]),
dailyData.map(d => d[1]),
'Daily Activity',
'#10b981');
const weekdayColors = ['#ef4444', '#f97316', '#fbbf24', '#a3e635', '#22d3ee', '#818cf8', '#c084fc'];
createPieChart('weekdayChart',
weekdayData.map(d => d[0]),
weekdayData.map(d => d[1]),
weekdayColors);
createBarChart('monthlyChart',
monthlyData.map(d => d[0]),
monthlyData.map(d => d[1]),
'Monthly Key Presses',
'#0ea5e9');
const topKeysContainer = document.getElementById('topKeysChart');
const maxKeyValue = Math.max(...topKeysData.map(d => d[1]));
topKeysData.forEach(([key, count]) => {{
const bar = document.createElement('div');
bar.className = 'bar-horizontal';
bar.style.width = (count / maxKeyValue * 100) + '%';
const label = document.createElement('span');
label.className = 'bar-label';
label.textContent = key.length === 1 ? key : key.substring(0, 12);
const value = document.createElement('span');
value.className = 'bar-value';
value.textContent = count.toLocaleString();
bar.appendChild(label);
bar.appendChild(value);
bar.title = `${{key}}: ${{count.toLocaleString()}} presses`;
topKeysContainer.appendChild(bar);
}});
const keyboardLayout = [
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\\\'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"],
['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/']
];
const keyboardContainer = document.getElementById('keyboardHeatmap');
const allCounts = Object.values(keyboardHeatmapData).sort((a, b) => a - b);
const percentileMax = allCounts[Math.floor(allCounts.length * 0.98)];
function getHeatColor(value, max) {{
if (value === 0) return '#e2e8f0';
const logValue = Math.log(value + 1);
const logMax = Math.log(max + 1);
const ratio = logValue / logMax;
const hue = (1 - ratio) * 240;
return `hsl(${{hue}}, 100%, 50%)`;
}}
keyboardLayout.forEach(row => {{
row.forEach(key => {{
const keyEl = document.createElement('div');
keyEl.className = 'key';
keyEl.textContent = key;
const count = keyboardHeatmapData[key] || keyboardHeatmapData[key.toUpperCase()] || 0;
keyEl.style.background = count > 0 ? getHeatColor(count, percentileMax) : '#f1f5f9';
keyEl.style.color = count > 5000 ? 'white' : '#1e293b';
keyEl.title = `${{key}}: ${{count.toLocaleString()}} presses`;
keyboardContainer.appendChild(keyEl);
}});
}});
const heatmapContainer = document.getElementById('hourDayHeatmap');
const heatmapMax = Math.max(...hourDayHeatmapData.map(d => d[2]));
const uniqueDays = [...new Set(hourDayHeatmapData.map(d => d[0]))].sort();
const heatmapMap = {{}};
hourDayHeatmapData.forEach(([day, hour, count]) => {{
heatmapMap[`${{day}}-${{hour}}`] = count;
}});
uniqueDays.forEach(day => {{
for (let hour = 0; hour < 24; hour++) {{
const key = `${{day}}-${{hour.toString().padStart(2, '0')}}`;
const count = heatmapMap[key] || 0;
const cell = document.createElement('div');
cell.className = 'heatmap-cell';
cell.style.background = count > 0 ? getHeatColor(count, heatmapMax) : '#e2e8f0';
cell.title = `${{day}} ${{hour}}:00 — ${{count.toLocaleString()}} presses`;
heatmapContainer.appendChild(cell);
}}
}});
createLineChart('typingSpeedChart',
typingSpeedData.map(d => d[0]),
typingSpeedData.map(d => d[1]),
'Key Presses per Hour',
'#f59e0b');
createBarChart('letterChart',
Object.keys(letterFrequency),
Object.values(letterFrequency),
'Letter Frequency',
'#10b981');
createBarChart('numberChart',
Object.keys(numberFrequency),
Object.values(numberFrequency),
'Number Usage',
'#f97316');
window.addEventListener('resize', () => {{
['hourlyChart', 'dailyChart', 'weekdayChart', 'monthlyChart', 'typingSpeedChart', 'letterChart', 'numberChart'].forEach(id => {{
const canvas = document.getElementById(id);
if (canvas) canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
}});
createBarChart('hourlyChart', hourlyData.map(d => d[0].padStart(2, '0') + ':00'), hourlyData.map(d => d[1]), 'Key Presses', '#4f46e5');
createLineChart('dailyChart', dailyData.map(d => d[0]), dailyData.map(d => d[1]), 'Daily Activity', '#10b981');
createPieChart('weekdayChart', weekdayData.map(d => d[0]), weekdayData.map(d => d[1]), weekdayColors);
createBarChart('monthlyChart', monthlyData.map(d => d[0]), monthlyData.map(d => d[1]), 'Monthly Key Presses', '#0ea5e9');
createLineChart('typingSpeedChart', typingSpeedData.map(d => d[0]), typingSpeedData.map(d => d[1]), 'Key Presses per Hour', '#f59e0b');
createBarChart('letterChart', Object.keys(letterFrequency), Object.values(letterFrequency), 'Letter Frequency', '#10b981');
createBarChart('numberChart', Object.keys(numberFrequency), Object.values(numberFrequency), 'Number Usage', '#f97316');
}});
</script>
</body>
</html>"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html)
overall_duration = time.time() - overall_start
logging.info(f"Enterprise HTML report generated in {overall_duration:.2f} seconds: {output_file}")
logging.info(f"Total events analyzed: {total_events:,}")
print(f"Enterprise performance report generated: {output_file}")
if __name__ == '__main__':
db_path = sys.argv[1] if len(sys.argv) > 1 else 'tikker.db'
output_path = sys.argv[2] if len(sys.argv) > 2 else 'tikker_report.html'
visualizer = TikkerVisualizer(db_path)
visualizer.generate_html(output_path)
visualizer.close()