# 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) # ----------------------------------------------------------------------------- @callback( Output('laporan-pusk-filter', 'options'), Output('laporan-tahun-filter', 'options'), Output('laporan-unduh-penyakit-filter', 'options'), Input('url', 'pathname') ) 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 [], [], [] @callback( Output('laporan-bulan-filter', 'options'), Output('laporan-bulan-filter', 'disabled'), Output('laporan-bulan-filter', 'value'), Input('laporan-tahun-filter', 'value') ) 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 ### @callback( Output('laporan-tab-content-tren', 'children'), Output('laporan-tab-content-demografi', 'children'), Output('laporan-filter-summary-text', 'children'), Output('laporan-btn-unduh-pdf', 'disabled'), Output('laporan-data-store', 'data'), Input('laporan-pusk-filter', 'value'), Input('laporan-tahun-filter', 'value'), Input('laporan-bulan-filter', 'value') ) 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='Peringkat 10 Penyakit Teratas') 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='Komposisi Menular vs 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 (Top 10)", 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="Tren Tahunan (Top 10)") 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="Tren Bulanan (Top 10)") 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="Tren Bulanan Kasus per Kategori", 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='Kontribusi Kasus per Puskesmas') 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="Perbandingan Tahunan (10 Penyakit Menular Teratas)", 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="Perbandingan Tahunan (10 Penyakit Tidak Menular Teratas)", 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='Distribusi Kasus Berdasarkan Jenis Kelamin', 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="Distribusi Kasus Berdasarkan Kelompok Umur", 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) ... @callback( Output("laporan-download-pdf", "data"), Input("laporan-btn-unduh-pdf", "n_clicks"), State("laporan-data-store", "data"), prevent_initial_call=True ) 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', '
').split('**') for i in range(1, len(parts), 2): parts[i] = f"{parts[i]}" 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") @callback( Output("laporan-download-excel", "data"), Input("laporan-btn-unduh-excel", "n_clicks"), State("laporan-pusk-filter", "value"), State("laporan-tahun-filter", "value"), State("laporan-bulan-filter", "value"), State("laporan-unduh-penyakit-filter", "value"), prevent_initial_call=True, ) 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")