Spaces:
Running
Running
Add search box to Item Tab
Browse files- streamlit_app.py +74 -27
streamlit_app.py
CHANGED
|
@@ -141,7 +141,11 @@ LANG = {
|
|
| 141 |
"it_chart_qty": "Top {n} items by quantity ordered",
|
| 142 |
"it_by_cat": "Items by sub-type",
|
| 143 |
"it_by_protein": "Items by Protein",
|
|
|
|
| 144 |
"it_detail": "Item detail",
|
|
|
|
|
|
|
|
|
|
| 145 |
},
|
| 146 |
"th": {
|
| 147 |
# Tab labels
|
|
@@ -245,7 +249,11 @@ LANG = {
|
|
| 245 |
"it_chart_qty": "{n} รายการที่ขายดีที่สุด (จำนวน)",
|
| 246 |
"it_by_cat": "รายการแบ่งตามประเภทย่อย",
|
| 247 |
"it_by_protein": "รายการแบ่งตามโปรตีน",
|
|
|
|
| 248 |
"it_detail": "รายละเอียดรายการ",
|
|
|
|
|
|
|
|
|
|
| 249 |
},
|
| 250 |
}
|
| 251 |
|
|
@@ -2014,8 +2022,26 @@ with tab_items:
|
|
| 2014 |
with fc3:
|
| 2015 |
top_n = st.slider(t("it_top_n"), 5, 50, 20, key="it_top_n")
|
| 2016 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2017 |
if it_view.empty:
|
| 2018 |
-
|
|
|
|
| 2019 |
else:
|
| 2020 |
# ── KPI tiles ────────────────────────────────────────────
|
| 2021 |
total_qty = int(it_view["Qty"].sum()) if "Qty" in it_view.columns else 0
|
|
@@ -2061,6 +2087,49 @@ with tab_items:
|
|
| 2061 |
# Sub-type = the menu category (Teppan, Sushi, etc.).
|
| 2062 |
# Group1 = the protein family (Seafood, Duck, Fish, etc.)
|
| 2063 |
# — captured by fact_items' Group1 column for Food rows.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2064 |
pie_left, pie_right = st.columns(2)
|
| 2065 |
|
| 2066 |
with pie_left:
|
|
@@ -2070,20 +2139,9 @@ with tab_items:
|
|
| 2070 |
.sort_values("Qty", ascending=False)
|
| 2071 |
)
|
| 2072 |
cat_agg = cat_agg[cat_agg["Qty"] > 0]
|
|
|
|
| 2073 |
if len(cat_agg) >= 2:
|
| 2074 |
-
|
| 2075 |
-
fig = px.pie(
|
| 2076 |
-
cat_agg, names="SubType", values="Qty", hole=0.55,
|
| 2077 |
-
color_discrete_sequence=BRAND_SEQUENCE,
|
| 2078 |
-
)
|
| 2079 |
-
fig.update_traces(
|
| 2080 |
-
textposition="inside",
|
| 2081 |
-
texttemplate="%{label}<br>%{value:,.0f}<br>%{percent}",
|
| 2082 |
-
insidetextfont=dict(size=11),
|
| 2083 |
-
)
|
| 2084 |
-
st.plotly_chart(
|
| 2085 |
-
style_plotly(fig, height=380), use_container_width=True,
|
| 2086 |
-
)
|
| 2087 |
|
| 2088 |
with pie_right:
|
| 2089 |
if "Group1" in it_view.columns:
|
|
@@ -2092,20 +2150,9 @@ with tab_items:
|
|
| 2092 |
.sort_values("Qty", ascending=False)
|
| 2093 |
)
|
| 2094 |
protein_agg = protein_agg[protein_agg["Qty"] > 0]
|
|
|
|
| 2095 |
if len(protein_agg) >= 2:
|
| 2096 |
-
|
| 2097 |
-
fig = px.pie(
|
| 2098 |
-
protein_agg, names="Group1", values="Qty", hole=0.55,
|
| 2099 |
-
color_discrete_sequence=BRAND_SEQUENCE,
|
| 2100 |
-
)
|
| 2101 |
-
fig.update_traces(
|
| 2102 |
-
textposition="inside",
|
| 2103 |
-
texttemplate="%{label}<br>%{value:,.0f}<br>%{percent}",
|
| 2104 |
-
insidetextfont=dict(size=11),
|
| 2105 |
-
)
|
| 2106 |
-
st.plotly_chart(
|
| 2107 |
-
style_plotly(fig, height=380), use_container_width=True,
|
| 2108 |
-
)
|
| 2109 |
|
| 2110 |
# ── Item detail table ───────────────────────────────────
|
| 2111 |
st.markdown(f"**{t('it_detail')}**")
|
|
|
|
| 141 |
"it_chart_qty": "Top {n} items by quantity ordered",
|
| 142 |
"it_by_cat": "Items by sub-type",
|
| 143 |
"it_by_protein": "Items by Protein",
|
| 144 |
+
"it_other": "Other",
|
| 145 |
"it_detail": "Item detail",
|
| 146 |
+
"it_search": "Search item",
|
| 147 |
+
"it_search_help": "Type any part of the item name (English or Thai). Case-insensitive.",
|
| 148 |
+
"it_search_no_match": "No items match \"{q}\". Clear the search box to see everything.",
|
| 149 |
},
|
| 150 |
"th": {
|
| 151 |
# Tab labels
|
|
|
|
| 249 |
"it_chart_qty": "{n} รายการที่ขายดีที่สุด (จำนวน)",
|
| 250 |
"it_by_cat": "รายการแบ่งตามประเภทย่อย",
|
| 251 |
"it_by_protein": "รายการแบ่งตามโปรตีน",
|
| 252 |
+
"it_other": "อื่นๆ",
|
| 253 |
"it_detail": "รายละเอียดรายการ",
|
| 254 |
+
"it_search": "ค้นหารายการ",
|
| 255 |
+
"it_search_help": "พิมพ์ส่วนใดส่วนหนึ่งของชื่อรายการ (ภาษาไทยหรืออังกฤษ) ไม่สนใจตัวพิมพ์เล็ก-ใหญ่",
|
| 256 |
+
"it_search_no_match": "ไม่พบรายการที่ตรงกับ \"{q}\" ล้างช่องค้นหาเพื่อแสดงทั้งหมด",
|
| 257 |
},
|
| 258 |
}
|
| 259 |
|
|
|
|
| 2022 |
with fc3:
|
| 2023 |
top_n = st.slider(t("it_top_n"), 5, 50, 20, key="it_top_n")
|
| 2024 |
|
| 2025 |
+
# ── Item search — case-insensitive substring match ────────────
|
| 2026 |
+
# Applied AFTER the Type/SubType filters so it narrows whatever
|
| 2027 |
+
# subset those produced. Empty search = pass-through.
|
| 2028 |
+
search_q = st.text_input(
|
| 2029 |
+
t("it_search"),
|
| 2030 |
+
key="it_search",
|
| 2031 |
+
placeholder=t("it_search_help"),
|
| 2032 |
+
).strip()
|
| 2033 |
+
if search_q and "Item" in it_view.columns:
|
| 2034 |
+
_matched = (
|
| 2035 |
+
it_view["Item"].astype(str)
|
| 2036 |
+
.str.contains(search_q, case=False, na=False, regex=False)
|
| 2037 |
+
)
|
| 2038 |
+
it_view = it_view[_matched]
|
| 2039 |
+
if it_view.empty:
|
| 2040 |
+
st.info(t("it_search_no_match", q=search_q))
|
| 2041 |
+
|
| 2042 |
if it_view.empty:
|
| 2043 |
+
if not search_q:
|
| 2044 |
+
st.info(t("it_no_data"))
|
| 2045 |
else:
|
| 2046 |
# ── KPI tiles ────────────────────────────────────────────
|
| 2047 |
total_qty = int(it_view["Qty"].sum()) if "Qty" in it_view.columns else 0
|
|
|
|
| 2087 |
# Sub-type = the menu category (Teppan, Sushi, etc.).
|
| 2088 |
# Group1 = the protein family (Seafood, Duck, Fish, etc.)
|
| 2089 |
# — captured by fact_items' Group1 column for Food rows.
|
| 2090 |
+
# Aggregate everything past the top N into a single
|
| 2091 |
+
# "Other" slice so the legend stays compact instead of
|
| 2092 |
+
# filling half the chart with single-item categories.
|
| 2093 |
+
def _top_n_with_other(df: pd.DataFrame, name_col: str,
|
| 2094 |
+
value_col: str, n: int = 8) -> pd.DataFrame:
|
| 2095 |
+
if df.empty or len(df) <= n:
|
| 2096 |
+
return df
|
| 2097 |
+
top = df.nlargest(n, value_col)
|
| 2098 |
+
rest = df[~df[name_col].isin(top[name_col])][value_col].sum()
|
| 2099 |
+
if rest > 0:
|
| 2100 |
+
other = pd.DataFrame({name_col: [t("it_other")],
|
| 2101 |
+
value_col: [rest]})
|
| 2102 |
+
return pd.concat([top, other], ignore_index=True)
|
| 2103 |
+
return top
|
| 2104 |
+
|
| 2105 |
+
def _render_donut(df: pd.DataFrame, name_col: str, title: str) -> None:
|
| 2106 |
+
"""Pie chart with bottom-horizontal legend so the
|
| 2107 |
+
chart stays roughly square instead of being squeezed
|
| 2108 |
+
by a tall right-side legend."""
|
| 2109 |
+
fig = px.pie(
|
| 2110 |
+
df, names=name_col, values="Qty", hole=0.55,
|
| 2111 |
+
color_discrete_sequence=BRAND_SEQUENCE,
|
| 2112 |
+
)
|
| 2113 |
+
fig.update_traces(
|
| 2114 |
+
textposition="inside",
|
| 2115 |
+
texttemplate="%{label}<br>%{value:,.0f}<br>%{percent}",
|
| 2116 |
+
insidetextfont=dict(size=11),
|
| 2117 |
+
)
|
| 2118 |
+
fig.update_layout(
|
| 2119 |
+
title=None,
|
| 2120 |
+
margin=dict(l=10, r=10, t=20, b=80),
|
| 2121 |
+
legend=dict(
|
| 2122 |
+
orientation="h",
|
| 2123 |
+
yanchor="top", y=-0.05,
|
| 2124 |
+
xanchor="center", x=0.5,
|
| 2125 |
+
font=dict(size=11),
|
| 2126 |
+
),
|
| 2127 |
+
)
|
| 2128 |
+
st.markdown(f"**{title}**")
|
| 2129 |
+
st.plotly_chart(
|
| 2130 |
+
style_plotly(fig, height=420), use_container_width=True,
|
| 2131 |
+
)
|
| 2132 |
+
|
| 2133 |
pie_left, pie_right = st.columns(2)
|
| 2134 |
|
| 2135 |
with pie_left:
|
|
|
|
| 2139 |
.sort_values("Qty", ascending=False)
|
| 2140 |
)
|
| 2141 |
cat_agg = cat_agg[cat_agg["Qty"] > 0]
|
| 2142 |
+
cat_agg = _top_n_with_other(cat_agg, "SubType", "Qty", n=8)
|
| 2143 |
if len(cat_agg) >= 2:
|
| 2144 |
+
_render_donut(cat_agg, "SubType", t("it_by_cat"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2145 |
|
| 2146 |
with pie_right:
|
| 2147 |
if "Group1" in it_view.columns:
|
|
|
|
| 2150 |
.sort_values("Qty", ascending=False)
|
| 2151 |
)
|
| 2152 |
protein_agg = protein_agg[protein_agg["Qty"] > 0]
|
| 2153 |
+
protein_agg = _top_n_with_other(protein_agg, "Group1", "Qty", n=8)
|
| 2154 |
if len(protein_agg) >= 2:
|
| 2155 |
+
_render_donut(protein_agg, "Group1", t("it_by_protein"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2156 |
|
| 2157 |
# ── Item detail table ───────────────────────────────────
|
| 2158 |
st.markdown(f"**{t('it_detail')}**")
|