GameSentenceMiner 2.18.14__py3-none-any.whl → 2.18.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/anki.py +8 -53
- GameSentenceMiner/obs.py +1 -2
- GameSentenceMiner/ui/anki_confirmation.py +16 -2
- GameSentenceMiner/util/db.py +11 -7
- GameSentenceMiner/util/games_table.py +320 -0
- GameSentenceMiner/vad.py +3 -3
- GameSentenceMiner/web/anki_api_endpoints.py +506 -0
- GameSentenceMiner/web/database_api.py +239 -117
- GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
- GameSentenceMiner/web/static/css/search.css +54 -0
- GameSentenceMiner/web/static/css/stats.css +76 -0
- GameSentenceMiner/web/static/js/anki_stats.js +304 -50
- GameSentenceMiner/web/static/js/database.js +44 -7
- GameSentenceMiner/web/static/js/heatmap.js +326 -0
- GameSentenceMiner/web/static/js/overview.js +20 -224
- GameSentenceMiner/web/static/js/search.js +190 -23
- GameSentenceMiner/web/static/js/stats.js +371 -1
- GameSentenceMiner/web/stats.py +188 -0
- GameSentenceMiner/web/templates/anki_stats.html +145 -58
- GameSentenceMiner/web/templates/components/date-range.html +19 -0
- GameSentenceMiner/web/templates/components/html-head.html +45 -0
- GameSentenceMiner/web/templates/components/js-config.html +37 -0
- GameSentenceMiner/web/templates/components/popups.html +15 -0
- GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
- GameSentenceMiner/web/templates/database.html +13 -3
- GameSentenceMiner/web/templates/goals.html +9 -31
- GameSentenceMiner/web/templates/overview.html +16 -223
- GameSentenceMiner/web/templates/search.html +46 -0
- GameSentenceMiner/web/templates/stats.html +49 -311
- GameSentenceMiner/web/texthooking_page.py +4 -66
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/RECORD +36 -27
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/top_level.txt +0 -0
|
@@ -527,6 +527,301 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
527
527
|
return `Speed: ${charsPerHour.toLocaleString()} chars/hour`;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
// Function to create hourly activity bar chart
|
|
531
|
+
function createHourlyActivityChart(canvasId, hourlyData) {
|
|
532
|
+
const canvas = document.getElementById(canvasId);
|
|
533
|
+
if (!canvas || !hourlyData || !Array.isArray(hourlyData)) return null;
|
|
534
|
+
|
|
535
|
+
const ctx = canvas.getContext('2d');
|
|
536
|
+
|
|
537
|
+
// Destroy existing chart if it exists
|
|
538
|
+
if (window.myCharts[canvasId]) {
|
|
539
|
+
window.myCharts[canvasId].destroy();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Create hour labels (0-23)
|
|
543
|
+
const hourLabels = [];
|
|
544
|
+
for (let i = 0; i < 24; i++) {
|
|
545
|
+
const hour12 = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
|
546
|
+
const ampm = i < 12 ? 'AM' : 'PM';
|
|
547
|
+
hourLabels.push(`${hour12}${ampm}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Generate gradient colors for bars based on activity values
|
|
551
|
+
const maxActivity = Math.max(...hourlyData.filter(activity => activity > 0));
|
|
552
|
+
const minActivity = Math.min(...hourlyData.filter(activity => activity > 0));
|
|
553
|
+
|
|
554
|
+
const barColors = hourlyData.map(activity => {
|
|
555
|
+
if (activity === 0) {
|
|
556
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Create color gradient from red (low activity) to green (high activity)
|
|
560
|
+
const normalizedActivity = maxActivity > minActivity ? (activity - minActivity) / (maxActivity - minActivity) : 0.5;
|
|
561
|
+
const hue = normalizedActivity * 120; // 0 = red, 120 = green
|
|
562
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const borderColors = hourlyData.map(activity => {
|
|
566
|
+
if (activity === 0) {
|
|
567
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const normalizedActivity = maxActivity > minActivity ? (activity - minActivity) / (maxActivity - minActivity) : 0.5;
|
|
571
|
+
const hue = normalizedActivity * 120;
|
|
572
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
576
|
+
type: 'bar',
|
|
577
|
+
data: {
|
|
578
|
+
labels: hourLabels,
|
|
579
|
+
datasets: [{
|
|
580
|
+
label: 'Characters Read',
|
|
581
|
+
data: hourlyData,
|
|
582
|
+
backgroundColor: barColors,
|
|
583
|
+
borderColor: borderColors,
|
|
584
|
+
borderWidth: 2
|
|
585
|
+
}]
|
|
586
|
+
},
|
|
587
|
+
options: {
|
|
588
|
+
responsive: true,
|
|
589
|
+
plugins: {
|
|
590
|
+
legend: {
|
|
591
|
+
display: false // Hide legend for cleaner look
|
|
592
|
+
},
|
|
593
|
+
title: {
|
|
594
|
+
display: true,
|
|
595
|
+
text: 'Reading Activity by Hour of Day',
|
|
596
|
+
color: getThemeTextColor(),
|
|
597
|
+
font: {
|
|
598
|
+
size: 16,
|
|
599
|
+
weight: 'bold'
|
|
600
|
+
},
|
|
601
|
+
padding: {
|
|
602
|
+
top: 10,
|
|
603
|
+
bottom: 20
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
tooltip: {
|
|
607
|
+
callbacks: {
|
|
608
|
+
title: function(context) {
|
|
609
|
+
const hourIndex = context[0].dataIndex;
|
|
610
|
+
const hour24 = hourIndex;
|
|
611
|
+
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
612
|
+
const ampm = hour24 < 12 ? 'AM' : 'PM';
|
|
613
|
+
return `${hour12}:00 ${ampm} (${hour24}:00)`;
|
|
614
|
+
},
|
|
615
|
+
label: function(context) {
|
|
616
|
+
const activity = context.parsed.y;
|
|
617
|
+
if (activity === 0) {
|
|
618
|
+
return 'No reading activity';
|
|
619
|
+
}
|
|
620
|
+
return `Characters: ${activity.toLocaleString()}`;
|
|
621
|
+
},
|
|
622
|
+
afterLabel: function(context) {
|
|
623
|
+
const activity = context.parsed.y;
|
|
624
|
+
if (activity === 0) return '';
|
|
625
|
+
|
|
626
|
+
const total = hourlyData.reduce((sum, val) => sum + val, 0);
|
|
627
|
+
const percentage = total > 0 ? ((activity / total) * 100).toFixed(1) : '0.0';
|
|
628
|
+
return `${percentage}% of total activity`;
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
632
|
+
titleColor: '#fff',
|
|
633
|
+
bodyColor: '#fff',
|
|
634
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
635
|
+
borderWidth: 1,
|
|
636
|
+
cornerRadius: 8,
|
|
637
|
+
displayColors: true
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
scales: {
|
|
641
|
+
y: {
|
|
642
|
+
beginAtZero: true,
|
|
643
|
+
title: {
|
|
644
|
+
display: true,
|
|
645
|
+
text: 'Characters Read',
|
|
646
|
+
color: getThemeTextColor()
|
|
647
|
+
},
|
|
648
|
+
ticks: {
|
|
649
|
+
color: getThemeTextColor(),
|
|
650
|
+
callback: function(value) {
|
|
651
|
+
return value.toLocaleString();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
x: {
|
|
656
|
+
title: {
|
|
657
|
+
display: true,
|
|
658
|
+
text: 'Hour of Day',
|
|
659
|
+
color: getThemeTextColor()
|
|
660
|
+
},
|
|
661
|
+
ticks: {
|
|
662
|
+
color: getThemeTextColor()
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
animation: {
|
|
667
|
+
duration: 1000,
|
|
668
|
+
easing: 'easeOutQuart'
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return window.myCharts[canvasId];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Function to create hourly reading speed bar chart
|
|
677
|
+
function createHourlyReadingSpeedChart(canvasId, hourlySpeedData) {
|
|
678
|
+
const canvas = document.getElementById(canvasId);
|
|
679
|
+
if (!canvas || !hourlySpeedData || !Array.isArray(hourlySpeedData)) return null;
|
|
680
|
+
|
|
681
|
+
const ctx = canvas.getContext('2d');
|
|
682
|
+
|
|
683
|
+
// Destroy existing chart if it exists
|
|
684
|
+
if (window.myCharts[canvasId]) {
|
|
685
|
+
window.myCharts[canvasId].destroy();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Create hour labels (0-23)
|
|
689
|
+
const hourLabels = [];
|
|
690
|
+
for (let i = 0; i < 24; i++) {
|
|
691
|
+
const hour12 = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
|
692
|
+
const ampm = i < 12 ? 'AM' : 'PM';
|
|
693
|
+
hourLabels.push(`${hour12}${ampm}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Generate gradient colors for bars based on speed values
|
|
697
|
+
const maxSpeed = Math.max(...hourlySpeedData.filter(speed => speed > 0));
|
|
698
|
+
const minSpeed = Math.min(...hourlySpeedData.filter(speed => speed > 0));
|
|
699
|
+
|
|
700
|
+
const barColors = hourlySpeedData.map(speed => {
|
|
701
|
+
if (speed === 0) {
|
|
702
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Create color gradient from red (slow) to green (fast)
|
|
706
|
+
const normalizedSpeed = maxSpeed > minSpeed ? (speed - minSpeed) / (maxSpeed - minSpeed) : 0.5;
|
|
707
|
+
const hue = normalizedSpeed * 120; // 0 = red, 120 = green
|
|
708
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const borderColors = hourlySpeedData.map(speed => {
|
|
712
|
+
if (speed === 0) {
|
|
713
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const normalizedSpeed = maxSpeed > minSpeed ? (speed - minSpeed) / (maxSpeed - minSpeed) : 0.5;
|
|
717
|
+
const hue = normalizedSpeed * 120;
|
|
718
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
722
|
+
type: 'bar',
|
|
723
|
+
data: {
|
|
724
|
+
labels: hourLabels,
|
|
725
|
+
datasets: [{
|
|
726
|
+
label: 'Average Reading Speed',
|
|
727
|
+
data: hourlySpeedData,
|
|
728
|
+
backgroundColor: barColors,
|
|
729
|
+
borderColor: borderColors,
|
|
730
|
+
borderWidth: 2
|
|
731
|
+
}]
|
|
732
|
+
},
|
|
733
|
+
options: {
|
|
734
|
+
responsive: true,
|
|
735
|
+
plugins: {
|
|
736
|
+
legend: {
|
|
737
|
+
display: false // Hide legend for cleaner look
|
|
738
|
+
},
|
|
739
|
+
title: {
|
|
740
|
+
display: true,
|
|
741
|
+
text: 'Average Reading Speed by Hour',
|
|
742
|
+
color: getThemeTextColor(),
|
|
743
|
+
font: {
|
|
744
|
+
size: 16,
|
|
745
|
+
weight: 'bold'
|
|
746
|
+
},
|
|
747
|
+
padding: {
|
|
748
|
+
top: 10,
|
|
749
|
+
bottom: 20
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
tooltip: {
|
|
753
|
+
callbacks: {
|
|
754
|
+
title: function(context) {
|
|
755
|
+
const hourIndex = context[0].dataIndex;
|
|
756
|
+
const hour24 = hourIndex;
|
|
757
|
+
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
758
|
+
const ampm = hour24 < 12 ? 'AM' : 'PM';
|
|
759
|
+
return `${hour12}:00 ${ampm} (${hour24}:00)`;
|
|
760
|
+
},
|
|
761
|
+
label: function(context) {
|
|
762
|
+
const speed = context.parsed.y;
|
|
763
|
+
if (speed === 0) {
|
|
764
|
+
return 'No reading activity';
|
|
765
|
+
}
|
|
766
|
+
return `Speed: ${speed.toLocaleString()} chars/hour`;
|
|
767
|
+
},
|
|
768
|
+
afterLabel: function(context) {
|
|
769
|
+
const speed = context.parsed.y;
|
|
770
|
+
if (speed === 0) return '';
|
|
771
|
+
|
|
772
|
+
const nonZeroSpeeds = hourlySpeedData.filter(s => s > 0);
|
|
773
|
+
const avgSpeed = nonZeroSpeeds.reduce((sum, s) => sum + s, 0) / nonZeroSpeeds.length;
|
|
774
|
+
const comparison = speed > avgSpeed ? 'above' : speed < avgSpeed ? 'below' : 'at';
|
|
775
|
+
const percentage = avgSpeed > 0 ? Math.abs(((speed - avgSpeed) / avgSpeed) * 100).toFixed(1) : '0';
|
|
776
|
+
|
|
777
|
+
return `${percentage}% ${comparison} average`;
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
781
|
+
titleColor: '#fff',
|
|
782
|
+
bodyColor: '#fff',
|
|
783
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
784
|
+
borderWidth: 1,
|
|
785
|
+
cornerRadius: 8,
|
|
786
|
+
displayColors: true
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
scales: {
|
|
790
|
+
y: {
|
|
791
|
+
beginAtZero: true,
|
|
792
|
+
title: {
|
|
793
|
+
display: true,
|
|
794
|
+
text: 'Characters per Hour',
|
|
795
|
+
color: getThemeTextColor()
|
|
796
|
+
},
|
|
797
|
+
ticks: {
|
|
798
|
+
color: getThemeTextColor(),
|
|
799
|
+
callback: function(value) {
|
|
800
|
+
return value.toLocaleString();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
x: {
|
|
805
|
+
title: {
|
|
806
|
+
display: true,
|
|
807
|
+
text: 'Hour of Day',
|
|
808
|
+
color: getThemeTextColor()
|
|
809
|
+
},
|
|
810
|
+
ticks: {
|
|
811
|
+
color: getThemeTextColor()
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
animation: {
|
|
816
|
+
duration: 1000,
|
|
817
|
+
easing: 'easeOutQuart'
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
return window.myCharts[canvasId];
|
|
823
|
+
}
|
|
824
|
+
|
|
530
825
|
// Initialize Kanji Grid Renderer (using shared component)
|
|
531
826
|
const kanjiGridRenderer = new KanjiGridRenderer({
|
|
532
827
|
containerSelector: '#kanjiGrid',
|
|
@@ -540,10 +835,70 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
540
835
|
kanjiGridRenderer.render(kanjiData);
|
|
541
836
|
}
|
|
542
837
|
|
|
838
|
+
// Function to update peak statistics display
|
|
839
|
+
function updatePeakStatistics(peakDailyStats, peakSessionStats) {
|
|
840
|
+
// Helper function to format large numbers
|
|
841
|
+
function formatLargeNumber(num) {
|
|
842
|
+
if (num >= 1000000) {
|
|
843
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
844
|
+
} else if (num >= 1000) {
|
|
845
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
846
|
+
} else {
|
|
847
|
+
return num.toString();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Helper function to format time in human-readable format
|
|
852
|
+
function formatTimeHuman(hours) {
|
|
853
|
+
if (hours < 1) {
|
|
854
|
+
const minutes = Math.round(hours * 60);
|
|
855
|
+
return minutes + 'm';
|
|
856
|
+
} else if (hours < 24) {
|
|
857
|
+
const wholeHours = Math.floor(hours);
|
|
858
|
+
const minutes = Math.round((hours - wholeHours) * 60);
|
|
859
|
+
if (minutes > 0) {
|
|
860
|
+
return wholeHours + 'h ' + minutes + 'm';
|
|
861
|
+
} else {
|
|
862
|
+
return wholeHours + 'h';
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
const days = Math.floor(hours / 24);
|
|
866
|
+
const remainingHours = Math.floor(hours % 24);
|
|
867
|
+
if (remainingHours > 0) {
|
|
868
|
+
return days + 'd ' + remainingHours + 'h';
|
|
869
|
+
} else {
|
|
870
|
+
return days + 'd';
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Update the display elements
|
|
876
|
+
const maxDailyCharsEl = document.getElementById('maxDailyChars');
|
|
877
|
+
const maxDailyHoursEl = document.getElementById('maxDailyHours');
|
|
878
|
+
const longestSessionEl = document.getElementById('longestSession');
|
|
879
|
+
const maxSessionCharsEl = document.getElementById('maxSessionChars');
|
|
880
|
+
|
|
881
|
+
if (maxDailyCharsEl) {
|
|
882
|
+
maxDailyCharsEl.textContent = formatLargeNumber(peakDailyStats.max_daily_chars || 0);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (maxDailyHoursEl) {
|
|
886
|
+
maxDailyHoursEl.textContent = formatTimeHuman(peakDailyStats.max_daily_hours || 0);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (longestSessionEl) {
|
|
890
|
+
longestSessionEl.textContent = formatTimeHuman(peakSessionStats.longest_session_hours || 0);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (maxSessionCharsEl) {
|
|
894
|
+
maxSessionCharsEl.textContent = formatLargeNumber(peakSessionStats.max_session_chars || 0);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
543
898
|
function showNoDataPopup() {
|
|
544
899
|
const popup = document.getElementById("noDataPopup");
|
|
545
900
|
if (popup) popup.classList.remove("hidden");
|
|
546
|
-
}
|
|
901
|
+
}
|
|
547
902
|
|
|
548
903
|
const closeNoDataPopup = document.getElementById("closeNoDataPopup");
|
|
549
904
|
if (closeNoDataPopup) {
|
|
@@ -618,11 +973,26 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
618
973
|
createGameBarChartWithCustomFormat('readingSpeedPerGameChart', data.readingSpeedPerGame, 'Reading Speed Improvement', 'Speed (chars/hour)', formatSpeed, true);
|
|
619
974
|
}
|
|
620
975
|
|
|
976
|
+
// Create hourly activity polar chart if data exists
|
|
977
|
+
if (data.hourlyActivityData) {
|
|
978
|
+
createHourlyActivityChart('hourlyActivityChart', data.hourlyActivityData);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Create hourly reading speed chart if data exists
|
|
982
|
+
if (data.hourlyReadingSpeedData) {
|
|
983
|
+
createHourlyReadingSpeedChart('hourlyReadingSpeedChart', data.hourlyReadingSpeedData);
|
|
984
|
+
}
|
|
985
|
+
|
|
621
986
|
// Create kanji grid if data exists
|
|
622
987
|
if (data.kanjiGridData) {
|
|
623
988
|
createKanjiGrid(data.kanjiGridData);
|
|
624
989
|
}
|
|
625
990
|
|
|
991
|
+
// Update peak statistics if data exists
|
|
992
|
+
if (data.peakDailyStats && data.peakSessionStats) {
|
|
993
|
+
updatePeakStatistics(data.peakDailyStats, data.peakSessionStats);
|
|
994
|
+
}
|
|
995
|
+
|
|
626
996
|
return data;
|
|
627
997
|
})
|
|
628
998
|
.catch(error => {
|
GameSentenceMiner/web/stats.py
CHANGED
|
@@ -139,6 +139,34 @@ def calculate_heatmap_data(all_lines, filter_year=None):
|
|
|
139
139
|
return dict(heatmap_data)
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def calculate_mining_heatmap_data(all_lines, filter_year=None):
|
|
143
|
+
"""
|
|
144
|
+
Calculate heatmap data for mining activity.
|
|
145
|
+
Counts lines where screenshot_in_anki OR audio_in_anki is not empty.
|
|
146
|
+
"""
|
|
147
|
+
heatmap_data = defaultdict(lambda: defaultdict(int))
|
|
148
|
+
|
|
149
|
+
for line in all_lines:
|
|
150
|
+
# Check if line has been mined (either screenshot or audio in Anki)
|
|
151
|
+
has_screenshot = line.screenshot_in_anki and line.screenshot_in_anki.strip()
|
|
152
|
+
has_audio = line.audio_in_anki and line.audio_in_anki.strip()
|
|
153
|
+
|
|
154
|
+
if not (has_screenshot or has_audio):
|
|
155
|
+
continue # Skip lines that haven't been mined
|
|
156
|
+
|
|
157
|
+
date_obj = datetime.date.fromtimestamp(float(line.timestamp))
|
|
158
|
+
year = str(date_obj.year)
|
|
159
|
+
|
|
160
|
+
# Filter by year if specified
|
|
161
|
+
if filter_year and year != filter_year:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
date_str = date_obj.strftime('%Y-%m-%d')
|
|
165
|
+
heatmap_data[year][date_str] += 1 # Count mined lines, not characters
|
|
166
|
+
|
|
167
|
+
return dict(heatmap_data)
|
|
168
|
+
|
|
169
|
+
|
|
142
170
|
def calculate_total_chars_per_game(all_lines):
|
|
143
171
|
"""Calculate total characters read per game."""
|
|
144
172
|
game_data = defaultdict(lambda: {'total_chars': 0, 'first_time': None})
|
|
@@ -515,6 +543,166 @@ def calculate_average_daily_reading_time(all_lines):
|
|
|
515
543
|
|
|
516
544
|
return average_hours
|
|
517
545
|
|
|
546
|
+
def calculate_hourly_activity(all_lines):
|
|
547
|
+
"""
|
|
548
|
+
Calculate reading activity aggregated by hour of day (0-23).
|
|
549
|
+
Returns character count for each hour across all days.
|
|
550
|
+
"""
|
|
551
|
+
if not all_lines:
|
|
552
|
+
return [0] * 24
|
|
553
|
+
|
|
554
|
+
hourly_chars = [0] * 24
|
|
555
|
+
|
|
556
|
+
for line in all_lines:
|
|
557
|
+
# Get hour from timestamp (0-23)
|
|
558
|
+
hour = datetime.datetime.fromtimestamp(float(line.timestamp)).hour
|
|
559
|
+
char_count = len(line.line_text) if line.line_text else 0
|
|
560
|
+
hourly_chars[hour] += char_count
|
|
561
|
+
|
|
562
|
+
return hourly_chars
|
|
563
|
+
|
|
564
|
+
def calculate_hourly_reading_speed(all_lines):
|
|
565
|
+
"""
|
|
566
|
+
Calculate average reading speed (chars/hour) aggregated by hour of day (0-23).
|
|
567
|
+
Returns average reading speed for each hour across all days.
|
|
568
|
+
"""
|
|
569
|
+
if not all_lines:
|
|
570
|
+
return [0] * 24
|
|
571
|
+
|
|
572
|
+
# Group lines by hour and collect timestamps for each hour
|
|
573
|
+
hourly_data = defaultdict(lambda: {'chars': 0, 'timestamps': []})
|
|
574
|
+
|
|
575
|
+
for line in all_lines:
|
|
576
|
+
hour = datetime.datetime.fromtimestamp(float(line.timestamp)).hour
|
|
577
|
+
char_count = len(line.line_text) if line.line_text else 0
|
|
578
|
+
|
|
579
|
+
hourly_data[hour]['chars'] += char_count
|
|
580
|
+
hourly_data[hour]['timestamps'].append(float(line.timestamp))
|
|
581
|
+
|
|
582
|
+
# Calculate average reading speed for each hour
|
|
583
|
+
hourly_speeds = [0] * 24
|
|
584
|
+
|
|
585
|
+
for hour in range(24):
|
|
586
|
+
if hour in hourly_data and len(hourly_data[hour]['timestamps']) >= 2:
|
|
587
|
+
chars = hourly_data[hour]['chars']
|
|
588
|
+
timestamps = hourly_data[hour]['timestamps']
|
|
589
|
+
|
|
590
|
+
# Calculate actual reading time for this hour across all days
|
|
591
|
+
reading_time_seconds = calculate_actual_reading_time(timestamps)
|
|
592
|
+
reading_time_hours = reading_time_seconds / 3600
|
|
593
|
+
|
|
594
|
+
# Calculate speed (chars per hour)
|
|
595
|
+
if reading_time_hours > 0:
|
|
596
|
+
hourly_speeds[hour] = int(chars / reading_time_hours)
|
|
597
|
+
|
|
598
|
+
return hourly_speeds
|
|
599
|
+
|
|
600
|
+
def calculate_peak_daily_stats(all_lines):
|
|
601
|
+
"""
|
|
602
|
+
Calculate peak daily statistics: most chars read in a day and most hours studied in a day.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
all_lines: List of game lines
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
dict: Dictionary containing max_daily_chars and max_daily_hours
|
|
609
|
+
"""
|
|
610
|
+
if not all_lines:
|
|
611
|
+
return {
|
|
612
|
+
'max_daily_chars': 0,
|
|
613
|
+
'max_daily_hours': 0.0
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
# Calculate daily reading time using existing function
|
|
617
|
+
daily_reading_time = calculate_daily_reading_time(all_lines)
|
|
618
|
+
|
|
619
|
+
# Calculate daily character counts
|
|
620
|
+
daily_chars = defaultdict(int)
|
|
621
|
+
for line in all_lines:
|
|
622
|
+
date_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
623
|
+
char_count = len(line.line_text) if line.line_text else 0
|
|
624
|
+
daily_chars[date_str] += char_count
|
|
625
|
+
|
|
626
|
+
# Find maximums
|
|
627
|
+
max_daily_chars = max(daily_chars.values()) if daily_chars else 0
|
|
628
|
+
max_daily_hours = max(daily_reading_time.values()) if daily_reading_time else 0.0
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
'max_daily_chars': max_daily_chars,
|
|
632
|
+
'max_daily_hours': max_daily_hours
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
def calculate_peak_session_stats(all_lines):
|
|
636
|
+
"""
|
|
637
|
+
Calculate peak session statistics: longest session and most chars in a session.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
all_lines: List of game lines
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
dict: Dictionary containing longest_session_hours and max_session_chars
|
|
644
|
+
"""
|
|
645
|
+
if not all_lines:
|
|
646
|
+
return {
|
|
647
|
+
'longest_session_hours': 0.0,
|
|
648
|
+
'max_session_chars': 0
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
# Sort lines by timestamp
|
|
652
|
+
sorted_lines = sorted(all_lines, key=lambda line: float(line.timestamp))
|
|
653
|
+
|
|
654
|
+
# Get session gap from config
|
|
655
|
+
session_gap = get_stats_config().session_gap_seconds
|
|
656
|
+
|
|
657
|
+
# Group lines into sessions
|
|
658
|
+
sessions = []
|
|
659
|
+
current_session = []
|
|
660
|
+
|
|
661
|
+
for line in sorted_lines:
|
|
662
|
+
if not current_session:
|
|
663
|
+
current_session = [line]
|
|
664
|
+
else:
|
|
665
|
+
# Check if this line belongs to the current session
|
|
666
|
+
time_gap = float(line.timestamp) - float(current_session[-1].timestamp)
|
|
667
|
+
if time_gap <= session_gap:
|
|
668
|
+
current_session.append(line)
|
|
669
|
+
else:
|
|
670
|
+
# Start a new session
|
|
671
|
+
if current_session:
|
|
672
|
+
sessions.append(current_session)
|
|
673
|
+
current_session = [line]
|
|
674
|
+
|
|
675
|
+
# Don't forget the last session
|
|
676
|
+
if current_session:
|
|
677
|
+
sessions.append(current_session)
|
|
678
|
+
|
|
679
|
+
# Calculate session statistics
|
|
680
|
+
longest_session_hours = 0.0
|
|
681
|
+
max_session_chars = 0
|
|
682
|
+
|
|
683
|
+
for session in sessions:
|
|
684
|
+
if len(session) >= 2:
|
|
685
|
+
# Calculate session duration using actual reading time
|
|
686
|
+
timestamps = [float(line.timestamp) for line in session]
|
|
687
|
+
session_time_seconds = calculate_actual_reading_time(timestamps)
|
|
688
|
+
session_hours = session_time_seconds / 3600
|
|
689
|
+
|
|
690
|
+
# Calculate session character count
|
|
691
|
+
session_chars = sum(len(line.line_text) if line.line_text else 0 for line in session)
|
|
692
|
+
|
|
693
|
+
# Update maximums
|
|
694
|
+
longest_session_hours = max(longest_session_hours, session_hours)
|
|
695
|
+
max_session_chars = max(max_session_chars, session_chars)
|
|
696
|
+
elif len(session) == 1:
|
|
697
|
+
# Single line session - count characters but no time
|
|
698
|
+
session_chars = len(session[0].line_text) if session[0].line_text else 0
|
|
699
|
+
max_session_chars = max(max_session_chars, session_chars)
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
'longest_session_hours': longest_session_hours,
|
|
703
|
+
'max_session_chars': max_session_chars
|
|
704
|
+
}
|
|
705
|
+
|
|
518
706
|
def calculate_all_games_stats(all_lines):
|
|
519
707
|
"""Calculate aggregate statistics for all games combined."""
|
|
520
708
|
if not all_lines:
|