diff options
Diffstat (limited to 'assets/chart.js')
| -rw-r--r-- | assets/chart.js | 146 |
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)`; + }, + }, + }, + }, + }, + }, + ); +}); |
