Skip to content

Commit 62e89a5

Browse files
committed
feat: add more graphs on admin page
todo: take tournament deadlines into account
1 parent 0e41c53 commit 62e89a5

File tree

3 files changed

+220
-3
lines changed

3 files changed

+220
-3
lines changed

src/routes/admin/charts.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Express } from 'express';
22
import { PrismaClient } from '@prisma/client';
33
import { CURSUS_ID } from '../../env';
44
import { ChartConfiguration } from 'chart.js';
5+
import { CoalitionScore, getCoalitionScore } from '../../utils';
56

67
export const setupAdminChartsRoutes = function(app: Express, prisma: PrismaClient): void {
78
app.get('/admin/charts/coalitions/users/distribution', async (req, res) => {
@@ -174,4 +175,185 @@ export const setupAdminChartsRoutes = function(app: Express, prisma: PrismaClien
174175
return res.status(400).json({ error: err });
175176
}
176177
});
178+
179+
app.get('/admin/charts/coalitions/scores/history', async (req, res) => {
180+
// TODO: change this to the full overview of a tournament deadline instead of past 30 days
181+
try {
182+
const coalitions = await prisma.intraCoalition.findMany({
183+
select: {
184+
id: true,
185+
name: true,
186+
color: true,
187+
}
188+
});
189+
if (coalitions.length === 0) {
190+
throw new Error('No coalitions found');
191+
}
192+
// Get the score for the past 30 days per day, 2 points per day (00:00 and 12:00)
193+
const dates = [];
194+
const now = new Date();
195+
now.setHours((now.getHours() > 12) ? 12 : 0, 0, 0, 0);
196+
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
197+
for (let i = 0; i <= 60; i++) {
198+
dates.push(new Date(monthAgo.getTime() + i * 12 * 60 * 60 * 1000));
199+
}
200+
201+
// Get the scores for each coalition
202+
const coalitionDataPoints: { [key: number]: CoalitionScore[] } = {};
203+
for (const coalition of coalitions) {
204+
const dataPoints: CoalitionScore[] = [];
205+
for (const date of dates) {
206+
dataPoints[date.getTime()] = await getCoalitionScore(prisma, coalition.id, date);
207+
}
208+
coalitionDataPoints[coalition.id] = dataPoints;
209+
}
210+
211+
// Compose the returnable data (in a format Chart.js can understand)
212+
const chartJSData: ChartConfiguration = {
213+
type: 'line',
214+
data: {
215+
labels: dates.map((date) => `${date.toLocaleDateString()} ${date.getHours()}:00`),
216+
datasets: [],
217+
},
218+
options: {
219+
showLines: true,
220+
scales: {
221+
// @ts-ignore
222+
x: {
223+
title: {
224+
display: false,
225+
text: 'Date',
226+
},
227+
},
228+
y: {
229+
title: {
230+
display: true,
231+
text: 'Amount of points',
232+
},
233+
},
234+
},
235+
}
236+
};
237+
for (const coalition of coalitions) {
238+
chartJSData.data!.datasets!.push({
239+
label: coalition.name,
240+
data: Object.values(coalitionDataPoints[coalition.id]).map((score) => score.score),
241+
borderColor: coalition.color ? coalition.color : '#808080',
242+
backgroundColor: coalition.color ? coalition.color : '#808080',
243+
fill: false,
244+
// @ts-ignore
245+
tension: 0.25,
246+
});
247+
}
248+
249+
return res.json(chartJSData);
250+
}
251+
catch (err) {
252+
console.error(err);
253+
return res.status(400).json({ error: err });
254+
}
255+
});
256+
257+
app.get('/admin/charts/coalitions/:coalitionId/scores/history', async (req, res) => {
258+
try {
259+
const coalitionId = parseInt(req.params.coalitionId);
260+
const coalition = await prisma.intraCoalition.findFirst({
261+
where: {
262+
id: coalitionId,
263+
},
264+
select: {
265+
id: true,
266+
name: true,
267+
color: true,
268+
}
269+
});
270+
if (!coalition) {
271+
throw new Error('Invalid coalition ID');
272+
}
273+
// Get the score for the past 30 days per day, 2 points per day (00:00 and 12:00)
274+
const dataPoints: CoalitionScore[] = [];
275+
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
276+
monthAgo.setHours(0, 0, 0, 0);
277+
for (let i = 0; i < 60; i++) {
278+
const date = new Date(monthAgo.getTime() + i * 12 * 60 * 60 * 1000);
279+
dataPoints[date.getTime()] = await getCoalitionScore(prisma, coalitionId, date);
280+
}
281+
282+
// Compose the returnable data (in a format Chart.js can understand)
283+
const chartJSData: ChartConfiguration = {
284+
type: 'line',
285+
data: {
286+
labels: Object.keys(dataPoints).map((timestamp) => new Date(parseInt(timestamp)).toLocaleDateString()),
287+
datasets: [
288+
{
289+
label: 'Score',
290+
data: Object.values(dataPoints).map((score) => score.score),
291+
// borderColor: coalition.color ? coalition.color : '#808080',
292+
// backgroundColor: coalition.color ? coalition.color : '#808080',
293+
fill: false,
294+
// @ts-ignore
295+
tension: 0.25,
296+
},
297+
{
298+
label: 'Average points',
299+
data: Object.values(dataPoints).map((score) => score.avgPoints),
300+
// borderColor: coalition.color ? coalition.color : '#808080',
301+
// backgroundColor: coalition.color ? coalition.color : '#808080',
302+
fill: false,
303+
// @ts-ignore
304+
tension: 0.25,
305+
},
306+
{
307+
label: 'Standard deviation',
308+
data: Object.values(dataPoints).map((score) => score.stdDevPoints),
309+
// borderColor: coalition.color ? coalition.color : '#808080',
310+
// backgroundColor: coalition.color ? coalition.color : '#808080',
311+
fill: false,
312+
// @ts-ignore
313+
tension: 0.25,
314+
},
315+
{
316+
label: 'Min active points',
317+
data: Object.values(dataPoints).map((score) => score.minActivePoints),
318+
// borderColor: coalition.color ? coalition.color : '#808080',
319+
// backgroundColor: coalition.color ? coalition.color : '#808080',
320+
fill: false,
321+
// @ts-ignore
322+
tension: 0.25,
323+
},
324+
],
325+
},
326+
options: {
327+
showLines: true,
328+
scales: {
329+
// @ts-ignore
330+
x: {
331+
title: {
332+
display: true,
333+
text: 'Date',
334+
},
335+
},
336+
y: {
337+
title: {
338+
display: true,
339+
text: 'Amount of points',
340+
},
341+
min: 0,
342+
},
343+
},
344+
plugins: {
345+
legend: {
346+
display: true,
347+
}
348+
},
349+
}
350+
};
351+
352+
return res.json(chartJSData);
353+
}
354+
catch (err) {
355+
console.error(err);
356+
return res.status(400).json({ error: err });
357+
}
358+
});
177359
}

src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export interface NormalDistribution {
202202
};
203203

204204
export const getScoresNormalDistribution = async function(prisma: PrismaClient, coalitionId: number, untilDate: Date = new Date()): Promise<NormalDistribution> {
205+
// TODO: calculate based on tournament deadlines
205206
const scores = await prisma.codamCoalitionScore.groupBy({
206207
by: ['user_id'],
207208
where: {
@@ -238,6 +239,8 @@ export interface CoalitionScore {
238239
avgPoints: number;
239240
stdDevPoints: number;
240241
minActivePoints: number; // Minimum score for a user to be considered active
242+
totalContributors: number;
243+
activeContributors: number;
241244
}
242245

243246
export const getCoalitionScore = async function(prisma: PrismaClient, coalitionId: number, atDateTime: Date = new Date()): Promise<CoalitionScore> {
@@ -252,5 +255,7 @@ export const getCoalitionScore = async function(prisma: PrismaClient, coalitionI
252255
stdDevPoints: normalDist.stdDev,
253256
minActivePoints: minScore,
254257
score: fairScore,
258+
totalContributors: normalDist.dataPoints.length,
259+
activeContributors: activeScores.length,
255260
};
256261
};

templates/admin/dashboard.njk

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@
3434
<div class="col">
3535
<div class="card h-100">
3636
<div class="card-header">
37-
<h5 class="card-title mb-0">Scores for {{ coalition.intra_coalition.name }} </h5>
37+
<h5 class="card-title mb-0">Scores for {{ coalition.intra_coalition.name }}</h5>
3838
</div>
3939
<div class="card-body">
4040
<ul>
4141
<li>Score: {{ coalitionScores[coalition.id].score }}</li>
4242
<li>Total points: {{ coalitionScores[coalition.id].totalPoints }}</li>
43+
<li>Total contributor count: {{ coalitionScores[coalition.id].totalContributors }}</li>
4344
<li>Minimum contrition: {{ coalitionScores[coalition.id].minActivePoints }}</li>
45+
<li>Active contributor count: {{ coalitionScores[coalition.id].activeContributors }}</li>
4446
<li>Average points: {{ coalitionScores[coalition.id].avgPoints | toFixed(2) }}</li>
4547
<li>Standard deviation points: {{ coalitionScores[coalition.id].stdDevPoints | toFixed(2) }}</li>
4648
</ul>
@@ -50,15 +52,43 @@
5052
{% endfor %}
5153
</div>
5254

55+
<div class="row ms-0 me-0 mb-4">
56+
<div class="col">
57+
<div class="card h-100">
58+
<div class="card-header">
59+
<h5 class="card-title mb-0">Coalitions score history</h5>
60+
</div>
61+
<div class="card-body">
62+
<canvas height="500" class="codam-chart" data-url="/admin/charts/coalitions/scores/history" id="coalition-score-history"></canvas>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
68+
<div class="row ms-0 me-0 mb-4">
69+
{% for coalition in coalitions %}
70+
<div class="col">
71+
<div class="card h-100">
72+
<div class="card-header">
73+
<h5 class="card-title mb-0">Score history for {{ coalition.intra_coalition.name }}</h5>
74+
</div>
75+
<div class="card-body">
76+
<canvas height="500" class="codam-chart" data-url="/admin/charts/coalitions/{{ coalition.id }}/scores/history" id="coalition-{{ coalition.id }}-scores-history"></canvas>
77+
</div>
78+
</div>
79+
</div>
80+
{% endfor %}
81+
</div>
82+
5383
<div class="row ms-0 me-0 mb-4">
5484
{% for coalition in coalitions %}
5585
<div class="col">
5686
<div class="card h-100">
5787
<div class="card-header">
58-
<h5 class="card-title mb-0">Score Distributions for {{ coalition.intra_coalition.name }} </h5>
88+
<h5 class="card-title mb-0">Current score distribution for {{ coalition.intra_coalition.name }}</h5>
5989
</div>
6090
<div class="card-body">
61-
<canvas class="codam-chart" data-url="/admin/charts/coalitions/{{ coalition.id }}/scores/distribution" id="coalition-{{ coalition.id }}-scores-distribution"></canvas>
91+
<canvas height="500" class="codam-chart" data-url="/admin/charts/coalitions/{{ coalition.id }}/scores/distribution" id="coalition-{{ coalition.id }}-scores-distribution"></canvas>
6292
</div>
6393
</div>
6494
</div>

0 commit comments

Comments
 (0)