KDE Store: Graphs

Misc

// ==UserScript==
// @name            KDE Store: Graphs
// @namespace       https://github.com/Zren/
// @description     Misc
// @icon            https://store.kde.org/images_sys/store_logo/kde-store.ico
// @author          Zren
// @version         7
// @match           https://www.opendesktop.org/member/*/plings*
// @match           https://www.opendesktop.org/u/*/plings*
// @match           https://store.kde.org/member/*/plings*
// @match           https://store.kde.org/u/*/plings*
// @grant           none
// @require         https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.min.js
// ==/UserScript==

var el = function(html) {
    var e = document.createElement('div');
    e.innerHTML = html;
    return e.removeChild(e.firstChild);
}

function daysLeftMultiplier() {
    var now = new Date()
    var startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
    var endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
    var totalTime = endOfMonth.valueOf() - startOfMonth.valueOf()
    var timeProcessed = now.valueOf() - startOfMonth.valueOf()
    var timeLeft = endOfMonth.valueOf() - now.valueOf()
    if (timeProcessed == 0) {
        return 1
    } else {
        return 1 + (timeLeft / timeProcessed)
    }

    //var timeLeftInMonth = timeLeft / totalTime
    // return 1 + timeLeftInMonth

    if (timeLeftInMonth <= 0) {
        return 1
    } else {
        return totalTime / timeLeft
    }

}

function zeropad(x, n) {
    var s = '' + x
    for (var i = s.length; i < n; i++) {
        s = '0' + s
    }
    return s
}

function getProductDownloadsForYearMonth(year, month) {
    // OpenDesktop defines:
    //     var json_member = {"member_id":"433956","username":"Zren", ... };

    var yearMonth = '' + year + zeropad(month+1, 2)
    var url = 'https://www.opendesktop.org/member/' + json_member.member_id + '/plingsmonthajax?yearmonth=' + yearMonth
    var cacheKey = 'ProductDownloadsForMonth-' + yearMonth

    var now = new Date()
    var isCurrentMonth = now.getFullYear() == year && now.getMonth() == month

    // Check cache first
    if (localStorage[cacheKey]) {
        var cacheData = JSON.parse(localStorage[cacheKey])
        console.log('Grabbed', cacheKey, 'from localStorage cache')
        return Promise.resolve(cacheData)
    }

    return fetch(url, {
    }).then(function(res){
        return res.text()
    }).then(function(text){
        var monthData = {}
        var root = document.createElement('div')
        root.innerHTML = text
        var myProductList = root.querySelector('.my-products-list')
        var rows = myProductList.querySelectorAll('.tab-pane > .row:not(.row-total)')
        for (var row of rows) {
            var productName = row.children[1].querySelector('span').textContent
            var productDownloads = row.children[2].querySelector('span').textContent
            //console.log('graphData', productName, productDownloads, parseInt(productDownloads, 10))
            productDownloads = parseInt(productDownloads, 10)

            monthData[productName] = productDownloads
        }

        monthData = {
            year: year,
            month: month,
            yearMonth: yearMonth,
            productDownloads: monthData,
        }

        // Save to cache
        if (!isCurrentMonth) { // Don't cache current month
            localStorage[cacheKey] = JSON.stringify(monthData)
        }

        return monthData
    })
}

function getProductDownloadsOverTime() {
    var now = new Date()
    var month = new Date(now.getFullYear(), now.getMonth(), 1)

    var promises = []
    for (var i = 0; i < 12; i++) {
        promises.push(getProductDownloadsForYearMonth(month.getFullYear(), month.getMonth())) // JavaScript's Date.month starts at 0-11
        month.setMonth(month.getMonth() - 1)
    }


    return Promise.all(promises).then(function(values){
        console.log('Promise.all.values', values)
        var graphData = {}
        graphData.labels = new Array(values.length).fill('')
        graphData.products = {}
        for (var monthIndex = 0; monthIndex < values.length; monthIndex++) {
            var monthData = values[monthIndex]
            graphData.labels[monthIndex] = monthData.yearMonth

            for (var productName of Object.keys(monthData.productDownloads)) {
                var productDownloads = monthData.productDownloads[productName]

                var productData = graphData.products[productName]
                if (!graphData.products[productName]) {
                    productData = new Array(values.length).fill(0)
                    graphData.products[productName] = productData
                }

                productData[monthIndex] = productDownloads
            }
        }

        return graphData
    })
}
function randomColor() {
    // Based on the Random Pastel code from StackOverflow
    // https://stackoverflow.com/a/43195379/947742
    return "hsl(" + 360 * Math.random() + ', ' + // Hue: Any
                 (25 + 70 * Math.random()) + '%, ' + // Saturation: 25-95
                 (40 + 30 * Math.random()) + '%)'; // Lightness: 40-70
}

// https://stackoverflow.com/a/44134328/947742
function hslToRgb(h, s, l) {
  h /= 360;
  s /= 100;
  l /= 100;
  var r, g, b;
  if (s === 0) {
    r = g = b = l; // achromatic
  } else {
    function hue2rgb(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    }
    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }
  return { r:r, g:g, b:b }
}
function rgbToHex(c) {
  function toHex(x) {
    const hex = Math.round(x * 255).toString(16);
    return hex.length === 1 ? '0' + hex : hex;
  }
  return '#' + toHex(c.r) + toHex(c.g) + toHex(c.b)
}
function hslToHex(h, s, l) {
  var c = hslToRgb(h, s, l)
  function toHex(x) {
    const hex = Math.round(x * 255).toString(16);
    return hex.length === 1 ? '0' + hex : hex;
  }
  return '#' + toHex(c.r) + toHex(c.g) + toHex(c.b)
}
function rgbToRgba(c, a) {
  return 'rgba(' + Math.round(c.r * 255) + ', ' + Math.round(c.g * 255) + ', ' + Math.round(c.b * 255) + ', ' + a + ')'
}
function getPastelColor(i, n) {
    return hslToRgb(i / n * 360, 55, 70)
}

function convertToDatasets(graphData) {
    var datasets = []
    var productNameList = Object.keys(graphData.products)
    productNameList.sort()

    for (var i = 0; i < productNameList.length; i++) {
        var productName = productNameList[i]
        var productData = graphData.products[productName]
        var dataset = {}
        dataset.label = productName
        dataset.data = Array.from(productData).reverse()
        dataset.fill = false
        var datasetColor = getPastelColor(i, productNameList.length)
        dataset.accentColor = rgbToHex(datasetColor)
        dataset.accentFadedColor = rgbToRgba(datasetColor, 0.2)
        dataset.backgroundColor = dataset.accentColor
        dataset.borderColor = dataset.accentColor
        dataset.lineTension = 0.1
        datasets.push(dataset)
    }
    return datasets
}

function buildGraph(graphData) {
    console.log('graphData', graphData)

    window.graphData = graphData
    var datasets = window.datasets = convertToDatasets(graphData)

    //var labels = document.querySelectorAll('#my-payout-list ul.nav-tabs li a')
    //labels = Array.prototype.map.call(labels, function(e){ return e.textContent })
    //labels = labels.reverse()

    //var labels = new Array(3).fill('Month')
    var labels = graphData.labels
    labels = labels.reverse()

    console.log('datasets', JSON.stringify(datasets))
    console.log('labels', JSON.stringify(labels))

    var graphParent = document.querySelector('.my-products-heading')
    var graphContainer = el('<div id="graphs" />')
    var graphCanvas = el('<canvas id="myChart" width="100vw" height="30vh"></canvas>')
    graphContainer.appendChild(graphCanvas)

    graphParent.parentNode.insertBefore(graphContainer, graphParent)

    //var navTabs = document.querySelector('#my-payout-list ul.nav-tabs')
    //var graphTab = el('<li><a href="#graphs" data-toggle="tab">Graphs</a></li>')
    //navTabs.insertBefore(graphTab, navTabs.firstChild)

    var ctx = document.getElementById("myChart").getContext("2d");
    var myChart = window.myChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: labels,
            datasets: datasets,
        },
        options: {
            title: {
                display: true,
                text: 'Product Downloads Over Time',
            },
            tooltips: {
                mode: 'index',
                intersect: false,
                itemSort: function (a, b, data) {
                    return b.yLabel - a.yLabel // descending
                }
            },
            legend: {
                position: 'left',
                onHover: function(e, legendItem) {
                    if (myChart.hoveringLegendIndex != legendItem.datasetIndex) {
                        myChart.hoveringLegendIndex = legendItem.datasetIndex
                        for (var i = 0; i < myChart.data.datasets.length; i++) {
                            var dataset = myChart.data.datasets[i]
                            if (i == legendItem.datasetIndex) {
                                dataset.borderColor = dataset.accentColor
                                dataset.pointBackgroundColor = dataset.accentColor
                            } else {
                                dataset.borderColor = dataset.accentFadedColor
                                dataset.pointBackgroundColor = dataset.accentFadedColor
                            }
                        }
                        myChart.options.tooltips.enabled = false
                        myChart.update()
                    }
                }
            },
            hover: {
                mode: 'nearest',
                intersect: true,
            },
            scales: {
                yAxes: [{
                    //type: 'logarithmic',
                    ticks: {
                        //stepSize: 5,
                        //beginAtZero:true,
                    }
                }]
            }
        }
    });

    myChart.hoveringLegendIndex = -1
    myChart.canvas.addEventListener('mousemove', function(e) {
        if (myChart.hoveringLegendIndex >= 0) {
            if (e.layerX < myChart.legend.left || myChart.legend.right < e.layerX
                || e.layerY < myChart.legend.top || myChart.legend.bottom < e.layerY
               ) {
                myChart.hoveringLegendIndex = -1
                for (var i = 0; i < myChart.data.datasets.length; i++) {
                    var dataset = myChart.data.datasets[i]
                    dataset.borderColor = dataset.accentColor
                    dataset.pointBackgroundColor = dataset.accentColor
                }
                myChart.options.tooltips.enabled = true
                myChart.update()
            }
        }
    })
}


function main() {
    getProductDownloadsOverTime().then(function(graphData){
        buildGraph(graphData)
    })
}

main()