ssscoring.notebook
Utility reusable code for notebooks.
1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt 2 3""" 4## Utility reusable code for notebooks. 5""" 6 7from ssscoring.calc import forwardLateralDisplacement 8from ssscoring.calc import jumpRunBearing 9from ssscoring.constants import DEFAULT_PLOT_MAX_V_SCALE 10from ssscoring.constants import DEFAULT_SPEED_ACCURACY_SCALE 11from ssscoring.constants import MAX_ALTITUDE_FT 12from ssscoring.constants import MAX_HORIZONTAL_DISTANCE 13from ssscoring.constants import SAFE_HORIZONTAL_COLOR 14from ssscoring.constants import SAFE_HORIZONTAL_DISTANCE 15from ssscoring.constants import SPEED_ACCURACY_THRESHOLD 16from ssscoring.constants import UNSAFE_HORIZONTAL_COLOR 17from ssscoring.datatypes import PerformanceWindow 18from ssscoring.errors import SSScoringError 19 20import pandas as pd 21import plotly.graph_objects as go 22 23 24# *** constants *** 25 26DEFAULT_AXIS_COLOR = 'lightsteelblue' 27""" 28CSS color name for the axis colors used in notebooks and Streamlit with Plotly. 29""" 30 31 32# Ref: https://www.w3schools.com/colors/colors_groups.asp 33SPEED_COLORS = colors = ('#32cd32', '#0000ff', '#ff6347', '#40e0d0', '#00bfff', '#22b822', '#ff7f50', '#008b8b',) 34""" 35Colors used for tracking the lines in a multi-jump plot, so that each track is 36associated with a different color and easier to visualize. 8 distinct colors, 37corresponding to each jump in a competition. 38""" 39 40COLOR_FASTEST = '#32cd32' 41"""Limegreen — fastest jump track color in aggregate displays; matches the table's max-score highlight.""" 42 43COLOR_SLOWEST = '#ff4500' 44"""Orangered — slowest jump track color in aggregate displays; matches the table's min-score highlight.""" 45 46COLORS_OTHERS = ( 47 '#1e90ff', 48 '#4169e1', 49 '#0000cd', 50 '#00bfff', 51 '#6495ed', 52 '#4682b4', 53) 54"""Six distinct blue shades for all non-fastest, non-slowest jumps in aggregate displays.""" 55 56 57# Map Bokeh-era named ranges to Plotly y-axis IDs. Preserves the rangeName 58# kwarg API on graphAltitude/graphAngle/graphAcceleration callers. 59_Y_AXIS_MAP = { 60 'speed': 'y', # default — vKMh, hKMh, score scatter 61 'altitudeFt': 'y2', 62 'angle': 'y3', 63 'vAccelMS2': 'y4', 64 'speedAccuracy': 'y5', 65} 66 67 68# *** functions *** 69 70def initializePlot(jumpTitle: str, 71 height=500, 72 width=900, 73 xLabel='seconds from exit', 74 yLabel='km/h', 75 xMax=35.0, 76 yMax=DEFAULT_PLOT_MAX_V_SCALE, 77 backgroundColorName='#1a1a1a', 78 colorName=DEFAULT_AXIS_COLOR): 79 """ 80 Initialize a Plotly figure for SSScoring jump plots, configured with the 81 main speed (km/h) Y axis. Extra Y axes for altitude / angle / acceleration / 82 speed-accuracy are added by initializeExtraYRanges(). 83 84 Returns 85 ------- 86 A `plotly.graph_objects.Figure`. 87 """ 88 fig = go.Figure() 89 fig.update_layout( 90 title=dict(text=jumpTitle, font=dict(color=colorName)), 91 height=height, 92 autosize=True, 93 plot_bgcolor=backgroundColorName, 94 paper_bgcolor=backgroundColorName, 95 font=dict(color=colorName), 96 hovermode='x unified', 97 showlegend=True, 98 legend=dict(font=dict(color=colorName)), 99 xaxis=dict( 100 title=dict(text=xLabel, font=dict(color=colorName)), 101 autorange=True, 102 color=colorName, 103 tickfont=dict(color=colorName), 104 showgrid=True, # ← changed 105 gridcolor='rgba(255,255,255,0.08)', # ← added — subtle grid on dark bg 106 showline=True, # ← added — visible axis line 107 linecolor=colorName, # ← added 108 zeroline=False, 109 ), 110 yaxis=dict( 111 title=dict(text=yLabel, font=dict(color=colorName)), 112 autorange=True, 113 color=colorName, 114 tickfont=dict(color=colorName), 115 anchor='x', 116 side='left', 117 showgrid=True, # ← changed 118 gridcolor='rgba(255,255,255,0.08)', # ← added 119 showline=True, # ← added 120 linecolor=colorName, # ← added 121 zeroline=False, 122 ), 123 ) 124 return fig 125 126 127def _graphSegment(fig, 128 x0=0.0, 129 y0=0.0, 130 x1=0.0, 131 y1=0.0, 132 lineWidth=1, 133 color='black'): 134 """ 135 Draw a line segment annotation on the plot's main axes. Plotly equivalent 136 of Bokeh's plot.segment(). 137 """ 138 fig.add_shape( 139 type='line', 140 x0=x0, y0=y0, 141 x1=x1, y1=y1, 142 line=dict(color=color, width=lineWidth), 143 xref='x', yref='y', 144 ) 145 146 147def initializeExtraYRanges(fig, 148 startY: float = 0.0, 149 endY: float = MAX_ALTITUDE_FT, 150 maxSpeedAccuracy: float | None = None): 151 """ 152 Configure additional Y axes on the plot for altitude (ft), angle, vertical 153 acceleration, and speed accuracy traces. Plotly equivalent of Bokeh's 154 extra_y_ranges + LinearAxis layout. 155 """ 156 LEFT_MARGIN = 0.24 157 AXIS_SPACING = 0.06 158 POSITIONS = [n*AXIS_SPACING for n in range(4)] # 0.00, 0.06, 0.12, 0.18 159 160 speedAccuracyEnd = DEFAULT_SPEED_ACCURACY_SCALE 161 if maxSpeedAccuracy is not None and maxSpeedAccuracy >= SPEED_ACCURACY_THRESHOLD: 162 speedAccuracyEnd = maxSpeedAccuracy * 1.1 163 164 color = DEFAULT_AXIS_COLOR 165 166 fig.update_xaxes(domain=(LEFT_MARGIN, 1.0)) 167 168 mainYTitle = (fig.layout.yaxis.title.text or 'km/h') 169 fig.update_yaxes(title=None) 170 171 fig.update_layout( 172 yaxis2=dict( 173 autorange=True, 174 anchor='free', overlaying='y', side='left', position=POSITIONS[0], 175 color=color, tickfont=dict(color=color), 176 showline=True, linecolor=color, 177 showgrid=False, zeroline=False, 178 ), 179 yaxis3=dict( 180 autorange=True, 181 anchor='free', overlaying='y', side='left', position=POSITIONS[1], 182 color=color, tickfont=dict(color=color), 183 showline=True, linecolor=color, 184 showgrid=False, zeroline=False, 185 ), 186 yaxis4=dict( 187 autorange=True, 188 anchor='free', overlaying='y', side='left', position=POSITIONS[2], 189 color=color, tickfont=dict(color=color), 190 showline=True, linecolor=color, 191 showgrid=False, zeroline=False, 192 ), 193 yaxis5=dict( 194 range=(0.0, speedAccuracyEnd), 195 anchor='free', overlaying='y', side='left', position=POSITIONS[3], 196 color=color, tickfont=dict(color=color), 197 showline=True, linecolor=color, 198 showgrid=False, zeroline=False, 199 ), 200 ) 201 202 titles = [ 203 (POSITIONS[0], 'Alt (ft)'), 204 (POSITIONS[1], 'angle'), 205 (POSITIONS[2], 'Vertical acceleration m/s²'), 206 (POSITIONS[3], 'Speed accuracy ISC'), 207 (LEFT_MARGIN, mainYTitle), 208 ] 209 for pos, text in titles: 210 fig.add_annotation( 211 xref='paper', yref='paper', 212 x=pos + 0.012, y=0.5, 213 text=text, 214 showarrow=False, 215 textangle=-90, 216 font=dict(color=color, size=11), 217 xanchor='center', yanchor='middle', 218 ) 219 220 return fig 221 222 223def validationWindowDataFrom(data: pd.DataFrame, window: PerformanceWindow) -> pd.DataFrame: 224 """ 225 Generate the validation window dataset for plotting the ISC speed accuracy 226 values. Subset defined from the end of the scoring window to the 227 validation start. 228 229 NOTE: return type changed from bokeh.models.ColumnDataSource (Bokeh era) to 230 pd.DataFrame as part of the Plotly migration. Columns: 'x', 'y'. 231 """ 232 validationData = data[data.altitudeAGL <= window.validationStart] 233 return pd.DataFrame({ 234 'x': validationData.plotTime.values, 235 'y': validationData.speedAccuracyISC.values, 236 }) 237 238 239def _plotSpeedAccuracy(fig, data, window): 240 accuracyData = validationWindowDataFrom(data, window) 241 fig.add_trace(go.Scatter( 242 x=accuracyData['x'], 243 y=accuracyData['y'], 244 mode='lines', 245 name='Speed accuracy ISC', 246 line=dict(color='lime', width=5.0), 247 yaxis='y5', 248 hovertemplate='accuracy: %{y:.2f}<extra></extra>', 249 )) 250 validationData = data[data.altitudeAGL <= window.validationStart] 251 fig.add_trace(go.Scatter( 252 x=validationData.plotTime, 253 y=[SPEED_ACCURACY_THRESHOLD] * len(validationData), 254 mode='lines', 255 line=dict(color='lime', width=1.0, dash='dash'), 256 yaxis='y5', 257 showlegend=False, 258 hoverinfo='skip', 259 )) 260 261 262def graphJumpResult(fig, 263 jumpResult, 264 lineColor='green', 265 legend='speed', 266 showIt=True, 267 showAccuracy=True): 268 """ 269 Graph the jump results onto the initialized Plotly figure. 270 271 Arguments 272 --------- 273 fig 274 A Plotly Figure where to render the plot. 275 276 jumpResult: ssscoring.JumpResults 277 A jump results named tuple with score, max speed, scores, data, etc. 278 279 lineColor: str 280 A valid CSS color name or hex string. See SPEED_COLORS for the 8 distinct 281 colors used in multi-jump plots. 282 283 legend: str 284 Legend label for the main speed trace. 285 286 showIt: bool 287 If True, render the max speed marker, horizontal speed, and score brackets. 288 Used to discriminate between single-jump plots and aggregate competition 289 overlays. 290 291 Streamlit usage: 292 293```python 294 graphJumpResult(fig, result) 295 st.plotly_chart(fig, width='stretch') 296``` 297 """ 298 if jumpResult.data is not None: 299 data = jumpResult.data 300 scores = jumpResult.scores 301 score = jumpResult.score 302 303 # Main speed line 304 fig.add_trace(go.Scatter( 305 x=data.plotTime, 306 y=data.vKMh, 307 mode='lines', 308 name=legend, 309 line=dict(color=lineColor, width=2), 310 yaxis='y', 311 hovertemplate='v: %{y:.2f} km/h<extra></extra>', 312 )) 313 314 if showIt: 315 maxSpeed = data.vKMh.max() 316 t = data[data.vKMh == maxSpeed].iloc[0].plotTime 317 318 # Horizontal speed 319 fig.add_trace(go.Scatter( 320 x=data.plotTime, 321 y=data.hKMh, 322 mode='lines', 323 name='H-speed', 324 line=dict(color='red', width=2), 325 yaxis='y', 326 hovertemplate='h: %{y:.2f} km/h<extra></extra>', 327 )) 328 329 _plotSpeedAccuracy(fig, data, jumpResult.window) 330 331 if scores is not None: 332 # Score window brackets 333 _graphSegment(fig, scores[score]+3.0, 0.0, scores[score]+3.0, score, 1, 'darkseagreen') 334 _graphSegment(fig, scores[score], 0.0, scores[score], score, 1, 'darkseagreen') 335 # Score marker 336 fig.add_trace(go.Scatter( 337 x=[scores[score]+1.5], 338 y=[score], 339 mode='markers', 340 marker=dict(symbol='circle-cross', size=15, 341 line=dict(color='limegreen', width=2), 342 color='darkgreen'), 343 name='score', 344 yaxis='y', 345 hovertemplate='score: %{y:.2f} km/h<extra></extra>', 346 )) 347 # Max-speed marker 348 fig.add_trace(go.Scatter( 349 x=[t], 350 y=[maxSpeed], 351 mode='markers', 352 marker=dict(symbol='diamond-dot', size=20, 353 line=dict(color='yellow', width=2), 354 color='red'), 355 name='max speed', 356 yaxis='y', 357 hovertemplate='max: %{y:.2f} km/h<extra></extra>', 358 )) 359 360 361def graphAltitude(fig, 362 jumpResult, 363 label='Alt (ft)', 364 lineColor='palegoldenrod', 365 rangeName='altitudeFt'): 366 """ 367 Graph altitude trace on the dedicated altitudeFt Y axis. 368 """ 369 data = jumpResult.data 370 yaxis = _Y_AXIS_MAP[rangeName] 371 fig.add_trace(go.Scatter( 372 x=data.plotTime, 373 y=data.altitudeAGLFt, 374 mode='lines', 375 name=label, 376 line=dict(color=lineColor, width=2), 377 yaxis=yaxis, 378 hovertemplate='alt: %{y:.0f} ft<extra></extra>', 379 )) 380 381 382def graphAngle(fig, 383 jumpResult, 384 label='angle', 385 lineColor='deepskyblue', 386 rangeName='angle'): 387 """ 388 Graph the flight angle trace on the dedicated angle Y axis. 389 """ 390 data = jumpResult.data 391 yaxis = _Y_AXIS_MAP[rangeName] 392 fig.add_trace(go.Scatter( 393 x=data.plotTime, 394 y=data.speedAngle, 395 mode='lines', 396 name=label, 397 line=dict(color=lineColor, width=2), 398 yaxis=yaxis, 399 hovertemplate='angle: %{y:.2f}°<extra></extra>', 400 )) 401 402 403def graphAcceleration(fig, 404 jumpResult, 405 label='V-accel m/s²', 406 lineColor='magenta', 407 rangeName='vAccelMS2'): 408 """ 409 Graph the flight vertical acceleration curve and its EMA-smoothed companion 410 on the dedicated vAccelMS2 Y axis. 411 """ 412 data = jumpResult.data 413 data['vAccelEMA'] = data.vAccelMS2.ewm(span=20, adjust=False).mean() 414 yaxis = _Y_AXIS_MAP[rangeName] 415 fig.add_trace(go.Scatter( 416 x=data.plotTime, 417 y=data.vAccelMS2, 418 mode='lines', 419 name=label, 420 line=dict(color='dimgrey', width=2), 421 yaxis=yaxis, 422 hovertemplate='a: %{y:.2f} m/s²<extra></extra>', 423 )) 424 fig.add_trace(go.Scatter( 425 x=data.plotTime, 426 y=data.vAccelEMA, 427 mode='lines', 428 name=label + ' (EMA)', 429 line=dict(color=lineColor, width=2), 430 yaxis=yaxis, 431 hovertemplate='a (EMA): %{y:.2f} m/s²<extra></extra>', 432 )) 433 434 435def initializeGroundTrackPlot(jumpTitle: str, 436 height=450, 437 backgroundColorName='#1a1a1a', 438 colorName=DEFAULT_AXIS_COLOR): 439 """ 440 Initialize a Plotly figure for the ground-track plot, configured with equal 441 axis scaling so that forward and lateral distances are not distorted. 442 443 X axis: metres forward along the jump run from exit. 444 Y axis: metres lateral (left = positive, right = negative). 445 446 Unlike initializePlot(), no extra Y ranges are added — this canvas is 447 dedicated to spatial displacement only. 448 449 Arguments 450 --------- 451 jumpTitle: str 452 Figure title, usually the jump tag. 453 454 height: int 455 Plot height in pixels. Default 450 — shorter than the main plot since 456 this is a companion chart. 457 458 backgroundColorName: str 459 CSS colour name or hex string for the plot and paper background. 460 461 colorName: str 462 CSS colour name or hex string for axes, tick labels, and title text. 463 464 Returns 465 ------- 466 A `plotly.graph_objects.Figure` ready to receive graphGroundTrack() traces. 467 """ 468 figure = go.Figure() 469 figure.update_layout( 470 title=dict(text=jumpTitle, font=dict(color=colorName)), 471 height=height, 472 autosize=True, 473 plot_bgcolor=backgroundColorName, 474 paper_bgcolor=backgroundColorName, 475 font=dict(color=colorName), 476 hovermode='closest', 477 showlegend=True, 478 legend=dict(font=dict(color=colorName)), 479 xaxis=dict( 480 title=dict(text='forward (m)', font=dict(color=colorName)), 481 autorange=True, 482 color=colorName, 483 tickfont=dict(color=colorName), 484 showgrid=True, 485 gridcolor='rgba(255,255,255,0.08)', 486 showline=True, 487 linecolor=colorName, 488 zeroline=True, 489 zerolinecolor='rgba(255,255,255,0.25)', 490 zerolinewidth=1, 491 ), 492 yaxis=dict( 493 title=dict(text='lateral (m)', font=dict(color=colorName)), 494 autorange=True, 495 color=colorName, 496 tickfont=dict(color=colorName), 497 showgrid=True, 498 gridcolor='rgba(255,255,255,0.08)', 499 showline=True, 500 linecolor=colorName, 501 zeroline=True, 502 zerolinecolor='rgba(255,255,255,0.25)', 503 zerolinewidth=1, 504 scaleanchor='x', 505 scaleratio=1, 506 ), 507 ) 508 return figure 509 510 511def graphGroundTrack(figure, 512 jumpResult, 513 lineColor='deepskyblue'): 514 """ 515 Graph the skydiver's ground track during the performance window as forward 516 vs. lateral displacement from exit, with markers coloured by vertical speed. 517 518 X axis = metres forward along the jump run (negative = reversed, i.e. back- 519 fall). Y axis = metres lateral (positive = left of jump run, negative = 520 right). Marker colour encodes vKMh so the speed progression is visible 521 without a separate time axis. 522 523 A clean belly-to-earth run produces a smooth rightward curve staying close 524 to the lateral zero line. A back-fall reverses toward the origin; the 525 line literally doubles back on itself — unmistakable at a glance. 526 527 Requires a figure initialised by initializeGroundTrackPlot() so that the 528 spatial axes are equal-scaled and the zero lines are present. 529 530 Arguments 531 --------- 532 figure 533 A Plotly Figure initialised by initializeGroundTrackPlot(). 534 535 jumpResult: ssscoring.JumpResults 536 A jump results named tuple. jumpResult.data must contain latitude, 537 longitude, plotTime, and vKMh columns (all present after processJump()). 538 539 lineColor: str 540 A valid CSS colour name or hex string for the connecting track line. 541 542 Streamlit usage: 543 544```python 545 figure = initializeGroundTrackPlot(tag) 546 graphGroundTrack(figure, jumpResult) 547 st.plotly_chart(figure, width='stretch') 548``` 549 """ 550 data = jumpResult.data 551 exitLat = float(data.latitude.iloc[0]) 552 exitLon = float(data.longitude.iloc[0]) 553 bearing = jumpRunBearing(data) 554 displacement = forwardLateralDisplacement(data, exitLat, exitLon, bearing) 555 556 figure.add_trace(go.Scatter( 557 x=displacement.forwardM, 558 y=displacement.lateralM, 559 mode='lines', 560 name='track', 561 line=dict(color='rgba(255,255,255,0.15)', width=1), 562 showlegend=False, 563 hoverinfo='skip', 564 )) 565 566 figure.add_trace(go.Scatter( 567 x=displacement.forwardM, 568 y=displacement.lateralM, 569 mode='markers', 570 name='fwd (m)', 571 marker=dict( 572 color=displacement.forwardM.clip(lower=0, upper=MAX_HORIZONTAL_DISTANCE), 573 colorscale=[ 574 [0.0, SAFE_HORIZONTAL_COLOR], 575 [SAFE_HORIZONTAL_DISTANCE / MAX_HORIZONTAL_DISTANCE, SAFE_HORIZONTAL_COLOR], 576 [1.0, UNSAFE_HORIZONTAL_COLOR], 577 ], 578 cmin=0, 579 cmax=MAX_HORIZONTAL_DISTANCE, 580 cauto=False, 581 size=5, 582 showscale=True, 583 colorbar=dict( 584 title=dict(text='fwd (m)', font=dict(color=DEFAULT_AXIS_COLOR)), 585 tickfont=dict(color=DEFAULT_AXIS_COLOR), 586 tickvals=[0, SAFE_HORIZONTAL_DISTANCE, MAX_HORIZONTAL_DISTANCE], 587 ticktext=['0', f'{int(SAFE_HORIZONTAL_DISTANCE)}m', f'{int(MAX_HORIZONTAL_DISTANCE)}m'], 588 thickness=12, 589 len=0.75, 590 ), 591 ), 592 hovertemplate='fwd: %{x:.1f} m lat: %{y:.1f} m<extra></extra>', 593 )) 594 595 figure.add_trace(go.Scatter( 596 x=[displacement.forwardM.iloc[0]], 597 y=[displacement.lateralM.iloc[0]], 598 mode='markers', 599 name='exit', 600 marker=dict(symbol='circle', size=10, 601 color=SAFE_HORIZONTAL_COLOR, 602 line=dict(color='white', width=1)), 603 hovertemplate='exit<extra></extra>', 604 )) 605 606 figure.add_trace(go.Scatter( 607 x=[displacement.forwardM.iloc[-1]], 608 y=[displacement.lateralM.iloc[-1]], 609 mode='markers', 610 name='end', 611 marker=dict(symbol='square', size=10, 612 color=UNSAFE_HORIZONTAL_COLOR, 613 line=dict(color='white', width=1)), 614 hovertemplate='end: fwd %{x:.1f} m lat: %{y:.1f} m<extra></extra>', 615 )) 616 617 618def graphForwardDisplacement(figure, 619 jumpResult): 620 """ 621 Graph the forward displacement (metres along the jump run from exit) as a 622 time series on the primary Y axis. 623 624 A skydiver on a clean belly run produces a monotonically rising curve. A 625 back-fall inflects and drops — the onset time and reversal depth are 626 immediately readable from the shape of the line and the zero reference. 627 628 Markers are coloured by the same green→red gradient used in the ground 629 track: SAFE_HORIZONTAL_COLOR up to SAFE_HORIZONTAL_DISTANCE, linear 630 transition to UNSAFE_HORIZONTAL_COLOR at MAX_HORIZONTAL_DISTANCE, solid 631 red beyond. 632 633 Pair this with graphJumpResult() on a second figure (same plotTime X axis) 634 to show the temporal relationship between displacement reversal and speed 635 loss. 636 637 Arguments 638 --------- 639 figure 640 A Plotly Figure initialised by initializePlot() with xLabel='seconds from 641 exit' and yLabel='forward (m)'. 642 643 jumpResult: ssscoring.JumpResults 644 A jump results named tuple. jumpResult.data must contain latitude, 645 longitude, and plotTime (all present after processJump()). 646 647 Streamlit usage: 648 649```python 650 figure = initializePlot(tag, yLabel='forward (m)', backgroundColorName='#2c2c2c') 651 graphForwardDisplacement(figure, jumpResult) 652 st.plotly_chart(figure, width='stretch') 653``` 654 """ 655 data = jumpResult.data 656 exitLat = float(data.latitude.iloc[0]) 657 exitLon = float(data.longitude.iloc[0]) 658 bearing = jumpRunBearing(data) 659 displacement = forwardLateralDisplacement(data, exitLat, exitLon, bearing) 660 661 figure.add_trace(go.Scatter( 662 x=displacement.plotTime, 663 y=displacement.forwardM, 664 mode='lines', 665 line=dict(color='rgba(255,255,255,0.15)', width=1), 666 showlegend=False, 667 hoverinfo='skip', 668 )) 669 670 figure.add_trace(go.Scatter( 671 x=displacement.plotTime, 672 y=displacement.forwardM, 673 mode='markers', 674 name='fwd (m)', 675 marker=dict( 676 color=displacement.forwardM.clip(lower=0, upper=MAX_HORIZONTAL_DISTANCE), 677 colorscale=[ 678 [0.0, SAFE_HORIZONTAL_COLOR], 679 [SAFE_HORIZONTAL_DISTANCE / MAX_HORIZONTAL_DISTANCE, SAFE_HORIZONTAL_COLOR], 680 [1.0, UNSAFE_HORIZONTAL_COLOR], 681 ], 682 cmin=0, 683 cmax=MAX_HORIZONTAL_DISTANCE, 684 cauto=False, 685 size=4, 686 showscale=True, 687 colorbar=dict( 688 title=dict(text='fwd (m)', font=dict(color=DEFAULT_AXIS_COLOR)), 689 tickfont=dict(color=DEFAULT_AXIS_COLOR), 690 tickvals=[0, SAFE_HORIZONTAL_DISTANCE, MAX_HORIZONTAL_DISTANCE], 691 ticktext=['0', f'{int(SAFE_HORIZONTAL_DISTANCE)}m', f'{int(MAX_HORIZONTAL_DISTANCE)}m'], 692 thickness=12, 693 len=0.75, 694 ), 695 ), 696 yaxis='y', 697 hovertemplate='t: %{x:.1f} s fwd: %{y:.1f} m<extra></extra>', 698 )) 699 700 figure.add_trace(go.Scatter( 701 x=[displacement.plotTime.iloc[0], displacement.plotTime.iloc[-1]], 702 y=[0.0, 0.0], 703 mode='lines', 704 line=dict(color='rgba(255,255,255,0.25)', width=1, dash='dot'), 705 showlegend=False, 706 hoverinfo='skip', 707 )) 708 709 710def resolveJumpColors(jumpResults: dict) -> dict: 711 """ 712 Build a tag→hex-color mapping for a set of jump results. 713 714 The fastest jump (highest score) gets `COLOR_FASTEST`, the slowest gets 715 `COLOR_SLOWEST`, and all remaining valid jumps cycle through `COLORS_OTHERS`. 716 """ 717 validScores = { 718 tag: result.score 719 for tag, result in jumpResults.items() 720 if result.score is not None 721 } 722 if not validScores: 723 return { 724 tag: COLORS_OTHERS[i % len(COLORS_OTHERS)] 725 for i, tag in enumerate(jumpResults) 726 } 727 fastestTag = max(validScores, key=validScores.get) 728 slowestTag = min(validScores, key=validScores.get) 729 tagColors = {} 730 blueIndex = 0 731 for tag in jumpResults: 732 if tag == fastestTag: 733 tagColors[tag] = COLOR_FASTEST 734 elif tag == slowestTag: 735 tagColors[tag] = COLOR_SLOWEST 736 else: 737 tagColors[tag] = COLORS_OTHERS[blueIndex % len(COLORS_OTHERS)] 738 blueIndex += 1 739 return tagColors 740 741 742def convertHexColorToRGB(color: str) -> list: 743 """ 744 Converts a color in the format `#a0b1c2` to its RGB equivalent as a list 745 of three values 0-255. 746 """ 747 if not isinstance(color, str): 748 raise TypeError('Invalid color type - must be str') 749 color = color.replace('#', '') 750 if len(color) != 6: 751 raise SSScoringError('Invalid hex value length') 752 result = [int(color[x:x+2], 16) for x in range(0, len(color), 2)] 753 return result
CSS color name for the axis colors used in notebooks and Streamlit with Plotly.
Limegreen — fastest jump track color in aggregate displays; matches the table's max-score highlight.
Orangered — slowest jump track color in aggregate displays; matches the table's min-score highlight.
Six distinct blue shades for all non-fastest, non-slowest jumps in aggregate displays.
71def initializePlot(jumpTitle: str, 72 height=500, 73 width=900, 74 xLabel='seconds from exit', 75 yLabel='km/h', 76 xMax=35.0, 77 yMax=DEFAULT_PLOT_MAX_V_SCALE, 78 backgroundColorName='#1a1a1a', 79 colorName=DEFAULT_AXIS_COLOR): 80 """ 81 Initialize a Plotly figure for SSScoring jump plots, configured with the 82 main speed (km/h) Y axis. Extra Y axes for altitude / angle / acceleration / 83 speed-accuracy are added by initializeExtraYRanges(). 84 85 Returns 86 ------- 87 A `plotly.graph_objects.Figure`. 88 """ 89 fig = go.Figure() 90 fig.update_layout( 91 title=dict(text=jumpTitle, font=dict(color=colorName)), 92 height=height, 93 autosize=True, 94 plot_bgcolor=backgroundColorName, 95 paper_bgcolor=backgroundColorName, 96 font=dict(color=colorName), 97 hovermode='x unified', 98 showlegend=True, 99 legend=dict(font=dict(color=colorName)), 100 xaxis=dict( 101 title=dict(text=xLabel, font=dict(color=colorName)), 102 autorange=True, 103 color=colorName, 104 tickfont=dict(color=colorName), 105 showgrid=True, # ← changed 106 gridcolor='rgba(255,255,255,0.08)', # ← added — subtle grid on dark bg 107 showline=True, # ← added — visible axis line 108 linecolor=colorName, # ← added 109 zeroline=False, 110 ), 111 yaxis=dict( 112 title=dict(text=yLabel, font=dict(color=colorName)), 113 autorange=True, 114 color=colorName, 115 tickfont=dict(color=colorName), 116 anchor='x', 117 side='left', 118 showgrid=True, # ← changed 119 gridcolor='rgba(255,255,255,0.08)', # ← added 120 showline=True, # ← added 121 linecolor=colorName, # ← added 122 zeroline=False, 123 ), 124 ) 125 return fig
Initialize a Plotly figure for SSScoring jump plots, configured with the main speed (km/h) Y axis. Extra Y axes for altitude / angle / acceleration / speed-accuracy are added by initializeExtraYRanges().
Returns
A plotly.graph_objects.Figure.
148def initializeExtraYRanges(fig, 149 startY: float = 0.0, 150 endY: float = MAX_ALTITUDE_FT, 151 maxSpeedAccuracy: float | None = None): 152 """ 153 Configure additional Y axes on the plot for altitude (ft), angle, vertical 154 acceleration, and speed accuracy traces. Plotly equivalent of Bokeh's 155 extra_y_ranges + LinearAxis layout. 156 """ 157 LEFT_MARGIN = 0.24 158 AXIS_SPACING = 0.06 159 POSITIONS = [n*AXIS_SPACING for n in range(4)] # 0.00, 0.06, 0.12, 0.18 160 161 speedAccuracyEnd = DEFAULT_SPEED_ACCURACY_SCALE 162 if maxSpeedAccuracy is not None and maxSpeedAccuracy >= SPEED_ACCURACY_THRESHOLD: 163 speedAccuracyEnd = maxSpeedAccuracy * 1.1 164 165 color = DEFAULT_AXIS_COLOR 166 167 fig.update_xaxes(domain=(LEFT_MARGIN, 1.0)) 168 169 mainYTitle = (fig.layout.yaxis.title.text or 'km/h') 170 fig.update_yaxes(title=None) 171 172 fig.update_layout( 173 yaxis2=dict( 174 autorange=True, 175 anchor='free', overlaying='y', side='left', position=POSITIONS[0], 176 color=color, tickfont=dict(color=color), 177 showline=True, linecolor=color, 178 showgrid=False, zeroline=False, 179 ), 180 yaxis3=dict( 181 autorange=True, 182 anchor='free', overlaying='y', side='left', position=POSITIONS[1], 183 color=color, tickfont=dict(color=color), 184 showline=True, linecolor=color, 185 showgrid=False, zeroline=False, 186 ), 187 yaxis4=dict( 188 autorange=True, 189 anchor='free', overlaying='y', side='left', position=POSITIONS[2], 190 color=color, tickfont=dict(color=color), 191 showline=True, linecolor=color, 192 showgrid=False, zeroline=False, 193 ), 194 yaxis5=dict( 195 range=(0.0, speedAccuracyEnd), 196 anchor='free', overlaying='y', side='left', position=POSITIONS[3], 197 color=color, tickfont=dict(color=color), 198 showline=True, linecolor=color, 199 showgrid=False, zeroline=False, 200 ), 201 ) 202 203 titles = [ 204 (POSITIONS[0], 'Alt (ft)'), 205 (POSITIONS[1], 'angle'), 206 (POSITIONS[2], 'Vertical acceleration m/s²'), 207 (POSITIONS[3], 'Speed accuracy ISC'), 208 (LEFT_MARGIN, mainYTitle), 209 ] 210 for pos, text in titles: 211 fig.add_annotation( 212 xref='paper', yref='paper', 213 x=pos + 0.012, y=0.5, 214 text=text, 215 showarrow=False, 216 textangle=-90, 217 font=dict(color=color, size=11), 218 xanchor='center', yanchor='middle', 219 ) 220 221 return fig
Configure additional Y axes on the plot for altitude (ft), angle, vertical acceleration, and speed accuracy traces. Plotly equivalent of Bokeh's extra_y_ranges + LinearAxis layout.
224def validationWindowDataFrom(data: pd.DataFrame, window: PerformanceWindow) -> pd.DataFrame: 225 """ 226 Generate the validation window dataset for plotting the ISC speed accuracy 227 values. Subset defined from the end of the scoring window to the 228 validation start. 229 230 NOTE: return type changed from bokeh.models.ColumnDataSource (Bokeh era) to 231 pd.DataFrame as part of the Plotly migration. Columns: 'x', 'y'. 232 """ 233 validationData = data[data.altitudeAGL <= window.validationStart] 234 return pd.DataFrame({ 235 'x': validationData.plotTime.values, 236 'y': validationData.speedAccuracyISC.values, 237 })
Generate the validation window dataset for plotting the ISC speed accuracy values. Subset defined from the end of the scoring window to the validation start.
NOTE: return type changed from bokeh.models.ColumnDataSource (Bokeh era) to pd.DataFrame as part of the Plotly migration. Columns: 'x', 'y'.
263def graphJumpResult(fig, 264 jumpResult, 265 lineColor='green', 266 legend='speed', 267 showIt=True, 268 showAccuracy=True): 269 """ 270 Graph the jump results onto the initialized Plotly figure. 271 272 Arguments 273 --------- 274 fig 275 A Plotly Figure where to render the plot. 276 277 jumpResult: ssscoring.JumpResults 278 A jump results named tuple with score, max speed, scores, data, etc. 279 280 lineColor: str 281 A valid CSS color name or hex string. See SPEED_COLORS for the 8 distinct 282 colors used in multi-jump plots. 283 284 legend: str 285 Legend label for the main speed trace. 286 287 showIt: bool 288 If True, render the max speed marker, horizontal speed, and score brackets. 289 Used to discriminate between single-jump plots and aggregate competition 290 overlays. 291 292 Streamlit usage: 293 294```python 295 graphJumpResult(fig, result) 296 st.plotly_chart(fig, width='stretch') 297``` 298 """ 299 if jumpResult.data is not None: 300 data = jumpResult.data 301 scores = jumpResult.scores 302 score = jumpResult.score 303 304 # Main speed line 305 fig.add_trace(go.Scatter( 306 x=data.plotTime, 307 y=data.vKMh, 308 mode='lines', 309 name=legend, 310 line=dict(color=lineColor, width=2), 311 yaxis='y', 312 hovertemplate='v: %{y:.2f} km/h<extra></extra>', 313 )) 314 315 if showIt: 316 maxSpeed = data.vKMh.max() 317 t = data[data.vKMh == maxSpeed].iloc[0].plotTime 318 319 # Horizontal speed 320 fig.add_trace(go.Scatter( 321 x=data.plotTime, 322 y=data.hKMh, 323 mode='lines', 324 name='H-speed', 325 line=dict(color='red', width=2), 326 yaxis='y', 327 hovertemplate='h: %{y:.2f} km/h<extra></extra>', 328 )) 329 330 _plotSpeedAccuracy(fig, data, jumpResult.window) 331 332 if scores is not None: 333 # Score window brackets 334 _graphSegment(fig, scores[score]+3.0, 0.0, scores[score]+3.0, score, 1, 'darkseagreen') 335 _graphSegment(fig, scores[score], 0.0, scores[score], score, 1, 'darkseagreen') 336 # Score marker 337 fig.add_trace(go.Scatter( 338 x=[scores[score]+1.5], 339 y=[score], 340 mode='markers', 341 marker=dict(symbol='circle-cross', size=15, 342 line=dict(color='limegreen', width=2), 343 color='darkgreen'), 344 name='score', 345 yaxis='y', 346 hovertemplate='score: %{y:.2f} km/h<extra></extra>', 347 )) 348 # Max-speed marker 349 fig.add_trace(go.Scatter( 350 x=[t], 351 y=[maxSpeed], 352 mode='markers', 353 marker=dict(symbol='diamond-dot', size=20, 354 line=dict(color='yellow', width=2), 355 color='red'), 356 name='max speed', 357 yaxis='y', 358 hovertemplate='max: %{y:.2f} km/h<extra></extra>', 359 ))
Graph the jump results onto the initialized Plotly figure.
Arguments
---------
fig
A Plotly Figure where to render the plot.
jumpResult: ssscoring.JumpResults
A jump results named tuple with score, max speed, scores, data, etc.
lineColor: str
A valid CSS color name or hex string. See SPEED_COLORS for the 8 distinct
colors used in multi-jump plots.
legend: str
Legend label for the main speed trace.
showIt: bool
If True, render the max speed marker, horizontal speed, and score brackets.
Used to discriminate between single-jump plots and aggregate competition
overlays.
Streamlit usage:
graphJumpResult(fig, result)
st.plotly_chart(fig, width='stretch')
362def graphAltitude(fig, 363 jumpResult, 364 label='Alt (ft)', 365 lineColor='palegoldenrod', 366 rangeName='altitudeFt'): 367 """ 368 Graph altitude trace on the dedicated altitudeFt Y axis. 369 """ 370 data = jumpResult.data 371 yaxis = _Y_AXIS_MAP[rangeName] 372 fig.add_trace(go.Scatter( 373 x=data.plotTime, 374 y=data.altitudeAGLFt, 375 mode='lines', 376 name=label, 377 line=dict(color=lineColor, width=2), 378 yaxis=yaxis, 379 hovertemplate='alt: %{y:.0f} ft<extra></extra>', 380 ))
Graph altitude trace on the dedicated altitudeFt Y axis.
383def graphAngle(fig, 384 jumpResult, 385 label='angle', 386 lineColor='deepskyblue', 387 rangeName='angle'): 388 """ 389 Graph the flight angle trace on the dedicated angle Y axis. 390 """ 391 data = jumpResult.data 392 yaxis = _Y_AXIS_MAP[rangeName] 393 fig.add_trace(go.Scatter( 394 x=data.plotTime, 395 y=data.speedAngle, 396 mode='lines', 397 name=label, 398 line=dict(color=lineColor, width=2), 399 yaxis=yaxis, 400 hovertemplate='angle: %{y:.2f}°<extra></extra>', 401 ))
Graph the flight angle trace on the dedicated angle Y axis.
404def graphAcceleration(fig, 405 jumpResult, 406 label='V-accel m/s²', 407 lineColor='magenta', 408 rangeName='vAccelMS2'): 409 """ 410 Graph the flight vertical acceleration curve and its EMA-smoothed companion 411 on the dedicated vAccelMS2 Y axis. 412 """ 413 data = jumpResult.data 414 data['vAccelEMA'] = data.vAccelMS2.ewm(span=20, adjust=False).mean() 415 yaxis = _Y_AXIS_MAP[rangeName] 416 fig.add_trace(go.Scatter( 417 x=data.plotTime, 418 y=data.vAccelMS2, 419 mode='lines', 420 name=label, 421 line=dict(color='dimgrey', width=2), 422 yaxis=yaxis, 423 hovertemplate='a: %{y:.2f} m/s²<extra></extra>', 424 )) 425 fig.add_trace(go.Scatter( 426 x=data.plotTime, 427 y=data.vAccelEMA, 428 mode='lines', 429 name=label + ' (EMA)', 430 line=dict(color=lineColor, width=2), 431 yaxis=yaxis, 432 hovertemplate='a (EMA): %{y:.2f} m/s²<extra></extra>', 433 ))
Graph the flight vertical acceleration curve and its EMA-smoothed companion on the dedicated vAccelMS2 Y axis.
436def initializeGroundTrackPlot(jumpTitle: str, 437 height=450, 438 backgroundColorName='#1a1a1a', 439 colorName=DEFAULT_AXIS_COLOR): 440 """ 441 Initialize a Plotly figure for the ground-track plot, configured with equal 442 axis scaling so that forward and lateral distances are not distorted. 443 444 X axis: metres forward along the jump run from exit. 445 Y axis: metres lateral (left = positive, right = negative). 446 447 Unlike initializePlot(), no extra Y ranges are added — this canvas is 448 dedicated to spatial displacement only. 449 450 Arguments 451 --------- 452 jumpTitle: str 453 Figure title, usually the jump tag. 454 455 height: int 456 Plot height in pixels. Default 450 — shorter than the main plot since 457 this is a companion chart. 458 459 backgroundColorName: str 460 CSS colour name or hex string for the plot and paper background. 461 462 colorName: str 463 CSS colour name or hex string for axes, tick labels, and title text. 464 465 Returns 466 ------- 467 A `plotly.graph_objects.Figure` ready to receive graphGroundTrack() traces. 468 """ 469 figure = go.Figure() 470 figure.update_layout( 471 title=dict(text=jumpTitle, font=dict(color=colorName)), 472 height=height, 473 autosize=True, 474 plot_bgcolor=backgroundColorName, 475 paper_bgcolor=backgroundColorName, 476 font=dict(color=colorName), 477 hovermode='closest', 478 showlegend=True, 479 legend=dict(font=dict(color=colorName)), 480 xaxis=dict( 481 title=dict(text='forward (m)', font=dict(color=colorName)), 482 autorange=True, 483 color=colorName, 484 tickfont=dict(color=colorName), 485 showgrid=True, 486 gridcolor='rgba(255,255,255,0.08)', 487 showline=True, 488 linecolor=colorName, 489 zeroline=True, 490 zerolinecolor='rgba(255,255,255,0.25)', 491 zerolinewidth=1, 492 ), 493 yaxis=dict( 494 title=dict(text='lateral (m)', font=dict(color=colorName)), 495 autorange=True, 496 color=colorName, 497 tickfont=dict(color=colorName), 498 showgrid=True, 499 gridcolor='rgba(255,255,255,0.08)', 500 showline=True, 501 linecolor=colorName, 502 zeroline=True, 503 zerolinecolor='rgba(255,255,255,0.25)', 504 zerolinewidth=1, 505 scaleanchor='x', 506 scaleratio=1, 507 ), 508 ) 509 return figure
Initialize a Plotly figure for the ground-track plot, configured with equal axis scaling so that forward and lateral distances are not distorted.
X axis: metres forward along the jump run from exit. Y axis: metres lateral (left = positive, right = negative).
Unlike initializePlot(), no extra Y ranges are added — this canvas is dedicated to spatial displacement only.
Arguments
jumpTitle: str
Figure title, usually the jump tag.
height: int
Plot height in pixels. Default 450 — shorter than the main plot since this is a companion chart.
backgroundColorName: str
CSS colour name or hex string for the plot and paper background.
colorName: str
CSS colour name or hex string for axes, tick labels, and title text.
Returns
A plotly.graph_objects.Figure ready to receive graphGroundTrack() traces.
512def graphGroundTrack(figure, 513 jumpResult, 514 lineColor='deepskyblue'): 515 """ 516 Graph the skydiver's ground track during the performance window as forward 517 vs. lateral displacement from exit, with markers coloured by vertical speed. 518 519 X axis = metres forward along the jump run (negative = reversed, i.e. back- 520 fall). Y axis = metres lateral (positive = left of jump run, negative = 521 right). Marker colour encodes vKMh so the speed progression is visible 522 without a separate time axis. 523 524 A clean belly-to-earth run produces a smooth rightward curve staying close 525 to the lateral zero line. A back-fall reverses toward the origin; the 526 line literally doubles back on itself — unmistakable at a glance. 527 528 Requires a figure initialised by initializeGroundTrackPlot() so that the 529 spatial axes are equal-scaled and the zero lines are present. 530 531 Arguments 532 --------- 533 figure 534 A Plotly Figure initialised by initializeGroundTrackPlot(). 535 536 jumpResult: ssscoring.JumpResults 537 A jump results named tuple. jumpResult.data must contain latitude, 538 longitude, plotTime, and vKMh columns (all present after processJump()). 539 540 lineColor: str 541 A valid CSS colour name or hex string for the connecting track line. 542 543 Streamlit usage: 544 545```python 546 figure = initializeGroundTrackPlot(tag) 547 graphGroundTrack(figure, jumpResult) 548 st.plotly_chart(figure, width='stretch') 549``` 550 """ 551 data = jumpResult.data 552 exitLat = float(data.latitude.iloc[0]) 553 exitLon = float(data.longitude.iloc[0]) 554 bearing = jumpRunBearing(data) 555 displacement = forwardLateralDisplacement(data, exitLat, exitLon, bearing) 556 557 figure.add_trace(go.Scatter( 558 x=displacement.forwardM, 559 y=displacement.lateralM, 560 mode='lines', 561 name='track', 562 line=dict(color='rgba(255,255,255,0.15)', width=1), 563 showlegend=False, 564 hoverinfo='skip', 565 )) 566 567 figure.add_trace(go.Scatter( 568 x=displacement.forwardM, 569 y=displacement.lateralM, 570 mode='markers', 571 name='fwd (m)', 572 marker=dict( 573 color=displacement.forwardM.clip(lower=0, upper=MAX_HORIZONTAL_DISTANCE), 574 colorscale=[ 575 [0.0, SAFE_HORIZONTAL_COLOR], 576 [SAFE_HORIZONTAL_DISTANCE / MAX_HORIZONTAL_DISTANCE, SAFE_HORIZONTAL_COLOR], 577 [1.0, UNSAFE_HORIZONTAL_COLOR], 578 ], 579 cmin=0, 580 cmax=MAX_HORIZONTAL_DISTANCE, 581 cauto=False, 582 size=5, 583 showscale=True, 584 colorbar=dict( 585 title=dict(text='fwd (m)', font=dict(color=DEFAULT_AXIS_COLOR)), 586 tickfont=dict(color=DEFAULT_AXIS_COLOR), 587 tickvals=[0, SAFE_HORIZONTAL_DISTANCE, MAX_HORIZONTAL_DISTANCE], 588 ticktext=['0', f'{int(SAFE_HORIZONTAL_DISTANCE)}m', f'{int(MAX_HORIZONTAL_DISTANCE)}m'], 589 thickness=12, 590 len=0.75, 591 ), 592 ), 593 hovertemplate='fwd: %{x:.1f} m lat: %{y:.1f} m<extra></extra>', 594 )) 595 596 figure.add_trace(go.Scatter( 597 x=[displacement.forwardM.iloc[0]], 598 y=[displacement.lateralM.iloc[0]], 599 mode='markers', 600 name='exit', 601 marker=dict(symbol='circle', size=10, 602 color=SAFE_HORIZONTAL_COLOR, 603 line=dict(color='white', width=1)), 604 hovertemplate='exit<extra></extra>', 605 )) 606 607 figure.add_trace(go.Scatter( 608 x=[displacement.forwardM.iloc[-1]], 609 y=[displacement.lateralM.iloc[-1]], 610 mode='markers', 611 name='end', 612 marker=dict(symbol='square', size=10, 613 color=UNSAFE_HORIZONTAL_COLOR, 614 line=dict(color='white', width=1)), 615 hovertemplate='end: fwd %{x:.1f} m lat: %{y:.1f} m<extra></extra>', 616 ))
Graph the skydiver's ground track during the performance window as forward vs. lateral displacement from exit, with markers coloured by vertical speed.
X axis = metres forward along the jump run (negative = reversed, i.e. back-
fall). Y axis = metres lateral (positive = left of jump run, negative =
right). Marker colour encodes vKMh so the speed progression is visible
without a separate time axis.
A clean belly-to-earth run produces a smooth rightward curve staying close
to the lateral zero line. A back-fall reverses toward the origin; the
line literally doubles back on itself — unmistakable at a glance.
Requires a figure initialised by initializeGroundTrackPlot() so that the
spatial axes are equal-scaled and the zero lines are present.
Arguments
---------
figure
A Plotly Figure initialised by initializeGroundTrackPlot().
jumpResult: ssscoring.JumpResults
A jump results named tuple. jumpResult.data must contain latitude,
longitude, plotTime, and vKMh columns (all present after processJump()).
lineColor: str
A valid CSS colour name or hex string for the connecting track line.
Streamlit usage:
figure = initializeGroundTrackPlot(tag)
graphGroundTrack(figure, jumpResult)
st.plotly_chart(figure, width='stretch')
619def graphForwardDisplacement(figure, 620 jumpResult): 621 """ 622 Graph the forward displacement (metres along the jump run from exit) as a 623 time series on the primary Y axis. 624 625 A skydiver on a clean belly run produces a monotonically rising curve. A 626 back-fall inflects and drops — the onset time and reversal depth are 627 immediately readable from the shape of the line and the zero reference. 628 629 Markers are coloured by the same green→red gradient used in the ground 630 track: SAFE_HORIZONTAL_COLOR up to SAFE_HORIZONTAL_DISTANCE, linear 631 transition to UNSAFE_HORIZONTAL_COLOR at MAX_HORIZONTAL_DISTANCE, solid 632 red beyond. 633 634 Pair this with graphJumpResult() on a second figure (same plotTime X axis) 635 to show the temporal relationship between displacement reversal and speed 636 loss. 637 638 Arguments 639 --------- 640 figure 641 A Plotly Figure initialised by initializePlot() with xLabel='seconds from 642 exit' and yLabel='forward (m)'. 643 644 jumpResult: ssscoring.JumpResults 645 A jump results named tuple. jumpResult.data must contain latitude, 646 longitude, and plotTime (all present after processJump()). 647 648 Streamlit usage: 649 650```python 651 figure = initializePlot(tag, yLabel='forward (m)', backgroundColorName='#2c2c2c') 652 graphForwardDisplacement(figure, jumpResult) 653 st.plotly_chart(figure, width='stretch') 654``` 655 """ 656 data = jumpResult.data 657 exitLat = float(data.latitude.iloc[0]) 658 exitLon = float(data.longitude.iloc[0]) 659 bearing = jumpRunBearing(data) 660 displacement = forwardLateralDisplacement(data, exitLat, exitLon, bearing) 661 662 figure.add_trace(go.Scatter( 663 x=displacement.plotTime, 664 y=displacement.forwardM, 665 mode='lines', 666 line=dict(color='rgba(255,255,255,0.15)', width=1), 667 showlegend=False, 668 hoverinfo='skip', 669 )) 670 671 figure.add_trace(go.Scatter( 672 x=displacement.plotTime, 673 y=displacement.forwardM, 674 mode='markers', 675 name='fwd (m)', 676 marker=dict( 677 color=displacement.forwardM.clip(lower=0, upper=MAX_HORIZONTAL_DISTANCE), 678 colorscale=[ 679 [0.0, SAFE_HORIZONTAL_COLOR], 680 [SAFE_HORIZONTAL_DISTANCE / MAX_HORIZONTAL_DISTANCE, SAFE_HORIZONTAL_COLOR], 681 [1.0, UNSAFE_HORIZONTAL_COLOR], 682 ], 683 cmin=0, 684 cmax=MAX_HORIZONTAL_DISTANCE, 685 cauto=False, 686 size=4, 687 showscale=True, 688 colorbar=dict( 689 title=dict(text='fwd (m)', font=dict(color=DEFAULT_AXIS_COLOR)), 690 tickfont=dict(color=DEFAULT_AXIS_COLOR), 691 tickvals=[0, SAFE_HORIZONTAL_DISTANCE, MAX_HORIZONTAL_DISTANCE], 692 ticktext=['0', f'{int(SAFE_HORIZONTAL_DISTANCE)}m', f'{int(MAX_HORIZONTAL_DISTANCE)}m'], 693 thickness=12, 694 len=0.75, 695 ), 696 ), 697 yaxis='y', 698 hovertemplate='t: %{x:.1f} s fwd: %{y:.1f} m<extra></extra>', 699 )) 700 701 figure.add_trace(go.Scatter( 702 x=[displacement.plotTime.iloc[0], displacement.plotTime.iloc[-1]], 703 y=[0.0, 0.0], 704 mode='lines', 705 line=dict(color='rgba(255,255,255,0.25)', width=1, dash='dot'), 706 showlegend=False, 707 hoverinfo='skip', 708 ))
Graph the forward displacement (metres along the jump run from exit) as a time series on the primary Y axis.
A skydiver on a clean belly run produces a monotonically rising curve. A
back-fall inflects and drops — the onset time and reversal depth are
immediately readable from the shape of the line and the zero reference.
Markers are coloured by the same green→red gradient used in the ground
track: SAFE_HORIZONTAL_COLOR up to SAFE_HORIZONTAL_DISTANCE, linear
transition to UNSAFE_HORIZONTAL_COLOR at MAX_HORIZONTAL_DISTANCE, solid
red beyond.
Pair this with graphJumpResult() on a second figure (same plotTime X axis)
to show the temporal relationship between displacement reversal and speed
loss.
Arguments
---------
figure
A Plotly Figure initialised by initializePlot() with xLabel='seconds from
exit' and yLabel='forward (m)'.
jumpResult: ssscoring.JumpResults
A jump results named tuple. jumpResult.data must contain latitude,
longitude, and plotTime (all present after processJump()).
Streamlit usage:
figure = initializePlot(tag, yLabel='forward (m)', backgroundColorName='#2c2c2c')
graphForwardDisplacement(figure, jumpResult)
st.plotly_chart(figure, width='stretch')
711def resolveJumpColors(jumpResults: dict) -> dict: 712 """ 713 Build a tag→hex-color mapping for a set of jump results. 714 715 The fastest jump (highest score) gets `COLOR_FASTEST`, the slowest gets 716 `COLOR_SLOWEST`, and all remaining valid jumps cycle through `COLORS_OTHERS`. 717 """ 718 validScores = { 719 tag: result.score 720 for tag, result in jumpResults.items() 721 if result.score is not None 722 } 723 if not validScores: 724 return { 725 tag: COLORS_OTHERS[i % len(COLORS_OTHERS)] 726 for i, tag in enumerate(jumpResults) 727 } 728 fastestTag = max(validScores, key=validScores.get) 729 slowestTag = min(validScores, key=validScores.get) 730 tagColors = {} 731 blueIndex = 0 732 for tag in jumpResults: 733 if tag == fastestTag: 734 tagColors[tag] = COLOR_FASTEST 735 elif tag == slowestTag: 736 tagColors[tag] = COLOR_SLOWEST 737 else: 738 tagColors[tag] = COLORS_OTHERS[blueIndex % len(COLORS_OTHERS)] 739 blueIndex += 1 740 return tagColors
Build a tag→hex-color mapping for a set of jump results.
The fastest jump (highest score) gets COLOR_FASTEST, the slowest gets
COLOR_SLOWEST, and all remaining valid jumps cycle through COLORS_OTHERS.
743def convertHexColorToRGB(color: str) -> list: 744 """ 745 Converts a color in the format `#a0b1c2` to its RGB equivalent as a list 746 of three values 0-255. 747 """ 748 if not isinstance(color, str): 749 raise TypeError('Invalid color type - must be str') 750 color = color.replace('#', '') 751 if len(color) != 6: 752 raise SSScoringError('Invalid hex value length') 753 result = [int(color[x:x+2], 16) for x in range(0, len(color), 2)] 754 return result
Converts a color in the format #a0b1c2 to its RGB equivalent as a list
of three values 0-255.