# -*- coding: utf-8 -*-
"""
Created on Thu Nov 6 12:51:45 2025
@author: win11
"""
import dash
from dash import html, Output, Input, dcc, clientside_callback
import dash_bootstrap_components as dbc
import pandas as pd
import dash_player as dp
import plotly.graph_objects as go
import numpy as np
##################################
# Datastuff###
##################################
df_raw=pd.read_csv('nba.csv')
#add swing rank, no duplicates in rank
df_raw["rank_swg_made_per_game"] = df_raw["swg_made_per_game"].rank(
ascending=False, method="dense"
).astype(int)
app = dash.Dash(
__name__,
external_stylesheets=[dbc.themes.CYBORG],
# Add meta tags for language and viewport
meta_tags=[
{"name": "viewport", "content": "width=device-width, initial-scale=1"},
]
)
clutchvideo = html.Div(dp.DashPlayer(
id="player",
url="https://www.youtube.com/watch?v=slLaJPkZltw",
controls=True,
width="100%",
#height="250px",
))
def create_table_clutch_vs_normal():
# Define the table data
table_header = [
html.Thead([
html.Tr([
html.Th("Aspect"),
html.Th("Normal Shot"),
html.Th("Clutch Shot")
])
])
]
table_body = [
html.Tbody([
html.Tr([
html.Td("When it happens"),
html.Td("Any time during the game — could be first quarter, middle of the game, or when the score isn't close."),
html.Td("In high-pressure moments, specifically late in close games. The NBA officially defines clutch time as the last 5 minutes of the 4th quarter or overtime when the score difference is 5 points or fewer.")
]),
html.Tr([
html.Td("Pressure level"),
html.Td("Varies — often low or moderate, since there's still time left to recover from misses."),
html.Td("Very high — every point can decide the outcome. Players feel the weight of the entire game.")
]),
html.Tr([
html.Td("Psychological impact"),
html.Td("Players shoot in rhythm, often relaxed, with normal decision-making."),
html.Td("Stress, adrenaline, and fatigue play a big role. Even elite players can shoot worse or better than normal depending on how they handle pressure.")
]),
html.Tr([
html.Td("How analysts use it"),
html.Td("To measure a player's general shooting skill and consistency."),
html.Td("To evaluate mental toughness and performance under pressure — \"Who delivers when it matters most?\"")
]),
html.Tr([
html.Td("Example"),
html.Td("A player takes a jump shot in the 2nd quarter when their team leads by 12."),
html.Td("A player takes a shot with 30 seconds left and the game tied — that's a clutch shot.")
])
])
]
# Create the table
clutch_vs_normal_table = dbc.Table(
table_header + table_body,
bordered=True,
hover=True,
responsive=True,
striped=True,
className="mb-0"
)
return clutch_vs_normal_table
def normal_vs_clutch_performance(selected_column):
#filter data based on selected_column
if selected_column == None or selected_column == '':
## show all datapoints
dff = df_raw.copy()
else:
#show 20 datapoints based on the highest scores for the selected column
dff = df_raw.sort_values(by=selected_column, ascending = False).head(20)
# Create figure
fig = go.Figure()
#average clutch performance, we keep the overall number
avg_clutch_perf = df_raw['pct_clutch'].mean()
# Separate data points based on y > x condition
above_line = dff[dff['pct_clutch'] > avg_clutch_perf]
customdata1 = np.column_stack((above_line['name'], above_line['ft_pct_clutch'],above_line['pct_clutch_adjusted'] ))
below_line = dff[dff['pct_clutch'] <= avg_clutch_perf]
customdata2 = np.column_stack((below_line['name'], below_line['ft_pct_clutch'],below_line['pct_clutch_adjusted'] ))
# Define hover template
hover_template = '<b><u>%{customdata[0]}</u></b><br>' + \
'Clutch Performance: %{y:.1f}%<br>' + \
'Normal Performance: %{x:.1f}%<br>' + \
'Adjusted Clutch Performance: %{customdata[2]:.1f}%<br>' + \
'<extra></extra>'
# Add scatter points for y > x (white)
fig.add_trace(go.Scatter(
x=above_line['ft_pct_all'],
y=above_line['pct_clutch'],
mode='markers',
marker=dict(color='white', size=8),
name='Better in Clutch',
showlegend=True,
customdata=customdata1,
hovertemplate=hover_template
))
# Add scatter points for y <= x (orange)
fig.add_trace(go.Scatter(
x=below_line['ft_pct_all'],
y=below_line['pct_clutch'],
mode='markers',
marker=dict(color='orange', size=8),
name='Worse in Clutch',
showlegend=True,
customdata=customdata2,
hovertemplate=hover_template
))
# Add average line
fig.add_trace(go.Scatter(
x=[0, 100],
y=[avg_clutch_perf, avg_clutch_perf],
mode='lines',
line=dict(color='gray', dash='dot', width=2),
name='Average Clutch Performance',
showlegend=True
))
# Update layout with zero margins
fig.update_layout(
plot_bgcolor='#0c0400',
paper_bgcolor='#0c0400',
xaxis_title='Normal Shot Performance',
yaxis_title='Clutch Shot Performance',
font=dict(color='white'),
xaxis=dict(
gridcolor='rgba(255,255,255,0.2)',
zerolinecolor='rgba(255,255,255,0.2)',
range=[0, 100]
),
yaxis=dict(
gridcolor='rgba(255,255,255,0.2)',
zerolinecolor='rgba(255,255,255,0.2)',
range=[0, 100]
),
hoverlabel=dict(
bgcolor='rgba(0,0,0,0.8)',
font_size=14,
font_family="Arial"
),
margin=dict(l=0, r=0, t=0, b=0),
legend=dict(
orientation="h",
yanchor="top",
y=0.99,
xanchor="left",
x=0.01,
bgcolor='rgba(0,0,0,0.5)',
bordercolor='rgba(255,255,255,0.3)',
borderwidth=1
)
)
return fig
def create_lollipop_clutch_adjusted(view):
usecol = 'pct_clutch' if view == 1 else 'pct_clutch_adjusted'
# Sort df_raw by adjusted desc, grab top 10
dff = df_raw.sort_values(by=[usecol], ascending=False).head(10)
# Reverse the order so highest is at the top when displayed
dff = dff.iloc[::-1].reset_index(drop=True)
# Define hover template
if view == 1:
customdata = np.column_stack((dff['pct_clutch_adjusted'], dff['ft_pct_all']))
hover_template = '<b><u>%{y}</u></b><br>' + \
'Clutch Performance: %{x:.1f}%<br>' + \
'Clutch Adjusted:%{customdata[0]:.1f}%<br>' + \
'Normal Performance: %{customdata[1]:.1f}%<br>' + \
'<extra></extra>'
else:
customdata = np.column_stack((dff['pct_clutch'], dff['ft_pct_all']))
hover_template = '<b><u>%{y}</u></b><br>' + \
'Clutch Performance: %{customdata[0]:.1f}%<br>' + \
'Clutch Adjusted: %{x:.1f}%<br>' + \
'Normal Performance: %{customdata[1]:.1f}%<br>' + \
'<extra></extra>'
data = [
go.Scatter(
x=dff[usecol],
y=dff['name'],
mode='markers',
marker=dict(color='red', size=10),
name='Clutch Adjusted %',
customdata=customdata,
hovertemplate=hover_template
)
]
# Create horizontal lines for lollipop chart
layout = go.Layout(
shapes=[dict(
type='line',
xref='x',
yref='y',
x0=0,
y0=i,
x1=value,
y1=i,
line=dict(
color='white',
width=2,
dash='dot'
)
) for i, value in enumerate(dff[usecol])],
# title='Top 10 Players by Clutch Adjusted Performance',
xaxis=dict(
#title='Clutch Adjusted %',
gridcolor='rgba(255,255,255,0.2)',
zerolinecolor='rgba(255,255,255,0.2)'
),
yaxis=dict(
title='',
gridcolor='rgba(255,255,255,0.2)'
),
showlegend=False,
height=400,
#margin=dict(l=150), # Extra left margin for player names
plot_bgcolor='#0c0400',
paper_bgcolor='#0c0400',
font=dict(color='white'),
hoverlabel=dict(
bgcolor='rgba(0,0,0,0.8)',
font_size=14,
font_family="Arial"
),
margin=dict(l=150, r=0, t=15, b=30),
)
return go.Figure(data, layout)
def create_top_10_list():
#first column 1 t/m 5
dfs1 = df_raw.sort_values(by=['rank_swg_made_per_game']).iloc[:5]
items1 = [dbc.ListGroupItem(name) for name in zip(dfs1['name'])]
#second column 6 t/m 10
dfs2 = df_raw.sort_values(by=['rank_swg_made_per_game']).iloc[5:10]
items2 = [dbc.ListGroupItem(name) for name in zip(dfs2['name'])]
list_group1 = dbc.ListGroup(
items1,
numbered=True,
)
list_group2 = dbc.ListGroup(
items2,
numbered=True,
class_name='secondlist'
)
outputdiv = html.Div([list_group1, list_group2], style={"display":"flex"})
return outputdiv
##############UI##########################
#playerlist
players = {pid : name for pid, name in zip(df_raw['pid'], df_raw['name'])}
dropdown_players = dcc.Dropdown(
options=players,
id='select_player'
)
#top10-view-select#####
top10_radioitems = html.Div(
[
#dbc.Label("Choose Top10"),
dbc.RadioItems(
options=[
{"label": "Highest % clutch shots made", "value": 1},
{"label": "Highest adjusted % clutch shots made", "value": 2},
],
value=1,
id="select_top10_view",
inline=True,
style={'marginBottom':'2rem','marginTop':'2rem'}
),
]
)
#columnlist#
columnslist = ['ft_pct_clutch','pct_clutch', 'ft_pct_all', 'pct_clutch_adjusted','rank_swg_made_per_game']
dropdown_scatter_filter = dcc.Dropdown(
[
{"label":"All", "value": ""},
{"label": "Players Top20 - % of clutch free throws made" , "value": "ft_pct_clutch" } ,
{"label": "Players Top20 - % of clutch shots made", "value": "pct_clutch" } ,
{"label": "Players Top20 - % of all free throws made", "value": "ft_pct_all"} ,
{"label": "Players Top20 - Adjusted % of clutch shots made", "value": "pct_clutch_adjusted"}
]
,
id='select_column',
value='',
style={'marginBottom':'2rem','marginTop':'2rem'}
)
#################THE APP#########################
# Set the language attribute for the HTML document
app.index_string = '''
<!DOCTYPE html>
<html lang="en">
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
'''
app.title = "NBA Most Clutches"
app.layout = dbc.Container([
dcc.Location(id="url", refresh='callback-nav'),
html.Main([ # Use main element for primary content
############INTRODUCTION SECTION########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H1('On NBA clutch shot performance'),
html.P('App build for #plotly #figurefriday 2025 week 45')
], width=12, md = 6),
dbc.Col([
html.H2("What is clutch performance?", className="section-title"),
html.H3("The ability to perform exceptionally well under pressure."),
dcc.Markdown('''
Clutch performance is the ability to perform exceptionally well under pressure, particularly in high-stakes or critical situations. It can mean maintaining a high level of performance when it matters most, or even exceeding one's usual standards, and is a phenomenon observed in sports, gaming, and professional environments.
''')
], width=12, md = 6),
]),
]),
className="section",
id="home",
**{"data-label": "Home"}
),
############WHY IT MATTERS########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2("Why Clutch Performance Matters?", className="section-title"),
html.H3("Clutch performance shows who can deliver best when win is on the line."),
dcc.Markdown('''
Basketball games often come down to just a few possessions in the final minutes. The difference between winning and losing can hinge on whether a player can still make good decisions and hit tough shots under maximum pressure.
''')
], width=12, md = 6),
dbc.Col([
clutchvideo
], width=12, md = 6),
]),
]),
className="section",
id="why",
**{"data-label": "About"}
),
############DIFFERENCE CLUTCH - NORMAL ########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2('What is the difference between a normal and a clutch shot?'),
html.H3("Some aspects focusessed on the NBA", style={"marginBottom":"2rem"}),
create_table_clutch_vs_normal()
], width=12),
]),
]),
className="section",
id="aspects",
**{"data-label": "Clutch aspects"}
),
############OVERALL PERFORMANCE AND COMPARISON########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2("Are Clutch Shot Killers the top20 in every stat?", className="section-title"),
html.H3("No"),
html.P(f"This app is based on the data of {len(df_raw)} NBA players."),
html.P(f"On average the NBA players have a normal shot making percentage of {round(df_raw['ft_pct_all'].mean(),1)}. This is an important goal to aim for if you strive for a career in the NBA."),
html.P('If you select different views in the plot you can see dots, representing players, jumping around. This explains the NO.')
], width=12, md = 6),
dbc.Col([
dropdown_scatter_filter,
html.Div(id="filteredscatter")
], width=12, md = 6),
]),
]),
className="section",
id="performance",
**{"data-label": "Overall comparison"}
),
############A CLUTCH IS A CLUTCH########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2("A clutch is a clutch, right?", className="section-title"),
html.H3("Yes & No"),
dcc.Markdown('''
Yes, a clutch is a clutch, but some clutch shots are more difficult to make than others. Adjusted clutch performance takes factors for difficulty into regard.
The difference is that clutch performance is the actual successful action under pressure, while clutch performance adjustment is a statistical method used to evaluate performance by adjusting raw statistics to account for the difficulty or context of clutch situations.
As you can see, these different perspectives can lead to different top 10 lists.
''')
], width=12, md = 6),
dbc.Col([
html.H3('The top10 Clutch Shot Killers'),
top10_radioitems,
html.Div(id='top10-lollipop')
], width=12, md = 6),
]),
]),
className="section",
id="clutch",
**{"data-label": "On clutches"}
),
############TOP 10 AND FIND PLAYER########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2("The outcome of the game", className="section-title"),
html.H3("Who has the highest average impact"),
dcc.Markdown('''
Impact on the outcome of the game is measured in the variable Swing.
Swing is the difference between the team's win probability if the shot is made vs. if it was missed.
The size of that gap is the swing value (greater values represent more clutch / high stakes moments).
"Swing made per game" might be thought of as the player's average impact on the team's win probability, normalized for games played.
''')
], width=12, md = 6),
dbc.Col([
html.H3('The top10 "Swing made per game"', style={"marginBottom":"2rem"}),
html.Div(create_top_10_list()),
html.Hr(),
html.P('Find your player:'),
html.Div(dropdown_players),
html.Div(id='player-rank')
], width=12, md = 6),
]),
]),
className="section",
id="top10",
**{"data-label": "Swings"}
),
############CONCLUSION AND CREDITS########################
html.Section(
dbc.Container([
dbc.Row([
dbc.Col([
html.H2("Credits"),
html.H3("Datasource The Pudding"),
html.A("View datasource", href='https://github.com/the-pudding/data/tree/master/clutch', target="_blank",style={"color":"white", "marginTop":"3rem"}),
html.H4("Some AI tools, to answer questions about clutch and speed up plot pimping."),
html.P('All errors in this app are my fault, it\'s not the data.', style={"marginTop":"2rem"}),
html.A("Have a good day! Marie-Anne", href='https://emma-design.nl/', target="_blank", style={"color":"white", "marginTop":"3rem"}),
],style={"textAlign":'center'}, width=12),
]),
]),
className="section",
id="credits",
**{"data-label": "Credits"}
),
]),
###############SECTION NAVIGATION########################
html.Nav(
id="my-navigation",
className="nav",
**{"role": "navigation", "aria-label": "Main navigation"},
children=[
html.Div(
className="nav-item",
children=[
html.A(
"Start", # Add text content to the link
href="#home",
className="nav-link",
id="link1",
**{"aria-label": "Navigate to Home section"}
),
html.Span("Start", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Why it matters", # Add text content to the link
href="#why",
className="nav-link",
id="link2",
**{"aria-label": "Navigate to Why it matters"}
),
html.Span("Why it matters", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Aspects", # Add text content to the link
href="#aspects",
className="nav-link",
id="link3",
**{"aria-label": "Navigate to aspects"}
),
html.Span("Aspects", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Overall performance", # Add text content to the link
href="#performance",
className="nav-link",
id="link4",
**{"aria-label": "Navigate to performance"}
),
html.Span("Performance", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Different clutches", # Add text content to the link
href="#clutch",
className="nav-link",
id="link5",
**{"aria-label": "Navigate to different clutches"}
),
html.Span("Different clutches", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Top 10", # Add text content to the link
href="#top10",
className="nav-link",
id="link6",
**{"aria-label": "Navigate to top-10"}
),
html.Span("Swing", className="nav-label", **{"aria-hidden": "true"})
]
),
html.Div(
className="nav-item",
children=[
html.A(
"Credits", # Add text content to the link
href="#credits",
className="nav-link",
id="link7",
**{"aria-label": "Navigate to credits"}
),
html.Span("Credits", className="nav-label", **{"aria-hidden": "true"})
]
),
])
], fluid=True)
####SHOW SWING RANK PLAYER#########
@app.callback(
Output('player-rank', 'children'),
Input('select_player', 'value')
)
def show_player_rank(playerid):
if playerid == None:
return ''
#find the player
row=df_raw[df_raw['pid'] == int(playerid)]
#output rankinfo
output=html.Div([
html.Div(row['rank_swg_made_per_game'].values[0],className='onerank'),
html.Div(row['name'].values[0], className='rankname')
], className='rank')
return output
####SWITCH TOP 10 VIEWS CLUTCH PERCENTAGE####
@app.callback(
Output('top10-lollipop', 'children'),
Input('select_top10_view', 'value')
)
def switch_top10(selectedview):
if selectedview == None:
return dcc.Graph(figure=create_lollipop_clutch_adjusted(1))
else:
return dcc.Graph(figure=create_lollipop_clutch_adjusted(selectedview))
####FILTER SCATTERPLOT####
@app.callback(
Output('filteredscatter', 'children'),
Input('select_column', 'value')
)
def switch_top20_filter(selectedcolumn):
if selectedcolumn == None:
selectedcolumn=''
return dcc.Graph(figure=normal_vs_clutch_performance(selectedcolumn))
######SCROLLFUNCTION####
app.clientside_callback(
"""function (id) {
activateNavigation();
return window.dash_clientside.no_update
}""",
Output('my-navigation', 'children'), Input('my-navigation', 'children'))
if __name__ == '__main__':
app.run(debug=False)