Uiaa Results Visualiser

Plots results achieved by competitors on routes in UIAA competitions.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
}