import streamlit as st import pandas as pd import plotly.express as px from io import StringIO from datetime import date # ------------------------------- # Helpers # ------------------------------- def format_rs(amount): """Format numeric amount into Rupee string with thousands separators.""" try: # show no decimals for rupee amounts 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) # ------------------------------- # Page config & CSS # ------------------------------- st.set_page_config(page_title="💸 Expensive Tracker", page_icon="💳", layout="centered") st.markdown( """ """, unsafe_allow_html=True, ) st.title("💸 Expensive Tracker") st.caption("Track expenses in Rupees (Rs). Enter whole numbers like 100, 500, 10000.") # ------------------------------- # Initialize in-memory storage # ------------------------------- if "expenses" not in st.session_state: # columns: Date, Category, Amount, Notes st.session_state.expenses = pd.DataFrame( columns=["Date", "Category", "Amount", "Notes"] ) # Ensure Amount column numeric if loaded previously if not st.session_state.expenses.empty: st.session_state.expenses["Amount"] = ensure_numeric_amount(st.session_state.expenses["Amount"]) # ------------------------------- # Sidebar navigation # ------------------------------- st.sidebar.header("⚙️ Menu") page = st.sidebar.radio("Choose view", ["Add Expense", "View Expenses", "Summary", "Import / Export"]) # Common categories CATEGORIES = ["Food", "Travel", "Shopping", "Bills", "Entertainment", "Health", "Other"] # ------------------------------- # Add Expense # ------------------------------- 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: # Integer rupee input 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() # Format Amount column for display preview["Amount (Rs)"] = preview["Amount"].apply(format_rs) st.dataframe(preview[["Date", "Category", "Amount (Rs)", "Notes"]]) # ------------------------------- # View Expenses # ------------------------------- 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"]) # Allow filtering 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"]]) # Option to delete last entry or clear all 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.") # ------------------------------- # Summary Dashboard # ------------------------------- 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("
", 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("
", unsafe_allow_html=True) st.markdown("### 📊 Expenses by Category") cat_summary = df.groupby("Category", as_index=False)["Amount"].sum().sort_values("Amount", ascending=False) # For plotly, keep numeric values; labels can show Rs via hover 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:,}") 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}
Amount: Rs %{y:,}") 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"]]) # ------------------------------- # Import / Export # ------------------------------- 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).") # Download if st.session_state.expenses.empty: st.info("No expenses to export.") else: # Ensure numeric amounts before export 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"]) # Basic validation 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: # Normalize columns and types 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}") # ------------------------------- # Footer / Tips # ------------------------------- st.markdown("---") st.markdown( "
Made with ❤️ — Deploy to Hugging Face Spaces (SDK: Streamlit). " "Tip: Use the Import/Export tab to keep your data between sessions.
", unsafe_allow_html=True, )