# 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) # ----------------------------------------------------------------------------- @callback( Output('atp-pusk-filter', 'options'), Output('atp-tahun-filter', 'options'), Input('url', 'pathname') ) 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 [], [] @callback( Output('atp-bulan-filter', 'options'), Output('atp-bulan-filter', 'disabled'), Output('atp-bulan-filter', 'value'), Input('atp-tahun-filter', 'value') ) 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 ### @callback( Output('tab-content-ranking', 'children'), Output('tab-content-trend', 'children'), Output('tab-content-comparison', 'children'), Output('atp-filter-summary-text', 'children'), Output('atp-btn-unduh-pdf', 'disabled'), Output('atp-data-store', 'data'), Input('atp-pusk-filter', 'value'), Input('atp-tahun-filter', 'value'), Input('atp-bulan-filter', 'value') ) 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='Peringkat 10 Penyakit Teratas (Keseluruhan)', 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='Proporsi Kasus Penyakit Menular dan Tidak Menular (%)', 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="Perbandingan Kasus Tahunan untuk 10 Penyakit Teratas", 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="Perbandingan Tahunan 10 Penyakit Menular Teratas", 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="Perbandingan Tahunan 10 Penyakit Tidak Menular Teratas", 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="Tren Tahunan 10 Penyakit Teratas (Keseluruhan)"); 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="Tren Bulanan 10 Penyakit Teratas"); 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="Perbandingan Tren Bulanan Kasus Menular dan Tidak Menular", 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='Kontribusi Kasus per Puskesmas', 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 ### @callback( Output("atp-download-pdf", "data"), Input("atp-btn-unduh-pdf", "n_clicks"), State("atp-data-store", "data"), prevent_initial_call=True ) 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("Gagal memuat gambar.", 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', '
') # Ganti markdown bold (**) dengan tag ... secara berpasangan parts = text_html.split('**') for i in range(1, len(parts), 2): parts[i] = f"{parts[i]}" 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")