Skip to content

Commit 76dbcf5

Browse files
committed
Share analysis graph code for submissions
The code for the contest analysis was updated, but not yet for the problem analysis. As these graphs should behave the same, the code for creating the graph has been moved from twig to static JavaScript and only the data is rendered in twig. This simplifies sharing the graph creation code and slightly reduces server rendering load.
1 parent 346b390 commit 76dbcf5

File tree

3 files changed

+105
-151
lines changed

3 files changed

+105
-151
lines changed

webapp/public/js/domjudge.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,94 @@ function resizeMobileTeamNamesAndProblemBadges() {
11191119
});
11201120
}
11211121

1122+
function createSubmissionGraph(submissionStats, contestStartTime, contestDurationSeconds, submissions) {
1123+
const minBucketCount = 30;
1124+
const maxBucketCount = 301;
1125+
const units = [
1126+
{ 'name': 'seconds', 'convert': 1, 'step': 60 },
1127+
{ 'name': 'minutes', 'convert': 60, 'step': 15 },
1128+
{ 'name': 'hours', 'convert': 60 * 60, 'step': 6 },
1129+
{ 'name': 'days', 'convert': 60 * 60 * 24, 'step': 7 },
1130+
{ 'name': 'weeks', 'convert': 60 * 60 * 24 * 7, 'step': 1 },
1131+
{ 'name': 'years', 'convert': 60 * 60 * 24 * 365, 'step': 1 }
1132+
];
1133+
let unit = units[0];
1134+
1135+
for (let u of units) {
1136+
const newDuration = Math.ceil(contestDurationSeconds / u.convert);
1137+
if (newDuration > minBucketCount) {
1138+
unit = u;
1139+
} else {
1140+
break;
1141+
}
1142+
}
1143+
const contestDuration = Math.ceil(contestDurationSeconds / unit.convert);
1144+
const bucketCount = Math.min(contestDuration + 1, maxBucketCount);
1145+
// Make sure buckets have whole unit
1146+
const secondsPerBucket = Math.ceil(contestDuration / (bucketCount - 1)) * unit.convert;
1147+
1148+
submissionStats.forEach(stat => {
1149+
stat.values = Array.from({ length: bucketCount }, (_, i) => [i * secondsPerBucket / unit.convert, 0]);
1150+
});
1151+
1152+
const statMap = submissionStats.reduce((map, stat) => {
1153+
map[stat.key] = stat;
1154+
return map;
1155+
}, {});
1156+
1157+
submissions.forEach(submission => {
1158+
const submissionBucket = Math.floor((submission.submittime - contestStartTime) / secondsPerBucket);
1159+
const stat = statMap[submission.result];
1160+
if (stat && submissionBucket >= 0 && submissionBucket < bucketCount) {
1161+
stat.values[submissionBucket][1]++;
1162+
}
1163+
});
1164+
1165+
let maxSubmissionsPerBucket = 1
1166+
for (let bucket = 0; bucket < bucketCount; bucket++) {
1167+
let sum = 0;
1168+
submissionStats.forEach(stat => {
1169+
sum += stat.values[bucket][1];
1170+
});
1171+
maxSubmissionsPerBucket = Math.max(maxSubmissionsPerBucket, sum);
1172+
}
1173+
1174+
// Pick a nice round tickDelta and tickValues based on the step size of units.
1175+
// We want whole values in the unit, and the ticks MUST match a corresponding bucket otherwise the resulting
1176+
// coordinate will be NaN.
1177+
const convertFactor = secondsPerBucket / unit.convert;
1178+
const maxTicks = Math.min(bucketCount, contestDuration / unit.step, minBucketCount)
1179+
const tickDelta = convertFactor * Math.ceil(contestDuration / (maxTicks * convertFactor));
1180+
const ticks = Math.floor(contestDuration / tickDelta) + 1;
1181+
const tickValues = Array.from({ length: ticks }, (_, i) => i * tickDelta);
1182+
1183+
nv.addGraph(function () {
1184+
var chart = nv.models.multiBarChart()
1185+
.showControls(false)
1186+
.stacked(true)
1187+
.x(function (d) { return d[0] })
1188+
.y(function (d) { return d[1] })
1189+
.showYAxis(true)
1190+
.showXAxis(true)
1191+
.reduceXTicks(false)
1192+
;
1193+
chart.xAxis //Chart x-axis settings
1194+
.axisLabel(`Contest Time (${unit.name})`)
1195+
.ticks(tickValues.length)
1196+
.tickValues(tickValues)
1197+
.tickFormat(d3.format('d'));
1198+
chart.yAxis //Chart y-axis settings
1199+
.axisLabel('Total Submissions')
1200+
.tickFormat(d3.format('d'));
1201+
1202+
d3.select('#graph_submissions svg')
1203+
.datum(submissionStats)
1204+
.call(chart);
1205+
nv.utils.windowResize(chart.update);
1206+
return chart;
1207+
});
1208+
}
1209+
11221210
$(function() {
11231211
if (document.querySelector('.mobile-scoreboard')) {
11241212
window.addEventListener('resize', resizeMobileTeamNamesAndProblemBadges);

webapp/templates/jury/analysis/contest_overview.html.twig

Lines changed: 3 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,10 @@ nv.addGraph(function() {
338338
return chart;
339339
});
340340
341-
342341
//////////////////////////////////////
343342
// Submissions over time
344-
// stacked graph of correct, runtime-error, wrong-answer, compiler-error, timelimit, etc
345-
// x-axis is contest time
346-
// y axis is # of submissions
347343
348-
var submission_stats = [
344+
const submission_stats = [
349345
{% for result in ['correct', 'wrong-answer', 'timelimit', 'run-error', 'compiler-error', 'no-output'] %}
350346
{
351347
key: "{{result}}",
@@ -354,8 +350,8 @@ var submission_stats = [
354350
},
355351
{% endfor %}
356352
];
357-
358353
const contest_start_time = {{ current_contest.starttime }};
354+
const contest_duration_seconds = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
359355
const submissions = [
360356
{% for submission in submissions %}
361357
{
@@ -364,98 +360,7 @@ const submissions = [
364360
}{{ loop.last ? '' : ',' }}
365361
{% endfor %}
366362
];
367-
368-
const min_bucket_count = 30;
369-
const max_bucket_count = 301;
370-
const units = [
371-
{'name': 'seconds', 'convert': 1, 'step': 60},
372-
{'name': 'minutes', 'convert': 60, 'step': 15},
373-
{'name': 'hours', 'convert': 60*60, 'step': 6},
374-
{'name': 'days', 'convert': 60*60*24, 'step': 7},
375-
{'name': 'weeks', 'convert': 60*60*24*7, 'step': 1},
376-
{'name': 'years', 'convert': 60*60*24*365, 'step': 1}
377-
];
378-
let unit = units[0];
379-
380-
let contest_duration = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
381-
for (let u of units) {
382-
const new_duration = Math.ceil(contest_duration / u.convert);
383-
if (new_duration > min_bucket_count) {
384-
unit = u;
385-
} else {
386-
break;
387-
}
388-
}
389-
contest_duration = Math.ceil(contest_duration / unit.convert);
390-
const bucket_count = Math.min(contest_duration + 1, max_bucket_count);
391-
// Make sure buckets have whole unit
392-
const seconds_per_bucket = Math.ceil(contest_duration / (bucket_count - 1)) * unit.convert;
393-
394-
submission_stats.forEach(stat => {
395-
stat.values = Array.from({ length: bucket_count }, (_, i) => [i * seconds_per_bucket / unit.convert, 0]);
396-
});
397-
398-
const statMap = submission_stats.reduce((map, stat) => {
399-
map[stat.key] = stat;
400-
return map;
401-
}, {});
402-
403-
submissions.forEach(submission => {
404-
const submission_bucket = Math.floor((submission.submittime - contest_start_time) / seconds_per_bucket);
405-
const stat = statMap[submission.result];
406-
if (stat && submission_bucket >= 0 && submission_bucket < bucket_count) {
407-
stat.values[submission_bucket][1]++;
408-
}
409-
});
410-
411-
let max_submissions_per_bucket = 1
412-
for (let bucket = 0; bucket < bucket_count; bucket++) {
413-
let sum = 0;
414-
submission_stats.forEach(stat => {
415-
sum += stat.values[bucket][1];
416-
});
417-
max_submissions_per_bucket = Math.max(max_submissions_per_bucket, sum);
418-
}
419-
420-
// Pick a nice round tickDelta and tickValues based on the step size of units.
421-
// We want whole values in the unit, and the ticks MUST match a corresponding bucket otherwise the resulting
422-
// coordinate will be NaN.
423-
const convert_factor = seconds_per_bucket / unit.convert;
424-
const maxTicks = Math.min(bucket_count, contest_duration / unit.step, min_bucket_count)
425-
const tickDelta = convert_factor * Math.ceil(contest_duration / (maxTicks * convert_factor));
426-
const ticks = Math.floor(contest_duration / tickDelta) + 1;
427-
const tickValues = Array.from({ length: ticks }, (_, i) => i * tickDelta);
428-
429-
nv.addGraph(function() {
430-
var chart = nv.models.multiBarChart()
431-
// .margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room.
432-
// .useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
433-
// .transitionDuration(350) //how fast do you want the lines to transition?
434-
// .showLegend(true) //Show the legend, allowing users to turn on/off line series.
435-
.showControls(false)
436-
.stacked(true)
437-
.x(function(d) { return d[0] }) //We can modify the data accessor functions...
438-
.y(function(d) { return d[1] }) //...in case your data is formatted differently.
439-
.showYAxis(true) //Show the y-axis
440-
.showXAxis(true) //Show the x-axis
441-
.reduceXTicks(false)
442-
;
443-
chart.xAxis //Chart x-axis settings
444-
.axisLabel(`Contest Time (${unit.name})`)
445-
.ticks(tickValues.length)
446-
.tickValues(tickValues)
447-
.tickFormat(d3.format('d'));
448-
chart.yAxis //Chart y-axis settings
449-
.axisLabel('Total Submissions')
450-
.tickFormat(d3.format('d'));
451-
452-
d3.select('#graph_submissions svg')
453-
.datum(submission_stats)
454-
.call(chart);
455-
nv.utils.windowResize(chart.update);
456-
return chart;
457-
});
458-
363+
createSubmissionGraph(submission_stats, contest_start_time, contest_duration_seconds, submissions);
459364
460365
</script>
461366
{% include 'jury/analysis/download_graphs.html.twig' %}

webapp/templates/jury/analysis/problem.html.twig

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
</div>
6161
</div>
6262
</div>
63-
<div class="col-lg-5 col-sm-12 mt-3" id="submission_times">
63+
<div class="col-lg-5 col-sm-12 mt-3" id="graph_submissions">
6464
<div class="card">
6565
<div class="card-header">
6666
Submissions over Time
@@ -214,68 +214,29 @@ $(function(){
214214
return chart;
215215
});
216216
217-
218217
//////////////////////////////////////
219218
// Submissions over time
220-
// stacked graph of correct, runtime-error, wrong-answer, compiler-error, timelimit, etc
221-
// x-axis is contest time
222-
// y axis is # of submissions
223219
224-
var submission_stats = [
220+
const submission_stats = [
225221
{% for result in ['correct', 'wrong-answer', 'timelimit', 'run-error', 'compiler-error', 'no-output'] %}
226222
{
227223
key: "{{result}}",
228224
color: "{{colors[result]}}",
229-
values: [
230-
{# TODO: make sure these are actually ordered by submittime #}
231-
{# TODO: also make sure these submissions are in the same contest #}
232-
[0,0],
233-
{% set count = 0 %}
234-
{% for submission in submissions | filter(submission => submission.result) %}
235-
{% if submission.result == result %}{% set count = count +1 %}{% endif %}
236-
[ {{ (submission.submittime - current_contest.starttime)/60.0 }},
237-
{{ count }}
238-
],
239-
{% endfor %}
240-
]
225+
values: []
241226
},
242227
{% endfor %}
243228
];
244-
nv.addGraph(function() {
245-
var chart = nv.models.stackedAreaChart()
246-
// .margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room.
247-
// .useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
248-
// .transitionDuration(350) //how fast do you want the lines to transition?
249-
// .showLegend(true) //Show the legend, allowing users to turn on/off line series.
250-
.showControls(false)
251-
.x(function(d) { return d[0] }) //We can modify the data accessor functions...
252-
.y(function(d) { return d[1] }) //...in case your data is formatted differently.
253-
.showYAxis(true) //Show the y-axis
254-
.showXAxis(true) //Show the x-axis
255-
.forceX([0, {{ (current_contest.endtime - current_contest.starttime) / 60 }}])
256-
.forceY([0, {{ submissions|length *1.10 }}])
257-
;
258-
chart.xAxis //Chart x-axis settings
259-
.axisLabel('Contest Time(minutes)')
260-
.tickFormat(d3.format('d'));
261-
262-
chart.yAxis //Chart y-axis settings
263-
.axisLabel('Total Submissions')
264-
.tickFormat(d3.format('d'));
265-
266-
d3.select('#submission_times svg')
267-
.datum(submission_stats)
268-
.call(chart);
269-
nv.utils.windowResize(chart.update);
270-
return chart;
271-
});
272-
273-
274-
275-
276-
277-
278-
229+
const contest_start_time = {{ current_contest.starttime }};
230+
const contest_duration_seconds = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
231+
const submissions = [
232+
{% for submission in submissions %}
233+
{
234+
result: "{{ submission.result }}",
235+
submittime: {{ submission.submittime }},
236+
}{{ loop.last ? '' : ',' }}
237+
{% endfor %}
238+
];
239+
createSubmissionGraph(submission_stats, contest_start_time, contest_duration_seconds, submissions);
279240
})
280241
</script>
281242
{% include 'jury/analysis/download_graphs.html.twig' %}

0 commit comments

Comments
 (0)