aboutsummaryrefslogtreecommitdiffhomepage
path: root/assets/chart.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/chart.js')
-rw-r--r--assets/chart.js146
1 files changed, 146 insertions, 0 deletions
diff --git a/assets/chart.js b/assets/chart.js
new file mode 100644
index 0000000..9ff08e4
--- /dev/null
+++ b/assets/chart.js
@@ -0,0 +1,146 @@
+import {
+ Chart,
+ Colors,
+ LineController,
+ LineElement,
+ LinearScale,
+ PointElement,
+ TimeScale,
+ Tooltip,
+} from 'chart.js'
+import 'chartjs-adapter-date-fns';
+
+Chart.register(
+ Colors,
+ LineController,
+ LineElement,
+ LinearScale,
+ PointElement,
+ TimeScale,
+ Tooltip,
+);
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const chartCanvas = document.getElementById('chart');
+ const quizId = chartCanvas.dataset.quizId;
+
+ const apiUrl = `${process.env.ALBATROSS_BASE_PATH}/api/quizzes/${quizId}/chart`;
+ const apiResult = await fetch(apiUrl).then(res => res.json());
+ if (apiResult.error) {
+ return;
+ }
+ const stats = apiResult.stats;
+
+ // Filter best scores.
+ for (const s of stats) {
+ const bestScores = [];
+ for (const score of s.scores) {
+ if (bestScores.length === 0 || bestScores[bestScores.length - 1].code_size > score.code_size) {
+ bestScores.push(score);
+ }
+ }
+ s.scores = bestScores;
+ }
+
+ const scoresInChronologicalOrder = stats
+ .flatMap(s => s.scores.map(score => ({ ...score, user: s.user })))
+ .toSorted((a, b) => a.submitted_at - b.submitted_at);
+
+ const scoresAndRanksAtEachTime = (() => {
+ const result = [];
+ const currentScoresForUser = new Map();
+ for (const { user, submitted_at, code_size } of scoresInChronologicalOrder) {
+ currentScoresForUser.set(user.name, { user, submitted_at, code_size });
+ const ranking = currentScoresForUser
+ .values()
+ .toArray()
+ .toSorted(
+ (a, b) => a.code_size === b.code_size ? a.submitted_at - b.submitted_at : a.code_size - b.code_size,
+ );
+ const scores = new Map();
+ for (const [i, { user, code_size }] of ranking.entries()) {
+ scores.set(user.name, {
+ user,
+ code_size,
+ rank: i + 1,
+ });
+ }
+ result.push({ submitted_at: submitted_at, scores });
+ }
+ return result;
+ })();
+
+ const rankingHistory = (() => {
+ const result = new Map();
+ for (const { submitted_at, scores } of scoresAndRanksAtEachTime) {
+ for (const [username, { user, code_size, rank }] of scores.entries()) {
+ if (!result.has(username)) {
+ result.set(username, []);
+ }
+ const scores = result.get(username);
+ scores.push({ user, code_size, rank, submitted_at });
+ }
+ }
+ return result
+ .values()
+ .toArray()
+ .toSorted((a, b) => {
+ const finalRankA = a[a.length - 1].rank;
+ const finalRankB = b[b.length - 1].rank;
+ return finalRankA - finalRankB;
+ });
+ })();
+
+ new Chart(
+ chartCanvas,
+ {
+ type: 'line',
+ data: {
+ datasets: rankingHistory.map(s => ({
+ label: `${s[0].user.name}${s[0].user.is_admin ? ' (staff)' : ''}`,
+ data: s.map(row => ({ x: row.submitted_at * 1000, y: row.rank, code_size: row.code_size })),
+ }))
+ },
+ options: {
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ parsing: false,
+ display: false,
+ unit: 'day',
+ tooltipFormat: 'yyyy-MM-dd HH:mm:ss',
+ displayFormats: {
+ day: 'yyyy-MM-dd',
+ },
+ },
+ title: {
+ display: true,
+ text: '日時',
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: '順位',
+ },
+ reverse: true,
+ min: 1,
+ offset: true,
+ },
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (context) => {
+ const label = context.dataset.label;
+ const code_size = context.raw.code_size;
+ return `${label} (${code_size} byte)`;
+ },
+ },
+ },
+ },
+ },
+ },
+ );
+});