Skip to content

Commit fd1041b

Browse files
feat: replace abstract plotly heatmaps with accurate 2D mplsoccer pitch maps
1 parent d6006a1 commit fd1041b

3 files changed

Lines changed: 36 additions & 39 deletions

File tree

app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
p_row = df_work[df_work["player"] == p_sel].iloc[0]
6969

7070
h1, h2 = st.columns(2)
71-
with h1: st.plotly_chart(graficar_heatmap_zonas(p_row), use_container_width=True)
72-
with h2: st.plotly_chart(graficar_heatmap_xt(p_row), use_container_width=True)
71+
with h1: st.pyplot(graficar_heatmap_zonas(p_row), use_container_width=True)
72+
with h2: st.pyplot(graficar_heatmap_xt(p_row), use_container_width=True)
7373

7474
st.markdown("<div class='section-header'>3. Red de Flujo</div>", unsafe_allow_html=True)
7575
st.plotly_chart(graficar_red_pases(df_all), use_container_width=True)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ streamlit>=1.20.0
22
pandas>=1.3.0
33
numpy>=1.21.0
44
plotly>=5.10.0
5+
mplsoccer>=1.2.0
6+
matplotlib>=3.7.0

src/streamlit/dashboard_plots.py

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import plotly.express as px
22
import plotly.graph_objects as go
33
import numpy as np
4+
import matplotlib
5+
matplotlib.use('Agg')
6+
import matplotlib.pyplot as plt
7+
from mplsoccer import Pitch
48

59
COLORS = {
610
"primary": "#DA291C", "secondary": "#FBE122", "accent": "#58a6ff",
@@ -67,47 +71,38 @@ def graficar_radar(df_f, players_sel):
6771
return fig_radar
6872

6973
def graficar_heatmap_zonas(player_row):
70-
zones_y = ["Bloque Bajo", "Zona Construc.", "Zona Ataque", "Último Tercio"]
71-
zones_x = ["Banda Izq.", "Semi-izq.", "Centro Izq.", "Centro Der.", "Semi-der.", "Banda Der."]
72-
74+
pitch = Pitch(pitch_type='statsbomb', pitch_color='#0d1117', line_color='#30363d')
75+
fig, ax = pitch.draw(figsize=(6, 4))
76+
fig.patch.set_facecolor('#0d1117')
77+
7378
np.random.seed(int(hash(player_row["player"])) % 999)
74-
base, vert = player_row["prog_passes"] * 3, player_row["vert_idx"]
75-
heat = np.random.exponential(base, (4, 6))
76-
heat[2:, 2:4] *= (1 + vert * 2)
77-
heat[:1, :] *= 0.4
78-
heat = np.clip(heat, 0, 35)
79-
80-
fig_hm = go.Figure(go.Heatmap(
81-
z=heat, x=zones_x, y=zones_y,
82-
colorscale=[[0, "#0d1117"], [0.25, "#1a3a5c"], [0.5, "#1f6feb"], [0.75, "#DA291C"], [1.0, "#FBE122"]],
83-
showscale=True, colorbar=dict(title="Intens.", thickness=14, bgcolor="rgba(0,0,0,0)"),
84-
hoverongaps=False
85-
))
86-
# Lineas tácticas
87-
fig_hm.add_shape(type="rect", x0=-0.5, x1=5.5, y0=-0.5, y1=3.5, line=dict(color="#30363d", width=1))
88-
fig_hm.add_shape(type="line", x0=-0.5, x1=5.5, y0=0.5, y1=0.5, line=dict(color="#30363d", dash="dot", width=1))
89-
fig_hm.add_shape(type="line", x0=-0.5, x1=5.5, y0=1.5, y1=1.5, line=dict(color="#30363d", dash="dot", width=1))
90-
fig_hm.add_shape(type="line", x0=-0.5, x1=5.5, y0=2.5, y1=2.5, line=dict(color="#DA291C", dash="dash", width=1.5))
79+
num_passes = int(player_row["prog_passes"] * 15) + 30
80+
81+
# Bias distribution based on metrics
82+
x = np.random.normal(50 + player_row["vert_idx"] * 25, 20, num_passes)
83+
y = np.random.normal(40, 25, num_passes)
84+
x = np.clip(x, 0, 120); y = np.clip(y, 0, 80)
9185

92-
fig_hm.update_layout(**PLOTLY_THEME, title=f"Pases progresivos — {player_row['player']}", height=400)
93-
fig_hm.update_xaxes(side="top", gridcolor="rgba(0,0,0,0)"); fig_hm.update_yaxes(gridcolor="rgba(0,0,0,0)")
94-
return fig_hm
86+
pitch.hexbin(x, y, ax=ax, edgecolors='#0d1117', gridsize=(8, 6), cmap='magma', alpha=0.8)
87+
ax.set_title(f"Distribución de Pases — {player_row['player']}", color="#e6edf3", size=12, pad=5)
88+
return fig
9589

9690
def graficar_heatmap_xt(player_row):
97-
zones_y = ["Bloque Bajo", "Zona Construc.", "Zona Ataque", "Último Tercio"]
98-
zones_x = ["Banda Izq.", "Semi-izq.", "Centro Izq.", "Centro Der.", "Semi-der.", "Banda Der."]
99-
100-
xT_zones = np.random.exponential(player_row["xT_gen"] * 5, (4, 6))
101-
xT_zones[3, 2:4] *= 2.5
102-
103-
fig_xt = go.Figure(go.Heatmap(
104-
z=xT_zones, x=zones_x, y=zones_y,
105-
colorscale=[[0, "#161b22"], [0.4, "#3fb950"], [0.8, "#FBE122"], [1, "#DA291C"]],
106-
colorbar=dict(title="xT", thickness=14, bgcolor="rgba(0,0,0,0)"),
107-
))
108-
fig_xt.update_layout(**PLOTLY_THEME, height=360, title=f"xT Generado — {player_row['player']}")
109-
fig_xt.update_xaxes(side="top", gridcolor="rgba(0,0,0,0)"); fig_xt.update_yaxes(gridcolor="rgba(0,0,0,0)")
110-
return fig_xt
91+
pitch = Pitch(pitch_type='statsbomb', pitch_color='#0d1117', line_color='#30363d', half=True)
92+
fig, ax = pitch.draw(figsize=(6, 4))
93+
fig.patch.set_facecolor('#0d1117')
94+
95+
np.random.seed(int(hash(player_row["player"])) % 999 + 1)
96+
num_events = int(player_row["xT_gen"] * 120) + 5
97+
98+
x = np.random.normal(95, 12, num_events)
99+
y = np.random.normal(40, 25, num_events)
100+
x = np.clip(x, 60, 120); y = np.clip(y, 0, 80)
101+
102+
sizes = np.random.uniform(30, 180, num_events)
103+
pitch.scatter(x, y, s=sizes, c='#DA291C', edgecolors='#0d1117', alpha=0.75, ax=ax)
104+
ax.set_title(f"Nodos de xT — {player_row['player']}", color="#e6edf3", size=12, pad=5)
105+
return fig
111106

112107
def graficar_red_pases(df):
113108
positions_xy = {

0 commit comments

Comments
 (0)