KDE Store: Graphs

Misc

  1. // ==UserScript==
  2. // @name KDE Store: Graphs
  3. // @namespace https://github.com/Zren/
  4. // @description Misc
  5. // @icon https://store.kde.org/images_sys/store_logo/kde-store.ico
  6. // @author Zren
  7. // @version 7
  8. // @match https://www.opendesktop.org/member/*/plings*
  9. // @match https://www.opendesktop.org/u/*/plings*
  10. // @match https://store.kde.org/member/*/plings*
  11. // @match https://store.kde.org/u/*/plings*
  12. // @grant none
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.min.js
  14. // ==/UserScript==
  15.  
  16. var el = function(html) {
  17. var e = document.createElement('div');
  18. e.innerHTML = html;
  19. return e.removeChild(e.firstChild);
  20. }
  21.  
  22. function daysLeftMultiplier() {
  23. var now = new Date()
  24. var startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
  25. var endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
  26. var totalTime = endOfMonth.valueOf() - startOfMonth.valueOf()
  27. var timeProcessed = now.valueOf() - startOfMonth.valueOf()
  28. var timeLeft = endOfMonth.valueOf() - now.valueOf()
  29. if (timeProcessed == 0) {
  30. return 1
  31. } else {
  32. return 1 + (timeLeft / timeProcessed)
  33. }
  34.  
  35. //var timeLeftInMonth = timeLeft / totalTime
  36. // return 1 + timeLeftInMonth
  37.  
  38. if (timeLeftInMonth <= 0) {
  39. return 1
  40. } else {
  41. return totalTime / timeLeft
  42. }
  43.  
  44. }
  45.  
  46. function zeropad(x, n) {
  47. var s = '' + x
  48. for (var i = s.length; i < n; i++) {
  49. s = '0' + s
  50. }
  51. return s
  52. }
  53.  
  54. function getProductDownloadsForYearMonth(year, month) {
  55. // OpenDesktop defines:
  56. // var json_member = {"member_id":"433956","username":"Zren", ... };
  57.  
  58. var yearMonth = '' + year + zeropad(month+1, 2)
  59. var url = 'https://www.opendesktop.org/member/' + json_member.member_id + '/plingsmonthajax?yearmonth=' + yearMonth
  60. var cacheKey = 'ProductDownloadsForMonth-' + yearMonth
  61.  
  62. var now = new Date()
  63. var isCurrentMonth = now.getFullYear() == year && now.getMonth() == month
  64.  
  65. // Check cache first
  66. if (localStorage[cacheKey]) {
  67. var cacheData = JSON.parse(localStorage[cacheKey])
  68. console.log('Grabbed', cacheKey, 'from localStorage cache')
  69. return Promise.resolve(cacheData)
  70. }
  71.  
  72. return fetch(url, {
  73. }).then(function(res){
  74. return res.text()
  75. }).then(function(text){
  76. var monthData = {}
  77. var root = document.createElement('div')
  78. root.innerHTML = text
  79. var myProductList = root.querySelector('.my-products-list')
  80. var rows = myProductList.querySelectorAll('.tab-pane > .row:not(.row-total)')
  81. for (var row of rows) {
  82. var productName = row.children[1].querySelector('span').textContent
  83. var productDownloads = row.children[2].querySelector('span').textContent
  84. //console.log('graphData', productName, productDownloads, parseInt(productDownloads, 10))
  85. productDownloads = parseInt(productDownloads, 10)
  86.  
  87. monthData[productName] = productDownloads
  88. }
  89.  
  90. monthData = {
  91. year: year,
  92. month: month,
  93. yearMonth: yearMonth,
  94. productDownloads: monthData,
  95. }
  96.  
  97. // Save to cache
  98. if (!isCurrentMonth) { // Don't cache current month
  99. localStorage[cacheKey] = JSON.stringify(monthData)
  100. }
  101.  
  102. return monthData
  103. })
  104. }
  105.  
  106. function getProductDownloadsOverTime() {
  107. var now = new Date()
  108. var month = new Date(now.getFullYear(), now.getMonth(), 1)
  109.  
  110. var promises = []
  111. for (var i = 0; i < 12; i++) {
  112. promises.push(getProductDownloadsForYearMonth(month.getFullYear(), month.getMonth())) // JavaScript's Date.month starts at 0-11
  113. month.setMonth(month.getMonth() - 1)
  114. }
  115.  
  116.  
  117. return Promise.all(promises).then(function(values){
  118. console.log('Promise.all.values', values)
  119. var graphData = {}
  120. graphData.labels = new Array(values.length).fill('')
  121. graphData.products = {}
  122. for (var monthIndex = 0; monthIndex < values.length; monthIndex++) {
  123. var monthData = values[monthIndex]
  124. graphData.labels[monthIndex] = monthData.yearMonth
  125.  
  126. for (var productName of Object.keys(monthData.productDownloads)) {
  127. var productDownloads = monthData.productDownloads[productName]
  128.  
  129. var productData = graphData.products[productName]
  130. if (!graphData.products[productName]) {
  131. productData = new Array(values.length).fill(0)
  132. graphData.products[productName] = productData
  133. }
  134.  
  135. productData[monthIndex] = productDownloads
  136. }
  137. }
  138.  
  139. return graphData
  140. })
  141. }
  142. function randomColor() {
  143. // Based on the Random Pastel code from StackOverflow
  144. // https://stackoverflow.com/a/43195379/947742
  145. return "hsl(" + 360 * Math.random() + ', ' + // Hue: Any
  146. (25 + 70 * Math.random()) + '%, ' + // Saturation: 25-95
  147. (40 + 30 * Math.random()) + '%)'; // Lightness: 40-70
  148. }
  149.  
  150. // https://stackoverflow.com/a/44134328/947742
  151. function hslToRgb(h, s, l) {
  152. h /= 360;
  153. s /= 100;
  154. l /= 100;
  155. var r, g, b;
  156. if (s === 0) {
  157. r = g = b = l; // achromatic
  158. } else {
  159. function hue2rgb(p, q, t) {
  160. if (t < 0) t += 1;
  161. if (t > 1) t -= 1;
  162. if (t < 1 / 6) return p + (q - p) * 6 * t;
  163. if (t < 1 / 2) return q;
  164. if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
  165. return p;
  166. }
  167. var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  168. var p = 2 * l - q;
  169. r = hue2rgb(p, q, h + 1 / 3);
  170. g = hue2rgb(p, q, h);
  171. b = hue2rgb(p, q, h - 1 / 3);
  172. }
  173. return { r:r, g:g, b:b }
  174. }
  175. function rgbToHex(c) {
  176. function toHex(x) {
  177. const hex = Math.round(x * 255).toString(16);
  178. return hex.length === 1 ? '0' + hex : hex;
  179. }
  180. return '#' + toHex(c.r) + toHex(c.g) + toHex(c.b)
  181. }
  182. function hslToHex(h, s, l) {
  183. var c = hslToRgb(h, s, l)
  184. function toHex(x) {
  185. const hex = Math.round(x * 255).toString(16);
  186. return hex.length === 1 ? '0' + hex : hex;
  187. }
  188. return '#' + toHex(c.r) + toHex(c.g) + toHex(c.b)
  189. }
  190. function rgbToRgba(c, a) {
  191. return 'rgba(' + Math.round(c.r * 255) + ', ' + Math.round(c.g * 255) + ', ' + Math.round(c.b * 255) + ', ' + a + ')'
  192. }
  193. function getPastelColor(i, n) {
  194. return hslToRgb(i / n * 360, 55, 70)
  195. }
  196.  
  197. function convertToDatasets(graphData) {
  198. var datasets = []
  199. var productNameList = Object.keys(graphData.products)
  200. productNameList.sort()
  201.  
  202. for (var i = 0; i < productNameList.length; i++) {
  203. var productName = productNameList[i]
  204. var productData = graphData.products[productName]
  205. var dataset = {}
  206. dataset.label = productName
  207. dataset.data = Array.from(productData).reverse()
  208. dataset.fill = false
  209. var datasetColor = getPastelColor(i, productNameList.length)
  210. dataset.accentColor = rgbToHex(datasetColor)
  211. dataset.accentFadedColor = rgbToRgba(datasetColor, 0.2)
  212. dataset.backgroundColor = dataset.accentColor
  213. dataset.borderColor = dataset.accentColor
  214. dataset.lineTension = 0.1
  215. datasets.push(dataset)
  216. }
  217. return datasets
  218. }
  219.  
  220. function buildGraph(graphData) {
  221. console.log('graphData', graphData)
  222.  
  223. window.graphData = graphData
  224. var datasets = window.datasets = convertToDatasets(graphData)
  225.  
  226. //var labels = document.querySelectorAll('#my-payout-list ul.nav-tabs li a')
  227. //labels = Array.prototype.map.call(labels, function(e){ return e.textContent })
  228. //labels = labels.reverse()
  229.  
  230. //var labels = new Array(3).fill('Month')
  231. var labels = graphData.labels
  232. labels = labels.reverse()
  233.  
  234. console.log('datasets', JSON.stringify(datasets))
  235. console.log('labels', JSON.stringify(labels))
  236.  
  237. var graphParent = document.querySelector('.my-products-heading')
  238. var graphContainer = el('<div id="graphs" />')
  239. var graphCanvas = el('<canvas id="myChart" width="100vw" height="30vh"></canvas>')
  240. graphContainer.appendChild(graphCanvas)
  241.  
  242. graphParent.parentNode.insertBefore(graphContainer, graphParent)
  243.  
  244. //var navTabs = document.querySelector('#my-payout-list ul.nav-tabs')
  245. //var graphTab = el('<li><a href="#graphs" data-toggle="tab">Graphs</a></li>')
  246. //navTabs.insertBefore(graphTab, navTabs.firstChild)
  247.  
  248. var ctx = document.getElementById("myChart").getContext("2d");
  249. var myChart = window.myChart = new Chart(ctx, {
  250. type: 'line',
  251. data: {
  252. labels: labels,
  253. datasets: datasets,
  254. },
  255. options: {
  256. title: {
  257. display: true,
  258. text: 'Product Downloads Over Time',
  259. },
  260. tooltips: {
  261. mode: 'index',
  262. intersect: false,
  263. itemSort: function (a, b, data) {
  264. return b.yLabel - a.yLabel // descending
  265. }
  266. },
  267. legend: {
  268. position: 'left',
  269. onHover: function(e, legendItem) {
  270. if (myChart.hoveringLegendIndex != legendItem.datasetIndex) {
  271. myChart.hoveringLegendIndex = legendItem.datasetIndex
  272. for (var i = 0; i < myChart.data.datasets.length; i++) {
  273. var dataset = myChart.data.datasets[i]
  274. if (i == legendItem.datasetIndex) {
  275. dataset.borderColor = dataset.accentColor
  276. dataset.pointBackgroundColor = dataset.accentColor
  277. } else {
  278. dataset.borderColor = dataset.accentFadedColor
  279. dataset.pointBackgroundColor = dataset.accentFadedColor
  280. }
  281. }
  282. myChart.options.tooltips.enabled = false
  283. myChart.update()
  284. }
  285. }
  286. },
  287. hover: {
  288. mode: 'nearest',
  289. intersect: true,
  290. },
  291. scales: {
  292. yAxes: [{
  293. //type: 'logarithmic',
  294. ticks: {
  295. //stepSize: 5,
  296. //beginAtZero:true,
  297. }
  298. }]
  299. }
  300. }
  301. });
  302.  
  303. myChart.hoveringLegendIndex = -1
  304. myChart.canvas.addEventListener('mousemove', function(e) {
  305. if (myChart.hoveringLegendIndex >= 0) {
  306. if (e.layerX < myChart.legend.left || myChart.legend.right < e.layerX
  307. || e.layerY < myChart.legend.top || myChart.legend.bottom < e.layerY
  308. ) {
  309. myChart.hoveringLegendIndex = -1
  310. for (var i = 0; i < myChart.data.datasets.length; i++) {
  311. var dataset = myChart.data.datasets[i]
  312. dataset.borderColor = dataset.accentColor
  313. dataset.pointBackgroundColor = dataset.accentColor
  314. }
  315. myChart.options.tooltips.enabled = true
  316. myChart.update()
  317. }
  318. }
  319. })
  320. }
  321.  
  322.  
  323. function main() {
  324. getProductDownloadsOverTime().then(function(graphData){
  325. buildGraph(graphData)
  326. })
  327. }
  328.  
  329. main()
  330.