// ==UserScript== // @name parkrun Annual Summary // @description Adds an annual participation summary (totals, averages, min/max) to parkrun event history pages // @author Pete Johns (@johnsyweb) // @downloadURL https://raw.githubusercontent.com/johnsyweb/tampermonkey-parkrun/refs/heads/main/parkrun-annual-summary.user.js // @grant GM_xmlhttpRequest // @homepage https://www.johnsy.com/tampermonkey-parkrun/ // @icon https://www.google.com/s2/favicons?sz=64&domain=parkrun.com.au // @license MIT // @match *://www.parkrun.ca/*/results/eventhistory/* // @match *://www.parkrun.co.at/*/results/eventhistory/* // @match *://www.parkrun.co.nl/*/results/eventhistory/* // @match *://www.parkrun.co.nz/*/results/eventhistory/* // @match *://www.parkrun.co.za/*/results/eventhistory/* // @match *://www.parkrun.com.au/*/results/eventhistory/* // @match *://www.parkrun.com.de/*/results/eventhistory/* // @match *://www.parkrun.dk/*/results/eventhistory/* // @match *://www.parkrun.fi/*/results/eventhistory/* // @match *://www.parkrun.fr/*/results/eventhistory/* // @match *://www.parkrun.ie/*/results/eventhistory/* // @match *://www.parkrun.it/*/results/eventhistory/* // @match *://www.parkrun.jp/*/results/eventhistory/* // @match *://www.parkrun.lt/*/results/eventhistory/* // @match *://www.parkrun.my/*/results/eventhistory/* // @match *://www.parkrun.no/*/results/eventhistory/* // @match *://www.parkrun.org.uk/*/results/eventhistory/* // @match *://www.parkrun.pl/*/results/eventhistory/* // @match *://www.parkrun.se/*/results/eventhistory/* // @match *://www.parkrun.sg/*/results/eventhistory/* // @match *://www.parkrun.us/*/results/eventhistory/* // @namespace http://tampermonkey.net/ // @run-at document-end // @supportURL https://github.com/johnsyweb/tampermonkey-parkrun/issues/ // @tag parkrun // @updateURL https://raw.githubusercontent.com/johnsyweb/tampermonkey-parkrun/refs/heads/main/parkrun-annual-summary.user.js // @require https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js // @version 0.2.9 // ==/UserScript== // DO NOT EDIT - generated from src/ by scripts/build-scripts.js function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); } function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); } function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; } (function () { 'use strict'; var STYLES = { backgroundColor: '#1c1b2a', barColor: '#f59e0b', // amber 500 lineColor: '#22d3ee', // cyan 400 textColor: '#f3f4f6', subtleTextColor: '#d1d5db', gridColor: 'rgba(243, 244, 246, 0.18)' }; var COMPARISON_COLORS = ['#f59e0b', // amber '#22d3ee', // cyan '#f97316', // orange '#10b981', // emerald '#a855f7', // purple '#ef4444', // red '#3b82f6', // blue '#84cc16' // lime ]; // Global state for comparison var state = { currentEvent: null, comparisonEvents: [], allParkruns: null }; function insertAfterFirst(selector, element) { var pageTitle = document.querySelector(selector); if (pageTitle && pageTitle.parentNode) { if (pageTitle.nextSibling) { pageTitle.parentNode.insertBefore(element, pageTitle.nextSibling); } else { pageTitle.parentNode.appendChild(element); } } } function fetchAllParkruns() { return _fetchAllParkruns.apply(this, arguments); } function _fetchAllParkruns() { _fetchAllParkruns = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { var CACHE_KEY, CACHE_DURATION_MS, _data$events, cached, _JSON$parse, _data, timestamp, age, response, data, features, _t3, _t4; return _regenerator().w(function (_context4) { while (1) switch (_context4.p = _context4.n) { case 0: CACHE_KEY = 'parkrun_events_cache'; CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours _context4.p = 1; // Check for cached data cached = localStorage.getItem(CACHE_KEY); if (!cached) { _context4.n = 5; break; } _context4.p = 2; _JSON$parse = JSON.parse(cached), _data = _JSON$parse.data, timestamp = _JSON$parse.timestamp; age = Date.now() - timestamp; if (!(age < CACHE_DURATION_MS)) { _context4.n = 3; break; } console.log("Using cached parkrun events (".concat(Math.round(age / 1000 / 60), " minutes old)")); return _context4.a(2, _data); case 3: _context4.n = 5; break; case 4: _context4.p = 4; _t3 = _context4.v; console.log('Cache parse error, fetching fresh data', _t3); case 5: console.log('Fetching parkrun events from https://images.parkrun.com/events.json'); _context4.n = 6; return fetch('https://images.parkrun.com/events.json'); case 6: response = _context4.v; console.log('Fetch response status:', response.status, response.statusText); if (response.ok) { _context4.n = 7; break; } console.error('Fetch failed with status:', response.status); return _context4.a(2, []); case 7: _context4.n = 8; return response.json(); case 8: data = _context4.v; // The events.json structure has events under data.events.features features = ((_data$events = data.events) === null || _data$events === void 0 ? void 0 : _data$events.features) || data.features || []; console.log('Features array length:', features.length); if (!(!features || features.length === 0)) { _context4.n = 9; break; } console.error('No features found in response data'); return _context4.a(2, []); case 9: // Cache the features with timestamp try { localStorage.setItem(CACHE_KEY, JSON.stringify({ data: features, timestamp: Date.now() })); console.log('Cached', features.length, 'parkrun events for 24 hours'); } catch (cacheError) { console.warn('Failed to cache parkrun events:', cacheError); } console.log('Successfully loaded', features.length, 'parkrun events'); return _context4.a(2, features); case 10: _context4.p = 10; _t4 = _context4.v; console.error('Failed to fetch parkruns:', _t4); console.error('Error details:', _t4.message, _t4.stack); return _context4.a(2, []); } }, _callee4, null, [[2, 4], [1, 10]]); })); return _fetchAllParkruns.apply(this, arguments); } function getCurrentEventInfo() { var pathParts = window.location.pathname.split('/'); var eventName = pathParts[1]; var domain = window.location.hostname; return { eventName: eventName, domain: domain, url: window.location.origin }; } function calculateDistance(lat1, lon1, lat2, lon2) { var R = 6371; // Earth's radius in km var dLat = (lat2 - lat1) * Math.PI / 180; var dLon = (lon2 - lon1) * Math.PI / 180; var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function findNearbyParkruns(currentEvent, allParkruns) { var maxDistanceKm = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 50; var current = allParkruns.find(function (p) { return p.properties.eventname === currentEvent.eventName; }); if (!current) return []; var _current$geometry$coo = _slicedToArray(current.geometry.coordinates, 2), currentLon = _current$geometry$coo[0], currentLat = _current$geometry$coo[1]; var currentCountry = current.properties.countrycode; var currentSeries = current.properties.seriesid; return allParkruns.filter(function (parkrun) { if (parkrun.properties.eventname === currentEvent.eventName) return false; if (parkrun.properties.countrycode !== currentCountry) return false; if (parkrun.properties.seriesid !== currentSeries) return false; var _parkrun$geometry$coo = _slicedToArray(parkrun.geometry.coordinates, 2), lon = _parkrun$geometry$coo[0], lat = _parkrun$geometry$coo[1]; var latDiff = Math.abs(lat - currentLat); var lonDiff = Math.abs(lon - currentLon); // Quick bounding box filter (~0.5 degrees ≈ 55km) if (latDiff > 0.5 || lonDiff > 0.5) return false; var distance = calculateDistance(currentLat, currentLon, lat, lon); return distance <= maxDistanceKm; }).map(function (parkrun) { var _parkrun$geometry$coo2 = _slicedToArray(parkrun.geometry.coordinates, 2), lon = _parkrun$geometry$coo2[0], lat = _parkrun$geometry$coo2[1]; var distance = calculateDistance(currentLat, currentLon, lat, lon); return _objectSpread(_objectSpread({}, parkrun), {}, { distance: distance }); }).sort(function (a, b) { return a.distance - b.distance; }); } function buildHtmlReport(_x, _x2) { return _buildHtmlReport.apply(this, arguments); } function _buildHtmlReport() { _buildHtmlReport = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(mainContainer, meta) { var clone, originalCanvases, clonedCanvases, stylesheet, header; return _regenerator().w(function (_context5) { while (1) switch (_context5.n) { case 0: clone = mainContainer.cloneNode(true); originalCanvases = mainContainer.querySelectorAll('canvas'); clonedCanvases = clone.querySelectorAll('canvas'); originalCanvases.forEach(function (canvas, idx) { try { var dataUrl = canvas.toDataURL('image/png'); var img = document.createElement('img'); img.src = dataUrl; img.alt = 'Chart snapshot'; img.style.maxWidth = '100%'; img.style.display = 'block'; img.style.backgroundColor = '#2b223d'; if (clonedCanvases[idx]) { clonedCanvases[idx].replaceWith(img); } } catch (error) { console.error('Failed to serialize chart canvas:', error); } }); stylesheet = "\n :root { color-scheme: dark; }\n body { margin: 0; padding: 20px; background: ".concat(STYLES.backgroundColor, "; color: ").concat(STYLES.textColor, "; font-family: \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif; line-height: 1.5; }\n a { color: ").concat(STYLES.lineColor, "; }\n h1, h2, h3, h4 { color: ").concat(STYLES.barColor, "; margin: 0 0 10px 0; }\n table { width: 100%; border-collapse: collapse; }\n th, td { border: 1px solid ").concat(STYLES.gridColor, "; padding: 10px; text-align: left; }\n th { background: #2b223d; color: ").concat(STYLES.barColor, "; }\n tr:nth-child(even) td { background: #241c35; }\n tr:nth-child(odd) td { background: #1f182e; }\n .parkrun-annual-summary { background: ").concat(STYLES.backgroundColor, "; padding: 16px; border-radius: 6px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); }\n .chart-img { max-width: 100%; display: block; }\n .meta { margin-bottom: 16px; color: ").concat(STYLES.subtleTextColor, "; font-size: 13px; }\n .meta strong { color: ").concat(STYLES.textColor, "; }\n "); header = "\n
\n

parkrun Annual Summary

\n
\n
Event: ".concat(meta.eventShortName, "
\n
Generated: ").concat(meta.generatedAt, "
\n
\n
\n "); return _context5.a(2, "parkrun Annual Summary - ".concat(meta.eventShortName, "").concat(header).concat(clone.outerHTML, "")); } }, _callee5); })); return _buildHtmlReport.apply(this, arguments); } function generateReportBlob(_x3, _x4) { return _generateReportBlob.apply(this, arguments); } function _generateReportBlob() { _generateReportBlob = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(mainContainer, meta) { var html, filename; return _regenerator().w(function (_context6) { while (1) switch (_context6.n) { case 0: _context6.n = 1; return buildHtmlReport(mainContainer, meta); case 1: html = _context6.v; filename = "parkrun-annual-summary-".concat(meta.eventShortName, "-").concat(meta.generatedAtISO, ".html"); return _context6.a(2, { blob: new Blob([html], { type: 'text/html' }), filename: filename }); } }, _callee6); })); return _generateReportBlob.apply(this, arguments); } function fetchEventHistory(_x5, _x6) { return _fetchEventHistory.apply(this, arguments); } function _fetchEventHistory() { _fetchEventHistory = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(eventName, domain) { var _doc$querySelector$te, _doc$querySelector, url, response, html, parser, doc, title, eventNumbers, dates, finishers, volunteers, rows, _t5; return _regenerator().w(function (_context7) { while (1) switch (_context7.p = _context7.n) { case 0: _context7.p = 0; url = "".concat(domain, "/").concat(eventName, "/results/eventhistory/"); _context7.n = 1; return fetch(url); case 1: response = _context7.v; _context7.n = 2; return response.text(); case 2: html = _context7.v; parser = new DOMParser(); doc = parser.parseFromString(html, 'text/html'); title = (_doc$querySelector$te = (_doc$querySelector = doc.querySelector('h1')) === null || _doc$querySelector === void 0 ? void 0 : _doc$querySelector.textContent.trim()) !== null && _doc$querySelector$te !== void 0 ? _doc$querySelector$te : "".concat(eventName, " Event History"); eventNumbers = []; dates = []; finishers = []; volunteers = []; rows = doc.querySelectorAll('tr.Results-table-row'); Array.from(rows).reverse().forEach(function (row) { var eventNumber = row.getAttribute('data-parkrun'); if (eventNumber) { eventNumbers.push(eventNumber); } var date = row.getAttribute('data-date'); if (date) { var dateObj = new Date(date); var formattedDate = dateObj.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); dates.push(formattedDate); } var finishersCount = row.getAttribute('data-finishers'); if (finishersCount) { finishers.push(parseInt(finishersCount, 10)); } var volunteersCount = row.getAttribute('data-volunteers'); if (volunteersCount) { volunteers.push(parseInt(volunteersCount, 10)); } }); return _context7.a(2, { eventName: eventName, title: title, eventNumbers: eventNumbers, dates: dates, finishers: finishers, volunteers: volunteers }); case 3: _context7.p = 3; _t5 = _context7.v; console.error("Failed to fetch event history for ".concat(eventName, ":"), _t5); return _context7.a(2, null); } }, _callee7, null, [[0, 3]]); })); return _fetchEventHistory.apply(this, arguments); } function extractEventHistoryData() { var _document$querySelect, _document$querySelect2; var title = (_document$querySelect = (_document$querySelect2 = document.querySelector('h1')) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.textContent.trim()) !== null && _document$querySelect !== void 0 ? _document$querySelect : 'Event History'; var eventNumbers = []; var dates = []; var finishers = []; var volunteers = []; var rows = document.querySelectorAll('tr.Results-table-row'); Array.from(rows).reverse().forEach(function (row) { var eventNumber = row.getAttribute('data-parkrun'); if (eventNumber) { eventNumbers.push(eventNumber); } var date = row.getAttribute('data-date'); if (date) { var dateObj = new Date(date); var formattedDate = dateObj.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); dates.push(formattedDate); } var finishersCount = row.getAttribute('data-finishers'); if (finishersCount) { finishers.push(parseInt(finishersCount, 10)); } var volunteersCount = row.getAttribute('data-volunteers'); if (volunteersCount) { volunteers.push(parseInt(volunteersCount, 10)); } }); return { title: title, eventNumbers: eventNumbers, dates: dates, finishers: finishers, volunteers: volunteers }; } function aggregateByYear(historyData) { var yearly = {}; historyData.dates.forEach(function (dateStr, index) { var _historyData$finisher, _historyData$voluntee; var date = new Date(dateStr); var year = date.getFullYear(); var finishers = (_historyData$finisher = historyData.finishers[index]) !== null && _historyData$finisher !== void 0 ? _historyData$finisher : 0; var volunteers = (_historyData$voluntee = historyData.volunteers[index]) !== null && _historyData$voluntee !== void 0 ? _historyData$voluntee : 0; var eventNumber = historyData.eventNumbers[index]; if (!yearly[year]) { yearly[year] = { year: year, eventCount: 0, totalFinishers: 0, totalVolunteers: 0, minFinishers: null, maxFinishers: null, minVolunteers: null, maxVolunteers: null }; } var current = yearly[year]; current.eventCount++; current.totalFinishers += finishers; current.totalVolunteers += volunteers; if (current.minFinishers === null || finishers < current.minFinishers.value) { current.minFinishers = { value: finishers, date: historyData.dates[index], eventNumber: eventNumber }; } if (current.maxFinishers === null || finishers > current.maxFinishers.value) { current.maxFinishers = { value: finishers, date: historyData.dates[index], eventNumber: eventNumber }; } if (current.minVolunteers === null || volunteers < current.minVolunteers.value) { current.minVolunteers = { value: volunteers, date: historyData.dates[index], eventNumber: eventNumber }; } if (current.maxVolunteers === null || volunteers > current.maxVolunteers.value) { current.maxVolunteers = { value: volunteers, date: historyData.dates[index], eventNumber: eventNumber }; } }); return Object.keys(yearly).map(Number).sort(function (a, b) { return a - b; }).map(function (year) { var data = yearly[year]; return { year: year, eventCount: data.eventCount, totalFinishers: data.totalFinishers, totalVolunteers: data.totalVolunteers, avgFinishers: Math.round(data.totalFinishers / data.eventCount), avgVolunteers: Math.round(data.totalVolunteers / data.eventCount), minFinishers: data.minFinishers, maxFinishers: data.maxFinishers, minVolunteers: data.minVolunteers, maxVolunteers: data.maxVolunteers, finishersGrowth: null, volunteersGrowth: null }; }).map(function (row, index, arr) { if (index === 0) { return row; } var prev = arr[index - 1]; var finishersGrowth = prev.avgFinishers ? (row.avgFinishers - prev.avgFinishers) / prev.avgFinishers * 100 : null; var volunteersGrowth = prev.avgVolunteers ? (row.avgVolunteers - prev.avgVolunteers) / prev.avgVolunteers * 100 : null; return _objectSpread(_objectSpread({}, row), {}, { finishersGrowth: finishersGrowth, volunteersGrowth: volunteersGrowth }); }); } function formatExtrema(record) { if (!record) return '-'; return record.value.toLocaleString(); } function formatGrowth(growth) { if (growth === null || Number.isNaN(growth)) return '-'; var sign = growth > 0 ? '+' : ''; var color = growth > 0 ? '#53BA9D' : growth < 0 ? '#ff6b6b' : STYLES.subtleTextColor; return "").concat(sign).concat(growth.toFixed(1), "%"); } function createComparisonSelector(nearbyParkruns) { var selectorContainer = document.createElement('div'); selectorContainer.style.marginBottom = '15px'; selectorContainer.style.padding = '10px'; selectorContainer.style.backgroundColor = STYLES.backgroundColor; selectorContainer.style.borderRadius = '6px'; selectorContainer.style.display = 'flex'; selectorContainer.style.alignItems = 'center'; selectorContainer.style.gap = '10px'; selectorContainer.style.flexWrap = 'wrap'; var label = document.createElement('span'); label.textContent = 'Compare with:'; label.style.color = STYLES.textColor; label.style.fontWeight = 'bold'; selectorContainer.appendChild(label); var select = document.createElement('select'); select.style.padding = '6px 12px'; select.style.backgroundColor = '#3a3250'; select.style.color = STYLES.textColor; select.style.border = "1px solid ".concat(STYLES.gridColor); select.style.borderRadius = '4px'; select.style.cursor = 'pointer'; var defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- Select parkrun --'; select.appendChild(defaultOption); nearbyParkruns.forEach(function (parkrun) { var option = document.createElement('option'); option.value = parkrun.properties.eventname; option.textContent = "".concat(parkrun.properties.EventShortName, " (").concat(parkrun.distance.toFixed(1), "km)"); select.appendChild(option); }); var addButton = document.createElement('button'); addButton.textContent = '+ Add'; addButton.style.padding = '6px 12px'; addButton.style.backgroundColor = STYLES.lineColor; addButton.style.color = '#2b223d'; addButton.style.border = 'none'; addButton.style.borderRadius = '4px'; addButton.style.cursor = 'pointer'; addButton.style.fontWeight = 'bold'; addButton.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var selectedEventName, eventInfo, historyData, parkrunInfo; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: selectedEventName = select.value; if (selectedEventName) { _context.n = 1; break; } return _context.a(2); case 1: if (!state.comparisonEvents.some(function (event) { return event.eventName === selectedEventName; })) { _context.n = 2; break; } alert('This parkrun is already selected for comparison'); return _context.a(2); case 2: addButton.disabled = true; addButton.textContent = 'Loading...'; eventInfo = getCurrentEventInfo(); _context.n = 3; return fetchEventHistory(selectedEventName, eventInfo.url); case 3: historyData = _context.v; if (historyData) { parkrunInfo = nearbyParkruns.find(function (p) { return p.properties.eventname === selectedEventName; }); state.comparisonEvents.push(_objectSpread(_objectSpread({}, historyData), {}, { distance: parkrunInfo === null || parkrunInfo === void 0 ? void 0 : parkrunInfo.distance })); renderAllSummaries(); } else { alert('Failed to fetch event history'); } addButton.disabled = false; addButton.textContent = '+ Add'; select.value = ''; case 4: return _context.a(2); } }, _callee); }))); selectorContainer.appendChild(select); selectorContainer.appendChild(addButton); return selectorContainer; } function createSelectedEventsDisplay() { var container = document.createElement('div'); container.id = 'selectedEventsDisplay'; container.style.display = 'flex'; container.style.gap = '8px'; container.style.flexWrap = 'wrap'; container.style.marginTop = '10px'; var updateDisplay = function updateDisplay() { container.innerHTML = ''; [state.currentEvent].concat(_toConsumableArray(state.comparisonEvents)).forEach(function (event, index) { var chip = document.createElement('div'); chip.style.display = 'inline-flex'; chip.style.alignItems = 'center'; chip.style.gap = '6px'; chip.style.padding = '4px 10px'; chip.style.backgroundColor = COMPARISON_COLORS[index % COMPARISON_COLORS.length]; chip.style.color = '#2b223d'; chip.style.borderRadius = '12px'; chip.style.fontSize = '12px'; chip.style.fontWeight = 'bold'; var label = document.createElement('span'); label.textContent = event.title || event.eventName; chip.appendChild(label); if (index > 0) { var removeBtn = document.createElement('span'); removeBtn.textContent = '×'; removeBtn.style.cursor = 'pointer'; removeBtn.style.marginLeft = '4px'; removeBtn.style.fontSize = '16px'; removeBtn.addEventListener('click', function () { state.comparisonEvents.splice(index - 1, 1); renderAllSummaries(); }); chip.appendChild(removeBtn); } container.appendChild(chip); }); }; updateDisplay(); return { container: container, updateDisplay: updateDisplay }; } function createTabsForEvents(events) { var tabsContainer = document.createElement('div'); tabsContainer.id = 'eventTabs'; tabsContainer.style.marginTop = '15px'; var tabButtons = document.createElement('div'); tabButtons.style.display = 'flex'; tabButtons.style.gap = '5px'; tabButtons.style.borderBottom = "2px solid ".concat(STYLES.gridColor); tabButtons.style.marginBottom = '15px'; var tabContents = document.createElement('div'); tabContents.id = 'tabContents'; events.forEach(function (event, index) { var button = document.createElement('button'); button.textContent = event.title || event.eventName; button.style.padding = '10px 20px'; button.style.backgroundColor = index === 0 ? COMPARISON_COLORS[index % COMPARISON_COLORS.length] : '#3a3250'; button.style.color = index === 0 ? '#2b223d' : STYLES.textColor; button.style.border = 'none'; button.style.borderRadius = '6px 6px 0 0'; button.style.cursor = 'pointer'; button.style.fontWeight = 'bold'; button.dataset.index = index; button.addEventListener('click', function () { // Update button styles tabButtons.querySelectorAll('button').forEach(function (btn, btnIndex) { btn.style.backgroundColor = btnIndex === index ? COMPARISON_COLORS[btnIndex % COMPARISON_COLORS.length] : '#3a3250'; btn.style.color = btnIndex === index ? '#2b223d' : STYLES.textColor; }); // Show corresponding tab content tabContents.querySelectorAll('.tab-content').forEach(function (content, contentIndex) { content.style.display = contentIndex === index ? 'block' : 'none'; }); }); tabButtons.appendChild(button); }); tabsContainer.appendChild(tabButtons); tabsContainer.appendChild(tabContents); return { tabsContainer: tabsContainer, tabContents: tabContents }; } function renderEventTab(historyData, eventIndex) { var yearly = aggregateByYear(historyData); if (yearly.length === 0) { return null; } var tabContent = document.createElement('div'); tabContent.className = 'tab-content'; tabContent.style.display = eventIndex === 0 ? 'block' : 'none'; tabContent.style.backgroundColor = STYLES.backgroundColor; tabContent.style.padding = '15px'; tabContent.style.borderRadius = '6px'; var tableWrap = document.createElement('div'); tableWrap.style.overflowX = 'auto'; var table = document.createElement('table'); table.className = 'annualSummaryTable'; table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.fontSize = '14px'; table.style.color = STYLES.textColor; table.style.backgroundColor = STYLES.backgroundColor; var sortState = { key: 'year', dir: 'asc' }; var columns = [{ key: 'year', label: 'Year', align: 'left' }, { key: 'eventCount', label: 'Events', align: 'center' }, { key: 'totalFinishers', label: 'Finishers Total', align: 'right', color: STYLES.barColor }, { key: 'minFinishers', label: 'Finishers Min', align: 'right', color: STYLES.barColor }, { key: 'maxFinishers', label: 'Finishers Max', align: 'right', color: STYLES.barColor }, { key: 'avgFinishers', label: 'Finishers Avg', align: 'right', color: STYLES.barColor }, { key: 'finishersGrowth', label: 'Finishers YoY', align: 'right' }, { key: 'totalVolunteers', label: 'Volunteers Total', align: 'right', color: STYLES.lineColor }, { key: 'minVolunteers', label: 'Volunteers Min', align: 'right', color: STYLES.lineColor }, { key: 'maxVolunteers', label: 'Volunteers Max', align: 'right', color: STYLES.lineColor }, { key: 'avgVolunteers', label: 'Volunteers Avg', align: 'right', color: STYLES.lineColor }, { key: 'volunteersGrowth', label: 'Volunteers YoY', align: 'right' }]; var thead = document.createElement('thead'); var headerRow = document.createElement('tr'); headerRow.style.borderBottom = "2px solid ".concat(STYLES.gridColor); columns.forEach(function (col) { var th = document.createElement('th'); th.textContent = col.label; th.style.padding = '10px'; th.style.textAlign = col.align; th.style.cursor = 'pointer'; if (col.color) th.style.color = col.color; th.addEventListener('click', function () { if (sortState.key === col.key) { sortState.dir = sortState.dir === 'asc' ? 'desc' : 'asc'; } else { sortState.key = col.key; sortState.dir = 'desc'; } renderBody(); }); headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); var tbody = document.createElement('tbody'); table.appendChild(tbody); tableWrap.appendChild(table); tabContent.appendChild(tableWrap); function renderBody() { tbody.innerHTML = ''; var sorted = _toConsumableArray(yearly).sort(function (a, b) { var key = sortState.key; var dir = sortState.dir === 'asc' ? 1 : -1; var getVal = function getVal(row) { var val = row[key]; if (val === null || val === undefined) return -Infinity; if (_typeof(val) === 'object' && val.value !== undefined) return val.value; return val; }; var av = getVal(a); var bv = getVal(b); if (av === bv) return 0; return av > bv ? dir : -dir; }); sorted.forEach(function (rowData) { var row = document.createElement('tr'); row.style.borderBottom = "1px solid ".concat(STYLES.gridColor); row.innerHTML = "\n ".concat(rowData.year, "\n ").concat(rowData.eventCount, "\n ").concat(rowData.totalFinishers.toLocaleString(), "\n ").concat(formatExtrema(rowData.minFinishers), "\n ").concat(formatExtrema(rowData.maxFinishers), "\n ").concat(rowData.avgFinishers, "\n ").concat(formatGrowth(rowData.finishersGrowth), "\n ").concat(rowData.totalVolunteers.toLocaleString(), "\n ").concat(formatExtrema(rowData.minVolunteers), "\n ").concat(formatExtrema(rowData.maxVolunteers), "\n ").concat(rowData.avgVolunteers, "\n ").concat(formatGrowth(rowData.volunteersGrowth), "\n "); tbody.appendChild(row); }); } renderBody(); // Add charts var chartsRow = document.createElement('div'); chartsRow.style.display = 'grid'; chartsRow.style.gridTemplateColumns = '1fr 1fr'; chartsRow.style.gap = '20px'; chartsRow.style.marginTop = '20px'; var totalsChartContainer = document.createElement('div'); totalsChartContainer.style.minWidth = '0'; var totalsCanvas = document.createElement('canvas'); totalsCanvas.className = "annualTotalsChart-".concat(eventIndex); totalsChartContainer.appendChild(totalsCanvas); var growthChartContainer = document.createElement('div'); growthChartContainer.style.minWidth = '0'; var growthCanvas = document.createElement('canvas'); growthCanvas.className = "annualGrowthChart-".concat(eventIndex); growthChartContainer.appendChild(growthCanvas); chartsRow.appendChild(totalsChartContainer); chartsRow.appendChild(growthChartContainer); tabContent.appendChild(chartsRow); // Render charts if (typeof Chart !== 'undefined') { var totalsCtx = totalsCanvas.getContext('2d'); var growthCtx = growthCanvas.getContext('2d'); var labels = yearly.map(function (d) { return d.year.toString(); }); // eslint-disable-next-line no-undef new Chart(totalsCtx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Total Finishers', data: yearly.map(function (d) { return d.totalFinishers; }), backgroundColor: STYLES.barColor, borderColor: STYLES.barColor, borderWidth: 1 }, { label: 'Total Volunteers', data: yearly.map(function (d) { return d.totalVolunteers; }), backgroundColor: STYLES.lineColor, borderColor: STYLES.lineColor, borderWidth: 1 }] }, options: { animation: false, responsive: true, maintainAspectRatio: true, aspectRatio: 1.3, plugins: { legend: { labels: { color: STYLES.textColor } }, title: { display: true, text: 'Annual Totals', color: STYLES.textColor } }, scales: { x: { title: { display: true, text: 'Year', color: STYLES.textColor }, ticks: { color: STYLES.subtleTextColor }, grid: { color: STYLES.gridColor } }, y: { beginAtZero: true, title: { display: true, text: 'Participants', color: STYLES.textColor }, ticks: { precision: 0, color: STYLES.subtleTextColor }, grid: { color: STYLES.gridColor } } } } }); var growthData = yearly.filter(function (d) { return d.finishersGrowth !== null; }); // eslint-disable-next-line no-undef new Chart(growthCtx, { type: 'line', data: { labels: growthData.map(function (d) { return d.year.toString(); }), datasets: [{ label: 'Finishers Growth', data: growthData.map(function (d) { return d.finishersGrowth; }), borderColor: STYLES.barColor, backgroundColor: STYLES.barColor, borderWidth: 2, pointRadius: 4, pointBackgroundColor: STYLES.barColor, fill: false, tension: 0.2 }, { label: 'Volunteers Growth', data: growthData.map(function (d) { return d.volunteersGrowth; }), borderColor: STYLES.lineColor, backgroundColor: STYLES.lineColor, borderWidth: 2, pointRadius: 4, pointBackgroundColor: STYLES.lineColor, fill: false, tension: 0.2 }] }, options: { animation: false, responsive: true, maintainAspectRatio: true, aspectRatio: 1.3, plugins: { legend: { labels: { color: STYLES.textColor } }, title: { display: true, text: 'Year-over-Year Growth (%)', color: STYLES.textColor } }, scales: { x: { title: { display: true, text: 'Year', color: STYLES.textColor }, ticks: { color: STYLES.subtleTextColor }, grid: { color: STYLES.gridColor } }, y: { title: { display: true, text: 'Growth (%)', color: STYLES.textColor }, ticks: { color: STYLES.subtleTextColor, callback: function callback(value) { return value + '%'; } }, grid: { color: STYLES.gridColor } } } } }); } return tabContent; } function renderComparisonCharts(events) { if (events.length < 2) return null; var comparisonSection = document.createElement('div'); comparisonSection.id = 'comparisonSection'; comparisonSection.style.marginTop = '30px'; comparisonSection.style.padding = '15px'; comparisonSection.style.backgroundColor = STYLES.backgroundColor; comparisonSection.style.borderRadius = '8px'; var heading = document.createElement('h3'); heading.textContent = 'Comparison Charts'; heading.style.textAlign = 'center'; heading.style.color = STYLES.barColor; heading.style.marginBottom = '20px'; comparisonSection.appendChild(heading); // Prepare data for all events var allYearlyData = events.map(function (event) { return { event: event, yearly: aggregateByYear(event) }; }); // Get all unique years across all events var allYears = new Set(); allYearlyData.forEach(function (_ref2) { var yearly = _ref2.yearly; yearly.forEach(function (y) { return allYears.add(y.year); }); }); var sortedYears = Array.from(allYears).sort(function (a, b) { return a - b; }); // Create 4 comparison charts var chartsGrid = document.createElement('div'); chartsGrid.style.display = 'grid'; chartsGrid.style.gridTemplateColumns = '1fr 1fr'; chartsGrid.style.gap = '20px'; // Chart 1: Annual Totals - Finishers var finishersTotalsCanvas = document.createElement('canvas'); chartsGrid.appendChild(createChartContainer(finishersTotalsCanvas)); // Chart 2: Annual Totals - Volunteers var volunteersTotalsCanvas = document.createElement('canvas'); chartsGrid.appendChild(createChartContainer(volunteersTotalsCanvas)); // Chart 3: YoY Growth - Finishers var finishersGrowthCanvas = document.createElement('canvas'); chartsGrid.appendChild(createChartContainer(finishersGrowthCanvas)); // Chart 4: YoY Growth - Volunteers var volunteersGrowthCanvas = document.createElement('canvas'); chartsGrid.appendChild(createChartContainer(volunteersGrowthCanvas)); comparisonSection.appendChild(chartsGrid); // Export/share full report buttons var downloadContainer = document.createElement('div'); downloadContainer.style.display = 'flex'; downloadContainer.style.justifyContent = 'center'; downloadContainer.style.marginTop = '20px'; downloadContainer.style.gap = '10px'; downloadContainer.style.flexWrap = 'wrap'; var exportHtmlBtn = document.createElement('button'); exportHtmlBtn.textContent = '📄 Export HTML'; exportHtmlBtn.style.padding = '8px 16px'; exportHtmlBtn.style.backgroundColor = STYLES.barColor; exportHtmlBtn.style.color = '#1c1b2a'; exportHtmlBtn.style.border = 'none'; exportHtmlBtn.style.borderRadius = '4px'; exportHtmlBtn.style.cursor = 'pointer'; exportHtmlBtn.style.fontWeight = 'bold'; exportHtmlBtn.style.fontSize = '14px'; var shareBtn = document.createElement('button'); shareBtn.textContent = '📤 Share Report'; shareBtn.style.padding = '8px 16px'; shareBtn.style.backgroundColor = STYLES.lineColor; shareBtn.style.color = '#2b223d'; shareBtn.style.border = 'none'; shareBtn.style.borderRadius = '4px'; shareBtn.style.cursor = 'pointer'; shareBtn.style.fontWeight = 'bold'; shareBtn.style.fontSize = '14px'; var getReportMeta = function getReportMeta() { var _state$allParkruns, _currentParkrun$prope; var eventInfo = getCurrentEventInfo(); var currentParkrun = (_state$allParkruns = state.allParkruns) === null || _state$allParkruns === void 0 ? void 0 : _state$allParkruns.find(function (p) { return p.properties.eventname === eventInfo.eventName; }); var eventShortName = (currentParkrun === null || currentParkrun === void 0 || (_currentParkrun$prope = currentParkrun.properties) === null || _currentParkrun$prope === void 0 ? void 0 : _currentParkrun$prope.EventShortName) || eventInfo.eventName; var generatedAt = new Date(); var generatedAtISO = generatedAt.toISOString().split('T')[0]; return { eventShortName: eventShortName, generatedAt: generatedAt.toLocaleString(), generatedAtISO: generatedAtISO }; }; exportHtmlBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() { var originalLabel, originalDisplay, mainContainer, meta, _yield$generateReport, blob, filename, url, link, _t; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: originalLabel = exportHtmlBtn.textContent; originalDisplay = exportHtmlBtn.style.display; exportHtmlBtn.textContent = 'Exporting...'; exportHtmlBtn.disabled = true; exportHtmlBtn.style.display = 'none'; _context2.p = 1; mainContainer = document.querySelector('.parkrun-annual-summary'); if (mainContainer) { _context2.n = 2; break; } throw new Error('Report container not found'); case 2: meta = getReportMeta(); _context2.n = 3; return generateReportBlob(mainContainer, meta); case 3: _yield$generateReport = _context2.v; blob = _yield$generateReport.blob; filename = _yield$generateReport.filename; url = URL.createObjectURL(blob); link = document.createElement('a'); link.href = url; link.download = filename; link.click(); setTimeout(function () { return URL.revokeObjectURL(url); }, 1000); console.log('Annual summary HTML export complete'); _context2.n = 5; break; case 4: _context2.p = 4; _t = _context2.v; console.error('Annual summary export failed:', _t); alert('Error exporting HTML: ' + _t.message); case 5: _context2.p = 5; exportHtmlBtn.disabled = false; exportHtmlBtn.textContent = originalLabel; exportHtmlBtn.style.display = originalDisplay; return _context2.f(5); case 6: return _context2.a(2); } }, _callee2, null, [[1, 4, 5, 6]]); }))); shareBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() { var originalLabel, originalDisplay, mainContainer, meta, _yield$generateReport2, blob, filename, file, url, link, _t2; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: originalLabel = shareBtn.textContent; originalDisplay = shareBtn.style.display; shareBtn.textContent = 'Sharing...'; shareBtn.disabled = true; shareBtn.style.display = 'none'; _context3.p = 1; mainContainer = document.querySelector('.parkrun-annual-summary'); if (mainContainer) { _context3.n = 2; break; } throw new Error('Report container not found'); case 2: meta = getReportMeta(); _context3.n = 3; return generateReportBlob(mainContainer, meta); case 3: _yield$generateReport2 = _context3.v; blob = _yield$generateReport2.blob; filename = _yield$generateReport2.filename; file = new File([blob], filename, { type: 'text/html' }); if (!(navigator.canShare && navigator.canShare({ files: [file] }))) { _context3.n = 5; break; } _context3.n = 4; return navigator.share({ title: "parkrun Annual Summary - ".concat(meta.eventShortName), text: 'Annual participation summary report', files: [file] }); case 4: console.log('Annual summary shared via Web Share API'); _context3.n = 6; break; case 5: url = URL.createObjectURL(blob); link = document.createElement('a'); link.href = url; link.download = filename; link.click(); setTimeout(function () { return URL.revokeObjectURL(url); }, 1000); alert('Sharing is not supported in this browser, so the HTML report was downloaded instead.'); case 6: _context3.n = 8; break; case 7: _context3.p = 7; _t2 = _context3.v; console.error('Annual summary share failed:', _t2); alert('Error sharing report: ' + _t2.message); case 8: _context3.p = 8; shareBtn.disabled = false; shareBtn.textContent = originalLabel; shareBtn.style.display = originalDisplay; return _context3.f(8); case 9: return _context3.a(2); } }, _callee3, null, [[1, 7, 8, 9]]); }))); downloadContainer.appendChild(exportHtmlBtn); downloadContainer.appendChild(shareBtn); comparisonSection.appendChild(downloadContainer); // Render charts if Chart.js is available if (typeof Chart !== 'undefined') { renderComparisonChart(finishersTotalsCanvas, 'Annual Totals - Finishers', sortedYears, allYearlyData, function (y) { return y.totalFinishers; }, 'Finishers'); renderComparisonChart(volunteersTotalsCanvas, 'Annual Totals - Volunteers', sortedYears, allYearlyData, function (y) { return y.totalVolunteers; }, 'Volunteers'); renderComparisonChart(finishersGrowthCanvas, 'YoY Growth - Finishers (%)', sortedYears, allYearlyData, function (y) { return y.finishersGrowth; }, 'Growth (%)', true); renderComparisonChart(volunteersGrowthCanvas, 'YoY Growth - Volunteers (%)', sortedYears, allYearlyData, function (y) { return y.volunteersGrowth; }, 'Growth (%)', true); } return comparisonSection; } function createChartContainer(canvas) { var container = document.createElement('div'); container.style.minWidth = '0'; container.appendChild(canvas); return container; } function renderComparisonChart(canvas, title, years, allYearlyData, valueGetter, yAxisLabel) { var isGrowth = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : false; var datasets = allYearlyData.map(function (_ref5, index) { var event = _ref5.event, yearly = _ref5.yearly; var color = COMPARISON_COLORS[index % COMPARISON_COLORS.length]; var data = years.map(function (year) { var yearData = yearly.find(function (y) { return y.year === year; }); if (!yearData) return null; var value = valueGetter(yearData); return value !== null && value !== undefined ? value : null; }); return { label: event.title || event.eventName, data: data, borderColor: color, backgroundColor: color, borderWidth: 2, pointRadius: 4, pointBackgroundColor: color, fill: false, tension: 0.2, spanGaps: true }; }); var ctx = canvas.getContext('2d'); // eslint-disable-next-line no-undef new Chart(ctx, { type: 'line', data: { labels: years.map(function (y) { return y.toString(); }), datasets: datasets }, options: { animation: false, responsive: true, maintainAspectRatio: true, aspectRatio: 1.3, plugins: { legend: { labels: { color: STYLES.textColor } }, title: { display: true, text: title, color: STYLES.textColor } }, scales: { x: { title: { display: true, text: 'Year', color: STYLES.textColor }, ticks: { color: STYLES.subtleTextColor }, grid: { color: STYLES.gridColor } }, y: { beginAtZero: !isGrowth, title: { display: true, text: yAxisLabel, color: STYLES.textColor }, ticks: { precision: 0, color: STYLES.subtleTextColor, callback: isGrowth ? function (value) { return value + '%'; } : undefined }, grid: { color: STYLES.gridColor } } } } }); } function renderAllSummaries() { // Remove existing summary var existing = document.querySelector('.parkrun-annual-summary'); if (existing) { existing.remove(); } var historyData = extractEventHistoryData(); if (historyData.eventNumbers.length === 0) { console.log('No event history data found'); return; } // Store current event state.currentEvent = _objectSpread(_objectSpread({}, historyData), {}, { eventName: getCurrentEventInfo().eventName }); var allEvents = [state.currentEvent].concat(_toConsumableArray(state.comparisonEvents)); var container = document.createElement('div'); container.className = 'parkrun-annual-summary'; container.style.width = '100%'; container.style.maxWidth = '1200px'; container.style.margin = '20px auto'; container.style.padding = '15px'; container.style.backgroundColor = STYLES.backgroundColor; container.style.borderRadius = '8px'; container.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; var heading = document.createElement('h3'); heading.textContent = 'Annual Participation Summary'; heading.style.textAlign = 'center'; heading.style.marginBottom = '15px'; heading.style.color = STYLES.barColor; container.appendChild(heading); // Add comparison selector if we have nearby parkruns if (state.allParkruns === null) { // Still loading (shouldn't happen as init() awaits the fetch) var message = document.createElement('div'); message.style.padding = '10px'; message.style.color = STYLES.subtleTextColor; message.style.textAlign = 'center'; message.style.fontSize = '13px'; message.textContent = 'Loading parkrun events for comparison...'; container.appendChild(message); } else if (state.allParkruns.length === 0) { // Fetch failed or returned no data var _message = document.createElement('div'); _message.style.padding = '10px'; _message.style.color = '#ff6b6b'; _message.style.textAlign = 'center'; _message.style.fontSize = '13px'; _message.textContent = 'Failed to load parkrun events data. Check console for details.'; container.appendChild(_message); console.error('No parkrun events loaded. Expected data from https://images.parkrun.com/events.json'); } else { var nearbyParkruns = findNearbyParkruns(getCurrentEventInfo(), state.allParkruns); if (nearbyParkruns.length > 0) { var selector = createComparisonSelector(nearbyParkruns); selector.className = 'parkrun-comparison-selector-controls'; container.appendChild(selector); } else { // Show message if no nearby parkruns found var _message2 = document.createElement('div'); _message2.style.padding = '10px'; _message2.style.color = STYLES.subtleTextColor; _message2.style.textAlign = 'center'; _message2.style.fontSize = '13px'; var eventInfo = getCurrentEventInfo(); _message2.textContent = "No nearby parkruns found for comparison (within 50km of ".concat(eventInfo.eventName, ")"); container.appendChild(_message2); console.log('Current event:', eventInfo.eventName, 'Total parkruns loaded:', state.allParkruns.length); } } // Add selected events display if (state.comparisonEvents.length > 0) { var _createSelectedEvents = createSelectedEventsDisplay(), eventsDisplay = _createSelectedEvents.container; container.appendChild(eventsDisplay); } // Create tabs var _createTabsForEvents = createTabsForEvents(allEvents), tabsContainer = _createTabsForEvents.tabsContainer, tabContents = _createTabsForEvents.tabContents; container.appendChild(tabsContainer); // Render each event's tab allEvents.forEach(function (event, index) { var tabContent = renderEventTab(event, index); if (tabContent) { tabContents.appendChild(tabContent); } }); // Add comparison charts if multiple events if (allEvents.length > 1) { var comparisonCharts = renderComparisonCharts(allEvents); if (comparisonCharts) { container.appendChild(comparisonCharts); } } // Insert into page var eventHistoryChart = document.getElementById('eventHistoryChart'); if (eventHistoryChart && eventHistoryChart.parentElement) { eventHistoryChart.parentElement.parentNode.insertBefore(container, eventHistoryChart.parentElement.nextSibling); } else { insertAfterFirst('h1', container); } } function renderAnnualSummary() { if (document.getElementById('annualSummaryTable') || document.getElementById('eventTabs')) { console.log('Annual summary already exists, skipping render'); return; } renderAllSummaries(); } function init() { return _init.apply(this, arguments); } function _init() { _init = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() { var resultsTable, pageUrl, isEventHistoryPage; return _regenerator().w(function (_context8) { while (1) switch (_context8.n) { case 0: resultsTable = document.querySelector('.Results-table'); pageUrl = window.location.href; isEventHistoryPage = pageUrl.includes('/eventhistory/'); if (!(!resultsTable || !isEventHistoryPage)) { _context8.n = 1; break; } return _context8.a(2); case 1: _context8.n = 2; return fetchAllParkruns(); case 2: state.allParkruns = _context8.v; renderAnnualSummary(); case 3: return _context8.a(2); } }, _callee8); })); return _init.apply(this, arguments); } init(); })();