DavMelchi commited on
Commit
a3ee202
·
1 Parent(s): 0b898d7

feat: panel fig fullscreen

Browse files
Files changed (1) hide show
  1. panel_app/trafic_analysis_panel.py +190 -148
panel_app/trafic_analysis_panel.py CHANGED
@@ -16,7 +16,12 @@ if ROOT_DIR not in sys.path:
16
  from panel_app.convert_to_excel_panel import write_dfs_to_excel
17
  from utils.utils_vars import get_physical_db
18
 
19
- pn.extension("plotly", "tabulator")
 
 
 
 
 
20
 
21
 
22
  def read_fileinput_to_df(file_input: pn.widgets.FileInput) -> pd.DataFrame | None:
@@ -966,15 +971,27 @@ persistent_table = pn.widgets.Tabulator(
966
  )
967
 
968
  site_select = pn.widgets.Select(name="Select a site for detailed view", options={})
969
- site_traffic_plot = pn.pane.Plotly(
 
 
 
 
 
 
970
  height=400,
971
  sizing_mode="stretch_width",
 
 
 
 
972
  config=PLOTLY_CONFIG,
 
973
  )
974
- site_avail_plot = pn.pane.Plotly(
 
975
  height=400,
976
  sizing_mode="stretch_width",
977
- config=PLOTLY_CONFIG,
978
  )
979
  site_degraded_table = pn.widgets.Tabulator(
980
  height=200,
@@ -983,15 +1000,27 @@ site_degraded_table = pn.widgets.Tabulator(
983
  )
984
 
985
  city_select = pn.widgets.Select(name="Select a City for aggregated view", options=[])
986
- city_traffic_plot = pn.pane.Plotly(
 
 
 
 
 
 
987
  height=400,
988
  sizing_mode="stretch_width",
 
 
 
 
989
  config=PLOTLY_CONFIG,
 
990
  )
991
- city_avail_plot = pn.pane.Plotly(
 
992
  height=400,
993
  sizing_mode="stretch_width",
994
- config=PLOTLY_CONFIG,
995
  )
996
  city_degraded_table = pn.widgets.Tabulator(
997
  height=200,
@@ -999,10 +1028,16 @@ city_degraded_table = pn.widgets.Tabulator(
999
  layout="fit_data_table",
1000
  )
1001
 
1002
- daily_avail_plot = pn.pane.Plotly(
 
 
 
 
 
 
1003
  height=400,
1004
  sizing_mode="stretch_width",
1005
- config=PLOTLY_CONFIG,
1006
  )
1007
  daily_degraded_table = pn.widgets.Tabulator(
1008
  height=200,
@@ -1020,33 +1055,52 @@ top_voice_sites_table = pn.widgets.Tabulator(
1020
  sizing_mode="stretch_width",
1021
  layout="fit_data_table",
1022
  )
1023
- top_data_bar_plot = pn.pane.Plotly(
 
 
 
 
 
 
1024
  height=400,
1025
  sizing_mode="stretch_width",
 
 
 
 
1026
  config=PLOTLY_CONFIG,
 
1027
  )
1028
- top_voice_bar_plot = pn.pane.Plotly(
 
1029
  height=400,
1030
  sizing_mode="stretch_width",
 
 
 
 
1031
  config=PLOTLY_CONFIG,
 
1032
  )
1033
- data_map_plot = pn.pane.Plotly(
 
1034
  height=500,
1035
  sizing_mode="stretch_width",
 
 
 
 
1036
  config=PLOTLY_CONFIG,
 
1037
  )
1038
- voice_map_plot = pn.pane.Plotly(
 
1039
  height=500,
1040
  sizing_mode="stretch_width",
1041
- config=PLOTLY_CONFIG,
1042
  )
1043
 
1044
- # Shared pane used inside the fullscreen modal
1045
- fullscreen_plot = pn.pane.Plotly(
1046
- sizing_mode="stretch_both",
1047
- min_height=700,
1048
- config=PLOTLY_CONFIG,
1049
- )
1050
 
1051
  # Fullscreen buttons for each Plotly plot
1052
  site_traffic_fullscreen_btn = pn.widgets.Button(
@@ -1350,8 +1404,8 @@ def _update_site_controls() -> None:
1350
  if current_analysis_df is None or current_analysis_df.empty:
1351
  site_select.options = {}
1352
  site_select.value = None
1353
- site_traffic_plot.object = None
1354
- site_avail_plot.object = None
1355
  site_degraded_table.value = pd.DataFrame()
1356
  return
1357
 
@@ -1381,15 +1435,15 @@ def _update_site_controls() -> None:
1381
  def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1382
  """Update site drill-down plots and table from current_analysis_df and site_select."""
1383
  if current_analysis_df is None or current_analysis_df.empty:
1384
- site_traffic_plot.object = None
1385
- site_avail_plot.object = None
1386
  site_degraded_table.value = pd.DataFrame()
1387
  return
1388
 
1389
  selected_code = site_select.value
1390
  if selected_code is None:
1391
- site_traffic_plot.object = None
1392
- site_avail_plot.object = None
1393
  site_degraded_table.value = pd.DataFrame()
1394
  return
1395
 
@@ -1397,8 +1451,8 @@ def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1397
  current_analysis_df["code"] == int(selected_code)
1398
  ].copy()
1399
  if site_detail_df.empty:
1400
- site_traffic_plot.object = None
1401
- site_avail_plot.object = None
1402
  site_degraded_table.value = pd.DataFrame()
1403
  return
1404
 
@@ -1429,9 +1483,9 @@ def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1429
  plot_bgcolor="white",
1430
  paper_bgcolor="white",
1431
  )
1432
- site_traffic_plot.object = fig_traffic
1433
  else:
1434
- site_traffic_plot.object = None
1435
 
1436
  # Availability over time per RAT
1437
  avail_cols: list[str] = []
@@ -1468,7 +1522,7 @@ def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1468
  plot_bgcolor="white",
1469
  paper_bgcolor="white",
1470
  )
1471
- site_avail_plot.object = fig_avail
1472
 
1473
  # Days with availability below SLA per RAT
1474
  site_detail_df["date_only"] = site_detail_df["date"].dt.date
@@ -1498,7 +1552,7 @@ def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1498
  else:
1499
  site_degraded_table.value = pd.DataFrame()
1500
  else:
1501
- site_avail_plot.object = None
1502
  site_degraded_table.value = pd.DataFrame()
1503
 
1504
 
@@ -1507,8 +1561,8 @@ def _update_city_controls() -> None:
1507
  if current_analysis_df is None or current_analysis_df.empty:
1508
  city_select.options = []
1509
  city_select.value = None
1510
- city_traffic_plot.object = None
1511
- city_avail_plot.object = None
1512
  city_degraded_table.value = pd.DataFrame()
1513
  return
1514
 
@@ -1518,8 +1572,8 @@ def _update_city_controls() -> None:
1518
  ):
1519
  city_select.options = []
1520
  city_select.value = None
1521
- city_traffic_plot.object = None
1522
- city_avail_plot.object = pd.DataFrame()
1523
  city_degraded_table.value = pd.DataFrame()
1524
  return
1525
 
@@ -1537,15 +1591,15 @@ def _update_city_controls() -> None:
1537
  def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1538
  """Update city drill-down plots and degraded days table based on city_select."""
1539
  if current_analysis_df is None or current_analysis_df.empty:
1540
- city_traffic_plot.object = None
1541
- city_avail_plot.object = None
1542
  city_degraded_table.value = pd.DataFrame()
1543
  return
1544
 
1545
  selected_city = city_select.value
1546
  if not selected_city:
1547
- city_traffic_plot.object = None
1548
- city_avail_plot.object = None
1549
  city_degraded_table.value = pd.DataFrame()
1550
  return
1551
 
@@ -1553,8 +1607,8 @@ def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1553
  current_analysis_df["City"] == selected_city
1554
  ].copy()
1555
  if city_detail_df.empty:
1556
- city_traffic_plot.object = None
1557
- city_avail_plot.object = None
1558
  city_degraded_table.value = pd.DataFrame()
1559
  return
1560
 
@@ -1588,9 +1642,9 @@ def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1588
  plot_bgcolor="white",
1589
  paper_bgcolor="white",
1590
  )
1591
- city_traffic_plot.object = fig_traffic_city
1592
  else:
1593
- city_traffic_plot.object = None
1594
 
1595
  # Availability aggregated at city level
1596
  avail_cols_city: list[str] = []
@@ -1627,7 +1681,7 @@ def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1627
  plot_bgcolor="white",
1628
  paper_bgcolor="white",
1629
  )
1630
- city_avail_plot.object = fig_avail_city
1631
 
1632
  city_detail_df["date_only"] = city_detail_df["date"].dt.date
1633
  degraded_rows_city: list[dict] = []
@@ -1656,14 +1710,14 @@ def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1656
  else:
1657
  city_degraded_table.value = pd.DataFrame()
1658
  else:
1659
- city_avail_plot.object = None
1660
  city_degraded_table.value = pd.DataFrame()
1661
 
1662
 
1663
  def _update_daily_availability_view() -> None:
1664
  """Daily average availability per RAT over the full analysis_df."""
1665
  if current_analysis_df is None or current_analysis_df.empty:
1666
- daily_avail_plot.object = None
1667
  daily_degraded_table.value = pd.DataFrame()
1668
  return
1669
 
@@ -1672,7 +1726,7 @@ def _update_daily_availability_view() -> None:
1672
  col in temp_df.columns
1673
  for col in ["2g_tch_avail", "3g_cell_avail", "lte_cell_avail"]
1674
  ):
1675
- daily_avail_plot.object = None
1676
  daily_degraded_table.value = pd.DataFrame()
1677
  return
1678
 
@@ -1693,7 +1747,7 @@ def _update_daily_availability_view() -> None:
1693
  )
1694
 
1695
  if daily_avail.empty:
1696
- daily_avail_plot.object = None
1697
  daily_degraded_table.value = pd.DataFrame()
1698
  return
1699
 
@@ -1709,7 +1763,7 @@ def _update_daily_availability_view() -> None:
1709
 
1710
  value_cols = [c for c in daily_avail.columns if c != "date_only"]
1711
  if not value_cols:
1712
- daily_avail_plot.object = None
1713
  daily_degraded_table.value = pd.DataFrame()
1714
  return
1715
 
@@ -1733,7 +1787,7 @@ def _update_daily_availability_view() -> None:
1733
  plot_bgcolor="white",
1734
  paper_bgcolor="white",
1735
  )
1736
- daily_avail_plot.object = fig
1737
 
1738
  degraded_rows: list[dict] = []
1739
  for rat_name, sla_value in [
@@ -1766,10 +1820,10 @@ def _update_top_sites_and_maps() -> None:
1766
  if current_analysis_last_period_df is None or current_analysis_last_period_df.empty:
1767
  top_data_sites_table.value = pd.DataFrame()
1768
  top_voice_sites_table.value = pd.DataFrame()
1769
- top_data_bar_plot.object = None
1770
- top_voice_bar_plot.object = None
1771
- data_map_plot.object = None
1772
- voice_map_plot.object = None
1773
  return
1774
 
1775
  df = current_analysis_last_period_df
@@ -1800,7 +1854,7 @@ def _update_top_sites_and_maps() -> None:
1800
  plot_bgcolor="white",
1801
  paper_bgcolor="white",
1802
  )
1803
- top_data_bar_plot.object = fig_data
1804
 
1805
  # Top sites by voice traffic
1806
  top_sites_voice = (
@@ -1829,7 +1883,7 @@ def _update_top_sites_and_maps() -> None:
1829
  plot_bgcolor="white",
1830
  paper_bgcolor="white",
1831
  )
1832
- top_voice_bar_plot.object = fig_voice
1833
 
1834
  # Maps
1835
  if {"Latitude", "Longitude"}.issubset(df.columns):
@@ -1885,9 +1939,9 @@ def _update_top_sites_and_maps() -> None:
1885
  coloraxis=dict(cmin=traffic_data_min, cmax=traffic_data_max),
1886
  font=dict(size=10, color="black"),
1887
  )
1888
- data_map_plot.object = fig_map_data
1889
  else:
1890
- data_map_plot.object = None
1891
 
1892
  # Voice traffic map
1893
  df_voice = (
@@ -1938,12 +1992,12 @@ def _update_top_sites_and_maps() -> None:
1938
  coloraxis=dict(cmin=traffic_voice_min, cmax=traffic_voice_max),
1939
  font=dict(size=10, color="black"),
1940
  )
1941
- voice_map_plot.object = fig_map_voice
1942
  else:
1943
- voice_map_plot.object = None
1944
  else:
1945
- data_map_plot.object = None
1946
- voice_map_plot.object = None
1947
 
1948
 
1949
  def _update_persistent_table_view(event=None) -> None: # noqa: D401, ARG001
@@ -2088,79 +2142,44 @@ def _download_top_voice_sites() -> io.BytesIO:
2088
  return _df_to_csv_bytes(value if isinstance(value, pd.DataFrame) else None)
2089
 
2090
 
2091
- def _open_fullscreen_from_pane(plot_pane: pn.pane.Plotly, title: str) -> None:
2092
- """Open the given plot in the template modal as fullscreen view."""
2093
- if plot_pane.object is None:
2094
- return
2095
-
2096
- fullscreen_plot.object = plot_pane.object
2097
- content = pn.Column(
2098
- pn.pane.Markdown(f"### {title}"),
2099
- fullscreen_plot,
2100
- sizing_mode="stretch_both",
2101
- styles={"width": "95vw", "height": "90vh"},
2102
- )
2103
-
2104
- if "template" not in globals():
2105
- return
2106
-
2107
- # Always populate modal content first
2108
- if hasattr(template, "modal"):
2109
- try:
2110
- template.modal[:] = [content]
2111
- except Exception: # noqa: BLE001
2112
- try:
2113
- template.modal.clear()
2114
- template.modal.append(content)
2115
- except Exception: # noqa: BLE001
2116
- pass
2117
-
2118
- # Preferred API on templates
2119
- if hasattr(template, "open_modal"):
2120
- template.open_modal()
2121
- return
2122
-
2123
- # Fallbacks across versions
2124
- if hasattr(template, "modal") and hasattr(template.modal, "open"):
2125
- template.modal.open = True
2126
- if hasattr(template, "modal") and hasattr(template.modal, "visible"):
2127
- template.modal.visible = True
2128
-
2129
-
2130
- def _on_site_traffic_fullscreen(event=None) -> None: # noqa: ARG001
2131
- _open_fullscreen_from_pane(site_traffic_plot, "Site traffic over time")
2132
-
2133
-
2134
- def _on_site_avail_fullscreen(event=None) -> None: # noqa: ARG001
2135
- _open_fullscreen_from_pane(site_avail_plot, "Site availability over time")
2136
-
2137
-
2138
- def _on_city_traffic_fullscreen(event=None) -> None: # noqa: ARG001
2139
- _open_fullscreen_from_pane(city_traffic_plot, "City traffic over time")
2140
-
2141
-
2142
- def _on_city_avail_fullscreen(event=None) -> None: # noqa: ARG001
2143
- _open_fullscreen_from_pane(city_avail_plot, "City availability over time")
2144
-
2145
-
2146
- def _on_daily_avail_fullscreen(event=None) -> None: # noqa: ARG001
2147
- _open_fullscreen_from_pane(daily_avail_plot, "Daily average availability per RAT")
2148
-
2149
-
2150
- def _on_top_data_fullscreen(event=None) -> None: # noqa: ARG001
2151
- _open_fullscreen_from_pane(top_data_bar_plot, "Top sites by data traffic")
2152
-
2153
-
2154
- def _on_top_voice_fullscreen(event=None) -> None: # noqa: ARG001
2155
- _open_fullscreen_from_pane(top_voice_bar_plot, "Top sites by voice traffic")
2156
 
 
 
 
 
2157
 
2158
- def _on_data_map_fullscreen(event=None) -> None: # noqa: ARG001
2159
- _open_fullscreen_from_pane(data_map_plot, "Data traffic map")
 
 
 
 
 
 
 
2160
 
 
2161
 
2162
- def _on_voice_map_fullscreen(event=None) -> None: # noqa: ARG001
2163
- _open_fullscreen_from_pane(voice_map_plot, "Voice traffic map")
 
 
 
 
 
 
 
 
 
 
 
2164
 
2165
 
2166
  # Reactive bindings for drill-down controls & export
@@ -2176,15 +2195,44 @@ persistent_download.callback = _download_persistent_table
2176
  top_data_download.callback = _download_top_data_sites
2177
  top_voice_download.callback = _download_top_voice_sites
2178
 
2179
- site_traffic_fullscreen_btn.on_click(_on_site_traffic_fullscreen)
2180
- site_avail_fullscreen_btn.on_click(_on_site_avail_fullscreen)
2181
- city_traffic_fullscreen_btn.on_click(_on_city_traffic_fullscreen)
2182
- city_avail_fullscreen_btn.on_click(_on_city_avail_fullscreen)
2183
- daily_avail_fullscreen_btn.on_click(_on_daily_avail_fullscreen)
2184
- top_data_fullscreen_btn.on_click(_on_top_data_fullscreen)
2185
- top_voice_fullscreen_btn.on_click(_on_top_voice_fullscreen)
2186
- data_map_fullscreen_btn.on_click(_on_data_map_fullscreen)
2187
- voice_map_fullscreen_btn.on_click(_on_voice_map_fullscreen)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2188
 
2189
 
2190
  # --------------------------------------------------------------------------------------
@@ -2197,13 +2245,7 @@ template = pn.template.MaterialTemplate(
2197
  )
2198
 
2199
  # Ensure the template modal is large enough for fullscreen charts
2200
- template.modal.sizing_mode = "stretch_both"
2201
- template.modal.styles = {
2202
- "width": "95vw",
2203
- "height": "90vh",
2204
- "maxWidth": "95vw",
2205
- "maxHeight": "90vh",
2206
- }
2207
 
2208
  sidebar_content = pn.Column(
2209
  """This Panel app is a migration of the existing Streamlit-based global traffic analysis.
 
16
  from panel_app.convert_to_excel_panel import write_dfs_to_excel
17
  from utils.utils_vars import get_physical_db
18
 
19
+ pn.extension("plotly", "tabulator", raw_css=[
20
+ ":fullscreen { background-color: white; overflow: auto; }",
21
+ "::backdrop { background-color: white; }",
22
+ ".plot-fullscreen-wrapper:fullscreen { padding: 20px; display: flex; flex-direction: column; }",
23
+ ".plot-fullscreen-wrapper:fullscreen > * { height: 100% !important; width: 100% !important; }",
24
+ ])
25
 
26
 
27
  def read_fileinput_to_df(file_input: pn.widgets.FileInput) -> pd.DataFrame | None:
 
971
  )
972
 
973
  site_select = pn.widgets.Select(name="Select a site for detailed view", options={})
974
+ site_traffic_plot_pane = pn.pane.Plotly(
975
+ sizing_mode="stretch_both",
976
+ config=PLOTLY_CONFIG,
977
+ css_classes=["fullscreen-target-site-traffic"],
978
+ )
979
+ site_traffic_plot = pn.Column(
980
+ site_traffic_plot_pane,
981
  height=400,
982
  sizing_mode="stretch_width",
983
+ css_classes=["plot-fullscreen-wrapper", "site-traffic-wrapper"],
984
+ )
985
+ site_avail_plot_pane = pn.pane.Plotly(
986
+ sizing_mode="stretch_both",
987
  config=PLOTLY_CONFIG,
988
+ css_classes=["fullscreen-target-site-avail"],
989
  )
990
+ site_avail_plot = pn.Column(
991
+ site_avail_plot_pane,
992
  height=400,
993
  sizing_mode="stretch_width",
994
+ css_classes=["plot-fullscreen-wrapper", "site-avail-wrapper"],
995
  )
996
  site_degraded_table = pn.widgets.Tabulator(
997
  height=200,
 
1000
  )
1001
 
1002
  city_select = pn.widgets.Select(name="Select a City for aggregated view", options=[])
1003
+ city_traffic_plot_pane = pn.pane.Plotly(
1004
+ sizing_mode="stretch_both",
1005
+ config=PLOTLY_CONFIG,
1006
+ css_classes=["fullscreen-target-city-traffic"],
1007
+ )
1008
+ city_traffic_plot = pn.Column(
1009
+ city_traffic_plot_pane,
1010
  height=400,
1011
  sizing_mode="stretch_width",
1012
+ css_classes=["plot-fullscreen-wrapper", "city-traffic-wrapper"],
1013
+ )
1014
+ city_avail_plot_pane = pn.pane.Plotly(
1015
+ sizing_mode="stretch_both",
1016
  config=PLOTLY_CONFIG,
1017
+ css_classes=["fullscreen-target-city-avail"],
1018
  )
1019
+ city_avail_plot = pn.Column(
1020
+ city_avail_plot_pane,
1021
  height=400,
1022
  sizing_mode="stretch_width",
1023
+ css_classes=["plot-fullscreen-wrapper", "city-avail-wrapper"],
1024
  )
1025
  city_degraded_table = pn.widgets.Tabulator(
1026
  height=200,
 
1028
  layout="fit_data_table",
1029
  )
1030
 
1031
+ daily_avail_plot_pane = pn.pane.Plotly(
1032
+ sizing_mode="stretch_both",
1033
+ config=PLOTLY_CONFIG,
1034
+ css_classes=["fullscreen-target-daily-avail"],
1035
+ )
1036
+ daily_avail_plot = pn.Column(
1037
+ daily_avail_plot_pane,
1038
  height=400,
1039
  sizing_mode="stretch_width",
1040
+ css_classes=["plot-fullscreen-wrapper", "daily-avail-wrapper"],
1041
  )
1042
  daily_degraded_table = pn.widgets.Tabulator(
1043
  height=200,
 
1055
  sizing_mode="stretch_width",
1056
  layout="fit_data_table",
1057
  )
1058
+ top_data_bar_plot_pane = pn.pane.Plotly(
1059
+ sizing_mode="stretch_both",
1060
+ config=PLOTLY_CONFIG,
1061
+ css_classes=["fullscreen-target-top-data"],
1062
+ )
1063
+ top_data_bar_plot = pn.Column(
1064
+ top_data_bar_plot_pane,
1065
  height=400,
1066
  sizing_mode="stretch_width",
1067
+ css_classes=["plot-fullscreen-wrapper", "top-data-bar-wrapper"],
1068
+ )
1069
+ top_voice_bar_plot_pane = pn.pane.Plotly(
1070
+ sizing_mode="stretch_both",
1071
  config=PLOTLY_CONFIG,
1072
+ css_classes=["fullscreen-target-top-voice"],
1073
  )
1074
+ top_voice_bar_plot = pn.Column(
1075
+ top_voice_bar_plot_pane,
1076
  height=400,
1077
  sizing_mode="stretch_width",
1078
+ css_classes=["plot-fullscreen-wrapper", "top-voice-bar-wrapper"],
1079
+ )
1080
+ data_map_plot_pane = pn.pane.Plotly(
1081
+ sizing_mode="stretch_both",
1082
  config=PLOTLY_CONFIG,
1083
+ css_classes=["fullscreen-target-data-map"],
1084
  )
1085
+ data_map_plot = pn.Column(
1086
+ data_map_plot_pane,
1087
  height=500,
1088
  sizing_mode="stretch_width",
1089
+ css_classes=["plot-fullscreen-wrapper", "data-map-wrapper"],
1090
+ )
1091
+ voice_map_plot_pane = pn.pane.Plotly(
1092
+ sizing_mode="stretch_both",
1093
  config=PLOTLY_CONFIG,
1094
+ css_classes=["fullscreen-target-voice-map"],
1095
  )
1096
+ voice_map_plot = pn.Column(
1097
+ voice_map_plot_pane,
1098
  height=500,
1099
  sizing_mode="stretch_width",
1100
+ css_classes=["plot-fullscreen-wrapper", "voice-map-wrapper"],
1101
  )
1102
 
1103
+ # Fullscreen helper logic has been replaced by client-side JS.
 
 
 
 
 
1104
 
1105
  # Fullscreen buttons for each Plotly plot
1106
  site_traffic_fullscreen_btn = pn.widgets.Button(
 
1404
  if current_analysis_df is None or current_analysis_df.empty:
1405
  site_select.options = {}
1406
  site_select.value = None
1407
+ site_traffic_plot_pane.object = None
1408
+ site_avail_plot_pane.object = None
1409
  site_degraded_table.value = pd.DataFrame()
1410
  return
1411
 
 
1435
  def _update_site_view(event=None) -> None: # noqa: D401, ARG001
1436
  """Update site drill-down plots and table from current_analysis_df and site_select."""
1437
  if current_analysis_df is None or current_analysis_df.empty:
1438
+ site_traffic_plot_pane.object = None
1439
+ site_avail_plot_pane.object = None
1440
  site_degraded_table.value = pd.DataFrame()
1441
  return
1442
 
1443
  selected_code = site_select.value
1444
  if selected_code is None:
1445
+ site_traffic_plot_pane.object = None
1446
+ site_avail_plot_pane.object = None
1447
  site_degraded_table.value = pd.DataFrame()
1448
  return
1449
 
 
1451
  current_analysis_df["code"] == int(selected_code)
1452
  ].copy()
1453
  if site_detail_df.empty:
1454
+ site_traffic_plot_pane.object = None
1455
+ site_avail_plot_pane.object = None
1456
  site_degraded_table.value = pd.DataFrame()
1457
  return
1458
 
 
1483
  plot_bgcolor="white",
1484
  paper_bgcolor="white",
1485
  )
1486
+ site_traffic_plot_pane.object = fig_traffic
1487
  else:
1488
+ site_traffic_plot_pane.object = None
1489
 
1490
  # Availability over time per RAT
1491
  avail_cols: list[str] = []
 
1522
  plot_bgcolor="white",
1523
  paper_bgcolor="white",
1524
  )
1525
+ site_avail_plot_pane.object = fig_avail
1526
 
1527
  # Days with availability below SLA per RAT
1528
  site_detail_df["date_only"] = site_detail_df["date"].dt.date
 
1552
  else:
1553
  site_degraded_table.value = pd.DataFrame()
1554
  else:
1555
+ site_avail_plot_pane.object = None
1556
  site_degraded_table.value = pd.DataFrame()
1557
 
1558
 
 
1561
  if current_analysis_df is None or current_analysis_df.empty:
1562
  city_select.options = []
1563
  city_select.value = None
1564
+ city_traffic_plot_pane.object = None
1565
+ city_avail_plot_pane.object = None
1566
  city_degraded_table.value = pd.DataFrame()
1567
  return
1568
 
 
1572
  ):
1573
  city_select.options = []
1574
  city_select.value = None
1575
+ city_traffic_plot_pane.object = None
1576
+ city_avail_plot_pane.object = pd.DataFrame()
1577
  city_degraded_table.value = pd.DataFrame()
1578
  return
1579
 
 
1591
  def _update_city_view(event=None) -> None: # noqa: D401, ARG001
1592
  """Update city drill-down plots and degraded days table based on city_select."""
1593
  if current_analysis_df is None or current_analysis_df.empty:
1594
+ city_traffic_plot_pane.object = None
1595
+ city_avail_plot_pane.object = None
1596
  city_degraded_table.value = pd.DataFrame()
1597
  return
1598
 
1599
  selected_city = city_select.value
1600
  if not selected_city:
1601
+ city_traffic_plot_pane.object = None
1602
+ city_avail_plot_pane.object = None
1603
  city_degraded_table.value = pd.DataFrame()
1604
  return
1605
 
 
1607
  current_analysis_df["City"] == selected_city
1608
  ].copy()
1609
  if city_detail_df.empty:
1610
+ city_traffic_plot_pane.object = None
1611
+ city_avail_plot_pane.object = None
1612
  city_degraded_table.value = pd.DataFrame()
1613
  return
1614
 
 
1642
  plot_bgcolor="white",
1643
  paper_bgcolor="white",
1644
  )
1645
+ city_traffic_plot_pane.object = fig_traffic_city
1646
  else:
1647
+ city_traffic_plot_pane.object = None
1648
 
1649
  # Availability aggregated at city level
1650
  avail_cols_city: list[str] = []
 
1681
  plot_bgcolor="white",
1682
  paper_bgcolor="white",
1683
  )
1684
+ city_avail_plot_pane.object = fig_avail_city
1685
 
1686
  city_detail_df["date_only"] = city_detail_df["date"].dt.date
1687
  degraded_rows_city: list[dict] = []
 
1710
  else:
1711
  city_degraded_table.value = pd.DataFrame()
1712
  else:
1713
+ city_avail_plot_pane.object = None
1714
  city_degraded_table.value = pd.DataFrame()
1715
 
1716
 
1717
  def _update_daily_availability_view() -> None:
1718
  """Daily average availability per RAT over the full analysis_df."""
1719
  if current_analysis_df is None or current_analysis_df.empty:
1720
+ daily_avail_plot_pane.object = None
1721
  daily_degraded_table.value = pd.DataFrame()
1722
  return
1723
 
 
1726
  col in temp_df.columns
1727
  for col in ["2g_tch_avail", "3g_cell_avail", "lte_cell_avail"]
1728
  ):
1729
+ daily_avail_plot_pane.object = None
1730
  daily_degraded_table.value = pd.DataFrame()
1731
  return
1732
 
 
1747
  )
1748
 
1749
  if daily_avail.empty:
1750
+ daily_avail_plot_pane.object = None
1751
  daily_degraded_table.value = pd.DataFrame()
1752
  return
1753
 
 
1763
 
1764
  value_cols = [c for c in daily_avail.columns if c != "date_only"]
1765
  if not value_cols:
1766
+ daily_avail_plot_pane.object = None
1767
  daily_degraded_table.value = pd.DataFrame()
1768
  return
1769
 
 
1787
  plot_bgcolor="white",
1788
  paper_bgcolor="white",
1789
  )
1790
+ daily_avail_plot_pane.object = fig
1791
 
1792
  degraded_rows: list[dict] = []
1793
  for rat_name, sla_value in [
 
1820
  if current_analysis_last_period_df is None or current_analysis_last_period_df.empty:
1821
  top_data_sites_table.value = pd.DataFrame()
1822
  top_voice_sites_table.value = pd.DataFrame()
1823
+ top_data_bar_plot_pane.object = None
1824
+ top_voice_bar_plot_pane.object = None
1825
+ data_map_plot_pane.object = None
1826
+ voice_map_plot_pane.object = None
1827
  return
1828
 
1829
  df = current_analysis_last_period_df
 
1854
  plot_bgcolor="white",
1855
  paper_bgcolor="white",
1856
  )
1857
+ top_data_bar_plot_pane.object = fig_data
1858
 
1859
  # Top sites by voice traffic
1860
  top_sites_voice = (
 
1883
  plot_bgcolor="white",
1884
  paper_bgcolor="white",
1885
  )
1886
+ top_voice_bar_plot_pane.object = fig_voice
1887
 
1888
  # Maps
1889
  if {"Latitude", "Longitude"}.issubset(df.columns):
 
1939
  coloraxis=dict(cmin=traffic_data_min, cmax=traffic_data_max),
1940
  font=dict(size=10, color="black"),
1941
  )
1942
+ data_map_plot_pane.object = fig_map_data
1943
  else:
1944
+ data_map_plot_pane.object = None
1945
 
1946
  # Voice traffic map
1947
  df_voice = (
 
1992
  coloraxis=dict(cmin=traffic_voice_min, cmax=traffic_voice_max),
1993
  font=dict(size=10, color="black"),
1994
  )
1995
+ voice_map_plot_pane.object = fig_map_voice
1996
  else:
1997
+ voice_map_plot_pane.object = None
1998
  else:
1999
+ data_map_plot_pane.object = None
2000
+ voice_map_plot_pane.object = None
2001
 
2002
 
2003
  def _update_persistent_table_view(event=None) -> None: # noqa: D401, ARG001
 
2142
  return _df_to_csv_bytes(value if isinstance(value, pd.DataFrame) else None)
2143
 
2144
 
2145
+ # Client-side Fullscreen JS logic
2146
+ # We target the specific CSS class assigned to each plot pane.
2147
+ # Client-side Fullscreen JS logic with Shadow DOM support
2148
+ _JS_FULLSCREEN = """
2149
+ function findDeep(root, cls) {
2150
+ if (!root) return null;
2151
+ if (root.classList && root.classList.contains(cls)) return root;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2152
 
2153
+ if (root.shadowRoot) {
2154
+ var found = findDeep(root.shadowRoot, cls);
2155
+ if (found) return found;
2156
+ }
2157
 
2158
+ var children = root.children;
2159
+ if (children) {
2160
+ for (var i = 0; i < children.length; i++) {
2161
+ var found = findDeep(children[i], cls);
2162
+ if (found) return found;
2163
+ }
2164
+ }
2165
+ return null;
2166
+ }
2167
 
2168
+ var el = findDeep(document.body, target_class);
2169
 
2170
+ if (el) {
2171
+ if (el.requestFullscreen) {
2172
+ el.requestFullscreen();
2173
+ } else if (el.webkitRequestFullscreen) {
2174
+ el.webkitRequestFullscreen();
2175
+ } else if (el.msRequestFullscreen) {
2176
+ el.msRequestFullscreen();
2177
+ }
2178
+ } else {
2179
+ // Debug info
2180
+ alert("Impossible de passer en plein écran : élément '" + target_class + "' introuvable même après recherche approfondie (Shadow DOM).");
2181
+ }
2182
+ """
2183
 
2184
 
2185
  # Reactive bindings for drill-down controls & export
 
2195
  top_data_download.callback = _download_top_data_sites
2196
  top_voice_download.callback = _download_top_voice_sites
2197
 
2198
+ site_traffic_fullscreen_btn.js_on_click(
2199
+ args={"target_class": "site-traffic-wrapper"},
2200
+ code=_JS_FULLSCREEN,
2201
+ )
2202
+ site_avail_fullscreen_btn.js_on_click(
2203
+ args={"target_class": "site-avail-wrapper"},
2204
+ code=_JS_FULLSCREEN,
2205
+ )
2206
+ city_traffic_fullscreen_btn.js_on_click(
2207
+ args={"target_class": "city-traffic-wrapper"},
2208
+ code=_JS_FULLSCREEN,
2209
+ )
2210
+ city_avail_fullscreen_btn.js_on_click(
2211
+ args={"target_class": "city-avail-wrapper"},
2212
+ code=_JS_FULLSCREEN,
2213
+ )
2214
+ daily_avail_fullscreen_btn.js_on_click(
2215
+ args={"target_class": "daily-avail-wrapper"},
2216
+ code=_JS_FULLSCREEN,
2217
+ )
2218
+ top_data_fullscreen_btn.js_on_click(
2219
+ args={"target_class": "top-data-bar-wrapper"},
2220
+ code=_JS_FULLSCREEN,
2221
+ )
2222
+ top_voice_fullscreen_btn.js_on_click(
2223
+ args={
2224
+ "target_class": "top-voice-bar-wrapper",
2225
+ },
2226
+ code=_JS_FULLSCREEN,
2227
+ )
2228
+ data_map_fullscreen_btn.js_on_click(
2229
+ args={"target_class": "data-map-wrapper"},
2230
+ code=_JS_FULLSCREEN,
2231
+ )
2232
+ voice_map_fullscreen_btn.js_on_click(
2233
+ args={"target_class": "voice-map-wrapper"},
2234
+ code=_JS_FULLSCREEN,
2235
+ )
2236
 
2237
 
2238
  # --------------------------------------------------------------------------------------
 
2245
  )
2246
 
2247
  # Ensure the template modal is large enough for fullscreen charts
2248
+ # Modal CSS override removed as we switched to native fullscreen.
 
 
 
 
 
 
2249
 
2250
  sidebar_content = pn.Column(
2251
  """This Panel app is a migration of the existing Streamlit-based global traffic analysis.