// ==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()