Spaces:
Sleeping
Sleeping
| # File: pages/analisis_tren_penyakit.py (Revisi Final dengan Perbaikan Parser PDF) | |
| 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 Anda dari file database.py | |
| from database import engine, detail_penyakit | |
| # Impor library untuk pembuatan PDF, dengan fallback jika tidak terinstall | |
| 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 ada perubahan) | |
| # ----------------------------------------------------------------------------- | |
| 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 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 = "Seluruh Bulan" if not bulan else ", ".join(sorted(bulan)) | |
| return f"Menampilkan data dari: {pusk_txt} | Tahun: {tahun_txt} | Bulan: {bulan_txt}" | |
| def create_ranking_analysis_text(df): | |
| if df.empty: return "Tidak ada data untuk dianalisis." | |
| top_disease = df.iloc[-1] | |
| bottom_disease = df.iloc[0] | |
| return dcc.Markdown(f""" | |
| - Penyakit dengan kasus tertinggi adalah **{top_disease['jenis_penyakit']}** dengan total **{int(top_disease['totall']):,}** kasus. | |
| - Penyakit peringkat ke-10 dalam daftar ini adalah **{bottom_disease['jenis_penyakit']}** dengan **{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 untuk dianalisis." | |
| total_cases = df['totall'].sum() | |
| if total_cases == 0: return "Total kasus adalah nol, tidak ada persentase untuk dihitung." | |
| 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 dibandingkan" | |
| return dcc.Markdown(f"""- Dari total kasus yang ada, **{perc_menular}%** merupakan penyakit **Menular**, sementara **{perc_tidak_menular}%** adalah penyakit **Tidak Menular**.\n- Kasus penyakit Menular **{kesimpulan}** kasus penyakit Tidak Menular pada periode ini.""") | |
| def create_trend_analysis_text(df, time_unit='tahun'): | |
| if df.empty: return dcc.Markdown("Data tidak cukup untuk analisis tren.") | |
| if time_unit == 'tahun' and df['tahun'].nunique() < 2: return dcc.Markdown("Analisis tren membutuhkan data dari minimal 2 tahun.") | |
| top_3_diseases = df.groupby('jenis_penyakit')['totall'].sum().nlargest(3).index.tolist() | |
| if not top_3_diseases: return dcc.Markdown("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"mengalami **kenaikan** dari {int(start_val):,} menjadi {int(end_val):,}" | |
| elif end_val < start_val: tren = f"mengalami **penurunan** dari {int(start_val):,} menjadi {int(end_val):,}" | |
| else: tren = f"**stabil** di angka {int(end_val):,}" | |
| analysis_points.append(f"- Kasus **{disease}** {tren} sepanjang periode.") | |
| else: analysis_points.append(f"- Kasus **{disease}** hanya memiliki data untuk satu periode, tren tidak dapat dihitung.") | |
| return dcc.Markdown("Ringkasan Tren untuk 3 Penyakit Teratas:\n\n" + "\n".join(analysis_points)) | |
| def create_category_trend_analysis_text(df): | |
| if df.empty or len(df['periode'].unique()) < 2: return dcc.Markdown("Data tidak cukup untuk analisis tren kategori.") | |
| analysis_points = [] | |
| for category in ['Menular', 'Tidak Menular']: | |
| df_cat = df[df['kategori'] == category].sort_values('periode') | |
| if not df_cat.empty and len(df_cat) > 1: | |
| start_val, end_val = df_cat['totall'].iloc[0], df_cat['totall'].iloc[-1] | |
| if end_val > start_val: tren = f"cenderung **naik** dari {int(start_val):,} menjadi {int(end_val):,}" | |
| elif end_val < start_val: tren = f"cenderung **turun** dari {int(start_val):,} menjadi {int(end_val):,}" | |
| else: tren = f"**stabil** di sekitar angka {int(end_val):,}" | |
| analysis_points.append(f"- Tren kasus **{category}** {tren} selama periode ini.") | |
| if not analysis_points: return dcc.Markdown("Tidak dapat menganalisis tren kategori.") | |
| return dcc.Markdown("\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 dcc.Markdown(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 create_yearly_bar_analysis_text(df): | |
| df_copy = df.copy(); df_copy['tahun'] = pd.to_numeric(df_copy['tahun']) | |
| if df_copy.empty or 'tahun' not in df_copy.columns or df_copy['tahun'].nunique() < 2: return dcc.Markdown("Data tidak cukup untuk membandingkan tren antar tahun.") | |
| df_pivot = df_copy.pivot(index='jenis_penyakit', columns='tahun', values='totall').fillna(0); df_pivot['perubahan'] = df_pivot.iloc[:, -1] - df_pivot.iloc[:, 0] | |
| penyakit_naik_terbesar, kenaikan = df_pivot['perubahan'].idxmax(), df_pivot['perubahan'].max() | |
| penyakit_turun_terbesar, penurunan = df_pivot['perubahan'].idxmin(), df_pivot['perubahan'].min() | |
| analysis_points = [] | |
| if kenaikan > 0: analysis_points.append(f"- Penyakit dengan **pertumbuhan kasus terbesar** adalah **{penyakit_naik_terbesar}**, bertambah **{int(kenaikan):,}** kasus.") | |
| if penurunan < 0: analysis_points.append(f"- Penyakit dengan **penurunan paling signifikan** adalah **{penyakit_turun_terbesar}**, berkurang **{int(abs(penurunan)):,}** kasus.") | |
| top_disease_last_year = df_copy[df_copy['tahun'] == df_copy['tahun'].max()].nlargest(1, 'totall')['jenis_penyakit'].iloc[0] | |
| analysis_points.append(f"- Pada tahun terakhir ({int(df_copy['tahun'].max())}), **{top_disease_last_year}** menjadi penyakit dengan kasus terbanyak di antara 10 penyakit ini.") | |
| if not analysis_points: return dcc.Markdown("Tidak dapat menghasilkan analisis tren dari data yang ada.") | |
| return dcc.Markdown("\n".join(analysis_points)) | |
| # ----------------------------------------------------------------------------- | |
| # BAGIAN 2: LAYOUT HALAMAN (Tidak ada perubahan) | |
| # ----------------------------------------------------------------------------- | |
| layout = dbc.Container([ | |
| dcc.Store(id='atp-data-store'), | |
| dcc.Download(id="atp-download-pdf"), | |
| dbc.Row([ | |
| dbc.Col(html.H3("Analisis Penyakit Komprehensif", className="mt-4 mb-4"), md=9), | |
| dbc.Col(dbc.Button("Unduh Laporan (PDF)", id="atp-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='atp-pusk-filter', multi=True, placeholder="Pilih...")], md=4), | |
| dbc.Col([dbc.Label("Pilih Tahun:"), dcc.Dropdown(id='atp-tahun-filter', multi=True, placeholder="Pilih...")], md=4), | |
| dbc.Col([dbc.Label("Pilih Bulan:"), dcc.Dropdown(id='atp-bulan-filter', multi=True, placeholder="Pilih Tahun dulu...", disabled=True)], md=4) | |
| ])]), className="mb-3 shadow-sm"), | |
| html.Div(id='atp-filter-summary-text', className="text-center text-muted fst-italic mb-3"), | |
| dcc.Loading(id="atp-loading-main", type="dot", children=[ | |
| dbc.Tabs(id="atp-tabs", active_tab='tab-ranking', children=[ | |
| dbc.Tab(label="Ringkasan Peringkat", tab_id="tab-ranking", children=html.Div(id='tab-content-ranking')), | |
| dbc.Tab(label="Analisis Tren", tab_id="tab-trend", children=html.Div(id='tab-content-trend')), | |
| dbc.Tab(label="Perbandingan Puskesmas", tab_id="tab-comparison", children=html.Div(id='tab-content-comparison')) | |
| ]) | |
| ]), | |
| ], fluid=True) | |
| # ----------------------------------------------------------------------------- | |
| # BAGIAN 3: CALLBACKS (Tidak ada perubahan di luar callback utama dan PDF) | |
| # ----------------------------------------------------------------------------- | |
| def atp_load_filters(pathname): | |
| if pathname != '/analisis_tren_penyakit': return 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]] | |
| return pusk_options, tahun_options | |
| except Exception as e: print(f"Error saat memuat filter: {e}"); return [], [] | |
| def update_bulan_options(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 saat memuat filter bulan: {e}"); return [], True, [] | |
| ### CALLBACK UTAMA ### | |
| def update_all_content_based_on_filters(selected_pusk, selected_tahun, selected_bulan): | |
| if not selected_pusk or not selected_tahun: | |
| msg = html.P("Silakan lengkapi semua filter untuk memulai analisis.", className="text-center text-primary mt-5") | |
| return msg, msg, msg, "", True, None | |
| filter_summary_text = get_filter_text(selected_pusk, selected_tahun, selected_bulan) | |
| filters = [detail_penyakit.c.kode_pusk.in_(selected_pusk), detail_penyakit.c.tahun.in_(selected_tahun)] | |
| if selected_bulan: filters.append(detail_penyakit.c.bulan.in_(selected_bulan)) | |
| stmt = select(detail_penyakit.c.jenis_penyakit, detail_penyakit.c.icd_x, detail_penyakit.c.tahun, detail_penyakit.c.bulan, detail_penyakit.c.kode_pusk, func.sum(detail_penyakit.c.totall).label('totall')).where(and_(*filters)).group_by(detail_penyakit.c.jenis_penyakit, detail_penyakit.c.icd_x, detail_penyakit.c.tahun, detail_penyakit.c.bulan, detail_penyakit.c.kode_pusk) | |
| 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, msg, filter_summary_text, True, None | |
| 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)] | |
| # --- KONTEN TAB 1 --- | |
| fig_ranking_simple = px.bar(df_ranking_total, x='totall', y='jenis_penyakit', orientation='h', template='plotly_white', title='<b>Peringkat 10 Penyakit Teratas (Keseluruhan)</b>', labels={'totall': 'Total Kasus', 'jenis_penyakit': ''}) | |
| analysis_ranking = create_ranking_analysis_text(df_ranking_total) | |
| table_ranking = create_ranking_table(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>Proporsi Kasus Penyakit Menular dan 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 untuk 10 Penyakit Teratas</b>", labels={'totall': 'Total Kasus', 'jenis_penyakit': '', 'tahun': 'Tahun'}, template='plotly_white') | |
| fig_bar_trend_yearly.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| analysis_yearly_bar = create_yearly_bar_analysis_text(df_yearly_data) | |
| 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', 'icd_x'])['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>", labels={'jenis_penyakit': '', 'totall': 'Total Kasus', 'tahun': 'Tahun'}, template='plotly_white', hover_data={'icd_x': True}); fig_trend_menular.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| analysis_menular = create_trend_analysis_text(df_trend_menular, 'tahun') | |
| 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', 'icd_x'])['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>", labels={'jenis_penyakit': '', 'totall': 'Total Kasus', 'tahun': 'Tahun'}, template='plotly_white', hover_data={'icd_x': True}); fig_trend_tidak_menular.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| analysis_tidak_menular = create_trend_analysis_text(df_trend_tidak_menular, 'tahun') | |
| tab1_content = html.Div([ | |
| dbc.Row(dbc.Col([ | |
| dcc.Graph(figure=fig_ranking_simple), | |
| html.H5("Tabel Peringkat 10 Besar", className="mt-4"), | |
| table_ranking, | |
| dbc.Card(dbc.CardBody(analysis_ranking), className="mt-3 mb-4") | |
| ])), | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_bar_trend_yearly), dbc.Card(dbc.CardBody(analysis_yearly_bar), className="mt-2 mb-4")])), | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_category_pie), dbc.Card(dbc.CardBody(analysis_pie), className="mt-2 mb-4")])), | |
| html.Hr(className="my-5"), | |
| html.H4("Analisis Detail Berdasarkan Kategori Penyakit", className="text-center mb-4"), | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_trend_menular), dbc.Card(dbc.CardBody(analysis_menular), className="mt-2 mb-4")])), | |
| dbc.Row(dbc.Col([dcc.Graph(figure=fig_trend_tidak_menular), dbc.Card(dbc.CardBody(analysis_tidak_menular), className="mt-2 mb-4")])) | |
| ]) | |
| # --- KONTEN TAB 2 --- | |
| df_yearly_data_for_line = df_top10_base.groupby(['tahun', 'jenis_penyakit'])['totall'].sum().reset_index(); fig_line_trend_yearly = px.line(df_yearly_data_for_line, x='tahun', y='totall', color='jenis_penyakit', markers=True, title="<b>Tren Tahunan 10 Penyakit Teratas (Keseluruhan)</b>"); analysis_yearly_trend = create_trend_analysis_text(df_yearly_data_for_line, 'tahun') | |
| df_line_data_monthly = df_top10_base.groupby(['tahun', 'bulan', 'jenis_penyakit'])['totall'].sum().reset_index(); df_line_data_monthly['periode'] = df_line_data_monthly['tahun'].astype(str) + '-' + df_line_data_monthly['bulan'].str.zfill(2); df_line_data_monthly.sort_values('periode', inplace=True); fig_line_trend_monthly = px.line(df_line_data_monthly, x='periode', y='totall', color='jenis_penyakit', markers=False, title="<b>Tren Bulanan 10 Penyakit Teratas</b>"); analysis_monthly_trend = create_trend_analysis_text(df_line_data_monthly, '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); df_monthly_cat_trend.sort_values('periode', inplace=True); fig_monthly_compare_trend = px.area(df_monthly_cat_trend, x='periode', y='totall', color='kategori', title="<b>Perbandingan Tren Bulanan Kasus Menular dan Tidak Menular</b>", color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'}); analysis_category_trend = create_category_trend_analysis_text(df_monthly_cat_trend) | |
| tab2_content = html.Div([dbc.Row(dbc.Col([dcc.Graph(figure=fig_line_trend_yearly), dbc.Card(dbc.CardBody(analysis_yearly_trend), className="mt-2 mb-4")])), dbc.Row(dbc.Col([dcc.Graph(figure=fig_line_trend_monthly), dbc.Card(dbc.CardBody(analysis_monthly_trend), className="mt-2 mb-4")])), dbc.Row(dbc.Col([dcc.Graph(figure=fig_monthly_compare_trend), dbc.Card(dbc.CardBody(analysis_category_trend), className="mt-2")]))]) | |
| # --- KONTEN TAB 3 --- | |
| 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', template='plotly_white', title='<b>Kontribusi Kasus per Puskesmas</b>', labels={'totall': 'Total Kasus', 'jenis_penyakit': '', 'kode_pusk': 'Puskesmas'}); analysis_pusk_comparison = create_comparison_analysis_text(df_pusk_compare) | |
| tab3_content = html.Div([dbc.Row(dbc.Col([dcc.Graph(figure=fig_pusk_stacked), dbc.Card(dbc.CardBody(analysis_pusk_comparison), className="mt-2")]))]) | |
| 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(), 'trend_menular': fig_trend_menular.to_json(), | |
| 'trend_tidak_menular': fig_trend_tidak_menular.to_json(), 'line_trend_yearly': fig_line_trend_yearly.to_json(), | |
| 'line_trend_monthly': fig_line_trend_monthly.to_json(), 'monthly_compare_trend': fig_monthly_compare_trend.to_json(), | |
| 'pusk_stacked': fig_pusk_stacked.to_json() | |
| }, | |
| 'analysis_texts': { | |
| 'ranking': analysis_ranking.children, 'pie': analysis_pie.children, | |
| 'yearly_bar': analysis_yearly_bar.children, 'menular': analysis_menular.children, | |
| 'tidak_menular': analysis_tidak_menular.children, 'yearly_trend': analysis_yearly_trend.children, | |
| 'monthly_trend': analysis_monthly_trend.children, 'category_trend': analysis_category_trend.children, | |
| 'pusk_comparison': analysis_pusk_comparison.children | |
| }, | |
| 'table_data': { | |
| 'ranking': df_ranking_total.to_dict('records') | |
| }, | |
| 'filter_text': filter_summary_text | |
| } | |
| return tab1_content, tab2_content, tab3_content, filter_summary_text, not PDF_CAPABLE, data_to_store | |
| ### CALLBACK PDF ### | |
| def download_report_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=12, spaceAfter=6, textColor=reportlab_colors.HexColor("#1A3A69")) | |
| style_h3 = ParagraphStyle(name='H3', parent=styles['h3'], alignment=TA_LEFT, fontSize=11, spaceBefore=10, spaceAfter=4, textColor=reportlab_colors.HexColor("#444444")) | |
| style_body = styles['BodyText'] | |
| style_body.leading = 14 | |
| def fig_to_image(fig_json): | |
| if not fig_json: return Spacer(1, 0.1*inch) | |
| try: | |
| 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) | |
| except Exception as e: | |
| print(f"Error saat membuat gambar PDF: {e}") | |
| return Paragraph("<i>Gagal memuat gambar.</i>", style_body) | |
| # ============================================================================== | |
| # <<< PERBAIKAN UTAMA ADA DI FUNGSI INI >>> | |
| # ============================================================================== | |
| def text_to_paragraph(text_markdown): | |
| if not isinstance(text_markdown, str): | |
| return Paragraph("Analisis tidak tersedia.", style_body) | |
| # Ganti newline dulu | |
| text_html = text_markdown.replace('\n', '<br/>') | |
| # Ganti markdown bold (**) dengan tag <b>...</b> secara berpasangan | |
| parts = text_html.split('**') | |
| for i in range(1, len(parts), 2): | |
| parts[i] = f"<b>{parts[i]}</b>" | |
| final_text = "".join(parts) | |
| return Paragraph(final_text, 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'] # Header manual agar urutan benar | |
| data = [headers] | |
| for row in table_data_records: | |
| # Format angka dengan koma | |
| row_data = [ | |
| row.get('jenis_penyakit', ''), | |
| f"{int(row.get('totall', 0)):,}" | |
| ] | |
| data.append(row_data) | |
| table = Table(data, colWidths=[5.5*inch, 1.5*inch]) | |
| style = 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]) | |
| ]) | |
| table.setStyle(style) | |
| return table | |
| story = [ | |
| Paragraph("Laporan Analisis Tren Penyakit", 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', {}) | |
| # --- HALAMAN 1: RINGKASAN PERINGKAT --- | |
| story.append(Paragraph("1. Ringkasan Peringkat", style_h2)) | |
| story.append(fig_to_image(figs.get('ranking_simple'))) | |
| story.append(Spacer(1, 0.2*inch)) | |
| story.append(Paragraph("Tabel Peringkat 10 Besar", style_h3)) | |
| story.append(create_pdf_table(tables.get('ranking'))) | |
| story.append(Spacer(1, 0.2*inch)) | |
| story.append(text_to_paragraph(analysis.get('ranking', 'Analisis tidak tersedia.'))) | |
| story.append(PageBreak()) | |
| story.append(fig_to_image(figs.get('bar_trend_yearly'))) | |
| story.append(Spacer(1, 0.1*inch)) | |
| story.append(text_to_paragraph(analysis.get('yearly_bar', 'Analisis tidak tersedia.'))) | |
| story.append(PageBreak()) | |
| # --- HALAMAN 2: DETAIL KATEGORI --- | |
| story.append(Paragraph("2. Analisis Berdasarkan Kategori", style_h2)) | |
| story.append(fig_to_image(figs.get('category_pie'))) | |
| story.append(Spacer(1, 0.1*inch)) | |
| story.append(text_to_paragraph(analysis.get('pie', 'Analisis tidak tersedia.'))) | |
| story.append(Spacer(1, 0.3*inch)) | |
| story.append(Paragraph("Tren 10 Penyakit Menular Teratas", style_h3)) | |
| story.append(fig_to_image(figs.get('trend_menular'))) | |
| story.append(Spacer(1, 0.1*inch)) | |
| story.append(text_to_paragraph(analysis.get('menular', 'Analisis tidak tersedia.'))) | |
| story.append(PageBreak()) | |
| story.append(Paragraph("Tren 10 Penyakit Tidak Menular Teratas", style_h3)) | |
| story.append(fig_to_image(figs.get('trend_tidak_menular'))) | |
| story.append(Spacer(1, 0.1*inch)) | |
| story.append(text_to_paragraph(analysis.get('tidak_menular', 'Analisis tidak tersedia.'))) | |
| story.append(PageBreak()) | |
| # --- HALAMAN 3: ANALISIS TREN --- | |
| story.append(Paragraph("3. Analisis Tren", style_h2)) | |
| 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', 'Analisis tidak tersedia.'))) | |
| story.append(Spacer(1, 0.3*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', 'Analisis tidak tersedia.'))) | |
| story.append(PageBreak()) | |
| # --- HALAMAN 4: PERBANDINGAN PUSKESMAS --- | |
| story.append(Paragraph("4. Perbandingan Antar Puskesmas", style_h2)) | |
| 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', 'Analisis tidak tersedia.'))) | |
| doc.build(story) | |
| return dcc.send_bytes(buffer.getvalue(), "Laporan_Analisis_Penyakit_Revisi.pdf") |