GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.0__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/__init__.py +39 -0
- GameSentenceMiner/anki.py +6 -3
- GameSentenceMiner/gametext.py +13 -2
- GameSentenceMiner/gsm.py +40 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +4 -1
- GameSentenceMiner/owocr/owocr/ocr.py +304 -134
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/ui/anki_confirmation.py +4 -2
- GameSentenceMiner/ui/config_gui.py +12 -0
- GameSentenceMiner/util/configuration.py +6 -2
- GameSentenceMiner/util/cron/__init__.py +12 -0
- GameSentenceMiner/util/cron/daily_rollup.py +613 -0
- GameSentenceMiner/util/cron/jiten_update.py +397 -0
- GameSentenceMiner/util/cron/populate_games.py +154 -0
- GameSentenceMiner/util/cron/run_crons.py +148 -0
- GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
- GameSentenceMiner/util/cron_table.py +334 -0
- GameSentenceMiner/util/db.py +236 -49
- GameSentenceMiner/util/ffmpeg.py +23 -4
- GameSentenceMiner/util/games_table.py +340 -93
- GameSentenceMiner/util/jiten_api_client.py +188 -0
- GameSentenceMiner/util/stats_rollup_table.py +216 -0
- GameSentenceMiner/web/anki_api_endpoints.py +438 -220
- GameSentenceMiner/web/database_api.py +955 -1259
- GameSentenceMiner/web/jiten_database_api.py +1015 -0
- GameSentenceMiner/web/rollup_stats.py +672 -0
- GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
- GameSentenceMiner/web/static/css/overview.css +604 -47
- GameSentenceMiner/web/static/css/search.css +226 -0
- GameSentenceMiner/web/static/css/shared.css +762 -0
- GameSentenceMiner/web/static/css/stats.css +221 -0
- GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
- GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
- GameSentenceMiner/web/static/js/database-game-data.js +390 -0
- GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
- GameSentenceMiner/web/static/js/database-helpers.js +44 -0
- GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
- GameSentenceMiner/web/static/js/database-popups.js +89 -0
- GameSentenceMiner/web/static/js/database-tabs.js +64 -0
- GameSentenceMiner/web/static/js/database-text-management.js +371 -0
- GameSentenceMiner/web/static/js/database.js +86 -718
- GameSentenceMiner/web/static/js/goals.js +79 -18
- GameSentenceMiner/web/static/js/heatmap.js +29 -23
- GameSentenceMiner/web/static/js/overview.js +1205 -339
- GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
- GameSentenceMiner/web/static/js/search.js +215 -18
- GameSentenceMiner/web/static/js/shared.js +193 -39
- GameSentenceMiner/web/static/js/stats.js +1536 -179
- GameSentenceMiner/web/stats.py +1142 -269
- GameSentenceMiner/web/stats_api.py +2104 -0
- GameSentenceMiner/web/templates/anki_stats.html +4 -18
- GameSentenceMiner/web/templates/components/date-range.html +118 -3
- GameSentenceMiner/web/templates/components/html-head.html +40 -6
- GameSentenceMiner/web/templates/components/js-config.html +8 -8
- GameSentenceMiner/web/templates/components/regex-input.html +160 -0
- GameSentenceMiner/web/templates/database.html +564 -117
- GameSentenceMiner/web/templates/goals.html +41 -5
- GameSentenceMiner/web/templates/overview.html +159 -129
- GameSentenceMiner/web/templates/search.html +78 -9
- GameSentenceMiner/web/templates/stats.html +159 -5
- GameSentenceMiner/web/texthooking_page.py +280 -111
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
|
@@ -529,8 +529,1097 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
529
529
|
|
|
530
530
|
// Function to create hourly activity bar chart
|
|
531
531
|
function createHourlyActivityChart(canvasId, hourlyData) {
|
|
532
|
+
if (!hourlyData || !Array.isArray(hourlyData)) return null;
|
|
533
|
+
|
|
534
|
+
// Create hour labels (0-23)
|
|
535
|
+
const hourLabels = [];
|
|
536
|
+
for (let i = 0; i < 24; i++) {
|
|
537
|
+
const hour12 = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
|
538
|
+
const ampm = i < 12 ? 'AM' : 'PM';
|
|
539
|
+
hourLabels.push(`${hour12}${ampm}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const chart = new BarChartComponent(canvasId, {
|
|
543
|
+
title: 'Reading Activity by Hour of Day',
|
|
544
|
+
colorScheme: 'gradient',
|
|
545
|
+
yAxisLabel: 'Characters Read',
|
|
546
|
+
xAxisLabel: 'Hour of Day',
|
|
547
|
+
datasetLabel: 'Characters Read',
|
|
548
|
+
maxRotation: 0,
|
|
549
|
+
minRotation: 0,
|
|
550
|
+
tooltipFormatter: {
|
|
551
|
+
title: (context) => {
|
|
552
|
+
const hourIndex = context[0].dataIndex;
|
|
553
|
+
const hour24 = hourIndex;
|
|
554
|
+
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
555
|
+
const ampm = hour24 < 12 ? 'AM' : 'PM';
|
|
556
|
+
return `${hour12}:00 ${ampm} (${hour24}:00)`;
|
|
557
|
+
},
|
|
558
|
+
label: (context) => {
|
|
559
|
+
const activity = context.parsed.y;
|
|
560
|
+
if (activity === 0) {
|
|
561
|
+
return 'No reading activity';
|
|
562
|
+
}
|
|
563
|
+
return `Characters: ${activity.toLocaleString()}`;
|
|
564
|
+
},
|
|
565
|
+
afterLabel: (context) => {
|
|
566
|
+
const activity = context.parsed.y;
|
|
567
|
+
if (activity === 0) return '';
|
|
568
|
+
|
|
569
|
+
const total = hourlyData.reduce((sum, val) => sum + val, 0);
|
|
570
|
+
const percentage = total > 0 ? ((activity / total) * 100).toFixed(1) : '0.0';
|
|
571
|
+
return `${percentage}% of total activity`;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
return chart.render(hourlyData, hourLabels);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Function to create top 5 reading speed days horizontal bar chart
|
|
580
|
+
function createTopReadingSpeedDaysChart(canvasId, readingSpeedHeatmapData) {
|
|
581
|
+
const canvas = document.getElementById(canvasId);
|
|
582
|
+
if (!canvas || !readingSpeedHeatmapData) return null;
|
|
583
|
+
|
|
584
|
+
const ctx = canvas.getContext('2d');
|
|
585
|
+
|
|
586
|
+
// Destroy existing chart if it exists
|
|
587
|
+
if (window.myCharts[canvasId]) {
|
|
588
|
+
window.myCharts[canvasId].destroy();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Extract all dates and speeds from heatmap data
|
|
592
|
+
const allDays = [];
|
|
593
|
+
for (const year in readingSpeedHeatmapData) {
|
|
594
|
+
for (const date in readingSpeedHeatmapData[year]) {
|
|
595
|
+
const speed = readingSpeedHeatmapData[year][date];
|
|
596
|
+
if (speed > 0) {
|
|
597
|
+
allDays.push({ date, speed });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Sort by speed descending and take top 5
|
|
603
|
+
allDays.sort((a, b) => b.speed - a.speed);
|
|
604
|
+
const top5Days = allDays.slice(0, 5);
|
|
605
|
+
|
|
606
|
+
// If no data, show empty chart
|
|
607
|
+
if (top5Days.length === 0) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Prepare data for horizontal bar chart (reverse order so highest is on top)
|
|
612
|
+
const labels = top5Days.reverse().map(day => day.date);
|
|
613
|
+
const speeds = top5Days.map(day => day.speed);
|
|
614
|
+
|
|
615
|
+
// Generate gradient colors from blue (fastest) to teal (5th fastest) - performance theme
|
|
616
|
+
const colors = speeds.map((speed, index) => {
|
|
617
|
+
// Reverse index so top bar gets best color
|
|
618
|
+
const reverseIndex = speeds.length - 1 - index;
|
|
619
|
+
const hue = 200 - (reverseIndex * 15); // 200 (blue) to 155 (cyan)
|
|
620
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const borderColors = speeds.map((speed, index) => {
|
|
624
|
+
const reverseIndex = speeds.length - 1 - index;
|
|
625
|
+
const hue = 200 - (reverseIndex * 15);
|
|
626
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
630
|
+
type: 'bar',
|
|
631
|
+
data: {
|
|
632
|
+
labels: labels,
|
|
633
|
+
datasets: [{
|
|
634
|
+
label: 'Reading Speed (chars/hour)',
|
|
635
|
+
data: speeds,
|
|
636
|
+
backgroundColor: colors,
|
|
637
|
+
borderColor: borderColors,
|
|
638
|
+
borderWidth: 2
|
|
639
|
+
}]
|
|
640
|
+
},
|
|
641
|
+
options: {
|
|
642
|
+
indexAxis: 'y', // This makes it horizontal
|
|
643
|
+
responsive: true,
|
|
644
|
+
plugins: {
|
|
645
|
+
legend: {
|
|
646
|
+
display: false
|
|
647
|
+
},
|
|
648
|
+
title: {
|
|
649
|
+
display: true,
|
|
650
|
+
text: 'Top 5 Fastest Reading Days',
|
|
651
|
+
color: getThemeTextColor(),
|
|
652
|
+
font: {
|
|
653
|
+
size: 16,
|
|
654
|
+
weight: 'bold'
|
|
655
|
+
},
|
|
656
|
+
padding: {
|
|
657
|
+
top: 10,
|
|
658
|
+
bottom: 20
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
tooltip: {
|
|
662
|
+
callbacks: {
|
|
663
|
+
title: function(context) {
|
|
664
|
+
return context[0].label;
|
|
665
|
+
},
|
|
666
|
+
label: function(context) {
|
|
667
|
+
const speed = context.parsed.x;
|
|
668
|
+
return `Speed: ${speed.toLocaleString()} chars/hour`;
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
672
|
+
titleColor: '#fff',
|
|
673
|
+
bodyColor: '#fff',
|
|
674
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
675
|
+
borderWidth: 1,
|
|
676
|
+
cornerRadius: 8,
|
|
677
|
+
displayColors: true
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
scales: {
|
|
681
|
+
x: {
|
|
682
|
+
beginAtZero: true,
|
|
683
|
+
title: {
|
|
684
|
+
display: true,
|
|
685
|
+
text: 'Reading Speed (chars/hour)',
|
|
686
|
+
color: getThemeTextColor()
|
|
687
|
+
},
|
|
688
|
+
ticks: {
|
|
689
|
+
color: getThemeTextColor(),
|
|
690
|
+
callback: function(value) {
|
|
691
|
+
return value.toLocaleString();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
y: {
|
|
696
|
+
title: {
|
|
697
|
+
display: true,
|
|
698
|
+
text: 'Date',
|
|
699
|
+
color: getThemeTextColor()
|
|
700
|
+
},
|
|
701
|
+
ticks: {
|
|
702
|
+
color: getThemeTextColor()
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
animation: {
|
|
707
|
+
duration: 1000,
|
|
708
|
+
easing: 'easeOutQuart'
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return window.myCharts[canvasId];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Function to create day of week activity bar chart
|
|
717
|
+
function createDayOfWeekChart(canvasId, dayOfWeekData) {
|
|
718
|
+
const canvas = document.getElementById(canvasId);
|
|
719
|
+
if (!canvas || !dayOfWeekData) return null;
|
|
720
|
+
|
|
721
|
+
const ctx = canvas.getContext('2d');
|
|
722
|
+
|
|
723
|
+
// Destroy existing chart if it exists
|
|
724
|
+
if (window.myCharts[canvasId]) {
|
|
725
|
+
window.myCharts[canvasId].destroy();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Day labels (Monday to Sunday)
|
|
729
|
+
const dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
730
|
+
|
|
731
|
+
// Get data arrays
|
|
732
|
+
const charsData = dayOfWeekData.chars || [0, 0, 0, 0, 0, 0, 0];
|
|
733
|
+
const hoursData = dayOfWeekData.hours || [0, 0, 0, 0, 0, 0, 0];
|
|
734
|
+
|
|
735
|
+
// Generate colors for each day - cohesive blue-purple gradient
|
|
736
|
+
const colors = [
|
|
737
|
+
'rgba(54, 162, 235, 0.8)', // Monday - Blue
|
|
738
|
+
'rgba(75, 192, 192, 0.8)', // Tuesday - Teal
|
|
739
|
+
'rgba(102, 187, 106, 0.8)', // Wednesday - Green
|
|
740
|
+
'rgba(255, 167, 38, 0.8)', // Thursday - Orange
|
|
741
|
+
'rgba(239, 83, 80, 0.8)', // Friday - Red
|
|
742
|
+
'rgba(171, 71, 188, 0.8)', // Saturday - Purple
|
|
743
|
+
'rgba(126, 87, 194, 0.8)' // Sunday - Deep Purple
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
const borderColors = [
|
|
747
|
+
'rgba(54, 162, 235, 1)',
|
|
748
|
+
'rgba(75, 192, 192, 1)',
|
|
749
|
+
'rgba(102, 187, 106, 1)',
|
|
750
|
+
'rgba(255, 167, 38, 1)',
|
|
751
|
+
'rgba(239, 83, 80, 1)',
|
|
752
|
+
'rgba(171, 71, 188, 1)',
|
|
753
|
+
'rgba(126, 87, 194, 1)'
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
757
|
+
type: 'bar',
|
|
758
|
+
data: {
|
|
759
|
+
labels: dayLabels,
|
|
760
|
+
datasets: [{
|
|
761
|
+
label: 'Characters Read',
|
|
762
|
+
data: charsData,
|
|
763
|
+
backgroundColor: colors,
|
|
764
|
+
borderColor: borderColors,
|
|
765
|
+
borderWidth: 2,
|
|
766
|
+
yAxisID: 'y'
|
|
767
|
+
}, {
|
|
768
|
+
label: 'Hours Read',
|
|
769
|
+
data: hoursData,
|
|
770
|
+
backgroundColor: colors.map(c => c.replace('0.8', '0.4')),
|
|
771
|
+
borderColor: borderColors,
|
|
772
|
+
borderWidth: 2,
|
|
773
|
+
yAxisID: 'y1',
|
|
774
|
+
hidden: true
|
|
775
|
+
}]
|
|
776
|
+
},
|
|
777
|
+
options: {
|
|
778
|
+
responsive: true,
|
|
779
|
+
interaction: {
|
|
780
|
+
mode: 'index',
|
|
781
|
+
intersect: false
|
|
782
|
+
},
|
|
783
|
+
plugins: {
|
|
784
|
+
legend: {
|
|
785
|
+
position: 'top',
|
|
786
|
+
labels: {
|
|
787
|
+
color: getThemeTextColor()
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
title: {
|
|
791
|
+
display: true,
|
|
792
|
+
text: 'Reading Activity by Day of Week',
|
|
793
|
+
color: getThemeTextColor(),
|
|
794
|
+
font: {
|
|
795
|
+
size: 16,
|
|
796
|
+
weight: 'bold'
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
tooltip: {
|
|
800
|
+
callbacks: {
|
|
801
|
+
label: function(context) {
|
|
802
|
+
const label = context.dataset.label || '';
|
|
803
|
+
const value = context.parsed.y;
|
|
804
|
+
if (label === 'Characters Read') {
|
|
805
|
+
return `${label}: ${value.toLocaleString()} chars`;
|
|
806
|
+
} else {
|
|
807
|
+
return `${label}: ${value.toFixed(2)} hours`;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
812
|
+
titleColor: '#fff',
|
|
813
|
+
bodyColor: '#fff'
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
scales: {
|
|
817
|
+
y: {
|
|
818
|
+
type: 'linear',
|
|
819
|
+
display: true,
|
|
820
|
+
position: 'left',
|
|
821
|
+
beginAtZero: true,
|
|
822
|
+
title: {
|
|
823
|
+
display: true,
|
|
824
|
+
text: 'Characters Read',
|
|
825
|
+
color: getThemeTextColor()
|
|
826
|
+
},
|
|
827
|
+
ticks: {
|
|
828
|
+
color: getThemeTextColor(),
|
|
829
|
+
callback: function(value) {
|
|
830
|
+
return value.toLocaleString();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
y1: {
|
|
835
|
+
type: 'linear',
|
|
836
|
+
display: true,
|
|
837
|
+
position: 'right',
|
|
838
|
+
beginAtZero: true,
|
|
839
|
+
title: {
|
|
840
|
+
display: true,
|
|
841
|
+
text: 'Hours Read',
|
|
842
|
+
color: getThemeTextColor()
|
|
843
|
+
},
|
|
844
|
+
ticks: {
|
|
845
|
+
color: getThemeTextColor()
|
|
846
|
+
},
|
|
847
|
+
grid: {
|
|
848
|
+
drawOnChartArea: false
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
x: {
|
|
852
|
+
title: {
|
|
853
|
+
display: true,
|
|
854
|
+
text: 'Day of Week',
|
|
855
|
+
color: getThemeTextColor()
|
|
856
|
+
},
|
|
857
|
+
ticks: {
|
|
858
|
+
color: getThemeTextColor()
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
return window.myCharts[canvasId];
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Function to create average hours by day bar chart
|
|
869
|
+
function createAvgHoursByDayChart(canvasId, dayOfWeekData) {
|
|
870
|
+
if (!dayOfWeekData) return null;
|
|
871
|
+
|
|
872
|
+
const dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
873
|
+
const hoursData = dayOfWeekData.avg_hours || [0, 0, 0, 0, 0, 0, 0];
|
|
874
|
+
|
|
875
|
+
const chart = new BarChartComponent(canvasId, {
|
|
876
|
+
title: 'Average Hours Read by Day of Week',
|
|
877
|
+
colorScheme: 'gradient',
|
|
878
|
+
yAxisLabel: 'Hours',
|
|
879
|
+
xAxisLabel: 'Day of Week',
|
|
880
|
+
datasetLabel: 'Hours Read',
|
|
881
|
+
maxRotation: 0,
|
|
882
|
+
minRotation: 0,
|
|
883
|
+
yAxisFormatter: (value) => value.toFixed(1),
|
|
884
|
+
tooltipFormatter: {
|
|
885
|
+
label: (context) => {
|
|
886
|
+
const hours = context.parsed.y;
|
|
887
|
+
if (hours === 0) {
|
|
888
|
+
return 'No reading activity';
|
|
889
|
+
}
|
|
890
|
+
return `Hours: ${hours.toFixed(2)}`;
|
|
891
|
+
},
|
|
892
|
+
afterLabel: (context) => {
|
|
893
|
+
const hours = context.parsed.y;
|
|
894
|
+
if (hours === 0) return '';
|
|
895
|
+
|
|
896
|
+
const nonZeroHours = hoursData.filter(h => h > 0);
|
|
897
|
+
if (nonZeroHours.length === 0) return '';
|
|
898
|
+
|
|
899
|
+
const avgHours = nonZeroHours.reduce((sum, h) => sum + h, 0) / nonZeroHours.length;
|
|
900
|
+
const comparison = hours > avgHours ? 'above' : hours < avgHours ? 'below' : 'at';
|
|
901
|
+
const percentage = avgHours > 0 ? Math.abs(((hours - avgHours) / avgHours) * 100).toFixed(1) : '0';
|
|
902
|
+
|
|
903
|
+
return `${percentage}% ${comparison} weekly average`;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
return chart.render(hoursData, dayLabels);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Function to create reading speed by difficulty bar chart
|
|
912
|
+
function createDifficultySpeedChart(canvasId, difficultySpeedData) {
|
|
913
|
+
const canvas = document.getElementById(canvasId);
|
|
914
|
+
const noDataEl = document.getElementById('difficultySpeedNoData');
|
|
915
|
+
if (!canvas) return null;
|
|
916
|
+
|
|
917
|
+
// Destroy existing chart if it exists
|
|
918
|
+
if (window.myCharts[canvasId]) {
|
|
919
|
+
window.myCharts[canvasId].destroy();
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const labels = difficultySpeedData?.labels || [];
|
|
923
|
+
const speeds = difficultySpeedData?.speeds || [];
|
|
924
|
+
|
|
925
|
+
if (!difficultySpeedData || labels.length === 0) {
|
|
926
|
+
canvas.style.display = 'none';
|
|
927
|
+
if (noDataEl) {
|
|
928
|
+
noDataEl.style.display = 'block';
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
canvas.style.display = 'block';
|
|
934
|
+
if (noDataEl) {
|
|
935
|
+
noDataEl.style.display = 'none';
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const ctx = canvas.getContext('2d');
|
|
939
|
+
|
|
940
|
+
// Generate gradient colors from blue (easy) to orange (hard) - difficulty theme
|
|
941
|
+
const colors = speeds.map((_, index) => {
|
|
942
|
+
const ratio = index / Math.max(labels.length - 1, 1);
|
|
943
|
+
const hue = 200 - (ratio * 170); // 200 (blue) to 30 (orange)
|
|
944
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const borderColors = speeds.map((_, index) => {
|
|
948
|
+
const ratio = index / Math.max(labels.length - 1, 1);
|
|
949
|
+
const hue = 200 - (ratio * 170);
|
|
950
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
954
|
+
type: 'bar',
|
|
955
|
+
data: {
|
|
956
|
+
labels: labels,
|
|
957
|
+
datasets: [{
|
|
958
|
+
label: 'Average Reading Speed',
|
|
959
|
+
data: speeds,
|
|
960
|
+
backgroundColor: colors,
|
|
961
|
+
borderColor: borderColors,
|
|
962
|
+
borderWidth: 2
|
|
963
|
+
}]
|
|
964
|
+
},
|
|
965
|
+
options: {
|
|
966
|
+
responsive: true,
|
|
967
|
+
plugins: {
|
|
968
|
+
legend: {
|
|
969
|
+
display: false
|
|
970
|
+
},
|
|
971
|
+
title: {
|
|
972
|
+
display: true,
|
|
973
|
+
text: 'Average Reading Speed by Game Difficulty',
|
|
974
|
+
color: getThemeTextColor(),
|
|
975
|
+
font: {
|
|
976
|
+
size: 16,
|
|
977
|
+
weight: 'bold'
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
tooltip: {
|
|
981
|
+
callbacks: {
|
|
982
|
+
label: function(context) {
|
|
983
|
+
const speed = context.parsed.y;
|
|
984
|
+
return `Speed: ${speed.toLocaleString()} chars/hour`;
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
988
|
+
titleColor: '#fff',
|
|
989
|
+
bodyColor: '#fff'
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
scales: {
|
|
993
|
+
y: {
|
|
994
|
+
beginAtZero: true,
|
|
995
|
+
title: {
|
|
996
|
+
display: true,
|
|
997
|
+
text: 'Characters per Hour',
|
|
998
|
+
color: getThemeTextColor()
|
|
999
|
+
},
|
|
1000
|
+
ticks: {
|
|
1001
|
+
color: getThemeTextColor(),
|
|
1002
|
+
callback: function(value) {
|
|
1003
|
+
return value.toLocaleString();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
x: {
|
|
1008
|
+
title: {
|
|
1009
|
+
display: true,
|
|
1010
|
+
text: 'Difficulty Level',
|
|
1011
|
+
color: getThemeTextColor()
|
|
1012
|
+
},
|
|
1013
|
+
ticks: {
|
|
1014
|
+
color: getThemeTextColor()
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
return window.myCharts[canvasId];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Function to create game type distribution bar chart
|
|
1025
|
+
function createGameTypeChart(canvasId, gameTypeData) {
|
|
1026
|
+
const canvas = document.getElementById(canvasId);
|
|
1027
|
+
const noDataEl = document.getElementById('gameTypeNoData');
|
|
1028
|
+
|
|
1029
|
+
if (!canvas) return null;
|
|
1030
|
+
|
|
1031
|
+
if (!gameTypeData || !gameTypeData.labels || gameTypeData.labels.length === 0) {
|
|
1032
|
+
canvas.style.display = 'none';
|
|
1033
|
+
if (noDataEl) {
|
|
1034
|
+
noDataEl.style.display = 'block';
|
|
1035
|
+
}
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
canvas.style.display = 'block';
|
|
1040
|
+
if (noDataEl) {
|
|
1041
|
+
noDataEl.style.display = 'none';
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const chart = new BarChartComponent(canvasId, {
|
|
1045
|
+
title: 'Games by Type',
|
|
1046
|
+
colorScheme: 'gradient',
|
|
1047
|
+
yAxisLabel: 'Number of Games',
|
|
1048
|
+
xAxisLabel: 'Game Type',
|
|
1049
|
+
datasetLabel: 'Games',
|
|
1050
|
+
maxRotation: 45,
|
|
1051
|
+
minRotation: 45,
|
|
1052
|
+
tooltipFormatter: {
|
|
1053
|
+
label: (context) => {
|
|
1054
|
+
const count = context.parsed.y;
|
|
1055
|
+
const total = gameTypeData.counts.reduce((sum, val) => sum + val, 0);
|
|
1056
|
+
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : '0.0';
|
|
1057
|
+
return `Games: ${count} (${percentage}%)`;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
return chart.render(gameTypeData.counts, gameTypeData.labels);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
function createCardsMinedChart(canvasId, chartData) {
|
|
1067
|
+
const canvas = document.getElementById(canvasId);
|
|
1068
|
+
const noDataEl = document.getElementById('cardsMinedNoData');
|
|
1069
|
+
if (!canvas) return null;
|
|
1070
|
+
|
|
1071
|
+
if (window.myCharts[canvasId]) {
|
|
1072
|
+
window.myCharts[canvasId].destroy();
|
|
1073
|
+
delete window.myCharts[canvasId];
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const hasLabels =
|
|
1077
|
+
chartData &&
|
|
1078
|
+
Array.isArray(chartData.labels) &&
|
|
1079
|
+
chartData.labels.length > 0;
|
|
1080
|
+
|
|
1081
|
+
const hasTotals =
|
|
1082
|
+
chartData &&
|
|
1083
|
+
Array.isArray(chartData.totals) &&
|
|
1084
|
+
chartData.totals.length > 0 &&
|
|
1085
|
+
chartData.labels.length === chartData.totals.length;
|
|
1086
|
+
|
|
1087
|
+
const hasNonZeroTotals = hasTotals
|
|
1088
|
+
? chartData.totals.some((value) => Number(value) > 0)
|
|
1089
|
+
: false;
|
|
1090
|
+
|
|
1091
|
+
if (!hasLabels || !hasTotals || !hasNonZeroTotals) {
|
|
1092
|
+
canvas.style.display = 'none';
|
|
1093
|
+
if (noDataEl) {
|
|
1094
|
+
noDataEl.style.display = 'block';
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
canvas.style.display = 'block';
|
|
1100
|
+
if (noDataEl) {
|
|
1101
|
+
noDataEl.style.display = 'none';
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const ctx = canvas.getContext('2d');
|
|
1105
|
+
|
|
1106
|
+
// Generate gradient colors based on values (more = greener)
|
|
1107
|
+
const maxVal = Math.max(...chartData.totals.filter(v => v > 0));
|
|
1108
|
+
const minVal = Math.min(...chartData.totals.filter(v => v > 0));
|
|
1109
|
+
const isDark = getCurrentTheme() === 'dark';
|
|
1110
|
+
|
|
1111
|
+
const barColors = chartData.totals.map(value => {
|
|
1112
|
+
if (value === 0) {
|
|
1113
|
+
return isDark ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
1114
|
+
}
|
|
1115
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1116
|
+
const hue = 30 + (normalized * 170); // 30 = orange, 200 = blue
|
|
1117
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
const borderColors = chartData.totals.map(value => {
|
|
1121
|
+
if (value === 0) {
|
|
1122
|
+
return isDark ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
1123
|
+
}
|
|
1124
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1125
|
+
const hue = 30 + (normalized * 170);
|
|
1126
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// Format labels to show weekend indicator
|
|
1130
|
+
const formattedLabels = chartData.labels.map(dateStr => {
|
|
1131
|
+
const date = new Date(dateStr);
|
|
1132
|
+
const dayOfWeek = date.getDay();
|
|
1133
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1134
|
+
return isWeekend ? `${dateStr} 📅` : dateStr;
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
1138
|
+
type: 'bar',
|
|
1139
|
+
data: {
|
|
1140
|
+
labels: formattedLabels,
|
|
1141
|
+
datasets: [
|
|
1142
|
+
{
|
|
1143
|
+
label: 'Cards Mined',
|
|
1144
|
+
data: chartData.totals,
|
|
1145
|
+
backgroundColor: barColors,
|
|
1146
|
+
borderColor: borderColors,
|
|
1147
|
+
borderWidth: 2,
|
|
1148
|
+
borderRadius: 4
|
|
1149
|
+
}
|
|
1150
|
+
]
|
|
1151
|
+
},
|
|
1152
|
+
options: {
|
|
1153
|
+
responsive: true,
|
|
1154
|
+
plugins: {
|
|
1155
|
+
legend: {
|
|
1156
|
+
display: false
|
|
1157
|
+
},
|
|
1158
|
+
tooltip: {
|
|
1159
|
+
callbacks: {
|
|
1160
|
+
title: function (context) {
|
|
1161
|
+
const index = context[0].dataIndex;
|
|
1162
|
+
return chartData.labels[index];
|
|
1163
|
+
},
|
|
1164
|
+
label: function (context) {
|
|
1165
|
+
const value = context.parsed.y || 0;
|
|
1166
|
+
return `Cards: ${value.toLocaleString()}`;
|
|
1167
|
+
},
|
|
1168
|
+
afterLabel: function(context) {
|
|
1169
|
+
const index = context[0].dataIndex;
|
|
1170
|
+
const date = new Date(chartData.labels[index]);
|
|
1171
|
+
const dayOfWeek = date.getDay();
|
|
1172
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1173
|
+
return isWeekend ? '📅 Weekend' : '';
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
1177
|
+
titleColor: '#fff',
|
|
1178
|
+
bodyColor: '#fff',
|
|
1179
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
1180
|
+
borderWidth: 1,
|
|
1181
|
+
cornerRadius: 6,
|
|
1182
|
+
displayColors: false
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
scales: {
|
|
1186
|
+
y: {
|
|
1187
|
+
beginAtZero: true,
|
|
1188
|
+
title: {
|
|
1189
|
+
display: true,
|
|
1190
|
+
text: 'Cards Mined',
|
|
1191
|
+
color: getThemeTextColor()
|
|
1192
|
+
},
|
|
1193
|
+
ticks: {
|
|
1194
|
+
color: getThemeTextColor(),
|
|
1195
|
+
callback: function (value) {
|
|
1196
|
+
return value.toLocaleString();
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
grid: {
|
|
1200
|
+
color:
|
|
1201
|
+
getCurrentTheme() === 'dark'
|
|
1202
|
+
? 'rgba(255, 255, 255, 0.1)'
|
|
1203
|
+
: 'rgba(0, 0, 0, 0.1)'
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
x: {
|
|
1207
|
+
title: {
|
|
1208
|
+
display: true,
|
|
1209
|
+
text: 'Date',
|
|
1210
|
+
color: getThemeTextColor()
|
|
1211
|
+
},
|
|
1212
|
+
ticks: {
|
|
1213
|
+
color: getThemeTextColor(),
|
|
1214
|
+
maxRotation: 45,
|
|
1215
|
+
minRotation: 45,
|
|
1216
|
+
callback: function (value) {
|
|
1217
|
+
return this.getLabelForValue(value);
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
grid: {
|
|
1221
|
+
display: false
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
return window.myCharts[canvasId];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Function to create top 5 character count days horizontal bar chart
|
|
1232
|
+
function createTopCharacterDaysChart(canvasId, heatmapData) {
|
|
1233
|
+
if (!heatmapData) return null;
|
|
1234
|
+
|
|
1235
|
+
// Extract all dates and character counts from heatmap data
|
|
1236
|
+
const allDays = [];
|
|
1237
|
+
for (const year in heatmapData) {
|
|
1238
|
+
for (const date in heatmapData[year]) {
|
|
1239
|
+
const chars = heatmapData[year][date];
|
|
1240
|
+
if (chars > 0) {
|
|
1241
|
+
allDays.push({ date, chars });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Sort by character count descending and take top 5
|
|
1247
|
+
allDays.sort((a, b) => b.chars - a.chars);
|
|
1248
|
+
const top5Days = allDays.slice(0, 5);
|
|
1249
|
+
|
|
1250
|
+
if (top5Days.length === 0) return null;
|
|
1251
|
+
|
|
1252
|
+
// Prepare data for horizontal bar chart (reverse order so highest is on top)
|
|
1253
|
+
const labels = top5Days.reverse().map(day => day.date);
|
|
1254
|
+
const charCounts = top5Days.map(day => day.chars);
|
|
1255
|
+
|
|
1256
|
+
const chart = new BarChartComponent(canvasId, {
|
|
1257
|
+
title: 'Top 5 Most Productive Reading Days',
|
|
1258
|
+
type: 'horizontal',
|
|
1259
|
+
colorScheme: 'performance',
|
|
1260
|
+
xAxisLabel: 'Characters Read',
|
|
1261
|
+
yAxisLabel: 'Date',
|
|
1262
|
+
datasetLabel: 'Characters Read',
|
|
1263
|
+
valueFormatter: (value) => `Characters: ${value.toLocaleString()}`
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
return chart.render(charCounts, labels);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Function to create hourly reading speed bar chart
|
|
1270
|
+
function createHourlyReadingSpeedChart(canvasId, hourlySpeedData) {
|
|
1271
|
+
const canvas = document.getElementById(canvasId);
|
|
1272
|
+
if (!canvas || !hourlySpeedData || !Array.isArray(hourlySpeedData)) return null;
|
|
1273
|
+
|
|
1274
|
+
const ctx = canvas.getContext('2d');
|
|
1275
|
+
|
|
1276
|
+
// Destroy existing chart if it exists
|
|
1277
|
+
if (window.myCharts[canvasId]) {
|
|
1278
|
+
window.myCharts[canvasId].destroy();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Create hour labels (0-23)
|
|
1282
|
+
const hourLabels = [];
|
|
1283
|
+
for (let i = 0; i < 24; i++) {
|
|
1284
|
+
const hour12 = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
|
1285
|
+
const ampm = i < 12 ? 'AM' : 'PM';
|
|
1286
|
+
hourLabels.push(`${hour12}${ampm}`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Generate gradient colors for bars based on speed values
|
|
1290
|
+
const maxSpeed = Math.max(...hourlySpeedData.filter(speed => speed > 0));
|
|
1291
|
+
const minSpeed = Math.min(...hourlySpeedData.filter(speed => speed > 0));
|
|
1292
|
+
|
|
1293
|
+
const barColors = hourlySpeedData.map(speed => {
|
|
1294
|
+
if (speed === 0) {
|
|
1295
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Create color gradient from orange (slow) to blue (fast) - performance theme
|
|
1299
|
+
const normalizedSpeed = maxSpeed > minSpeed ? (speed - minSpeed) / (maxSpeed - minSpeed) : 0.5;
|
|
1300
|
+
const hue = 30 + (normalizedSpeed * 170); // 30 = orange, 200 = blue
|
|
1301
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const borderColors = hourlySpeedData.map(speed => {
|
|
1305
|
+
if (speed === 0) {
|
|
1306
|
+
return getCurrentTheme() === 'dark' ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const normalizedSpeed = maxSpeed > minSpeed ? (speed - minSpeed) / (maxSpeed - minSpeed) : 0.5;
|
|
1310
|
+
const hue = 30 + (normalizedSpeed * 170);
|
|
1311
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
1315
|
+
type: 'bar',
|
|
1316
|
+
data: {
|
|
1317
|
+
labels: hourLabels,
|
|
1318
|
+
datasets: [{
|
|
1319
|
+
label: 'Average Reading Speed',
|
|
1320
|
+
data: hourlySpeedData,
|
|
1321
|
+
backgroundColor: barColors,
|
|
1322
|
+
borderColor: borderColors,
|
|
1323
|
+
borderWidth: 2
|
|
1324
|
+
}]
|
|
1325
|
+
},
|
|
1326
|
+
options: {
|
|
1327
|
+
responsive: true,
|
|
1328
|
+
plugins: {
|
|
1329
|
+
legend: {
|
|
1330
|
+
display: false // Hide legend for cleaner look
|
|
1331
|
+
},
|
|
1332
|
+
title: {
|
|
1333
|
+
display: true,
|
|
1334
|
+
text: 'Average Reading Speed by Hour',
|
|
1335
|
+
color: getThemeTextColor(),
|
|
1336
|
+
font: {
|
|
1337
|
+
size: 16,
|
|
1338
|
+
weight: 'bold'
|
|
1339
|
+
},
|
|
1340
|
+
padding: {
|
|
1341
|
+
top: 10,
|
|
1342
|
+
bottom: 20
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
tooltip: {
|
|
1346
|
+
callbacks: {
|
|
1347
|
+
title: function(context) {
|
|
1348
|
+
const hourIndex = context[0].dataIndex;
|
|
1349
|
+
const hour24 = hourIndex;
|
|
1350
|
+
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
1351
|
+
const ampm = hour24 < 12 ? 'AM' : 'PM';
|
|
1352
|
+
return `${hour12}:00 ${ampm} (${hour24}:00)`;
|
|
1353
|
+
},
|
|
1354
|
+
label: function(context) {
|
|
1355
|
+
const speed = context.parsed.y;
|
|
1356
|
+
if (speed === 0) {
|
|
1357
|
+
return 'No reading activity';
|
|
1358
|
+
}
|
|
1359
|
+
return `Speed: ${speed.toLocaleString()} chars/hour`;
|
|
1360
|
+
},
|
|
1361
|
+
afterLabel: function(context) {
|
|
1362
|
+
const speed = context.parsed.y;
|
|
1363
|
+
if (speed === 0) return '';
|
|
1364
|
+
|
|
1365
|
+
const nonZeroSpeeds = hourlySpeedData.filter(s => s > 0);
|
|
1366
|
+
const avgSpeed = nonZeroSpeeds.reduce((sum, s) => sum + s, 0) / nonZeroSpeeds.length;
|
|
1367
|
+
const comparison = speed > avgSpeed ? 'above' : speed < avgSpeed ? 'below' : 'at';
|
|
1368
|
+
const percentage = avgSpeed > 0 ? Math.abs(((speed - avgSpeed) / avgSpeed) * 100).toFixed(1) : '0';
|
|
1369
|
+
|
|
1370
|
+
return `${percentage}% ${comparison} average`;
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
1374
|
+
titleColor: '#fff',
|
|
1375
|
+
bodyColor: '#fff',
|
|
1376
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
1377
|
+
borderWidth: 1,
|
|
1378
|
+
cornerRadius: 8,
|
|
1379
|
+
displayColors: true
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
scales: {
|
|
1383
|
+
y: {
|
|
1384
|
+
beginAtZero: true,
|
|
1385
|
+
title: {
|
|
1386
|
+
display: true,
|
|
1387
|
+
text: 'Characters per Hour',
|
|
1388
|
+
color: getThemeTextColor()
|
|
1389
|
+
},
|
|
1390
|
+
ticks: {
|
|
1391
|
+
color: getThemeTextColor(),
|
|
1392
|
+
callback: function(value) {
|
|
1393
|
+
return value.toLocaleString();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
x: {
|
|
1398
|
+
title: {
|
|
1399
|
+
display: true,
|
|
1400
|
+
text: 'Hour of Day',
|
|
1401
|
+
color: getThemeTextColor()
|
|
1402
|
+
},
|
|
1403
|
+
ticks: {
|
|
1404
|
+
color: getThemeTextColor()
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
animation: {
|
|
1409
|
+
duration: 1000,
|
|
1410
|
+
easing: 'easeOutQuart'
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
return window.myCharts[canvasId];
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Initialize heatmap renderer with mining-specific configuration
|
|
1419
|
+
const miningHeatmapRenderer = new HeatmapRenderer({
|
|
1420
|
+
containerId: 'miningHeatmapContainer',
|
|
1421
|
+
metricName: 'sentences',
|
|
1422
|
+
metricLabel: 'sentences mined'
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// Function to create GitHub-style heatmap for mining activity using shared component
|
|
1426
|
+
function createMiningHeatmap(heatmapData) {
|
|
1427
|
+
miningHeatmapRenderer.render(heatmapData);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Initialize Kanji Grid Renderer (using shared component)
|
|
1431
|
+
const kanjiGridRenderer = new KanjiGridRenderer({
|
|
1432
|
+
containerSelector: '#kanjiGrid',
|
|
1433
|
+
counterSelector: '#kanjiCount',
|
|
1434
|
+
colorMode: 'backend',
|
|
1435
|
+
emptyMessage: 'No kanji data available'
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
// Function to create daily time bar chart with weekend markers
|
|
1439
|
+
function createDailyTimeChart(canvasId, chartData, isAllTime = false) {
|
|
1440
|
+
const canvas = document.getElementById(canvasId);
|
|
1441
|
+
if (!canvas || !chartData) return null;
|
|
1442
|
+
|
|
1443
|
+
const ctx = canvas.getContext('2d');
|
|
1444
|
+
|
|
1445
|
+
// Destroy existing chart if it exists
|
|
1446
|
+
if (window.myCharts[canvasId]) {
|
|
1447
|
+
window.myCharts[canvasId].destroy();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Generate gradient colors based on values (more = greener)
|
|
1451
|
+
const maxVal = Math.max(...chartData.timeData.filter(v => v > 0));
|
|
1452
|
+
const minVal = Math.min(...chartData.timeData.filter(v => v > 0));
|
|
1453
|
+
const isDark = getCurrentTheme() === 'dark';
|
|
1454
|
+
|
|
1455
|
+
const barColors = chartData.timeData.map(value => {
|
|
1456
|
+
if (value === 0) {
|
|
1457
|
+
return isDark ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
1458
|
+
}
|
|
1459
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1460
|
+
const hue = 30 + (normalized * 170); // 30 = orange, 200 = blue
|
|
1461
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
const borderColors = chartData.timeData.map(value => {
|
|
1465
|
+
if (value === 0) {
|
|
1466
|
+
return isDark ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
1467
|
+
}
|
|
1468
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1469
|
+
const hue = 30 + (normalized * 170);
|
|
1470
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// Format labels to show day of week with weekend indicator
|
|
1474
|
+
const formattedLabels = chartData.labels.map(dateStr => {
|
|
1475
|
+
const date = new Date(dateStr);
|
|
1476
|
+
const dayOfWeek = date.getDay();
|
|
1477
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1478
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1479
|
+
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1480
|
+
return isWeekend ? `${dayNames[dayOfWeek]} ${monthDay} 📅` : `${dayNames[dayOfWeek]} ${monthDay}`;
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
// Update the title element
|
|
1484
|
+
const titleElement = document.getElementById('dailyTimeChartTitle');
|
|
1485
|
+
if (titleElement) {
|
|
1486
|
+
titleElement.textContent = isAllTime ? '📊 Daily Reading Time (All Time)' : '📊 Daily Reading Time (Last 4 Weeks)';
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
1490
|
+
type: 'bar',
|
|
1491
|
+
data: {
|
|
1492
|
+
labels: formattedLabels,
|
|
1493
|
+
datasets: [{
|
|
1494
|
+
label: 'Hours Read',
|
|
1495
|
+
data: chartData.timeData,
|
|
1496
|
+
backgroundColor: barColors,
|
|
1497
|
+
borderColor: borderColors,
|
|
1498
|
+
borderWidth: 2
|
|
1499
|
+
}]
|
|
1500
|
+
},
|
|
1501
|
+
options: {
|
|
1502
|
+
responsive: true,
|
|
1503
|
+
plugins: {
|
|
1504
|
+
legend: {
|
|
1505
|
+
display: false
|
|
1506
|
+
},
|
|
1507
|
+
title: {
|
|
1508
|
+
display: false
|
|
1509
|
+
},
|
|
1510
|
+
tooltip: {
|
|
1511
|
+
callbacks: {
|
|
1512
|
+
title: function(context) {
|
|
1513
|
+
const index = context[0].dataIndex;
|
|
1514
|
+
return chartData.labels[index];
|
|
1515
|
+
},
|
|
1516
|
+
label: function(context) {
|
|
1517
|
+
const hours = context.parsed.y;
|
|
1518
|
+
if (hours === 0) {
|
|
1519
|
+
return 'No reading activity';
|
|
1520
|
+
}
|
|
1521
|
+
const wholeHours = Math.floor(hours);
|
|
1522
|
+
const minutes = Math.round((hours - wholeHours) * 60);
|
|
1523
|
+
if (minutes > 0) {
|
|
1524
|
+
return `Time: ${wholeHours}h ${minutes}m`;
|
|
1525
|
+
} else {
|
|
1526
|
+
return `Time: ${wholeHours}h`;
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
afterLabel: function(context) {
|
|
1530
|
+
const index = context[0].dataIndex;
|
|
1531
|
+
const date = new Date(chartData.labels[index]);
|
|
1532
|
+
const dayOfWeek = date.getDay();
|
|
1533
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1534
|
+
return isWeekend ? '📅 Weekend' : '';
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
1538
|
+
titleColor: '#fff',
|
|
1539
|
+
bodyColor: '#fff',
|
|
1540
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
1541
|
+
borderWidth: 1,
|
|
1542
|
+
cornerRadius: 8,
|
|
1543
|
+
displayColors: true
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
scales: {
|
|
1547
|
+
y: {
|
|
1548
|
+
beginAtZero: true,
|
|
1549
|
+
title: {
|
|
1550
|
+
display: true,
|
|
1551
|
+
text: 'Hours',
|
|
1552
|
+
color: getThemeTextColor()
|
|
1553
|
+
},
|
|
1554
|
+
ticks: {
|
|
1555
|
+
color: getThemeTextColor(),
|
|
1556
|
+
callback: function(value) {
|
|
1557
|
+
return value.toFixed(1);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
x: {
|
|
1562
|
+
title: {
|
|
1563
|
+
display: true,
|
|
1564
|
+
text: 'Date',
|
|
1565
|
+
color: getThemeTextColor()
|
|
1566
|
+
},
|
|
1567
|
+
ticks: {
|
|
1568
|
+
color: getThemeTextColor(),
|
|
1569
|
+
maxRotation: 45,
|
|
1570
|
+
minRotation: 45
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
animation: {
|
|
1575
|
+
duration: 1000,
|
|
1576
|
+
easing: 'easeOutQuart'
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
return window.myCharts[canvasId];
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Function to create daily characters bar chart with gradient colors
|
|
1585
|
+
function createDailyCharsChart(canvasId, chartData, isAllTime = false) {
|
|
1586
|
+
if (!chartData) return null;
|
|
1587
|
+
|
|
1588
|
+
// Format labels to show day of week with weekend indicator
|
|
1589
|
+
const formattedLabels = chartData.labels.map(dateStr => {
|
|
1590
|
+
const date = new Date(dateStr);
|
|
1591
|
+
const dayOfWeek = date.getDay();
|
|
1592
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1593
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1594
|
+
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1595
|
+
return isWeekend ? `${dayNames[dayOfWeek]} ${monthDay} 📅` : `${dayNames[dayOfWeek]} ${monthDay}`;
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// Generate gradient colors based on values (more = greener)
|
|
1599
|
+
const maxVal = Math.max(...chartData.charsData.filter(v => v > 0));
|
|
1600
|
+
const minVal = Math.min(...chartData.charsData.filter(v => v > 0));
|
|
1601
|
+
const isDark = getCurrentTheme() === 'dark';
|
|
1602
|
+
|
|
1603
|
+
const barColors = chartData.charsData.map(value => {
|
|
1604
|
+
if (value === 0) {
|
|
1605
|
+
return isDark ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)';
|
|
1606
|
+
}
|
|
1607
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1608
|
+
const hue = 30 + (normalized * 170); // 30 = orange, 200 = blue
|
|
1609
|
+
return `hsla(${hue}, 70%, 50%, 0.8)`;
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
const borderColors = chartData.charsData.map(value => {
|
|
1613
|
+
if (value === 0) {
|
|
1614
|
+
return isDark ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)';
|
|
1615
|
+
}
|
|
1616
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
1617
|
+
const hue = 30 + (normalized * 170);
|
|
1618
|
+
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
1619
|
+
});
|
|
1620
|
+
|
|
532
1621
|
const canvas = document.getElementById(canvasId);
|
|
533
|
-
if (!canvas
|
|
1622
|
+
if (!canvas) return null;
|
|
534
1623
|
|
|
535
1624
|
const ctx = canvas.getContext('2d');
|
|
536
1625
|
|
|
@@ -539,46 +1628,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
539
1628
|
window.myCharts[canvasId].destroy();
|
|
540
1629
|
}
|
|
541
1630
|
|
|
542
|
-
//
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const ampm = i < 12 ? 'AM' : 'PM';
|
|
547
|
-
hourLabels.push(`${hour12}${ampm}`);
|
|
1631
|
+
// Update the title element
|
|
1632
|
+
const titleElement = document.getElementById('dailyCharsChartTitle');
|
|
1633
|
+
if (titleElement) {
|
|
1634
|
+
titleElement.textContent = isAllTime ? '📚 Daily Characters Read (All Time)' : '📚 Daily Characters Read (Last 4 Weeks)';
|
|
548
1635
|
}
|
|
549
1636
|
|
|
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
1637
|
window.myCharts[canvasId] = new Chart(ctx, {
|
|
576
1638
|
type: 'bar',
|
|
577
1639
|
data: {
|
|
578
|
-
labels:
|
|
1640
|
+
labels: formattedLabels,
|
|
579
1641
|
datasets: [{
|
|
580
1642
|
label: 'Characters Read',
|
|
581
|
-
data:
|
|
1643
|
+
data: chartData.charsData,
|
|
582
1644
|
backgroundColor: barColors,
|
|
583
1645
|
borderColor: borderColors,
|
|
584
1646
|
borderWidth: 2
|
|
@@ -588,44 +1650,30 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
588
1650
|
responsive: true,
|
|
589
1651
|
plugins: {
|
|
590
1652
|
legend: {
|
|
591
|
-
display: false
|
|
1653
|
+
display: false
|
|
592
1654
|
},
|
|
593
1655
|
title: {
|
|
594
|
-
display:
|
|
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
|
-
}
|
|
1656
|
+
display: false
|
|
605
1657
|
},
|
|
606
1658
|
tooltip: {
|
|
607
1659
|
callbacks: {
|
|
608
1660
|
title: function(context) {
|
|
609
|
-
const
|
|
610
|
-
|
|
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)`;
|
|
1661
|
+
const index = context[0].dataIndex;
|
|
1662
|
+
return chartData.labels[index];
|
|
614
1663
|
},
|
|
615
1664
|
label: function(context) {
|
|
616
|
-
const
|
|
617
|
-
if (
|
|
1665
|
+
const chars = context.parsed.y;
|
|
1666
|
+
if (chars === 0) {
|
|
618
1667
|
return 'No reading activity';
|
|
619
1668
|
}
|
|
620
|
-
return `Characters: ${
|
|
1669
|
+
return `Characters: ${chars.toLocaleString()}`;
|
|
621
1670
|
},
|
|
622
1671
|
afterLabel: function(context) {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
return `${percentage}% of total activity`;
|
|
1672
|
+
const index = context[0].dataIndex;
|
|
1673
|
+
const date = new Date(chartData.labels[index]);
|
|
1674
|
+
const dayOfWeek = date.getDay();
|
|
1675
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1676
|
+
return isWeekend ? '📅 Weekend' : '';
|
|
629
1677
|
}
|
|
630
1678
|
},
|
|
631
1679
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
@@ -642,7 +1690,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
642
1690
|
beginAtZero: true,
|
|
643
1691
|
title: {
|
|
644
1692
|
display: true,
|
|
645
|
-
text: 'Characters
|
|
1693
|
+
text: 'Characters',
|
|
646
1694
|
color: getThemeTextColor()
|
|
647
1695
|
},
|
|
648
1696
|
ticks: {
|
|
@@ -655,11 +1703,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
655
1703
|
x: {
|
|
656
1704
|
title: {
|
|
657
1705
|
display: true,
|
|
658
|
-
text: '
|
|
1706
|
+
text: 'Date',
|
|
659
1707
|
color: getThemeTextColor()
|
|
660
1708
|
},
|
|
661
1709
|
ticks: {
|
|
662
|
-
color: getThemeTextColor()
|
|
1710
|
+
color: getThemeTextColor(),
|
|
1711
|
+
maxRotation: 45,
|
|
1712
|
+
minRotation: 45
|
|
663
1713
|
}
|
|
664
1714
|
}
|
|
665
1715
|
},
|
|
@@ -673,10 +1723,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
673
1723
|
return window.myCharts[canvasId];
|
|
674
1724
|
}
|
|
675
1725
|
|
|
676
|
-
// Function to create
|
|
677
|
-
function
|
|
1726
|
+
// Function to create daily reading speed line chart
|
|
1727
|
+
function createDailySpeedChart(canvasId, chartData, isAllTime = false) {
|
|
678
1728
|
const canvas = document.getElementById(canvasId);
|
|
679
|
-
if (!canvas || !
|
|
1729
|
+
if (!canvas || !chartData) return null;
|
|
680
1730
|
|
|
681
1731
|
const ctx = canvas.getContext('2d');
|
|
682
1732
|
|
|
@@ -685,96 +1735,80 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
685
1735
|
window.myCharts[canvasId].destroy();
|
|
686
1736
|
}
|
|
687
1737
|
|
|
688
|
-
//
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
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));
|
|
1738
|
+
// Filter out days with no data for cleaner line chart
|
|
1739
|
+
const filteredLabels = [];
|
|
1740
|
+
const filteredSpeedData = [];
|
|
1741
|
+
const filteredOriginalLabels = [];
|
|
699
1742
|
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
1743
|
+
chartData.labels.forEach((dateStr, index) => {
|
|
1744
|
+
if (chartData.speedData[index] > 0) {
|
|
1745
|
+
const date = new Date(dateStr);
|
|
1746
|
+
const dayOfWeek = date.getDay();
|
|
1747
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1748
|
+
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1749
|
+
filteredLabels.push(`${dayNames[dayOfWeek]} ${monthDay}`);
|
|
1750
|
+
filteredSpeedData.push(chartData.speedData[index]);
|
|
1751
|
+
filteredOriginalLabels.push(dateStr);
|
|
703
1752
|
}
|
|
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
1753
|
});
|
|
710
1754
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const hue = normalizedSpeed * 120;
|
|
718
|
-
return `hsla(${hue}, 70%, 40%, 1)`;
|
|
1755
|
+
// Generate point colors based on weekend - consistent with other daily charts
|
|
1756
|
+
const pointColors = filteredOriginalLabels.map(dateStr => {
|
|
1757
|
+
const date = new Date(dateStr);
|
|
1758
|
+
const dayOfWeek = date.getDay();
|
|
1759
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1760
|
+
return isWeekend ? 'rgba(171, 71, 188, 1)' : 'rgba(54, 162, 235, 1)';
|
|
719
1761
|
});
|
|
720
|
-
|
|
1762
|
+
|
|
1763
|
+
// Update the title element
|
|
1764
|
+
const titleElement = document.getElementById('dailySpeedChartTitle');
|
|
1765
|
+
if (titleElement) {
|
|
1766
|
+
titleElement.textContent = isAllTime ? '⚡ Daily Reading Speed (All Time)' : '⚡ Daily Reading Speed (Last 4 Weeks)';
|
|
1767
|
+
}
|
|
1768
|
+
|
|
721
1769
|
window.myCharts[canvasId] = new Chart(ctx, {
|
|
722
|
-
type: '
|
|
1770
|
+
type: 'line',
|
|
723
1771
|
data: {
|
|
724
|
-
labels:
|
|
1772
|
+
labels: filteredLabels,
|
|
725
1773
|
datasets: [{
|
|
726
|
-
label: '
|
|
727
|
-
data:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1774
|
+
label: 'Reading Speed (chars/hour)',
|
|
1775
|
+
data: filteredSpeedData,
|
|
1776
|
+
borderColor: 'rgba(54, 162, 235, 1)',
|
|
1777
|
+
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
1778
|
+
pointBackgroundColor: pointColors,
|
|
1779
|
+
pointBorderColor: pointColors,
|
|
1780
|
+
pointRadius: 5,
|
|
1781
|
+
pointHoverRadius: 7,
|
|
1782
|
+
borderWidth: 2,
|
|
1783
|
+
tension: 0.3,
|
|
1784
|
+
fill: true
|
|
731
1785
|
}]
|
|
732
1786
|
},
|
|
733
1787
|
options: {
|
|
734
1788
|
responsive: true,
|
|
735
1789
|
plugins: {
|
|
736
1790
|
legend: {
|
|
737
|
-
display: false
|
|
1791
|
+
display: false
|
|
738
1792
|
},
|
|
739
1793
|
title: {
|
|
740
|
-
display:
|
|
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
|
-
}
|
|
1794
|
+
display: false
|
|
751
1795
|
},
|
|
752
1796
|
tooltip: {
|
|
753
1797
|
callbacks: {
|
|
754
1798
|
title: function(context) {
|
|
755
|
-
const
|
|
756
|
-
|
|
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)`;
|
|
1799
|
+
const index = context[0].dataIndex;
|
|
1800
|
+
return filteredOriginalLabels[index];
|
|
760
1801
|
},
|
|
761
1802
|
label: function(context) {
|
|
762
1803
|
const speed = context.parsed.y;
|
|
763
|
-
if (speed === 0) {
|
|
764
|
-
return 'No reading activity';
|
|
765
|
-
}
|
|
766
1804
|
return `Speed: ${speed.toLocaleString()} chars/hour`;
|
|
767
1805
|
},
|
|
768
1806
|
afterLabel: function(context) {
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
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`;
|
|
1807
|
+
const index = context[0].dataIndex;
|
|
1808
|
+
const date = new Date(filteredOriginalLabels[index]);
|
|
1809
|
+
const dayOfWeek = date.getDay();
|
|
1810
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
1811
|
+
return isWeekend ? '📅 Weekend' : '';
|
|
778
1812
|
}
|
|
779
1813
|
},
|
|
780
1814
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
@@ -804,11 +1838,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
804
1838
|
x: {
|
|
805
1839
|
title: {
|
|
806
1840
|
display: true,
|
|
807
|
-
text: '
|
|
1841
|
+
text: 'Date',
|
|
808
1842
|
color: getThemeTextColor()
|
|
809
1843
|
},
|
|
810
1844
|
ticks: {
|
|
811
|
-
color: getThemeTextColor()
|
|
1845
|
+
color: getThemeTextColor(),
|
|
1846
|
+
maxRotation: 45,
|
|
1847
|
+
minRotation: 45
|
|
812
1848
|
}
|
|
813
1849
|
}
|
|
814
1850
|
},
|
|
@@ -822,19 +1858,151 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
822
1858
|
return window.myCharts[canvasId];
|
|
823
1859
|
}
|
|
824
1860
|
|
|
825
|
-
// Initialize Kanji Grid Renderer (using shared component)
|
|
826
|
-
const kanjiGridRenderer = new KanjiGridRenderer({
|
|
827
|
-
containerSelector: '#kanjiGrid',
|
|
828
|
-
counterSelector: '#kanjiCount',
|
|
829
|
-
colorMode: 'backend',
|
|
830
|
-
emptyMessage: 'No kanji data available'
|
|
831
|
-
});
|
|
832
|
-
|
|
833
1861
|
// Function to create kanji grid (now using shared renderer)
|
|
834
1862
|
function createKanjiGrid(kanjiData) {
|
|
835
1863
|
kanjiGridRenderer.render(kanjiData);
|
|
836
1864
|
}
|
|
837
1865
|
|
|
1866
|
+
// Function to update game milestones display
|
|
1867
|
+
function updateGameMilestones(gameMilestones) {
|
|
1868
|
+
const oldestCard = document.getElementById('oldestGameCard');
|
|
1869
|
+
const newestCard = document.getElementById('newestGameCard');
|
|
1870
|
+
const noDataMsg = document.getElementById('milestonesNoData');
|
|
1871
|
+
|
|
1872
|
+
if (!gameMilestones || (!gameMilestones.oldest_game && !gameMilestones.newest_game)) {
|
|
1873
|
+
// No milestone data available
|
|
1874
|
+
if (oldestCard) oldestCard.style.display = 'none';
|
|
1875
|
+
if (newestCard) newestCard.style.display = 'none';
|
|
1876
|
+
if (noDataMsg) noDataMsg.style.display = 'block';
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Hide no data message
|
|
1881
|
+
if (noDataMsg) noDataMsg.style.display = 'none';
|
|
1882
|
+
|
|
1883
|
+
// Update oldest game card
|
|
1884
|
+
if (gameMilestones.oldest_game && oldestCard) {
|
|
1885
|
+
const game = gameMilestones.oldest_game;
|
|
1886
|
+
|
|
1887
|
+
// Update image with proper base64 handling
|
|
1888
|
+
const imageEl = document.getElementById('oldestGameImage');
|
|
1889
|
+
if (game.image && game.image.trim()) {
|
|
1890
|
+
let imageSrc = game.image.trim();
|
|
1891
|
+
|
|
1892
|
+
// Check if it's a base64 image or URL
|
|
1893
|
+
if (imageSrc.startsWith('data:image')) {
|
|
1894
|
+
console.log('[DEBUG] Setting base64 image with data URI for oldest game');
|
|
1895
|
+
imageEl.src = imageSrc;
|
|
1896
|
+
imageEl.style.display = 'block';
|
|
1897
|
+
} else if (imageSrc.startsWith('http')) {
|
|
1898
|
+
console.log('[DEBUG] Setting URL image for oldest game:', imageSrc);
|
|
1899
|
+
imageEl.src = imageSrc;
|
|
1900
|
+
imageEl.style.display = 'block';
|
|
1901
|
+
} else if (imageSrc.startsWith('/9j/') || imageSrc.startsWith('iVBOR')) {
|
|
1902
|
+
// Raw base64 data without data URI prefix - add it
|
|
1903
|
+
// /9j/ is JPEG, iVBOR is PNG
|
|
1904
|
+
const mimeType = imageSrc.startsWith('/9j/') ? 'image/jpeg' : 'image/png';
|
|
1905
|
+
imageSrc = `data:${mimeType};base64,${imageSrc}`;
|
|
1906
|
+
console.log('[DEBUG] Added data URI prefix to raw base64 data for oldest game');
|
|
1907
|
+
imageEl.src = imageSrc;
|
|
1908
|
+
imageEl.style.display = 'block';
|
|
1909
|
+
} else {
|
|
1910
|
+
// Invalid image format, use placeholder
|
|
1911
|
+
console.log('[DEBUG] Invalid image format for oldest game, using placeholder');
|
|
1912
|
+
imageEl.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
imageEl.onerror = function() {
|
|
1916
|
+
this.style.display = 'none';
|
|
1917
|
+
this.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1918
|
+
};
|
|
1919
|
+
} else {
|
|
1920
|
+
console.log('[DEBUG] No image data for oldest game, using placeholder');
|
|
1921
|
+
imageEl.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Update title
|
|
1925
|
+
document.getElementById('oldestGameTitle').textContent = game.title_original || 'Unknown Game';
|
|
1926
|
+
|
|
1927
|
+
// Update subtitle (romaji or english)
|
|
1928
|
+
const subtitle = game.title_romaji || game.title_english || '';
|
|
1929
|
+
const subtitleEl = document.getElementById('oldestGameSubtitle');
|
|
1930
|
+
if (subtitle) {
|
|
1931
|
+
subtitleEl.textContent = subtitle;
|
|
1932
|
+
subtitleEl.style.display = 'block';
|
|
1933
|
+
} else {
|
|
1934
|
+
subtitleEl.style.display = 'none';
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Update release date
|
|
1938
|
+
document.getElementById('oldestGameReleaseYear').textContent = game.release_date || 'Unknown';
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
oldestCard.style.display = 'flex';
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// Update newest game card
|
|
1945
|
+
if (gameMilestones.newest_game && newestCard) {
|
|
1946
|
+
const game = gameMilestones.newest_game;
|
|
1947
|
+
|
|
1948
|
+
// Update image with proper base64 handling
|
|
1949
|
+
const imageEl = document.getElementById('newestGameImage');
|
|
1950
|
+
if (game.image && game.image.trim()) {
|
|
1951
|
+
let imageSrc = game.image.trim();
|
|
1952
|
+
|
|
1953
|
+
// Check if it's a base64 image or URL
|
|
1954
|
+
if (imageSrc.startsWith('data:image')) {
|
|
1955
|
+
console.log('[DEBUG] Setting base64 image with data URI for newest game');
|
|
1956
|
+
imageEl.src = imageSrc;
|
|
1957
|
+
imageEl.style.display = 'block';
|
|
1958
|
+
} else if (imageSrc.startsWith('http')) {
|
|
1959
|
+
console.log('[DEBUG] Setting URL image for newest game:', imageSrc);
|
|
1960
|
+
imageEl.src = imageSrc;
|
|
1961
|
+
imageEl.style.display = 'block';
|
|
1962
|
+
} else if (imageSrc.startsWith('/9j/') || imageSrc.startsWith('iVBOR')) {
|
|
1963
|
+
// Raw base64 data without data URI prefix - add it
|
|
1964
|
+
// /9j/ is JPEG, iVBOR is PNG
|
|
1965
|
+
const mimeType = imageSrc.startsWith('/9j/') ? 'image/jpeg' : 'image/png';
|
|
1966
|
+
imageSrc = `data:${mimeType};base64,${imageSrc}`;
|
|
1967
|
+
console.log('[DEBUG] Added data URI prefix to raw base64 data for newest game');
|
|
1968
|
+
imageEl.src = imageSrc;
|
|
1969
|
+
imageEl.style.display = 'block';
|
|
1970
|
+
} else {
|
|
1971
|
+
// Invalid image format, use placeholder
|
|
1972
|
+
console.log('[DEBUG] Invalid image format for newest game, using placeholder');
|
|
1973
|
+
imageEl.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
imageEl.onerror = function() {
|
|
1977
|
+
this.style.display = 'none';
|
|
1978
|
+
this.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1979
|
+
};
|
|
1980
|
+
} else {
|
|
1981
|
+
console.log('[DEBUG] No image data for newest game, using placeholder');
|
|
1982
|
+
imageEl.parentElement.innerHTML = '<div class="milestone-game-image placeholder">🎮</div>';
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Update title
|
|
1986
|
+
document.getElementById('newestGameTitle').textContent = game.title_original || 'Unknown Game';
|
|
1987
|
+
|
|
1988
|
+
// Update subtitle (romaji or english)
|
|
1989
|
+
const subtitle = game.title_romaji || game.title_english || '';
|
|
1990
|
+
const subtitleEl = document.getElementById('newestGameSubtitle');
|
|
1991
|
+
if (subtitle) {
|
|
1992
|
+
subtitleEl.textContent = subtitle;
|
|
1993
|
+
subtitleEl.style.display = 'block';
|
|
1994
|
+
} else {
|
|
1995
|
+
subtitleEl.style.display = 'none';
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Update release date
|
|
1999
|
+
document.getElementById('newestGameReleaseYear').textContent = game.release_date || 'Unknown';
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
newestCard.style.display = 'flex';
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
838
2006
|
// Function to update peak statistics display
|
|
839
2007
|
function updatePeakStatistics(peakDailyStats, peakSessionStats) {
|
|
840
2008
|
// Helper function to format large numbers
|
|
@@ -908,6 +2076,105 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
908
2076
|
});
|
|
909
2077
|
}
|
|
910
2078
|
|
|
2079
|
+
// Cache for filtered datasets to avoid re-filtering
|
|
2080
|
+
let cachedFilteredDatasets = null;
|
|
2081
|
+
|
|
2082
|
+
// Function to get or create filtered datasets
|
|
2083
|
+
function getFilteredDatasets(data) {
|
|
2084
|
+
// Return cached version if available and data hasn't changed
|
|
2085
|
+
if (cachedFilteredDatasets && cachedFilteredDatasets.sourceData === data.datasets) {
|
|
2086
|
+
return cachedFilteredDatasets;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Filter datasets for each chart
|
|
2090
|
+
const linesData = {
|
|
2091
|
+
labels: data.labels,
|
|
2092
|
+
datasets: data.datasets.filter(d => d.for === "Lines Received")
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
const charsData = {
|
|
2096
|
+
labels: data.labels,
|
|
2097
|
+
datasets: data.datasets.filter(d => d.for === 'Characters Read')
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
// Remove the 'hidden' property so they appear on their own charts
|
|
2101
|
+
[...charsData.datasets].forEach(d => delete d.hidden);
|
|
2102
|
+
|
|
2103
|
+
// Cache the result
|
|
2104
|
+
cachedFilteredDatasets = {
|
|
2105
|
+
sourceData: data.datasets,
|
|
2106
|
+
linesData: linesData,
|
|
2107
|
+
charsData: charsData
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
return cachedFilteredDatasets;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Function to load and render daily activity charts
|
|
2114
|
+
async function loadDailyActivityCharts(useAllTimeData = false) {
|
|
2115
|
+
try {
|
|
2116
|
+
let url = '/api/daily-activity';
|
|
2117
|
+
if (useAllTimeData) {
|
|
2118
|
+
url += '?all_time=true';
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const response = await fetch(url);
|
|
2122
|
+
if (!response.ok) {
|
|
2123
|
+
throw new Error('Failed to load daily activity data');
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
const data = await response.json();
|
|
2127
|
+
|
|
2128
|
+
// Create the charts with the isAllTime flag
|
|
2129
|
+
if (data.labels && data.labels.length > 0) {
|
|
2130
|
+
createDailyTimeChart('dailyTimeChart', data, useAllTimeData);
|
|
2131
|
+
createDailyCharsChart('dailyCharsChart', data, useAllTimeData);
|
|
2132
|
+
createDailySpeedChart('dailySpeedChart', data, useAllTimeData);
|
|
2133
|
+
} else {
|
|
2134
|
+
console.log('No daily activity data available');
|
|
2135
|
+
}
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
console.error('Error loading daily activity charts:', error);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Function to load mining heatmap data
|
|
2142
|
+
async function loadMiningHeatmap(start_timestamp = null, end_timestamp = null) {
|
|
2143
|
+
try {
|
|
2144
|
+
let url = '/api/mining_heatmap';
|
|
2145
|
+
const params = new URLSearchParams();
|
|
2146
|
+
|
|
2147
|
+
if (start_timestamp && end_timestamp) {
|
|
2148
|
+
params.append('start', start_timestamp);
|
|
2149
|
+
params.append('end', end_timestamp);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const queryString = params.toString();
|
|
2153
|
+
if (queryString) {
|
|
2154
|
+
url += `?${queryString}`;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const resp = await fetch(url);
|
|
2158
|
+
if (!resp.ok) throw new Error('Failed to load mining heatmap');
|
|
2159
|
+
const data = await resp.json();
|
|
2160
|
+
|
|
2161
|
+
if (data && Object.keys(data).length > 0) {
|
|
2162
|
+
createMiningHeatmap(data);
|
|
2163
|
+
} else {
|
|
2164
|
+
const container = document.getElementById('miningHeatmapContainer');
|
|
2165
|
+
if (container) {
|
|
2166
|
+
container.innerHTML = '<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">No mining data available for the selected date range.</p>';
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
console.error('Failed to load mining heatmap:', e);
|
|
2171
|
+
const container = document.getElementById('miningHeatmapContainer');
|
|
2172
|
+
if (container) {
|
|
2173
|
+
container.innerHTML = '<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">Failed to load mining heatmap.</p>';
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
911
2178
|
// Function to load stats data with optional year filter
|
|
912
2179
|
function loadStatsData(start_timestamp = null, end_timestamp = null) {
|
|
913
2180
|
let url = '/api/stats';
|
|
@@ -924,6 +2191,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
924
2191
|
url += `?${queryString}`;
|
|
925
2192
|
}
|
|
926
2193
|
|
|
2194
|
+
// Load mining heatmap separately
|
|
2195
|
+
loadMiningHeatmap(start_timestamp, end_timestamp);
|
|
2196
|
+
|
|
927
2197
|
return fetch(url)
|
|
928
2198
|
.then(response => response.json())
|
|
929
2199
|
.then(data => {
|
|
@@ -940,19 +2210,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
940
2210
|
return data;
|
|
941
2211
|
}
|
|
942
2212
|
|
|
943
|
-
//
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const charsData = {
|
|
950
|
-
labels: data.labels,
|
|
951
|
-
datasets: data.datasets.filter(d => d.for === 'Characters Read')
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
// Remove the 'hidden' property so they appear on their own charts
|
|
955
|
-
[...charsData.datasets].forEach(d => delete d.hidden);
|
|
2213
|
+
// Get filtered datasets (cached if possible)
|
|
2214
|
+
const filtered = getFilteredDatasets(data);
|
|
2215
|
+
const linesData = filtered.linesData;
|
|
2216
|
+
const charsData = filtered.charsData;
|
|
956
2217
|
|
|
957
2218
|
// Charts are re-created with the new data
|
|
958
2219
|
createChart('linesChart', linesData, 'Cumulative Lines Received');
|
|
@@ -960,12 +2221,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
960
2221
|
|
|
961
2222
|
// Create reading chars quantity chart if data exists (with trendline)
|
|
962
2223
|
if (data.totalCharsPerGame) {
|
|
963
|
-
createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read',
|
|
2224
|
+
createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read', false);
|
|
964
2225
|
}
|
|
965
2226
|
|
|
966
2227
|
// Create reading time quantity chart if data exists (with trendline)
|
|
967
2228
|
if (data.readingTimePerGame) {
|
|
968
|
-
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime,
|
|
2229
|
+
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime, false);
|
|
969
2230
|
}
|
|
970
2231
|
|
|
971
2232
|
// Create reading speed per game chart if data exists (with trendline)
|
|
@@ -983,6 +2244,53 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
983
2244
|
createHourlyReadingSpeedChart('hourlyReadingSpeedChart', data.hourlyReadingSpeedData);
|
|
984
2245
|
}
|
|
985
2246
|
|
|
2247
|
+
// Create top 5 reading speed days chart if data exists
|
|
2248
|
+
if (data.readingSpeedHeatmapData) {
|
|
2249
|
+
createTopReadingSpeedDaysChart('topReadingSpeedDaysChart', data.readingSpeedHeatmapData);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// Create top 5 character count days chart if data exists
|
|
2253
|
+
if (data.heatmapData) {
|
|
2254
|
+
createTopCharacterDaysChart('topCharacterDaysChart', data.heatmapData);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// Create day of week activity chart if data exists
|
|
2258
|
+
if (data.dayOfWeekData) {
|
|
2259
|
+
createDayOfWeekChart('dayOfWeekChart', data.dayOfWeekData);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// Create average hours by day chart if data exists
|
|
2263
|
+
if (data.dayOfWeekData) {
|
|
2264
|
+
createAvgHoursByDayChart('avgHoursByDayChart', data.dayOfWeekData);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// Create difficulty speed chart if data exists
|
|
2268
|
+
if (data.difficultySpeedData) {
|
|
2269
|
+
createDifficultySpeedChart('difficultySpeedChart', data.difficultySpeedData);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// Create game type chart if data exists
|
|
2273
|
+
if (data.gameTypeData) {
|
|
2274
|
+
createGameTypeChart('gameTypeChart', data.gameTypeData);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
createCardsMinedChart('cardsMinedChart', data.cardsMinedLast30Days || null);
|
|
2278
|
+
|
|
2279
|
+
// Create mining heatmap if data exists
|
|
2280
|
+
if (data.miningHeatmapData) {
|
|
2281
|
+
if (Object.keys(data.miningHeatmapData).length > 0) {
|
|
2282
|
+
createMiningHeatmap(data.miningHeatmapData);
|
|
2283
|
+
} else {
|
|
2284
|
+
const container = document.getElementById('miningHeatmapContainer');
|
|
2285
|
+
if (container) {
|
|
2286
|
+
container.innerHTML = '<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">No mining data available for the selected date range.</p>';
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// Load and create daily activity charts
|
|
2292
|
+
loadDailyActivityCharts();
|
|
2293
|
+
|
|
986
2294
|
// Create kanji grid if data exists
|
|
987
2295
|
if (data.kanjiGridData) {
|
|
988
2296
|
createKanjiGrid(data.kanjiGridData);
|
|
@@ -993,6 +2301,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
993
2301
|
updatePeakStatistics(data.peakDailyStats, data.peakSessionStats);
|
|
994
2302
|
}
|
|
995
2303
|
|
|
2304
|
+
// Update game milestones if data exists
|
|
2305
|
+
if (data.gameMilestones) {
|
|
2306
|
+
updateGameMilestones(data.gameMilestones);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
996
2309
|
return data;
|
|
997
2310
|
})
|
|
998
2311
|
.catch(error => {
|
|
@@ -1016,7 +2329,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1016
2329
|
}
|
|
1017
2330
|
|
|
1018
2331
|
// ================================
|
|
1019
|
-
// Initialize date inputs with sessionStorage or
|
|
2332
|
+
// Initialize date inputs with sessionStorage or use config values
|
|
1020
2333
|
// Dispatches "datesSet" event once dates are set
|
|
1021
2334
|
// ================================
|
|
1022
2335
|
function initializeDates() {
|
|
@@ -1026,27 +2339,26 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1026
2339
|
if (!fromDateInput || !toDateInput) return; // Null check
|
|
1027
2340
|
|
|
1028
2341
|
const fromDate = sessionStorage.getItem("fromDate");
|
|
1029
|
-
const toDate = sessionStorage.getItem("toDate");
|
|
2342
|
+
const toDate = sessionStorage.getItem("toDate");
|
|
1030
2343
|
|
|
1031
2344
|
if (!(fromDate && toDate)) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
.
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
});
|
|
2345
|
+
// Use first_date from statsConfig if available (avoids extra API call)
|
|
2346
|
+
const firstDate = window.statsConfig && window.statsConfig.firstDate
|
|
2347
|
+
? window.statsConfig.firstDate
|
|
2348
|
+
: new Date().toLocaleDateString('en-CA'); // Fallback to today
|
|
2349
|
+
|
|
2350
|
+
fromDateInput.value = firstDate;
|
|
2351
|
+
|
|
2352
|
+
// Get today's date
|
|
2353
|
+
const today = new Date();
|
|
2354
|
+
const todayStr = today.toLocaleDateString('en-CA');
|
|
2355
|
+
toDateInput.value = todayStr;
|
|
2356
|
+
|
|
2357
|
+
// Save in sessionStorage
|
|
2358
|
+
sessionStorage.setItem("fromDate", firstDate);
|
|
2359
|
+
sessionStorage.setItem("toDate", todayStr);
|
|
2360
|
+
|
|
2361
|
+
document.dispatchEvent(new Event("datesSet"));
|
|
1050
2362
|
} else {
|
|
1051
2363
|
// If values already in sessionStorage, set inputs from there
|
|
1052
2364
|
fromDateInput.value = fromDate;
|
|
@@ -1130,6 +2442,53 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1130
2442
|
|
|
1131
2443
|
// Make functions globally available
|
|
1132
2444
|
window.loadStatsData = loadStatsData;
|
|
2445
|
+
|
|
2446
|
+
// Add toggle button functionality for all three daily charts
|
|
2447
|
+
const toggleTimeDataBtn = document.getElementById('toggleTimeDataBtn');
|
|
2448
|
+
const toggleCharsDataBtn = document.getElementById('toggleCharsDataBtn');
|
|
2449
|
+
const toggleSpeedDataBtn = document.getElementById('toggleSpeedDataBtn');
|
|
2450
|
+
|
|
2451
|
+
// Helper function to handle toggle button clicks
|
|
2452
|
+
function setupToggleButton(button) {
|
|
2453
|
+
if (!button) return;
|
|
2454
|
+
|
|
2455
|
+
button.addEventListener('click', function() {
|
|
2456
|
+
const currentMode = this.getAttribute('data-mode');
|
|
2457
|
+
|
|
2458
|
+
if (currentMode === '30days') {
|
|
2459
|
+
// Switch to all-time data
|
|
2460
|
+
this.setAttribute('data-mode', 'alltime');
|
|
2461
|
+
this.textContent = 'View 30 days data';
|
|
2462
|
+
loadDailyActivityCharts(true);
|
|
2463
|
+
|
|
2464
|
+
// Update all other buttons to match
|
|
2465
|
+
[toggleTimeDataBtn, toggleCharsDataBtn, toggleSpeedDataBtn].forEach(btn => {
|
|
2466
|
+
if (btn && btn !== this) {
|
|
2467
|
+
btn.setAttribute('data-mode', 'alltime');
|
|
2468
|
+
btn.textContent = 'View 30 days data';
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
} else {
|
|
2472
|
+
// Switch back to 30 days data
|
|
2473
|
+
this.setAttribute('data-mode', '30days');
|
|
2474
|
+
this.textContent = 'View all time data';
|
|
2475
|
+
loadDailyActivityCharts(false);
|
|
2476
|
+
|
|
2477
|
+
// Update all other buttons to match
|
|
2478
|
+
[toggleTimeDataBtn, toggleCharsDataBtn, toggleSpeedDataBtn].forEach(btn => {
|
|
2479
|
+
if (btn && btn !== this) {
|
|
2480
|
+
btn.setAttribute('data-mode', '30days');
|
|
2481
|
+
btn.textContent = 'View all time data';
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Setup all toggle buttons
|
|
2489
|
+
setupToggleButton(toggleTimeDataBtn);
|
|
2490
|
+
setupToggleButton(toggleCharsDataBtn);
|
|
2491
|
+
setupToggleButton(toggleSpeedDataBtn);
|
|
1133
2492
|
|
|
1134
2493
|
// ExStatic Import Functionality
|
|
1135
2494
|
const exstaticFileInput = document.getElementById('exstaticFile');
|
|
@@ -1143,7 +2502,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1143
2502
|
// Enable/disable import button based on file selection
|
|
1144
2503
|
exstaticFileInput.addEventListener('change', function(e) {
|
|
1145
2504
|
const file = e.target.files[0];
|
|
1146
|
-
|
|
2505
|
+
// Enable button whenever any file is selected
|
|
2506
|
+
if (file) {
|
|
1147
2507
|
importExstaticBtn.disabled = false;
|
|
1148
2508
|
importExstaticBtn.style.background = '#2980b9';
|
|
1149
2509
|
importExstaticBtn.style.cursor = 'pointer';
|
|
@@ -1152,9 +2512,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1152
2512
|
importExstaticBtn.disabled = true;
|
|
1153
2513
|
importExstaticBtn.style.background = '#666';
|
|
1154
2514
|
importExstaticBtn.style.cursor = 'not-allowed';
|
|
1155
|
-
if (file) {
|
|
1156
|
-
showImportStatus('Please select a valid CSV file.', 'error', true);
|
|
1157
|
-
}
|
|
1158
2515
|
}
|
|
1159
2516
|
});
|
|
1160
2517
|
|