// ==UserScript==
// @name OsuTopPlaysTimeGraph
// @namespace https://github.com/Magnus-Cosmos
// @version 1.0.0
// @description Adds graphing for which hours top plays were set at
// @author Magnus Cosmos
// @match https://osu.ppy.sh/users/*
// @require https://greasyfork.org/scripts/441005-osuweb/code/OsuWeb.js
// @require https://greasyfork.org/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.id] = location.pathname.split("/").slice(1).map(val => {
const int = parseInt(val);
return val == int ? int : val;
});
}
function beatmapHandler(beatmapset) {
const [currMode, currBeatmap] = location.hash.replace("#", "").split("/").map(val => {
const int = parseInt(val);
return val == int ? int : val;
});
const beatmap = beatmapset.beatmaps.find((b) => {
if (b.id === currBeatmap) {
return b;
}
});
$(".beatmap-basic-stats__entry span").last().text(beatmap.count_spinners);
}
const osuWebObserver = new OsuWebObserver(staticPage, function() {
switch(this.type) {
case "users": {
const bestPlaysH3 = $(".title--page-extra-small").toArray().find(val => {
if (val.innerText.includes("Best Performance")) {
return val;
}
});
$(bestPlaysH3).css("display", "inline-block");
$(`<button type="button" class="show-more-link show-more-link--profile-page graph-button">
<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(bestPlaysH3);
const userId = this.id;
$(".graph-button").on("click", function() {
$(this).children().first().css("display", "inline-flex");
$(this).children().last().css("visibility", "hidden");
web.get(`/users/${userId}/scores/best`, { mode: "osu", limit: 100 }, res => {
res.json().then(data => {
const scores = data;
const bins = hourBins(scores);
$(`<div class="graph-container"></div>`).append(`<canvas id="best-plays-graph"></canvas>`).insertAfter(this);
graph("best-plays-graph", 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").css("display") === "none";
if (hidden) {
$(this).find(".show-more-link__label-text").text("close graph");
$(this).addClass("graph-close");
$(".graph-container").show();
} else {
$(this).find(".show-more-link__label-text").text("show graph");
$(this).removeClass("graph-close");
$(".graph-container").hide();
}
});
}).catch(err => {
$(this).children().first().removeAttr("style");
$(this).children().last().removeAttr("style");
console.log(err);
});
}, { credentials: true }); // for restricted users (need credentials to get top plays)
});
}
}
});
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 top 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]
});
}