AO3 Statistics tracker

Track statistics on the stats page and display the stats in charts. Stores the stats in the browser's local storage.

// ==UserScript==
// @name         AO3 Statistics tracker
// @description  Track statistics on the stats page and display the stats in charts. Stores the stats in the browser's local storage.
// @author       Ifky_
// @namespace    https://greasyfork.org/en/scripts/518523
// @version      1.0.0
// @history      1.0.0 — Track stats, draw charts, download chart, clean stats, import/export stats, toggle view
// @match        https://archiveofourown.org/users/*/stats*
// @match        http://archiveofourown.org/users/*/stats*
// @icon         https://archiveofourown.org/images/logo.png
// @license      GPL-3.0-only
// @grant        none
// ==/UserScript==
(function () {
    "use strict";
    /** ==================================== **/
    /*  FLAGS MEANT TO BE EDITED: CAPITALIZED */
    /*  Keep in mind that these might be      */
    /*  reset if the script is auto-updated,  */
    /*  so create a backup somewhere safe     */
    /** ==================================== **/
    // Clean data by removing unnecessary points.
    // Options: "day" / "hour" / "month"
    const CLEAN_DATA = "hour";
    // Theme mode for the chart.
    // Options: "light" / "dark"
    const THEME_MODE = "light";
    // Whether to include or exclude work stats
    // Can be a good idea to turn off if you have many works
    // Options: true / false
    const INCLUDE_WORKS = true;
    // The sign used to separate values
    // Change it to something else if it clashes with a work name
    // Options: any string
    const DELIMITER = ";";
    const getTheme = (mode) => {
        if (mode === "dark") {
            return {
                text: "#999",
                background: "#222",
                gridLines: "#333",
                userSubscriptions: "#F94144",
                kudos: "#F3722C",
                commentThreads: "#F9C74F",
                bookmarks: "#90BE6D",
                subscriptions: "#43AA8B",
                wordCount: "#6C8EAD",
                hits: "#8552BA",
            };
        }
        else {
            // Default to light mode
            return {
                text: "#777",
                background: "#FFF",
                gridLines: "#DDD",
                userSubscriptions: "#F94144",
                kudos: "#F3722C",
                commentThreads: "#F9C74F",
                bookmarks: "#90BE6D",
                subscriptions: "#43AA8B",
                wordCount: "#577590",
                hits: "#663C91",
            };
        }
    };
    const sleep = (ms) => {
        return new Promise((resolve) => setTimeout(resolve, ms));
    };
    const trackStats = () => {
        const newTotalStats = getTotalStats();
        storeInLocalStorage(newTotalStats);
        if (INCLUDE_WORKS) {
            const newWorkStats = getWorkStats();
            storeInLocalStorage(newWorkStats);
        }
        drawChart(chartContainer, canvasTotal, "total");
    };
    const exportStats = () => {
        let csvContent = "data:text/csv;charset=utf-8,";
        // Headers
        csvContent += "type;workName;statName;date;value\r\n";
        const keys = Object.keys(localStorage).filter((key) => isValidKey(key));
        keys.forEach((key, index) => {
            // Add \r\n to every row but the last one
            csvContent +=
                [key, localStorage.getItem(key)].join(DELIMITER) +
                    (index !== keys.length - 1 ? "\r\n" : "");
        });
        var encodedUri = encodeURI(csvContent);
        var link = document.createElement("a");
        link.href = encodedUri;
        link.download = `stats-${formatDate(new Date()).yyyyMMdd}.csv`;
        link.click();
    };
    const importStats = (event) => {
        const target = event.target;
        if (target instanceof HTMLInputElement) {
            Array.from(target.files).forEach((file) => {
                csvFileToStatsRow(file)
                    .then((rows) => {
                    storeInLocalStorage(rows);
                    alert(`${rows.length} rows imported!`);
                    drawChart(chartContainer, canvasTotal, "total");
                })
                    .catch((error) => {
                    alert(error);
                });
            });
            // Reset input
            target.value = "";
        }
    };
    const csvFileToStatsRow = (file) => {
        return new Promise((resolve, reject) => {
            // Check if the file is of .csv type
            if (file.type !== "text/csv" && !file.name.endsWith(".csv")) {
                reject(new Error("The file must be a .csv file."));
            }
            const reader = new FileReader();
            // Handle the file load event
            reader.onload = () => {
                if (typeof reader.result === "string") {
                    const lines = reader.result.split("\r\n");
                    // Validate header
                    const header = lines[0];
                    const [headerType, headerWorkName, headerStatName, headerDate, headerValue,] = header.split(DELIMITER);
                    if (headerType !== "type" ||
                        headerWorkName !== "workName" ||
                        headerStatName !== "statName" ||
                        headerDate !== "date" ||
                        headerValue !== "value") {
                        reject(new Error(`Header(s) could not be inferred: ${header}`));
                    }
                    const rows = [];
                    for (let i = 1; i < lines.length; i++) {
                        const line = lines[i];
                        const [type, workName, statName, date, value] = line.split(DELIMITER);
                        try {
                            validateStatRow({
                                type,
                                workName,
                                statName,
                                date,
                                value,
                            }, i);
                            const row = {
                                type: type,
                                workName: workName,
                                statName: statName,
                                date: new Date(date),
                                value: Number(value),
                            };
                            rows.push(row);
                        }
                        catch (error) {
                            reject(error);
                        }
                    }
                    resolve(rows);
                }
                else {
                    reject(new Error("File content could not be read as text."));
                }
            };
            reader.onerror = () => {
                reject(new Error("An error occurred while reading the file."));
            };
            reader.readAsText(file);
        });
    };
    let statChart = null;
    let statisticsMetaGroup = null;
    let statisticsIndexGroup = null;
    let canvasTotal = null;
    let chartWidth = 300;
    const chartHeight = 250;
    const chartContainer = document.createElement("div");
    chartContainer.id = "chart-container-total";
    chartContainer.style.marginBlock = "5px";
    chartContainer.style.transition = "height 500ms ease-in-out";
    chartContainer.style.overflow = "hidden";
    const toggleChartContainer = (container, open) => {
        if (open === true) {
            container.style.height = `${chartHeight}px`;
            return;
        }
        else if (open === false) {
            container.style.height = "0px";
            return;
        }
        if (container.style.height === "0px") {
            container.style.height = `${chartHeight}px`;
        }
        else {
            container.style.height = "0px";
        }
    };
    const observer = new MutationObserver(() => {
        statChart = document.getElementById("stat_chart");
        statisticsMetaGroup = document.querySelector(".statistics.meta.group");
        statisticsIndexGroup = document.querySelector(".statistics.index.group");
        if (statChart && statisticsMetaGroup && statisticsIndexGroup) {
            observer.disconnect(); // Stop observing once the element is found
            chartWidth = Math.round(statChart.offsetWidth);
            chartContainer.style.width = `${chartWidth}px`;
            chartContainer.style.height = `0px`;
            statChart.prepend(chartContainer);
            canvasTotal = document.createElement("canvas");
            canvasTotal.id = "tracked-stats-chart";
            canvasTotal.width = chartWidth;
            canvasTotal.height = chartHeight;
            canvasTotal.style.display = "block";
            canvasTotal.style.backgroundColor = "#8888";
            chartContainer.append(canvasTotal);
            // Append scripts for chart
            const chartScript = document.createElement("script");
            chartScript.src = "https://cdn.jsdelivr.net/npm/chart.js";
            statChart.appendChild(chartScript);
            chartScript.onload = () => {
                const chartDateScript = document.createElement("script");
                chartDateScript.src =
                    "https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns";
                statChart.appendChild(chartDateScript);
                chartDateScript.onload = () => {
                    const buttonContainer = document.createElement("div");
                    buttonContainer.style.width = "100%";
                    buttonContainer.style.display = "flex";
                    buttonContainer.style.justifyContent = "flex-end";
                    buttonContainer.style.flexWrap = "wrap";
                    buttonContainer.style.gap = "1em";
                    buttonContainer.style.paddingInline = "1em";
                    buttonContainer.style.boxSizing = "border-box";
                    statisticsMetaGroup.append(buttonContainer);
                    const chartExists = () => {
                        // eslint-disable-next-line
                        const chartStatus = Chart.getChart("tracked-stats-chart");
                        return chartStatus !== undefined;
                    };
                    const downloadChart = async () => {
                        if (!chartExists) {
                            drawChart(chartContainer, canvasTotal, "total");
                            // Sleep to allow chart to be drawn
                            await sleep(500);
                        }
                        const url = canvasTotal.toDataURL("image/png");
                        var a = document.createElement("a");
                        a.href = url;
                        a.download = `stats-chart-${formatDate(new Date()).yyyyMMdd}.png`;
                        a.click();
                    };
                    // Button to track statistics
                    const logButton = document.createElement("button");
                    logButton.style.cursor = "pointer";
                    logButton.innerText = "Track";
                    logButton.onclick = trackStats;
                    buttonContainer.append(logButton);
                    // Button to draw chart
                    const drawButton = document.createElement("button");
                    drawButton.style.cursor = "pointer";
                    drawButton.innerText = "Draw Chart";
                    drawButton.onclick = () => drawChart(chartContainer, canvasTotal, "total");
                    buttonContainer.append(drawButton);
                    // Button to download chart
                    const downloadButton = document.createElement("button");
                    downloadButton.style.cursor = "pointer";
                    downloadButton.innerText = "Download";
                    downloadButton.onclick = downloadChart;
                    buttonContainer.append(downloadButton);
                    // Button to clean stats
                    const cleanButton = document.createElement("button");
                    cleanButton.style.cursor = "pointer";
                    cleanButton.innerText = "Clean Stats";
                    cleanButton.onclick = () => cleanStats(CLEAN_DATA);
                    buttonContainer.append(cleanButton);
                    // Button to export stats
                    const exportButton = document.createElement("button");
                    exportButton.style.cursor = "pointer";
                    exportButton.innerText = "Export";
                    exportButton.onclick = exportStats;
                    buttonContainer.append(exportButton);
                    // Actions container (for styling purposes)
                    const actionsContainer = document.createElement("div");
                    actionsContainer.classList.add("actions");
                    buttonContainer.append(actionsContainer);
                    // Input to import stats
                    const importInput = document.createElement("input");
                    importInput.id = "import-stats-input";
                    importInput.type = "file";
                    importInput.accept = ".csv";
                    importInput.multiple = true;
                    importInput.style.height = "0";
                    importInput.style.width = "0";
                    importInput.style.overflow = "hidden !important";
                    importInput.style.opacity = "0";
                    importInput.style.position = "absolute";
                    importInput.style.zIndex = "-100";
                    importInput.onchange = importStats;
                    actionsContainer.append(importInput);
                    // Label to import stats
                    const importLabel = document.createElement("label");
                    importLabel.htmlFor = "import-stats-input";
                    importLabel.style.cursor = "pointer";
                    importLabel.style.margin = "0";
                    importLabel.innerText = "Import";
                    importLabel.classList.add("button");
                    actionsContainer.append(importLabel);
                    // Toggle to hide/show chart
                    const toggleLabel = document.createElement("button");
                    toggleLabel.style.cursor = "pointer";
                    toggleLabel.innerText = "Toggle View";
                    toggleLabel.onclick = () => toggleChartContainer(chartContainer);
                    buttonContainer.append(toggleLabel);
                    // Buttons for each work
                    const workElements = statisticsIndexGroup.querySelectorAll(".index.group>li:not(.group)");
                    workElements.forEach((item) => {
                        const workName = item.querySelector("dt>a:link").innerHTML;
                        const chartContainer = document.createElement("div");
                        chartContainer.id = `chart-container:${workName}`;
                        chartContainer.style.marginBlock = "5px";
                        chartContainer.style.transition = "height 500ms ease-in-out";
                        chartContainer.style.overflow = "hidden";
                        chartContainer.style.width = `100%`;
                        chartContainer.style.height = `0px`;
                        item.append(chartContainer);
                        const canvasWork = document.createElement("canvas");
                        canvasWork.id = `stats-chart:${workName}`;
                        canvasWork.width = item.offsetWidth;
                        canvasWork.height = chartHeight;
                        canvasWork.style.display = "block";
                        canvasWork.style.backgroundColor = "#8888";
                        chartContainer.append(canvasWork);
                        const buttonContainer = document.createElement("div");
                        buttonContainer.style.width = "100%";
                        buttonContainer.style.display = "flex";
                        buttonContainer.style.justifyContent = "flex-end";
                        buttonContainer.style.flexWrap = "wrap";
                        buttonContainer.style.gap = "1em";
                        buttonContainer.style.paddingInline = "1em";
                        buttonContainer.style.boxSizing = "border-box";
                        const drawButton = document.createElement("button");
                        drawButton.style.cursor = "pointer";
                        drawButton.innerText = "Draw";
                        drawButton.onclick = () => drawChart(chartContainer, canvasWork, "work", workName);
                        buttonContainer.append(drawButton);
                        const toggleLabel = document.createElement("button");
                        toggleLabel.style.cursor = "pointer";
                        toggleLabel.innerText = "Toggle";
                        toggleLabel.onclick = () => toggleChartContainer(chartContainer);
                        buttonContainer.append(toggleLabel);
                        const dt = item.querySelector("dt");
                        dt.append(buttonContainer);
                    });
                };
            };
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
    const drawChart = async (container, canvas, type, workName) => {
        // If chart is hidden, show it
        if (container.style.height === "0px") {
            toggleChartContainer(container, true);
            await sleep(1000);
        }
        let storageStats = getStatsFromLocalStorage().filter((item) => item.type === type);
        if (workName) {
            storageStats = storageStats.filter((item) => item.workName === workName);
        }
        const { datasets } = statRowToDataSet(storageStats, type);
        // Destroy existing chart
        // eslint-disable-next-line
        const chartStatus = Chart.getChart(canvas.id);
        if (chartStatus !== undefined) {
            chartStatus.destroy();
        }
        // eslint-disable-next-line
        new Chart(canvas.getContext("2d"), {
            type: "line",
            data: {
                datasets: datasets,
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    x: {
                        type: "time",
                        time: {
                            unit: "day",
                        },
                        distribution: "linear",
                        grid: {
                            color: getTheme(THEME_MODE).gridLines,
                        },
                        ticks: {
                            color: getTheme(THEME_MODE).text,
                        },
                    },
                    y: {
                        ticks: {
                            precision: 0,
                            color: getTheme(THEME_MODE).text,
                        },
                        grid: {
                            color: getTheme(THEME_MODE).gridLines,
                        },
                    },
                },
                plugins: {
                    customCanvasBackgroundColor: {
                        color: getTheme(THEME_MODE).background,
                    },
                    tooltip: {
                        callbacks: {
                            title: function (context) {
                                const date = new Date(context[0].parsed.x);
                                return date.toLocaleString("en-GB", {
                                    year: "numeric",
                                    month: "long",
                                    day: "numeric",
                                    hour: "2-digit",
                                    minute: "2-digit",
                                    hour12: false, // Ensure 24-hour clock
                                });
                            },
                        },
                    },
                },
            },
            plugins: [plugin],
        });
    };
    const getStatKey = (type, workName, statName, date) => {
        return `${type};${workName};${statName};${date}`;
    };
    // Element names (arbitrary) and their DOM identifier
    const elementsTotal = new Map([
        [
            "user-subscriptions",
            {
                selector: "dd.user.subscriptions",
                color: getTheme(THEME_MODE).userSubscriptions,
                hidden: true,
            },
        ],
        [
            "kudos",
            {
                selector: "dd.kudos",
                color: getTheme(THEME_MODE).kudos,
                hidden: true,
            },
        ],
        [
            "comment-threads",
            {
                selector: "dd.comment.thread",
                color: getTheme(THEME_MODE).commentThreads,
                hidden: true,
            },
        ],
        [
            "bookmarks",
            {
                selector: "dd.bookmarks",
                color: getTheme(THEME_MODE).bookmarks,
                hidden: true,
            },
        ],
        [
            "subscriptions",
            {
                selector: "dd.subscriptions:not(.user)",
                color: getTheme(THEME_MODE).subscriptions,
                hidden: true,
            },
        ],
        [
            "word-count",
            {
                selector: "dd.words",
                color: getTheme(THEME_MODE).wordCount,
                hidden: true,
            },
        ],
        [
            "hits",
            {
                selector: "dd.hits",
                color: getTheme(THEME_MODE).hits,
                hidden: false,
            },
        ],
    ]);
    const elementsWork = new Map([
        [
            "kudos",
            {
                selector: "dd.kudos",
                color: getTheme(THEME_MODE).kudos,
                hidden: true,
            },
        ],
        [
            "comments",
            {
                selector: "dd.comments",
                color: getTheme(THEME_MODE).commentThreads,
                hidden: true,
            },
        ],
        [
            "bookmarks",
            {
                selector: "dd.bookmarks",
                color: getTheme(THEME_MODE).bookmarks,
                hidden: true,
            },
        ],
        [
            "subscriptions",
            {
                selector: "dd.subscriptions",
                color: getTheme(THEME_MODE).subscriptions,
                hidden: true,
            },
        ],
        [
            "words",
            {
                selector: "span.words",
                color: getTheme(THEME_MODE).wordCount,
                hidden: true,
            },
        ],
        [
            "hits",
            {
                selector: "dd.hits",
                color: getTheme(THEME_MODE).hits,
                hidden: false,
            },
        ],
    ]);
    const getTotalStats = () => {
        const stats = [];
        elementsTotal.forEach((value, key) => {
            const valueString = statisticsMetaGroup.querySelector(value.selector).innerHTML;
            // Regex to remove any non-digit symbols
            const valueNumber = Number(valueString.replace(/\D/g, ""));
            stats.push({
                type: "total",
                workName: "",
                statName: key,
                date: new Date(),
                value: valueNumber,
            });
        });
        return stats;
    };
    const getWorkStats = () => {
        const stats = [];
        const workElements = statisticsIndexGroup.querySelectorAll(".index.group>li:not(.group)");
        workElements.forEach((elem) => {
            const workName = elem.querySelector("dt>a:link").innerHTML;
            elementsWork.forEach((value, key) => {
                const valueString = elem.querySelector(value.selector);
                // Some stats might not exist on a work. Skip them
                if (valueString) {
                    // Regex to remove any non-digit symbols
                    const valueNumber = Number(valueString.innerHTML.replace(/\D/g, ""));
                    stats.push({
                        type: "work",
                        workName: workName,
                        statName: key,
                        date: new Date(),
                        value: valueNumber,
                    });
                }
            });
        });
        return stats;
    };
    const storeInLocalStorage = (stats) => {
        stats.forEach((stat, index) => {
            const key = getStatKey(stat.type, stat.workName, stat.statName, formatDate(stat.date).yyyyMMdd_hhmm);
            localStorage.setItem(key, stat.value.toString());
        });
    };
    const isValidKey = (key) => {
        return key.startsWith("work") || key.startsWith("total");
    };
    const getStatsFromLocalStorage = () => {
        const rows = [];
        const keys = Object.keys(localStorage);
        let i = keys.length;
        while (i--) {
            if (isValidKey(keys[i])) {
                const [type, workName, statName, date] = keys[i].split(DELIMITER);
                rows.push({
                    type: type,
                    workName: workName,
                    statName: statName,
                    date: new Date(date),
                    value: Number(localStorage.getItem(keys[i])),
                });
            }
        }
        return rows.sort((a, b) => a.date.getTime() - b.date.getTime());
    };
    const kebabToTitleCase = (input) => {
        return input
            .split("-")
            .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
            .join(" ");
    };
    const formatDate = (date) => {
        // Helper to pad single digits with leading zero
        const pad = (num) => num.toString().padStart(2, "0");
        const year = date.getFullYear();
        const month = pad(date.getMonth() + 1); // Months are 0-indexed
        const day = pad(date.getDate());
        const hours = pad(date.getHours());
        const minutes = pad(date.getMinutes());
        return {
            yyyyMMdd_hhmm: `${year}-${month}-${day} ${hours}:${minutes}`,
            yyyyMMdd_hh: `${year}-${month}-${day} ${hours}`,
            yyyyMMdd: `${year}-${month}-${day}`,
            yyyyMM: `${year}-${month}`,
        };
    };
    const statRowToDataSet = (stats, type) => {
        const dataset = new Map();
        let keys = [];
        switch (type) {
            case "total":
                keys = Array.from(elementsTotal.keys());
                break;
            case "work":
                keys = Array.from(elementsWork.keys());
                break;
        }
        // Find each stat type
        keys.forEach((key) => {
            const list = [];
            // Get the stats for the stat type
            stats.forEach((stat) => {
                if (stat.statName === key) {
                    list.push([stat.date, stat.value]);
                }
            });
            // Convert keys from kebab-case to Title Case
            dataset.set(kebabToTitleCase(key), list);
        });
        return {
            datasets: Array.from(dataset).map((data, index) => {
                const values = data[1].map((entry) => {
                    return {
                        x: formatDate(entry[0]).yyyyMMdd_hhmm,
                        y: entry[1],
                    };
                });
                let backgroundColor = "";
                let hidden = false;
                switch (type) {
                    case "total":
                        backgroundColor = elementsTotal.get(keys[index]).color;
                        hidden = elementsTotal.get(keys[index]).hidden;
                        break;
                    case "work":
                        backgroundColor = elementsWork.get(keys[index]).color;
                        hidden = elementsWork.get(keys[index]).hidden;
                        break;
                }
                return {
                    label: data[0],
                    data: values,
                    borderWidth: 2,
                    // Set border color same as background except with less opacity
                    borderColor: `${backgroundColor}88`,
                    backgroundColor: backgroundColor,
                    hidden: hidden,
                };
            }),
        };
    };
    const cleanStats = (mode) => {
        const keys = Object.keys(localStorage).filter((key) => isValidKey(key));
        const dataPoints = new Map(keys.map((key) => {
            const [type, workName, statName, date] = key.split(DELIMITER);
            return [
                key,
                {
                    type,
                    workName,
                    statName,
                    date: new Date(date),
                },
            ];
        }));
        const toKeep = new Map();
        const toDelete = new Set();
        const findAndSortData = (value, key, shortDate) => {
            const shortKey = getStatKey(value.type, value.workName, value.statName, shortDate);
            const storedVal = toKeep.get(shortKey);
            // If not stored, store it
            if (!storedVal) {
                toKeep.set(shortKey, {
                    fullDate: value.date,
                    key,
                });
            }
            else if (value.date > storedVal.fullDate) {
                // If current date is later than stored date
                // Move stored item to delete
                toDelete.add(storedVal.key);
                // Set new stored item
                toKeep.set(shortKey, {
                    fullDate: value.date,
                    key,
                });
            }
            else if (value.date < storedVal.fullDate) {
                // If current date is before stored date
                // Set it to be deleted
                toDelete.add(key);
            }
        };
        if (mode === "hour") {
            dataPoints.forEach((value, key) => {
                findAndSortData(value, key, formatDate(value.date).yyyyMMdd_hh);
            });
        }
        else if (mode === "day") {
            dataPoints.forEach((value, key) => {
                findAndSortData(value, key, formatDate(value.date).yyyyMMdd);
            });
        }
        else if (mode === "month") {
            dataPoints.forEach((value, key) => {
                findAndSortData(value, key, formatDate(value.date).yyyyMM);
            });
        }
        toDelete.forEach((item) => {
            localStorage.removeItem(item);
        });
        drawChart(chartContainer, canvasTotal, "total");
    };
    const validateStatRow = (row, index) => {
        if (row.type !== "total" && row.type !== "work") {
            throw new Error(`Type "${row.type}" for row ${index} not recognized.`);
        }
        if (row.type !== "work" && row.workName !== "") {
            throw new Error(`Work name "${row.workName}" was found for row ${index}, but the type is not "work"`);
        }
        else if (row.type === "work" && row.workName === "") {
            throw new Error(`Type of row ${index} is "work", but work name is empty.`);
        }
        if (!Array.from(elementsTotal.keys()).includes(row.statName) &&
            !Array.from(elementsWork.keys()).includes(row.statName)) {
            throw new Error(`Stat name "${row.statName}" for row ${index} not recognized.`);
        }
        if (!new Date(row.date).getDate()) {
            throw new Error(`Date "${row.date}" for row ${index} is invalid.`);
        }
        if (!row.value || Number(row.value) < 0) {
            throw new Error(`Value "${row.value}" for row ${index} is invalid.`);
        }
        return true;
    };
    const plugin = {
        id: "customCanvasBackgroundColor",
        beforeDraw: (chart, _, options) => {
            const { ctx: context } = chart;
            context.save();
            context.globalCompositeOperation = "destination-over";
            context.fillStyle = options.color || "white";
            context.fillRect(0, 0, chart.width, chart.height);
            context.restore();
        },
    };
    // Style buttons to match skin
    const sheets = document.styleSheets;
    for (const sheet of sheets) {
        try {
            const rules = sheet.cssRules;
            for (const rule of rules) {
                if (rule instanceof CSSStyleRule &&
                    rule.selectorText.includes("button:focus")) {
                    const newSelector = `${rule.selectorText}, #import-stats-input:focus+label`;
                    sheet.deleteRule([...rules].indexOf(rule));
                    sheet.insertRule(`${newSelector} { ${rule.style.cssText} }`, rules.length);
                    break;
                }
            }
            for (const rule of rules) {
                if (rule instanceof CSSStyleRule &&
                    rule.selectorText.includes(".actions label:hover")) {
                    const newSelector = `${rule.selectorText}, button:hover`;
                    sheet.deleteRule([...rules].indexOf(rule));
                    sheet.insertRule(`${newSelector} { ${rule.style.cssText} }`, rules.length);
                    break;
                }
            }
        }
        catch (e) {
            //
        }
    }
})();