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])