Spaces:
Sleeping
Sleeping
| # File: pages/laporan.py (Revisi Final dengan Layout Satu Baris per Grafik) | |
| from dash import dcc, html, Input, Output, callback, no_update, State, dash_table | |
| import dash_bootstrap_components as dbc | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from sqlalchemy import select, distinct, func, and_ | |
| import io | |
| # Impor engine dan tabel dari file database.py | |
| from database import engine, detail_penyakit | |
| # Impor library untuk pembuatan PDF | |
| try: | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, PageBreak, Table, TableStyle | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT | |
| from reportlab.lib import colors as reportlab_colors | |
| import plotly.io as pio | |
| if hasattr(pio, 'kaleido'): | |
| pio.kaleido.scope.mathjax = None | |
| PDF_CAPABLE = True | |
| except (AttributeError, ImportError): | |
| PDF_CAPABLE = False | |
| print("WARNING: Pustaka 'reportlab' atau 'kaleido' tidak terinstall. Fitur unduh PDF tidak akan berfungsi.") | |
| # ----------------------------------------------------------------------------- | |
| # BAGIAN 1: FUNGSI-FUNGSI HELPER (Tidak Berubah) | |
| # ----------------------------------------------------------------------------- | |
| def get_filter_text(pusk, tahun, bulan): | |
| pusk_txt = "Seluruh Puskesmas" if not pusk else ", ".join(pusk) | |
| tahun_txt = "Seluruh Tahun" if not tahun else ", ".join(map(str, sorted(tahun))) | |
| bulan_txt = "Semua Bulan" if not bulan else ", ".join(sorted(bulan)) | |
| return f"Data dari: {pusk_txt} | Tahun: {tahun_txt} | Bulan: {bulan_txt}" | |
| def kategori_penyakit_atp(icd): | |
| if pd.isna(icd) or str(icd).strip() == "": return 'Tidak Menular' | |
| icd_clean = str(icd).strip().upper() | |
| if icd_clean.startswith(('A', 'B')): return 'Menular' | |
| return 'Tidak Menular' | |
| def create_ranking_analysis_text(df): | |
| if df.empty: return "Tidak ada data untuk dianalisis." | |
| top_disease, bottom_disease = df.iloc[-1], df.iloc[0] | |
| return f"- Kasus tertinggi: **{top_disease['jenis_penyakit']}** ({int(top_disease['totall']):,} kasus).\n- Peringkat ke-10: **{bottom_disease['jenis_penyakit']}** ({int(bottom_disease['totall']):,} kasus)." | |
| def create_ranking_table(df): | |
| if df.empty: return None | |
| df_display = df.copy(); df_display['sort_val'] = pd.to_numeric(df_display['totall']) | |
| df_display = df_display.sort_values('sort_val', ascending=False).drop(columns=['sort_val']) | |
| df_display['totall'] = df_display['totall'].apply(lambda x: f"{int(x):,}") | |
| df_display = df_display.rename(columns={'jenis_penyakit': 'Jenis Penyakit', 'totall': 'Total Kasus'}) | |
| return dash_table.DataTable(data=df_display.to_dict('records'), columns=[{"name": i, "id": i} for i in df_display.columns], style_table={'overflowX': 'auto', 'marginTop': '15px', 'border': '1px solid #ddd'}, style_cell={'textAlign': 'left', 'padding': '8px', 'fontFamily': 'sans-serif'}, style_header={'fontWeight': 'bold', 'backgroundColor': 'rgb(230, 230, 230)'}, style_data_conditional=[{'if': {'row_index': 'odd'}, 'backgroundColor': 'rgb(248, 248, 248)'}]) | |
| def create_pie_analysis_text(df): | |
| if df.empty: return "Tidak ada data kategori untuk dianalisis." | |
| total_cases = df['totall'].sum() | |
| if total_cases == 0: return "Total kasus adalah nol." | |
| df['persentase'] = (df['totall'] / total_cases * 100).round(1) | |
| menular = df[df['kategori'] == 'Menular']; tidak_menular = df[df['kategori'] == 'Tidak Menular'] | |
| perc_menular = menular['persentase'].iloc[0] if not menular.empty else 0 | |
| perc_tidak_menular = tidak_menular['persentase'].iloc[0] if not tidak_menular.empty else 0 | |
| kesimpulan = "dominan" if perc_menular > perc_tidak_menular else "lebih sedikit" | |
| return f"- Penyakit **Menular** ({perc_menular}%) **{kesimpulan}** dibandingkan Tidak Menular ({perc_tidak_menular}%)." | |
| def create_trend_analysis_text(df, time_unit='tahun'): | |
| if df.empty or (time_unit == 'tahun' and df['tahun'].nunique() < 2): return "Data tidak cukup untuk analisis tren (minimal 2 periode)." | |
| top_3_diseases = df.groupby('jenis_penyakit')['totall'].sum().nlargest(3).index.tolist() | |
| if not top_3_diseases: return "Tidak ada data penyakit untuk dianalisis trennya." | |
| analysis_points = [] | |
| for disease in top_3_diseases: | |
| df_disease = df[df['jenis_penyakit'] == disease].sort_values(time_unit) | |
| if len(df_disease) > 1: | |
| start_val, end_val = df_disease['totall'].iloc[0], df_disease['totall'].iloc[-1] | |
| if end_val > start_val: tren = f"naik dari {int(start_val):,} menjadi {int(end_val):,}" | |
| elif end_val < start_val: tren = f"turun dari {int(start_val):,} menjadi {int(end_val):,}" | |
| else: tren = f"stabil di {int(end_val):,}" | |
| analysis_points.append(f"- Kasus **{disease}** {tren}.") | |
| return "Ringkasan Tren 3 Penyakit Teratas:\n\n" + "\n".join(analysis_points) | |
| def create_comparison_analysis_text(df): | |
| if df.empty: return "Tidak ada data untuk dianalisis." | |
| pusk_contribution = df.groupby('kode_pusk')['totall'].sum().sort_values(ascending=False) | |
| if pusk_contribution.empty: return "Tidak ada data puskesmas untuk dianalisis." | |
| top_pusk, top_pusk_cases, total_cases = pusk_contribution.index[0], pusk_contribution.iloc[0], pusk_contribution.sum() | |
| if total_cases == 0: return "Total kasus adalah nol." | |
| top_pusk_percent = (top_pusk_cases / total_cases * 100).round(1) | |
| top_disease_by_pusk = df[df['kode_pusk'] == top_pusk].groupby('jenis_penyakit')['totall'].sum().idxmax() | |
| return f"- **{top_pusk}** menjadi puskesmas dengan kontribusi kasus tertinggi, yaitu **{int(top_pusk_cases):,}** kasus ({top_pusk_percent}% dari total).\n- Penyakit yang paling banyak disumbangkan oleh {top_pusk} adalah **{top_disease_by_pusk}**." | |
| def generate_gender_analysis_text(total_lk, total_pr): | |
| if total_lk == 0 and total_pr == 0: return "Tidak ada data gender untuk dianalisis." | |
| total_kasus = total_lk + total_pr | |
| persen_lk = (total_lk / total_kasus * 100) if total_kasus > 0 else 0 | |
| persen_pr = (total_pr / total_kasus * 100) if total_kasus > 0 else 0 | |
| if persen_lk > persen_pr: kesimpulan = f"lebih banyak menyerang **laki-laki** ({persen_lk:.1f}%)" | |
| elif persen_pr > persen_lk: kesimpulan = f"lebih banyak menyerang **perempuan** ({persen_pr:.1f}%)" | |
| else: kesimpulan = f"memiliki distribusi yang **seimbang**" | |
| return f"Dari total **{int(total_kasus):,}** kasus, penyakit {kesimpulan}." | |
| def generate_age_analysis_text(baru_data, lama_data): | |
| total_kasus_umur = sum(baru_data.values()) + sum(lama_data.values()) | |
| if total_kasus_umur == 0: return "Tidak ada data umur untuk dianalisis." | |
| total_per_kelompok = {k: baru_data.get(k, 0) + lama_data.get(k, 0) for k in set(baru_data) | set(lama_data)} | |
| if not total_per_kelompok: return "Tidak ada data umur untuk dianalisis." | |
| max_total_kelompok = max(total_per_kelompok, key=total_per_kelompok.get) | |
| return f"Kelompok umur dengan total kasus tertinggi adalah **{max_total_kelompok}** ({int(total_per_kelompok[max_total_kelompok]):,} kasus)." | |
| # ----------------------------------------------------------------------------- | |
| # BAGIAN 2: LAYOUT HALAMAN (Tidak Berubah) | |
| # ----------------------------------------------------------------------------- | |
| layout = dbc.Container([ | |
| dcc.Store(id='laporan-data-store'), | |
| dcc.Download(id="laporan-download-pdf"), | |
| dcc.Download(id="laporan-download-excel"), | |
| dbc.Row([ | |
| dbc.Col(html.H3("Laporan Analisis Terpadu", className="mt-4 mb-4"), md=9), | |
| dbc.Col(dbc.Button("Unduh Laporan (PDF)", id="laporan-btn-unduh-pdf", color="primary", className="mt-4 float-end", disabled=not PDF_CAPABLE), md=3) | |
| ], align="center"), | |
| dbc.Card(dbc.CardBody([dbc.Row([ | |
| dbc.Col([dbc.Label("Pilih Puskesmas:"), dcc.Dropdown(id='laporan-pusk-filter', multi=True, placeholder="Pilih...")], md=4), | |
| dbc.Col([dbc.Label("Pilih Tahun:"), dcc.Dropdown(id='laporan-tahun-filter', multi=True, placeholder="Pilih...")], md=4), | |
| dbc.Col([dbc.Label("Pilih Bulan (Opsional):"), dcc.Dropdown(id='laporan-bulan-filter', multi=True, placeholder="Pilih Tahun dulu...", disabled=True)], md=4) | |
| ])]), className="mb-3 shadow-sm"), | |
| html.Div(id='laporan-filter-summary-text', className="text-center text-muted fst-italic mb-3"), | |
| dcc.Loading(id="laporan-loading-main", type="dot", children=[ | |
| dbc.Tabs(id="laporan-tabs", active_tab='tab-tren', children=[ | |
| dbc.Tab(label="Analisis Tren Penyakit", tab_id="tab-tren", children=html.Div(id='laporan-tab-content-tren')), | |
| dbc.Tab(label="Analisis Demografi", tab_id="tab-demografi", children=html.Div(id='laporan-tab-content-demografi')), | |
| ]) | |
| ]), | |
| dbc.Card(dbc.CardBody([ | |
| html.H5("Unduh Data Mentah ke Excel", className="card-title"), | |
| html.P("Filter utama di atas akan diterapkan. Pilih filter penyakit tambahan di bawah jika perlu.", className="card-text"), | |
| dbc.Row([ | |
| dbc.Col([dbc.Label("Pilih Penyakit (Opsional):"), dcc.Dropdown(id='laporan-unduh-penyakit-filter', multi=True, placeholder="Ketik untuk mencari...")], md=8), | |
| dbc.Col([html.Div(dbc.Button("Unduh Excel", id='laporan-btn-unduh-excel', color="success", className="w-100"), style={'paddingTop': '31px'})], md=4) | |
| ]) | |
| ]), className="my-4 shadow-sm") | |
| ], fluid=True) | |
| # ----------------------------------------------------------------------------- | |
| # BAGIAN 3: CALLBACKS (Tidak Berubah) | |
| # ----------------------------------------------------------------------------- | |
| def laporan_load_main_filters(pathname): | |
| if pathname != '/laporan': return no_update, no_update, no_update | |
| try: | |
| with engine.connect() as conn: | |
| pusk_options = [{'label': p[0], 'value': p[0]} for p in conn.execute(select(distinct(detail_penyakit.c.kode_pusk)).order_by(detail_penyakit.c.kode_pusk)).fetchall() if p[0]] | |
| tahun_options = [{'label': str(t[0]), 'value': t[0]} for t in conn.execute(select(distinct(detail_penyakit.c.tahun)).order_by(detail_penyakit.c.tahun.desc())).fetchall() if t[0]] | |
| penyakit_options = [{'label': f"{p.jenis_penyakit} ({p.icd_x})", 'value': p.jenis_penyakit} for p in conn.execute(select(distinct(detail_penyakit.c.jenis_penyakit), detail_penyakit.c.icd_x).where(detail_penyakit.c.jenis_penyakit.isnot(None)).order_by(detail_penyakit.c.jenis_penyakit)).fetchall()] | |
| return pusk_options, tahun_options, penyakit_options | |
| except Exception as e: print(f"Error load filter laporan: {e}"); return [], [], [] | |
| def laporan_update_bulan_filter(selected_tahun): | |
| if not selected_tahun: return [], True, [] | |
| nama_bulan = {'01':'Januari','02':'Februari','03':'Maret','04':'April','05':'Mei','06':'Juni','07':'Juli','08':'Agustus','09':'September','10':'Oktober','11':'November','12':'Desember'} | |
| try: | |
| with engine.connect() as conn: | |
| stmt = select(distinct(detail_penyakit.c.bulan)).where(detail_penyakit.c.tahun.in_(selected_tahun)).order_by(detail_penyakit.c.bulan) | |
| bulan_list = [row[0] for row in conn.execute(stmt).fetchall() if row[0]] | |
| bulan_options = [{'label': nama_bulan.get(b, b), 'value': b} for b in bulan_list] | |
| return bulan_options, False, [] | |
| except Exception as e: print(f"Error load bulan filter laporan: {e}"); return [], True, [] | |
| ### CALLBACK UTAMA YANG DIGABUNGKAN ### | |
| def update_laporan_terpadu_tabs(selected_pusk, selected_tahun, selected_bulan): | |
| # Logika pengambilan dan pemrosesan data awal tidak berubah | |
| if not selected_pusk or not selected_tahun: | |
| msg = html.P("Silakan pilih minimal Puskesmas dan Tahun.", className="text-center text-primary mt-5") | |
| return msg, msg, "", True, None | |
| filter_summary_text = get_filter_text(selected_pusk, selected_tahun, selected_bulan) | |
| base_filters = [detail_penyakit.c.kode_pusk.in_(selected_pusk), detail_penyakit.c.tahun.in_(selected_tahun)] | |
| if selected_bulan: base_filters.append(detail_penyakit.c.bulan.in_(selected_bulan)) | |
| all_cols = ['jenis_penyakit', 'icd_x', 'tahun', 'bulan', 'kode_pusk', 'laki_laki', 'perempuan', 'usia_0_7_hr_baru', 'usia_0_7_hr_lama', 'usia_8_28_hr_baru', 'usia_8_28_hr_lama', 'usia_1bl_1th_baru', 'usia_1bl_1th_lama', 'usia_1_4th_baru', 'usia_1_4th_lama', 'usia_5_9th_baru', 'usia_5_9th_lama', 'usia_10_14th_baru', 'usia_10_14th_lama', 'usia_15_19th_baru', 'usia_15_19th_lama', 'usia_20_44th_baru', 'usia_20_44th_lama', 'usia_45_54th_baru', 'usia_45_54th_lama', 'usia_55_59th_baru', 'usia_55_59th_lama', 'usia_60_69th_baru', 'usia_60_69th_lama', 'usia_70pl_baru', 'usia_70pl_lama', 'totall'] | |
| stmt = select(*[getattr(detail_penyakit.c, col) for col in all_cols]).where(and_(*base_filters)) | |
| with engine.connect() as conn: | |
| df_base = pd.read_sql(stmt, conn) | |
| if df_base.empty: | |
| msg = dbc.Alert("Tidak ada data ditemukan untuk kriteria yang dipilih.", color="warning", className="m-4") | |
| return msg, msg, filter_summary_text, True, None | |
| # Pembuatan semua 9 grafik + 1 tabel (tidak berubah) | |
| kode_dihindari = ('V', 'W', 'X', 'Y', 'Z') | |
| df_base['icd_x_str'] = df_base['icd_x'].astype(str).str.strip().str.upper() | |
| df_filtered_icd = df_base[~df_base['icd_x_str'].str.startswith(kode_dihindari, na=False)].copy() | |
| df_filtered_icd['kategori'] = df_filtered_icd['icd_x'].apply(kategori_penyakit_atp) | |
| df_ranking_total = df_filtered_icd.groupby('jenis_penyakit')['totall'].sum().nlargest(10).sort_values().reset_index() | |
| top_10_penyakit_list = df_ranking_total['jenis_penyakit'].tolist() | |
| df_top10_base = df_filtered_icd[df_filtered_icd['jenis_penyakit'].isin(top_10_penyakit_list)].copy() | |
| fig_ranking_simple = px.bar(df_ranking_total, x='totall', y='jenis_penyakit', orientation='h', template='plotly_white', title='<b>Peringkat 10 Penyakit Teratas</b>') | |
| table_ranking = create_ranking_table(df_ranking_total) | |
| analysis_ranking = create_ranking_analysis_text(df_ranking_total) | |
| df_category_pie = df_filtered_icd.groupby('kategori')['totall'].sum().reset_index() | |
| fig_category_pie = px.pie(df_category_pie, values='totall', names='kategori', title='<b>Komposisi Menular vs Tidak Menular</b>', color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'}) | |
| analysis_pie = create_pie_analysis_text(df_category_pie) | |
| df_yearly_data = df_top10_base.groupby(['tahun', 'jenis_penyakit'])['totall'].sum().reset_index() | |
| df_yearly_data['tahun'] = df_yearly_data['tahun'].astype(str) | |
| fig_bar_trend_yearly = px.bar(df_yearly_data, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Kasus Tahunan (Top 10)</b>", template='plotly_white') | |
| fig_bar_trend_yearly.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| fig_line_trend_yearly = px.line(df_yearly_data, x='tahun', y='totall', color='jenis_penyakit', markers=True, title="<b>Tren Tahunan (Top 10)</b>") | |
| analysis_yearly_trend = create_trend_analysis_text(df_yearly_data, 'tahun') | |
| df_monthly_line = df_top10_base.groupby(['tahun', 'bulan', 'jenis_penyakit'])['totall'].sum().reset_index() | |
| df_monthly_line['periode'] = df_monthly_line['tahun'].astype(str) + '-' + df_monthly_line['bulan'].str.zfill(2) | |
| fig_line_monthly = px.line(df_monthly_line.sort_values('periode'), x='periode', y='totall', color='jenis_penyakit', title="<b>Tren Bulanan (Top 10)</b>") | |
| analysis_monthly_trend = create_trend_analysis_text(df_monthly_line, 'periode') | |
| df_monthly_cat_trend = df_filtered_icd.groupby(['tahun', 'bulan', 'kategori'])['totall'].sum().reset_index() | |
| df_monthly_cat_trend['periode'] = df_monthly_cat_trend['tahun'].astype(str) + '-' + df_monthly_cat_trend['bulan'].str.zfill(2) | |
| fig_monthly_compare_trend = px.area(df_monthly_cat_trend.sort_values('periode'), x='periode', y='totall', color='kategori', title="<b>Tren Bulanan Kasus per Kategori</b>", color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'}) | |
| df_pusk_compare = df_top10_base.groupby(['kode_pusk', 'jenis_penyakit'])['totall'].sum().reset_index() | |
| fig_pusk_stacked = px.bar(df_pusk_compare, x='totall', y='jenis_penyakit', color='kode_pusk', orientation='h', title='<b>Kontribusi Kasus per Puskesmas</b>') | |
| fig_pusk_stacked.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| analysis_pusk_comparison = create_comparison_analysis_text(df_pusk_compare) | |
| df_menular = df_filtered_icd[df_filtered_icd['kategori'] == 'Menular']; top_10_menular = df_menular.groupby('jenis_penyakit')['totall'].sum().nlargest(10).index | |
| df_trend_menular = df_menular[df_menular['jenis_penyakit'].isin(top_10_menular)].groupby(['tahun', 'jenis_penyakit'])['totall'].sum().reset_index(); df_trend_menular['tahun'] = df_trend_menular['tahun'].astype(str) | |
| fig_trend_menular = px.bar(df_trend_menular, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Tahunan (10 Penyakit Menular Teratas)</b>", template='plotly_white'); fig_trend_menular.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| df_tidak_menular = df_filtered_icd[df_filtered_icd['kategori'] == 'Tidak Menular']; top_10_tidak_menular = df_tidak_menular.groupby('jenis_penyakit')['totall'].sum().nlargest(10).index | |
| df_trend_tidak_menular = df_tidak_menular[df_tidak_menular['jenis_penyakit'].isin(top_10_tidak_menular)].groupby(['tahun', 'jenis_penyakit'])['totall'].sum().reset_index(); df_trend_tidak_menular['tahun'] = df_trend_tidak_menular['tahun'].astype(str) | |
| fig_trend_tidak_menular = px.bar(df_trend_tidak_menular, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Tahunan (10 Penyakit Tidak Menular Teratas)</b>", template='plotly_white'); fig_trend_tidak_menular.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| # ================================================================= | |
| # <<< INI ADALAH BAGIAN YANG DIREVISI SESUAI PERMINTAAN ANDA >>> | |
| # ================================================================= | |
| # --- Menyusun Layout Baru untuk Tab 1 (Satu Visualisasi per Baris) --- | |
| tab1_content = html.Div([ | |
| # 1. Grafik Peringkat dan Tabel | |
| dbc.Row(dbc.Col(dcc.Graph(figure=fig_ranking_simple), md=12), className="mb-2"), | |
| dbc.Row(dbc.Col([html.H5("Tabel Peringkat 10 Besar"), table_ranking, dbc.Card(dbc.CardBody(dcc.Markdown(analysis_ranking)), className="mt-3")], md=12)), | |
| html.Hr(className="my-4"), | |
| # 2. Grafik Pie Komposisi | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_category_pie), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_pie)), className="mt-2")], md=12)), | |
| html.Hr(className="my-4"), | |
| # 3. Grafik Perbandingan Tahunan (Batang) | |
| dbc.Row(dbc.Col(dcc.Graph(figure=fig_bar_trend_yearly), md=12)), | |
| html.Hr(className="my-4"), | |
| # 4. Grafik Tren Tahunan (Garis) | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_line_trend_yearly), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_yearly_trend)), className="mt-2")], md=12)), | |
| html.Hr(className="my-4"), | |
| # 5. Grafik Tren Bulanan (Garis) | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_line_monthly), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_monthly_trend)), className="mt-2")], md=12)), | |
| html.Hr(className="my-4"), | |
| # 6. Grafik Tren Bulanan per Kategori (Area) | |
| dbc.Row(dbc.Col(dcc.Graph(figure=fig_monthly_compare_trend), md=12)), | |
| html.Hr(className="my-4"), | |
| # 7. Grafik Kontribusi Puskesmas | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_pusk_stacked), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_pusk_comparison)), className="mt-2")], md=12)), | |
| html.Hr(className="my-4"), | |
| # 8 & 9. Grafik Perbandingan Tahunan per Kategori | |
| html.H4("Analisis Detail Berdasarkan Kategori Penyakit", className="text-center my-4"), | |
| dbc.Row(dbc.Col(dcc.Graph(figure=fig_trend_menular), md=12), className="mb-4"), | |
| dbc.Row(dbc.Col(dcc.Graph(figure=fig_trend_tidak_menular), md=12)), | |
| ], className="mt-3") | |
| # BAGIAN B: Proses Data untuk TAB 2 (Analisis Demografi) - Tidak Berubah | |
| total_lk = df_base['laki_laki'].sum() | |
| total_pr = df_base['perempuan'].sum() | |
| fig_gender = go.Figure(data=[go.Pie(labels=['Laki-laki', 'Perempuan'], values=[total_lk, total_pr], hole=.4, marker_colors=['royalblue', 'crimson'])]) | |
| fig_gender.update_layout(title_text='<b>Distribusi Kasus Berdasarkan Jenis Kelamin</b>', template='plotly_white') | |
| analysis_gender = generate_gender_analysis_text(total_lk, total_pr) | |
| kelompok_map = {'Bayi & Balita (<5th)':['usia_0_7_hr_baru','usia_0_7_hr_lama','usia_8_28_hr_baru','usia_8_28_hr_lama','usia_1bl_1th_baru','usia_1bl_1th_lama','usia_1_4th_baru','usia_1_4th_lama'],'Anak (5-9th)':['usia_5_9th_baru','usia_5_9th_lama'],'Remaja (10-19th)':['usia_10_14th_baru','usia_10_14th_lama','usia_15_19th_baru','usia_15_19th_lama'],'Dewasa (20-59th)':['usia_20_44th_baru','usia_20_44th_lama','usia_45_54th_baru','usia_45_54th_lama','usia_55_59th_baru','usia_55_59th_lama'],'Lansia (60+th)':['usia_60_69th_baru','usia_60_69th_lama','usia_70pl_baru','usia_70pl_lama']} | |
| age_new_data, age_old_data = {}, {} | |
| for kelompok, cols in kelompok_map.items(): | |
| age_new_data[kelompok] = df_base[[c for c in cols if 'baru' in c]].sum().sum() | |
| age_old_data[kelompok] = df_base[[c for c in cols if 'lama' in c]].sum().sum() | |
| fig_age = go.Figure(data=[go.Bar(name='Kasus Baru', x=list(age_new_data.keys()), y=list(age_new_data.values())), go.Bar(name='Kasus Lama', x=list(age_old_data.keys()), y=list(age_old_data.values()))]).update_layout(barmode='group', title_text="<b>Distribusi Kasus Berdasarkan Kelompok Umur</b>", template='plotly_white') | |
| analysis_age = generate_age_analysis_text(age_new_data, age_old_data) | |
| tab2_content = html.Div([dbc.Row([dbc.Col(dcc.Graph(figure=fig_gender), md=6),dbc.Col(dcc.Graph(figure=fig_age), md=6),dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_gender))), md=6),dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_age))), md=6)], className="mb-4")], className="mt-3") | |
| # Penyimpanan data ke dcc.Store tidak berubah | |
| data_to_store = {'figs_json': {'ranking_simple': fig_ranking_simple.to_json(), 'category_pie': fig_category_pie.to_json(),'bar_trend_yearly': fig_bar_trend_yearly.to_json(),'line_trend_yearly': fig_line_trend_yearly.to_json(),'line_trend_monthly': fig_line_monthly.to_json(),'monthly_compare_trend': fig_monthly_compare_trend.to_json(),'pusk_stacked': fig_pusk_stacked.to_json(),'trend_menular': fig_trend_menular.to_json(),'trend_tidak_menular': fig_trend_tidak_menular.to_json(),'gender_pie': fig_gender.to_json(),'age_bar': fig_age.to_json(),},'table_data': {'ranking': df_ranking_total.to_dict('records')},'analysis_texts': {'ranking': analysis_ranking, 'pie': analysis_pie,'yearly_trend': analysis_yearly_trend,'monthly_trend': analysis_monthly_trend,'pusk_comparison': analysis_pusk_comparison,'gender': analysis_gender, 'age': analysis_age,},'filter_text': filter_summary_text,} | |
| return tab1_content, tab2_content, filter_summary_text, not PDF_CAPABLE, data_to_store | |
| # Callback PDF dan Excel tidak perlu diubah, karena mereka mengambil dari dcc.Store | |
| # ... (sisa kode sama seperti sebelumnya) ... | |
| def download_laporan_as_pdf(n_clicks, stored_data): | |
| if not n_clicks or not stored_data or not PDF_CAPABLE: return no_update | |
| buffer = io.BytesIO() | |
| doc = SimpleDocTemplate(buffer, pagesize=(8.5*inch, 11*inch), rightMargin=0.5*inch, leftMargin=0.5*inch, topMargin=0.5*inch, bottomMargin=0.5*inch) | |
| styles = getSampleStyleSheet() | |
| style_h1 = ParagraphStyle(name='H1', parent=styles['h1'], alignment=TA_CENTER, fontSize=16, spaceAfter=14) | |
| style_h2 = ParagraphStyle(name='H2', parent=styles['h2'], alignment=TA_LEFT, fontSize=14, spaceBefore=20, spaceAfter=6, textColor=reportlab_colors.HexColor("#1A3A69")) | |
| style_body = styles['BodyText']; style_body.leading = 14 | |
| def fig_to_image(fig_json): | |
| if not fig_json: return Spacer(1, 0.1 * inch) | |
| fig = go.Figure(pio.from_json(fig_json)); fig.update_layout(margin=dict(l=20, r=20, t=50, b=20), title_x=0.5) | |
| img_bytes = pio.to_image(fig, format="png", width=800, height=450, scale=2) | |
| return Image(io.BytesIO(img_bytes), width=7*inch, height=(7*450/800)*inch) | |
| def text_to_paragraph(text_markdown): | |
| if not isinstance(text_markdown, str): return Paragraph("Analisis tidak tersedia.", style_body) | |
| parts = text_markdown.replace('\n', '<br/>').split('**') | |
| for i in range(1, len(parts), 2): parts[i] = f"<b>{parts[i]}</b>" | |
| return Paragraph("".join(parts), style_body) | |
| def create_pdf_table(table_data_records): | |
| if not table_data_records: return Spacer(1, 0.1*inch) | |
| headers = ['Jenis Penyakit', 'Total Kasus']; data = [headers] | |
| for row in table_data_records: data.append([row.get('jenis_penyakit', ''), f"{int(row.get('totall', 0)):,}"]) | |
| table = Table(data, colWidths=[5.5*inch, 1.5*inch]) | |
| table.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,0), reportlab_colors.HexColor("#4682B4")),('TEXTCOLOR',(0,0),(-1,0), reportlab_colors.whitesmoke),('ALIGN', (0,0), (-1,-1), 'LEFT'),('VALIGN', (0,0), (-1,-1), 'MIDDLE'),('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),('BOTTOMPADDING', (0,0), (-1,0), 12),('BACKGROUND', (0,1), (-1,-1), reportlab_colors.HexColor("#F0F8FF")),('GRID', (0,0), (-1,-1), 1, reportlab_colors.black),('ROWBACKGROUNDS', (0,1), (-1,-1), [reportlab_colors.HexColor("#F0F8FF"), reportlab_colors.white])])) | |
| return table | |
| story = [Paragraph("Laporan Analisis Terpadu", style_h1), Paragraph(stored_data.get('filter_text', ''), styles['Italic']), Spacer(1, 0.3*inch)] | |
| analysis = stored_data.get('analysis_texts', {}); figs = stored_data.get('figs_json', {}); tables = stored_data.get('table_data', {}) | |
| story.append(Paragraph("BAGIAN 1: ANALISIS TREN PENYAKIT", style_h2)) | |
| story.append(fig_to_image(figs.get('ranking_simple'))); story.append(create_pdf_table(tables.get('ranking'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('ranking'))); story.append(PageBreak()) | |
| story.append(fig_to_image(figs.get('category_pie'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('pie'))); story.append(Spacer(1, 0.2*inch)) | |
| story.append(fig_to_image(figs.get('bar_trend_yearly'))); story.append(PageBreak()) | |
| story.append(fig_to_image(figs.get('line_trend_yearly'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('yearly_trend'))); story.append(Spacer(1, 0.2*inch)) | |
| story.append(fig_to_image(figs.get('line_trend_monthly'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('monthly_trend'))); story.append(PageBreak()) | |
| story.append(fig_to_image(figs.get('monthly_compare_trend'))); story.append(Spacer(1, 0.2*inch)) | |
| story.append(fig_to_image(figs.get('pusk_stacked'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('pusk_comparison'))); story.append(PageBreak()) | |
| story.append(fig_to_image(figs.get('trend_menular'))); story.append(Spacer(1, 0.2*inch)) | |
| story.append(fig_to_image(figs.get('trend_tidak_menular'))); story.append(PageBreak()) | |
| story.append(Paragraph("BAGIAN 2: ANALISIS DEMOGRAFI", style_h2)) | |
| story.append(fig_to_image(figs.get('gender_pie'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('gender'))) | |
| story.append(Spacer(1, 0.3*inch)); story.append(fig_to_image(figs.get('age_bar'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('age'))) | |
| doc.build(story); return dcc.send_bytes(buffer.getvalue(), "Laporan_Terpadu_Penyakit.pdf") | |
| def download_data_as_excel(n_clicks, pusk, tahun, bulan, penyakit): | |
| if not n_clicks: return no_update | |
| filters = [] | |
| if pusk: filters.append(detail_penyakit.c.kode_pusk.in_(pusk)) | |
| if tahun: filters.append(detail_penyakit.c.tahun.in_(tahun)) | |
| if bulan: filters.append(detail_penyakit.c.bulan.in_(bulan)) | |
| if penyakit: filters.append(detail_penyakit.c.jenis_penyakit.in_(penyakit)) | |
| if not filters: return no_update | |
| stmt = select(detail_penyakit).where(and_(*filters)) | |
| with engine.connect() as conn: df_to_download = pd.read_sql(stmt, conn) | |
| if df_to_download.empty: return no_update | |
| output = io.BytesIO() | |
| with pd.ExcelWriter(output, engine='xlsxwriter') as writer: | |
| df_to_download.to_excel(writer, index=False, sheet_name='Data_Penyakit_Terfilter') | |
| return dcc.send_bytes(output.getvalue(), "laporan_data_mentah.xlsx") |