Spaces:
Running
Running
Change theme to match the brand
Browse files- .streamlit/config.toml +10 -5
- streamlit_app.py +61 -90
.streamlit/config.toml
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 1 |
[theme]
|
| 2 |
-
|
| 3 |
-
primaryColor
|
| 4 |
-
backgroundColor
|
| 5 |
-
secondaryBackgroundColor =
|
| 6 |
-
textColor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
font = "sans serif"
|
| 8 |
|
| 9 |
[server]
|
|
|
|
| 1 |
[theme]
|
| 2 |
+
# Light theme tuned to the Copper Group brand palette.
|
| 3 |
+
# primaryColor = Copper Buffet wordmark color (#906848)
|
| 4 |
+
# backgroundColor = warm off-white / cream
|
| 5 |
+
# secondaryBackgroundColor = slightly darker cream for sidebar + cards
|
| 6 |
+
# textColor = dark espresso-brown for body text
|
| 7 |
+
base = "light"
|
| 8 |
+
primaryColor = "#906848"
|
| 9 |
+
backgroundColor = "#FAF7F2"
|
| 10 |
+
secondaryBackgroundColor = "#F2EBE0"
|
| 11 |
+
textColor = "#2A1F15"
|
| 12 |
font = "sans serif"
|
| 13 |
|
| 14 |
[server]
|
streamlit_app.py
CHANGED
|
@@ -44,29 +44,54 @@ st.set_page_config(
|
|
| 44 |
st.markdown(
|
| 45 |
"""
|
| 46 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
div[data-testid="stMetric"] {
|
| 48 |
-
background:
|
| 49 |
-
border: 1px solid
|
| 50 |
border-radius: 10px;
|
| 51 |
padding: 16px 20px;
|
| 52 |
}
|
| 53 |
div[data-testid="stMetric"] [data-testid="stMetricValue"] {
|
| 54 |
font-size: 28px;
|
| 55 |
font-weight: 600;
|
|
|
|
| 56 |
}
|
| 57 |
div[data-testid="stMetric"] [data-testid="stMetricLabel"] {
|
| 58 |
-
color:
|
| 59 |
font-size: 12px;
|
| 60 |
letter-spacing: 0.5px;
|
| 61 |
text-transform: uppercase;
|
| 62 |
}
|
| 63 |
section[data-testid="stSidebar"] {
|
| 64 |
-
background:
|
| 65 |
}
|
| 66 |
.small-caption {
|
| 67 |
-
color:
|
| 68 |
font-size: 12px;
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
/* ββ Mobile-responsive overrides (phones / narrow tablets) βββββββββββ
|
| 72 |
Streamlit's default st.columns() lays out equal-width children in a
|
|
@@ -257,8 +282,8 @@ def _oauth_gate() -> None:
|
|
| 257 |
f"""
|
| 258 |
<div style="max-width: 420px; margin: 80px auto 0 auto; text-align: center;">
|
| 259 |
<div style="font-size: 32px; margin-bottom: 8px;">π</div>
|
| 260 |
-
<h2 style="margin: 0 0 8px 0;">Copper Group Dashboard</h2>
|
| 261 |
-
<p style="color: #
|
| 262 |
Restricted to members of the
|
| 263 |
<code>CB-Group</code> organization on Hugging Face.
|
| 264 |
Sign in with your HF account to continue.
|
|
@@ -267,19 +292,20 @@ def _oauth_gate() -> None:
|
|
| 267 |
display: inline-flex;
|
| 268 |
align-items: center;
|
| 269 |
gap: 10px;
|
| 270 |
-
background: #
|
| 271 |
-
color: #
|
| 272 |
padding: 12px 28px;
|
| 273 |
border-radius: 10px;
|
| 274 |
font-weight: 600;
|
| 275 |
font-size: 15px;
|
| 276 |
text-decoration: none;
|
|
|
|
| 277 |
">π€ Sign in with Hugging Face</a>
|
| 278 |
-
<p style="color: #
|
| 279 |
Don't have an HF account? Ask the dashboard owner to invite
|
| 280 |
you to the <code>CB-Group</code> org, then
|
| 281 |
<a href="https://huggingface.co/join" target="_blank"
|
| 282 |
-
style="color: #
|
| 283 |
</p>
|
| 284 |
</div>
|
| 285 |
""",
|
|
@@ -719,15 +745,15 @@ if not kpi_daily.empty and "Year" in kpi_daily.columns:
|
|
| 719 |
years_avail = sorted(years_avail, reverse=True)
|
| 720 |
|
| 721 |
# Default the year filter to the current calendar year when present in the
|
| 722 |
-
# data; otherwise
|
| 723 |
-
# something on empty / future-dated runs.
|
| 724 |
from datetime import datetime as _dt
|
| 725 |
_current_year = _dt.now().year
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
|
|
|
| 729 |
)
|
| 730 |
-
sel_year = st.sidebar.selectbox("Year", _year_options, index=_year_default_index)
|
| 731 |
|
| 732 |
if min_date and max_date:
|
| 733 |
# Default range: Jan 1 of the current year β yesterday. Clamp both ends
|
|
@@ -788,11 +814,11 @@ def apply_filters(df: pd.DataFrame, *, use_date: bool = True) -> pd.DataFrame:
|
|
| 788 |
out = out[out["Date"] >= pd.to_datetime(date_from)]
|
| 789 |
if date_to is not None:
|
| 790 |
out = out[out["Date"] <= pd.to_datetime(date_to)]
|
| 791 |
-
if
|
| 792 |
if "Year" in out.columns:
|
| 793 |
-
out = out[out["Year"]
|
| 794 |
elif "Date" in out.columns:
|
| 795 |
-
out = out[out["Date"].dt.year
|
| 796 |
return out
|
| 797 |
|
| 798 |
|
|
@@ -827,24 +853,29 @@ def fmt_qty(n) -> str:
|
|
| 827 |
|
| 828 |
|
| 829 |
def style_plotly(fig, *, height: int | None = None):
|
| 830 |
-
"""Common Plotly cosmetics β transparent background + theme
|
| 831 |
fig.update_layout(
|
| 832 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 833 |
plot_bgcolor="rgba(0,0,0,0)",
|
| 834 |
margin=dict(l=10, r=10, t=30, b=10),
|
| 835 |
legend=dict(orientation="h", yanchor="bottom", y=-0.25, x=0),
|
|
|
|
| 836 |
)
|
| 837 |
if height is not None:
|
| 838 |
fig.update_layout(height=height)
|
| 839 |
-
fig.update_xaxes(gridcolor="#
|
| 840 |
-
fig.update_yaxes(gridcolor="#
|
| 841 |
return fig
|
| 842 |
|
| 843 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
RESTAURANT_COLOR = {
|
| 845 |
-
"Copper Buffet": "#
|
| 846 |
-
"Tiew Copper": "#
|
| 847 |
-
"Group": "#
|
| 848 |
}
|
| 849 |
|
| 850 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -980,11 +1011,11 @@ with tab_summary:
|
|
| 980 |
out = out[out["Date"] >= pd.to_datetime(date_from)]
|
| 981 |
if date_to is not None:
|
| 982 |
out = out[out["Date"] <= pd.to_datetime(date_to)]
|
| 983 |
-
if
|
| 984 |
if "Year" in out.columns:
|
| 985 |
-
out = out[out["Year"]
|
| 986 |
elif "Date" in out.columns:
|
| 987 |
-
out = out[out["Date"].dt.year
|
| 988 |
return out
|
| 989 |
|
| 990 |
def _render_restaurant_summary(restaurant_name: str) -> None:
|
|
@@ -1001,11 +1032,6 @@ with tab_summary:
|
|
| 1001 |
st.info(f"No data for {restaurant_name} in the current filters.")
|
| 1002 |
return
|
| 1003 |
|
| 1004 |
-
# ββ Top-line totals for this restaurant ββββββββββββββββββββββββββ
|
| 1005 |
-
tot_rev = daily["Revenue"].sum() if "Revenue" in daily.columns else 0
|
| 1006 |
-
tot_cust = daily["Customers"].sum() if "Customers" in daily.columns else 0
|
| 1007 |
-
rev_ph = (tot_rev / tot_cust) if tot_cust else 0
|
| 1008 |
-
|
| 1009 |
# Pre-compute extended monthly DataFrame *once*, reused for both the
|
| 1010 |
# trend charts (below) and the Monthly summary table further down.
|
| 1011 |
# For Copper Buffet, append per-round customer columns + %Cap +
|
|
@@ -1106,61 +1132,6 @@ with tab_summary:
|
|
| 1106 |
)
|
| 1107 |
m = m.drop(columns=["_Cap", "_TotCust2"])
|
| 1108 |
|
| 1109 |
-
# ββ KPI strip β current totals + most-recent-month MoM delta βββββ
|
| 1110 |
-
# MoM helper: pick the latest complete (Year, Month) pair in the
|
| 1111 |
-
# monthly DataFrame and the one before it. Aggregate across the
|
| 1112 |
-
# branches in the current filter so each metric is single-valued.
|
| 1113 |
-
def _mom_delta(df: pd.DataFrame, value_col: str, agg: str = "sum"):
|
| 1114 |
-
if df.empty or value_col not in df.columns:
|
| 1115 |
-
return None, None, None
|
| 1116 |
-
keys = df[["Year", "Month"]].dropna().drop_duplicates()
|
| 1117 |
-
keys = keys.sort_values(["Year", "Month"])
|
| 1118 |
-
if len(keys) < 1:
|
| 1119 |
-
return None, None, None
|
| 1120 |
-
latest = keys.iloc[-1]
|
| 1121 |
-
prev = keys.iloc[-2] if len(keys) >= 2 else None
|
| 1122 |
-
def _val(row):
|
| 1123 |
-
mask = (df["Year"] == row["Year"]) & (df["Month"] == row["Month"])
|
| 1124 |
-
if agg == "sum":
|
| 1125 |
-
return df.loc[mask, value_col].sum()
|
| 1126 |
-
return df.loc[mask, value_col].mean()
|
| 1127 |
-
cur_v = _val(latest)
|
| 1128 |
-
prev_v = _val(prev) if prev is not None else None
|
| 1129 |
-
delta = (cur_v - prev_v) if (prev_v is not None and pd.notna(prev_v)) else None
|
| 1130 |
-
return cur_v, prev_v, delta
|
| 1131 |
-
|
| 1132 |
-
def _money_delta(d):
|
| 1133 |
-
return None if d is None or pd.isna(d) else fmt_money(d)
|
| 1134 |
-
def _num_delta(d):
|
| 1135 |
-
return None if d is None or pd.isna(d) else f"{d:+,.0f}"
|
| 1136 |
-
def _pct_delta(d):
|
| 1137 |
-
return None if d is None or pd.isna(d) else f"{d:+.1f} pp"
|
| 1138 |
-
|
| 1139 |
-
rev_cur, _, rev_delta = _mom_delta(m, "Revenue", "sum")
|
| 1140 |
-
cust_cur, _, cust_delta = _mom_delta(m, "Customers", "sum")
|
| 1141 |
-
rph_cur, _, rph_delta = _mom_delta(m, "Rev_Per_Head", "mean")
|
| 1142 |
-
|
| 1143 |
-
# Always show the period totals as the headline numbers, but layer
|
| 1144 |
-
# the most-recent-month MoM delta underneath using st.metric's
|
| 1145 |
-
# delta param β keeps "all of the above" use cases in one glance.
|
| 1146 |
-
has_pct = ("%Cap" in m.columns) or ("%Premium" in m.columns)
|
| 1147 |
-
if has_pct:
|
| 1148 |
-
kc1, kc2, kc3, kc4, kc5 = st.columns(5)
|
| 1149 |
-
else:
|
| 1150 |
-
kc1, kc2, kc3 = st.columns(3)
|
| 1151 |
-
kc1.metric("Revenue", fmt_money(tot_rev), delta=_money_delta(rev_delta))
|
| 1152 |
-
kc2.metric("Customers", fmt_num(tot_cust), delta=_num_delta(cust_delta))
|
| 1153 |
-
kc3.metric("Rev / Head", fmt_money(rev_ph), delta=_money_delta(rph_delta))
|
| 1154 |
-
if has_pct:
|
| 1155 |
-
cap_cur, _, cap_delta = _mom_delta(m, "%Cap", "mean")
|
| 1156 |
-
prem_cur, _, prem_delta = _mom_delta(m, "%Premium", "mean")
|
| 1157 |
-
kc4.metric("%Cap (latest mo.)",
|
| 1158 |
-
fmt_pct(cap_cur) if cap_cur is not None else "β",
|
| 1159 |
-
delta=_pct_delta(cap_delta))
|
| 1160 |
-
kc5.metric("%Premium (latest mo.)",
|
| 1161 |
-
fmt_pct(prem_cur) if prem_cur is not None else "β",
|
| 1162 |
-
delta=_pct_delta(prem_delta))
|
| 1163 |
-
|
| 1164 |
# ββ Trends β small charts above the tables βββββββββββββββββββββββ
|
| 1165 |
if not m.empty:
|
| 1166 |
st.markdown("**Trends**")
|
|
@@ -1430,7 +1401,7 @@ with tab_pl:
|
|
| 1430 |
fig = px.bar(
|
| 1431 |
agg.sort_values("Amount"),
|
| 1432 |
x="Amount", y=group_col, orientation="h", color="Flow",
|
| 1433 |
-
color_discrete_map={"Income": "#
|
| 1434 |
text=agg["Amount"].apply(fmt_money),
|
| 1435 |
)
|
| 1436 |
fig.update_layout(yaxis_title=None, xaxis_title="Amount (THB)", showlegend=True)
|
|
|
|
| 44 |
st.markdown(
|
| 45 |
"""
|
| 46 |
<style>
|
| 47 |
+
/* Light-theme Copper Group palette.
|
| 48 |
+
--copper = Copper Buffet wordmark (primary brand)
|
| 49 |
+
--copper-soft = lighter wash for borders / dividers
|
| 50 |
+
--cream = page surface
|
| 51 |
+
--cream-2 = card / sidebar surface
|
| 52 |
+
--ink = body text
|
| 53 |
+
--muted = secondary text */
|
| 54 |
+
:root {
|
| 55 |
+
--copper: #906848;
|
| 56 |
+
--copper-soft: #D4C4B0;
|
| 57 |
+
--cream: #FAF7F2;
|
| 58 |
+
--cream-2: #F2EBE0;
|
| 59 |
+
--ink: #2A1F15;
|
| 60 |
+
--muted: #7A6248;
|
| 61 |
+
}
|
| 62 |
div[data-testid="stMetric"] {
|
| 63 |
+
background: var(--cream-2);
|
| 64 |
+
border: 1px solid var(--copper-soft);
|
| 65 |
border-radius: 10px;
|
| 66 |
padding: 16px 20px;
|
| 67 |
}
|
| 68 |
div[data-testid="stMetric"] [data-testid="stMetricValue"] {
|
| 69 |
font-size: 28px;
|
| 70 |
font-weight: 600;
|
| 71 |
+
color: var(--ink);
|
| 72 |
}
|
| 73 |
div[data-testid="stMetric"] [data-testid="stMetricLabel"] {
|
| 74 |
+
color: var(--muted);
|
| 75 |
font-size: 12px;
|
| 76 |
letter-spacing: 0.5px;
|
| 77 |
text-transform: uppercase;
|
| 78 |
}
|
| 79 |
section[data-testid="stSidebar"] {
|
| 80 |
+
background: var(--cream-2);
|
| 81 |
}
|
| 82 |
.small-caption {
|
| 83 |
+
color: var(--muted);
|
| 84 |
font-size: 12px;
|
| 85 |
}
|
| 86 |
+
/* Make the page background pick up the cream tone consistently. */
|
| 87 |
+
.stApp { background: var(--cream); }
|
| 88 |
+
/* Tab labels lean copper to reinforce the brand on the main nav. */
|
| 89 |
+
button[data-baseweb="tab"] {
|
| 90 |
+
color: var(--muted) !important;
|
| 91 |
+
}
|
| 92 |
+
button[data-baseweb="tab"][aria-selected="true"] {
|
| 93 |
+
color: var(--copper) !important;
|
| 94 |
+
}
|
| 95 |
|
| 96 |
/* ββ Mobile-responsive overrides (phones / narrow tablets) βββββββββββ
|
| 97 |
Streamlit's default st.columns() lays out equal-width children in a
|
|
|
|
| 282 |
f"""
|
| 283 |
<div style="max-width: 420px; margin: 80px auto 0 auto; text-align: center;">
|
| 284 |
<div style="font-size: 32px; margin-bottom: 8px;">π</div>
|
| 285 |
+
<h2 style="margin: 0 0 8px 0; color: #2A1F15;">Copper Group Dashboard</h2>
|
| 286 |
+
<p style="color: #7A6248; margin: 0 0 28px 0; line-height: 1.5;">
|
| 287 |
Restricted to members of the
|
| 288 |
<code>CB-Group</code> organization on Hugging Face.
|
| 289 |
Sign in with your HF account to continue.
|
|
|
|
| 292 |
display: inline-flex;
|
| 293 |
align-items: center;
|
| 294 |
gap: 10px;
|
| 295 |
+
background: #906848;
|
| 296 |
+
color: #FAF7F2;
|
| 297 |
padding: 12px 28px;
|
| 298 |
border-radius: 10px;
|
| 299 |
font-weight: 600;
|
| 300 |
font-size: 15px;
|
| 301 |
text-decoration: none;
|
| 302 |
+
box-shadow: 0 1px 3px rgba(42, 31, 21, 0.15);
|
| 303 |
">π€ Sign in with Hugging Face</a>
|
| 304 |
+
<p style="color: #7A6248; font-size: 12px; margin-top: 32px;">
|
| 305 |
Don't have an HF account? Ask the dashboard owner to invite
|
| 306 |
you to the <code>CB-Group</code> org, then
|
| 307 |
<a href="https://huggingface.co/join" target="_blank"
|
| 308 |
+
style="color: #906848;">sign up here</a>.
|
| 309 |
</p>
|
| 310 |
</div>
|
| 311 |
""",
|
|
|
|
| 745 |
years_avail = sorted(years_avail, reverse=True)
|
| 746 |
|
| 747 |
# Default the year filter to the current calendar year when present in the
|
| 748 |
+
# data; otherwise leave it empty (= "all years") so the dashboard still
|
| 749 |
+
# loads something on empty / future-dated runs.
|
| 750 |
from datetime import datetime as _dt
|
| 751 |
_current_year = _dt.now().year
|
| 752 |
+
_year_default = [_current_year] if _current_year in years_avail else []
|
| 753 |
+
sel_years = st.sidebar.multiselect(
|
| 754 |
+
"Year", years_avail, default=_year_default,
|
| 755 |
+
help="Leave empty to include every year in the dataset.",
|
| 756 |
)
|
|
|
|
| 757 |
|
| 758 |
if min_date and max_date:
|
| 759 |
# Default range: Jan 1 of the current year β yesterday. Clamp both ends
|
|
|
|
| 814 |
out = out[out["Date"] >= pd.to_datetime(date_from)]
|
| 815 |
if date_to is not None:
|
| 816 |
out = out[out["Date"] <= pd.to_datetime(date_to)]
|
| 817 |
+
if sel_years:
|
| 818 |
if "Year" in out.columns:
|
| 819 |
+
out = out[out["Year"].isin(sel_years)]
|
| 820 |
elif "Date" in out.columns:
|
| 821 |
+
out = out[out["Date"].dt.year.isin(sel_years)]
|
| 822 |
return out
|
| 823 |
|
| 824 |
|
|
|
|
| 853 |
|
| 854 |
|
| 855 |
def style_plotly(fig, *, height: int | None = None):
|
| 856 |
+
"""Common Plotly cosmetics β transparent background + light-theme gridlines."""
|
| 857 |
fig.update_layout(
|
| 858 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 859 |
plot_bgcolor="rgba(0,0,0,0)",
|
| 860 |
margin=dict(l=10, r=10, t=30, b=10),
|
| 861 |
legend=dict(orientation="h", yanchor="bottom", y=-0.25, x=0),
|
| 862 |
+
font=dict(color="#2A1F15"),
|
| 863 |
)
|
| 864 |
if height is not None:
|
| 865 |
fig.update_layout(height=height)
|
| 866 |
+
fig.update_xaxes(gridcolor="#E5DDD0", zerolinecolor="#D4C4B0", linecolor="#D4C4B0")
|
| 867 |
+
fig.update_yaxes(gridcolor="#E5DDD0", zerolinecolor="#D4C4B0", linecolor="#D4C4B0")
|
| 868 |
return fig
|
| 869 |
|
| 870 |
|
| 871 |
+
# Restaurant chart colors taken directly from the brand assets:
|
| 872 |
+
# Copper Buffet = #906848 (wordmark color in CopperBuffet_Logo.jpg)
|
| 873 |
+
# Tiew Copper = #D87838 (background color in TiewCopper_Logo.png)
|
| 874 |
+
# Group = neutral copper-grey for Holding / CK / Conso rows.
|
| 875 |
RESTAURANT_COLOR = {
|
| 876 |
+
"Copper Buffet": "#906848",
|
| 877 |
+
"Tiew Copper": "#D87838",
|
| 878 |
+
"Group": "#8B7355",
|
| 879 |
}
|
| 880 |
|
| 881 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1011 |
out = out[out["Date"] >= pd.to_datetime(date_from)]
|
| 1012 |
if date_to is not None:
|
| 1013 |
out = out[out["Date"] <= pd.to_datetime(date_to)]
|
| 1014 |
+
if sel_years:
|
| 1015 |
if "Year" in out.columns:
|
| 1016 |
+
out = out[out["Year"].isin(sel_years)]
|
| 1017 |
elif "Date" in out.columns:
|
| 1018 |
+
out = out[out["Date"].dt.year.isin(sel_years)]
|
| 1019 |
return out
|
| 1020 |
|
| 1021 |
def _render_restaurant_summary(restaurant_name: str) -> None:
|
|
|
|
| 1032 |
st.info(f"No data for {restaurant_name} in the current filters.")
|
| 1033 |
return
|
| 1034 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
# Pre-compute extended monthly DataFrame *once*, reused for both the
|
| 1036 |
# trend charts (below) and the Monthly summary table further down.
|
| 1037 |
# For Copper Buffet, append per-round customer columns + %Cap +
|
|
|
|
| 1132 |
)
|
| 1133 |
m = m.drop(columns=["_Cap", "_TotCust2"])
|
| 1134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
# ββ Trends β small charts above the tables βββββββββββββββββββββββ
|
| 1136 |
if not m.empty:
|
| 1137 |
st.markdown("**Trends**")
|
|
|
|
| 1401 |
fig = px.bar(
|
| 1402 |
agg.sort_values("Amount"),
|
| 1403 |
x="Amount", y=group_col, orientation="h", color="Flow",
|
| 1404 |
+
color_discrete_map={"Income": "#5C8A52", "Expense": "#C44A3B"},
|
| 1405 |
text=agg["Amount"].apply(fmt_money),
|
| 1406 |
)
|
| 1407 |
fig.update_layout(yaxis_title=None, xaxis_title="Amount (THB)", showlegend=True)
|