Uiaa Results Visualiser

Plots results achieved by competitors on routes in UIAA competitions.

// ==UserScript==
// @name         Uiaa Results Visualiser
// @namespace    http://tampermonkey.net/
// @version      2025-01-19
// @description  Plots results achieved by competitors on routes in UIAA competitions.
// @author       [email protected]
// @match        https://uiaa.results.info/*
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @run-at       document-idle
// @grant        none
// ==/UserScript==

'use strict';


const HEADER_CLASS = 'cr-head-container';
const BIB_INPUT_ID = 'tracked_bib_input';
const CREATE_CHARTS_BUTTON_ID = 'create_charts_button';

let contentDiv = null;
let bibInput = null;
let createChartsSubmitButton = null;
let intervalId = null;
let dataByBib = null;
let resultsPerRoute = null;
let bibToTrack = null;
let presentCharts = [];

let pathname = URL.parse(document.URL).pathname;
let roundId = pathname.split("/")[4];

let resultsApiUrl = 'https://uiaa.results.info/api/v1/category_rounds/' + roundId + '/results'

fetch(resultsApiUrl)
    .then(response => response.json())
    .then(data => { processResultsData(data)});

intervalId = setInterval(function() {
    if (document.getElementsByClassName(HEADER_CLASS).length) {
        contentDiv = document.getElementsByClassName(HEADER_CLASS)[0];
        bibInput = createTrackedBibInput();
        createChartsSubmitButton = createDrawChartsSubmitButton();

        contentDiv.appendChild(bibInput);
        contentDiv.appendChild(createChartsSubmitButton);

        clearInterval(intervalId);
    }
}, 1000);

function createTrackedBibInput() {
    let input = document.createElement('input');
    input.setAttribute('id', BIB_INPUT_ID);
    input.setAttribute('placeholder', 'Bib to track');
    input.required = false;
    input.style['margin-right'] = '15px';
    input.style.width = '30%';
    return input;
}

function createDrawChartsSubmitButton() {
    let button = document.createElement('button');
    let buttonTextNode = document.createTextNode('Show route results distribution');

    button.setAttribute('id', CREATE_CHARTS_BUTTON_ID);
    button.appendChild(buttonTextNode);
    button.addEventListener('click', function() {
        bibToTrack = bibInput.value;
        addCharts();
    }, false);
    return button;
}



function processResultsData(rawResults) {
    let routes = getRoutes(rawResults.routes)
    dataByBib = getResultsByBib(rawResults.ranking);
    resultsPerRoute = getResultsPerRoute(routes, dataByBib)
}

function getRoutes(rawRoutes) {
    let routes = [];
    for(let route in rawRoutes) {
        routes.push(rawRoutes[route].name)
    }
    return routes;
}

function getResultsByBib(ranking) {
    let dataByBib = {};
    for(let index in ranking) {
        let dataForBib = {};
        dataForBib.name = ranking[index].name
        dataForBib.ascents = ascentsForBib(ranking[index]);
        let bib = ranking[index].bib ? ranking[index].bib : ranking[index].athlete_id;
        dataByBib[bib] = dataForBib;
    }
    return dataByBib;
}

function ascentsForBib(onePersonData) {
    let ascents = {}
    for(let r in onePersonData.ascents){
        let routeName = onePersonData.ascents[r].route_name;
        let score = onePersonData.ascents[r].score;
        ascents[routeName] = score;
    }
    return ascents;
}

function getResultsPerRoute(routes, resultsByBib) {
    let resultsPerRoute = {};
    for( let r in routes) {
        resultsPerRoute[routes[r]] = {};
    }

    for( let bib in resultsByBib) {
        for( let route in resultsByBib[bib].ascents){

            let score = resultsByBib[bib].ascents[route];
            let currentTotal = resultsPerRoute[route][score] ? resultsPerRoute[route][score]: 0;
            resultsPerRoute[route][score] = currentTotal + 1;
        }
    }

    return resultsPerRoute;
}

function addCharts() {

    for(let index in presentCharts) {
        presentCharts[index].remove();
    }
    presentCharts = [];

    for( let route in resultsPerRoute) {
        presentCharts.push(addChartForRoute(route, resultsPerRoute[route]));
    }

}

function addChartForRoute(routeName, resultsForRoute) {

    let div = document.createElement('div');
    let canvas = document.createElement('canvas');
    let canvasId = "resultsChart-" + routeName;
    canvas.setAttribute('id', canvasId);

    div.appendChild(canvas);
    contentDiv.appendChild(div);

    let data = getBarChartDatasets(resultsForRoute);
    let resultForTrackedBib = null;
    if(bibToTrack in dataByBib) {
        resultForTrackedBib = dataByBib[bibToTrack].ascents[routeName];
    }
    if(resultForTrackedBib) {
        data = augmentDatasetsForTrackedBib(bibToTrack, resultForTrackedBib, data);
    }


    var ctx = document.getElementById(canvasId);

    const chart = new Chart(ctx, {
        type: 'bar',
        data: data,
        options: {
            scales: {
                y: {
                    beginAtZero: true,
                    stacked: true
                },
                x: {
                    stacked: true
                }
            },
            plugins: {
                title: {
                    display: true,
                    text: "Result distribution for Route " + routeName
                }
            }
        }
    });

    return div;
}

function augmentDatasetsForTrackedBib(bibToTrack, resultForTrackedBib, datasets) {
    let index = 0;
    let yPosition = 0;
    if(resultForTrackedBib == "TOP") {
        index = datasets.labels.length - 1;
        let values = datasets.datasets[0].data;
        yPosition = values[index];
    }
    else {
        let parsedPoints = resultForTrackedBib.split(".");
        index = parseInt(parsedPoints[0]);
        let topDatasetIndex = parseInt(parsedPoints.length == 2 ? parsedPoints[1][0]: 0);

        for(let i=0; i<=topDatasetIndex; i=i+1) {
            let values = datasets.datasets[i].data;
            yPosition = yPosition + values[index];
        }
    }



    let scatterDataset = {
        type: 'scatter',
        label: 'Result for BIB ' + bibToTrack,
        data: [{x: datasets.labels[index], y: yPosition}],
        backgroundColor: 'rgb(255, 99, 132)',
        pointRadius: 5,
        order: 1
    };

    for(let i=0; i<3; i=i+1) {
        datasets.datasets[i].order=2;
    }

    datasets.datasets.push(scatterDataset);
    return datasets;

}

function getBarChartDatasets(resultsForRoute) {
    /**
    Aiming at output like
    let data = {
             labels: [1,2,3,4,5],
             datasets: [{
                  label: 'n',
                  data: [12, 19, 4, 8, 5],
              },
              {
                  label: 'n.1',
                  data: [0,0,1,2, 6]
              },
         };
    **/
    let numberOfHolds = getNumberOfHolds(resultsForRoute);
    let labels = Array.from((new Array(numberOfHolds)).keys());
    labels.push("TOP");

    let datasets = [
        Array.from((new Array(numberOfHolds+1)).keys(), x => 0),
        Array.from((new Array(numberOfHolds+1)).keys(), x => 0),
        Array.from((new Array(numberOfHolds+1)).keys(), x => 0)
    ];

    for( let key in resultsForRoute) {
        if(key === "DNS") {
            continue;
        }
        if(key === "TOP") {
            datasets[0][numberOfHolds] = resultsForRoute[key];
            continue;
        }

        let parsedPoints = key.split(".");
        let index = parseInt(parsedPoints[0]);
        let datasetIndex = parseInt(parsedPoints.length == 2 ? parsedPoints[1][0]: 0);
        datasets[datasetIndex][index] = resultsForRoute[key];

    }

    return {
        labels: labels,
        datasets: [
            {
                label: "Hold reached",
                backgroundColor: 'rgba(75, 192, 192, 0.6)',
                data: datasets[0]
            },
            {
                label: "Next hold touched",
                backgroundColor: 'rgba(54, 162, 235, 0.7)',
                data: datasets[1]
            },
            {
                label: "Next hold controlled",
                backgroundColor: 'rgba(153, 102, 255, 0.8)',
                data: datasets[2]
            }
        ]
    }

}

function getNumberOfHolds(resultsForRoute) {
    let max = 0;

    for( let key in resultsForRoute) {
        if(key === "TOP" || key === "DNS") {
            continue;
        }
        let value = parseFloat(key);
        if(value > max) {
            max = value;
        }
    }

    max = max+1; //to account for TOP
    return Math.floor(max);
}