From afb7d782ddb96d82a93ed3f39dc8047be8c455d1 Mon Sep 17 00:00:00 2001 From: agungbima009 Date: Sun, 29 Jun 2025 22:18:18 +0700 Subject: [PATCH] v1.2 --- __pycache__/dashboard_user.cpython-310.pyc | Bin 2241 -> 2241 bytes __pycache__/test.cpython-310.pyc | Bin 0 -> 2983 bytes config.json | 7 +- data/files.db | Bin 270336 -> 278528 bytes main.py | 134 ++++++- models/kmeans_model.pkl | Bin 9315 -> 9619 bytes utils/__pycache__/auth.cpython-310.pyc | Bin 3587 -> 4175 bytes .../visualizations.cpython-310.pyc | Bin 6424 -> 12949 bytes utils/auth.py | 84 ++++- utils/visualizations.py | 350 +++++++++++++----- 10 files changed, 461 insertions(+), 114 deletions(-) create mode 100644 __pycache__/test.cpython-310.pyc diff --git a/__pycache__/dashboard_user.cpython-310.pyc b/__pycache__/dashboard_user.cpython-310.pyc index b81b5390761f7a06e51c8c094785e0b653e900f0..f145690aa81fda65ef353b414751635f5ca0a68f 100644 GIT binary patch delta 19 ZcmX>ocuocuH(8X5R3{#fAax zhnL5}R||&mixwuE1;Rt<@-7U-AZBDdHHk$k&y1%QF%OJZ^*8up?0b5gY*i3;q0475 zDC5K=*?-?yW-e8=l|q^;Nwt!6t0dLO(sPTxglX z_15D4;86ozG)H6IE;EBBc7DY9*>(Hr=3sj6#pK!%;BAs zN!+E!IEr}hI8bqf--Tcma~y`xkQZSox0|kS3rg(Gl@*lO51L~`8jC~1Vxq?xxMx(L zxNqZBany}yABBp>()K7*@Z}_xD(s%yE>%5B*_5@+B1VCr-Ro?1D9h#oHj0ln+7No>`}XKZ(ic|)vg16M#Wa<)))BPURzmdb{_MX zUh4OT_V%#C|EIkjm#6AdgH9V#SB}?|%4q`&#xn+0#}ys?HEfMBPp(XV(!Y`~z6c4@ zgYEH1+N&rgTVrt@`FNIZSy=DeHrj=Rqg79 zLDeVio2Tk0*vHY$7IT7_qCtD|L*sn zzC|CW%$FfLP=ew(3Kcq}3Mc3zDo$Wuv%&5TBN&Q;RuY_Gg<{HlKV|q7Gjz)1gfq$z z!zw(Z6J{h$s8AsaS-3U0_gNI4qC+0TrDQy3@XoYGyX|Ix?zd_~OR8Z##^Mwz4;#+Q z)D0*mR1D{O08)x7-rlbLT73_mi?x?a xcN&$3*)W&P>fhov{L-obzEKD>OPP9%Cx6oy@nPMGIY}ehF||W|hprn7)<2{+60-mR literal 0 HcmV?d00001 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 2482842e2c03163761c2ee11036520602295a1ae..e5d557cc46370ac90d1aa41195c8290365b898db 100644 GIT binary patch delta 846 zcmX|-eMr=I9LMkb`QH7W@AvziL(9@TP1B~Q-FfH3@5Z&n!8@RTtEf@qL(FoVr3Pu~XL0J7ZsSmvW`Mmf%yx*~u z8H;%i&GtNYI2=oCy{ziP{t$aSyl&7)a~d0-A0?@5R0`!u`4hQW&XjITN2N8AD5k`{Vx6c8DPfmT zBRE_s`^`-*+;lk?xM#M2%2{`pxqkr`tQd0`myP#~u>Pk$s5j}Rd)&Rx zy;OUqrL^r@xq4qcsWz&*a!u({7Ryue6}e3=kS3)8sYUXMx5blUlb9#m6FwH!2%2lm zj%~9Gqho9iem_t7m?hZ$D&o8t_202P^jxreI|Z)c`zKU`iO;x+(gt1t?^V`_!HYB( zUl-c+)K#`DIn~YQGgQm=T(p-LC0l=B6%45e&&AC?9))j+XW-HmDneA{UhLY>i*S6L z72>Tg?fCyK=J{~<^WtQ!pJy}F-(X>UFhwc`N_gzQJmx|ir2;g0?cg^Y Pf^Wm7H&`t$8?5#PIqL-O delta 208 zcmZo@5NtRgFhN@I9RmY{6A;6I^+X+G#&;VNmhcNP^ZPLIuj6mz_u&`kd(5|+XnZ? + @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; + } + + +""", 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 9f23191bb828dd7a43eb36713d9cafe711bb2f03..1afe5f4467242001daaa11a564d0f369bd659801 100644 GIT binary patch literal 9619 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 diff --git a/utils/__pycache__/auth.cpython-310.pyc b/utils/__pycache__/auth.cpython-310.pyc index 11ec8b3e22e9e3da101bb4c2ece25a0c33da0c63..62461f497bec97fee6932eba260d281dbc814301 100644 GIT binary patch delta 1960 zcmZuyO>7%Q6yBNr@p`>>erm^Qo3@)a{J5nFX(=T%Dxe}jP*q3~LbQUg_H5&go!w}5 z7lX7>kOC4yNNpHE6~DH`frukQATA)zAnq%~p@K^Vl!CZ$Xn1cNN{aAmzL|OR=Dj!X zeKTGg`_3ta#iFIa@8XZoHovT{m&V8|>xUj$pBg=!XWC23*&MT&0lmoz%!b}#MOK2o zzy{bL^fn`NN^NNU;%0t>WaRvY!iU8r@&qZ1UrC#6h;!7L-JqG#2!r$f0v`fjeE0_J zfKgV6+~>Ucz5=fQVF1`QJHBNN&wXwf|YvBC(W zYxF{KMTtq5rb>@IsZfO}8hmRiQRb9sC8~4?QSf^T8UtBPJ;N#N=V+~_@*<8)2m=U% z2*cwN5iovU2Dlws)Q{UF6Pw~1aYnWx7+N7QS%e5u!c?Y-+h~pHTV5)-e*~claFf5C zDBXpy?esVu1GobRdjM|A0>+Fn$Siy>zEvM0KZ~+x}CT&gg0R*6SaWvzd_ zBHP*bA(r*Z&;Z-@k4I($7j}!c^stIGzWnaX&gpQ$51hKkn{MQ{$AxAbvy!8*_}d3> zTy(?<<0$z{oXhVLuNo6)5+vc}x4TzAzu-LX@z@PsaNWRZdG3-Mz@YB6lO>1wkrzA5 zE_0ohydMb%+mn1RY))$bS`csM$JNQ?nE1jNUv=jEw%Y<5Z5dF@TdKQlfAY^;QUATy zoJZY`TZiT4|8X0+ZMQjDt4hvhG>Wrg-9247`pM{pVI> zy3EF1;3#4M2C-C=Sft-fY7vW;pxKmej#7Hv)J(Ys8n8B~qb81QY7^A6P7zJDh_<7A z)dn*f8C6XR#-N!*)2!L9=-Z{Rl~H1B_y0kKtPvOKU}iwz(>?V3Hf zjX-7%5y<>>Y^CZNiDrSw1!mnKGLSu0Q71OU?`Cs~<1@pN7#zKDEIB`jkV8-b5*J{L zctD%8`2=%<{1w;o(;hzrvsy*gpUYQd5(Io2VG7|+gd+$@?M#ni?k{t+!SU7QGYI!0 zoIsdGz!!-hLpY2uA$nHTJPyN}4#oV++E+P=^7|0BJ!aynwP)%@5JpHis2F75lG(S( zC?*@yK897KrV3S2wO{zDPi6`qk^>kxFwSRQ-R1p|rRN7h-apIC>&5cHyJ1?Rnbru~ z(-MztcaJscuo3Xx@1I>}2Fbv& x@Ei?o^CXXquSjOjE`=;Bf9x6o$C4kJvGl|_eyVlGa z3$sxwg(C<>b4K+_y>LcIoDc^paqGEzNGtvZP9PE9Tc;rs>}q~9`#$EqdGELLw7Op@ z1o?bcgXhKLdh_S8gF=OTb};k)xeUvGrLAXKj=3=BSe_MNb{SdKY9j|f9c5}HA)DW8 zqNsi$AChr3skc7`nceak_rsbg^029J5efi_$+|n8nx5E=z~5|biBZ%PPhbpfa?<;p z*M|nEk~V-##>mAZQC83OA9JT1)Fb1q>GNRFfN}CrSmC%0L;k83t-IufdT7n;ZA2u} z1{s!%$+j-m!t5Xy=>s>`QYnoIu4B+JHzW`Ay212xwn8$<#~RW6%&`U~1(uHq(a_?7 zC1g6A66OJAg_sC=Y->8kSSTQo#*f+ZR#4hh0M&@AjIHd_l z3D%J8Gf1`=S(qTK#xTvnc&zQ(ksWD_E^FZ#Hp!;;^=}KiPB<}`jI_v!b)tQwG0lW` z-+=V#f+nBzAU?ipFd(BTR>F*qk7}wv?U7=Em4~ubjk(b6v+Ar9sMxuo_vh4e=X#}| zyU#`F`CoXRzXUDMb=qEoH-k3gLe0`kuF#?Q;tW9dyn5GtMIBOmrHdW@{kyNLA9y`) z!}F`sYkSSPT1kw9BGF}-lZJtRg=}dKKm2UnaR(|+0|Co4q5a3OV@eNO; zM;K;t3-?dYwwzitf_8Vumx4l^h!;}4b~+jKuz_4bH|(f~Ba3@?&^(`H zpg7#yX}7{*0n?JC^v!^Qs34@5EdE8qx?l~(62b}sZgeVAT7dMnrOP`3>$dqV0v56a VKs6*|O31uU4eC&f7U)fP>K`V+W|jZ| diff --git a/utils/__pycache__/visualizations.cpython-310.pyc b/utils/__pycache__/visualizations.cpython-310.pyc index 3482ae0d0ea41dcfd3921ee97ea485fcfeb3c835..22f89f7471fde11318b915eef1f764074f977458 100644 GIT binary patch literal 12949 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 U^i1g4P$Hy+*6=)sCpN15H|U&oKL7v# literal 6424 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 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])