taotanapol commited on
Commit
c488ca1
Β·
verified Β·
1 Parent(s): 651e156

Change theme to match the brand

Browse files
Files changed (2) hide show
  1. .streamlit/config.toml +10 -5
  2. streamlit_app.py +61 -90
.streamlit/config.toml CHANGED
@@ -1,9 +1,14 @@
1
  [theme]
2
- base = "dark"
3
- primaryColor = "#4dabf7"
4
- backgroundColor = "#0f1419"
5
- secondaryBackgroundColor = "#1a2028"
6
- textColor = "#e8eaed"
 
 
 
 
 
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: #1a2028;
49
- border: 1px solid #2d3744;
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: #9aa4b2;
59
  font-size: 12px;
60
  letter-spacing: 0.5px;
61
  text-transform: uppercase;
62
  }
63
  section[data-testid="stSidebar"] {
64
- background: #1a2028;
65
  }
66
  .small-caption {
67
- color: #9aa4b2;
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: #9aa4b2; margin: 0 0 28px 0; line-height: 1.5;">
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: #ffd21e;
271
- color: #000;
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: #6b7280; font-size: 12px; margin-top: 32px;">
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: #6b7280;">sign up here</a>.
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 fall back to "All years" so the dashboard still loads
723
- # something on empty / future-dated runs.
724
  from datetime import datetime as _dt
725
  _current_year = _dt.now().year
726
- _year_options = ["All years"] + years_avail
727
- _year_default_index = (
728
- _year_options.index(_current_year) if _current_year in years_avail else 0
 
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 sel_year != "All years":
792
  if "Year" in out.columns:
793
- out = out[out["Year"] == sel_year]
794
  elif "Date" in out.columns:
795
- out = out[out["Date"].dt.year == sel_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 colours."""
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="#2d3744", zerolinecolor="#2d3744")
840
- fig.update_yaxes(gridcolor="#2d3744", zerolinecolor="#2d3744")
841
  return fig
842
 
843
 
 
 
 
 
844
  RESTAURANT_COLOR = {
845
- "Copper Buffet": "#d97706",
846
- "Tiew Copper": "#3b82f6",
847
- "Group": "#6b7280",
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 sel_year != "All years":
984
  if "Year" in out.columns:
985
- out = out[out["Year"] == sel_year]
986
  elif "Date" in out.columns:
987
- out = out[out["Date"].dt.year == sel_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": "#51cf66", "Expense": "#ff6b6b"},
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)