Charts p-index progression over finishes with next-step planning details for p-index challenge analysis.
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
-
Install a userscript manager for your browser:
- Desktop: Userscripts (Safari), Tampermonkey (Chrome, Firefox, Edge, Opera), or Violentmonkey (Orion)
- iOS: Userscripts (Safari) or Violentmonkey (Orion)
- Android: Install Kiwi Browser, then install Tampermonkey or Violentmonkey from the Chrome Web Store.
- Click the install link below for this script.
- Click “Install” when prompted by your userscript manager.
Install parkrun p-index progression
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 p-index progression bookmarklet
Mobile bookmarklet
On mobile browsers that let you edit bookmark URLs:
- Copy the JavaScript code below to your clipboard.
- Create a new bookmark for any page.
- Edit the bookmark and replace its URL with the code you copied.
- Navigate to the relevant parkrun page.
- Select the bookmark to run the script.
javascript:(()=>{var n=Object.getOwnPropertyNames,e=(e,t)=>function(){return t||(0,e[n(e)[0]])((t={exports:{}}).exports,t),t.exports},y=e({"lib/p-index-core.js"(e,t){function u(e){e=e.reduce((e,{eventName:t,date:n,eventNumber:i})=>(e[t]||(e[t]=[]),e[t].push({date:n,eventNumber:i}),e),{});return Object.entries(e).sort((e,t)=>t[1].length-e[1].length)}function h(e){return e.filter(([,e],t)=>e.length>t).length}function m(e){let i=new Map;return e.forEach((e,t)=>{var n=i.get(e.eventName)||{count:0,lastVisitIndex:-1};i.set(e.eventName,{count:n.count+1,lastVisitIndex:t})}),Array.from(i.entries()).map(([e,t])=>({eventName:e,count:t.count,lastVisitIndex:t.lastVisitIndex}))}function x(e,n){var t=e.map(e=>({eventName:e.eventName,count:e.count,lastVisitIndex:"number"==typeof e.lastVisitIndex?e.lastVisitIndex:Number.NEGATIVE_INFINITY,score:Math.min(e.count,n)})).sort((e,t)=>t.score!==e.score?t.score-e.score:t.count!==e.count?t.count-e.count:t.lastVisitIndex!==e.lastVisitIndex?t.lastVisitIndex-e.lastVisitIndex:e.eventName.localeCompare(t.eventName)).slice(0,n);let i=[],a=0;var o=new Set(e.map(e=>e.eventName)),s=(t.filter(e=>e.count<n).sort((e,t)=>e.lastVisitIndex!==t.lastVisitIndex?e.lastVisitIndex-t.lastVisitIndex:e.eventName.localeCompare(t.eventName)).forEach(e=>{var t=Math.max(0,n-e.count);i.push({eventName:e.eventName,additionalFinishes:t,isNewEvent:!1}),a+=t}),Math.max(0,n-t.length));let l=1;for(let e=0;e<s;e++){for(;o.has("New event "+l);)l++;var r="New event "+l;o.add(r),i.push({eventName:r,additionalFinishes:n,isNewEvent:!0}),a+=n,l++}return{targetPIndex:n,totalAdditionalFinishes:a,actions:i}}function g(e,t){let n=new Map(e.map(e=>[e.eventName,{count:e.count,lastVisitIndex:"number"==typeof e.lastVisitIndex?e.lastVisitIndex:Number.NEGATIVE_INFINITY}]));e=e.reduce((e,t)=>"number"==typeof t.lastVisitIndex?Math.max(e,t.lastVisitIndex):e,Number.NEGATIVE_INFINITY)+1;let i=Number.isFinite(e)?e:0;return t.forEach(e=>{var t=n.get(e.eventName)||{count:0,lastVisitIndex:Number.NEGATIVE_INFINITY};n.set(e.eventName,{count:t.count+e.additionalFinishes,lastVisitIndex:i+e.additionalFinishes-1}),i+=e.additionalFinishes}),Array.from(n.entries()).map(([e,t])=>({eventName:e,count:t.count,lastVisitIndex:t.lastVisitIndex}))}t.exports={applyPlanToEventCounts:g,buildDifficultyMetrics:function(e,t){var n;return 0===e.length?{latestGap:0,longestGap:0,startLevel:0,endLevel:0,nextTarget:1,nextPlan:{targetPIndex:1,totalAdditionalFinishes:1,actions:[{eventName:"New event 1",additionalFinishes:1,isNewEvent:!0}]},lookaheadTarget:2,lookaheadPlan:{targetPIndex:2,totalAdditionalFinishes:3,actions:[{eventName:"New event 2",additionalFinishes:2,isNewEvent:!0},{eventName:"New event 3",additionalFinishes:1,isNewEvent:!0}]}}:(n=e.filter(e=>e.isJump),e=e[e.length-1],n=n.reduce((e,t)=>!e||t.finishesSincePreviousIncrease>e.finishesSincePreviousIncrease?t:e,null),{latestGap:e.finishesSincePreviousIncrease,longestGap:n?n.finishesSincePreviousIncrease:0,startLevel:n?n.previousPIndex:0,endLevel:n?n.pIndex:0,nextTarget:t+1,nextPlan:e.nextPlan,lookaheadTarget:t+2,lookaheadPlan:e.lookaheadPlan})},buildEventStats:m,calculateMinimumFinishesPlan:x,calculatePIndex:h,calculatePIndexProgression:function(t){var n=[];let i=0,a=0;for(let e=0;e<t.length;e++){var o=t.slice(0,e+1),s=m(o),o=h(u(o)),l=x(s,o+1),s=x(g(s,l.actions),o+2),r=t[e],d=e+1,c=o>i,p=d-a;n.push({finishes:d,date:r.date,eventName:r.eventName,eventNumber:r.eventNumber,pIndex:o,previousPIndex:i,isJump:c,finishesSincePreviousIncrease:p,nextPlan:l,lookaheadPlan:s}),c&&(a=d,i=o)}return n},groupFinishesByEvent:u,parseDateDdMmYyyy:function(e){var[e,t,n]=e.split("/").map(e=>parseInt(e,10));return new Date(n,t-1,e)}}}});e({"src/p-index-progression.user.js"(e,i){{let{buildDifficultyMetrics:a,calculatePIndex:e,calculatePIndexProgression:t,groupFinishesByEvent:n,parseDateDdMmYyyy:o}=y(),s={backgroundColor:"#2b223d",accentColor:"#ffa300",textColor:"#f3f4f6",subtleTextColor:"#d1d5db",gridColor:"rgba(243, 244, 246, 0.18)"};if(void 0!==i&&i.exports&&"undefined"!=typeof globalThis&&globalThis.process&&globalThis.process.versions&&globalThis.process.versions.node)i.exports={buildCompactPlanSummary:x,buildDifficultySummary:m,buildSentencePlanSummary:g,extractFinishTimeline:u,findResultsTable:h};else{i=h();if(i){i=u(i),l=n(i),l=e(l);var l,i=t(i).map(e=>({...e,nextPlan:{...e.nextPlan,compactSummary:x(e.nextPlan.actions)},lookaheadPlan:{...e.lookaheadPlan,compactSummary:x(e.lookaheadPlan.actions)}})),r=window.innerWidth<768?{container:{padding:"10px",marginTop:"10px"},typography:{heading:"1.2em"},button:{padding:"6px 12px",fontSize:"0.9em"},chart:{height:"280px",titleSize:14,axisTitleSize:12,tickSize:10}}:{container:{padding:"20px",marginTop:"20px"},typography:{heading:"1.5em"},button:{padding:"8px 15px",fontSize:"1em"},chart:{height:"360px",titleSize:16,axisTitleSize:14,tickSize:12}},d=document.querySelector("h2");if(d){let e=document.createElement("section");e.id="p-index-progression-display",e.style.width="100%",e.style.maxWidth="800px",e.style.margin=r.container.marginTop+" auto",e.style.backgroundColor=s.backgroundColor,e.style.color=s.accentColor,e.style.padding=r.container.padding,e.style.borderRadius="5px",e.style.boxSizing="border-box";var c=document.createElement("h3"),c=(c.textContent="p-index progression over finishes",c.style.margin="0 0 15px 0",c.style.fontSize=r.typography.heading,c.style.textAlign="center",c.style.color=s.accentColor,e.appendChild(c),document.createElement("div")),p=(c.style.height=r.chart.height,c.style.position="relative",c.style.width="100%",c.id="p-index-progression-chart-container",e.appendChild(c),document.createElement("canvas"));p.setAttribute("aria-label","p-index progression chart"),c.appendChild(p),void 0===window.Chart?((c=document.createElement("p")).textContent="Chart unavailable: failed to load Chart.js.",c.style.color=s.subtleTextColor,c.style.marginTop="12px",e.appendChild(c)):((e,t,n)=>(t=t.map(e=>({x:e.finishes,y:e.pIndex,point:e})),new window.Chart(e.getContext("2d"),{type:"line",data:{datasets:[{label:"p-index",data:t,stepped:!0,borderColor:s.accentColor,backgroundColor:s.accentColor,borderWidth:2,pointRadius:2,pointHoverRadius:4}]},options:{responsive:!0,maintainAspectRatio:!1,scales:{x:{type:"linear",title:{display:!0,text:"Finishes",font:{size:n.chart.axisTitleSize},color:s.textColor},ticks:{font:{size:n.chart.tickSize},stepSize:1,color:s.subtleTextColor},grid:{color:s.gridColor},min:1},y:{beginAtZero:!0,title:{display:!0,text:"p-index",font:{size:n.chart.axisTitleSize},color:s.textColor},ticks:{font:{size:n.chart.tickSize},stepSize:1,color:s.subtleTextColor},grid:{color:s.gridColor}}},plugins:{title:{display:!1},legend:{display:!1},tooltip:{callbacks:{label(e){var e=e.raw.point,t=["p-index: "+e.pIndex,"Finishes: "+e.finishes,"Date: "+e.date,`Event: ${e.eventName} (#${e.eventNumber})`,"Finishes since previous increase: "+e.finishesSincePreviousIncrease,"Minimum finishes to next p-index: "+e.nextPlan.totalAdditionalFinishes,"Plan: "+e.nextPlan.compactSummary];return e.isJump&&t.push(`Increase reached: ${e.previousPIndex} and `+e.pIndex),t}}}}},plugins:[{id:"jumpLabels",afterDatasetsDraw(e){var t=e.data.datasets[0];let i=e.getDatasetMeta(0),a=e.ctx;a.save(),a.fillStyle=s.subtleTextColor,a.font="11px sans-serif",a.textAlign="center",t.data.forEach((e,t)=>{var n;null!=(n=null==e?void 0:e.point)&&n.isJump&&(n=i.data[t])&&a.fillText("+"+e.point.finishesSincePreviousIncrease,n.x,n.y-4)}),a.restore()}}]})))(p,i,r),e.appendChild(((e,t)=>{let n=document.createElement("div"),i=(n.style.marginTop="12px",n.style.marginBottom="12px",n.style.color=s.subtleTextColor,n.style.fontSize="0.95em",document.createElement("ul"));return i.style.listStyleType="none",i.style.padding="0",i.style.margin="0",i.style.textAlign="left",e.forEach(e=>{var t=document.createElement("li");t.style.marginBottom="6px",t.textContent=e,i.appendChild(t)}),n.appendChild(i),t<3&&((e=document.createElement("p")).style.margin="6px 0 0 0",e.style.fontStyle="italic",e.textContent="Trend note: very early p-index values can shift quickly.",n.appendChild(e)),n})(m(i,l),l));let t=document.createElement("div"),n=(t.style.display="flex",t.style.justifyContent="center",t.style.marginTop="12px",e.appendChild(t),document.createElement("button"));n.type="button",n.textContent="💾 Save chart image",((e,t)=>{e.style.padding=t.button.padding,e.style.backgroundColor=s.accentColor,e.style.color=s.backgroundColor,e.style.border="none",e.style.borderRadius="4px",e.style.cursor="pointer",e.style.fontWeight="bold",e.style.fontSize=t.button.fontSize})(n,r),t.appendChild(n),n.addEventListener("click",()=>{n.disabled=!0,n.textContent="Saving...",t.style.visibility="hidden",html2canvas(e,{backgroundColor:s.backgroundColor,scale:2,logging:!1,allowTaint:!0,useCORS:!0}).then(e=>{var t=document.createElement("a"),n=(new Date).toISOString().split("T")[0],i=window.location.pathname.split("/")[2]||"parkrunner";t.download=`p-index-progression-${i}-${n}.png`,t.href=e.toDataURL("image/png"),t.click()}).finally(()=>{t.style.visibility="visible",n.disabled=!1,n.textContent="💾 Save chart image"})}),d.parentNode.insertBefore(e,d.nextSibling)}}else console.error("Results table not found")}function u(e){let i=[];return e.querySelectorAll("tbody > tr").forEach(e=>{var t=e.querySelector("td:nth-child(1)").textContent.trim(),n=e.querySelector("td:nth-child(2)").textContent.trim(),e=e.querySelector("td:nth-child(3)").textContent.trim();i.push({eventName:t,date:n,eventNumber:e})}),i.sort((e,t)=>o(e.date)-o(t.date))}function h(){var e=document.querySelectorAll("#results");return e[e.length-1]}function m(e,t){var e=a(e,t),n=1===e.nextPlan.totalAdditionalFinishes?"finish":"finishes",i=1===e.lookaheadPlan.totalAdditionalFinishes?"finish":"finishes";return["Current p-index: "+t,"Finishes since previous increase: "+e.latestGap,`Longest gap: ${e.longestGap} finishes (between p-index ${e.startLevel} and ${e.endLevel})`,`Minimum finishes to next p-index ${e.nextTarget}: ${e.nextPlan.totalAdditionalFinishes} ${n}. `+g(e.nextPlan.actions),`Then minimum finishes to p-index ${e.lookaheadTarget}: ${e.lookaheadPlan.totalAdditionalFinishes} ${i}. `+g(e.lookaheadPlan.actions)]}function x(e){return 0===e.length?"No additional finishes required.":e.map(e=>`+${e.additionalFinishes} `+e.eventName).join(", ")}function g(e){if(0===e.length)return"No additional finishes required.";var t=e.filter(e=>1===e.additionalFinishes).map(e=>e.eventName),e=e.filter(e=>1<e.additionalFinishes);let n=[];return 1===t.length?n.push(`add ${t[0]} once`):1<t.length&&n.push(`add ${t=t,0===t.length?"":1===t.length?t[0]:2===t.length?t[0]+" and "+t[1]:t.slice(0,-1).join(", ")+", and "+t[t.length-1]} once`),e.forEach(e=>{n.push(`add ${e.eventName} ${e.additionalFinishes} times`)}),1===n.length?v(n[0])+".":2===n.length?`${v(n[0])}, and ${n[1]}.`:`${v(n.slice(0,-1).join(", "))}, and ${n[n.length-1]}.`}function v(e){return e&&e.charAt(0).toUpperCase()+e.slice(1)}}}})()})();