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