v1.2
This commit is contained in:
parent
a7bd349400
commit
afb7d782dd
Binary file not shown.
Binary file not shown.
|
@ -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"
|
||||||
|
}
|
BIN
data/files.db
BIN
data/files.db
Binary file not shown.
134
main.py
134
main.py
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -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():
|
||||||
|
|
|
@ -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 = len(df)
|
||||||
# Jumlah lokasi = banyak baris
|
|
||||||
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]
|
||||||
|
|
||||||
# ===============================
|
st.dataframe(df[columns_to_display])
|
||||||
# ✅ Tampilkan dataframe dengan kolom yang sudah di-hide
|
|
||||||
# ===============================
|
|
||||||
st.dataframe(df[columns_to_display])
|
|
||||||
|
|
Loading…
Reference in New Issue