Skip to content

Commit

Permalink
Compound lineplot logic basically working
Browse files Browse the repository at this point in the history
  • Loading branch information
Casper-Guo committed Aug 20, 2024
1 parent 8917822 commit f8f0844
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 11 deletions.
75 changes: 73 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Dash app layout and callbacks."""

from typing import TypeAlias
from pathlib import Path
from typing import Iterable, TypeAlias

import dash_bootstrap_components as dbc
import fastf1 as f
import pandas as pd
from dash import Dash, Input, Output, State, callback
import tomli
from dash import Dash, Input, Output, State, callback, html
from plotly import graph_objects as go

import f1_visualization.plotly_dash.graphs as pg
Expand All @@ -18,6 +20,16 @@
# must not be modified
DF_DICT = load_laps()

with open(
Path(__file__).absolute().parent
/ "f1_visualization"
/ "plotly_dash"
/ "visualization_config.toml",
"rb",
) as toml:
# TODO: revisit the palette
COMPOUND_PALETTE = tomli.load(toml)["relative"]["high_contrast_palette"]


def df_convert_timedelta(df: pd.DataFrame) -> pd.DataFrame:
"""
Expand Down Expand Up @@ -144,6 +156,23 @@ def enable_load_session(season: int | None, event: str | None, session: str | No
return not (season is not None and event is not None and session is not None)


def create_compound_dropdown_options(compounds: Iterable[str]) -> list[dict]:
"""Create compound dropdown options with styling."""
# sort the compounds
compound_order = ["SOFT", "MEDIUM", "HARD", "INTERMEDIATE", "WET"]
compound_index = [compound_order.index(compound) for compound in compounds]
sorted_compounds = sorted(zip(compounds, compound_index), key=lambda x: x[1])
compounds = [compound for compound, _ in sorted_compounds]

return [
{
"label": html.Span(compound, style={"color": COMPOUND_PALETTE[compound]}),
"value": compound,
}
for compound in compounds
]


@callback(
Output("session-info", "data"),
Input("load-session", "n_clicks"),
Expand Down Expand Up @@ -415,5 +444,47 @@ def render_distplot(
return fig


@callback(
Output("compound-plot", "figure"),
Input("compounds", "value"),
Input("compound-type", "value"),
Input("compound-unit", "value"),
State("laps", "data"),
State("session-info", "data"),
)
def render_compound_plot(
compounds: list[str],
plot_type: str,
show_seconds: bool,
included_laps: dict,
session_info: Session_info,
) -> go.Figure:
"""Filter laps and render compound performance plot."""
if not included_laps or not compounds:
return go.Figure()

included_laps = pd.DataFrame.from_dict(included_laps)
included_laps = included_laps[included_laps["Compound"].isin(compounds)]

y = "DeltaToLapRep" if show_seconds else "PctFromLapRep"
fig = go.Figure()

match plot_type:
case "lineplot":
fig = pg.compounds_lineplot(included_laps, y, compounds)
case "boxplot":
fig = pg.compounds_distplot(included_laps, y, compounds, False)
case "violinplot":
fig = pg.compounds_distplot(included_laps, y, compounds, True)
case _:
# this should never be triggered
# but just in case, return empty plot
return fig

event_name = session_info[1]
fig.update_layout(title=event_name)
return fig


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
77 changes: 68 additions & 9 deletions f1_visualization/plotly_dash/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def stats_scatterplot(
"color": driver_laps[args[0]].map(args[1]),
"symbol": driver_laps["FreshTyre"].map(VISUAL_CONFIG["fresh"]["markers"]),
},
name=f"{driver}",
name=driver,
),
row=row,
col=col,
Expand Down Expand Up @@ -209,21 +209,16 @@ def stats_lineplot(
if y in {"PctFromLapRep", "DeltaToLapRep"}:
included_laps = included_laps[included_laps["PctFromLapRep"] > -5]

for index, driver in enumerate(reversed(drivers)):
for _, driver in enumerate(reversed(drivers)):
driver_laps = included_laps[(included_laps["Driver"] == driver)]

# the top left subplot is indexed (1, 1)
row, col = divmod(index, 4)
row += 1
col += 1

fig.add_trace(
go.Scatter(
x=driver_laps["LapNumber"],
y=driver_laps[y],
mode="lines",
line={"color": pick_driver_color(driver)},
name=f"{driver}",
name=driver,
)
)

Expand Down Expand Up @@ -253,7 +248,7 @@ def stats_distplot(
drivers: list[str],
boxplot: bool,
) -> go.Figure:
"""Make distribution plot of lap times, with optional swarm and boxplots."""
"""Make distribution plot of lap times, either as boxplot or as violin plot."""
fig = go.Figure()

for driver in drivers:
Expand Down Expand Up @@ -292,3 +287,67 @@ def stats_distplot(
height=500,
)
return fig


def compounds_lineplot(included_laps: pd.DataFrame, y: str, compounds: list[str]) -> go.Figure:
"""Plot compound degradation curve as a lineplot."""
fig = go.Figure()
yaxis_title = "Seconds to LRT" if y == "DeltaToLapRep" else "Percent from LRT"

# TODO: remove this hard-coded value
_, palette, marker, _ = _plot_args(2024)

for compound in compounds:
compound_laps = included_laps[included_laps["Compound"] == compound]
# clip tyre life range to where there are at least three records
tyre_life_range = compound_laps.groupby("TyreLife").size()
tyre_life_range = tyre_life_range[tyre_life_range >= 3].index
median_LRT = compound_laps.groupby("TyreLife")[y].median(numeric_only=True) # noqa: N806
median_LRT = median_LRT.loc[tyre_life_range] # noqa: N806

fig.add_trace(
go.Scatter(
x=tyre_life_range,
y=median_LRT,
line={"color": palette[compound]},
marker={
# TODO: tune these parameters
"line": {"width": 1, "color": "white"},
"color": palette[compound],
"symbol": marker[compound],
"size": 8,
},
mode="lines+markers",
name=compound,
)
)

fig.update_layout(
template="plotly_dark",
xaxis_title="Tyre Age",
yaxis_title=yaxis_title,
showlegend=False,
autosize=False,
width=1250,
height=500,
)
return fig


def compounds_distplot(
included_laps: pd.DataFrame, y: str, compounds: list[str], violin_plot: bool
) -> go.Figure:
"""PLot compound performance vs tyre age as either a boxplot or violin plot."""
fig = go.Figure()
yaxis_title = "Seconds to LRT" if y == "DeltaToLapRep" else "Percent from LRT"

fig.update_layout(
template="plotly_dark",
xaxis_title="Tyre Age",
yaxis_title=yaxis_title,
showlegend=False,
autosize=False,
width=1250,
height=500,
)
return fig
10 changes: 10 additions & 0 deletions f1_visualization/plotly_dash/visualization_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ INTERMEDIATE = "#43b02a"
WET = "#0067ad"
UNKNOWN = "#00ffff"

# For display on a white background
# the customary palette is kept unchanged where the contrast is sufficiently high
[relative.high_contrast_palette]
SOFT = "#da291c"
MEDIUM = "8a6d00"
HARD = "#000000"
INTERMEDIATE = "#276719"
WET = "#0067ad"
UNKNOWN = "#004aaa"

[relative.markers]
SOFT = "circle"
MEDIUM = "triangle-up"
Expand Down

0 comments on commit f8f0844

Please sign in to comment.