# 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")