From 09b95fbbfd2763dca3eb7a6e90d8cd905a48755f Mon Sep 17 00:00:00 2001 From: indrasuhyar Date: Mon, 27 Apr 2026 21:54:02 -0400 Subject: [PATCH 1/4] feat: vertical layout, navy sidebar, soft blue theme, clearer gridlines --- .streamlit/config.toml | 6 ++ Homepage.py | 174 ++++++++++++++++++++------------ pages/2_WTI_Price.py | 152 +++++++++++++++++++--------- pages/3_Event_Context.py | 207 ++++++++++++++++++++++++++++----------- 4 files changed, 375 insertions(+), 164 deletions(-) create mode 100644 .streamlit/config.toml diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..df3ef0c --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[theme] +primaryColor = "#1B4F8A" +backgroundColor = "#F0F4F9" +secondaryBackgroundColor = "#E4EBF4" +textColor = "#1A1A1A" +font = "serif" diff --git a/Homepage.py b/Homepage.py index e5b8b27..103e380 100644 --- a/Homepage.py +++ b/Homepage.py @@ -1,7 +1,8 @@ import time -import matplotlib.pyplot as plt import pandas as pd +import plotly.express as px +import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account @@ -13,25 +14,50 @@ st.set_page_config(page_title="Weekly U.S. Petroleum Supply", layout="wide") # ========================= -# Sidebar title +# Sidebar title (above nav via CSS) # ========================= -st.sidebar.markdown( +st.markdown( """ -

- U.S. Petroleum & WTI Weekly Monitor -

+ """, unsafe_allow_html=True, ) -st.sidebar.caption("Source: EIA") -st.sidebar.divider() # ========================= # Main page header # ========================= st.title("Weekly U.S. Petroleum Supply") -st.subheader("Team Members: Irina, Indra") -st.caption("Source: U.S. Energy Information Administration (EIA)") +st.caption("Team Members: Irina, Indra ยท Source: U.S. Energy Information Administration (EIA)") # ========================= # Project Proposal @@ -332,50 +358,71 @@ def compute_product_price_sensitivity( st.divider() # ========================= -# Two side-by-side charts +# Stacked charts # ========================= -left_col, right_col = st.columns(TWO_COLUMN_LAYOUT) - -with left_col: - st.subheader("Total Product Supplied") - - fig, ax = plt.subplots(figsize=(7, 4)) - ax.plot(filtered_total["week"], filtered_total["total_supply"]) - ax.set_xlabel("Week") - ax.set_ylabel("Total Product Supplied") - st.pyplot(fig) - - with st.expander("Show total supply data table"): - total_display = filtered_total.sort_values("week", ascending=False).copy() - total_display["week"] = total_display["week"].dt.strftime("%Y-%m-%d") - st.dataframe(total_display, width="stretch") - -with right_col: - st.subheader("Product-Level Weekly Supply") - - if not selected_products: - st.warning("Please select at least one product from the sidebar.") - else: - product_plot_df = filtered_product[ - filtered_product["product_name"].isin(selected_products) - ].copy() - - fig2, ax2 = plt.subplots(figsize=(7, 4)) - for product_name in selected_products: - temp = product_plot_df[product_plot_df["product_name"] == product_name] - ax2.plot(temp["week"], temp["product_supplied"], label=product_name) - - ax2.set_xlabel("Week") - ax2.set_ylabel("Product Supplied") - ax2.legend() - st.pyplot(fig2) - - with st.expander("Show product-level data table"): - product_display = product_plot_df.sort_values( - ["product_name", "week"], ascending=[True, False] - ).copy() - product_display["week"] = product_display["week"].dt.strftime("%Y-%m-%d") - st.dataframe(product_display, width="stretch") +st.subheader("Total Product Supplied") + +fig = px.line( + filtered_total, + x="week", + y="total_supply", + labels={"week": "Week", "total_supply": "Total Product Supplied"}, +) +fig.update_layout( + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", +) +fig.update_traces(hovertemplate="%{x|%b %d, %Y}
Total Supply: %{y:,.0f}") +st.plotly_chart(fig, use_container_width=True) + +with st.expander("Show total supply data table"): + total_display = filtered_total.sort_values("week", ascending=False).copy() + total_display["week"] = total_display["week"].dt.strftime("%Y-%m-%d") + st.dataframe(total_display, width="stretch") + +st.divider() + +st.subheader("Product-Level Weekly Supply") + +if not selected_products: + st.warning("Please select at least one product from the sidebar.") +else: + product_plot_df = filtered_product[ + filtered_product["product_name"].isin(selected_products) + ].copy() + + fig2 = px.line( + product_plot_df, + x="week", + y="product_supplied", + color="product_name", + labels={ + "week": "Week", + "product_supplied": "Product Supplied", + "product_name": "Product", + }, + ) + fig2.update_layout( + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", + ) + fig2.update_traces( + hovertemplate="%{fullData.name}
%{x|%b %d, %Y}: %{y:,.0f}" + ) + st.plotly_chart(fig2, use_container_width=True) + + with st.expander("Show product-level data table"): + product_display = product_plot_df.sort_values( + ["product_name", "week"], ascending=[True, False] + ).copy() + product_display["week"] = product_display["week"].dt.strftime("%Y-%m-%d") + st.dataframe(product_display, width="stretch") st.divider() @@ -409,15 +456,20 @@ def compute_product_price_sensitivity( "abs_correlation", ascending=True ) - fig3, ax3 = plt.subplots(figsize=(6, 3.5)) - ax3.barh( - chart_df["product_name"], - chart_df["abs_correlation"], - color="darkorange", + fig3 = px.bar( + chart_df, + x="abs_correlation", + y="product_name", + orientation="h", + labels={ + "abs_correlation": "Absolute correlation with WTI price", + "product_name": "Product", + }, + color_discrete_sequence=["darkorange"], ) - ax3.set_xlabel("Absolute correlation with WTI price") - ax3.set_ylabel("Product") - st.pyplot(fig3) + fig3.update_layout(showlegend=False) + fig3.update_traces(hovertemplate="%{y}
Correlation: %{x:.4f}") + st.plotly_chart(fig3, use_container_width=True) with st.expander("Show product sensitivity table"): st.dataframe( diff --git a/pages/2_WTI_Price.py b/pages/2_WTI_Price.py index 04acf93..c3a2700 100644 --- a/pages/2_WTI_Price.py +++ b/pages/2_WTI_Price.py @@ -1,7 +1,8 @@ import time -import matplotlib.pyplot as plt import pandas as pd +import plotly.express as px +import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account @@ -13,18 +14,44 @@ st.set_page_config(page_title="WTI Price", layout="wide") # ========================= -# Sidebar title +# Sidebar title (above nav via CSS) # ========================= -st.sidebar.markdown( +st.markdown( """ -

- U.S. Petroleum & WTI Weekly Monitor -

+ """, unsafe_allow_html=True, ) -st.sidebar.caption("Source: EIA") -st.sidebar.divider() # ========================= # Main page header @@ -255,34 +282,66 @@ def find_top_highest_years(yearly_avg: pd.DataFrame, top_n: int) -> set[int]: st.divider() # ========================= -# Two charts side by side +# Stacked charts # ========================= -left_col, right_col = st.columns(TWO_COLUMN_LAYOUT) - -with left_col: - st.subheader("WTI Price Over Time (Weekly)") - - fig, ax = plt.subplots(figsize=(7, 4)) - ax.plot(filtered_wti["week"], filtered_wti["wti_price"], label="WTI price") - ax.plot( - filtered_wti["week"], - filtered_wti["wti_ma"], - label=f"{ma_window}-week moving average", +st.subheader("WTI Price Over Time (Weekly)") + +fig = go.Figure() +fig.add_trace( + go.Scatter( + x=filtered_wti["week"], + y=filtered_wti["wti_price"], + name="WTI Price", + mode="lines", + hovertemplate="WTI Price: $%{y:.2f}", + ) +) +fig.add_trace( + go.Scatter( + x=filtered_wti["week"], + y=filtered_wti["wti_ma"], + name=f"{ma_window}-Week Moving Average", + mode="lines", + hovertemplate=f"{ma_window}-Week Avg: $%{{y:.2f}}", ) - ax.set_xlabel("Week") - ax.set_ylabel("WTI price ($/barrel)") - ax.legend() - st.pyplot(fig) +) +fig.update_layout( + xaxis_title="Week", + yaxis_title="WTI Price ($/barrel)", + hovermode="x unified", + hoverlabel=dict(namelength=-1), + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", +) +st.plotly_chart(fig, use_container_width=True) -with right_col: - st.subheader("Weekly Change in WTI Price") +st.divider() - fig2, ax2 = plt.subplots(figsize=(7, 4)) - ax2.plot(filtered_wti["week"], filtered_wti["weekly_change"]) - ax2.axhline(0, color="gray", linewidth=1) - ax2.set_xlabel("Week") - ax2.set_ylabel("Weekly change ($/barrel)") - st.pyplot(fig2) +st.subheader("Weekly Change in WTI Price") + +fig2 = go.Figure() +fig2.add_trace( + go.Scatter( + x=filtered_wti["week"], + y=filtered_wti["weekly_change"], + mode="lines", + name="Weekly Change", + hovertemplate="%{x|%b %d, %Y}
Change: $%{y:.2f}", + ) +) +fig2.add_hline(y=0, line_color="#0D2B5E", line_width=1.5) +fig2.update_layout( + xaxis_title="Week", + yaxis_title="Weekly Change ($/barrel)", + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", +) +st.plotly_chart(fig2, use_container_width=True) st.divider() st.subheader("Real-Time Interpretation") @@ -302,20 +361,25 @@ def find_top_highest_years(yearly_avg: pd.DataFrame, top_n: int) -> set[int]: highlight_years = find_top_highest_years(yearly_avg, TOP_HIGHLIGHT_YEARS) -bar_colors = [ - YEAR_BAR_HIGHLIGHT_COLOR if year in highlight_years else YEAR_BAR_DEFAULT_COLOR - for year in yearly_avg["year"] -] +yearly_avg["color"] = yearly_avg["year"].apply( + lambda y: "Top 5" if y in highlight_years else "Other" +) -fig3, ax3 = plt.subplots(figsize=(8, 5)) -ax3.barh( - yearly_avg["year"].astype(str), - yearly_avg["avg_wti_price"], - color=bar_colors, +fig3 = px.bar( + yearly_avg, + x="avg_wti_price", + y=yearly_avg["year"].astype(str), + orientation="h", + color="color", + color_discrete_map={"Top 5": "darkorange", "Other": "steelblue"}, + labels={"avg_wti_price": "Average WTI Price ($/barrel)", "y": "Year"}, +) +fig3.update_layout( + showlegend=False, + hoverlabel=dict(namelength=-1), ) -ax3.set_xlabel("Average WTI price ($/barrel)") -ax3.set_ylabel("Year") -st.pyplot(fig3) +fig3.update_traces(hovertemplate="%{y}
Avg WTI Price: $%{x:.2f}") +st.plotly_chart(fig3, use_container_width=True) if highlight_years: top_year_text = ", ".join( diff --git a/pages/3_Event_Context.py b/pages/3_Event_Context.py index d445a25..88eb26d 100644 --- a/pages/3_Event_Context.py +++ b/pages/3_Event_Context.py @@ -1,7 +1,8 @@ import time -import matplotlib.pyplot as plt import pandas as pd +import plotly.express as px +import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account @@ -13,16 +14,44 @@ # ========================= # Sidebar title # ========================= -st.sidebar.markdown( +# Sidebar title (above nav via CSS) +# ========================= +st.markdown( """ -

- U.S. Petroleum & WTI Weekly Monitor -

+ """, unsafe_allow_html=True, ) -st.sidebar.caption("Source: EIA + GDELT") -st.sidebar.divider() # ========================= # Main page header @@ -262,62 +291,118 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame: st.divider() # ========================= -# Two side-by-side charts +# Stacked charts # ========================= -left_col, right_col = st.columns(TWO_COLUMN_LAYOUT) - -with left_col: - st.subheader("Weekly Event Count") +st.subheader("Weekly Event Count") - fig1, ax1 = plt.subplots(figsize=(7, 4)) - ax1.plot(filtered["week"], filtered["event_count"]) - ax1.set_xlabel("Week") - ax1.set_ylabel("Event count") - st.pyplot(fig1) +fig1 = px.line( + filtered, + x="week", + y="event_count", + labels={"week": "Week", "event_count": "Event Count"}, +) +fig1.update_layout( + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", +) +fig1.update_traces(hovertemplate="%{x|%b %d, %Y}
Events: %{y:,.0f}") +st.plotly_chart(fig1, use_container_width=True) - st.caption( - "This chart shows how many GDELT-recorded events occurred each week. " - "Use it to see whether broader event activity rises during weeks when " - "WTI or petroleum supply experiences unusual movement." - ) +st.caption( + "This chart shows how many GDELT-recorded events occurred each week. " + "Use it to see whether broader event activity rises during weeks when " + "WTI or petroleum supply experiences unusual movement." +) -with right_col: - st.subheader("Average Event Tone") +st.divider() - fig2, ax2 = plt.subplots(figsize=(7, 4)) - ax2.plot(filtered["week"], filtered["avg_tone"]) - ax2.axhline(0, color="gray", linewidth=1) - ax2.set_xlabel("Week") - ax2.set_ylabel("Average tone") - st.pyplot(fig2) +st.subheader("Average Event Tone") - st.caption( - "This chart tracks the average tone of events in each week. " - "More negative values suggest a more adverse event environment, which may " - "help contextualize stress periods in the energy data." +fig2 = go.Figure() +fig2.add_trace( + go.Scatter( + x=filtered["week"], + y=filtered["avg_tone"], + mode="lines", + name="Average Tone", + hovertemplate="%{x|%b %d, %Y}
Avg Tone: %{y:.2f}", ) +) +fig2.add_hline(y=0, line_color="#0D2B5E", line_width=1.5) +fig2.update_layout( + xaxis_title="Week", + yaxis_title="Average Tone", + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", +) +st.plotly_chart(fig2, use_container_width=True) + +st.caption( + "This chart tracks the average tone of events in each week. " + "More negative values suggest a more adverse event environment, which may " + "help contextualize stress periods in the energy data." +) st.divider() st.subheader("Weekly Event Composition") -fig3, ax3 = plt.subplots(figsize=(10, 4.5)) -ax3.stackplot( - filtered["week"], - filtered["verbal_cooperation_count"], - filtered["material_cooperation_count"], - filtered["verbal_conflict_count"], - filtered["material_conflict_count"], - labels=[ - "Verbal cooperation", - "Material cooperation", - "Verbal conflict", - "Material conflict", - ], +fig3 = go.Figure() +fig3.add_trace( + go.Scatter( + x=filtered["week"], + y=filtered["verbal_cooperation_count"], + name="Verbal Cooperation", + stackgroup="one", + mode="lines", + hovertemplate="Verbal Cooperation: %{y:,.0f}", + ) +) +fig3.add_trace( + go.Scatter( + x=filtered["week"], + y=filtered["material_cooperation_count"], + name="Material Cooperation", + stackgroup="one", + mode="lines", + hovertemplate="Material Cooperation: %{y:,.0f}", + ) +) +fig3.add_trace( + go.Scatter( + x=filtered["week"], + y=filtered["verbal_conflict_count"], + name="Verbal Conflict", + stackgroup="one", + mode="lines", + hovertemplate="Verbal Conflict: %{y:,.0f}", + ) +) +fig3.add_trace( + go.Scatter( + x=filtered["week"], + y=filtered["material_conflict_count"], + name="Material Conflict", + stackgroup="one", + mode="lines", + hovertemplate="Material Conflict: %{y:,.0f}", + ) +) +fig3.update_layout( + xaxis_title="Week", + yaxis_title="Event Count", + hovermode="x unified", + xaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + yaxis=dict(gridcolor="rgba(13,43,94,0.2)", linecolor="#0D2B5E"), + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", ) -ax3.set_xlabel("Week") -ax3.set_ylabel("Event count") -ax3.legend(loc="upper left") -st.pyplot(fig3) +st.plotly_chart(fig3, use_container_width=True) st.caption( "This stacked chart breaks weekly events into the four broad GDELT classes. " @@ -351,15 +436,19 @@ def build_anomaly_table(merged_df: pd.DataFrame, top_n: int) -> pd.DataFrame: chart_df["week_label"] = chart_df["week"].dt.strftime("%Y-%m-%d") chart_df = chart_df.sort_values("combined_shock_score", ascending=True) - fig4, ax4 = plt.subplots(figsize=(5, 3)) - ax4.barh( - chart_df["week_label"], - chart_df["combined_shock_score"], - color=ANOMALY_BAR_COLOR, + fig4 = px.bar( + chart_df, + x="combined_shock_score", + y="week_label", + orientation="h", + labels={ + "combined_shock_score": "Combined Shock Score", + "week_label": "Week", + }, + color_discrete_sequence=[ANOMALY_BAR_COLOR], ) - ax4.set_xlabel("Combined shock score") - ax4.set_ylabel("Week") - st.pyplot(fig4) + fig4.update_traces(hovertemplate="Week: %{y}
Shock Score: %{x:.2f}") + st.plotly_chart(fig4, use_container_width=True) st.caption( "This chart visualizes the anomaly ranking used to build the table below. " From a6193b67be37b18a8bc7e8f7b1871554818cf77a Mon Sep 17 00:00:00 2001 From: indrasuhyar Date: Mon, 27 Apr 2026 22:06:50 -0400 Subject: [PATCH 2/4] fix: remove unused go import, apply ruff format --- Homepage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Homepage.py b/Homepage.py index 103e380..9e2a332 100644 --- a/Homepage.py +++ b/Homepage.py @@ -2,7 +2,6 @@ import pandas as pd import plotly.express as px -import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account From 927efb7a599628dd4debc8bdfed4ce6b7fc5209b Mon Sep 17 00:00:00 2001 From: indrasuhyar Date: Mon, 27 Apr 2026 22:21:03 -0400 Subject: [PATCH 3/4] fix: dark text on sidebar date inputs --- Homepage.py | 8 ++++++++ pages/2_WTI_Price.py | 7 +++++++ pages/3_Event_Context.py | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/Homepage.py b/Homepage.py index 9e2a332..08ad7cb 100644 --- a/Homepage.py +++ b/Homepage.py @@ -2,6 +2,7 @@ import pandas as pd import plotly.express as px +import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account @@ -24,6 +25,13 @@ [data-testid="stSidebar"] * { color: white !important; } + [data-testid="stSidebar"] input { + color: #1A1A1A !important; + } + [data-testid="stSidebar"] .stDateInput input { + color: #1A1A1A !important; + background-color: #E4EBF4 !important; + } [data-testid="stSidebarNav"] a { color: rgba(255,255,255,0.8) !important; } diff --git a/pages/2_WTI_Price.py b/pages/2_WTI_Price.py index c3a2700..de98665 100644 --- a/pages/2_WTI_Price.py +++ b/pages/2_WTI_Price.py @@ -25,6 +25,13 @@ [data-testid="stSidebar"] * { color: white !important; } + [data-testid="stSidebar"] input { + color: #1A1A1A !important; + } + [data-testid="stSidebar"] .stDateInput input { + color: #1A1A1A !important; + background-color: #E4EBF4 !important; + } [data-testid="stSidebarNav"] a { color: rgba(255,255,255,0.8) !important; } diff --git a/pages/3_Event_Context.py b/pages/3_Event_Context.py index 88eb26d..503ef29 100644 --- a/pages/3_Event_Context.py +++ b/pages/3_Event_Context.py @@ -25,6 +25,13 @@ [data-testid="stSidebar"] * { color: white !important; } + [data-testid="stSidebar"] input { + color: #1A1A1A !important; + } + [data-testid="stSidebar"] .stDateInput input { + color: #1A1A1A !important; + background-color: #E4EBF4 !important; + } [data-testid="stSidebarNav"] a { color: rgba(255,255,255,0.8) !important; } From 5b0f09fd936f3a01fabb47900e878cf1135099b3 Mon Sep 17 00:00:00 2001 From: indrasuhyar Date: Mon, 27 Apr 2026 22:23:42 -0400 Subject: [PATCH 4/4] fix: remove unused go import, dark text on date inputs --- Homepage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Homepage.py b/Homepage.py index 08ad7cb..b14427b 100644 --- a/Homepage.py +++ b/Homepage.py @@ -2,7 +2,6 @@ import pandas as pd import plotly.express as px -import plotly.graph_objects as go import streamlit as st from google.cloud import bigquery from google.oauth2 import service_account