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:00– 23: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 ( )