| | import streamlit as st |
| | import pandas as pd |
| | import plotly.express as px |
| | from io import StringIO |
| | from datetime import date |
| |
|
| | |
| | |
| | |
| | def format_rs(amount): |
| | """Format numeric amount into Rupee string with thousands separators.""" |
| | try: |
| | |
| | return f"Rs {int(round(float(amount))):,}" |
| | except Exception: |
| | return str(amount) |
| |
|
| | def ensure_numeric_amount(col): |
| | """Convert amount column to numeric (int) safely.""" |
| | return pd.to_numeric(col, errors="coerce").fillna(0).astype(int) |
| |
|
| | |
| | |
| | |
| | st.set_page_config(page_title="πΈ Expensive Tracker", page_icon="π³", layout="centered") |
| |
|
| | st.markdown( |
| | """ |
| | <style> |
| | /* Page background gradient */ |
| | .reportview-container { |
| | background: linear-gradient(135deg,#0f172a,#3b4252); |
| | color: #e6eef8; |
| | } |
| | .stButton>button { |
| | background: linear-gradient(90deg,#ff7a18,#af002d); |
| | color: white; |
| | border-radius: 10px; |
| | padding: 0.55em 1em; |
| | font-weight: 600; |
| | } |
| | .stButton>button:hover { |
| | transform: scale(1.02); |
| | filter: brightness(1.05); |
| | } |
| | .card { |
| | background: rgba(255,255,255,0.06); |
| | padding: 12px; |
| | border-radius: 12px; |
| | border: 1px solid rgba(255,255,255,0.06); |
| | box-shadow: 0 6px 18px rgba(0,0,0,0.3); |
| | } |
| | .dataframe td, .dataframe th { |
| | color: #e6eef8 !important; |
| | } |
| | </style> |
| | """, |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | st.title("πΈ Expensive Tracker") |
| | st.caption("Track expenses in Rupees (Rs). Enter whole numbers like 100, 500, 10000.") |
| |
|
| | |
| | |
| | |
| | if "expenses" not in st.session_state: |
| | |
| | st.session_state.expenses = pd.DataFrame( |
| | columns=["Date", "Category", "Amount", "Notes"] |
| | ) |
| |
|
| | |
| | if not st.session_state.expenses.empty: |
| | st.session_state.expenses["Amount"] = ensure_numeric_amount(st.session_state.expenses["Amount"]) |
| |
|
| | |
| | |
| | |
| | st.sidebar.header("βοΈ Menu") |
| | page = st.sidebar.radio("Choose view", ["Add Expense", "View Expenses", "Summary", "Import / Export"]) |
| |
|
| | |
| | CATEGORIES = ["Food", "Travel", "Shopping", "Bills", "Entertainment", "Health", "Other"] |
| |
|
| | |
| | |
| | |
| | if page == "Add Expense": |
| | st.header("Add a new expense (Amount in Rs)") |
| | with st.form("add_expense_form", clear_on_submit=True): |
| | c1, c2, c3 = st.columns([1, 1, 1]) |
| | with c1: |
| | exp_date = st.date_input("Date", value=date.today()) |
| | with c2: |
| | category = st.selectbox("Category", options=CATEGORIES) |
| | with c3: |
| | |
| | amount = st.number_input("Amount (Rs)", min_value=0, step=1, format="%d", value=0) |
| | notes = st.text_area("Notes (optional)", max_chars=200, placeholder="Where/what for?") |
| | submitted = st.form_submit_button("β Add Expense") |
| |
|
| | if submitted: |
| | new_row = { |
| | "Date": pd.to_datetime(exp_date).date(), |
| | "Category": category, |
| | "Amount": int(amount), |
| | "Notes": notes |
| | } |
| | st.session_state.expenses = pd.concat([st.session_state.expenses, pd.DataFrame([new_row])], ignore_index=True) |
| | st.success(f"Expense added β
{format_rs(amount)}") |
| | st.balloons() |
| |
|
| | if not st.session_state.expenses.empty: |
| | st.markdown("**Quick preview of latest expenses**") |
| | preview = st.session_state.expenses.tail(6).reset_index(drop=True).copy() |
| | |
| | preview["Amount (Rs)"] = preview["Amount"].apply(format_rs) |
| | st.dataframe(preview[["Date", "Category", "Amount (Rs)", "Notes"]]) |
| |
|
| | |
| | |
| | |
| | elif page == "View Expenses": |
| | st.header("All Expenses (Amounts in Rs)") |
| | if st.session_state.expenses.empty: |
| | st.info("No expenses yet β add some from the 'Add Expense' tab.") |
| | else: |
| | df = st.session_state.expenses.copy() |
| | df["Amount"] = ensure_numeric_amount(df["Amount"]) |
| | |
| | st.markdown("Filter") |
| | cols = st.columns([1, 1, 1]) |
| | with cols[0]: |
| | min_date = st.date_input("From", value=pd.to_datetime(df["Date"]).min().date()) |
| | with cols[1]: |
| | max_date = st.date_input("To", value=pd.to_datetime(df["Date"]).max().date()) |
| | with cols[2]: |
| | sel_cat = st.multiselect("Category", options=["All"] + CATEGORIES, default=["All"]) |
| | filtered = df[ |
| | (pd.to_datetime(df["Date"]) >= pd.to_datetime(min_date)) & |
| | (pd.to_datetime(df["Date"]) <= pd.to_datetime(max_date)) |
| | ] |
| | if sel_cat and "All" not in sel_cat: |
| | filtered = filtered[filtered["Category"].isin(sel_cat)] |
| |
|
| | display_df = filtered.sort_values(by="Date", ascending=False).reset_index(drop=True).copy() |
| | display_df["Amount (Rs)"] = display_df["Amount"].apply(format_rs) |
| | st.dataframe(display_df[["Date", "Category", "Amount (Rs)", "Notes"]]) |
| |
|
| | |
| | st.markdown("---") |
| | cdel, cclear = st.columns(2) |
| | with cdel: |
| | if st.button("ποΈ Delete last entry"): |
| | st.session_state.expenses = st.session_state.expenses[:-1].reset_index(drop=True) |
| | st.success("Last entry removed.") |
| | with cclear: |
| | if st.button("β οΈ Clear all expenses"): |
| | st.session_state.expenses = pd.DataFrame(columns=["Date", "Category", "Amount", "Notes"]) |
| | st.success("All expenses cleared.") |
| |
|
| | |
| | |
| | |
| | elif page == "Summary": |
| | st.header("Summary Dashboard (Rs)") |
| | if st.session_state.expenses.empty: |
| | st.info("No data yet β add expenses to see the summary.") |
| | else: |
| | df = st.session_state.expenses.copy() |
| | df["Date"] = pd.to_datetime(df["Date"]) |
| | df["Amount"] = ensure_numeric_amount(df["Amount"]) |
| |
|
| | total = df["Amount"].sum() |
| | avg = df["Amount"].mean() |
| | max_exp = df["Amount"].max() |
| | st.markdown("<div class='card'>", unsafe_allow_html=True) |
| | c1, c2, c3 = st.columns(3) |
| | c1.metric("Total Spent", format_rs(total)) |
| | c2.metric("Average Expense", format_rs(avg)) |
| | c3.metric("Largest Expense", format_rs(max_exp)) |
| | st.markdown("</div>", unsafe_allow_html=True) |
| |
|
| | st.markdown("### π Expenses by Category") |
| | cat_summary = df.groupby("Category", as_index=False)["Amount"].sum().sort_values("Amount", ascending=False) |
| | |
| | fig_pie = px.pie(cat_summary, names="Category", values="Amount", title="Spending by Category", hole=0.4) |
| | fig_pie.update_traces(textinfo="percent+label", hovertemplate="%{label}: Rs %{value:,}<extra></extra>") |
| | st.plotly_chart(fig_pie, use_container_width=True) |
| |
|
| | st.markdown("### π Expenses Over Time") |
| | timeseries = df.groupby(pd.Grouper(key="Date", freq="D"))["Amount"].sum().reset_index() |
| | timeseries = timeseries.set_index("Date").resample("D").sum().fillna(0).reset_index() |
| | fig_line = px.bar(timeseries, x="Date", y="Amount", title="Daily Spending (bar)") |
| | fig_line.update_traces(hovertemplate="Date: %{x}<br>Amount: Rs %{y:,}<extra></extra>") |
| | st.plotly_chart(fig_line, use_container_width=True) |
| |
|
| | st.markdown("### π Top 5 Expenses") |
| | top5 = df.nlargest(5, "Amount")[["Date", "Category", "Amount", "Notes"]].reset_index(drop=True).copy() |
| | top5["Amount (Rs)"] = top5["Amount"].apply(format_rs) |
| | st.dataframe(top5[["Date", "Category", "Amount (Rs)", "Notes"]]) |
| |
|
| | |
| | |
| | |
| | elif page == "Import / Export": |
| | st.header("Import or Export your data (CSV)") |
| | st.markdown("You can download your current expenses as a CSV or upload a CSV to load expenses. Amounts are stored as integers (Rs).") |
| |
|
| | |
| | if st.session_state.expenses.empty: |
| | st.info("No expenses to export.") |
| | else: |
| | |
| | export_df = st.session_state.expenses.copy() |
| | export_df["Amount"] = ensure_numeric_amount(export_df["Amount"]) |
| | csv = export_df.to_csv(index=False) |
| | st.download_button("β¬οΈ Download CSV", data=csv, file_name="expenses.csv", mime="text/csv") |
| |
|
| | st.markdown("---") |
| | st.markdown("Upload a CSV file (columns: Date, Category, Amount, Notes). Amounts should be numeric (Rs).") |
| | uploaded = st.file_uploader("Upload CSV", type=["csv"]) |
| | if uploaded is not None: |
| | try: |
| | uploaded_df = pd.read_csv(uploaded, parse_dates=["Date"]) |
| | |
| | required = {"Date", "Category", "Amount"} |
| | if not required.issubset(set(uploaded_df.columns)): |
| | st.error("CSV must include at least Date, Category, and Amount columns.") |
| | else: |
| | |
| | if "Notes" not in uploaded_df.columns: |
| | uploaded_df["Notes"] = "" |
| | uploaded_df = uploaded_df[["Date", "Category", "Amount", "Notes"]] |
| | uploaded_df["Amount"] = ensure_numeric_amount(uploaded_df["Amount"]) |
| | uploaded_df["Date"] = pd.to_datetime(uploaded_df["Date"]).dt.date |
| | st.session_state.expenses = pd.concat([st.session_state.expenses, uploaded_df], ignore_index=True) |
| | st.success("Uploaded expenses added to your tracker.") |
| | except Exception as e: |
| | st.error(f"Failed to parse CSV: {e}") |
| |
|
| | |
| | |
| | |
| | st.markdown("---") |
| | st.markdown( |
| | "<div style='text-align:center;opacity:0.8'>Made with β€οΈ β Deploy to Hugging Face Spaces (SDK: Streamlit). " |
| | "Tip: Use the Import/Export tab to keep your data between sessions.</div>", |
| | unsafe_allow_html=True, |
| | ) |
| |
|