// ==UserScript==
// @name OsuProfileScoresTimeGraph
// @namespace https://github.com/Magnus-Cosmos
// @version 1.0.5
// @description Adds graphing for which hours scores were set at
// @author Magnus Cosmos
// @match https://osu.ppy.sh/users/*
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://gf.qytechs.cn/scripts/441005-osuweb/code/OsuWeb.js
// @require https://gf.qytechs.cn/scripts/441010-osupageobserver/code/OsuPageObserver.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
// ==/UserScript==
const web = new Web();
$(document.head).append($("<style class='circlescript-style'></style>").html(
`
.graph-button {
display: inline-block;
right: 40px;
position: absolute;
}
.graph-close {
background-color: hsl(var(--hsl-red-3)) !important;
}
.graph-close:hover {
background-color: hsl(var(--hsl-red-2)) !important;
}
.graph-container {
padding: 10px;
border-radius: 10px;
background-color: hsl(var(--hsl-b3));
}
#best-plays-graph {
display: block;
}
`
));
function staticPage() {
[this.type, ...this.paths] = location.pathname.split("/").slice(1).map(val => {
const int = parseInt(val);
return val == int ? int : val;
});
}
function getProfileJson() {
const json = $(".osu-layout--full").attr("data-initial-data");
if (json) {
return JSON.parse(json);
}
return null;
}
const osuWebObserver = new OsuWebObserver(staticPage, function() {
switch(this.type) {
case "users": {
const profile = getProfileJson();
const user = profile.user;
const mode = this.paths[1] ? this.paths[1] : user.playmode;
if (user) {
graphTimes("best", user, mode);
graphTimes("firsts", user, mode);
}
}
}
});
const ScoreType = {
firsts: "First Place Ranks",
best: "Best Performance"
}
function graphTimes(type, user, mode) {
const h3 = $(".title--page-extra-small").toArray().find(val => {
if (val.innerText.includes(ScoreType[type])) {
return val;
}
});
const amount = $(h3).find(".title__count").text();
if (parseInt(amount) === 0 || !h3) {
return;
}
$(h3).css("display", "inline-block");
$(`<button type="button" class="show-more-link show-more-link--profile-page graph-button graph-button--${type}">
<span class="show-more-link__spinner">
<div class="la-ball-clip-rotate"></div>
</span>
<span class="show-more-link__label">
<span class="show-more-link__label-text">graph</span>
</span>
</button>`).insertAfter(h3);
$(`.graph-button--${type}`).on("click", function() {
$(this).children().first().css("display", "inline-flex");
$(this).children().last().css("visibility", "hidden");
const successFn = scores => {
const bins = hourBins(scores);
$(`<div class="graph-container graph-container--${type}"></div>`).append(`<canvas id="graph-${type}"></canvas>`).insertAfter(this);
graph(`graph-${type}`, bins);
$(this).children().first().removeAttr("style");
$(this).children().last().removeAttr("style");
$(this).find(".show-more-link__label-text").text("close graph");
$(this).addClass("graph-close");
$(this).unbind("click");
$(this).on("click", function() {
const hidden = $(`.graph-container--${type}`).css("display") === "none";
if (hidden) {
$(this).find(".show-more-link__label-text").text("close graph");
$(this).addClass("graph-close");
$(`.graph-container--${type}`).show();
} else {
$(this).find(".show-more-link__label-text").text("show graph");
$(this).removeClass("graph-close");
$(`.graph-container--${type}`).hide();
}
});
};
const errorFn = err => {
$(this).children().first().removeAttr("style");
$(this).children().last().removeAttr("style");
console.log(err);
}
switch (type) {
case "firsts": {
getFirstPlaces(user, [], mode, 0, successFn, errorFn);
break;
}
case "best": {
web.get(`/users/${user.id}/scores/best`, { mode: mode, limit: 100 }, res => {
res.json().then(successFn).catch(errorFn);
}, { credentials: "include" });
break;
}
}
});
}
function getFirstPlaces(user, scores, mode, offset, successFn, errorFn) {
web.get(`/users/${user.id}/scores/firsts`, { mode: mode, limit: 100, offset: offset }, res => {
res.json().then(data => {
if (data.length === 0) {
successFn(scores);
return;
}
scores.push(...data);
getFirstPlaces(user, scores, mode, offset + 100, successFn, errorFn);
}).catch(errorFn);
}, { credentials: "include" });
}
function hourBins(scores) {
const bins = scores.reduce((obj, score) => {
const date = new Date(score.created_at);
const hour = date.getHours();
obj[hour]++;
return obj;
}, Object.fromEntries([...Array(24).keys()].map(k => [k, 0])));
return Object.entries(bins).map(([k, v]) => {
return {
x: k,
y: v
};
});
}
function graph(id, data) {
const ctx = document.getElementById(id).getContext("2d");
const font = "Torus,Inter,Helvetica Neue,Tahoma,Arial,Hiragino Kaku Gothic ProN,Meiryo,Microsoft YaHei,Apple SD Gothic Neo,sans-serif";
const chartAreaBorder = {
id: "chartAreaBorder",
beforeDraw(chart, args, options) {
const {ctx, chartArea: {left, top, width, height}} = chart;
ctx.save();
ctx.strokeStyle = options.borderColor;
ctx.lineWidth = options.borderWidth;
ctx.setLineDash(options.borderDash || []);
ctx.lineDashOffset = options.borderDashOffset;
ctx.strokeRect(left, top, width, height);
ctx.restore();
}
};
return new Chart(ctx, {
type: "bar",
data: {
datasets: [{
data: data,
backgroundColor: [
"rgba(31, 119, 180, 0.5)",
],
barPercentage: 1,
categoryPercentage: 0.96,
borderRadius: 4,
borderWidth: 1
}]
},
options: {
responsive: true,
layout: {
padding: {
right: 10
}
},
scales: {
x: {
title: {
display: true,
text: "Hour of the day",
color: "rgba(255,255,255,0.6)",
font: {
family: font,
weight: 400,
size: 14
}
},
ticks: {
offset: false,
color: "rgba(255,255,255,0.4)",
font: {
family: font,
weight: 100
}
},
offset: true,
grid: {
offset: true,
drawBorder: false
}
},
y: {
title: {
display: true,
text: "# of plays set",
color: "rgba(255,255,255,0.6)",
font: {
family: font,
weight: 400,
size: 14
}
},
ticks: {
color: "rgba(255,255,255,0.4)",
font: {
family: font,
weight: 100
},
callback: (label, index, labels) => {
if (Math.floor(label) === label) {
return label;
}
}
},
grid: {
drawBorder: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
},
chartAreaBorder: {
borderColor: "rgba(0,0,0,0.25)",
borderWidth: 1
}
}
},
plugins: [chartAreaBorder]
});
}