Skip to content
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

Add page with language stats #2647

Merged
merged 3 commits into from
Aug 16, 2024
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
21 changes: 21 additions & 0 deletions webapp/src/Controller/Jury/AnalysisController.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,25 @@ public function problemAction(
$this->stats->getProblemStats($contest, $problem, $view)
);
}

#[Route(path: '/languages', name: 'analysis_languages')]
public function languagesAction(
#[MapQueryParameter]
?string $view = null
): Response {
$contest = $this->dj->getCurrentContest();

if ($contest === null) {
return $this->render('jury/error.html.twig', [
'error' => 'No contest selected',
]);
}

$filterKeys = array_keys(StatisticsService::FILTERS);
$view = $view ?: reset($filterKeys);

return $this->render('jury/analysis/languages.html.twig',
$this->stats->getLanguagesStats($contest, $view)
);
}
}
115 changes: 115 additions & 0 deletions webapp/src/Service/StatisticsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Entity\Contest;
use App\Entity\ContestProblem;
use App\Entity\Judging;
use App\Entity\Language;
use App\Entity\Problem;
use App\Entity\Submission;
use App\Entity\Team;
Expand Down Expand Up @@ -58,6 +59,7 @@ public function getTeams(Contest $contest, string $filter): array
->join('t.category', 'tc')
->leftJoin('t.affiliation', 'a')
->join('t.submissions', 'ts')
->join('ts.language', 'l')
->join('ts.judgings', 'j')
->andWhere('j.valid = true')
->join('ts.language', 'lang')
Expand All @@ -72,6 +74,7 @@ public function getTeams(Contest $contest, string $filter): array
->join('t.category', 'tc')
->leftJoin('tc.contests', 'cc')
->join('t.submissions', 'ts')
->join('ts.language', 'l')
->join('ts.judgings', 'j')
->andWhere('j.valid = true')
->join('ts.language', 'lang')
Expand Down Expand Up @@ -514,6 +517,118 @@ public function getGroupedProblemsStats(
return $stats;
}

/**
* @return array{
* contest: Contest,
* problems: ContestProblem[],
* filters: array<string, string>,
* view: string,
* languages: array<string, array{
* name: string,
* teams: array<array{
* team: Team,
* solved: int,
* total: int,
* }>,
* team_count: int,
* solved: int,
* not_solved: int,
* total: int,
* problems_solved: array<int, ContestProblem>,
* problems_solved_count: int,
* problems_attempted: array<int, ContestProblem>,
* problems_attempted_count: int,
* }>
* }
*/
public function getLanguagesStats(Contest $contest, string $view): array
{
/** @var Language[] $languages */
$languages = $this->em->getRepository(Language::class)
->createQueryBuilder('l')
->andWhere('l.allowSubmit = 1')
->orderBy('l.name')
->getQuery()
->getResult();

$languageStats = [];

foreach ($languages as $language) {
$languageStats[$language->getLangid()] = [
'name' => $language->getName(),
'teams' => [],
'team_count' => 0,
'solved' => 0,
'not_solved' => 0,
'total' => 0,
'problems_solved' => [],
'problems_solved_count' => 0,
'problems_attempted' => [],
'problems_attempted_count' => 0,
];
}

$teams = $this->getTeams($contest, $view);
foreach ($teams as $team) {
foreach ($team->getSubmissions() as $s) {
if ($s->getContest() != $contest) {
continue;
}
if ($s->getSubmitTime() > $contest->getEndTime()) {
continue;
}
if ($s->getSubmitTime() < $contest->getStartTime()) {
continue;
}
if ($s->getSubmittime() > $contest->getFreezetime()) {
continue;
}

$language = $s->getLanguage();

if (!isset($languageStats[$language->getLangid()]['teams'][$team->getTeamid()])) {
$languageStats[$language->getLangid()]['teams'][$team->getTeamid()] = [
'team' => $team,
'solved' => 0,
'total' => 0,
];
}
$languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['total']++;
$languageStats[$language->getLangid()]['total']++;
if ($s->getResult() === 'correct') {
$languageStats[$language->getLangid()]['solved']++;
$languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['solved']++;
$languageStats[$language->getLangid()]['problems_solved'][$s->getProblem()->getProbId()] = $s->getContestProblem();
} else {
$languageStats[$language->getLangid()]['not_solved']++;
}
$languageStats[$language->getLangid()]['problems_attempted'][$s->getProblem()->getProbId()] = $s->getContestProblem();
}
}

foreach ($languageStats as &$languageStat) {
usort($languageStat['teams'], static function (array $a, array $b): int {
if ($a['solved'] === $b['solved']) {
return $b['total'] <=> $a['total'];
}

return $b['solved'] <=> $a['solved'];
});
$languageStat['team_count'] = count($languageStat['teams']);
$languageStat['problems_solved_count'] = count($languageStat['problems_solved']);
$languageStat['problems_attempted_count'] = count($languageStat['problems_attempted']);
}
unset($languageStat);

return [
'contest' => $contest,
'problems' => $this->getContestProblems($contest),
'filters' => StatisticsService::FILTERS,
'view' => $view,
'languages' => $languageStats,
];
}

/**
* Apply the filter to the given query builder.
*/
Expand Down
9 changes: 8 additions & 1 deletion webapp/src/Twig/TwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -1061,9 +1061,12 @@ public function fileTypeIcon(string $type): string
return 'fas fa-file-' . $iconName;
}

public function problemBadge(ContestProblem $problem): string
public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string
{
$rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff');
if ($grayedOut) {
$rgb = 'whitesmoke';
}
$background = Utils::parseHexColor($rgb);

// Pick a border that's a bit darker.
Expand All @@ -1075,6 +1078,10 @@ public function problemBadge(ContestProblem $problem): string

// Pick the foreground text color based on the background color.
$foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff';
if ($grayedOut) {
$foreground = 'silver';
$border = 'linen';
}
return sprintf(
'<span class="badge problem-badge" style="background-color: %s; border: 1px solid %s"><span style="color: %s;">%s</span></span>',
$rgb,
Expand Down
4 changes: 4 additions & 0 deletions webapp/templates/jury/analysis/contest_overview.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ $(function() {
<div class="card">
<div class="card-header">
Language Stats
<a href="{{ path('analysis_languages', {'view': view}) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-list"></i>
Details
</a>
</div>
<div class="card-body">
<svg style="height: 300px"></svg>
Expand Down
100 changes: 100 additions & 0 deletions webapp/templates/jury/analysis/languages.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{% extends "jury/base.html.twig" %}

{% block title %}Analysis - Languages in {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %}

{% block content %}
<h1>Language stats</h1>
{% include 'jury/partials/analysis_filter.html.twig' %}

<div class="row">
{% for langid, language in languages %}
<div class="col-12 mt-3">
<div class="card">
<div class="card-header">
{{ language.name }}
</div>
<div class="card-body">
<i class="fas fa-users fa-fw"></i> {{ language.team_count }} team{% if language.team_count != 1 %}s{% endif %}
{% if language.team_count > 0 %}
<div class="btn-group" role="group">
<input type="checkbox"
class="btn-check team-list-toggle"
id="team-list-toggle-{{ langid }}"
data-team-list-container="#team-list-{{ langid }}"
autocomplete="off" />
<label for="team-list-toggle-{{ langid }}" class="btn-sm btn btn-outline-secondary">
<i class="fas fa-eye"></i> Show
</label>
</div>
<div class="card mt-2 mb-2 d-none" id="team-list-{{ langid }}">
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
<tr>
<td colspan="2">Team</td>
<td>Number of solved problems in {{ language.name }}</td>
<td>Total attempts in {{ language.name }}</td>
</tr>
</thead>
<tbody>
{% for team in language.teams %}
<tr>
<td>
<a href="{{ path('jury_team', {'teamId': team.team.teamid}) }}">
{{ team.team | entityIdBadge('t') }}
</a>
</td>
<td>
<a href="{{ path('jury_team', {'teamId': team.team.teamid}) }}">
{{ team.team.effectiveName }}
</a>
</td>
<td>{{ team.solved }}</td>
<td>{{ team.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<br/>
<i class="fas fa-file-code fa-fw"></i> {{ language.total }} total submission{% if language.total != 1 %}s{% endif %}
for {{ language.problems_attempted_count }} problem{% if language.problems_attempted_count != 1 %}s{% endif %}:<br/>
{% for problem in problems %}
<a href="{{ path('jury_problem', {'probId': problem.probid}) }}">
{{ problem | problemBadge(language.problems_attempted[problem.probid] is not defined) }}
</a>
{% endfor %}
<br />
<i class="fas fa-check fa-fw"></i> {{ language.solved }} submission{% if language.solved != 1 %}s{% endif %} solved problems
for {{ language.problems_solved_count }} problem{% if language.problems_solved_count != 1 %}s{% endif %}:<br/>
{% for problem in problems %}
<a href="{{ path('jury_problem', {'probId': problem.probid}) }}">
{{ problem | problemBadge(language.problems_solved[problem.probid] is not defined) }}
</a>
{% endfor %}
<br />
<i class="fas fa-xmark fa-fw"></i> {{ language.not_solved }} submission{% if language.not_solved != 1 %}s{% endif %} did not solve a problem<br />
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

{% block extrafooter %}
<script>
$('.team-list-toggle').on('change', function () {
const $container = $($(this).data('team-list-container'));
const $label = $(this).parent().find('label');
if ($(this).is(':checked')) {
$container.removeClass('d-none');
$label.html('<i class="fas fa-eye-slash"></i> Hide');
} else {
$container.addClass('d-none');
$label.html('<i class="fas fa-eye"></i> Show');
}
});
</script>
{% endblock %}
Loading