Add diagrams to stats page
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 16s

This commit is contained in:
2024-03-03 03:19:51 +01:00
parent a3d234a754
commit 0d598e75a2
4 changed files with 285 additions and 87 deletions

View File

@ -40,14 +40,10 @@ import formula10.controller.error_controller
# - For season guess calc there is missing: Fastest laps + sprint points + sprint DNFs (in race result) # - For season guess calc there is missing: Fastest laps + sprint points + sprint DNFs (in race result)
# - Auto calculate season points (display season points?) # - Auto calculate season points (display season points?)
# - Generate static diagram using chart.js + templating the js (funny yikes)
# - Interesting stats: # - Interesting stats:
# - Which driver was voted most for dnf (top 5)? # - Which driver was voted most for dnf (top 5)?
# General # General
# - Decouple names from IDs + Fix Valtteri/Russel spelling errors # - Decouple names from IDs + Fix Valtteri/Russel spelling errors
# - Unit testing (as much as possible, but especially points calculation) # - Unit testing (as much as possible, but especially points calculation)
# - Add links to the official F1 stats page (for quali/result), probably best to store entire link in DB (because they are not entirely regular)? # - Add links to the official F1 stats page (for quali/result), probably best to store entire link in DB (because they are not entirely regular)?
# Possible but probably not
# - Show cards of previous race results, like with season guesses?

View File

@ -1,5 +1,5 @@
import json import json
from typing import Any, Callable, Dict, List, Tuple, overload from typing import Any, Callable, Dict, List, overload
import numpy as np import numpy as np
from formula10.domain.domain_model import Model from formula10.domain.domain_model import Model
@ -105,8 +105,8 @@ class PointsModel(Model):
""" """
_points_per_step: Dict[str, List[int]] | None = None _points_per_step: Dict[str, List[int]] | None = None
_wdc_points: Dict[str, int] | None = None _driver_points_per_step: Dict[str, List[int]] | None = None
_wcc_points: Dict[str, int] | None = None _team_points_per_step: Dict[str, List[int]] | None = None
_dnfs: Dict[str, int] | None = None _dnfs: Dict[str, int] | None = None
def __init__(self): def __init__(self):
@ -134,32 +134,42 @@ class PointsModel(Model):
return self._points_per_step return self._points_per_step
# @todo Doesn't include fastest lap + sprint points # @todo Doesn't include fastest lap + sprint points
def wdc_points(self) -> Dict[str, int]: def driver_points_per_step(self) -> Dict[str, List[int]]:
if self._wdc_points is None: """
self._wdc_points = dict() Returns a dictionary of lists, containing points per race for each driver.
"""
if self._driver_points_per_step is None:
self._driver_points_per_step = dict()
for driver in self.all_drivers(include_none=False): for driver in self.all_drivers(include_none=False):
self._wdc_points[driver.name] = 0 self._driver_points_per_step[driver.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for position, driver in race_result.standing.items(): for position, driver in race_result.standing.items():
self._wdc_points[driver.name] += DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0 driver_name: str = driver.name
race_number: int = race_result.race.number
self._driver_points_per_step[driver_name][race_number] = DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
return self._wdc_points return self._driver_points_per_step
def wcc_points(self) -> Dict[str, int]:
if self._wcc_points is None:
self._wcc_points = dict()
# @todo Doesn't include fastest lap + sprint points
def team_points_per_step(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing points per race for each team.
"""
if self._team_points_per_step is None:
self._team_points_per_step = dict()
for team in self.all_teams(include_none=False): for team in self.all_teams(include_none=False):
self._wcc_points[team.name] = 0 self._team_points_per_step[team.name] = [0] * (len(self.all_races()) + 1) # Start at index 1, like the race numbers
for race_result in self.all_race_results(): for race_result in self.all_race_results():
for driver in race_result.standing.values(): for position, driver in race_result.standing.items():
self._wcc_points[driver.team.name] += self.wdc_points()[driver.name] team_name: str = driver.team.name
race_number: int = race_result.race.number
return self._wcc_points self._team_points_per_step[team_name][race_number] += DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0
return self._team_points_per_step
# @todo Doesn't include sprint dnfs # @todo Doesn't include sprint dnfs
def dnfs(self) -> Dict[str, int]: def dnfs(self) -> Dict[str, int]:
@ -175,38 +185,79 @@ class PointsModel(Model):
return self._dnfs return self._dnfs
def wdc_diff_2023(self) -> Dict[str, int]: #
diff: Dict[str, int] = dict() # Driver stats
#
for driver in self.all_drivers(include_none=False): def driver_points_per_step_cumulative(self) -> Dict[str, List[int]]:
diff[driver.name] = WDC_STANDING_2023[driver.name] - self.wdc_standing_by_driver()[driver.name] """
Returns a dictionary of lists, containing cumulative points per race for each driver.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for driver_name, points in self.driver_points_per_step().items():
points_per_step_cumulative[driver_name] = np.cumsum(points).tolist()
return diff return points_per_step_cumulative
def wcc_diff_2023(self) -> Dict[str, int]: @overload
diff: Dict[str, int] = dict() def driver_points_by(self, *, driver_name: str) -> List[int]:
"""
Returns a list of points per race for a specific driver.
"""
return self.driver_points_by(driver_name=driver_name)
for team in self.all_teams(include_none=False): @overload
diff[team.name] = WCC_STANDING_2023[team.name] - self.wcc_standing_by_team()[team.name] def driver_points_by(self, *, race_name: str) -> Dict[str, int]:
"""
Returns a dictionary of points per driver for a specific race.
"""
return self.driver_points_by(race_name=race_name)
return diff @overload
def driver_points_by(self, *, driver_name: str, race_name: str) -> int:
"""
Returns the points for a specific race for a specific driver.
"""
return self.driver_points_by(driver_name=driver_name, race_name=race_name)
def driver_points_by(self, *, driver_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int:
if driver_name is not None and race_name is None:
return self.driver_points_per_step()[driver_name]
if driver_name is None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
points_by_race: Dict[str, int] = dict()
for _driver_name, points in self.driver_points_per_step().items():
points_by_race[_driver_name] = points[race_number]
return points_by_race
if driver_name is not None and race_name is not None:
race_number: int = self.race_by(race_name=race_name).number
return self.driver_points_per_step()[driver_name][race_number]
raise Exception("driver_points_by received an illegal combination of arguments")
def total_driver_points_by(self, driver_name: str) -> int:
return sum(self.driver_points_by(driver_name=driver_name))
def wdc_standing_by_position(self) -> Dict[int, List[str]]: def wdc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict() standing: Dict[int, List[str]] = dict()
for position in range(1, 21): for position in range(1, len(self.all_drivers(include_none=False)) + 1):
standing[position] = list() standing[position] = list()
position: int = 1 position: int = 1
last_points: int = 0 last_points: int = 0
comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] for driver in self.all_drivers(include_none=False):
for driver_name, points in sorted(self.wdc_points().items(), key=comparator, reverse=True): points: int = self.total_driver_points_by(driver.name)
if points < last_points: if points < last_points:
position += 1 position += 1
standing[position].append(driver_name) standing[position].append(driver.name)
last_points = points last_points = points
return standing return standing
@ -217,53 +268,18 @@ class PointsModel(Model):
position: int = 1 position: int = 1
last_points: int = 0 last_points: int = 0
comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] for driver in self.all_drivers(include_none=False):
for driver_name, points in sorted(self.wdc_points().items(), key=comparator, reverse=True): points: int = self.total_driver_points_by(driver.name)
if points < last_points: if points < last_points:
position += 1 position += 1
standing[driver_name] = position standing[driver.name] = position
last_points = points last_points = points
return standing return standing
def wcc_standing_by_position(self) -> Dict[int, List[str]]: def wdc_diff_2023_by(self, driver_name: str) -> int:
standing: Dict[int, List[str]] = dict() return WDC_STANDING_2023[driver_name] - self.wdc_standing_by_driver()[driver_name]
for position in range (1, 11):
standing[position] = list()
position: int = 1
last_points: int = 0
comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1]
for team_name, points in sorted(self.wcc_points().items(), key=comparator, reverse=True):
if points < last_points:
position += 1
standing[position].append(team_name)
last_points = points
return standing
def wcc_standing_by_team(self) -> Dict[str, int]:
standing: Dict[str, int] = dict()
position: int = 1
last_points: int = 0
comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1]
for team_name, points in sorted(self.wcc_points().items(), key=comparator, reverse=True):
if points < last_points:
position += 1
standing[team_name] = position
last_points = points
return standing
def most_dnf_names(self) -> List[str]: def most_dnf_names(self) -> List[str]:
dnf_names: List[str] = list() dnf_names: List[str] = list()
@ -284,13 +300,13 @@ class PointsModel(Model):
most_gained: int = 0 most_gained: int = 0
for driver in self.all_drivers(include_none=False): for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023()[driver.name] gained: int = self.wdc_diff_2023_by(driver.name)
if gained > most_gained: if gained > most_gained:
most_gained = gained most_gained = gained
for driver in self.all_drivers(include_none=False): for driver in self.all_drivers(include_none=False):
gained: int = self.wdc_diff_2023()[driver.name] gained: int = self.wdc_diff_2023_by(driver.name)
if gained == most_gained: if gained == most_gained:
most_gained_names.append(driver.name) most_gained_names.append(driver.name)
@ -302,13 +318,13 @@ class PointsModel(Model):
most_lost: int = 100 most_lost: int = 100
for driver in self.all_drivers(include_none=False): for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023()[driver.name] lost: int = self.wdc_diff_2023_by(driver.name)
if lost < most_lost: if lost < most_lost:
most_lost = lost most_lost = lost
for driver in self.all_drivers(include_none=False): for driver in self.all_drivers(include_none=False):
lost: int = self.wdc_diff_2023()[driver.name] lost: int = self.wdc_diff_2023_by(driver.name)
if lost == most_lost: if lost == most_lost:
most_lost_names.append(driver.name) most_lost_names.append(driver.name)
@ -319,10 +335,70 @@ class PointsModel(Model):
comparator: Callable[[Driver], int] = lambda driver: self.wdc_standing_by_driver()[driver.name] comparator: Callable[[Driver], int] = lambda driver: self.wdc_standing_by_driver()[driver.name]
return sorted(self.all_drivers(include_none=False), key=comparator) return sorted(self.all_drivers(include_none=False), key=comparator)
#
# Team points
#
def team_points_per_step_cumulative(self) -> Dict[str, List[int]]:
"""
Returns a dictionary of lists, containing cumulative points per race for each team.
"""
points_per_step_cumulative: Dict[str, List[int]] = dict()
for team_name, points in self.team_points_per_step().items():
points_per_step_cumulative[team_name] = np.cumsum(points).tolist()
return points_per_step_cumulative
def total_team_points_by(self, team_name: str) -> int:
teammates: List[Driver] = self.drivers_by(team_name=team_name)
return sum(self.driver_points_by(driver_name=teammates[0].name)) + sum(self.driver_points_by(driver_name=teammates[1].name))
def wcc_standing_by_position(self) -> Dict[int, List[str]]:
standing: Dict[int, List[str]] = dict()
for position in range (1, len(self.all_teams(include_none=False)) + 1):
standing[position] = list()
position: int = 1
last_points: int = 0
for team in self.all_teams(include_none=False):
points: int = self.total_team_points_by(team.name)
if points < last_points:
position += 1
standing[position].append(team.name)
last_points = points
return standing
def wcc_standing_by_team(self) -> Dict[str, int]:
standing: Dict[str, int] = dict()
position: int = 1
last_points: int = 0
for team in self.all_teams(include_none=False):
points: int = self.total_team_points_by(team.name)
if points < last_points:
position += 1
standing[team.name] = position
last_points = points
return standing
def wcc_diff_2023_by(self, team_name: str) -> int:
return WCC_STANDING_2023[team_name] - self.wcc_standing_by_team()[team_name]
def teams_sorted_by_points(self) -> List[Team]: def teams_sorted_by_points(self) -> List[Team]:
comparator: Callable[[Team], int] = lambda team: self.wcc_standing_by_team()[team.name] comparator: Callable[[Team], int] = lambda team: self.wcc_standing_by_team()[team.name]
return sorted(self.all_teams(include_none=False), key=comparator) return sorted(self.all_teams(include_none=False), key=comparator)
#
# User stats
#
def points_per_step_cumulative(self) -> Dict[str, List[int]]: def points_per_step_cumulative(self) -> Dict[str, List[int]]:
""" """
Returns a dictionary of lists, containing cumulative points per race for each user. Returns a dictionary of lists, containing cumulative points per race for each user.
@ -510,3 +586,39 @@ class PointsModel(Model):
] ]
return json.dumps(data) return json.dumps(data)
def cumulative_driver_points_data(self) -> str:
data: Dict[Any, Any] = dict()
data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number)
]
data["datasets"] = [
{
"data": self.driver_points_per_step_cumulative()[driver.name],
"label": driver.name,
"fill": False
}
for driver in self.all_drivers(include_none=False)
]
return json.dumps(data)
def cumulative_team_points_data(self) -> str:
data: Dict[Any, Any] = dict()
data["labels"] = [0] + [
race.name for race in sorted(self.all_races(), key=lambda race: race.number)
]
data["datasets"] = [
{
"data": self.team_points_per_step_cumulative()[team.name],
"label": team.name,
"fill": False
}
for team in self.all_teams(include_none=False)
]
return json.dumps(data)

View File

@ -11,3 +11,17 @@
grid-column-gap: 8px; grid-column-gap: 8px;
} }
} }
.card-grid-2 {
grid-template-columns: repeat(1, minmax(325px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
@media only screen and (min-width: 950px) {
.card-grid-2 {
grid-template-columns: repeat(2, minmax(450px, 1fr));
grid-row-gap: 0;
grid-column-gap: 8px;
}
}

View File

@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div class="grid card-grid"> <div class="grid card-grid-2">
<div class="card shadow-sm mb-2"> <div class="card shadow-sm mb-2">
<div class="card-header"> <div class="card-header">
@ -42,9 +42,9 @@
<tr class="{% if driver_standing == 1 %}table-danger{% endif %}"> <tr class="{% if driver_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ driver_standing }}</td> <td class="text-center text-nowrap">{{ driver_standing }}</td>
<td class="text-center text-nowrap">{{ driver.name }}</td> <td class="text-center text-nowrap">{{ driver.name }}</td>
<td class="text-center text-nowrap">{{ points.wdc_points()[driver.name] }}</td> <td class="text-center text-nowrap">{{ points.total_driver_points_by(driver.name) }}</td>
<td class="text-center text-nowrap">{{ points.dnfs()[driver.name] }}</td> <td class="text-center text-nowrap">{{ points.dnfs()[driver.name] }}</td>
<td class="text-center text-nowrap">{{ "%+d" % points.wdc_diff_2023()[driver.name] }}</td> <td class="text-center text-nowrap">{{ "%+d" % points.wdc_diff_2023_by(driver.name) }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -76,8 +76,8 @@
<tr class="{% if team_standing == 1 %}table-danger{% endif %}"> <tr class="{% if team_standing == 1 %}table-danger{% endif %}">
<td class="text-center text-nowrap">{{ team_standing }}</td> <td class="text-center text-nowrap">{{ team_standing }}</td>
<td class="text-center text-nowrap">{{ team.name }}</td> <td class="text-center text-nowrap">{{ team.name }}</td>
<td class="text-center text-nowrap">{{ points.wcc_points()[team.name] }}</td> <td class="text-center text-nowrap">{{ points.total_team_points_by(team.name) }}</td>
<td class="text-center text-nowrap">{{ points.wcc_diff_2023()[team.name] }}</td> <td class="text-center text-nowrap">{{ points.wcc_diff_2023_by(team.name) }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -86,6 +86,82 @@
</div> </div>
</div> </div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Driver history
</div>
<div class="card-body">
<div class="chart-container" style="width: 100%; height: 40vh;">
<canvas id="driver-line-chart"></canvas>
</div>
<script>
function cumulative_driver_points(data) {
return new Chart(document.getElementById("driver-line-chart"), {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'History'
},
{#tension: 0,#}
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
min: 0,
{#max: 100#}
}
}
}
});
}
cumulative_driver_points({{ points.cumulative_driver_points_data() | safe }})
</script>
</div>
</div>
<div class="card shadow-sm mb-2">
<div class="card-header">
Team history
</div>
<div class="card-body">
<div class="chart-container" style="width: 100%; height: 40vh;">
<canvas id="team-line-chart"></canvas>
</div>
<script>
function cumulative_team_points(data) {
return new Chart(document.getElementById("team-line-chart"), {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'History'
},
{#tension: 0,#}
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
min: 0,
{#max: 100#}
}
}
}
});
}
cumulative_team_points({{ points.cumulative_team_points_data() | safe }})
</script>
</div>
</div>
</div> </div>
{% endblock body %} {% endblock body %}