aslasacacc commited on
Commit
dcfb91a
·
verified ·
1 Parent(s): 84aedf4

Update pages/laporan.py

Browse files
Files changed (1) hide show
  1. pages/laporan.py +103 -67
pages/laporan.py CHANGED
@@ -1,4 +1,4 @@
1
- # File: pages/laporan.py (Revisi Final - Dashboard Terpadu)
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 (GABUNGAN DARI SEMUA HALAMAN)
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
- # --- Helper untuk Analisis Demografi ---
 
 
 
 
 
 
 
 
 
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 (DENGAN TAB BARU)
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
- filters = [detail_penyakit.c.kode_pusk.in_(selected_pusk), detail_penyakit.c.tahun.in_(selected_tahun)]
192
- if selected_bulan: filters.append(detail_penyakit.c.bulan.in_(selected_bulan))
193
 
194
- # --- Eksekusi Query Gabungan ---
195
- all_cols = ['jenis_penyakit', 'icd_x', 'tahun', 'bulan', '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']
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 Penyakit Teratas</b>")
228
  analysis_monthly_trend = create_trend_analysis_text(df_monthly_line, 'periode')
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  tab1_content = html.Div([
231
- dbc.Row([
232
- dbc.Col(dcc.Graph(figure=fig_ranking_simple), md=12),
233
- dbc.Col([html.H5("Tabel Peringkat", className="mt-3"), table_ranking, dbc.Card(dbc.CardBody(dcc.Markdown(analysis_ranking)), className="mt-3")], md=12),
234
- ], className="mb-4"),
235
- html.Hr(),
236
- dbc.Row([
237
- dbc.Col(dcc.Graph(figure=fig_category_pie), md=6),
238
- dbc.Col(dcc.Graph(figure=fig_line_monthly), md=6),
239
- dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_pie))), md=6),
240
- dbc.Col(dbc.Card(dbc.CardBody(dcc.Markdown(analysis_monthly_trend))), md=6),
241
- ], className="mb-4"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- tab2_content = html.Div([
263
- dbc.Row([
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 4: Unduh PDF
 
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', '...'))); story.append(PageBreak())
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', '...'))); story.append(Spacer(1, 0.3*inch))
332
- 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())
 
 
 
 
 
 
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"),