datalog-theme 0.6.0

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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. metadata +555 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @fileoverview Academic features for citation display and research tools.
3
+ * Handles citation metrics, publication lists, and academic integrations.
4
+ * @module academic
5
+ */
6
+
7
+ /**
8
+ * Populates citation metric elements with data from global academic data.
9
+ * Supports total citations, h-index, i10-index, and yearly breakdowns.
10
+ * @returns {void}
11
+ */
12
+ export function populateCitationMetrics() {
13
+ const academicData = window.DatalogAcademic || {};
14
+ const publicationsData = window.DatalogPublications || {};
15
+ const metrics = publicationsData.metrics || (academicData.citations ? academicData.citations.metrics : {}) || {};
16
+ const yearlyTotals = publicationsData.yearly_totals || (academicData.citations ? academicData.citations.yearly_totals : {}) || {};
17
+
18
+ document.querySelectorAll('[data-citation-metric]').forEach((element) => {
19
+ const key = element.getAttribute('data-citation-metric');
20
+ if (!key) {
21
+ return;
22
+ }
23
+ let value;
24
+ switch (key) {
25
+ case 'total':
26
+ value = metrics.total;
27
+ break;
28
+ case 'h_index':
29
+ case 'h-index':
30
+ value = metrics.h_index;
31
+ break;
32
+ case 'i10_index':
33
+ case 'i10-index':
34
+ value = metrics.i10_index;
35
+ break;
36
+ case 'since_total':
37
+ value = metrics.since_2019 ? metrics.since_2019.total : undefined;
38
+ break;
39
+ case 'since_h_index':
40
+ value = metrics.since_2019 ? metrics.since_2019.h_index : undefined;
41
+ break;
42
+ case 'since_i10':
43
+ value = metrics.since_2019 ? metrics.since_2019.i10_index : undefined;
44
+ break;
45
+ default:
46
+ if (key.startsWith('year_')) {
47
+ const yearKey = key.replace('year_', '');
48
+ value = yearlyTotals[yearKey];
49
+ }
50
+ break;
51
+ }
52
+ if (typeof value === 'number') {
53
+ element.textContent = value.toLocaleString();
54
+ } else if (value) {
55
+ element.textContent = value;
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Populates citation counts for individual publications.
62
+ * @returns {void}
63
+ */
64
+ export function populatePublicationCitations() {
65
+ const academicData = window.DatalogAcademic || {};
66
+ const publicationsData = window.DatalogPublications || {};
67
+ const perPublication = publicationsData.citation_totals || (academicData.citations ? academicData.citations.per_publication : {}) || {};
68
+
69
+ document.querySelectorAll('[data-publication-citations]').forEach((element) => {
70
+ const publicationId = element.getAttribute('data-publication-citations');
71
+ if (!publicationId) {
72
+ return;
73
+ }
74
+ const lower = publicationId.toLowerCase();
75
+ let value;
76
+ if (perPublication[publicationId]) {
77
+ value = perPublication[publicationId].total || perPublication[publicationId];
78
+ } else if (perPublication[lower]) {
79
+ value = perPublication[lower].total || perPublication[lower];
80
+ }
81
+ if (typeof value === 'number') {
82
+ element.textContent = value.toLocaleString();
83
+ }
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Populates the citation timeline chart with yearly data.
89
+ * @returns {void}
90
+ */
91
+ export function populateCitationTimeline() {
92
+ const academicData = window.DatalogAcademic || {};
93
+ const publicationsData = window.DatalogPublications || {};
94
+ const yearlyTotals = publicationsData.yearly_totals || (academicData.citations ? academicData.citations.yearly_totals : {}) || {};
95
+
96
+ const timelineContainer = document.querySelector('[data-citation-timeline]');
97
+ if (timelineContainer && yearlyTotals) {
98
+ const listItems = timelineContainer.querySelectorAll('li[data-year]');
99
+ listItems.forEach((item) => {
100
+ const year = item.getAttribute('data-year');
101
+ if (!year) {
102
+ return;
103
+ }
104
+ const display = item.querySelector('[data-year-total]');
105
+ const total = yearlyTotals[year];
106
+ if (display && typeof total !== 'undefined') {
107
+ display.textContent = Number(total).toLocaleString();
108
+ item.style.setProperty('--timeline-value', Math.min(Number(total) / 5, 100));
109
+ }
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Attaches copy-to-clipboard handlers to elements matching the selector.
116
+ * @param {string} selector - CSS selector for trigger elements
117
+ * @param {function(HTMLElement): string|string} getText - Function to get text or static text
118
+ * @returns {void}
119
+ */
120
+ export function attachCopyHandler(selector, getText) {
121
+ document.querySelectorAll(selector).forEach((trigger) => {
122
+ trigger.addEventListener('click', () => {
123
+ const text = typeof getText === 'function' ? getText(trigger) : getText;
124
+ if (!text) {
125
+ return;
126
+ }
127
+ if (navigator.clipboard && navigator.clipboard.writeText) {
128
+ navigator.clipboard
129
+ .writeText(text)
130
+ .then(() => {
131
+ trigger.dataset.copied = 'true';
132
+ trigger.textContent = 'Copied!';
133
+ setTimeout(() => {
134
+ trigger.dataset.copied = 'false';
135
+ trigger.textContent = trigger.dataset.originalText || 'Copy';
136
+ }, 2000);
137
+ })
138
+ .catch(() => {
139
+ trigger.dataset.copied = 'error';
140
+ trigger.textContent = 'Error';
141
+ });
142
+ }
143
+ });
144
+ trigger.dataset.originalText = trigger.textContent;
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Initializes copy handlers for citation and bibliography buttons.
150
+ * @returns {void}
151
+ */
152
+ export function initCopyHandlers() {
153
+ attachCopyHandler('[data-copy-citation]', (trigger) => {
154
+ const targetId = trigger.getAttribute('data-target');
155
+ if (!targetId) {
156
+ return '';
157
+ }
158
+ const target = document.getElementById(targetId);
159
+ return target ? target.value : '';
160
+ });
161
+
162
+ attachCopyHandler('[data-copy-bibliography]', (trigger) => {
163
+ const targetId = trigger.getAttribute('data-target');
164
+ if (!targetId) {
165
+ return '';
166
+ }
167
+ const target = document.getElementById(targetId);
168
+ return target ? target.value : '';
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Initializes the submission status filter for paper tracking.
174
+ * @returns {void}
175
+ */
176
+ export function initSubmissionFilter() {
177
+ const submissionFilter = document.querySelector('[data-submission-filter]');
178
+ if (submissionFilter) {
179
+ const cards = document.querySelectorAll('.submission-card');
180
+ const filterCards = () => {
181
+ const value = submissionFilter.value;
182
+ cards.forEach((card) => {
183
+ if (!value) {
184
+ card.hidden = false;
185
+ return;
186
+ }
187
+ const status = card.getAttribute('data-submission-status');
188
+ card.hidden = status !== value;
189
+ });
190
+ };
191
+ submissionFilter.addEventListener('change', filterCards);
192
+ filterCards();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Initializes the academic calendar event type filter.
198
+ * @returns {void}
199
+ */
200
+ export function initCalendarFilter() {
201
+ const calendarFilter = document.querySelector('[data-calendar-filter]');
202
+ const calendarList = document.querySelector('[data-academic-calendar]');
203
+ if (calendarFilter && calendarList) {
204
+ const events = calendarList.querySelectorAll('li[data-event-type]');
205
+ const filterEvents = () => {
206
+ const value = calendarFilter.value;
207
+ events.forEach((event) => {
208
+ if (!value) {
209
+ event.hidden = false;
210
+ return;
211
+ }
212
+ const type = event.getAttribute('data-event-type');
213
+ event.hidden = type !== value;
214
+ });
215
+ };
216
+ calendarFilter.addEventListener('change', filterEvents);
217
+ filterEvents();
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Initializes open science badges with tooltip descriptions.
223
+ * @returns {void}
224
+ */
225
+ export function initOpenScienceBadges() {
226
+ const badges = document.querySelectorAll('.open-science-badge');
227
+ badges.forEach((badge) => {
228
+ const description = badge.querySelector('.open-science-badge__description');
229
+ const criteria = badge.querySelector('.open-science-badge__criteria');
230
+ const tooltipParts = [];
231
+ if (description) {
232
+ tooltipParts.push(description.textContent.trim());
233
+ }
234
+ if (criteria) {
235
+ tooltipParts.push(criteria.textContent.trim());
236
+ }
237
+ if (tooltipParts.length) {
238
+ badge.setAttribute('title', tooltipParts.join(' • '));
239
+ }
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Initializes all academic features on the page.
245
+ * @returns {void}
246
+ */
247
+ export function initAcademicFeatures() {
248
+ populateCitationMetrics();
249
+ populatePublicationCitations();
250
+ populateCitationTimeline();
251
+ initCopyHandlers();
252
+ initSubmissionFilter();
253
+ initCalendarFilter();
254
+ initOpenScienceBadges();
255
+ }
256
+
257
+ // Auto-initialize when module loads (backward compatibility)
258
+ if (typeof window !== 'undefined' && document.readyState !== 'loading') {
259
+ initAcademicFeatures();
260
+ } else if (typeof window !== 'undefined') {
261
+ document.addEventListener('DOMContentLoaded', initAcademicFeatures);
262
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * @fileoverview Analytics dashboard for displaying site metrics.
3
+ * Renders visitor charts, engagement stats, and search term reports.
4
+ * @module analytics-dashboard
5
+ */
6
+
7
+ /**
8
+ * Retrieves analytics data from global window object.
9
+ * @returns {Object} Analytics data object
10
+ */
11
+ function getAnalyticsData() {
12
+ return window.__DATALOG_ANALYTICS__ || {};
13
+ }
14
+
15
+ /**
16
+ * Executes callback when DOM is ready.
17
+ * @param {function} callback - Function to execute
18
+ * @returns {void}
19
+ */
20
+ export function onReady(callback) {
21
+ if (document.readyState === "loading") {
22
+ document.addEventListener("DOMContentLoaded", callback, { once: true });
23
+ } else {
24
+ callback();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Parses rows from an analytics report.
30
+ * @param {Object} report - Report object with rows array
31
+ * @returns {Array} Array of row objects
32
+ */
33
+ export function parseRows(report) {
34
+ if (!report || !Array.isArray(report.rows)) {
35
+ return [];
36
+ }
37
+ return report.rows;
38
+ }
39
+
40
+ /**
41
+ * Formats a number for display using locale settings.
42
+ * @param {number} value - Number to format
43
+ * @param {Object} [options] - Intl.NumberFormat options
44
+ * @returns {string} Formatted number string
45
+ */
46
+ export function formatNumber(value, options) {
47
+ const formatter = new Intl.NumberFormat(undefined, options || { maximumFractionDigits: 0 });
48
+ return formatter.format(value || 0);
49
+ }
50
+
51
+ /**
52
+ * Displays a placeholder message in a table container.
53
+ * @param {HTMLElement} container - Table body element
54
+ * @param {string} message - Message to display
55
+ * @param {number} [columns] - Number of columns to span
56
+ * @returns {void}
57
+ */
58
+ export function ensurePlaceholder(container, message, columns) {
59
+ if (!container) return;
60
+ const colSpan = columns || container.closest("table")?.querySelectorAll("th").length || 1;
61
+ const tr = document.createElement("tr");
62
+ tr.className = "placeholder";
63
+ const td = document.createElement("td");
64
+ td.setAttribute("colspan", colSpan);
65
+ td.textContent = message;
66
+ tr.appendChild(td);
67
+ container.replaceChildren(tr);
68
+ }
69
+
70
+ /**
71
+ * Renders the top posts table with page view and engagement data.
72
+ * @returns {void}
73
+ */
74
+ export function renderTopPosts() {
75
+ const tbody = document.getElementById("analytics-top-posts");
76
+ const rows = parseRows(getAnalyticsData().top_posts);
77
+ if (!tbody) return;
78
+
79
+ if (!rows.length) {
80
+ ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No data available", 3);
81
+ return;
82
+ }
83
+
84
+ tbody.replaceChildren();
85
+ rows.forEach((row) => {
86
+ const dimensions = row.dimensionValues || [];
87
+ const metrics = row.metricValues || [];
88
+ const path = dimensions[0]?.value || "";
89
+ const views = Number(metrics[0]?.value || 0);
90
+ const engagement = Number(metrics[1]?.value || 0);
91
+ const tr = document.createElement("tr");
92
+ const link = document.createElement("a");
93
+ link.href = path.startsWith("/") ? path : `/${path}`;
94
+ link.textContent = path || "—";
95
+ link.rel = "noopener";
96
+ link.className = "analytics-link";
97
+
98
+ const pathCell = document.createElement("td");
99
+ pathCell.appendChild(link);
100
+
101
+ const viewsCell = document.createElement("td");
102
+ viewsCell.textContent = formatNumber(views);
103
+
104
+ const engagementCell = document.createElement("td");
105
+ engagementCell.textContent = formatNumber(engagement, { maximumFractionDigits: 0 });
106
+
107
+ tr.append(pathCell, viewsCell, engagementCell);
108
+ tbody.appendChild(tr);
109
+ });
110
+ }
111
+
112
+ export function renderSearchTerms() {
113
+ const tbody = document.getElementById("analytics-search-terms");
114
+ if (!tbody) return;
115
+ const rows = parseRows(getAnalyticsData().search_terms);
116
+
117
+ if (!rows.length) {
118
+ ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No search queries recorded", 2);
119
+ return;
120
+ }
121
+
122
+ tbody.replaceChildren();
123
+ rows.forEach((row) => {
124
+ const dimensions = row.dimensionValues || [];
125
+ const metrics = row.metricValues || [];
126
+ const term = dimensions[0]?.value || "(not provided)";
127
+ const sessions = Number(metrics[0]?.value || 0);
128
+ const tr = document.createElement("tr");
129
+ const termCell = document.createElement("td");
130
+ termCell.textContent = term;
131
+ const countCell = document.createElement("td");
132
+ countCell.textContent = formatNumber(sessions);
133
+ tr.append(termCell, countCell);
134
+ tbody.appendChild(tr);
135
+ });
136
+ }
137
+
138
+ export function formatDateLabel(value) {
139
+ if (!value) return value;
140
+ if (value.includes("-")) return value;
141
+ if (value.length === 8) {
142
+ return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
143
+ }
144
+ return value;
145
+ }
146
+
147
+ export function renderVisitorChart() {
148
+ const canvas = document.getElementById("analytics-visitors-chart");
149
+ if (!canvas || typeof Chart === "undefined") return;
150
+
151
+ const rows = parseRows(getAnalyticsData().visitor_trends);
152
+ if (!rows.length) return;
153
+
154
+ const labels = rows.map((row) => formatDateLabel(row.dimensionValues?.[0]?.value));
155
+ const totalUsers = rows.map((row) => Number(row.metricValues?.[0]?.value || 0));
156
+ const newUsers = rows.map((row) => Number(row.metricValues?.[1]?.value || 0));
157
+ const sessions = rows.map((row) => Number(row.metricValues?.[2]?.value || 0));
158
+ const pageViews = rows.map((row) => Number(row.metricValues?.[3]?.value || 0));
159
+
160
+ new Chart(canvas, {
161
+ type: "line",
162
+ data: {
163
+ labels,
164
+ datasets: [
165
+ {
166
+ label: "Total users",
167
+ data: totalUsers,
168
+ borderColor: "#2563eb",
169
+ backgroundColor: "rgba(37, 99, 235, 0.15)",
170
+ tension: 0.35,
171
+ fill: true,
172
+ },
173
+ {
174
+ label: "New users",
175
+ data: newUsers,
176
+ borderColor: "#10b981",
177
+ backgroundColor: "rgba(16, 185, 129, 0.12)",
178
+ tension: 0.35,
179
+ fill: false,
180
+ },
181
+ {
182
+ label: "Sessions",
183
+ data: sessions,
184
+ borderColor: "#9333ea",
185
+ backgroundColor: "rgba(147, 51, 234, 0.1)",
186
+ tension: 0.35,
187
+ fill: false,
188
+ },
189
+ {
190
+ label: "Page views",
191
+ data: pageViews,
192
+ borderColor: "#f97316",
193
+ backgroundColor: "rgba(249, 115, 22, 0.1)",
194
+ borderDash: [6, 6],
195
+ tension: 0.35,
196
+ fill: false,
197
+ },
198
+ ],
199
+ },
200
+ options: {
201
+ responsive: true,
202
+ maintainAspectRatio: false,
203
+ interaction: { mode: "index", intersect: false },
204
+ scales: {
205
+ y: {
206
+ beginAtZero: true,
207
+ ticks: { callback: (val) => formatNumber(val) },
208
+ },
209
+ },
210
+ plugins: {
211
+ legend: { position: "top" },
212
+ },
213
+ },
214
+ });
215
+ }
216
+
217
+ export function renderEngagementChart() {
218
+ const canvas = document.getElementById("analytics-engagement-chart");
219
+ if (!canvas || typeof Chart === "undefined") return;
220
+ const rows = parseRows(getAnalyticsData().engagement_by_page).slice(0, 10);
221
+ if (!rows.length) return;
222
+
223
+ const labels = rows.map((row) => row.dimensionValues?.[0]?.value || "—");
224
+ const engagedSessions = rows.map((row) => Number(row.metricValues?.[2]?.value || 0));
225
+ const views = rows.map((row) => Number(row.metricValues?.[0]?.value || 0));
226
+
227
+ new Chart(canvas, {
228
+ type: "bar",
229
+ data: {
230
+ labels,
231
+ datasets: [
232
+ {
233
+ label: "Engaged sessions",
234
+ data: engagedSessions,
235
+ backgroundColor: "rgba(59, 130, 246, 0.7)",
236
+ },
237
+ {
238
+ label: "Page views",
239
+ data: views,
240
+ backgroundColor: "rgba(14, 165, 233, 0.5)",
241
+ },
242
+ ],
243
+ },
244
+ options: {
245
+ responsive: true,
246
+ maintainAspectRatio: false,
247
+ indexAxis: "y",
248
+ scales: {
249
+ x: { beginAtZero: true, ticks: { callback: (val) => formatNumber(val) } },
250
+ },
251
+ plugins: {
252
+ legend: { position: "top" },
253
+ },
254
+ },
255
+ });
256
+ }
257
+
258
+ export function renderKeyEvents() {
259
+ const container = document.getElementById("analytics-key-events");
260
+ if (!container) return;
261
+ const rows = parseRows(getAnalyticsData().key_events);
262
+ if (!rows.length) {
263
+ const li = document.createElement("li");
264
+ li.className = "placeholder";
265
+ li.textContent = "No tracked events in the selected window.";
266
+ container.replaceChildren(li);
267
+ return;
268
+ }
269
+
270
+ container.replaceChildren();
271
+ rows.forEach((row) => {
272
+ const name = row.dimensionValues?.[0]?.value || "event";
273
+ const count = Number(row.metricValues?.[0]?.value || 0);
274
+ const li = document.createElement("li");
275
+ const strong = document.createElement("strong");
276
+ strong.textContent = formatNumber(count);
277
+ li.appendChild(strong);
278
+ li.appendChild(document.createTextNode(` ${name.replace(/_/g, " ")}`));
279
+ container.appendChild(li);
280
+ });
281
+ }
282
+
283
+ export function renderScholar() {
284
+ const metrics = getAnalyticsData().scholar || {};
285
+ const totalField = document.querySelector('[data-field="scholar-total"]');
286
+ const hIndexField = document.querySelector('[data-field="scholar-h-index"]');
287
+ const i10Field = document.querySelector('[data-field="scholar-i10-index"]');
288
+ if (totalField) totalField.textContent = metrics.total ? formatNumber(Number(metrics.total)) : "—";
289
+ if (hIndexField) hIndexField.textContent = metrics.h_index ? formatNumber(Number(metrics.h_index)) : "—";
290
+ if (i10Field) i10Field.textContent = metrics.i10_index ? formatNumber(Number(metrics.i10_index)) : "—";
291
+
292
+ const canvas = document.getElementById("analytics-scholar-chart");
293
+ if (!canvas || typeof Chart === "undefined") return;
294
+ const yearly = metrics.yearly_totals || {};
295
+ const entries = Object.entries(yearly)
296
+ .map(([year, value]) => [Number(year), Number(value)])
297
+ .sort((a, b) => a[0] - b[0]);
298
+ if (!entries.length) return;
299
+
300
+ new Chart(canvas, {
301
+ type: "bar",
302
+ data: {
303
+ labels: entries.map((item) => item[0]),
304
+ datasets: [
305
+ {
306
+ label: "Citations",
307
+ data: entries.map((item) => item[1]),
308
+ backgroundColor: "rgba(99, 102, 241, 0.8)",
309
+ },
310
+ ],
311
+ },
312
+ options: {
313
+ responsive: true,
314
+ maintainAspectRatio: false,
315
+ scales: {
316
+ y: { beginAtZero: true, ticks: { callback: (val) => formatNumber(val) } },
317
+ },
318
+ },
319
+ });
320
+ }
321
+
322
+ export function renderMonthlyReports() {
323
+ const tbody = document.getElementById("analytics-monthly-reports");
324
+ if (!tbody) return;
325
+ const reports = Array.isArray(getAnalyticsData().monthly_reports) ? getAnalyticsData().monthly_reports : [];
326
+ if (!reports.length) {
327
+ ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No monthly reports available", 5);
328
+ return;
329
+ }
330
+
331
+ tbody.replaceChildren();
332
+ reports.forEach((report) => {
333
+ const tr = document.createElement("tr");
334
+ const month = document.createElement("td");
335
+ month.textContent = report.month || "—";
336
+ const total = document.createElement("td");
337
+ total.textContent = formatNumber(report.total_users || 0);
338
+ const news = document.createElement("td");
339
+ news.textContent = formatNumber(report.new_users || 0);
340
+ const sessions = document.createElement("td");
341
+ sessions.textContent = formatNumber(report.sessions || 0);
342
+ const views = document.createElement("td");
343
+ views.textContent = formatNumber(report.page_views || 0);
344
+ tr.append(month, total, news, sessions, views);
345
+ tbody.appendChild(tr);
346
+ });
347
+ }
348
+
349
+ export function render() {
350
+ const status = getAnalyticsData().status;
351
+ if (status !== "ok") {
352
+ ["analytics-top-posts", "analytics-search-terms", "analytics-monthly-reports"].forEach((id) => {
353
+ const el = document.getElementById(id);
354
+ if (el) {
355
+ ensurePlaceholder(el, getAnalyticsData().message || "Analytics data unavailable.");
356
+ }
357
+ });
358
+ const events = document.getElementById("analytics-key-events");
359
+ if (events) {
360
+ const li = document.createElement("li");
361
+ li.className = "placeholder";
362
+ li.textContent = getAnalyticsData().message || "Analytics data unavailable.";
363
+ events.replaceChildren(li);
364
+ }
365
+ renderScholar();
366
+ return;
367
+ }
368
+
369
+ renderTopPosts();
370
+ renderSearchTerms();
371
+ renderVisitorChart();
372
+ renderEngagementChart();
373
+ renderKeyEvents();
374
+ renderScholar();
375
+ renderMonthlyReports();
376
+ }
377
+
378
+ // Auto-initialize when module loads (backward compatibility)
379
+ // Skip auto-init in test environment
380
+ if (typeof window !== 'undefined' && !window.__VITEST__) {
381
+ onReady(render);
382
+ }