expensive / app.py
manaskhan's picture
Update app.py
798afe4 verified
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(
"""
<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.")
# -------------------------------
# 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("<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)
# 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:,}<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"]])
# -------------------------------
# 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(
"<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,
)