stats.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. const Stats = (() => {
  2. const ALL_TYPES = ['hiking', 'running', 'cycling', 'driving', 'train', 'other', 'none'];
  3. const TYPE_LABELS = {
  4. hiking: '🥾 Hiking', running: '🏃 Running', cycling: '🚴 Cycling',
  5. driving: '🚗 Driving', train: '🚆 Train', other: '📍 Other', none: '— Untyped'
  6. };
  7. // null = all; otherwise Set of selected type strings
  8. let selectedTypes = null;
  9. async function init() {
  10. await loadStats();
  11. }
  12. async function loadStats() {
  13. const content = document.getElementById('stats-content');
  14. content.innerHTML = '<div class="loading">Loading stats...</div>';
  15. try {
  16. const types = selectedTypes ? [...selectedTypes] : [];
  17. const stats = await API.getStats(types);
  18. renderStats(stats);
  19. } catch (e) {
  20. content.innerHTML = '<div class="error-msg">Error loading stats: ' + escHtml(e.message) + '</div>';
  21. }
  22. }
  23. function renderFilterBar() {
  24. const allSelected = selectedTypes === null;
  25. let html = '<div class="stats-filter">';
  26. html += `<button class="stats-type-btn${allSelected ? ' active' : ''}" data-type="all">All</button>`;
  27. for (const t of ALL_TYPES) {
  28. const active = !allSelected && selectedTypes.has(t);
  29. html += `<button class="stats-type-btn${active ? ' active' : ''}" data-type="${escAttr(t)}">${TYPE_LABELS[t]}</button>`;
  30. }
  31. html += '</div>';
  32. return html;
  33. }
  34. function renderStats(stats) {
  35. const content = document.getElementById('stats-content');
  36. let html = renderFilterBar();
  37. // Summary totals
  38. if (stats.total !== undefined || stats.totalDistance !== undefined) {
  39. const totalDist = stats.total?.distance || stats.totalDistance || 0;
  40. const totalTracks = stats.total?.count || stats.trackCount || 0;
  41. html += `<div class="stats-section">
  42. <h3>Total</h3>
  43. <div class="stat-row">
  44. <span class="stat-label">Tracks</span>
  45. <div class="stat-bar-wrap"><div class="stat-bar" style="width:100%"></div></div>
  46. <span class="stat-value">${totalTracks}</span>
  47. </div>
  48. <div class="stat-row">
  49. <span class="stat-label">Distance</span>
  50. <div class="stat-bar-wrap"><div class="stat-bar" style="width:100%"></div></div>
  51. <span class="stat-value">${formatDistance(totalDist)}</span>
  52. </div>
  53. </div>`;
  54. }
  55. // By year
  56. if (stats.byYear && stats.byYear.length > 0) {
  57. const maxDist = Math.max(...stats.byYear.map(s => s.distance || 0));
  58. html += '<div class="stats-section"><h3>By Year</h3>';
  59. for (const s of stats.byYear) {
  60. const pct = maxDist > 0 ? ((s.distance || 0) / maxDist * 100) : 0;
  61. html += `<div class="stat-row">
  62. <span class="stat-label">${s.year}</span>
  63. <div class="stat-bar-wrap"><div class="stat-bar" style="width:${pct.toFixed(1)}%"></div></div>
  64. <span class="stat-value">${formatDistance(s.distance)}</span>
  65. </div>`;
  66. }
  67. html += '</div>';
  68. }
  69. // By month (last 24 months)
  70. if (stats.byMonth && stats.byMonth.length > 0) {
  71. const recent = stats.byMonth.slice(-24);
  72. const maxDist = Math.max(...recent.map(s => s.distance || 0));
  73. html += '<div class="stats-section"><h3>By Month (last 24)</h3>';
  74. for (const s of recent) {
  75. const pct = maxDist > 0 ? ((s.distance || 0) / maxDist * 100) : 0;
  76. const label = `${s.year}-${String(s.month).padStart(2, '0')}`;
  77. html += `<div class="stat-row">
  78. <span class="stat-label">${label}</span>
  79. <div class="stat-bar-wrap"><div class="stat-bar" style="width:${pct.toFixed(1)}%"></div></div>
  80. <span class="stat-value">${formatDistance(s.distance)}</span>
  81. </div>`;
  82. }
  83. html += '</div>';
  84. }
  85. // By week (last 52 weeks)
  86. if (stats.byWeek && stats.byWeek.length > 0) {
  87. const recent = stats.byWeek.slice(-52);
  88. const maxDist = Math.max(...recent.map(s => s.distance || 0));
  89. html += '<div class="stats-section"><h3>By Week (last 52)</h3>';
  90. for (const s of recent) {
  91. const pct = maxDist > 0 ? ((s.distance || 0) / maxDist * 100) : 0;
  92. const label = `${s.year}-W${String(s.week).padStart(2, '0')}`;
  93. html += `<div class="stat-row">
  94. <span class="stat-label">${label}</span>
  95. <div class="stat-bar-wrap"><div class="stat-bar" style="width:${pct.toFixed(1)}%"></div></div>
  96. <span class="stat-value">${formatDistance(s.distance)}</span>
  97. </div>`;
  98. }
  99. html += '</div>';
  100. }
  101. if (!html.includes('stats-section')) {
  102. html += '<div class="empty-list">No tracks match the current filter.</div>';
  103. }
  104. content.innerHTML = html;
  105. // Bind filter button events
  106. content.querySelectorAll('.stats-type-btn').forEach(btn => {
  107. btn.addEventListener('click', () => {
  108. const t = btn.dataset.type;
  109. if (t === 'all') {
  110. selectedTypes = null;
  111. } else {
  112. if (selectedTypes === null) {
  113. selectedTypes = new Set([t]);
  114. } else if (selectedTypes.has(t)) {
  115. selectedTypes.delete(t);
  116. if (selectedTypes.size === 0) selectedTypes = null;
  117. } else {
  118. selectedTypes.add(t);
  119. }
  120. }
  121. loadStats();
  122. });
  123. });
  124. }
  125. return { init, loadStats };
  126. })();