Spaces:
Sleeping
Sleeping
Update pages/laporan.py
Browse files- pages/laporan.py +103 -67
pages/laporan.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# File: pages/laporan.py (Revisi Final
|
| 2 |
|
| 3 |
from dash import dcc, html, Input, Output, callback, no_update, State, dash_table
|
| 4 |
import dash_bootstrap_components as dbc
|
|
@@ -27,7 +27,7 @@ except (AttributeError, ImportError):
|
|
| 27 |
print("WARNING: Pustaka 'reportlab' atau 'kaleido' tidak terinstall. Fitur unduh PDF tidak akan berfungsi.")
|
| 28 |
|
| 29 |
# -----------------------------------------------------------------------------
|
| 30 |
-
# BAGIAN 1: FUNGSI-FUNGSI HELPER (
|
| 31 |
# -----------------------------------------------------------------------------
|
| 32 |
|
| 33 |
def get_filter_text(pusk, tahun, bulan):
|
|
@@ -36,7 +36,6 @@ def get_filter_text(pusk, tahun, bulan):
|
|
| 36 |
bulan_txt = "Semua Bulan" if not bulan else ", ".join(sorted(bulan))
|
| 37 |
return f"Data dari: {pusk_txt} | Tahun: {tahun_txt} | Bulan: {bulan_txt}"
|
| 38 |
|
| 39 |
-
# --- Helper untuk Analisis Tren Penyakit ---
|
| 40 |
def kategori_penyakit_atp(icd):
|
| 41 |
if pd.isna(icd) or str(icd).strip() == "": return 'Tidak Menular'
|
| 42 |
icd_clean = str(icd).strip().upper()
|
|
@@ -82,7 +81,16 @@ def create_trend_analysis_text(df, time_unit='tahun'):
|
|
| 82 |
analysis_points.append(f"- Kasus **{disease}** {tren}.")
|
| 83 |
return "Ringkasan Tren 3 Penyakit Teratas:\n\n" + "\n".join(analysis_points)
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def generate_gender_analysis_text(total_lk, total_pr):
|
| 87 |
if total_lk == 0 and total_pr == 0: return "Tidak ada data gender untuk dianalisis."
|
| 88 |
total_kasus = total_lk + total_pr
|
|
@@ -102,7 +110,7 @@ def generate_age_analysis_text(baru_data, lama_data):
|
|
| 102 |
return f"Kelompok umur dengan total kasus tertinggi adalah **{max_total_kelompok}** ({int(total_per_kelompok[max_total_kelompok]):,} kasus)."
|
| 103 |
|
| 104 |
# -----------------------------------------------------------------------------
|
| 105 |
-
# BAGIAN 2: LAYOUT HALAMAN (
|
| 106 |
# -----------------------------------------------------------------------------
|
| 107 |
layout = dbc.Container([
|
| 108 |
dcc.Store(id='laporan-data-store'),
|
|
@@ -135,9 +143,8 @@ layout = dbc.Container([
|
|
| 135 |
], fluid=True)
|
| 136 |
|
| 137 |
# -----------------------------------------------------------------------------
|
| 138 |
-
# BAGIAN 3: CALLBACKS
|
| 139 |
# -----------------------------------------------------------------------------
|
| 140 |
-
# Callback filter tidak berubah
|
| 141 |
@callback(
|
| 142 |
Output('laporan-pusk-filter', 'options'),
|
| 143 |
Output('laporan-tahun-filter', 'options'),
|
|
@@ -183,17 +190,17 @@ def laporan_update_bulan_filter(selected_tahun):
|
|
| 183 |
Input('laporan-bulan-filter', 'value')
|
| 184 |
)
|
| 185 |
def update_laporan_terpadu_tabs(selected_pusk, selected_tahun, selected_bulan):
|
|
|
|
| 186 |
if not selected_pusk or not selected_tahun:
|
| 187 |
msg = html.P("Silakan pilih minimal Puskesmas dan Tahun.", className="text-center text-primary mt-5")
|
| 188 |
return msg, msg, "", True, None
|
| 189 |
|
| 190 |
filter_summary_text = get_filter_text(selected_pusk, selected_tahun, selected_bulan)
|
| 191 |
-
|
| 192 |
-
if selected_bulan:
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
stmt = select(*[getattr(detail_penyakit.c, col) for col in all_cols]).where(and_(*filters))
|
| 197 |
|
| 198 |
with engine.connect() as conn:
|
| 199 |
df_base = pd.read_sql(stmt, conn)
|
|
@@ -202,18 +209,15 @@ def update_laporan_terpadu_tabs(selected_pusk, selected_tahun, selected_bulan):
|
|
| 202 |
msg = dbc.Alert("Tidak ada data ditemukan untuk kriteria yang dipilih.", color="warning", className="m-4")
|
| 203 |
return msg, msg, filter_summary_text, True, None
|
| 204 |
|
| 205 |
-
#
|
| 206 |
-
# BAGIAN A: Proses Data untuk TAB 1 (Analisis Tren Penyakit)
|
| 207 |
-
# =========================================================
|
| 208 |
kode_dihindari = ('V', 'W', 'X', 'Y', 'Z')
|
| 209 |
df_base['icd_x_str'] = df_base['icd_x'].astype(str).str.strip().str.upper()
|
| 210 |
df_filtered_icd = df_base[~df_base['icd_x_str'].str.startswith(kode_dihindari, na=False)].copy()
|
| 211 |
-
|
| 212 |
df_filtered_icd['kategori'] = df_filtered_icd['icd_x'].apply(kategori_penyakit_atp)
|
| 213 |
df_ranking_total = df_filtered_icd.groupby('jenis_penyakit')['totall'].sum().nlargest(10).sort_values().reset_index()
|
| 214 |
top_10_penyakit_list = df_ranking_total['jenis_penyakit'].tolist()
|
| 215 |
-
df_top10_base = df_filtered_icd[df_filtered_icd['jenis_penyakit'].isin(top_10_penyakit_list)]
|
| 216 |
-
|
| 217 |
fig_ranking_simple = px.bar(df_ranking_total, x='totall', y='jenis_penyakit', orientation='h', template='plotly_white', title='<b>Peringkat 10 Penyakit Teratas</b>')
|
| 218 |
table_ranking = create_ranking_table(df_ranking_total)
|
| 219 |
analysis_ranking = create_ranking_analysis_text(df_ranking_total)
|
|
@@ -222,70 +226,99 @@ def update_laporan_terpadu_tabs(selected_pusk, selected_tahun, selected_bulan):
|
|
| 222 |
fig_category_pie = px.pie(df_category_pie, values='totall', names='kategori', title='<b>Komposisi Menular vs Tidak Menular</b>', color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'})
|
| 223 |
analysis_pie = create_pie_analysis_text(df_category_pie)
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
df_monthly_line = df_top10_base.groupby(['tahun', 'bulan', 'jenis_penyakit'])['totall'].sum().reset_index()
|
| 226 |
df_monthly_line['periode'] = df_monthly_line['tahun'].astype(str) + '-' + df_monthly_line['bulan'].str.zfill(2)
|
| 227 |
-
fig_line_monthly = px.line(df_monthly_line.sort_values('periode'), x='periode', y='totall', color='jenis_penyakit', title="<b>Tren Bulanan 10
|
| 228 |
analysis_monthly_trend = create_trend_analysis_text(df_monthly_line, 'periode')
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
tab1_content = html.Div([
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
], className="mt-3")
|
| 243 |
|
| 244 |
-
#
|
| 245 |
-
# BAGIAN B: Proses Data untuk TAB 2 (Analisis Demografi)
|
| 246 |
-
# =========================================================
|
| 247 |
total_lk = df_base['laki_laki'].sum()
|
| 248 |
total_pr = df_base['perempuan'].sum()
|
| 249 |
fig_gender = go.Figure(data=[go.Pie(labels=['Laki-laki', 'Perempuan'], values=[total_lk, total_pr], hole=.4, marker_colors=['royalblue', 'crimson'])])
|
| 250 |
fig_gender.update_layout(title_text='<b>Distribusi Kasus Berdasarkan Jenis Kelamin</b>', template='plotly_white')
|
| 251 |
analysis_gender = generate_gender_analysis_text(total_lk, total_pr)
|
| 252 |
-
|
| 253 |
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']}
|
| 254 |
age_new_data, age_old_data = {}, {}
|
| 255 |
for kelompok, cols in kelompok_map.items():
|
| 256 |
age_new_data[kelompok] = df_base[[c for c in cols if 'baru' in c]].sum().sum()
|
| 257 |
age_old_data[kelompok] = df_base[[c for c in cols if 'lama' in c]].sum().sum()
|
| 258 |
-
|
| 259 |
fig_age = go.Figure(data=[go.Bar(name='Kasus Baru', x=list(age_new_data.keys()), y=list(age_new_data.values())), go.Bar(name='Kasus Lama', x=list(age_old_data.keys()), y=list(age_old_data.values()))]).update_layout(barmode='group', title_text="<b>Distribusi Kasus Berdasarkan Kelompok Umur</b>", template='plotly_white')
|
| 260 |
analysis_age = generate_age_analysis_text(age_new_data, age_old_data)
|
|
|
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
dbc.Col(dcc.Graph(figure=fig_gender), md=6),
|
| 265 |
-
dbc.Col(dcc.Graph(figure=fig_age), md=6),
|
| 266 |
-
dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_gender))), md=6),
|
| 267 |
-
dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_age))), md=6),
|
| 268 |
-
], className="mb-4"),
|
| 269 |
-
], className="mt-3")
|
| 270 |
-
|
| 271 |
-
# --- SIMPAN SEMUA DATA UNTUK DIUNDUH ---
|
| 272 |
-
data_to_store = {
|
| 273 |
-
'figs_json': {
|
| 274 |
-
'ranking_simple': fig_ranking_simple.to_json(), 'category_pie': fig_category_pie.to_json(),
|
| 275 |
-
'line_trend_monthly': fig_line_monthly.to_json(), 'gender_pie': fig_gender.to_json(),
|
| 276 |
-
'age_bar': fig_age.to_json(),
|
| 277 |
-
},
|
| 278 |
-
'table_data': {'ranking': df_ranking_total.to_dict('records')},
|
| 279 |
-
'analysis_texts': {
|
| 280 |
-
'ranking': analysis_ranking, 'pie': analysis_pie, 'monthly_trend': analysis_monthly_trend,
|
| 281 |
-
'gender': analysis_gender, 'age': analysis_age,
|
| 282 |
-
},
|
| 283 |
-
'filter_text': filter_summary_text,
|
| 284 |
-
}
|
| 285 |
|
| 286 |
return tab1_content, tab2_content, filter_summary_text, not PDF_CAPABLE, data_to_store
|
| 287 |
|
| 288 |
-
# Callback
|
|
|
|
| 289 |
@callback(
|
| 290 |
Output("laporan-download-pdf", "data"),
|
| 291 |
Input("laporan-btn-unduh-pdf", "n_clicks"),
|
|
@@ -325,20 +358,23 @@ def download_laporan_as_pdf(n_clicks, stored_data):
|
|
| 325 |
story = [Paragraph("Laporan Analisis Terpadu", style_h1), Paragraph(stored_data.get('filter_text', ''), styles['Italic']), Spacer(1, 0.3*inch)]
|
| 326 |
analysis = stored_data.get('analysis_texts', {}); figs = stored_data.get('figs_json', {}); tables = stored_data.get('table_data', {})
|
| 327 |
|
| 328 |
-
# HALAMAN TREN PENYAKIT
|
| 329 |
story.append(Paragraph("BAGIAN 1: ANALISIS TREN PENYAKIT", style_h2))
|
| 330 |
-
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'
|
| 331 |
-
story.append(fig_to_image(figs.get('category_pie'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('pie'
|
| 332 |
-
story.append(fig_to_image(figs.get('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
|
| 334 |
-
# HALAMAN ANALISIS DEMOGRAFI
|
| 335 |
story.append(Paragraph("BAGIAN 2: ANALISIS DEMOGRAFI", style_h2))
|
| 336 |
-
story.append(fig_to_image(figs.get('gender_pie'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('gender'
|
| 337 |
-
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'
|
| 338 |
|
| 339 |
doc.build(story); return dcc.send_bytes(buffer.getvalue(), "Laporan_Terpadu_Penyakit.pdf")
|
| 340 |
|
| 341 |
-
# Callback 5: Unduh Excel
|
| 342 |
@callback(
|
| 343 |
Output("laporan-download-excel", "data"),
|
| 344 |
Input("laporan-btn-unduh-excel", "n_clicks"),
|
|
|
|
| 1 |
+
# File: pages/laporan.py (Revisi Final dengan Layout Satu Baris per Grafik)
|
| 2 |
|
| 3 |
from dash import dcc, html, Input, Output, callback, no_update, State, dash_table
|
| 4 |
import dash_bootstrap_components as dbc
|
|
|
|
| 27 |
print("WARNING: Pustaka 'reportlab' atau 'kaleido' tidak terinstall. Fitur unduh PDF tidak akan berfungsi.")
|
| 28 |
|
| 29 |
# -----------------------------------------------------------------------------
|
| 30 |
+
# BAGIAN 1: FUNGSI-FUNGSI HELPER (Tidak Berubah)
|
| 31 |
# -----------------------------------------------------------------------------
|
| 32 |
|
| 33 |
def get_filter_text(pusk, tahun, bulan):
|
|
|
|
| 36 |
bulan_txt = "Semua Bulan" if not bulan else ", ".join(sorted(bulan))
|
| 37 |
return f"Data dari: {pusk_txt} | Tahun: {tahun_txt} | Bulan: {bulan_txt}"
|
| 38 |
|
|
|
|
| 39 |
def kategori_penyakit_atp(icd):
|
| 40 |
if pd.isna(icd) or str(icd).strip() == "": return 'Tidak Menular'
|
| 41 |
icd_clean = str(icd).strip().upper()
|
|
|
|
| 81 |
analysis_points.append(f"- Kasus **{disease}** {tren}.")
|
| 82 |
return "Ringkasan Tren 3 Penyakit Teratas:\n\n" + "\n".join(analysis_points)
|
| 83 |
|
| 84 |
+
def create_comparison_analysis_text(df):
|
| 85 |
+
if df.empty: return "Tidak ada data untuk dianalisis."
|
| 86 |
+
pusk_contribution = df.groupby('kode_pusk')['totall'].sum().sort_values(ascending=False)
|
| 87 |
+
if pusk_contribution.empty: return "Tidak ada data puskesmas untuk dianalisis."
|
| 88 |
+
top_pusk, top_pusk_cases, total_cases = pusk_contribution.index[0], pusk_contribution.iloc[0], pusk_contribution.sum()
|
| 89 |
+
if total_cases == 0: return "Total kasus adalah nol."
|
| 90 |
+
top_pusk_percent = (top_pusk_cases / total_cases * 100).round(1)
|
| 91 |
+
top_disease_by_pusk = df[df['kode_pusk'] == top_pusk].groupby('jenis_penyakit')['totall'].sum().idxmax()
|
| 92 |
+
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}**."
|
| 93 |
+
|
| 94 |
def generate_gender_analysis_text(total_lk, total_pr):
|
| 95 |
if total_lk == 0 and total_pr == 0: return "Tidak ada data gender untuk dianalisis."
|
| 96 |
total_kasus = total_lk + total_pr
|
|
|
|
| 110 |
return f"Kelompok umur dengan total kasus tertinggi adalah **{max_total_kelompok}** ({int(total_per_kelompok[max_total_kelompok]):,} kasus)."
|
| 111 |
|
| 112 |
# -----------------------------------------------------------------------------
|
| 113 |
+
# BAGIAN 2: LAYOUT HALAMAN (Tidak Berubah)
|
| 114 |
# -----------------------------------------------------------------------------
|
| 115 |
layout = dbc.Container([
|
| 116 |
dcc.Store(id='laporan-data-store'),
|
|
|
|
| 143 |
], fluid=True)
|
| 144 |
|
| 145 |
# -----------------------------------------------------------------------------
|
| 146 |
+
# BAGIAN 3: CALLBACKS (Tidak Berubah)
|
| 147 |
# -----------------------------------------------------------------------------
|
|
|
|
| 148 |
@callback(
|
| 149 |
Output('laporan-pusk-filter', 'options'),
|
| 150 |
Output('laporan-tahun-filter', 'options'),
|
|
|
|
| 190 |
Input('laporan-bulan-filter', 'value')
|
| 191 |
)
|
| 192 |
def update_laporan_terpadu_tabs(selected_pusk, selected_tahun, selected_bulan):
|
| 193 |
+
# Logika pengambilan dan pemrosesan data awal tidak berubah
|
| 194 |
if not selected_pusk or not selected_tahun:
|
| 195 |
msg = html.P("Silakan pilih minimal Puskesmas dan Tahun.", className="text-center text-primary mt-5")
|
| 196 |
return msg, msg, "", True, None
|
| 197 |
|
| 198 |
filter_summary_text = get_filter_text(selected_pusk, selected_tahun, selected_bulan)
|
| 199 |
+
base_filters = [detail_penyakit.c.kode_pusk.in_(selected_pusk), detail_penyakit.c.tahun.in_(selected_tahun)]
|
| 200 |
+
if selected_bulan: base_filters.append(detail_penyakit.c.bulan.in_(selected_bulan))
|
| 201 |
|
| 202 |
+
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']
|
| 203 |
+
stmt = select(*[getattr(detail_penyakit.c, col) for col in all_cols]).where(and_(*base_filters))
|
|
|
|
| 204 |
|
| 205 |
with engine.connect() as conn:
|
| 206 |
df_base = pd.read_sql(stmt, conn)
|
|
|
|
| 209 |
msg = dbc.Alert("Tidak ada data ditemukan untuk kriteria yang dipilih.", color="warning", className="m-4")
|
| 210 |
return msg, msg, filter_summary_text, True, None
|
| 211 |
|
| 212 |
+
# Pembuatan semua 9 grafik + 1 tabel (tidak berubah)
|
|
|
|
|
|
|
| 213 |
kode_dihindari = ('V', 'W', 'X', 'Y', 'Z')
|
| 214 |
df_base['icd_x_str'] = df_base['icd_x'].astype(str).str.strip().str.upper()
|
| 215 |
df_filtered_icd = df_base[~df_base['icd_x_str'].str.startswith(kode_dihindari, na=False)].copy()
|
|
|
|
| 216 |
df_filtered_icd['kategori'] = df_filtered_icd['icd_x'].apply(kategori_penyakit_atp)
|
| 217 |
df_ranking_total = df_filtered_icd.groupby('jenis_penyakit')['totall'].sum().nlargest(10).sort_values().reset_index()
|
| 218 |
top_10_penyakit_list = df_ranking_total['jenis_penyakit'].tolist()
|
| 219 |
+
df_top10_base = df_filtered_icd[df_filtered_icd['jenis_penyakit'].isin(top_10_penyakit_list)].copy()
|
| 220 |
+
|
| 221 |
fig_ranking_simple = px.bar(df_ranking_total, x='totall', y='jenis_penyakit', orientation='h', template='plotly_white', title='<b>Peringkat 10 Penyakit Teratas</b>')
|
| 222 |
table_ranking = create_ranking_table(df_ranking_total)
|
| 223 |
analysis_ranking = create_ranking_analysis_text(df_ranking_total)
|
|
|
|
| 226 |
fig_category_pie = px.pie(df_category_pie, values='totall', names='kategori', title='<b>Komposisi Menular vs Tidak Menular</b>', color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'})
|
| 227 |
analysis_pie = create_pie_analysis_text(df_category_pie)
|
| 228 |
|
| 229 |
+
df_yearly_data = df_top10_base.groupby(['tahun', 'jenis_penyakit'])['totall'].sum().reset_index()
|
| 230 |
+
df_yearly_data['tahun'] = df_yearly_data['tahun'].astype(str)
|
| 231 |
+
fig_bar_trend_yearly = px.bar(df_yearly_data, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Kasus Tahunan (Top 10)</b>", template='plotly_white')
|
| 232 |
+
fig_bar_trend_yearly.update_layout(yaxis={'categoryorder':'total ascending'})
|
| 233 |
+
|
| 234 |
+
fig_line_trend_yearly = px.line(df_yearly_data, x='tahun', y='totall', color='jenis_penyakit', markers=True, title="<b>Tren Tahunan (Top 10)</b>")
|
| 235 |
+
analysis_yearly_trend = create_trend_analysis_text(df_yearly_data, 'tahun')
|
| 236 |
+
|
| 237 |
df_monthly_line = df_top10_base.groupby(['tahun', 'bulan', 'jenis_penyakit'])['totall'].sum().reset_index()
|
| 238 |
df_monthly_line['periode'] = df_monthly_line['tahun'].astype(str) + '-' + df_monthly_line['bulan'].str.zfill(2)
|
| 239 |
+
fig_line_monthly = px.line(df_monthly_line.sort_values('periode'), x='periode', y='totall', color='jenis_penyakit', title="<b>Tren Bulanan (Top 10)</b>")
|
| 240 |
analysis_monthly_trend = create_trend_analysis_text(df_monthly_line, 'periode')
|
| 241 |
|
| 242 |
+
df_monthly_cat_trend = df_filtered_icd.groupby(['tahun', 'bulan', 'kategori'])['totall'].sum().reset_index()
|
| 243 |
+
df_monthly_cat_trend['periode'] = df_monthly_cat_trend['tahun'].astype(str) + '-' + df_monthly_cat_trend['bulan'].str.zfill(2)
|
| 244 |
+
fig_monthly_compare_trend = px.area(df_monthly_cat_trend.sort_values('periode'), x='periode', y='totall', color='kategori', title="<b>Tren Bulanan Kasus per Kategori</b>", color_discrete_map={'Menular':'crimson', 'Tidak Menular':'royalblue'})
|
| 245 |
+
|
| 246 |
+
df_pusk_compare = df_top10_base.groupby(['kode_pusk', 'jenis_penyakit'])['totall'].sum().reset_index()
|
| 247 |
+
fig_pusk_stacked = px.bar(df_pusk_compare, x='totall', y='jenis_penyakit', color='kode_pusk', orientation='h', title='<b>Kontribusi Kasus per Puskesmas</b>')
|
| 248 |
+
fig_pusk_stacked.update_layout(yaxis={'categoryorder':'total ascending'})
|
| 249 |
+
analysis_pusk_comparison = create_comparison_analysis_text(df_pusk_compare)
|
| 250 |
+
|
| 251 |
+
df_menular = df_filtered_icd[df_filtered_icd['kategori'] == 'Menular']; top_10_menular = df_menular.groupby('jenis_penyakit')['totall'].sum().nlargest(10).index
|
| 252 |
+
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)
|
| 253 |
+
fig_trend_menular = px.bar(df_trend_menular, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Tahunan (10 Penyakit Menular Teratas)</b>", template='plotly_white'); fig_trend_menular.update_layout(yaxis={'categoryorder':'total ascending'})
|
| 254 |
+
|
| 255 |
+
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
|
| 256 |
+
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)
|
| 257 |
+
fig_trend_tidak_menular = px.bar(df_trend_tidak_menular, x='totall', y='jenis_penyakit', color='tahun', orientation='h', barmode='group', title="<b>Perbandingan Tahunan (10 Penyakit Tidak Menular Teratas)</b>", template='plotly_white'); fig_trend_tidak_menular.update_layout(yaxis={'categoryorder':'total ascending'})
|
| 258 |
+
|
| 259 |
+
# =================================================================
|
| 260 |
+
# <<< INI ADALAH BAGIAN YANG DIREVISI SESUAI PERMINTAAN ANDA >>>
|
| 261 |
+
# =================================================================
|
| 262 |
+
# --- Menyusun Layout Baru untuk Tab 1 (Satu Visualisasi per Baris) ---
|
| 263 |
tab1_content = html.Div([
|
| 264 |
+
# 1. Grafik Peringkat dan Tabel
|
| 265 |
+
dbc.Row(dbc.Col(dcc.Graph(figure=fig_ranking_simple), md=12), className="mb-2"),
|
| 266 |
+
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)),
|
| 267 |
+
html.Hr(className="my-4"),
|
| 268 |
+
|
| 269 |
+
# 2. Grafik Pie Komposisi
|
| 270 |
+
dbc.Row(dbc.Col([dcc.Graph(figure=fig_category_pie), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_pie)), className="mt-2")], md=12)),
|
| 271 |
+
html.Hr(className="my-4"),
|
| 272 |
+
|
| 273 |
+
# 3. Grafik Perbandingan Tahunan (Batang)
|
| 274 |
+
dbc.Row(dbc.Col(dcc.Graph(figure=fig_bar_trend_yearly), md=12)),
|
| 275 |
+
html.Hr(className="my-4"),
|
| 276 |
+
|
| 277 |
+
# 4. Grafik Tren Tahunan (Garis)
|
| 278 |
+
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)),
|
| 279 |
+
html.Hr(className="my-4"),
|
| 280 |
+
|
| 281 |
+
# 5. Grafik Tren Bulanan (Garis)
|
| 282 |
+
dbc.Row(dbc.Col([dcc.Graph(figure=fig_line_monthly), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_monthly_trend)), className="mt-2")], md=12)),
|
| 283 |
+
html.Hr(className="my-4"),
|
| 284 |
+
|
| 285 |
+
# 6. Grafik Tren Bulanan per Kategori (Area)
|
| 286 |
+
dbc.Row(dbc.Col(dcc.Graph(figure=fig_monthly_compare_trend), md=12)),
|
| 287 |
+
html.Hr(className="my-4"),
|
| 288 |
+
|
| 289 |
+
# 7. Grafik Kontribusi Puskesmas
|
| 290 |
+
dbc.Row(dbc.Col([dcc.Graph(figure=fig_pusk_stacked), dbc.Card(dbc.CardBody(dcc.Markdown(analysis_pusk_comparison)), className="mt-2")], md=12)),
|
| 291 |
+
html.Hr(className="my-4"),
|
| 292 |
+
|
| 293 |
+
# 8 & 9. Grafik Perbandingan Tahunan per Kategori
|
| 294 |
+
html.H4("Analisis Detail Berdasarkan Kategori Penyakit", className="text-center my-4"),
|
| 295 |
+
dbc.Row(dbc.Col(dcc.Graph(figure=fig_trend_menular), md=12), className="mb-4"),
|
| 296 |
+
dbc.Row(dbc.Col(dcc.Graph(figure=fig_trend_tidak_menular), md=12)),
|
| 297 |
+
|
| 298 |
], className="mt-3")
|
| 299 |
|
| 300 |
+
# BAGIAN B: Proses Data untuk TAB 2 (Analisis Demografi) - Tidak Berubah
|
|
|
|
|
|
|
| 301 |
total_lk = df_base['laki_laki'].sum()
|
| 302 |
total_pr = df_base['perempuan'].sum()
|
| 303 |
fig_gender = go.Figure(data=[go.Pie(labels=['Laki-laki', 'Perempuan'], values=[total_lk, total_pr], hole=.4, marker_colors=['royalblue', 'crimson'])])
|
| 304 |
fig_gender.update_layout(title_text='<b>Distribusi Kasus Berdasarkan Jenis Kelamin</b>', template='plotly_white')
|
| 305 |
analysis_gender = generate_gender_analysis_text(total_lk, total_pr)
|
|
|
|
| 306 |
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']}
|
| 307 |
age_new_data, age_old_data = {}, {}
|
| 308 |
for kelompok, cols in kelompok_map.items():
|
| 309 |
age_new_data[kelompok] = df_base[[c for c in cols if 'baru' in c]].sum().sum()
|
| 310 |
age_old_data[kelompok] = df_base[[c for c in cols if 'lama' in c]].sum().sum()
|
|
|
|
| 311 |
fig_age = go.Figure(data=[go.Bar(name='Kasus Baru', x=list(age_new_data.keys()), y=list(age_new_data.values())), go.Bar(name='Kasus Lama', x=list(age_old_data.keys()), y=list(age_old_data.values()))]).update_layout(barmode='group', title_text="<b>Distribusi Kasus Berdasarkan Kelompok Umur</b>", template='plotly_white')
|
| 312 |
analysis_age = generate_age_analysis_text(age_new_data, age_old_data)
|
| 313 |
+
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")
|
| 314 |
|
| 315 |
+
# Penyimpanan data ke dcc.Store tidak berubah
|
| 316 |
+
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,}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
return tab1_content, tab2_content, filter_summary_text, not PDF_CAPABLE, data_to_store
|
| 319 |
|
| 320 |
+
# Callback PDF dan Excel tidak perlu diubah, karena mereka mengambil dari dcc.Store
|
| 321 |
+
# ... (sisa kode sama seperti sebelumnya) ...
|
| 322 |
@callback(
|
| 323 |
Output("laporan-download-pdf", "data"),
|
| 324 |
Input("laporan-btn-unduh-pdf", "n_clicks"),
|
|
|
|
| 358 |
story = [Paragraph("Laporan Analisis Terpadu", style_h1), Paragraph(stored_data.get('filter_text', ''), styles['Italic']), Spacer(1, 0.3*inch)]
|
| 359 |
analysis = stored_data.get('analysis_texts', {}); figs = stored_data.get('figs_json', {}); tables = stored_data.get('table_data', {})
|
| 360 |
|
|
|
|
| 361 |
story.append(Paragraph("BAGIAN 1: ANALISIS TREN PENYAKIT", style_h2))
|
| 362 |
+
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())
|
| 363 |
+
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))
|
| 364 |
+
story.append(fig_to_image(figs.get('bar_trend_yearly'))); story.append(PageBreak())
|
| 365 |
+
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))
|
| 366 |
+
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())
|
| 367 |
+
story.append(fig_to_image(figs.get('monthly_compare_trend'))); story.append(Spacer(1, 0.2*inch))
|
| 368 |
+
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())
|
| 369 |
+
story.append(fig_to_image(figs.get('trend_menular'))); story.append(Spacer(1, 0.2*inch))
|
| 370 |
+
story.append(fig_to_image(figs.get('trend_tidak_menular'))); story.append(PageBreak())
|
| 371 |
|
|
|
|
| 372 |
story.append(Paragraph("BAGIAN 2: ANALISIS DEMOGRAFI", style_h2))
|
| 373 |
+
story.append(fig_to_image(figs.get('gender_pie'))); story.append(Spacer(1, 0.1*inch)); story.append(text_to_paragraph(analysis.get('gender')))
|
| 374 |
+
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')))
|
| 375 |
|
| 376 |
doc.build(story); return dcc.send_bytes(buffer.getvalue(), "Laporan_Terpadu_Penyakit.pdf")
|
| 377 |
|
|
|
|
| 378 |
@callback(
|
| 379 |
Output("laporan-download-excel", "data"),
|
| 380 |
Input("laporan-btn-unduh-excel", "n_clicks"),
|