DavMelchi commited on
Commit
6b0959d
·
1 Parent(s): ac45eb3

Add complaint sites filtering to overview tables with dedicated tab, separate complaint-specific tables for multi-RAT summary and top anomalies, and include complaint data in Excel export with two additional sheets

Browse files
panel_app/kpi_health_check_panel.py CHANGED
@@ -417,7 +417,6 @@ except Exception: # noqa: BLE001
417
  rules_table.configuration = cfg
418
  except Exception: # noqa: BLE001
419
  pass
420
-
421
  site_summary_table = pn.widgets.Tabulator(
422
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
423
  )
@@ -426,10 +425,18 @@ multirat_summary_table = pn.widgets.Tabulator(
426
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
427
  )
428
 
 
 
 
 
429
  top_anomalies_table = pn.widgets.Tabulator(
430
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
431
  )
432
 
 
 
 
 
433
  site_select = pn.widgets.AutocompleteInput(
434
  name="Select a site (Type to search)",
435
  options={},
@@ -446,7 +453,6 @@ kpi_compare_norm = pn.widgets.Select(
446
  name="Normalization", options=["None", "Min-Max", "Z-score"], value="None"
447
  )
448
 
449
- # NEW WIDGETS
450
  kpi_group_select = pn.widgets.Select(
451
  name="KPI Group", options=["All (selected KPIs)"], value="All (selected KPIs)"
452
  )
@@ -535,6 +541,8 @@ except Exception: # noqa: BLE001
535
  _set_tabulator_pagination(site_summary_table, page_size=50)
536
  _set_tabulator_pagination(multirat_summary_table, page_size=50)
537
  _set_tabulator_pagination(top_anomalies_table, page_size=50)
 
 
538
  _set_tabulator_pagination(site_kpi_table, page_size=50)
539
  trend_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
540
  heatmap_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
@@ -780,7 +788,6 @@ def _update_kpi_options() -> None:
780
  ]
781
  kpis = sorted([str(c) for c in kpis])
782
 
783
- # Apply Grouping if needed
784
  groups = get_kpis_by_group(kpis)
785
  group_options = ["All (selected KPIs)"] + sorted(
786
  [g for g in groups.keys() if g != "Other"]
@@ -794,7 +801,6 @@ def _update_kpi_options() -> None:
794
  if kpi_group_select.value not in group_options:
795
  kpi_group_select.value = group_options[0]
796
 
797
- # Filter KPIs based on group
798
  filtered_kpis = filter_kpis(
799
  kpis, kpi_group_select.value, mode=kpi_group_mode.value
800
  )
@@ -852,20 +858,14 @@ def _update_site_view(event=None) -> None:
852
  except Exception: # noqa: BLE001
853
  available_sites = set()
854
 
855
- # Robustly resolve code_int from site_select.value
856
- # AutocompleteInput might return the Label (str) or Value (int) depending on usage
857
  code_int = None
858
  if code is not None:
859
- # 1. Try if code is already a known value (int)
860
  if hasattr(site_select, "options") and isinstance(site_select.options, dict):
861
- # Check if it matches a Key (Label)
862
  if code in site_select.options:
863
  code_int = site_select.options[code]
864
- # Check if it is a Value in the dict
865
  elif code in site_select.options.values():
866
  code_int = code
867
 
868
- # 2. If not checking opts or not found, try direct cast
869
  if code_int is None:
870
  try:
871
  code_int = int(code)
@@ -932,36 +932,38 @@ def _update_site_view(event=None) -> None:
932
  trend_plot_pane.object, heatmap_plot_pane.object, hist_plot_pane.object = cached
933
  return
934
 
935
- # Determine KPIs to plot based on group mode
936
  kpis_to_plot = []
937
 
938
- # 1. Start with explicitly selected 'Compare KPIs'
939
  selected = [str(x) for x in (kpi_compare_select.value or []) if str(x)]
940
 
941
- # 2. Add the primary selected KPI if not present
942
  if kpi and str(kpi) not in selected:
943
  selected = [str(kpi)] + selected
944
 
945
- # 3. Handle Group Mode "Add top 12 KPIs"
946
- # If mode is "Add top...", we fetch from group and append
947
  if "Add top" in str(kpi_group_mode.value):
948
  from_group = filter_kpis(
949
  d.columns.tolist(), kpi_group_select.value, mode="Top-N", top_n=12
950
  )
951
- # Merge unique
952
  for gk in from_group:
953
  if gk not in selected:
954
  selected.append(gk)
955
 
956
- # Safeguard: Limit to 15 max to prevent browser crash
957
  kpis_to_plot = selected[:15]
958
 
959
- # Build Plot using new module
960
- # We need the rules for this RAT/KPIs to show SLA
961
- relevant_rules = pd.DataFrame()
962
- if isinstance(rules_table.value, pd.DataFrame) and not rules_table.value.empty:
963
- r = rules_table.value
964
- relevant_rules = r[r["RAT"] == rat]
 
 
 
 
 
 
 
 
 
965
 
966
  fig = build_drilldown_plot(
967
  df=d[d["site_code"] == int(code_int)],
@@ -973,12 +975,6 @@ def _update_site_view(event=None) -> None:
973
  rat=rat,
974
  )
975
  trend_plot_pane.object = fig
976
-
977
- rules_df = (
978
- rules_table.value
979
- if isinstance(rules_table.value, pd.DataFrame)
980
- else pd.DataFrame()
981
- )
982
  kpis_for_heatmap = []
983
  if (
984
  isinstance(site_df, pd.DataFrame)
@@ -1488,6 +1484,52 @@ def _refresh_filtered_results(event=None) -> None:
1488
  current_top_anomalies_df = pd.DataFrame()
1489
  top_anomalies_table.value = current_top_anomalies_df
1490
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1491
  current_export_bytes = None
1492
 
1493
 
@@ -1833,6 +1875,8 @@ def load_datasets(event=None) -> None:
1833
  site_summary_table.value = pd.DataFrame()
1834
  multirat_summary_table.value = pd.DataFrame()
1835
  top_anomalies_table.value = pd.DataFrame()
 
 
1836
  site_kpi_table.value = pd.DataFrame()
1837
  trend_plot_pane.object = None
1838
  heatmap_plot_pane.object = None
@@ -1924,8 +1968,20 @@ def load_datasets(event=None) -> None:
1924
  _loading_datasets = False
1925
  try:
1926
  _update_site_options()
 
 
 
 
1927
  _update_kpi_options()
 
 
 
 
1928
  _update_site_view()
 
 
 
 
1929
  _refresh_validation_state()
1930
  except Exception: # noqa: BLE001
1931
  pass
@@ -2088,6 +2144,16 @@ def _build_export_bytes() -> bytes:
2088
  if isinstance(current_top_anomalies_df, pd.DataFrame)
2089
  else None
2090
  ),
 
 
 
 
 
 
 
 
 
 
2091
  )
2092
 
2093
 
@@ -2176,6 +2242,13 @@ try:
2176
  except Exception: # noqa: BLE001
2177
  pass
2178
 
 
 
 
 
 
 
 
2179
  try:
2180
  multirat_summary_table.on_click(
2181
  lambda e: _handle_double_click("multirat", multirat_summary_table, e)
@@ -2183,6 +2256,13 @@ try:
2183
  except Exception: # noqa: BLE001
2184
  pass
2185
 
 
 
 
 
 
 
 
2186
  min_criticality.param.watch(_refresh_filtered_results, "value")
2187
  min_anomaly_score.param.watch(_refresh_filtered_results, "value")
2188
  city_filter.param.watch(_refresh_filtered_results, "value")
@@ -2228,7 +2308,7 @@ def _build_drilldown_export_bytes() -> bytes:
2228
  if kpi_select.value and str(kpi_select.value) not in selected_kpis:
2229
  selected_kpis = [str(kpi_select.value)] + selected_kpis
2230
 
2231
- selected_kpis = [k for k in selected_kpis if k in s.columns]
2232
  base_cols = ["date_only"]
2233
  daily_cols = base_cols + selected_kpis
2234
  daily_out = s[daily_cols].copy() if selected_kpis else s[["date_only"]].copy()
@@ -2243,6 +2323,7 @@ def _build_drilldown_export_bytes() -> bytes:
2243
  summary_out = pd.DataFrame()
2244
  else:
2245
  baseline_start, baseline_end, recent_start, recent_end = windows
 
2246
  rows = []
2247
  for k in selected_kpis:
2248
  rule = _infer_rule_row(rules_df, str(rat), str(k))
@@ -2275,31 +2356,42 @@ def _build_drilldown_export_bytes() -> bytes:
2275
 
2276
  bad_flags = []
2277
  recent_vals = sk.loc[recent_mask, ["date_only", k]].sort_values("date_only")
 
2278
  for _, r in recent_vals.iterrows():
2279
  v = r.get(k)
2280
- bad_flags.append(
2281
- bool(
2282
- is_bad(
2283
- float(v) if pd.notna(v) else None,
2284
- baseline_med,
2285
- direction,
2286
- float(rel_threshold_pct.value),
2287
- sla_val,
2288
- )
2289
  )
2290
  )
 
 
 
 
 
 
 
 
 
 
 
 
2291
 
2292
  rows.append(
2293
  {
2294
  "RAT": str(rat),
2295
- "site_code": int(code),
2296
  "KPI": str(k),
2297
  "direction": direction,
2298
  "sla": sla_val,
2299
  "baseline_median": baseline_med,
2300
  "recent_median": recent_med,
2301
  "bad_days_recent": int(sum(bad_flags)),
2302
- "max_streak_recent": int(max_consecutive_days(bad_flags)),
2303
  }
2304
  )
2305
  summary_out = pd.DataFrame(rows)
@@ -2334,7 +2426,6 @@ def _drilldown_export_callback() -> io.BytesIO:
2334
  drilldown_export_button.callback = _drilldown_export_callback
2335
 
2336
 
2337
- # Page layout components (used by the multipage portal)
2338
  sidebar = pn.Column(
2339
  file_2g,
2340
  file_3g,
@@ -2373,9 +2464,7 @@ sidebar = pn.Column(
2373
  export_button,
2374
  )
2375
 
2376
- main = pn.Column(
2377
- status_pane,
2378
- validation_pane,
2379
  pn.pane.Markdown("## Datasets"),
2380
  datasets_table,
2381
  pn.pane.Markdown("## KPI Rules (editable)"),
@@ -2386,7 +2475,19 @@ main = pn.Column(
2386
  multirat_summary_table,
2387
  pn.pane.Markdown("## Top anomalies (cross-RAT)"),
2388
  top_anomalies_table,
2389
- pn.layout.Divider(),
 
 
 
 
 
 
 
 
 
 
 
 
2390
  pn.pane.Markdown("## Drill-down"),
2391
  pn.Row(site_select, rat_select),
2392
  pn.Row(kpi_group_select, kpi_group_mode),
@@ -2395,6 +2496,21 @@ main = pn.Column(
2395
  pn.Column(trend_plot_pane, sizing_mode="stretch_both", min_height=500),
2396
  pn.Column(heatmap_plot_pane, sizing_mode="stretch_both", min_height=400),
2397
  pn.Column(hist_plot_pane, sizing_mode="stretch_both", min_height=400),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2398
  )
2399
 
2400
 
 
417
  rules_table.configuration = cfg
418
  except Exception: # noqa: BLE001
419
  pass
 
420
  site_summary_table = pn.widgets.Tabulator(
421
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
422
  )
 
425
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
426
  )
427
 
428
+ complaint_multirat_summary_table = pn.widgets.Tabulator(
429
+ height=260, sizing_mode="stretch_width", layout="fit_data_table"
430
+ )
431
+
432
  top_anomalies_table = pn.widgets.Tabulator(
433
  height=260, sizing_mode="stretch_width", layout="fit_data_table"
434
  )
435
 
436
+ complaint_top_anomalies_table = pn.widgets.Tabulator(
437
+ height=260, sizing_mode="stretch_width", layout="fit_data_table"
438
+ )
439
+
440
  site_select = pn.widgets.AutocompleteInput(
441
  name="Select a site (Type to search)",
442
  options={},
 
453
  name="Normalization", options=["None", "Min-Max", "Z-score"], value="None"
454
  )
455
 
 
456
  kpi_group_select = pn.widgets.Select(
457
  name="KPI Group", options=["All (selected KPIs)"], value="All (selected KPIs)"
458
  )
 
541
  _set_tabulator_pagination(site_summary_table, page_size=50)
542
  _set_tabulator_pagination(multirat_summary_table, page_size=50)
543
  _set_tabulator_pagination(top_anomalies_table, page_size=50)
544
+ _set_tabulator_pagination(complaint_multirat_summary_table, page_size=50)
545
+ _set_tabulator_pagination(complaint_top_anomalies_table, page_size=50)
546
  _set_tabulator_pagination(site_kpi_table, page_size=50)
547
  trend_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
548
  heatmap_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
 
788
  ]
789
  kpis = sorted([str(c) for c in kpis])
790
 
 
791
  groups = get_kpis_by_group(kpis)
792
  group_options = ["All (selected KPIs)"] + sorted(
793
  [g for g in groups.keys() if g != "Other"]
 
801
  if kpi_group_select.value not in group_options:
802
  kpi_group_select.value = group_options[0]
803
 
 
804
  filtered_kpis = filter_kpis(
805
  kpis, kpi_group_select.value, mode=kpi_group_mode.value
806
  )
 
858
  except Exception: # noqa: BLE001
859
  available_sites = set()
860
 
 
 
861
  code_int = None
862
  if code is not None:
 
863
  if hasattr(site_select, "options") and isinstance(site_select.options, dict):
 
864
  if code in site_select.options:
865
  code_int = site_select.options[code]
 
866
  elif code in site_select.options.values():
867
  code_int = code
868
 
 
869
  if code_int is None:
870
  try:
871
  code_int = int(code)
 
932
  trend_plot_pane.object, heatmap_plot_pane.object, hist_plot_pane.object = cached
933
  return
934
 
 
935
  kpis_to_plot = []
936
 
 
937
  selected = [str(x) for x in (kpi_compare_select.value or []) if str(x)]
938
 
 
939
  if kpi and str(kpi) not in selected:
940
  selected = [str(kpi)] + selected
941
 
 
 
942
  if "Add top" in str(kpi_group_mode.value):
943
  from_group = filter_kpis(
944
  d.columns.tolist(), kpi_group_select.value, mode="Top-N", top_n=12
945
  )
 
946
  for gk in from_group:
947
  if gk not in selected:
948
  selected.append(gk)
949
 
 
950
  kpis_to_plot = selected[:15]
951
 
952
+ rules_df = (
953
+ rules_table.value
954
+ if isinstance(rules_table.value, pd.DataFrame)
955
+ else pd.DataFrame()
956
+ )
957
+ relevant_rules = rules_df
958
+ try:
959
+ if (
960
+ isinstance(rules_df, pd.DataFrame)
961
+ and not rules_df.empty
962
+ and "RAT" in rules_df.columns
963
+ ):
964
+ relevant_rules = rules_df[rules_df["RAT"] == rat].copy()
965
+ except Exception: # noqa: BLE001
966
+ relevant_rules = rules_df
967
 
968
  fig = build_drilldown_plot(
969
  df=d[d["site_code"] == int(code_int)],
 
975
  rat=rat,
976
  )
977
  trend_plot_pane.object = fig
 
 
 
 
 
 
978
  kpis_for_heatmap = []
979
  if (
980
  isinstance(site_df, pd.DataFrame)
 
1484
  current_top_anomalies_df = pd.DataFrame()
1485
  top_anomalies_table.value = current_top_anomalies_df
1486
 
1487
+ if current_multirat_raw is not None and not current_multirat_raw.empty:
1488
+ cm = _apply_city_filter(current_multirat_raw)
1489
+ if "is_complaint_site" in cm.columns:
1490
+ cm = cm[cm["is_complaint_site"] == True] # noqa: E712
1491
+ else:
1492
+ cm = cm.iloc[0:0].copy()
1493
+ score_col = (
1494
+ "criticality_score_weighted"
1495
+ if "criticality_score_weighted" in cm.columns
1496
+ else "criticality_score"
1497
+ )
1498
+ if score_col in cm.columns:
1499
+ cm = cm[
1500
+ pd.to_numeric(cm[score_col], errors="coerce").fillna(0)
1501
+ >= int(min_criticality.value)
1502
+ ]
1503
+ cm = cm.sort_values(by=[score_col], ascending=False)
1504
+ complaint_multirat_summary_table.value = cm
1505
+ else:
1506
+ complaint_multirat_summary_table.value = pd.DataFrame()
1507
+
1508
+ if current_top_anomalies_raw is not None and not current_top_anomalies_raw.empty:
1509
+ ct = _apply_city_filter(current_top_anomalies_raw)
1510
+ if "is_complaint_site" in ct.columns:
1511
+ ct = ct[ct["is_complaint_site"] == True] # noqa: E712
1512
+ else:
1513
+ ct = ct.iloc[0:0].copy()
1514
+ if top_rat_filter.value:
1515
+ ct = ct[ct["RAT"].isin(list(top_rat_filter.value))]
1516
+ if top_status_filter.value and "status" in ct.columns:
1517
+ ct = ct[ct["status"].isin(list(top_status_filter.value))]
1518
+ score_col = (
1519
+ "anomaly_score_weighted"
1520
+ if "anomaly_score_weighted" in ct.columns
1521
+ else "anomaly_score"
1522
+ )
1523
+ if score_col in ct.columns:
1524
+ ct = ct[
1525
+ pd.to_numeric(ct[score_col], errors="coerce").fillna(0)
1526
+ >= int(min_anomaly_score.value)
1527
+ ]
1528
+ ct = ct.sort_values(by=[score_col], ascending=False)
1529
+ complaint_top_anomalies_table.value = ct
1530
+ else:
1531
+ complaint_top_anomalies_table.value = pd.DataFrame()
1532
+
1533
  current_export_bytes = None
1534
 
1535
 
 
1875
  site_summary_table.value = pd.DataFrame()
1876
  multirat_summary_table.value = pd.DataFrame()
1877
  top_anomalies_table.value = pd.DataFrame()
1878
+ complaint_multirat_summary_table.value = pd.DataFrame()
1879
+ complaint_top_anomalies_table.value = pd.DataFrame()
1880
  site_kpi_table.value = pd.DataFrame()
1881
  trend_plot_pane.object = None
1882
  heatmap_plot_pane.object = None
 
1968
  _loading_datasets = False
1969
  try:
1970
  _update_site_options()
1971
+ except Exception: # noqa: BLE001
1972
+ pass
1973
+
1974
+ try:
1975
  _update_kpi_options()
1976
+ except Exception: # noqa: BLE001
1977
+ pass
1978
+
1979
+ try:
1980
  _update_site_view()
1981
+ except Exception: # noqa: BLE001
1982
+ pass
1983
+
1984
+ try:
1985
  _refresh_validation_state()
1986
  except Exception: # noqa: BLE001
1987
  pass
 
2144
  if isinstance(current_top_anomalies_df, pd.DataFrame)
2145
  else None
2146
  ),
2147
+ (
2148
+ complaint_multirat_summary_table.value
2149
+ if isinstance(complaint_multirat_summary_table.value, pd.DataFrame)
2150
+ else None
2151
+ ),
2152
+ (
2153
+ complaint_top_anomalies_table.value
2154
+ if isinstance(complaint_top_anomalies_table.value, pd.DataFrame)
2155
+ else None
2156
+ ),
2157
  )
2158
 
2159
 
 
2242
  except Exception: # noqa: BLE001
2243
  pass
2244
 
2245
+ try:
2246
+ complaint_top_anomalies_table.on_click(
2247
+ lambda e: _handle_double_click("top", complaint_top_anomalies_table, e)
2248
+ )
2249
+ except Exception: # noqa: BLE001
2250
+ pass
2251
+
2252
  try:
2253
  multirat_summary_table.on_click(
2254
  lambda e: _handle_double_click("multirat", multirat_summary_table, e)
 
2256
  except Exception: # noqa: BLE001
2257
  pass
2258
 
2259
+ try:
2260
+ complaint_multirat_summary_table.on_click(
2261
+ lambda e: _handle_double_click("multirat", complaint_multirat_summary_table, e)
2262
+ )
2263
+ except Exception: # noqa: BLE001
2264
+ pass
2265
+
2266
  min_criticality.param.watch(_refresh_filtered_results, "value")
2267
  min_anomaly_score.param.watch(_refresh_filtered_results, "value")
2268
  city_filter.param.watch(_refresh_filtered_results, "value")
 
2308
  if kpi_select.value and str(kpi_select.value) not in selected_kpis:
2309
  selected_kpis = [str(kpi_select.value)] + selected_kpis
2310
 
2311
+ selected_kpis = [k for k in selected_kpis if k in d.columns]
2312
  base_cols = ["date_only"]
2313
  daily_cols = base_cols + selected_kpis
2314
  daily_out = s[daily_cols].copy() if selected_kpis else s[["date_only"]].copy()
 
2323
  summary_out = pd.DataFrame()
2324
  else:
2325
  baseline_start, baseline_end, recent_start, recent_end = windows
2326
+
2327
  rows = []
2328
  for k in selected_kpis:
2329
  rule = _infer_rule_row(rules_df, str(rat), str(k))
 
2356
 
2357
  bad_flags = []
2358
  recent_vals = sk.loc[recent_mask, ["date_only", k]].sort_values("date_only")
2359
+ bad_dates: list[date] = []
2360
  for _, r in recent_vals.iterrows():
2361
  v = r.get(k)
2362
+ is_bad_day = bool(
2363
+ is_bad(
2364
+ float(v) if pd.notna(v) else None,
2365
+ baseline_med,
2366
+ direction,
2367
+ float(rel_threshold_pct.value),
2368
+ sla_val,
 
 
2369
  )
2370
  )
2371
+ bad_flags.append(is_bad_day)
2372
+ if is_bad_day:
2373
+ try:
2374
+ d0 = r.get("date_only")
2375
+ if d0 is not None:
2376
+ bad_dates.append(
2377
+ d0
2378
+ if isinstance(d0, date)
2379
+ else pd.to_datetime(d0).date()
2380
+ )
2381
+ except Exception: # noqa: BLE001
2382
+ pass
2383
 
2384
  rows.append(
2385
  {
2386
  "RAT": str(rat),
2387
+ "site_code": int(code_int),
2388
  "KPI": str(k),
2389
  "direction": direction,
2390
  "sla": sla_val,
2391
  "baseline_median": baseline_med,
2392
  "recent_median": recent_med,
2393
  "bad_days_recent": int(sum(bad_flags)),
2394
+ "max_streak_recent": int(max_consecutive_days(bad_dates)),
2395
  }
2396
  )
2397
  summary_out = pd.DataFrame(rows)
 
2426
  drilldown_export_button.callback = _drilldown_export_callback
2427
 
2428
 
 
2429
  sidebar = pn.Column(
2430
  file_2g,
2431
  file_3g,
 
2464
  export_button,
2465
  )
2466
 
2467
+ _tab_overview = pn.Column(
 
 
2468
  pn.pane.Markdown("## Datasets"),
2469
  datasets_table,
2470
  pn.pane.Markdown("## KPI Rules (editable)"),
 
2475
  multirat_summary_table,
2476
  pn.pane.Markdown("## Top anomalies (cross-RAT)"),
2477
  top_anomalies_table,
2478
+ sizing_mode="stretch_width",
2479
+ )
2480
+
2481
+ _tab_complaint = pn.Column(
2482
+ pn.pane.Markdown("## Complaint sites only"),
2483
+ pn.pane.Markdown("### Multi-RAT Summary (Complaint)"),
2484
+ complaint_multirat_summary_table,
2485
+ pn.pane.Markdown("### Top anomalies (Complaint)"),
2486
+ complaint_top_anomalies_table,
2487
+ sizing_mode="stretch_width",
2488
+ )
2489
+
2490
+ _tab_drilldown = pn.Column(
2491
  pn.pane.Markdown("## Drill-down"),
2492
  pn.Row(site_select, rat_select),
2493
  pn.Row(kpi_group_select, kpi_group_mode),
 
2496
  pn.Column(trend_plot_pane, sizing_mode="stretch_both", min_height=500),
2497
  pn.Column(heatmap_plot_pane, sizing_mode="stretch_both", min_height=400),
2498
  pn.Column(hist_plot_pane, sizing_mode="stretch_both", min_height=400),
2499
+ sizing_mode="stretch_both",
2500
+ )
2501
+
2502
+ _tabs_main = pn.Tabs(
2503
+ ("Overview", _tab_overview),
2504
+ ("Complaint sites only", _tab_complaint),
2505
+ ("Drill-down", _tab_drilldown),
2506
+ dynamic=True,
2507
+ sizing_mode="stretch_both",
2508
+ )
2509
+
2510
+ main = pn.Column(
2511
+ status_pane,
2512
+ validation_pane,
2513
+ _tabs_main,
2514
  )
2515
 
2516
 
process_kpi/kpi_health_check/export.py CHANGED
@@ -10,6 +10,8 @@ def build_export_bytes(
10
  status_df: pd.DataFrame | None,
11
  multirat_summary_df: pd.DataFrame | None = None,
12
  top_anomalies_df: pd.DataFrame | None = None,
 
 
13
  ) -> bytes:
14
  dfs = [
15
  datasets_df if isinstance(datasets_df, pd.DataFrame) else pd.DataFrame(),
@@ -26,6 +28,16 @@ def build_export_bytes(
26
  if isinstance(top_anomalies_df, pd.DataFrame)
27
  else pd.DataFrame()
28
  ),
 
 
 
 
 
 
 
 
 
 
29
  ]
30
  sheet_names = [
31
  "Datasets",
@@ -34,5 +46,7 @@ def build_export_bytes(
34
  "Site_KPI_Status",
35
  "MultiRAT_Summary",
36
  "Top_Anomalies",
 
 
37
  ]
38
  return write_dfs_to_excel(dfs, sheet_names, index=False)
 
10
  status_df: pd.DataFrame | None,
11
  multirat_summary_df: pd.DataFrame | None = None,
12
  top_anomalies_df: pd.DataFrame | None = None,
13
+ complaint_multirat_df: pd.DataFrame | None = None,
14
+ complaint_top_anomalies_df: pd.DataFrame | None = None,
15
  ) -> bytes:
16
  dfs = [
17
  datasets_df if isinstance(datasets_df, pd.DataFrame) else pd.DataFrame(),
 
28
  if isinstance(top_anomalies_df, pd.DataFrame)
29
  else pd.DataFrame()
30
  ),
31
+ (
32
+ complaint_multirat_df
33
+ if isinstance(complaint_multirat_df, pd.DataFrame)
34
+ else pd.DataFrame()
35
+ ),
36
+ (
37
+ complaint_top_anomalies_df
38
+ if isinstance(complaint_top_anomalies_df, pd.DataFrame)
39
+ else pd.DataFrame()
40
+ ),
41
  ]
42
  sheet_names = [
43
  "Datasets",
 
46
  "Site_KPI_Status",
47
  "MultiRAT_Summary",
48
  "Top_Anomalies",
49
+ "Complaint_MultiRAT",
50
+ "Complaint_Top_Anomalies",
51
  ]
52
  return write_dfs_to_excel(dfs, sheet_names, index=False)