Displays charts on parkrun pages: finishers per minute on results pages and event history on event history pages

What is a userscript?

A userscript is a small piece of JavaScript that runs in your browser and enhances specific websites. These scripts work with parkrun event pages, parkrunner profile pages, and results pages, and can be used with any userscript manager including Userscripts, Tampermonkey, Violentmonkey, or any compatible browser extension.

Installation

  1. Install a userscript manager for your browser:
  2. Click the install link below for this script.
  3. Click “Install” when prompted by your userscript manager.

parkrun Charts Screenshot

Install parkrun Charts

Bookmarklet version

You can also use this script as a bookmarklet (a bookmark whose URL is JavaScript), which can be useful on browsers that support bookmarks but not userscript managers.

Desktop bookmarklet

Drag this link to your bookmarks bar:

parkrun Charts bookmarklet

Mobile bookmarklet

On mobile browsers that let you edit bookmark URLs:

  1. Copy the JavaScript code below to your clipboard.
  2. Create a new bookmark for any page.
  3. Edit the bookmark and replace its URL with the code you copied.
  4. Navigate to the relevant parkrun page.
  5. Select the bookmark to run the script.
javascript:function _slicedToArray(e,t){return _arrayWithHoles(e)||_iterableToArrayLimit(e,t)||_unsupportedIterableToArray(e,t)||_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(e,t){var n;if(e)return"string"==typeof e?_arrayLikeToArray(e,t):"Map"===(n="Object"===(n={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:n)||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(e,t):void 0}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function _iterableToArrayLimit(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,o,a,l,i=[],s=!0,c=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;s=!1}else for(;!(s=(r=a.call(n)).done)&&(i.push(r.value),i.length!==t);s=!0);}catch(e){c=!0,o=e}finally{try{if(!s&&null!=n.return&&(l=n.return(),Object(l)!==l))return}finally{if(c)throw o}}return i}}function _arrayWithHoles(e){if(Array.isArray(e))return e}(()=>{var b={backgroundColor:"#2b223d",barColor:"#FFA300",lineColor:"#53BA9D",textColor:"#e0e0e0",subtleTextColor:"#cccccc",gridColor:"rgba(200, 200, 200, 0.2)"},d=!1;function v(e,t,n){var n=2<arguments.length&&void 0!==n?n:800,r=document.createElement("div"),n=(r.className="parkrun-chart-container "+t+"-container",r.style.width="100%",r.style.maxWidth=n+"px",r.style.margin="20px auto",r.style.padding="15px",r.style.backgroundColor=b.backgroundColor,r.style.borderRadius="8px",r.style.boxShadow="0 2px 4px rgba(0,0,0,0.1)",document.createElement("h3")),e=(n.textContent=e,n.style.textAlign="center",n.style.marginBottom="15px",n.style.color=b.barColor,r.appendChild(n),document.createElement("canvas"));return e.id=t,r.appendChild(e),{container:r,canvas:e}}function f(e,t){e=document.querySelector(e);e&&e.parentNode&&(e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t))}function x(a){var e=document.createElement("div"),l=(e.style.display="flex",e.style.justifyContent="center",e.style.marginTop="10px",e.style.marginBottom="10px",document.createElement("button"));l.textContent="💾 Save as Image",l.style.padding="6px 12px",l.style.backgroundColor=b.barColor,l.style.color="#2b223d",l.style.border="none",l.style.borderRadius="4px",l.style.cursor="pointer",l.style.fontWeight="bold",l.style.display="inline-block",l.style.margin="0 5px",l.title="Download chart as PNG image",l.addEventListener("mouseover",function(){this.style.backgroundColor="#e59200"}),l.addEventListener("mouseout",function(){this.style.backgroundColor=b.barColor}),l.addEventListener("click",function(){l.style.display="none",html2canvas(a,{backgroundColor:b.backgroundColor,scale:2,logging:!1,allowTaint:!0,useCORS:!0}).then(function(e){l.style.display="block";var t=document.createElement("a"),n=(new Date).toISOString().split("T")[0],r=window.location.pathname.split("/")[1],o=a.classList.contains("eventHistoryChart-container")?"event-history":"finishers";t.download="".concat(r,"-").concat(o,"-").concat(n,".png"),t.href=e.toDataURL("image/png"),t.click()})}),e.appendChild(l),a.appendChild(e)}function C(e){for(var t=e.getContext("2d"),n="parkrun-charts",r="unknown",o="",a=("undefined"!=typeof GM_info&&GM_info&&GM_info.script&&(n=GM_info.script.name||n,r=GM_info.script.version||r,o=GM_info.script.homepage||""),["Generated by ".concat(n," v").concat(r),o]),l=(t.save(),t.font=d?"bold 16px Arial":"10px Arial",t.fillStyle=d?"rgba(255, 0, 0, 0.5)":"rgba(200, 200, 200, 0.1)",t.textAlign="right",t.textBaseline="bottom",d?20:14),i=e.width-10,s=e.height-10,c=a.length-1;0<=c;c--){var u=a[c];u&&(t.fillText(u,i,s),s-=l)}t.restore()}function e(){var e,t,n=[[null==(n=document.querySelector("h1"))||null==(n=n.textContent)?void 0:n.trim(),null==(n=document.querySelector("h3"))||null==(n=n.textContent)?void 0:n.trim()].filter(Boolean).join(" | "),"Finishers per Minute"].filter(Boolean).join(": "),r=(e=>{for(var t=e.timeData,n=e.maxMinute,r=[],o=[],a=e.minMinute;a<=n;a++)r.push(a),o.push(t[a]||0);return{labels:r.map(function(e){var t=Math.floor(e/60),e=e%60;return"".concat(t,":").concat(e.toString().padStart(2,"0"))}),data:o}})((()=>{var r={},e=document.querySelectorAll("tr.Results-table-row"),o=1/0,a=0;e.forEach(function(e){e=e.querySelector("td.Results-table-td--time");if(e){var e=e.textContent.trim(),t=e.match(/(\d+):(\d+):(\d+)/);if(t)var n=60*parseInt(t[1])+parseInt(t[2]);else{t=e.match(/(\d+):(\d+)/);if(!t)return;n=parseInt(t[1])}o=Math.min(o,n),a=Math.max(a,n),r[n]?r[n]++:r[n]=1}});for(var t=o;t<=a;t++)r[t]||(r[t]=0);return{timeData:r,minMinute:o,maxMinute:a}})());0===r.labels.length?console.log("No finish time data found"):document.getElementById("finishersChart")?console.log("Finishers chart already exists, skipping render"):(e=(n=v(n,"finishersChart")).container,t=n.canvas,f("h3",e),x(e),n=t.getContext("2d"),new Chart(n,{type:"bar",data:{labels:r.labels,datasets:[{label:"Number of Finishers",data:r.data,backgroundColor:b.barColor,borderColor:b.barColor,borderWidth:1}]},options:{animation:!1,responsive:!0,plugins:{legend:{labels:{color:b.textColor}},title:{display:!1,color:b.textColor},tooltip:{callbacks:{title:function(e){var t,n,e=e[0].label;return e.includes(":")?(n=(t=_slicedToArray(e.split(":"),2))[0],t=t[1],"".concat(n," hour").concat("1"===n?"":"s"," ").concat(t," minute").concat("01"===t?"":"s")):(n=e.replace("'",""),"".concat(n," minute").concat("1"===n?"":"s"))},label:function(e){return"".concat(e.raw," finisher").concat(1===e.raw?"":"s")}}}},scales:{x:{title:{display:!0,text:"Finish Time",color:b.textColor},ticks:{color:b.subtleTextColor},grid:{color:b.gridColor}},y:{beginAtZero:!0,title:{display:!0,text:"Number of Finishers",color:b.textColor},ticks:{precision:0,color:b.subtleTextColor},grid:{color:b.gridColor}}},onHover:function(){setTimeout(function(){return C(t)},0)},onResize:function(){setTimeout(function(){return C(t)},0)},customPlugin:{id:"watermarkPlugin",afterDraw:function(){setTimeout(function(){return C(t)},0)}}}}),setTimeout(function(){return C(t)},0))}function i(e,t){return t<=e}function A(e,t){for(var n=[],r=0;r<e.length;r++)if(r<t-1)n.push(null);else{for(var o=0,a=0;a<t;a++)o+=e[r-a];n.push(parseFloat((o/t).toFixed(1)))}return n}function k(e,t,n){for(var r=1/0,o=-1/0,a=-1,l=-1,i=0;i<e.length;i++)e[i]<r&&(r=e[i],a=i),e[i]===r&&(a=i),o<e[i]&&(o=e[i],l=i),e[i]===o&&(l=i);return{min:{value:r,eventNumber:t[a],date:n[a],index:a},max:{value:o,eventNumber:t[l],date:n[l],index:l}}}function w(e,t){return 0!==e&&0!==t&&Math.floor(Math.log10(e))===Math.floor(Math.log10(t))}function E(e){var t=e.historyData,n=e.finishersAxisId,r=e.volunteersAxisId,o=e.finishersRollingAvg,a=e.volunteersRollingAvg,e=e.rollingAvgWindowSize,l=[{label:"Finishers",data:t.finishers,backgroundColor:b.barColor,borderColor:b.barColor,borderWidth:1,yAxisID:n,order:1},{label:"Volunteers",data:t.volunteers,type:"line",borderColor:b.lineColor,backgroundColor:"rgba(83, 186, 157, 0.2)",borderWidth:1,pointBackgroundColor:b.lineColor,pointRadius:2,fill:!1,tension:.2,yAxisID:r,order:2}];return i(t.eventNumbers.length,e)&&l.push({label:"".concat(e,"-Event Avg (Finishers)"),data:o,type:"line",borderColor:b.barColor,backgroundColor:"transparent",borderWidth:2,borderDash:[5,5],pointRadius:0,fill:!1,yAxisID:n,order:0},{label:"".concat(e,"-Event Avg (Volunteers)"),data:a,type:"line",borderColor:b.lineColor,backgroundColor:"transparent",borderWidth:2,borderDash:[5,5],pointRadius:0,fill:!1,yAxisID:r,order:3}),l}function S(e){var t=e.latestDate,n=e.latestEventNumber,r=e.latestFinishers,o=e.latestVolunteers,a=e.latestFinishersAvg,l=e.latestVolunteersAvg,e=e.rollingAvgWindowSize,a=null!=a?" (".concat(e,"-Event avg: ").concat(a.toFixed(1),")"):"",e=null!=l?" (".concat(e,"-Event avg: ").concat(l.toFixed(1),")"):"";return"\n      <strong>Latest event:</strong> ".concat(t," (Event #").concat(n,') |\n      <span style="color: ').concat(b.barColor,'">Finishers: ').concat(r).concat(a,'</span> |\n      <span style="color: ').concat(b.lineColor,'">Volunteers: ').concat(o).concat(e,"</span>\n    ")}function t(){var o,n,r,a,l,e,t,i,s,c,u,d,p,m,g,h,y;document.getElementById("eventHistoryChart")?console.log("Event history chart already exists, skipping render"):(t=null!=(t=null==(t=document.querySelector("h1"))?void 0:t.textContent.trim())?t:"Event History",n=[],r=[],a=[],l=[],e=document.querySelectorAll("tr.Results-table-row"),Array.from(e).reverse().forEach(function(e){var t=e.getAttribute("data-parkrun"),t=(t&&n.push(t),e.getAttribute("data-date")),t=(t&&(t=new Date(t).toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"}),r.push(t)),e.getAttribute("data-finishers")),t=(t&&a.push(parseInt(t,10)),e.getAttribute("data-volunteers"));t&&l.push(parseInt(t,10))}),0===(o={title:t,eventNumbers:n,dates:r,finishers:a,volunteers:l}).eventNumbers.length?console.log("No event history data found"):(e=A(o.finishers,12),t=A(o.volunteers,12),h=k(o.finishers,o.eventNumbers,o.dates),y=k(o.volunteers,o.eventNumbers,o.dates),i={"y-parkrunners":{type:"linear",position:"left",beginAtZero:!0,title:{display:!0,text:"parkrunners",color:b.textColor},ticks:{precision:0,color:b.subtleTextColor},grid:{color:b.gridColor}},"y-finishers":{type:"linear",position:"left",beginAtZero:!0,title:{display:!0,text:"Number of Finishers",color:b.barColor},ticks:{precision:0,color:b.barColor},grid:{color:b.gridColor}},"y-volunteers":{type:"linear",position:"right",beginAtZero:!0,title:{display:!0,text:"Number of Volunteers",color:b.lineColor},ticks:{precision:0,color:b.lineColor},grid:{display:!1}}},s=(c=w(h.max.value,y.max.value))?"y-parkrunners":"y-finishers",c=c?"y-parkrunners":"y-volunteers",u=(p=v("".concat(o.title,": Finishers & Volunteers"),"eventHistoryChart",1e3)).container,(d=p.canvas).height=400,d.style.height="400px",d.style.maxHeight="400px",f("h1",u),p=d.getContext("2d"),m=o.dates.length-1,(g=document.createElement("div")).className="chart-latest-summary",g.style.marginTop="10px",g.style.color=b.textColor,g.style.fontSize="14px",g.style.textAlign="center",g.style.wordBreak="break-word",g.innerHTML=S({latestDate:o.dates[m],latestEventNumber:o.eventNumbers[m],latestFinishers:o.finishers[m],latestVolunteers:o.volunteers[m],latestFinishersAvg:e[m],latestVolunteersAvg:t[m],rollingAvgWindowSize:12}),u.appendChild(g),(m=document.createElement("div")).className="chart-stats-footer",m.style.marginTop="10px",m.style.padding="10px",m.style.backgroundColor=b.backgroundColor,m.style.color=b.textColor,m.style.borderRadius="4px",m.style.fontSize="14px",m.style.textAlign="center",m.innerHTML='\n      <span style="color: '.concat(b.barColor,'">Finishers:</span> Min: ').concat(h.min.value," (").concat(h.min.date,", Event #").concat(h.min.eventNumber,") |\n      Max: ").concat(h.max.value," (").concat(h.max.date,", Event #").concat(h.max.eventNumber,')<br>\n      <span style="color: ').concat(b.lineColor,'">Volunteers:</span> Min: ').concat(y.min.value," (").concat(y.min.date,", Event #").concat(y.min.eventNumber,") |\n      Max: ").concat(y.max.value," (").concat(y.max.date,", Event #").concat(y.max.eventNumber,")\n    "),u.appendChild(m),g={title:{display:!0,text:"Date",color:b.textColor},ticks:{color:b.subtleTextColor,maxRotation:45,minRotation:45,callback:function(e,t){var n=o.dates.length,r=1;return 100<n?r=10:50<n?r=5:20<n&&(r=2),0===t||t===o.dates.length-1||t%r==0?o.dates[t]:""}},grid:{color:b.gridColor}},h=E({historyData:o,finishersAxisId:s,volunteersAxisId:c,finishersRollingAvg:e,volunteersRollingAvg:t,rollingAvgWindowSize:12}),(y={x:g})[s]=i[s],c!=s&&(y[c]=i[c]),new Chart(p,{type:"bar",data:{labels:o.dates,datasets:h},options:{animation:!1,responsive:!0,maintainAspectRatio:!0,aspectRatio:2.5,plugins:{legend:{labels:{color:b.textColor,usePointStyle:!0}},tooltip:{mode:"index",callbacks:{title:function(e){var e=e[0].dataIndex,t=o.eventNumbers[e],e=o.dates[e];return"".concat(e," (Event #").concat(t,")")},label:function(e){var t=e.dataset.label||"";return"Finishers"===t?"Finishers: ".concat(e.raw):"Volunteers"===t?"Volunteers: ".concat(e.raw):t.includes("Avg")?"".concat(t,": ").concat(e.raw):e.formattedValue}}}},scales:y,onHover:function(){setTimeout(function(){return C(d)},0)},onResize:function(){setTimeout(function(){return C(d)},0)},customPlugin:{id:"watermarkPlugin",afterDraw:function(){setTimeout(function(){return C(d)},0)}}}}),setTimeout(function(){return C(d)},0),x(u)))}"undefined"!=typeof module&&void 0!==module.exports?(module.exports.sameOrderOfMagnitude=w,module.exports.eventHistoryHasEnoughEventsForRollingAverage=i,module.exports.isRecordFinisherOrVolunteerCount=function(e,t){return e===t.min.value||e===t.max.value},module.exports.buildEventHistoryDatasets=E,module.exports.formatLatestEventSummaryHtml=S):"undefined"==typeof Chart?console.error("Chart.js not loaded"):document.querySelector(".Results-table")?(window.location.href.includes("/eventhistory/")?t:e)():console.log("Results table not found")})();
Last updated
Version
1.1.8
Author
Pete Johns (@johnsyweb)
Homepage
https://www.johnsy.com/tampermonkey-parkrun/
Support
Issues / support
License
MIT