feat: panel fig fullscreen
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
height=400,
|
| 971 |
sizing_mode="stretch_width",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
config=PLOTLY_CONFIG,
|
|
|
|
| 973 |
)
|
| 974 |
-
site_avail_plot = pn.
|
|
|
|
| 975 |
height=400,
|
| 976 |
sizing_mode="stretch_width",
|
| 977 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
height=400,
|
| 988 |
sizing_mode="stretch_width",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
config=PLOTLY_CONFIG,
|
|
|
|
| 990 |
)
|
| 991 |
-
city_avail_plot = pn.
|
|
|
|
| 992 |
height=400,
|
| 993 |
sizing_mode="stretch_width",
|
| 994 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
height=400,
|
| 1004 |
sizing_mode="stretch_width",
|
| 1005 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1024 |
height=400,
|
| 1025 |
sizing_mode="stretch_width",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
config=PLOTLY_CONFIG,
|
|
|
|
| 1027 |
)
|
| 1028 |
-
top_voice_bar_plot = pn.
|
|
|
|
| 1029 |
height=400,
|
| 1030 |
sizing_mode="stretch_width",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1031 |
config=PLOTLY_CONFIG,
|
|
|
|
| 1032 |
)
|
| 1033 |
-
data_map_plot = pn.
|
|
|
|
| 1034 |
height=500,
|
| 1035 |
sizing_mode="stretch_width",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
config=PLOTLY_CONFIG,
|
|
|
|
| 1037 |
)
|
| 1038 |
-
voice_map_plot = pn.
|
|
|
|
| 1039 |
height=500,
|
| 1040 |
sizing_mode="stretch_width",
|
| 1041 |
-
|
| 1042 |
)
|
| 1043 |
|
| 1044 |
-
#
|
| 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 |
-
|
| 1354 |
-
|
| 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 |
-
|
| 1385 |
-
|
| 1386 |
site_degraded_table.value = pd.DataFrame()
|
| 1387 |
return
|
| 1388 |
|
| 1389 |
selected_code = site_select.value
|
| 1390 |
if selected_code is None:
|
| 1391 |
-
|
| 1392 |
-
|
| 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 |
-
|
| 1401 |
-
|
| 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 |
-
|
| 1433 |
else:
|
| 1434 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1511 |
-
|
| 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 |
-
|
| 1522 |
-
|
| 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 |
-
|
| 1541 |
-
|
| 1542 |
city_degraded_table.value = pd.DataFrame()
|
| 1543 |
return
|
| 1544 |
|
| 1545 |
selected_city = city_select.value
|
| 1546 |
if not selected_city:
|
| 1547 |
-
|
| 1548 |
-
|
| 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 |
-
|
| 1557 |
-
|
| 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 |
-
|
| 1592 |
else:
|
| 1593 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1770 |
-
|
| 1771 |
-
|
| 1772 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1889 |
else:
|
| 1890 |
-
|
| 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 |
-
|
| 1942 |
else:
|
| 1943 |
-
|
| 1944 |
else:
|
| 1945 |
-
|
| 1946 |
-
|
| 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 |
-
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
|
| 2096 |
-
|
| 2097 |
-
|
| 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 |
-
|
| 2159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2160 |
|
|
|
|
| 2161 |
|
| 2162 |
-
|
| 2163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 2180 |
-
|
| 2181 |
-
|
| 2182 |
-
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
|
| 2186 |
-
|
| 2187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|