fastlap_app.py
1 import dash 2 import dash_bootstrap_components as dbc 3 import pandas as pd 4 from dash import dash_table, dcc, html 5 from dash.dependencies import Input, Output 6 7 # from django.db.models import Count 8 from django_plotly_dash import DjangoDash 9 10 from telemetry.models import Driver 11 from telemetry.pitcrew.history import History 12 from telemetry.pitcrew.message import MessageTrackGuide 13 from telemetry.racing_stats import RacingStats 14 from telemetry.visualizer import fig_add_features, lap_fig 15 16 app = DjangoDash("Fastlap", serve_locally=True, add_bootstrap_links=True) 17 18 app.layout = html.Div( 19 [ 20 dcc.Dropdown(id="game-dropdown", multi=False, placeholder="Select Game"), 21 dcc.Dropdown(id="car-dropdown", multi=False, placeholder="Select Car"), 22 dcc.Dropdown(id="track-dropdown", multi=False, placeholder="Select Track"), 23 html.Div(id="data-table"), 24 dbc.Alert("fast lap", id="fast-lap-info", color="info"), 25 html.Div(id="graphs-container"), 26 html.Button("Submit", id="submit-val", n_clicks=0, style={"display": "none"}), 27 ] 28 ) 29 30 31 @app.callback(Output("game-dropdown", "options"), Output("game-dropdown", "value"), [Input("submit-val", "n_clicks")]) 32 def set_games_options(n_clicks, session_state=None): 33 racing_stats = RacingStats() 34 laps = list(racing_stats.fast_lap_values()) 35 games = sorted(set(lap["game__name"] for lap in laps)) 36 game = None 37 if session_state is not None: 38 game = session_state.get("game__name") 39 return [{"label": game, "value": game} for game in games], game 40 41 42 @app.callback( 43 Output("car-dropdown", "options"), 44 Output("car-dropdown", "value"), 45 Output("car-dropdown", "disabled"), 46 [Input("game-dropdown", "value")], 47 ) 48 def set_cars_options(game, session_state=None): 49 if game is not None: 50 racing_stats = RacingStats() 51 laps = list(racing_stats.fast_lap_values()) 52 cars = sorted(set(lap["car__name"] for lap in laps if lap["game__name"] == game)) 53 car = None 54 if session_state is not None: 55 # set game to session_state 56 session_state["game__name"] = game 57 car = session_state.get("car__name") 58 return [{"label": car, "value": car} for car in cars], car, False 59 return [], None, True 60 61 62 @app.callback( 63 Output("track-dropdown", "options"), 64 Output("track-dropdown", "value"), 65 Output("track-dropdown", "disabled"), 66 [Input("game-dropdown", "value"), Input("car-dropdown", "value")], 67 ) 68 def set_tracks_options(game, car, session_state=None): 69 if game is not None and car is not None: 70 racing_stats = RacingStats() 71 laps = list(racing_stats.fast_lap_values()) 72 tracks = sorted( 73 set(lap["track__name"] for lap in laps if lap["game__name"] == game and lap["car__name"] == car) 74 ) 75 track = None 76 if session_state is not None: 77 # set game and car to session_state 78 session_state["game__name"] = game 79 session_state["car__name"] = car 80 track = session_state.get("track__name") 81 return [{"label": track, "value": track} for track in tracks], track, False 82 return [], None, True 83 84 85 @app.callback( 86 Output("data-table", "children"), 87 [Input("game-dropdown", "value"), Input("car-dropdown", "value"), Input("track-dropdown", "value")], 88 ) 89 def update_table(game, car, track, session_state=None): 90 # if all([game, car, track]): 91 # return dash.no_update 92 93 if session_state is not None: 94 session_state["game__name"] = game 95 session_state["car__name"] = car 96 session_state["track__name"] = track 97 98 racing_stats = RacingStats() 99 # laps = racing_stats.fast_lap_values() 100 laps = racing_stats.known_combos(valid=True) 101 # laps = laps.annotate(count=Count("id")) 102 laps = list(laps) 103 104 data = [ 105 lap 106 for lap in laps 107 if (game is None or lap["track__game__name"] == game) 108 and (car is None or lap["car__name"] == car) 109 and (track is None or lap["track__name"] == track) 110 ] 111 112 return dash_table.DataTable( 113 columns=[ 114 {"name": "Game", "id": "track__game__name"}, 115 {"name": "Car", "id": "car__name"}, 116 {"name": "Track", "id": "track__name"}, 117 {"name": "Laps", "id": "count"}, 118 ], 119 data=data, 120 style_cell={"textAlign": "left"}, 121 style_as_list_view=True, 122 cell_selectable=False, 123 # row_selectable=True, 124 style_header={"backgroundColor": "lightgrey", "fontWeight": "bold"}, 125 ) 126 127 128 # https://dash.plotly.com/datatable 129 # @app.callback(Output('tbl_out', 'children'), Input('tbl', 'active_cell')) 130 # def update_graphs(active_cell): 131 # return str(active_cell) if active_cell else "Click the table" 132 133 134 @app.callback( 135 Output("graphs-container", "children"), 136 Output("fast-lap-info", "children"), 137 [ 138 Input("submit-val", "n_clicks"), 139 Input("game-dropdown", "value"), 140 Input("car-dropdown", "value"), 141 Input("track-dropdown", "value"), 142 ], 143 ) 144 def update_graph(n_clicks, game, car, track, session_state=None): 145 if n_clicks is None or not all([game, car, track]): 146 return dash.no_update 147 148 driver = None 149 if session_state is not None: 150 session_state["game__name"] = game 151 session_state["car__name"] = car 152 session_state["track__name"] = track 153 154 driver_name = session_state.get("driver_name") 155 if driver_name is not None: 156 driver = Driver.objects.filter(name=driver_name).first() 157 158 racing_stats = RacingStats() 159 fast_laps = list(racing_stats.fast_laps(game=game, track=track, car=car)) 160 laps_count = racing_stats.laps(game=game, track=track, car=car, valid=True).count() 161 162 if len(fast_laps) == 0: 163 return dash.no_update 164 if driver is not None: 165 driver_name = driver.name 166 else: 167 driver_name = "Jim" 168 169 history = History() 170 history.set_filter({"GameName": game, "TrackCode": track, "CarModel": car, "Driver": driver_name}) 171 history.init() 172 # fast_lap = fast_laps[0] 173 # segments = fast_lap.data.get("segments", []) 174 # for i, segment in enumerate(segments): 175 # next_index = (i + 1) % len(segments) 176 # previous_index = (i - 1) % len(segments) 177 # segment.previous_segment = segments[previous_index] 178 # segment.next_segment = segments[next_index] 179 180 segments = history.segments 181 182 graphs = [] 183 if driver is not None: 184 driver_name = driver.name 185 graphs.append(html.H2(f"Showing stats for driver {driver_name}")) 186 delta = history.driver_delta() 187 if delta >= -1000: 188 graphs.append(html.H3(f"Delta: {delta:.3f}")) 189 else: 190 graphs.append(html.H3("No laps recorded")) 191 192 for segment in segments: 193 sector = segment.telemetry 194 fig = lap_fig(sector, columns=["Throttle", "Brake"]) 195 brake_features = segment.brake_features() 196 throttle_features = segment.throttle_features() 197 if brake_features: 198 fig_add_features(fig, brake_features) 199 if throttle_features: 200 fig_add_features(fig, throttle_features, color="green") 201 202 title = f"{segment.time:.3f} seconds - {segment.start}m to {segment.end}m" 203 fig.update_layout(title=dict(text=title)) 204 graph = dcc.Graph(figure=fig) 205 md = get_segment_header(segment, segment.turn) 206 graphs.append(dcc.Markdown(md)) 207 graphs.append(graph) 208 209 if True: 210 # graphs.append(html.Hr()) 211 # header = html.H6(f"type: {segment.type}") 212 # graphs.append(header) 213 brake_features = segment.brake_features() 214 throttle_features = segment.throttle_features() 215 data = [brake_features, throttle_features] 216 df = pd.DataFrame.from_records(data, index=["brake", "throttle"]) 217 table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm") 218 graphs.append(table) 219 gear_features = segment.gear_features() 220 data = [gear_features["distance_gear"]] 221 df = pd.DataFrame.from_records(data, index=["gear"]) 222 table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm") 223 graphs.append(table) 224 data = [ 225 { 226 "brake / throttle": segment.brake_point() or segment.throttle_point(), 227 "force": segment.brake_force() or segment.throttle_force(), 228 "turn_in": segment.turn_in(), 229 "apex": segment.apex(), 230 "gear": segment.gear(), 231 } 232 ] 233 df = pd.DataFrame.from_records(data, index=["markers"]) 234 table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm") 235 graphs.append(table) 236 # tb = html.Div(f"tb: {segment.trail_brake()} - {segment._tb_reason}") 237 # sector from {segment.start} to {segment.end} 238 # graphs.append(tb) 239 240 if driver is not None: 241 graphs.append(html.Hr()) 242 graphs.append(html.Pre(f"Driver delta: {segment.driver_delta():.3f}")) 243 244 graphs.append(html.Hr()) 245 246 info = ( 247 f"Based on a lap time of { history.lap_time_human() } extracted from { history.fast_lap.laps.count() } laps - " 248 ) 249 laps_count = racing_stats.laps(game=game, track=track, car=car, valid=True).count() 250 info += f"Valid laps: {laps_count}" 251 252 return graphs, info 253 254 255 def get_segment_header(segment, turn): 256 message = MessageTrackGuide(segment) 257 msg = message.msg 258 259 md = f""" 260 ## Turn {turn} 261 {message.at} meters: {msg} - read_frame: {message.max_distance - message.at} meters 262 """ 263 264 return md