Skip to content

Commit 1c40c18

Browse files
committed
refactor: replace matplotlib with plotly for interactive charts
1 parent b7b3ed5 commit 1c40c18

3 files changed

Lines changed: 161 additions & 89 deletions

File tree

Homepage.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import time
22

3-
import matplotlib.pyplot as plt
43
import pandas as pd
4+
import plotly.express as px
5+
import plotly.graph_objects as go
56
import streamlit as st
67
from google.cloud import bigquery
78
from google.oauth2 import service_account
@@ -339,11 +340,17 @@ def compute_product_price_sensitivity(
339340
with left_col:
340341
st.subheader("Total Product Supplied")
341342

342-
fig, ax = plt.subplots(figsize=(7, 4))
343-
ax.plot(filtered_total["week"], filtered_total["total_supply"])
344-
ax.set_xlabel("Week")
345-
ax.set_ylabel("Total Product Supplied")
346-
st.pyplot(fig)
343+
fig = px.line(
344+
filtered_total,
345+
x="week",
346+
y="total_supply",
347+
labels={"week": "Week", "total_supply": "Total Product Supplied"},
348+
)
349+
fig.update_layout(hovermode="x unified")
350+
fig.update_traces(
351+
hovertemplate="<b>%{x|%b %d, %Y}</b><br>Total Supply: %{y:,.0f}<extra></extra>"
352+
)
353+
st.plotly_chart(fig, use_container_width=True)
347354

348355
with st.expander("Show total supply data table"):
349356
total_display = filtered_total.sort_values("week", ascending=False).copy()
@@ -360,15 +367,22 @@ def compute_product_price_sensitivity(
360367
filtered_product["product_name"].isin(selected_products)
361368
].copy()
362369

363-
fig2, ax2 = plt.subplots(figsize=(7, 4))
364-
for product_name in selected_products:
365-
temp = product_plot_df[product_plot_df["product_name"] == product_name]
366-
ax2.plot(temp["week"], temp["product_supplied"], label=product_name)
367-
368-
ax2.set_xlabel("Week")
369-
ax2.set_ylabel("Product Supplied")
370-
ax2.legend()
371-
st.pyplot(fig2)
370+
fig2 = px.line(
371+
product_plot_df,
372+
x="week",
373+
y="product_supplied",
374+
color="product_name",
375+
labels={
376+
"week": "Week",
377+
"product_supplied": "Product Supplied",
378+
"product_name": "Product",
379+
},
380+
)
381+
fig2.update_layout(hovermode="x unified")
382+
fig2.update_traces(
383+
hovertemplate="<b>%{fullData.name}</b><br>%{x|%b %d, %Y}: %{y:,.0f}<extra></extra>"
384+
)
385+
st.plotly_chart(fig2, use_container_width=True)
372386

373387
with st.expander("Show product-level data table"):
374388
product_display = product_plot_df.sort_values(
@@ -409,15 +423,22 @@ def compute_product_price_sensitivity(
409423
"abs_correlation", ascending=True
410424
)
411425

412-
fig3, ax3 = plt.subplots(figsize=(6, 3.5))
413-
ax3.barh(
414-
chart_df["product_name"],
415-
chart_df["abs_correlation"],
416-
color="darkorange",
426+
fig3 = px.bar(
427+
chart_df,
428+
x="abs_correlation",
429+
y="product_name",
430+
orientation="h",
431+
labels={
432+
"abs_correlation": "Absolute correlation with WTI price",
433+
"product_name": "Product",
434+
},
435+
color_discrete_sequence=["darkorange"],
436+
)
437+
fig3.update_layout(showlegend=False)
438+
fig3.update_traces(
439+
hovertemplate="<b>%{y}</b><br>Correlation: %{x:.4f}<extra></extra>"
417440
)
418-
ax3.set_xlabel("Absolute correlation with WTI price")
419-
ax3.set_ylabel("Product")
420-
st.pyplot(fig3)
441+
st.plotly_chart(fig3, use_container_width=True)
421442

422443
with st.expander("Show product sensitivity table"):
423444
st.dataframe(

pages/2_WTI_Price.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import time
22

3-
import matplotlib.pyplot as plt
43
import pandas as pd
4+
import plotly.express as px
5+
import plotly.graph_objects as go
56
import streamlit as st
67
from google.cloud import bigquery
78
from google.oauth2 import service_account
@@ -262,27 +263,41 @@ def find_top_highest_years(yearly_avg: pd.DataFrame, top_n: int) -> set[int]:
262263
with left_col:
263264
st.subheader("WTI Price Over Time (Weekly)")
264265

265-
fig, ax = plt.subplots(figsize=(7, 4))
266-
ax.plot(filtered_wti["week"], filtered_wti["wti_price"], label="WTI price")
267-
ax.plot(
268-
filtered_wti["week"],
269-
filtered_wti["wti_ma"],
270-
label=f"{ma_window}-week moving average",
266+
fig = go.Figure()
267+
fig.add_trace(go.Scatter(
268+
x=filtered_wti["week"], y=filtered_wti["wti_price"],
269+
name="WTI Price", mode="lines",
270+
hovertemplate="WTI Price: $%{y:.2f}<extra></extra>",
271+
))
272+
fig.add_trace(go.Scatter(
273+
x=filtered_wti["week"], y=filtered_wti["wti_ma"],
274+
name=f"{ma_window}-Week Moving Average", mode="lines",
275+
hovertemplate=f"{ma_window}-Week Avg: $%{{y:.2f}}<extra></extra>",
276+
))
277+
fig.update_layout(
278+
xaxis_title="Week",
279+
yaxis_title="WTI Price ($/barrel)",
280+
hovermode="x unified",
281+
hoverlabel=dict(namelength=-1),
271282
)
272-
ax.set_xlabel("Week")
273-
ax.set_ylabel("WTI price ($/barrel)")
274-
ax.legend()
275-
st.pyplot(fig)
283+
st.plotly_chart(fig, use_container_width=True)
276284

277285
with right_col:
278286
st.subheader("Weekly Change in WTI Price")
279287

280-
fig2, ax2 = plt.subplots(figsize=(7, 4))
281-
ax2.plot(filtered_wti["week"], filtered_wti["weekly_change"])
282-
ax2.axhline(0, color="gray", linewidth=1)
283-
ax2.set_xlabel("Week")
284-
ax2.set_ylabel("Weekly change ($/barrel)")
285-
st.pyplot(fig2)
288+
fig2 = go.Figure()
289+
fig2.add_trace(go.Scatter(
290+
x=filtered_wti["week"], y=filtered_wti["weekly_change"],
291+
mode="lines", name="Weekly Change",
292+
hovertemplate="<b>%{x|%b %d, %Y}</b><br>Change: $%{y:.2f}<extra></extra>",
293+
))
294+
fig2.add_hline(y=0, line_color="gray", line_width=1)
295+
fig2.update_layout(
296+
xaxis_title="Week",
297+
yaxis_title="Weekly Change ($/barrel)",
298+
hovermode="x unified",
299+
)
300+
st.plotly_chart(fig2, use_container_width=True)
286301

287302
st.divider()
288303
st.subheader("Real-Time Interpretation")
@@ -302,20 +317,27 @@ def find_top_highest_years(yearly_avg: pd.DataFrame, top_n: int) -> set[int]:
302317

303318
highlight_years = find_top_highest_years(yearly_avg, TOP_HIGHLIGHT_YEARS)
304319

305-
bar_colors = [
306-
YEAR_BAR_HIGHLIGHT_COLOR if year in highlight_years else YEAR_BAR_DEFAULT_COLOR
307-
for year in yearly_avg["year"]
308-
]
320+
yearly_avg["color"] = yearly_avg["year"].apply(
321+
lambda y: "Top 5" if y in highlight_years else "Other"
322+
)
309323

310-
fig3, ax3 = plt.subplots(figsize=(8, 5))
311-
ax3.barh(
312-
yearly_avg["year"].astype(str),
313-
yearly_avg["avg_wti_price"],
314-
color=bar_colors,
324+
fig3 = px.bar(
325+
yearly_avg,
326+
x="avg_wti_price",
327+
y=yearly_avg["year"].astype(str),
328+
orientation="h",
329+
color="color",
330+
color_discrete_map={"Top 5": "darkorange", "Other": "steelblue"},
331+
labels={"avg_wti_price": "Average WTI Price ($/barrel)", "y": "Year"},
332+
)
333+
fig3.update_layout(
334+
showlegend=False,
335+
hoverlabel=dict(namelength=-1),
336+
)
337+
fig3.update_traces(
338+
hovertemplate="<b>%{y}</b><br>Avg WTI Price: $%{x:.2f}<extra></extra>"
315339
)
316-
ax3.set_xlabel("Average WTI price ($/barrel)")
317-
ax3.set_ylabel("Year")
318-
st.pyplot(fig3)
340+
st.plotly_chart(fig3, use_container_width=True)
319341

320342
if highlight_years:
321343
top_year_text = ", ".join(

pages/3_Event_Context.py

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import time
22

3-
import matplotlib.pyplot as plt
43
import pandas as pd
4+
import plotly.express as px
5+
import plotly.graph_objects as go
56
import streamlit as st
67
from google.cloud import bigquery
78
from google.oauth2 import service_account
@@ -269,11 +270,17 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame:
269270
with left_col:
270271
st.subheader("Weekly Event Count")
271272

272-
fig1, ax1 = plt.subplots(figsize=(7, 4))
273-
ax1.plot(filtered["week"], filtered["event_count"])
274-
ax1.set_xlabel("Week")
275-
ax1.set_ylabel("Event count")
276-
st.pyplot(fig1)
273+
fig1 = px.line(
274+
filtered,
275+
x="week",
276+
y="event_count",
277+
labels={"week": "Week", "event_count": "Event Count"},
278+
)
279+
fig1.update_layout(hovermode="x unified")
280+
fig1.update_traces(
281+
hovertemplate="<b>%{x|%b %d, %Y}</b><br>Events: %{y:,.0f}<extra></extra>"
282+
)
283+
st.plotly_chart(fig1, use_container_width=True)
277284

278285
st.caption(
279286
"This chart shows how many GDELT-recorded events occurred each week. "
@@ -284,12 +291,19 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame:
284291
with right_col:
285292
st.subheader("Average Event Tone")
286293

287-
fig2, ax2 = plt.subplots(figsize=(7, 4))
288-
ax2.plot(filtered["week"], filtered["avg_tone"])
289-
ax2.axhline(0, color="gray", linewidth=1)
290-
ax2.set_xlabel("Week")
291-
ax2.set_ylabel("Average tone")
292-
st.pyplot(fig2)
294+
fig2 = go.Figure()
295+
fig2.add_trace(go.Scatter(
296+
x=filtered["week"], y=filtered["avg_tone"],
297+
mode="lines", name="Average Tone",
298+
hovertemplate="<b>%{x|%b %d, %Y}</b><br>Avg Tone: %{y:.2f}<extra></extra>",
299+
))
300+
fig2.add_hline(y=0, line_color="gray", line_width=1)
301+
fig2.update_layout(
302+
xaxis_title="Week",
303+
yaxis_title="Average Tone",
304+
hovermode="x unified",
305+
)
306+
st.plotly_chart(fig2, use_container_width=True)
293307

294308
st.caption(
295309
"This chart tracks the average tone of events in each week. "
@@ -300,24 +314,33 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame:
300314
st.divider()
301315
st.subheader("Weekly Event Composition")
302316

303-
fig3, ax3 = plt.subplots(figsize=(10, 4.5))
304-
ax3.stackplot(
305-
filtered["week"],
306-
filtered["verbal_cooperation_count"],
307-
filtered["material_cooperation_count"],
308-
filtered["verbal_conflict_count"],
309-
filtered["material_conflict_count"],
310-
labels=[
311-
"Verbal cooperation",
312-
"Material cooperation",
313-
"Verbal conflict",
314-
"Material conflict",
315-
],
317+
fig3 = go.Figure()
318+
fig3.add_trace(go.Scatter(
319+
x=filtered["week"], y=filtered["verbal_cooperation_count"],
320+
name="Verbal Cooperation", stackgroup="one", mode="lines",
321+
hovertemplate="Verbal Cooperation: %{y:,.0f}<extra></extra>",
322+
))
323+
fig3.add_trace(go.Scatter(
324+
x=filtered["week"], y=filtered["material_cooperation_count"],
325+
name="Material Cooperation", stackgroup="one", mode="lines",
326+
hovertemplate="Material Cooperation: %{y:,.0f}<extra></extra>",
327+
))
328+
fig3.add_trace(go.Scatter(
329+
x=filtered["week"], y=filtered["verbal_conflict_count"],
330+
name="Verbal Conflict", stackgroup="one", mode="lines",
331+
hovertemplate="Verbal Conflict: %{y:,.0f}<extra></extra>",
332+
))
333+
fig3.add_trace(go.Scatter(
334+
x=filtered["week"], y=filtered["material_conflict_count"],
335+
name="Material Conflict", stackgroup="one", mode="lines",
336+
hovertemplate="Material Conflict: %{y:,.0f}<extra></extra>",
337+
))
338+
fig3.update_layout(
339+
xaxis_title="Week",
340+
yaxis_title="Event Count",
341+
hovermode="x unified",
316342
)
317-
ax3.set_xlabel("Week")
318-
ax3.set_ylabel("Event count")
319-
ax3.legend(loc="upper left")
320-
st.pyplot(fig3)
343+
st.plotly_chart(fig3, use_container_width=True)
321344

322345
st.caption(
323346
"This stacked chart breaks weekly events into the four broad GDELT classes. "
@@ -351,15 +374,21 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame:
351374
chart_df["week_label"] = chart_df["week"].dt.strftime("%Y-%m-%d")
352375
chart_df = chart_df.sort_values("combined_shock_score", ascending=True)
353376

354-
fig4, ax4 = plt.subplots(figsize=(5, 3))
355-
ax4.barh(
356-
chart_df["week_label"],
357-
chart_df["combined_shock_score"],
358-
color=ANOMALY_BAR_COLOR,
377+
fig4 = px.bar(
378+
chart_df,
379+
x="combined_shock_score",
380+
y="week_label",
381+
orientation="h",
382+
labels={
383+
"combined_shock_score": "Combined Shock Score",
384+
"week_label": "Week",
385+
},
386+
color_discrete_sequence=[ANOMALY_BAR_COLOR],
387+
)
388+
fig4.update_traces(
389+
hovertemplate="<b>Week: %{y}</b><br>Shock Score: %{x:.2f}<extra></extra>"
359390
)
360-
ax4.set_xlabel("Combined shock score")
361-
ax4.set_ylabel("Week")
362-
st.pyplot(fig4)
391+
st.plotly_chart(fig4, use_container_width=True)
363392

364393
st.caption(
365394
"This chart visualizes the anomaly ranking used to build the table below. "

0 commit comments

Comments
 (0)