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
|
@@ -10,6 +10,29 @@
|
|
|
10
10
|
border: 1px solid var(--border-color);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
.chart-toggle-btn {
|
|
14
|
+
background: var(--accent-color);
|
|
15
|
+
color: white;
|
|
16
|
+
border: none;
|
|
17
|
+
padding: 8px 16px;
|
|
18
|
+
border-radius: 6px;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
font-size: 14px;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
transition: all 0.3s ease;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.chart-toggle-btn:hover {
|
|
27
|
+
background: #0056b3;
|
|
28
|
+
transform: translateY(-1px);
|
|
29
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.chart-toggle-btn:active {
|
|
33
|
+
transform: translateY(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
.heatmap-year {
|
|
14
37
|
margin-bottom: 30px;
|
|
15
38
|
}
|
|
@@ -443,6 +466,204 @@
|
|
|
443
466
|
margin-bottom: 16px;
|
|
444
467
|
}
|
|
445
468
|
|
|
469
|
+
/* ================================
|
|
470
|
+
Game Milestones Card Styles
|
|
471
|
+
================================ */
|
|
472
|
+
.game-milestones {
|
|
473
|
+
margin-bottom: 24px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.game-milestones-grid {
|
|
477
|
+
display: grid;
|
|
478
|
+
grid-template-columns: repeat(2, 1fr);
|
|
479
|
+
gap: 20px;
|
|
480
|
+
margin-top: 16px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.milestone-game-card {
|
|
484
|
+
background: var(--bg-tertiary);
|
|
485
|
+
border-radius: 12px;
|
|
486
|
+
padding: 20px;
|
|
487
|
+
border: 1px solid var(--border-color);
|
|
488
|
+
transition: all 0.3s ease;
|
|
489
|
+
display: flex;
|
|
490
|
+
gap: 16px;
|
|
491
|
+
align-items: flex-start;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.milestone-game-card:hover {
|
|
495
|
+
background: var(--border-color);
|
|
496
|
+
transform: translateY(-2px);
|
|
497
|
+
box-shadow: 0 4px 12px var(--shadow-color);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.milestone-game-image-container {
|
|
501
|
+
flex-shrink: 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.milestone-game-image {
|
|
505
|
+
width: 120px;
|
|
506
|
+
height: 160px;
|
|
507
|
+
object-fit: cover;
|
|
508
|
+
border-radius: 8px;
|
|
509
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
510
|
+
border: 1px solid var(--border-color);
|
|
511
|
+
background: var(--bg-secondary);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.milestone-game-image.placeholder {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
font-size: 48px;
|
|
519
|
+
opacity: 0.3;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.milestone-game-info {
|
|
523
|
+
flex: 1;
|
|
524
|
+
display: flex;
|
|
525
|
+
flex-direction: column;
|
|
526
|
+
gap: 8px;
|
|
527
|
+
min-width: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.milestone-label {
|
|
531
|
+
font-size: 11px;
|
|
532
|
+
font-weight: 700;
|
|
533
|
+
text-transform: uppercase;
|
|
534
|
+
letter-spacing: 0.8px;
|
|
535
|
+
color: var(--text-tertiary);
|
|
536
|
+
margin-bottom: 4px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.milestone-label.oldest {
|
|
540
|
+
color: #e6dc2e;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.milestone-label.newest {
|
|
544
|
+
color: #2ee6e0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.milestone-game-title {
|
|
548
|
+
font-size: 18px;
|
|
549
|
+
font-weight: 700;
|
|
550
|
+
color: var(--text-primary);
|
|
551
|
+
line-height: 1.3;
|
|
552
|
+
margin-bottom: 2px;
|
|
553
|
+
overflow: hidden;
|
|
554
|
+
text-overflow: ellipsis;
|
|
555
|
+
display: -webkit-box;
|
|
556
|
+
-webkit-line-clamp: 2;
|
|
557
|
+
-webkit-box-orient: vertical;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.milestone-game-subtitle {
|
|
561
|
+
font-size: 13px;
|
|
562
|
+
font-weight: 500;
|
|
563
|
+
color: var(--text-secondary);
|
|
564
|
+
font-style: italic;
|
|
565
|
+
margin-bottom: 8px;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.milestone-game-details {
|
|
569
|
+
display: flex;
|
|
570
|
+
flex-direction: column;
|
|
571
|
+
gap: 6px;
|
|
572
|
+
font-size: 12px;
|
|
573
|
+
color: var(--text-secondary);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.milestone-detail-row {
|
|
577
|
+
display: flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
gap: 6px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.milestone-detail-icon {
|
|
583
|
+
font-size: 14px;
|
|
584
|
+
opacity: 0.7;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.milestone-detail-label {
|
|
588
|
+
font-weight: 600;
|
|
589
|
+
color: var(--text-tertiary);
|
|
590
|
+
min-width: 80px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.milestone-detail-value {
|
|
594
|
+
color: var(--text-primary);
|
|
595
|
+
font-weight: 500;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.milestone-type-badge {
|
|
599
|
+
display: inline-block;
|
|
600
|
+
padding: 3px 10px;
|
|
601
|
+
background: var(--accent-color);
|
|
602
|
+
color: white;
|
|
603
|
+
border-radius: 12px;
|
|
604
|
+
font-size: 10px;
|
|
605
|
+
font-weight: 700;
|
|
606
|
+
text-transform: uppercase;
|
|
607
|
+
letter-spacing: 0.5px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.milestone-no-data {
|
|
611
|
+
text-align: center;
|
|
612
|
+
padding: 40px 20px;
|
|
613
|
+
color: var(--text-tertiary);
|
|
614
|
+
font-style: italic;
|
|
615
|
+
grid-column: 1 / -1;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.milestone-no-data::before {
|
|
619
|
+
content: '📅';
|
|
620
|
+
display: block;
|
|
621
|
+
font-size: 48px;
|
|
622
|
+
margin-bottom: 12px;
|
|
623
|
+
opacity: 0.5;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Responsive Design for Game Milestones */
|
|
627
|
+
@media (max-width: 768px) {
|
|
628
|
+
.game-milestones-grid {
|
|
629
|
+
grid-template-columns: 1fr;
|
|
630
|
+
gap: 16px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.milestone-game-card {
|
|
634
|
+
flex-direction: column;
|
|
635
|
+
align-items: center;
|
|
636
|
+
text-align: center;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.milestone-game-info {
|
|
640
|
+
align-items: center;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.milestone-game-details {
|
|
644
|
+
align-items: center;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.milestone-detail-row {
|
|
648
|
+
justify-content: center;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
@media (max-width: 480px) {
|
|
653
|
+
.milestone-game-image {
|
|
654
|
+
width: 100px;
|
|
655
|
+
height: 133px;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.milestone-game-title {
|
|
659
|
+
font-size: 16px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.milestone-game-subtitle {
|
|
663
|
+
font-size: 12px;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
446
667
|
/* Dashboard card, stats grid, progress section, streak indicator, and date range styles are now in dashboard-shared.css */
|
|
447
668
|
|
|
448
669
|
/* ================================
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Bar Chart Component for GSM Statistics
|
|
3
|
+
* Provides a consistent interface for creating bar charts with the app's color system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class BarChartComponent {
|
|
7
|
+
constructor(canvasId, options = {}) {
|
|
8
|
+
this.canvasId = canvasId;
|
|
9
|
+
this.canvas = document.getElementById(canvasId);
|
|
10
|
+
this.chart = null;
|
|
11
|
+
|
|
12
|
+
// Default options
|
|
13
|
+
this.options = {
|
|
14
|
+
title: options.title || '',
|
|
15
|
+
type: options.type || 'vertical', // 'vertical' or 'horizontal'
|
|
16
|
+
yAxisLabel: options.yAxisLabel || '',
|
|
17
|
+
xAxisLabel: options.xAxisLabel || '',
|
|
18
|
+
colorScheme: options.colorScheme || 'gradient', // 'gradient', 'fixed', 'weekendHighlight', 'performance'
|
|
19
|
+
showLegend: options.showLegend !== undefined ? options.showLegend : false,
|
|
20
|
+
tooltipFormatter: options.tooltipFormatter || null,
|
|
21
|
+
valueFormatter: options.valueFormatter || null,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate colors based on the specified color scheme
|
|
28
|
+
*/
|
|
29
|
+
generateColors(data, scheme = 'gradient') {
|
|
30
|
+
const length = data.length;
|
|
31
|
+
const colors = [];
|
|
32
|
+
const borderColors = [];
|
|
33
|
+
const isDark = getCurrentTheme() === 'dark';
|
|
34
|
+
|
|
35
|
+
switch (scheme) {
|
|
36
|
+
case 'gradient':
|
|
37
|
+
// Orange to blue gradient based on normalized values (performance theme)
|
|
38
|
+
const maxVal = Math.max(...data.filter(v => v > 0));
|
|
39
|
+
const minVal = Math.min(...data.filter(v => v > 0));
|
|
40
|
+
|
|
41
|
+
data.forEach(value => {
|
|
42
|
+
if (value === 0) {
|
|
43
|
+
colors.push(isDark ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)');
|
|
44
|
+
borderColors.push(isDark ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)');
|
|
45
|
+
} else {
|
|
46
|
+
const normalized = maxVal > minVal ? (value - minVal) / (maxVal - minVal) : 0.5;
|
|
47
|
+
const hue = 30 + (normalized * 170); // 30 = orange, 200 = blue
|
|
48
|
+
colors.push(`hsla(${hue}, 70%, 50%, 0.8)`);
|
|
49
|
+
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'reverseGradient':
|
|
55
|
+
// Blue to orange gradient (for difficulty, etc.)
|
|
56
|
+
data.forEach((_, index) => {
|
|
57
|
+
const ratio = index / Math.max(length - 1, 1);
|
|
58
|
+
const hue = 200 - (ratio * 170); // 200 (blue) to 30 (orange)
|
|
59
|
+
colors.push(`hsla(${hue}, 70%, 50%, 0.8)`);
|
|
60
|
+
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'fixed':
|
|
65
|
+
// Fixed color scheme for day of week - cohesive gradient
|
|
66
|
+
const fixedColors = [
|
|
67
|
+
'rgba(54, 162, 235, 0.8)', // Monday - Blue
|
|
68
|
+
'rgba(75, 192, 192, 0.8)', // Tuesday - Teal
|
|
69
|
+
'rgba(102, 187, 106, 0.8)', // Wednesday - Green
|
|
70
|
+
'rgba(255, 167, 38, 0.8)', // Thursday - Orange
|
|
71
|
+
'rgba(239, 83, 80, 0.8)', // Friday - Red
|
|
72
|
+
'rgba(171, 71, 188, 0.8)', // Saturday - Purple
|
|
73
|
+
'rgba(126, 87, 194, 0.8)' // Sunday - Deep Purple
|
|
74
|
+
];
|
|
75
|
+
const fixedBorders = [
|
|
76
|
+
'rgba(54, 162, 235, 1)',
|
|
77
|
+
'rgba(75, 192, 192, 1)',
|
|
78
|
+
'rgba(102, 187, 106, 1)',
|
|
79
|
+
'rgba(255, 167, 38, 1)',
|
|
80
|
+
'rgba(239, 83, 80, 1)',
|
|
81
|
+
'rgba(171, 71, 188, 1)',
|
|
82
|
+
'rgba(126, 87, 194, 1)'
|
|
83
|
+
];
|
|
84
|
+
colors.push(...fixedColors.slice(0, length));
|
|
85
|
+
borderColors.push(...fixedBorders.slice(0, length));
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'weekendHighlight':
|
|
89
|
+
// Highlight weekends with different colors
|
|
90
|
+
this.options.labels.forEach((dateStr, index) => {
|
|
91
|
+
const date = new Date(dateStr);
|
|
92
|
+
const dayOfWeek = date.getDay();
|
|
93
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
94
|
+
|
|
95
|
+
if (data[index] === 0) {
|
|
96
|
+
colors.push(isDark ? 'rgba(100, 100, 100, 0.3)' : 'rgba(200, 200, 200, 0.3)');
|
|
97
|
+
borderColors.push(isDark ? 'rgba(100, 100, 100, 0.6)' : 'rgba(200, 200, 200, 0.6)');
|
|
98
|
+
} else if (isWeekend) {
|
|
99
|
+
colors.push(this.options.weekendColor || 'rgba(171, 71, 188, 0.8)');
|
|
100
|
+
borderColors.push(this.options.weekendBorderColor || 'rgba(171, 71, 188, 1)');
|
|
101
|
+
} else {
|
|
102
|
+
colors.push(this.options.weekdayColor || 'rgba(54, 162, 235, 0.8)');
|
|
103
|
+
borderColors.push(this.options.weekdayBorderColor || 'rgba(54, 162, 235, 1)');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'performance':
|
|
109
|
+
// Blue gradient for top performance charts
|
|
110
|
+
data.forEach((_, index) => {
|
|
111
|
+
const reverseIndex = length - 1 - index;
|
|
112
|
+
const hue = 200 - (reverseIndex * 15); // 200 (blue) to 155 (cyan)
|
|
113
|
+
colors.push(`hsla(${hue}, 70%, 50%, 0.8)`);
|
|
114
|
+
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'single':
|
|
119
|
+
// Single primary color
|
|
120
|
+
const primaryColor = isDark ? '#4e9fff' : '#2980b9';
|
|
121
|
+
data.forEach(() => {
|
|
122
|
+
colors.push(`${primaryColor}CC`);
|
|
123
|
+
borderColors.push(primaryColor);
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
// Default to gradient
|
|
129
|
+
return this.generateColors(data, 'gradient');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { colors, borderColors };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create the bar chart
|
|
137
|
+
*/
|
|
138
|
+
render(data, labels) {
|
|
139
|
+
if (!this.canvas) {
|
|
140
|
+
console.error(`Canvas with id '${this.canvasId}' not found`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Destroy existing chart
|
|
145
|
+
if (this.chart) {
|
|
146
|
+
this.chart.destroy();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ctx = this.canvas.getContext('2d');
|
|
150
|
+
const { colors, borderColors } = this.generateColors(data, this.options.colorScheme);
|
|
151
|
+
|
|
152
|
+
const chartConfig = {
|
|
153
|
+
type: 'bar',
|
|
154
|
+
data: {
|
|
155
|
+
labels: labels,
|
|
156
|
+
datasets: [{
|
|
157
|
+
label: this.options.datasetLabel || this.options.title,
|
|
158
|
+
data: data,
|
|
159
|
+
backgroundColor: colors,
|
|
160
|
+
borderColor: borderColors,
|
|
161
|
+
borderWidth: 2,
|
|
162
|
+
borderRadius: this.options.borderRadius || 4
|
|
163
|
+
}]
|
|
164
|
+
},
|
|
165
|
+
options: {
|
|
166
|
+
indexAxis: this.options.type === 'horizontal' ? 'y' : 'x',
|
|
167
|
+
responsive: true,
|
|
168
|
+
plugins: {
|
|
169
|
+
legend: {
|
|
170
|
+
display: this.options.showLegend,
|
|
171
|
+
labels: {
|
|
172
|
+
color: getThemeTextColor()
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
title: {
|
|
176
|
+
display: !!this.options.title,
|
|
177
|
+
text: this.options.title,
|
|
178
|
+
color: getThemeTextColor(),
|
|
179
|
+
font: {
|
|
180
|
+
size: 16,
|
|
181
|
+
weight: 'bold'
|
|
182
|
+
},
|
|
183
|
+
padding: {
|
|
184
|
+
top: 10,
|
|
185
|
+
bottom: 20
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
tooltip: {
|
|
189
|
+
callbacks: {
|
|
190
|
+
title: (context) => {
|
|
191
|
+
if (this.options.tooltipFormatter?.title) {
|
|
192
|
+
return this.options.tooltipFormatter.title(context);
|
|
193
|
+
}
|
|
194
|
+
// Get the actual label from the dataset
|
|
195
|
+
const index = context[0].dataIndex;
|
|
196
|
+
return labels[index];
|
|
197
|
+
},
|
|
198
|
+
label: (context) => {
|
|
199
|
+
if (this.options.tooltipFormatter?.label) {
|
|
200
|
+
return this.options.tooltipFormatter.label(context);
|
|
201
|
+
}
|
|
202
|
+
// For horizontal charts, value is in parsed.x; for vertical, it's in parsed.y
|
|
203
|
+
const value = chartConfig.options.indexAxis === 'y' ? context.parsed.x : context.parsed.y;
|
|
204
|
+
if (this.options.valueFormatter) {
|
|
205
|
+
return this.options.valueFormatter(value);
|
|
206
|
+
}
|
|
207
|
+
return `${this.options.datasetLabel || 'Value'}: ${value.toLocaleString()}`;
|
|
208
|
+
},
|
|
209
|
+
afterLabel: (context) => {
|
|
210
|
+
if (this.options.tooltipFormatter?.afterLabel) {
|
|
211
|
+
return this.options.tooltipFormatter.afterLabel(context);
|
|
212
|
+
}
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
217
|
+
titleColor: '#fff',
|
|
218
|
+
bodyColor: '#fff',
|
|
219
|
+
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
220
|
+
borderWidth: 1,
|
|
221
|
+
cornerRadius: 8,
|
|
222
|
+
displayColors: true
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
scales: {
|
|
226
|
+
y: {
|
|
227
|
+
beginAtZero: true,
|
|
228
|
+
title: {
|
|
229
|
+
display: !!this.options.yAxisLabel,
|
|
230
|
+
text: this.options.yAxisLabel,
|
|
231
|
+
color: getThemeTextColor()
|
|
232
|
+
},
|
|
233
|
+
ticks: {
|
|
234
|
+
color: getThemeTextColor(),
|
|
235
|
+
callback: function(value) {
|
|
236
|
+
// For horizontal charts, Y-axis shows labels (dates, etc.)
|
|
237
|
+
if (chartConfig.options.indexAxis === 'y') {
|
|
238
|
+
return this.getLabelForValue(value);
|
|
239
|
+
}
|
|
240
|
+
// For vertical charts, Y-axis shows numeric values
|
|
241
|
+
if (chartConfig.options.yAxisFormatter) {
|
|
242
|
+
return chartConfig.options.yAxisFormatter(value);
|
|
243
|
+
}
|
|
244
|
+
return value.toLocaleString();
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
grid: {
|
|
248
|
+
color: getCurrentTheme() === 'dark'
|
|
249
|
+
? 'rgba(255, 255, 255, 0.1)'
|
|
250
|
+
: 'rgba(0, 0, 0, 0.1)'
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
x: {
|
|
254
|
+
title: {
|
|
255
|
+
display: !!this.options.xAxisLabel,
|
|
256
|
+
text: this.options.xAxisLabel,
|
|
257
|
+
color: getThemeTextColor()
|
|
258
|
+
},
|
|
259
|
+
ticks: {
|
|
260
|
+
color: getThemeTextColor(),
|
|
261
|
+
maxRotation: this.options.maxRotation || 45,
|
|
262
|
+
minRotation: this.options.minRotation || 45,
|
|
263
|
+
callback: function(value) {
|
|
264
|
+
// For horizontal charts, X-axis shows numeric values
|
|
265
|
+
if (chartConfig.options.indexAxis === 'y') {
|
|
266
|
+
return value.toLocaleString();
|
|
267
|
+
}
|
|
268
|
+
// For vertical charts, X-axis shows labels
|
|
269
|
+
return this.getLabelForValue(value);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
grid: {
|
|
273
|
+
display: this.options.type === 'horizontal'
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
animation: {
|
|
278
|
+
duration: 1000,
|
|
279
|
+
easing: 'easeOutQuart'
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
this.chart = new Chart(ctx, chartConfig);
|
|
285
|
+
|
|
286
|
+
// Store in global charts object
|
|
287
|
+
if (window.myCharts) {
|
|
288
|
+
window.myCharts[this.canvasId] = this.chart;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this.chart;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Update chart data
|
|
296
|
+
*/
|
|
297
|
+
update(data, labels) {
|
|
298
|
+
if (!this.chart) {
|
|
299
|
+
return this.render(data, labels);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { colors, borderColors } = this.generateColors(data, this.options.colorScheme);
|
|
303
|
+
|
|
304
|
+
this.chart.data.labels = labels;
|
|
305
|
+
this.chart.data.datasets[0].data = data;
|
|
306
|
+
this.chart.data.datasets[0].backgroundColor = colors;
|
|
307
|
+
this.chart.data.datasets[0].borderColor = borderColors;
|
|
308
|
+
this.chart.update();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Destroy the chart
|
|
313
|
+
*/
|
|
314
|
+
destroy() {
|
|
315
|
+
if (this.chart) {
|
|
316
|
+
this.chart.destroy();
|
|
317
|
+
this.chart = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Helper function to get theme text color (should match stats.js)
|
|
323
|
+
function getThemeTextColor() {
|
|
324
|
+
const theme = getCurrentTheme();
|
|
325
|
+
return theme === 'dark' ? '#fff' : '#222';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Helper function to get current theme (should match stats.js)
|
|
329
|
+
function getCurrentTheme() {
|
|
330
|
+
const dataTheme = document.documentElement.getAttribute('data-theme');
|
|
331
|
+
if (dataTheme === 'dark' || dataTheme === 'light') {
|
|
332
|
+
return dataTheme;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
336
|
+
return 'dark';
|
|
337
|
+
}
|
|
338
|
+
return 'light';
|
|
339
|
+
}
|