diff --git a/__pycache__/dashboard_user.cpython-310.pyc b/__pycache__/dashboard_user.cpython-310.pyc index b81b539..f145690 100644 Binary files a/__pycache__/dashboard_user.cpython-310.pyc and b/__pycache__/dashboard_user.cpython-310.pyc differ diff --git a/__pycache__/test.cpython-310.pyc b/__pycache__/test.cpython-310.pyc new file mode 100644 index 0000000..f650c82 Binary files /dev/null and b/__pycache__/test.cpython-310.pyc differ diff --git a/config.json b/config.json index f31339c..9a86ae6 100644 --- a/config.json +++ b/config.json @@ -1 +1,6 @@ -{"username": "admin", "password": "$2b$12$u7yQNfe6p93rcm/0iPMUtejIbJPHJOgpuxX/1HWt5KkJCc8W5HYUa", "security_question": "Nama hewan peliharaan pertama Anda?", "security_answer": "nalakucinglucu"} \ No newline at end of file +{ + "username": "admin", + "password": "$2b$12$kUz0oZpa/Utyu7TV/wGazOGySvTkE457J1ZqfbwBgV2npXI5f.4n2", + "security_question": "Nama hewan peliharaan pertama Anda?", + "security_answer": "$2b$12$YZnmM72z4wGc0q6cS9WyG.UV70xYnX9lIbpSSGWZWRAF51SSZ3BSO" +} \ No newline at end of file diff --git a/data/files.db b/data/files.db index 2482842..e5d557c 100644 Binary files a/data/files.db and b/data/files.db differ diff --git a/main.py b/main.py index 0d18591..812b38d 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,139 @@ import streamlit as st import admin_dashboard -import dashboard_user + from utils.auth import login -st.set_page_config(page_title="Visualisasi Clustering", layout="wide") +# =============================== +# Konfigurasi Halaman +# =============================== +st.set_page_config( + page_title="Visualisasi Clustering", + page_icon="πŸ“Š", + layout="wide", + initial_sidebar_state="expanded" +) +import dashboard_user -page = st.sidebar.selectbox("Pilih Halaman", [ - "User Dashboard", - "Admin Dashboard" -]) +# =============================== +# Styling CSS β€” Sama dengan Visualisasi.py +# =============================== +st.markdown(""" + +""", unsafe_allow_html=True) + + +# =============================== +# Sidebar dengan Desain Modern +# =============================== +with st.sidebar: + st.markdown(''' + + ''', unsafe_allow_html=True) + + page = st.selectbox( + "Pilih Halaman", + [ + "User Dashboard", + "Admin Dashboard" + ], + help="Pilih halaman yang ingin Anda akses" + ) + + if page == "User Dashboard": + st.markdown(""" +
+ πŸ“Š Mode: User Dashboard +
+ """, unsafe_allow_html=True) + else: + st.markdown(""" +
+ βš™οΈ Mode: Admin Dashboard +
+ """, unsafe_allow_html=True) + + + + + +# =============================== +# Routing halaman +# =============================== if page == "User Dashboard": dashboard_user.render() + elif page == "Admin Dashboard": if st.session_state.get("authenticated"): admin_dashboard.render() diff --git a/models/kmeans_model.pkl b/models/kmeans_model.pkl index 9f23191..1afe5f4 100644 Binary files a/models/kmeans_model.pkl and b/models/kmeans_model.pkl differ diff --git a/utils/__pycache__/auth.cpython-310.pyc b/utils/__pycache__/auth.cpython-310.pyc index 11ec8b3..62461f4 100644 Binary files a/utils/__pycache__/auth.cpython-310.pyc and b/utils/__pycache__/auth.cpython-310.pyc differ diff --git a/utils/__pycache__/visualizations.cpython-310.pyc b/utils/__pycache__/visualizations.cpython-310.pyc index 3482ae0..22f89f7 100644 Binary files a/utils/__pycache__/visualizations.cpython-310.pyc and b/utils/__pycache__/visualizations.cpython-310.pyc differ diff --git a/utils/auth.py b/utils/auth.py index a6b1313..af6b962 100644 --- a/utils/auth.py +++ b/utils/auth.py @@ -33,9 +33,26 @@ def is_strong_password(password): return True # Login form +import streamlit as st +import bcrypt +import json + +def load_config(): + with open("config.json", "r") as f: + return json.load(f) + +def save_config(config): + with open("config.json", "w") as f: + json.dump(config, f, indent=4) + + +# ==================== +# Fungsi Login Utama +# ==================== def login(): config = load_config() + # Inisialisasi session state if 'authenticated' not in st.session_state: st.session_state.authenticated = False if 'show_reset' not in st.session_state: @@ -50,37 +67,59 @@ def login(): password = st.text_input("Password", type="password") if st.button("Login"): - if username == config.get("username") and bcrypt.checkpw(password.encode(), config.get("password").encode()): + if ( + username == config.get("username") + and bcrypt.checkpw(password.encode(), config.get("password").encode()) + ): st.session_state.authenticated = True st.session_state.login_failed = False + st.session_state.show_reset = False + st.success("βœ… Login berhasil!") st.rerun() else: st.session_state.login_failed = True st.error("❌ Username atau password salah.") - # Hanya muncul jika login gagal + # ======================== + # πŸ” Bagian Reset Password + # ======================== if st.session_state.login_failed and not st.session_state.show_reset: if st.button("Lupa Password"): st.session_state.show_reset = True if st.session_state.show_reset: - st.info("Jawab pertanyaan rahasia untuk reset password.") - question = config.get("security_question") - answer = st.text_input("Pertanyaan:", placeholder=question) + st.info("πŸ” Jawab pertanyaan rahasia untuk reset password.") + + question = config.get("security_question", "Pertanyaan tidak tersedia") + answer = st.text_input(f"{question}") new_pass = st.text_input("Password Baru", type="password") + if st.button("Ganti Password"): - if answer.lower() == config.get("security_answer").lower(): - new_hash = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode() - config["password"] = new_hash - with open("config.json", "w") as f: - json.dump(config, f) - st.success("βœ… Password berhasil diganti!") - st.session_state.show_reset = False - st.session_state.login_failed = False + stored_hashed_answer = config.get("security_answer") + + # Validasi apakah security_answer ada + if not stored_hashed_answer: + st.error("⚠️ Pertanyaan keamanan belum diset pada konfigurasi.") else: - st.error("❌ Jawaban salah.") + if bcrypt.checkpw(answer.encode(), stored_hashed_answer.encode()): + if len(new_pass) < 8: + st.warning("⚠️ Password harus minimal 8 karakter.") + return + + # Hash password baru + new_hash = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode() + + config["password"] = new_hash + save_config(config) + + st.success("βœ… Password berhasil diganti! Silakan login kembali.") + st.session_state.show_reset = False + st.session_state.login_failed = False + else: + st.error("❌ Jawaban pertanyaan keamanan salah.") return False + else: return True @@ -95,18 +134,25 @@ def show_password_reset_form(): new_pass = st.text_input("Password Baru", type="password") if st.button("Ganti Password"): - if jawaban_input.strip().lower() == config.get("security_answer", "").strip().lower(): + stored_hashed_answer = config.get("security_answer", "") + + if bcrypt.checkpw(jawaban_input.strip().encode(), stored_hashed_answer.encode()): if not is_strong_password(new_pass): - st.warning("Password harus minimal 8 karakter, mengandung huruf besar, huruf kecil, angka, dan simbol.") + st.warning("⚠️ Password harus minimal 8 karakter, mengandung huruf besar, huruf kecil, angka, dan simbol.") return + + # Simpan password baru dengan hashing config["password"] = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode() save_config(config) - st.success("Password berhasil diubah! Silakan login kembali.") - # Reset semua flag + + st.success("βœ… Password berhasil diubah! Silakan login kembali.") + + # Reset flag session st.session_state.show_reset = False st.session_state.show_reset_button = False + else: - st.error("Jawaban pertanyaan salah.") + st.error("❌ Jawaban pertanyaan keamanan salah.") # Logout def logout(): diff --git a/utils/visualizations.py b/utils/visualizations.py index 20dfc3d..b7d5cb6 100644 --- a/utils/visualizations.py +++ b/utils/visualizations.py @@ -1,45 +1,202 @@ import streamlit as st -import plotly.express as px -import matplotlib.pyplot as plt -import seaborn as sns import pandas as pd -from sqlalchemy import text +import plotly.express as px from utils.db import engine -from sklearn.metrics import silhouette_score + +# ====================================== +# CSS Styling Glassmorphism +# ====================================== +def set_background(): + st.markdown(""" + + """, unsafe_allow_html=True) + + + + +# ====================================== +# Glassmorphism Container +# ====================================== +def glassmorphism_container(title, content): + st.markdown(f""" +
+

{title}

+ + """, unsafe_allow_html=True) + + content() + + st.markdown("
", unsafe_allow_html=True) + + +def render_card(title, value, subtitle=""): + st.markdown(f""" +
+
{title}
+
+
{subtitle}
+
{value}
+ +
+ +
+ """, unsafe_allow_html=True) + + + +# ====================================== +# Plotly Styling +# ====================================== +def set_plotly_style(fig): + fig.update_layout( + paper_bgcolor='rgba(255,255,255,0.05)', + plot_bgcolor='rgba(255,255,255,0.05)', + font=dict(color='white'), + xaxis=dict( + showgrid=True, + gridcolor='rgba(255,255,255,0.2)', + zeroline=False, + color='white' + ), + yaxis=dict( + showgrid=True, + gridcolor='rgba(255,255,255,0.2)', + zeroline=False, + color='white' + ), + legend=dict( + bgcolor='rgba(255,255,255,0.1)', + bordercolor='rgba(255,255,255,0.2)', + borderwidth=1, + font=dict(color='white'), + orientation='h', + yanchor='bottom', + y=0.01, + xanchor='left', + x=0.01 + ) + ) + return fig + + +# ====================================== +# Scorecard β€” Individual Glass Cards +# ====================================== def show_scorecard(df): - col1, col2, col3, col4 = st.columns(4) - - # Jumlah lokasi = banyak baris - col1.metric("Jumlah Lokasi", len(df)) - - # Jumlah total pasar dari seluruh baris + + jumlah_lokasi = len(df) total_pasar = int(df["JumlahPasar"].sum()) - col2.metric("Total Jumlah Pasar", total_pasar) + rata_rata_harga = round(df["RataRataHarga"].mean(), 2) - # Rata-rata harga - rata_rata_harga = round(df['RataRataHarga'].mean(), 2) - col3.metric("Rata-rata Harga Jawa Timur", f"Rp {rata_rata_harga:,.2f}") - - # Harga pasar tertinggi if not df.empty: max_row = df.loc[df['RataRataHargaTertinggiDiPasar'].idxmax()] - nama_pasar = max_row['PasarDenganRataRataHargaTertinggi'] - nama_lokasi = max_row['Lokasi'] - harga_tertinggi = max_row['RataRataHargaTertinggiDiPasar'] - - col4.metric( - label="Harga Tertinggi di Pasar", - value=f"Rp {harga_tertinggi:,.2f}", - delta=f"{nama_pasar} - {nama_lokasi}" - ) + harga_tertinggi = max_row["RataRataHargaTertinggiDiPasar"] + nama_pasar = max_row["PasarDenganRataRataHargaTertinggi"] + nama_lokasi = max_row["Lokasi"] else: - col4.metric("Harga Tertinggi di Pasar", "Data tidak tersedia", "") + harga_tertinggi = 0 + nama_pasar = "-" + nama_lokasi = "-" + + + + # Baris 1 β†’ Dua kolom + col1, col2, col3 = st.columns([1, 1, 1]) + + # Baris 2β†’ Full width + col4 = st.columns([1])[0] + with col1: + render_card("""""", jumlah_lokasi, "Lokasi yang tersedia") + + with col2: + render_card("""""", total_pasar, "Jumlah seluruh pasar") + + with col3: + render_card( + """""", + f"Rp {rata_rata_harga:,.2f}", + "Harga rata-rata Jawa Timur" + ) + + with col4: + render_card( + """""", + f"Rp {harga_tertinggi:,.2f}", + f" Rata-Rata Harga Tertinggi di {nama_pasar} - {nama_lokasi}" + ) + + +# ====================================== +# Cluster Map +# ====================================== def show_clustermap(df): - st.subheader("πŸ—ΊοΈ Wilayah Dengan Cluster Tingkat Harga di Jawa Timur") + glassmorphism_container("πŸ—ΊοΈ Wilayah Dengan Cluster Tingkat Harga di Jawa Timur", lambda: render_map(df)) + +def render_map(df): required_columns = {"cluster", "Latitude", "Longitude", "Lokasi", "RataRataHarga"} if not required_columns.issubset(df.columns): missing_cols = required_columns - set(df.columns) @@ -49,23 +206,28 @@ def show_clustermap(df): try: df_map = df.copy() - df_map['cluster'] = df_map['cluster'].astype(int) + + # Konversi tipe data + df_map['cluster'] = pd.to_numeric(df_map['cluster'], errors='coerce').astype('Int64') df_map['Latitude'] = pd.to_numeric(df_map['Latitude'], errors='coerce') df_map['Longitude'] = pd.to_numeric(df_map['Longitude'], errors='coerce') df_map['RataRataHarga'] = pd.to_numeric(df_map['RataRataHarga'], errors='coerce') - df_map = df_map.dropna(subset=['Latitude', 'Longitude', 'RataRataHarga']) - if len(df_map) == 0: - st.error("❌ Tidak ada data valid untuk ditampilkan") + # Buang data yang tidak valid + df_map = df_map.dropna(subset=['Latitude', 'Longitude', 'cluster', 'RataRataHarga']) + + if df_map.empty: + st.error("❌ Tidak ada data valid untuk ditampilkan di peta.") return + # Mapping cluster default_labels = {0: "Tinggi", 1: "Sedang", 2: "Rendah"} cluster_labels = st.session_state.get("cluster_labels", default_labels) color_map = {"Tinggi": "#4CAF50", "Rendah": "#F44336", "Sedang": "#FFC107"} size_map = {"Tinggi": 20, "Sedang": 10, "Rendah": 5} - df_map['cluster_label'] = df_map['cluster'].map(cluster_labels) + df_map['cluster_label'] = df_map['cluster'].map(cluster_labels).fillna("Tidak Terdefinisi") df_map['size'] = df_map['cluster_label'].map(size_map).fillna(8) fig = px.scatter_mapbox( @@ -82,35 +244,54 @@ def show_clustermap(df): size_max=30 ) + # βœ… Layout Map + Legend Style fig.update_layout( mapbox_style="open-street-map", mapbox_center={"lat": -7.5, "lon": 112.5}, - margin={"r": 0, "t": 0, "l": 0, "b": 0} + margin={"r": 0, "t": 20, "l": 0, "b": 0}, + showlegend=True, + legend=dict( + title=None, # βœ… Menghilangkan judul legenda + bgcolor="rgba(255, 255, 255, 0.7)", # Background transparan putih + bordercolor="rgba(0, 0, 0, 0.5)", + borderwidth=1, + font=dict( + color="black", # βœ… Warna text jadi hitam + size=16 # βœ… Perbesar ukuran teks legenda + ), + orientation="h", + yanchor="bottom", + y=0.01, + xanchor="left", + x=0.01 + ) ) - st.plotly_chart(fig, use_container_width=True) + st.plotly_chart(set_plotly_style(fig), use_container_width=True) except Exception as e: st.error(f"❌ Error saat membuat peta: {str(e)}") - st.write("Debug info:") + st.write("πŸ”§ Debug Info:") st.write(f"Data shape: {df.shape}") st.write(f"Columns: {list(df.columns)}") - if len(df) > 0: - st.write("Sample data:") - st.dataframe(df.head(3)) + st.dataframe(df.head(3)) + +# ====================================== +# Top Bottom Bar Chart +# ====================================== def show_top_bottom_locations(df): - st.subheader("πŸ“Š 5 Lokasi dengan Harga Tertinggi dan Terendah") + glassmorphism_container("πŸ“Š 5 Lokasi Dengan Harga Tertinggi dan Terendah", lambda: render_top_bottom(df)) - if "Lokasi" not in df.columns or "RataRataHarga" not in df.columns: - st.error("Data tidak mengandung kolom 'Lokasi' atau 'RataRataHarga'") - return +def render_top_bottom(df): df_grouped = df.groupby("Lokasi")["RataRataHarga"].mean().reset_index() + df_top5 = df_grouped.sort_values(by="RataRataHarga", ascending=False).head(5) df_bottom5 = df_grouped.sort_values(by="RataRataHarga", ascending=True).head(5) - col1, col2 = st.columns([1, 1]) + col1, col2 = st.columns(2) + with col1: st.markdown("

5 Lokasi Tertinggi

", unsafe_allow_html=True) fig_top = px.bar( @@ -119,11 +300,10 @@ def show_top_bottom_locations(df): y="Lokasi", orientation="h", text="RataRataHarga", - color_discrete_sequence=["#73D2F6"] + color_discrete_sequence=["#00C9FF"] ) - fig_top.update_traces(texttemplate='%{text:.2f}', textposition='outside', width=0.4) - fig_top.update_layout(xaxis_title=None, yaxis_title=None, margin=dict(l=10, r=10, t=30, b=10)) - st.plotly_chart(fig_top, use_container_width=True) + fig_top.update_traces(texttemplate='%{text:.2f}', textposition='outside') + st.plotly_chart(set_plotly_style(fig_top), use_container_width=True, key="top5_chart") with col2: st.markdown("

5 Lokasi Terendah

", unsafe_allow_html=True) @@ -133,18 +313,24 @@ def show_top_bottom_locations(df): y="Lokasi", orientation="h", text="RataRataHarga", - color_discrete_sequence=["#73D2F6"] + color_discrete_sequence=["#FA5F55"] ) - fig_bot.update_traces(texttemplate='%{text:.2f}', textposition='outside', width=0.4) - fig_bot.update_layout(xaxis_title=None, yaxis_title=None, margin=dict(l=10, r=10, t=30, b=10)) - st.plotly_chart(fig_bot, use_container_width=True) + fig_bot.update_traces(texttemplate='%{text:.2f}', textposition='outside') + st.plotly_chart(set_plotly_style(fig_bot), use_container_width=True, key="bottom5_chart") + +# ====================================== +# Line Chart Price Trend +# ====================================== def show_price_trend(): - st.subheader("πŸ“ˆ Linechart Rata-Rata Harga Seluruh Kab/Kota") - tables = pd.read_sql(""" - SELECT name FROM sqlite_master - WHERE type='table' AND name LIKE 'dataset_%' - """, engine) + glassmorphism_container("πŸ“ˆ Tren Rata-Rata Harga Seluruh Kabupaten/Kota", render_trend) + + +def render_trend(): + tables = pd.read_sql( + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'dataset_%'", + engine + ) trend_data = [] for (table_name,) in tables.itertuples(index=False): @@ -153,66 +339,54 @@ def show_price_trend(): file_id = int(table_name.replace("dataset_", "")) uploaded_at_df = pd.read_sql( - f"SELECT uploaded_at FROM uploaded_files WHERE id={file_id}", engine + f"SELECT uploaded_at FROM uploaded_files WHERE id={file_id}", + engine ) - if uploaded_at_df.empty: - continue # Lewati jika tidak ada info waktu upload + continue uploaded_at = uploaded_at_df.iloc[0, 0] rata_rata = df["RataRataHarga"].mean() + trend_data.append({ "uploaded_at": uploaded_at, "RataRataHarga": rata_rata }) except Exception as e: - st.warning(f"Gagal membaca data dari {table_name}: {e}") + st.warning(f"Gagal membaca {table_name}: {e}") if not trend_data: - st.info("Belum ada data yang bisa ditampilkan untuk tren harga.") + st.info("Belum ada data untuk tren harga.") return df_trend = pd.DataFrame(trend_data).sort_values("uploaded_at") df_trend['uploaded_at'] = pd.to_datetime(df_trend['uploaded_at']) fig = px.line(df_trend, x="uploaded_at", y="RataRataHarga", markers=True) - fig.update_layout( - xaxis_title="Waktu Upload", - yaxis_title="Rata-rata Harga", - # title={"text": "Perkembangan Harga dari Waktu ke Waktu", "x": 0.5}, - height=450 - ) - st.plotly_chart(fig, use_container_width=True) + fig.update_traces(line_color="#00FFD5", marker_color="#00FFD5") + st.plotly_chart(set_plotly_style(fig), use_container_width=True) + + +# ====================================== +# Data Table +# ====================================== def show_data_table(df): - st.subheader("πŸ—’οΈ Data Tabel") + glassmorphism_container("πŸ—’οΈ Data Tabel", lambda: render_table(df)) + +def render_table(df): if df.empty: st.info("Data kosong atau belum dipilih.") return - # Deteksi jika ada kolom index seperti Unnamed: 0, hapus itu dulu first_col = df.columns[0] if first_col.lower() in ["unnamed: 0", "index", "0"]: df = df.drop(columns=first_col) - # =============================== - # βœ… Tentukan kolom yang akan di-hide - # =============================== - hide_columns_positions = [0, 6, 7, 8] # Kolom ke-1,7,8,9 (posisi 0-based) - - # Pastikan posisi tidak melebihi jumlah kolom yang ada - hide_columns_positions = [ - idx for idx in hide_columns_positions if idx < len(df.columns) - ] - - # Dapatkan nama kolom yang ingin disembunyikan + hide_columns_positions = [0, 6, 7, 8] + hide_columns_positions = [idx for idx in hide_columns_positions if idx < len(df.columns)] hide_columns = [df.columns[idx] for idx in hide_columns_positions] - - # Pilih kolom yang tidak disembunyikan columns_to_display = [col for col in df.columns if col not in hide_columns] - # =============================== - # βœ… Tampilkan dataframe dengan kolom yang sudah di-hide - # =============================== - st.dataframe(df[columns_to_display]) \ No newline at end of file + st.dataframe(df[columns_to_display])