This commit is contained in:
agungbima009 2025-06-29 22:18:18 +07:00
parent a7bd349400
commit afb7d782dd
10 changed files with 461 additions and 114 deletions

Binary file not shown.

View File

@ -1 +1,6 @@
{"username": "admin", "password": "$2b$12$u7yQNfe6p93rcm/0iPMUtejIbJPHJOgpuxX/1HWt5KkJCc8W5HYUa", "security_question": "Nama hewan peliharaan pertama Anda?", "security_answer": "nalakucinglucu"} {
"username": "admin",
"password": "$2b$12$kUz0oZpa/Utyu7TV/wGazOGySvTkE457J1ZqfbwBgV2npXI5f.4n2",
"security_question": "Nama hewan peliharaan pertama Anda?",
"security_answer": "$2b$12$YZnmM72z4wGc0q6cS9WyG.UV70xYnX9lIbpSSGWZWRAF51SSZ3BSO"
}

Binary file not shown.

134
main.py
View File

@ -1,17 +1,139 @@
import streamlit as st import streamlit as st
import admin_dashboard import admin_dashboard
import dashboard_user
from utils.auth import login 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", # Styling CSS — Sama dengan Visualisasi.py
"Admin Dashboard" # ===============================
]) st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');
.stApp {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Inter', sans-serif;
}
.stApp header {
background: linear-gradient(90deg, #667eea, #764ba2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
[data-testid="stSidebar"] {
background-color: rgba(33, 33, 33, 0.9);
border-right: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.sidebar-title {
color: white;
font-size: 1.6rem;
font-weight: 700;
text-align: center;
margin-bottom: 1.5rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
.page-indicator {
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 0.7rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(8px);
color: white;
text-align: center;
margin-top: 1rem;
font-size: 0.85rem;
font-weight: 500;
}
.nav-section {
background: rgba(255, 255, 255, 0.08);
border-radius: 15px;
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
margin-top: 1.5rem;
color: white;
}
h1, h2, h3, h4, h5, h6 {
color: white;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
</style>
""", unsafe_allow_html=True)
# ===============================
# Sidebar dengan Desain Modern
# ===============================
with st.sidebar:
st.markdown('''
<div class="sidebar-title">
Navigation
</div>
''', 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("""
<div class="page-indicator">
📊 Mode: User Dashboard
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="page-indicator">
Mode: Admin Dashboard
</div>
""", unsafe_allow_html=True)
# ===============================
# Routing halaman
# ===============================
if page == "User Dashboard": if page == "User Dashboard":
dashboard_user.render() dashboard_user.render()
elif page == "Admin Dashboard": elif page == "Admin Dashboard":
if st.session_state.get("authenticated"): if st.session_state.get("authenticated"):
admin_dashboard.render() admin_dashboard.render()

Binary file not shown.

View File

@ -33,9 +33,26 @@ def is_strong_password(password):
return True return True
# Login form # 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(): def login():
config = load_config() config = load_config()
# Inisialisasi session state
if 'authenticated' not in st.session_state: if 'authenticated' not in st.session_state:
st.session_state.authenticated = False st.session_state.authenticated = False
if 'show_reset' not in st.session_state: if 'show_reset' not in st.session_state:
@ -50,37 +67,59 @@ def login():
password = st.text_input("Password", type="password") password = st.text_input("Password", type="password")
if st.button("Login"): 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.authenticated = True
st.session_state.login_failed = False st.session_state.login_failed = False
st.session_state.show_reset = False
st.success("✅ Login berhasil!")
st.rerun() st.rerun()
else: else:
st.session_state.login_failed = True st.session_state.login_failed = True
st.error("❌ Username atau password salah.") 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.session_state.login_failed and not st.session_state.show_reset:
if st.button("Lupa Password"): if st.button("Lupa Password"):
st.session_state.show_reset = True st.session_state.show_reset = True
if st.session_state.show_reset: if st.session_state.show_reset:
st.info("Jawab pertanyaan rahasia untuk reset password.") st.info("🔐 Jawab pertanyaan rahasia untuk reset password.")
question = config.get("security_question")
answer = st.text_input("Pertanyaan:", placeholder=question) question = config.get("security_question", "Pertanyaan tidak tersedia")
answer = st.text_input(f"{question}")
new_pass = st.text_input("Password Baru", type="password") new_pass = st.text_input("Password Baru", type="password")
if st.button("Ganti Password"): if st.button("Ganti Password"):
if answer.lower() == config.get("security_answer").lower(): stored_hashed_answer = config.get("security_answer")
new_hash = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode()
config["password"] = new_hash # Validasi apakah security_answer ada
with open("config.json", "w") as f: if not stored_hashed_answer:
json.dump(config, f) st.error("⚠️ Pertanyaan keamanan belum diset pada konfigurasi.")
st.success("✅ Password berhasil diganti!")
st.session_state.show_reset = False
st.session_state.login_failed = False
else: 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 return False
else: else:
return True return True
@ -95,18 +134,25 @@ def show_password_reset_form():
new_pass = st.text_input("Password Baru", type="password") new_pass = st.text_input("Password Baru", type="password")
if st.button("Ganti 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): 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 return
# Simpan password baru dengan hashing
config["password"] = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode() config["password"] = bcrypt.hashpw(new_pass.encode(), bcrypt.gensalt()).decode()
save_config(config) 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 = False
st.session_state.show_reset_button = False st.session_state.show_reset_button = False
else: else:
st.error("Jawaban pertanyaan salah.") st.error("Jawaban pertanyaan keamanan salah.")
# Logout # Logout
def logout(): def logout():

View File

@ -1,45 +1,202 @@
import streamlit as st import streamlit as st
import plotly.express as px
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd import pandas as pd
from sqlalchemy import text import plotly.express as px
from utils.db import engine from utils.db import engine
from sklearn.metrics import silhouette_score
# ======================================
# CSS Styling Glassmorphism
# ======================================
def set_background():
st.markdown("""
<style>
.stApp {
background: linear-gradient(135deg, #7F00FF, #E100FF);
background-attachment: fixed;
color: white;
}
[data-testid="stSidebar"] {
background-color: rgba(33, 33, 33, 0.95);
border-radius: 16px;
padding: 16px;
}
div[data-testid="metric-container"] {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px;
padding: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.37);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: all 0.3s ease-in-out;
}
div[data-testid="metric-container"]:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 10px 40px rgba(0,0,0,0.45);
}
h1, h2, h3, h4, h5, h6 {
color: white;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
</style>
""", unsafe_allow_html=True)
# ======================================
# Glassmorphism Container
# ======================================
def glassmorphism_container(title, content):
st.markdown(f"""
<div style="
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(0,0,0,0.37);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
">
<h3 style='text-align:center'>{title}</h3>
""", unsafe_allow_html=True)
content()
st.markdown("</div>", unsafe_allow_html=True)
def render_card(title, value, subtitle=""):
st.markdown(f"""
<div style="
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.37);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
text-align: center;
color: white;
display: flex;
gap: 20px;
align-items: center;
justify-content: center;
">
<div style="align-items: center;">{title}</div>
<div>
<div style="font-size:16px; opacity:0.8; text-align: left;">{subtitle}</div>
<div style="font-size:30px;font-weight:bold;">{value}</div>
</div>
</div>
""", 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): def show_scorecard(df):
col1, col2, col3, col4 = st.columns(4)
# Jumlah lokasi = banyak baris jumlah_lokasi = len(df)
col1.metric("Jumlah Lokasi", len(df))
# Jumlah total pasar dari seluruh baris
total_pasar = int(df["JumlahPasar"].sum()) 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: if not df.empty:
max_row = df.loc[df['RataRataHargaTertinggiDiPasar'].idxmax()] max_row = df.loc[df['RataRataHargaTertinggiDiPasar'].idxmax()]
nama_pasar = max_row['PasarDenganRataRataHargaTertinggi'] harga_tertinggi = max_row["RataRataHargaTertinggiDiPasar"]
nama_lokasi = max_row['Lokasi'] nama_pasar = max_row["PasarDenganRataRataHargaTertinggi"]
harga_tertinggi = max_row['RataRataHargaTertinggiDiPasar'] nama_lokasi = max_row["Lokasi"]
col4.metric(
label="Harga Tertinggi di Pasar",
value=f"Rp {harga_tertinggi:,.2f}",
delta=f"{nama_pasar} - {nama_lokasi}"
)
else: 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("""<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10m0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/></svg>""", jumlah_lokasi, "Lokasi yang tersedia")
with col2:
render_card("""<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-shop-window" viewBox="0 0 16 16"><path d="M2.97 1.35A1 1 0 0 1 3.73 1h8.54a1 1 0 0 1 .76.35l2.609 3.044A1.5 1.5 0 0 1 16 5.37v.255a2.375 2.375 0 0 1-4.25 1.458A2.37 2.37 0 0 1 9.875 8 2.37 2.37 0 0 1 8 7.083 2.37 2.37 0 0 1 6.125 8a2.37 2.37 0 0 1-1.875-.917A2.375 2.375 0 0 1 0 5.625V5.37a1.5 1.5 0 0 1 .361-.976zm1.78 4.275a1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0 1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0 1.375 1.375 0 1 0 2.75 0V5.37a.5.5 0 0 0-.12-.325L12.27 2H3.73L1.12 5.045A.5.5 0 0 0 1 5.37v.255a1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0M1.5 8.5A.5.5 0 0 1 2 9v6h12V9a.5.5 0 0 1 1 0v6h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1V9a.5.5 0 0 1 .5-.5m2 .5a.5.5 0 0 1 .5.5V13h8V9.5a.5.5 0 0 1 1 0V13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5a.5.5 0 0 1 .5-.5"/></svg>""", total_pasar, "Jumlah seluruh pasar")
with col3:
render_card(
"""<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-radar" viewBox="0 0 16 16"><path d="M6.634 1.135A7 7 0 0 1 15 8a.5.5 0 0 1-1 0 6 6 0 1 0-6.5 5.98v-1.005A5 5 0 1 1 13 8a.5.5 0 0 1-1 0 4 4 0 1 0-4.5 3.969v-1.011A2.999 2.999 0 1 1 11 8a.5.5 0 0 1-1 0 2 2 0 1 0-2.5 1.936v-1.07a1 1 0 1 1 1 0V15.5a.5.5 0 0 1-1 0v-.518a7 7 0 0 1-.866-13.847"/></svg>""",
f"Rp {rata_rata_harga:,.2f}",
"Harga rata-rata Jawa Timur"
)
with col4:
render_card(
"""<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-clipboard2-pulse-fill" viewBox="0 0 16 16"><path d="M10 .5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5.5.5 0 0 1-.5.5.5.5 0 0 0-.5.5V2a.5.5 0 0 0 .5.5h5A.5.5 0 0 0 11 2v-.5a.5.5 0 0 0-.5-.5.5.5 0 0 1-.5-.5"/><path d="M4.085 1H3.5A1.5 1.5 0 0 0 2 2.5v12A1.5 1.5 0 0 0 3.5 16h9a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 12.5 1h-.585q.084.236.085.5V2a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 2v-.5q.001-.264.085-.5M9.98 5.356 11.372 10h.128a.5.5 0 0 1 0 1H11a.5.5 0 0 1-.479-.356l-.94-3.135-1.092 5.096a.5.5 0 0 1-.968.039L6.383 8.85l-.936 1.873A.5.5 0 0 1 5 11h-.5a.5.5 0 0 1 0-1h.191l1.362-2.724a.5.5 0 0 1 .926.08l.94 3.135 1.092-5.096a.5.5 0 0 1 .968-.039Z"/></svg>""",
f"Rp {harga_tertinggi:,.2f}",
f" Rata-Rata Harga Tertinggi di {nama_pasar} - {nama_lokasi}"
)
# ======================================
# Cluster Map
# ======================================
def show_clustermap(df): 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"} required_columns = {"cluster", "Latitude", "Longitude", "Lokasi", "RataRataHarga"}
if not required_columns.issubset(df.columns): if not required_columns.issubset(df.columns):
missing_cols = required_columns - set(df.columns) missing_cols = required_columns - set(df.columns)
@ -49,23 +206,28 @@ def show_clustermap(df):
try: try:
df_map = df.copy() 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['Latitude'] = pd.to_numeric(df_map['Latitude'], errors='coerce')
df_map['Longitude'] = pd.to_numeric(df_map['Longitude'], 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['RataRataHarga'] = pd.to_numeric(df_map['RataRataHarga'], errors='coerce')
df_map = df_map.dropna(subset=['Latitude', 'Longitude', 'RataRataHarga'])
if len(df_map) == 0: # Buang data yang tidak valid
st.error("❌ Tidak ada data valid untuk ditampilkan") 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 return
# Mapping cluster
default_labels = {0: "Tinggi", 1: "Sedang", 2: "Rendah"} default_labels = {0: "Tinggi", 1: "Sedang", 2: "Rendah"}
cluster_labels = st.session_state.get("cluster_labels", default_labels) cluster_labels = st.session_state.get("cluster_labels", default_labels)
color_map = {"Tinggi": "#4CAF50", "Rendah": "#F44336", "Sedang": "#FFC107"} color_map = {"Tinggi": "#4CAF50", "Rendah": "#F44336", "Sedang": "#FFC107"}
size_map = {"Tinggi": 20, "Sedang": 10, "Rendah": 5} 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) df_map['size'] = df_map['cluster_label'].map(size_map).fillna(8)
fig = px.scatter_mapbox( fig = px.scatter_mapbox(
@ -82,35 +244,54 @@ def show_clustermap(df):
size_max=30 size_max=30
) )
# ✅ Layout Map + Legend Style
fig.update_layout( fig.update_layout(
mapbox_style="open-street-map", mapbox_style="open-street-map",
mapbox_center={"lat": -7.5, "lon": 112.5}, 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: except Exception as e:
st.error(f"❌ Error saat membuat peta: {str(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"Data shape: {df.shape}")
st.write(f"Columns: {list(df.columns)}") st.write(f"Columns: {list(df.columns)}")
if len(df) > 0: st.dataframe(df.head(3))
st.write("Sample data:")
st.dataframe(df.head(3))
# ======================================
# Top Bottom Bar Chart
# ======================================
def show_top_bottom_locations(df): 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_grouped = df.groupby("Lokasi")["RataRataHarga"].mean().reset_index()
df_top5 = df_grouped.sort_values(by="RataRataHarga", ascending=False).head(5) df_top5 = df_grouped.sort_values(by="RataRataHarga", ascending=False).head(5)
df_bottom5 = df_grouped.sort_values(by="RataRataHarga", ascending=True).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: with col1:
st.markdown("<h4 style='text-align:center;'>5 Lokasi Tertinggi</h4>", unsafe_allow_html=True) st.markdown("<h4 style='text-align:center;'>5 Lokasi Tertinggi</h4>", unsafe_allow_html=True)
fig_top = px.bar( fig_top = px.bar(
@ -119,11 +300,10 @@ def show_top_bottom_locations(df):
y="Lokasi", y="Lokasi",
orientation="h", orientation="h",
text="RataRataHarga", 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_traces(texttemplate='%{text:.2f}', textposition='outside')
fig_top.update_layout(xaxis_title=None, yaxis_title=None, margin=dict(l=10, r=10, t=30, b=10)) st.plotly_chart(set_plotly_style(fig_top), use_container_width=True, key="top5_chart")
st.plotly_chart(fig_top, use_container_width=True)
with col2: with col2:
st.markdown("<h4 style='text-align:center;'>5 Lokasi Terendah</h4>", unsafe_allow_html=True) st.markdown("<h4 style='text-align:center;'>5 Lokasi Terendah</h4>", unsafe_allow_html=True)
@ -133,18 +313,24 @@ def show_top_bottom_locations(df):
y="Lokasi", y="Lokasi",
orientation="h", orientation="h",
text="RataRataHarga", 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_traces(texttemplate='%{text:.2f}', textposition='outside')
fig_bot.update_layout(xaxis_title=None, yaxis_title=None, margin=dict(l=10, r=10, t=30, b=10)) st.plotly_chart(set_plotly_style(fig_bot), use_container_width=True, key="bottom5_chart")
st.plotly_chart(fig_bot, use_container_width=True)
# ======================================
# Line Chart Price Trend
# ======================================
def show_price_trend(): def show_price_trend():
st.subheader("📈 Linechart Rata-Rata Harga Seluruh Kab/Kota") glassmorphism_container("📈 Tren Rata-Rata Harga Seluruh Kabupaten/Kota", render_trend)
tables = pd.read_sql("""
SELECT name FROM sqlite_master
WHERE type='table' AND name LIKE 'dataset_%' def render_trend():
""", engine) tables = pd.read_sql(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'dataset_%'",
engine
)
trend_data = [] trend_data = []
for (table_name,) in tables.itertuples(index=False): for (table_name,) in tables.itertuples(index=False):
@ -153,66 +339,54 @@ def show_price_trend():
file_id = int(table_name.replace("dataset_", "")) file_id = int(table_name.replace("dataset_", ""))
uploaded_at_df = pd.read_sql( 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: if uploaded_at_df.empty:
continue # Lewati jika tidak ada info waktu upload continue
uploaded_at = uploaded_at_df.iloc[0, 0] uploaded_at = uploaded_at_df.iloc[0, 0]
rata_rata = df["RataRataHarga"].mean() rata_rata = df["RataRataHarga"].mean()
trend_data.append({ trend_data.append({
"uploaded_at": uploaded_at, "uploaded_at": uploaded_at,
"RataRataHarga": rata_rata "RataRataHarga": rata_rata
}) })
except Exception as e: 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: if not trend_data:
st.info("Belum ada data yang bisa ditampilkan untuk tren harga.") st.info("Belum ada data untuk tren harga.")
return return
df_trend = pd.DataFrame(trend_data).sort_values("uploaded_at") df_trend = pd.DataFrame(trend_data).sort_values("uploaded_at")
df_trend['uploaded_at'] = pd.to_datetime(df_trend['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 = px.line(df_trend, x="uploaded_at", y="RataRataHarga", markers=True)
fig.update_layout( fig.update_traces(line_color="#00FFD5", marker_color="#00FFD5")
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)
st.plotly_chart(set_plotly_style(fig), use_container_width=True)
# ======================================
# Data Table
# ======================================
def show_data_table(df): def show_data_table(df):
st.subheader("🗒️ Data Tabel") glassmorphism_container("🗒️ Data Tabel", lambda: render_table(df))
def render_table(df):
if df.empty: if df.empty:
st.info("Data kosong atau belum dipilih.") st.info("Data kosong atau belum dipilih.")
return return
# Deteksi jika ada kolom index seperti Unnamed: 0, hapus itu dulu
first_col = df.columns[0] first_col = df.columns[0]
if first_col.lower() in ["unnamed: 0", "index", "0"]: if first_col.lower() in ["unnamed: 0", "index", "0"]:
df = df.drop(columns=first_col) df = df.drop(columns=first_col)
# =============================== hide_columns_positions = [0, 6, 7, 8]
# ✅ Tentukan kolom yang akan di-hide hide_columns_positions = [idx for idx in hide_columns_positions if idx < len(df.columns)]
# ===============================
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 = [df.columns[idx] for idx in hide_columns_positions] 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] 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]) st.dataframe(df[columns_to_display])