Skip to content

Share analysis graph code for submissions #2956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions webapp/public/js/domjudge.js
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,94 @@ function resizeMobileTeamNamesAndProblemBadges() {
});
}

function createSubmissionGraph(submissionStats, contestStartTime, contestDurationSeconds, submissions) {
const minBucketCount = 30;
const maxBucketCount = 301;
const units = [
{ 'name': 'seconds', 'convert': 1, 'step': 60 },
{ 'name': 'minutes', 'convert': 60, 'step': 15 },
{ 'name': 'hours', 'convert': 60 * 60, 'step': 6 },
{ 'name': 'days', 'convert': 60 * 60 * 24, 'step': 7 },
{ 'name': 'weeks', 'convert': 60 * 60 * 24 * 7, 'step': 1 },
{ 'name': 'years', 'convert': 60 * 60 * 24 * 365, 'step': 1 }
];
let unit = units[0];

for (let u of units) {
const newDuration = Math.ceil(contestDurationSeconds / u.convert);
if (newDuration > minBucketCount) {
unit = u;
} else {
break;
}
}
const contestDuration = Math.ceil(contestDurationSeconds / unit.convert);
const bucketCount = Math.min(contestDuration + 1, maxBucketCount);
// Make sure buckets have whole unit
const secondsPerBucket = Math.ceil(contestDuration / (bucketCount - 1)) * unit.convert;

submissionStats.forEach(stat => {
stat.values = Array.from({ length: bucketCount }, (_, i) => [i * secondsPerBucket / unit.convert, 0]);
});

const statMap = submissionStats.reduce((map, stat) => {
map[stat.key] = stat;
return map;
}, {});

submissions.forEach(submission => {
const submissionBucket = Math.floor((submission.submittime - contestStartTime) / secondsPerBucket);
const stat = statMap[submission.result];
if (stat && submissionBucket >= 0 && submissionBucket < bucketCount) {
stat.values[submissionBucket][1]++;
}
});

let maxSubmissionsPerBucket = 1
for (let bucket = 0; bucket < bucketCount; bucket++) {
let sum = 0;
submissionStats.forEach(stat => {
sum += stat.values[bucket][1];
});
maxSubmissionsPerBucket = Math.max(maxSubmissionsPerBucket, sum);
}

// Pick a nice round tickDelta and tickValues based on the step size of units.
// We want whole values in the unit, and the ticks MUST match a corresponding bucket otherwise the resulting
// coordinate will be NaN.
const convertFactor = secondsPerBucket / unit.convert;
const maxTicks = Math.min(bucketCount, contestDuration / unit.step, minBucketCount)
const tickDelta = convertFactor * Math.ceil(contestDuration / (maxTicks * convertFactor));
const ticks = Math.floor(contestDuration / tickDelta) + 1;
const tickValues = Array.from({ length: ticks }, (_, i) => i * tickDelta);

nv.addGraph(function () {
var chart = nv.models.multiBarChart()
.showControls(false)
.stacked(true)
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.showYAxis(true)
.showXAxis(true)
.reduceXTicks(false)
;
chart.xAxis //Chart x-axis settings
.axisLabel(`Contest Time (${unit.name})`)
.ticks(tickValues.length)
.tickValues(tickValues)
.tickFormat(d3.format('d'));
chart.yAxis //Chart y-axis settings
.axisLabel('Total Submissions')
.tickFormat(d3.format('d'));

d3.select('#graph_submissions svg')
.datum(submissionStats)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}

$(function() {
if (document.querySelector('.mobile-scoreboard')) {
window.addEventListener('resize', resizeMobileTeamNamesAndProblemBadges);
Expand Down
101 changes: 3 additions & 98 deletions webapp/templates/jury/analysis/contest_overview.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,10 @@ nv.addGraph(function() {
return chart;
});


//////////////////////////////////////
// Submissions over time
// stacked graph of correct, runtime-error, wrong-answer, compiler-error, timelimit, etc
// x-axis is contest time
// y axis is # of submissions

var submission_stats = [
const submission_stats = [
{% for result in ['correct', 'wrong-answer', 'timelimit', 'run-error', 'compiler-error', 'no-output'] %}
{
key: "{{result}}",
Expand All @@ -354,8 +350,8 @@ var submission_stats = [
},
{% endfor %}
];

const contest_start_time = {{ current_contest.starttime }};
const contest_duration_seconds = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
const submissions = [
{% for submission in submissions %}
{
Expand All @@ -364,98 +360,7 @@ const submissions = [
}{{ loop.last ? '' : ',' }}
{% endfor %}
];

const min_bucket_count = 30;
const max_bucket_count = 301;
const units = [
{'name': 'seconds', 'convert': 1, 'step': 60},
{'name': 'minutes', 'convert': 60, 'step': 15},
{'name': 'hours', 'convert': 60*60, 'step': 6},
{'name': 'days', 'convert': 60*60*24, 'step': 7},
{'name': 'weeks', 'convert': 60*60*24*7, 'step': 1},
{'name': 'years', 'convert': 60*60*24*365, 'step': 1}
];
let unit = units[0];

let contest_duration = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
for (let u of units) {
const new_duration = Math.ceil(contest_duration / u.convert);
if (new_duration > min_bucket_count) {
unit = u;
} else {
break;
}
}
contest_duration = Math.ceil(contest_duration / unit.convert);
const bucket_count = Math.min(contest_duration + 1, max_bucket_count);
// Make sure buckets have whole unit
const seconds_per_bucket = Math.ceil(contest_duration / (bucket_count - 1)) * unit.convert;

submission_stats.forEach(stat => {
stat.values = Array.from({ length: bucket_count }, (_, i) => [i * seconds_per_bucket / unit.convert, 0]);
});

const statMap = submission_stats.reduce((map, stat) => {
map[stat.key] = stat;
return map;
}, {});

submissions.forEach(submission => {
const submission_bucket = Math.floor((submission.submittime - contest_start_time) / seconds_per_bucket);
const stat = statMap[submission.result];
if (stat && submission_bucket >= 0 && submission_bucket < bucket_count) {
stat.values[submission_bucket][1]++;
}
});

let max_submissions_per_bucket = 1
for (let bucket = 0; bucket < bucket_count; bucket++) {
let sum = 0;
submission_stats.forEach(stat => {
sum += stat.values[bucket][1];
});
max_submissions_per_bucket = Math.max(max_submissions_per_bucket, sum);
}

// Pick a nice round tickDelta and tickValues based on the step size of units.
// We want whole values in the unit, and the ticks MUST match a corresponding bucket otherwise the resulting
// coordinate will be NaN.
const convert_factor = seconds_per_bucket / unit.convert;
const maxTicks = Math.min(bucket_count, contest_duration / unit.step, min_bucket_count)
const tickDelta = convert_factor * Math.ceil(contest_duration / (maxTicks * convert_factor));
const ticks = Math.floor(contest_duration / tickDelta) + 1;
const tickValues = Array.from({ length: ticks }, (_, i) => i * tickDelta);

nv.addGraph(function() {
var chart = nv.models.multiBarChart()
// .margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room.
// .useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
// .transitionDuration(350) //how fast do you want the lines to transition?
// .showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showControls(false)
.stacked(true)
.x(function(d) { return d[0] }) //We can modify the data accessor functions...
.y(function(d) { return d[1] }) //...in case your data is formatted differently.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
.reduceXTicks(false)
;
chart.xAxis //Chart x-axis settings
.axisLabel(`Contest Time (${unit.name})`)
.ticks(tickValues.length)
.tickValues(tickValues)
.tickFormat(d3.format('d'));
chart.yAxis //Chart y-axis settings
.axisLabel('Total Submissions')
.tickFormat(d3.format('d'));

d3.select('#graph_submissions svg')
.datum(submission_stats)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});

createSubmissionGraph(submission_stats, contest_start_time, contest_duration_seconds, submissions);

</script>
{% include 'jury/analysis/download_graphs.html.twig' %}
Expand Down
67 changes: 14 additions & 53 deletions webapp/templates/jury/analysis/problem.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
</div>
</div>
</div>
<div class="col-lg-5 col-sm-12 mt-3" id="submission_times">
<div class="col-lg-5 col-sm-12 mt-3" id="graph_submissions">
<div class="card">
<div class="card-header">
Submissions over Time
Expand Down Expand Up @@ -214,68 +214,29 @@ $(function(){
return chart;
});


//////////////////////////////////////
// Submissions over time
// stacked graph of correct, runtime-error, wrong-answer, compiler-error, timelimit, etc
// x-axis is contest time
// y axis is # of submissions

var submission_stats = [
const submission_stats = [
{% for result in ['correct', 'wrong-answer', 'timelimit', 'run-error', 'compiler-error', 'no-output'] %}
{
key: "{{result}}",
color: "{{colors[result]}}",
values: [
{# TODO: make sure these are actually ordered by submittime #}
{# TODO: also make sure these submissions are in the same contest #}
[0,0],
{% set count = 0 %}
{% for submission in submissions | filter(submission => submission.result) %}
{% if submission.result == result %}{% set count = count +1 %}{% endif %}
[ {{ (submission.submittime - current_contest.starttime)/60.0 }},
{{ count }}
],
{% endfor %}
]
values: []
},
{% endfor %}
];
nv.addGraph(function() {
var chart = nv.models.stackedAreaChart()
// .margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room.
// .useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
// .transitionDuration(350) //how fast do you want the lines to transition?
// .showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showControls(false)
.x(function(d) { return d[0] }) //We can modify the data accessor functions...
.y(function(d) { return d[1] }) //...in case your data is formatted differently.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
.forceX([0, {{ (current_contest.endtime - current_contest.starttime) / 60 }}])
.forceY([0, {{ submissions|length *1.10 }}])
;
chart.xAxis //Chart x-axis settings
.axisLabel('Contest Time(minutes)')
.tickFormat(d3.format('d'));

chart.yAxis //Chart y-axis settings
.axisLabel('Total Submissions')
.tickFormat(d3.format('d'));

d3.select('#submission_times svg')
.datum(submission_stats)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});







const contest_start_time = {{ current_contest.starttime }};
const contest_duration_seconds = {{ (current_contest.endtime - current_contest.starttime) | round(0, 'ceil') }};
const submissions = [
{% for submission in submissions %}
{
result: "{{ submission.result }}",
submittime: {{ submission.submittime }},
}{{ loop.last ? '' : ',' }}
{% endfor %}
];
createSubmissionGraph(submission_stats, contest_start_time, contest_duration_seconds, submissions);
})
</script>
{% include 'jury/analysis/download_graphs.html.twig' %}
Expand Down
Loading