// ==UserScript== // @name Trakt.tv | Charts - Seasons // @description Adds a line chart to /seasons pages which shows the ratings (personal + general) and the number of watchers and comments for each individual episode. See README for details. // @version 0.1.1 // @namespace https://github.com/Fenn3c401 // @author Fenn3c401 // @license GPL-3.0-or-later // @homepageURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection#readme // @supportURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection/issues // @updateURL https://raw.githubusercontent.com/Fenn3c401/Trakt.tv-Userscript-Collection/main/userscripts/meta/cs1u5z40.meta.js // @downloadURL https://raw.githubusercontent.com/Fenn3c401/Trakt.tv-Userscript-Collection/main/userscripts/dist/cs1u5z40.min.user.js // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDQ4IDQ4Ij4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgICBmaWxsOiB1cmwoI3JhZGlhbC1ncmFkaWVudCk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQogICAgPC9zdHlsZT4KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50IiBjeD0iNDguNDYiIGN5PSItLjk1IiBmeD0iNDguNDYiIGZ5PSItLjk1IiByPSI2NC44NCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5ZjQyYzYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuMjciIHN0b3AtY29sb3I9IiNhMDQxYzMiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNDIiIHN0b3AtY29sb3I9IiNhNDNlYmIiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNTMiIHN0b3AtY29sb3I9IiNhYTM5YWQiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNjQiIHN0b3AtY29sb3I9IiNiNDMzOWEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuNzMiIHN0b3AtY29sb3I9IiNjMDJiODEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuODIiIHN0b3AtY29sb3I9IiNjZjIwNjEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIuOSIgc3RvcC1jb2xvcj0iI2UxMTQzYyIvPgogICAgICA8c3RvcCBvZmZzZXQ9Ii45NyIgc3RvcC1jb2xvcj0iI2Y1MDYxMyIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9InJlZCIvPgogICAgPC9yYWRpYWxHcmFkaWVudD4KICA8L2RlZnM+CiAgPGcgaWQ9Il94MkRfLXByb2R1Y3Rpb24iPgogICAgPGcgaWQ9ImxvZ29tYXJrLnNxdWFyZS5ncmFkaWVudCI+CiAgICAgIDxwYXRoIGlkPSJiYWNrZ3JvdW5kIiBjbGFzcz0iY2xzLTEiIGQ9Ik00OCwxMS4yNnYyNS40N2MwLDYuMjItNS4wNSwxMS4yNy0xMS4yNywxMS4yN0gxMS4yNmMtNi4yMiwwLTExLjI2LTUuMDUtMTEuMjYtMTEuMjdWMTEuMjZDMCw1LjA0LDUuMDQsMCwxMS4yNiwwaDI1LjQ3YzMuMzIsMCw2LjMsMS40Myw4LjM3LDMuNzIuNDcuNTIuODksMS4wOCwxLjI1LDEuNjguMTguMjkuMzQuNTkuNS44OS4zMy42OC42LDEuMzkuNzksMi4xNC4xLjM3LjE4Ljc2LjIzLDEuMTUuMDkuNTQuMTMsMS4xMS4xMywxLjY4WiIvPgogICAgICA8ZyBpZD0iY2hlY2tib3giPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEzLjYyLDE3Ljk3bDcuOTIsNy45MiwxLjQ3LTEuNDctNy45Mi03LjkyLTEuNDcsMS40N1pNMjguMDEsMzIuMzdsMS40Ny0xLjQ2LTIuMTYtMi4xNiwyMC4zMi0yMC4zMmMtLjE5LS43NS0uNDYtMS40Ni0uNzktMi4xNGwtMjIuNDYsMjIuNDYsMy42MiwzLjYyWk0xMi45MiwxOC42N2wtMS40NiwxLjQ2LDE0LjQsMTQuNCwxLjQ2LTEuNDctNC4zMi00LjMxTDQ2LjM1LDUuNGMtLjM2LS42LS43OC0xLjE2LTEuMjUtMS42OGwtMjMuNTYsMjMuNTYtOC42Mi04LjYxWk00Ny44Nyw5LjU4bC0xOS4xNywxOS4xNywxLjQ3LDEuNDYsMTcuODMtMTcuODN2LTEuMTJjMC0uNTctLjA0LTEuMTQtLjEzLTEuNjhaTTI1LjE2LDIyLjI3bC03LjkyLTcuOTItMS40NywxLjQ3LDcuOTIsNy45MiwxLjQ3LTEuNDdaTTQxLjMyLDM1LjEyYzAsMy40Mi0yLjc4LDYuMi02LjIsNi4ySDEyLjg4Yy0zLjQyLDAtNi4yLTIuNzgtNi4yLTYuMlYxMi44OGMwLTMuNDIsMi43OC02LjIxLDYuMi02LjIxaDIwLjc4di0yLjA3SDEyLjg4Yy00LjU2LDAtOC4yOCwzLjcxLTguMjgsOC4yOHYyMi4yNGMwLDQuNTYsMy43MSw4LjI4LDguMjgsOC4yOGgyMi4yNGM0LjU2LDAsOC4yOC0zLjcxLDguMjgtOC4yOHYtMy41MWgtMi4wN3YzLjUxWiIvPjwhLS0gNDVkMjM4NWQzYWFjYmI1OTMyNmEzODYxNDljNWE4NzggLS0+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg== // @match https://trakt.tv/* // @run-at document-start // @require https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js#sha256-vOFUCAlZxXS+C7axqST/MvCOvG/0YMFZFx9RxTgCyEQ= // @require https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js#sha256-5KCI5bq5O+buR8k57rnrqoDgs5FW1L39GvnIRL6BtsQ= // @grant unsafeWindow // @grant GM_addStyle // @grant GM_openInTab // ==/UserScript== /* README ### General - Clicking on the individual data points takes you to the summary page of the respective episode (or the comment page for comment data points). - For charts with more than eight episodes, you can also zoom in by highlighting a section of the x-axis with your mouse. You can zoom out again by clicking anywhere inside the chart. - This script won't work (well) on mobile devices and the chart is no beauty on light mode either. Basically the whole thing needs an overhaul and is not even close to being finished, but the core functionality is there and it might be while until I get back to it, which is why I'm putting it out there as it is right now. */ "use strict";let $,trakt,$grid,isSeasonChart,filterSpecials,labelsCallback,chart,datasetsData,firstRunDelay;Chart.defaults.borderColor="rgb(44 44 44 / 0.5)";const numFormatCompact=new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1});numFormatCompact.formatTLC=e=>numFormatCompact.format(e).toLowerCase(),addStyles(),document.addEventListener("turbo:load",async()=>{/^\/shows\/[^/]+\/seasons\/[^/]+$/.test(location.pathname)&&($??=unsafeWindow.jQuery,trakt??=unsafeWindow.userscriptTraktAPIModule?.isFulfilled?await unsafeWindow.userscriptTraktAPIModule:null,$&&($grid=$("#seasons-episodes-sortable"),$grid.length&&(isSeasonChart=location.pathname.includes("/seasons/"),filterSpecials=!location.pathname.includes("/seasons/0"),labelsCallback=isSeasonChart?e=>`${e.seasonNum}x${String(e.episodeNum).padStart(2,"0")} ${e.watched?"\u2714":"\u2718"}`:e=>`S. ${e.seasonNum} ${e.watched?e.watched==100?"\u2714":`(${e.watched}%)`:"\u2718"}`,chart=null,datasetsData=[],firstRunDelay=!0,!(!isSeasonChart&&+$(".season-count").text().split(" ")[0]<4||location.pathname.includes("/alternate/")&&location.pathname.split("/").filter(Boolean).length<6)&&($grid.on("arrangeComplete",()=>{$grid.data("isotope")&&(chart?updateChart():initializeChart())}),$(document).off("ajaxSuccess.userscript48372").on("ajaxSuccess.userscript48372",(e,t,a)=>{a.url.includes("/rate")&&chart&&updateChart()})))))},{capture:!0});function initializeChart(){const e=$('
').insertBefore($grid).children()[0];chart=new Chart(e.getContext("2d"),{type:"line",data:getChartData(),options:getChartOptions()});const t=new IntersectionObserver(a=>{a.forEach(r=>{r.isIntersecting&&(t.disconnect(),document.hidden?$(document).one("visibilitychange",updateChart):updateChart())})},{threshold:1});t.observe(e),e.addEventListener("click",a=>{const r=chart.getElementsAtEventForMode(a,"nearest",{axis:"x",intersect:!1},!0);if(!r.length)return;const s=r.sort((n,o)=>Math.abs(n.element.y-a.layerY)-Math.abs(o.element.y-a.layerY))[0];if(Math.abs(s.element.y-a.layerY)<10){const n=`${datasetsData[s.index].urlFull}${s.datasetIndex===3?"/comments":""}`;GM_openInTab(n,{active:!0,insert:!0})}else chart.isZoomedOrPanned()&&chart.resetZoom("active")})}async function updateChart(){const e=await getDatasetsData();JSON.stringify(datasetsData)!==JSON.stringify(e)&&(datasetsData=e,chart.data=getChartData(),chart.options=getChartOptions(),chart.update(),firstRunDelay&&(firstRunDelay=!1))}function getDatasetsData(){const e=$grid.data("isotope").filteredItems.filter(t=>filterSpecials?t.element.dataset.seasonNumber!=="0":!0).map(async t=>{const a={generalRating:t.sortData.percentage,votes:t.sortData.votes,watchers:t.sortData.watchers,episodeNum:t.element.dataset.number||null,seasonNum:t.element.dataset.seasonNumber,urlFull:$(t.element).find('meta[itemprop="url"]').attr("content"),personalRating:$(t.element).find(".corner-rating > .text").text()||null,watched:$(t.element).find("a.watch.selected").attr("data-percentage")??0,releaseDate:$(t.element).find(".percentage").attr("data-earliest-release-date")};if(isSeasonChart)a.mainTitle=$(t.element).find(".under-info .main-title").text(),a.comments=$(t.element).find('.episode-stats > a[data-original-title="Comments"]').text()||0;else if(a.mainTitle=$(t.element).find('div[data-type="season"] .titles-link h3').text(),trakt){const r=await trakt.seasons.comments({id:t.element.dataset.showId,season:a.seasonNum,pagination:!0,limit:1});a.comments=r.pagination.item_count}else{const r=await fetch(t.element.dataset.url+".json");if(!r.ok)throw new Error(`XHR for: ${r.url} failed with status: ${r.status}`);a.comments=(await r.json()).stats.comment_count}return a});return Promise.all(isSeasonChart?e:e.reverse())}function getGradientY(e,t,a,...r){if(!e)return r.pop().color;const{ctx:s,chartArea:n,scales:o}=e.chart;if(n){if(s[t]??={},!s[t].gradient||s[t].height!==n.height||s[t].yAxisMin!==o[a].min||s[t].yAxisMax!==o[a].max){s[t].height=n.height,s[t].yAxisMin=o[a].min,s[t].yAxisMax=o[a].max;let i=o[a].max-o[a].min;i=i?o[a].max/i:1,i=n.bottom*i,s[t].gradient=s.createLinearGradient(0,i,0,n.top),r.forEach(l=>s[t].gradient.addColorStop(l.offset,l.color))}return s[t].gradient}}function getChartData(){return{labels:datasetsData.map(labelsCallback),datasets:[{label:"Personal Rating",data:datasetsData.map(e=>e.personalRating?e.personalRating*10:null),yAxisID:"yAxisRating",borderColor:e=>getGradientY(e,"_ratingPersonal","yAxisRating",{offset:0,color:"rgb(97 97 97 / 0.6)"},{offset:.1,color:"rgb(97 97 97 / 0.6)"},{offset:1,color:"rgb(175 2 0)"}),backgroundColor:e=>getGradientY(e,"_ratingPersonal","yAxisRating",{offset:0,color:"rgb(97 97 97 / 0.6)"},{offset:.1,color:"rgb(97 97 97 / 0.6)"},{offset:1,color:"rgb(175 2 0)"})},{label:"General Rating",data:datasetsData.map(e=>e.generalRating),yAxisID:"yAxisRating",fill:{target:"-1",above:`rgb(255 0 0 / ${$("body").hasClass("dark-knight")?.15:.3})`,below:`rgb(0 255 0 / ${$("body").hasClass("dark-knight")?.15:.3})`},borderColor:e=>getGradientY(e,"_ratingGeneral","yAxisRating",{offset:0,color:"rgb(97 97 97 / 0.6)"},{offset:.1,color:"rgb(97 97 97 / 0.6)"},{offset:1,color:"rgb(225 31 117)"}),backgroundColor:e=>getGradientY(e,"_ratingGeneral","yAxisRating",{offset:0,color:"rgb(97 97 97 / 0.6)"},{offset:.1,color:"rgb(97 97 97 / 0.6)"},{offset:1,color:"rgb(225 31 117)"})},{label:"Watchers",data:datasetsData.map(e=>e.watchers),yAxisID:"yAxisWatchers",borderColor:e=>getGradientY(e,"_watchers","yAxisWatchers",{offset:0,color:"rgb(154 67 201 / 0.2)"},{offset:1,color:"rgb(154 67 201)"}),backgroundColor:e=>getGradientY(e,"_watchers","yAxisWatchers",{offset:0,color:"rgb(154 67 201 / 0.2)"},{offset:1,color:"rgb(154 67 201)"})},{label:"Comments",data:datasetsData.map(e=>e.comments),yAxisID:"yAxisComments",borderColor:e=>getGradientY(e,"_comments","yAxisComments",{offset:0,color:"rgb(54 157 226 / 0.2)"},{offset:1,color:"rgb(54 157 226)"}),backgroundColor:e=>getGradientY(e,"_comments","yAxisComments",{offset:0,color:"rgb(54 157 226 / 0.2)"},{offset:1,color:"rgb(54 157 226)"})}]}}function getChartOptions(){return{responsive:!0,maintainAspectRatio:!1,interaction:{mode:"nearest",axis:"x",intersect:!1},animation:{delay:e=>e.type==="data"&&e.mode==="default"?(firstRunDelay?500:0)+e.dataIndex*(750/Math.max(datasetsData.length-1,1))+e.datasetIndex*100:0},scales:{x:{offset:!0},yAxisRating:{type:"linear",position:"left",offset:!0,suggestedMin:60,max:100,title:{display:!0,text:"Rating"},grid:{color:e=>e.tick.value%10?Chart.defaults.borderColor:"rgb(55 55 55)"},ticks:{callback:e=>`${e}%`}},yAxisWatchers:{type:"linear",position:"right",offset:!0,min:0,suggestedMax:10,title:{display:!0,text:"Watchers"},grid:{drawOnChartArea:!1},ticks:{callback:e=>numFormatCompact.formatTLC(e)}},yAxisComments:{type:"linear",position:"right",offset:!0,min:0,suggestedMax:10,title:{display:!0,text:"Comments"},grid:{drawOnChartArea:!1}}},plugins:{tooltip:{usePointStyle:!0,boxPadding:5,backgroundColor:"rgb(0 0 0 / 0.5)",caretSize:10,padding:{x:18,y:6},titleFont:{size:13,weight:"bold"},callbacks:{title:e=>{let t=datasetsData[e[0].parsed.x].mainTitle;return t=t.length>20?t.slice(0,20).trim()+"...":t,`${e[0].label}${t?` ${t}`:""}`},label:e=>{const t=e.parsed.x,a=e.parsed.y,r=unsafeWindow.userscriptAvgSeasonEpisodeRatings;return e.datasetIndex===0?`${a/10}${r?.personal?.average?` (avg: ${r.personal.average.toFixed(1)})`:""}`:e.datasetIndex===1?`${a}% (${numFormatCompact.formatTLC(datasetsData[t].votes)} v.)${r?.general?` (avg: ${r.general.average?Math.round(r.general.average):"--"}%)`:""}`:e.datasetIndex===2?`${numFormatCompact.formatTLC(a)}${datasetsData[0].watchers?` (${Math.round(a*100/datasetsData[0].watchers)}%)`:""}`:`${a}`},labelColor:e=>({borderColor:e.dataset.borderColor(),backgroundColor:e.dataset.backgroundColor()}),footer:e=>{const t=datasetsData[e[0].parsed.x].releaseDate;return t?unsafeWindow.formatDate?.(t)||t:void 0}}},legend:{labels:{usePointStyle:!0,filter:(e,t)=>t.datasets[e.datasetIndex].data.some(a=>a!==null)}},filler:{propagate:!1},zoom:{zoom:{mode:"x",drag:{enabled:!0,threshold:0}},limits:{x:{minRange:8}}}}}}function addStyles(){GM_addStyle(` #seasons-episodes-chart-wrapper { position: relative; margin-top: 20px; width: 100%; height: 250px; } @media (width <= 767px) { #seasons-episodes-chart-wrapper { margin-left: -10px; margin-right: -10px; width: calc(100% + 20px); } } @media (991px < width) { #seasons-episodes-chart-wrapper { height: 300px; } } `)}