@@ -923,22 +923,37 @@ def update_map_features(
923923 query = query .strip ()
924924 dfs = self ._get_map_catalog ()
925925 if self .map_filters_table :
926- # In filter mode the map is decoupled from the table: always show the
927- # full map catalog (not restricted to display_table.current_view) and
928- # never highlight features based on table selection while this mode is active.
929- current_view = dfs
926+ # In filter mode the map is restricted to stations visible in the
927+ # current table view (based on header filters / query) so that the
928+ # user can see which stations are available before clicking to select.
929+ # When nothing is selected on the map yet the full filtered set is
930+ # shown; once a map click fires, select_data_catalog filters the table.
931+ table_view = self ._safe_current_view ()
932+ if (
933+ self ._station_id_column
934+ and self ._station_id_column in table_view .columns
935+ ):
936+ current_view = dfs [
937+ dfs [self ._station_id_column ].isin (
938+ table_view [self ._station_id_column ]
939+ )
940+ ]
941+ else :
942+ current_view = dfs .loc [
943+ dfs .index .isin (table_view .index )
944+ ]
930945 if isinstance (current_view , gpd .GeoDataFrame ):
931946 current_view = current_view .loc [current_view .is_valid ]
932947 current_selection = []
933948 else :
934949 # select only those rows in dfs that have station_id_column in self.display_table.current_view
935950 if (
936951 self ._station_id_column
937- and self ._station_id_column in self .display_table . current_view .columns
952+ and self ._station_id_column in self ._safe_current_view () .columns
938953 ):
939954 current_view = dfs [
940955 dfs [self ._station_id_column ].isin (
941- self .display_table . current_view [self ._station_id_column ]
956+ self ._safe_current_view () [self ._station_id_column ]
942957 )
943958 ]
944959 # if current_view is a geodataframe, keep only valid geometries
@@ -951,7 +966,7 @@ def update_map_features(
951966 )
952967 ]
953968 else :
954- current_view = dfs .loc [self .display_table . current_view .index ]
969+ current_view = dfs .loc [self ._safe_current_view () .index ]
955970 if isinstance (current_view , gpd .GeoDataFrame ):
956971 current_view = current_view .loc [current_view .is_valid ]
957972 current_table_selected = self ._dfcat .iloc [selection ]
@@ -1114,8 +1129,8 @@ def select_data_catalog(self, index=[]):
11141129 set (stations_map_selected ) - set (stations_table_selected )
11151130 )
11161131 # get the indices of the stations that are not in the selected stations in the current view
1117- current_view_selected_indices = table . current_view [
1118- table . current_view [idcol ].isin (stations_to_be_selected )
1132+ current_view_selected_indices = self . _safe_current_view () [
1133+ self . _safe_current_view () [idcol ].isin (stations_to_be_selected )
11191134 ].index .to_list ()
11201135 # First get the indices of matching rows
11211136 matching_indices = table .selected_dataframe [
@@ -1403,20 +1418,92 @@ def _on_selection_change(event):
14031418
14041419 self .display_table .param .watch (_on_selection_change , "selection" )
14051420
1421+ def _safe_current_view (self ):
1422+ """Return ``display_table.current_view``, falling back to ``display_table.value``.
1423+
1424+ Panel's server-side ``current_view`` computation raises
1425+ ``ValueError('Regex filtering not supported.')`` when any header filter
1426+ has ``func='regex'``, because Panel cannot apply regex filters in Python.
1427+ When regex mode is active we therefore fall back to the unfiltered
1428+ ``display_table.value`` so map interactions (which only need the
1429+ station-ID column, not strict row-count accuracy) continue to work.
1430+ """
1431+ try :
1432+ return self .display_table .current_view
1433+ except ValueError :
1434+ return self .display_table .value
1435+
14061436 @param .depends ("use_regex_filter" , watch = True )
14071437 def update_data_table_filters (self ):
1408- """Update the table filters based on the use_regex_filter parameter."""
1409- if self .use_regex_filter :
1410- # Update filters to use regex
1411- for column in self .display_table .header_filters :
1412- # self.display_table.header_filters[column]["type"] = "regex"
1413- self .display_table .header_filters [column ]["func" ] = "regex"
1414- else :
1415- # Revert to 'like' functionality
1416- for column in self .display_table .header_filters :
1417- # self.display_table.header_filters[column]["type"] = "input"
1418- self .display_table .header_filters [column ]["func" ] = "like"
1419- self .display_table .header_filters = self .display_table .header_filters
1438+ """Recreate the Tabulator widget to apply changed header filter functions.
1439+
1440+ Tabulator column definitions (including ``headerFilterFunc``) are fixed
1441+ at JS initialisation time and cannot be changed reactively. A new
1442+ widget is built with the correct ``func``, current data/state is copied
1443+ across, all registered watchers are re-wired, and the old widget is
1444+ swapped out of its container.
1445+ """
1446+ if not hasattr (self , "_table_container" ):
1447+ return # called before create_view(); nothing to do
1448+
1449+ if pn .state .notifications :
1450+ pn .state .notifications .warning (
1451+ "Regex filter mode changed — rebuilding table. "
1452+ "Active filters and current selection will be cleared." ,
1453+ duration = 5000 ,
1454+ )
1455+
1456+ old = self .display_table
1457+ self .display_table = pn .widgets .Tabulator (
1458+ old .value ,
1459+ disabled = True ,
1460+ widths = old .widths ,
1461+ hidden_columns = list (old .hidden_columns or []),
1462+ show_index = False ,
1463+ sizing_mode = "stretch_width" ,
1464+ header_filters = self ._build_header_filters (self .use_regex_filter ),
1465+ sorters = list (old .sorters or []),
1466+ pagination = "local" ,
1467+ page_size = old .page_size or 200 ,
1468+ configuration = {
1469+ "headerFilterLiveFilterDelay" : 600 ,
1470+ "columnDefaults" : {"tooltip" : True },
1471+ },
1472+ )
1473+ # Re-wire watchers on the new widget.
1474+ self ._setup_selection_btn_watcher ()
1475+ for event_name , callback in getattr (self , "_tbl_watchers" , []):
1476+ self .display_table .param .watch (callback , event_name )
1477+ # Swap the widget into the live container.
1478+ self ._table_container .clear ()
1479+ self ._table_container .append (self .display_table )
1480+
1481+ def _build_header_filters (self , use_regex : bool ) -> dict :
1482+ """Return header_filters dict with ``func`` set to ``'regex'`` or ``'like'``.
1483+
1484+ Only overrides ``func`` for columns whose filter type is ``'input'``
1485+ (the default text-search type); numeric/boolean filters are left unchanged.
1486+ """
1487+ func = "regex" if use_regex else "like"
1488+ result = {}
1489+ for col , spec in self ._dataui_manager .get_table_filters ().items ():
1490+ new_spec = dict (spec )
1491+ if new_spec .get ("type" , "input" ) == "input" :
1492+ new_spec ["func" ] = func
1493+ result [col ] = new_spec
1494+ return result
1495+
1496+ def _register_table_watcher (self , event_name : str , callback ) -> None :
1497+ """Register *callback* on ``display_table`` and remember it for re-registration.
1498+
1499+ When ``update_data_table_filters`` swaps the Tabulator widget, all
1500+ callbacks registered through this method are automatically re-wired
1501+ onto the new widget.
1502+ """
1503+ if not hasattr (self , "_tbl_watchers" ):
1504+ self ._tbl_watchers = []
1505+ self ._tbl_watchers .append ((event_name , callback ))
1506+ self .display_table .param .watch (callback , event_name )
14201507
14211508 def create_data_table (self , dfs ):
14221509 column_width_map = self ._dataui_manager .get_table_column_width_map ()
@@ -1455,14 +1542,15 @@ def create_data_table(self, dfs):
14551542 if not TimeSeriesDataUIManager ._has_mixed_ref_types (dfs ):
14561543 if "ref_type" not in initial_hidden :
14571544 initial_hidden .append ("ref_type" )
1545+ self ._tbl_watchers = []
14581546 self .display_table = pn .widgets .Tabulator (
14591547 dfs ,
14601548 disabled = True ,
14611549 widths = column_width_map ,
14621550 hidden_columns = initial_hidden ,
14631551 show_index = False ,
14641552 sizing_mode = "stretch_width" ,
1465- header_filters = self ._dataui_manager . get_table_filters ( ),
1553+ header_filters = self ._build_header_filters ( self . use_regex_filter ),
14661554 pagination = "local" ,
14671555 page_size = 200 ,
14681556 configuration = {
@@ -1506,7 +1594,8 @@ def create_data_table(self, dfs):
15061594 )
15071595 )
15081596 gspec = pn .GridStack (sizing_mode = "stretch_both" , allow_resize = True , allow_drag = False )
1509- gspec [0 :4 , 0 :10 ] = fullscreen .FullScreen (pn .Row (self .display_table , scroll = True ))
1597+ self ._table_container = pn .Row (self .display_table , scroll = True )
1598+ gspec [0 :4 , 0 :10 ] = fullscreen .FullScreen (self ._table_container )
15101599 gspec [5 :14 , 0 :10 ] = fullscreen .FullScreen (self ._display_panel )
15111600 self ._main_panel = gspec
15121601 return gspec
@@ -1633,7 +1722,7 @@ def _on_selection_change(event):
16331722 loc .update_query (sel = sel_encoded )
16341723
16351724 if hasattr (self , "display_table" ):
1636- self .display_table . param . watch ( _on_selection_change , "selection" )
1725+ self ._register_table_watcher ( "selection" , _on_selection_change )
16371726
16381727 def _setup_filter_url_sync (self ):
16391728 """Sync table header filter values with the URL ``flt`` query parameter.
@@ -1747,7 +1836,7 @@ def _create_main_view(self):
17471836 """Create the main view content based on the current view_type"""
17481837 if self .view_type == "table" :
17491838 gspec = pn .GridStack (sizing_mode = "stretch_both" , allow_resize = False , allow_drag = False )
1750- gspec [0 :14 , 0 :10 ] = fullscreen .FullScreen (pn . Row ( self .display_table , scroll = True ) )
1839+ gspec [0 :14 , 0 :10 ] = fullscreen .FullScreen (self ._table_container )
17511840 return pn .Column (self ._action_panel , gspec , sizing_mode = "stretch_both" )
17521841 elif self .view_type == "display" :
17531842 gspec = pn .GridStack (sizing_mode = "stretch_both" , allow_resize = False , allow_drag = False )
@@ -2093,11 +2182,13 @@ def _on_tbl_selection(event):
20932182 self ._map_sel_proxy = list (event .new )
20942183
20952184 def _on_tbl_filters (event ):
2096- if not self .map_filters_table :
2097- self ._map_filter_proxy += 1
2185+ # Always refresh the map when table filters change:
2186+ # - map_filters_table=False: map highlights the filtered subset
2187+ # - map_filters_table=True: map restricts to filtered stations
2188+ self ._map_filter_proxy += 1
20982189
2099- self .display_table . param . watch ( _on_tbl_selection , "selection" )
2100- self .display_table . param . watch ( _on_tbl_filters , "filters" )
2190+ self ._register_table_watcher ( "selection" , _on_tbl_selection )
2191+ self ._register_table_watcher ( "filters" , _on_tbl_filters )
21012192 self .param .watch (self ._on_map_filter_mode_changed , "map_filters_table" )
21022193 map_tooltip = pn .widgets .TooltipIcon (
21032194 value = """Map of geographical features. Click on a feature to see data available in the table. <br/>
@@ -2366,11 +2457,11 @@ def _on_tbl_selection(event):
23662457 self ._map_sel_proxy = list (event .new )
23672458
23682459 def _on_tbl_filters (event ):
2369- if not self . map_filters_table :
2370- self ._map_filter_proxy += 1
2460+ # Always refresh the map when table filters change.
2461+ self ._map_filter_proxy += 1
23712462
2372- self .display_table . param . watch ( _on_tbl_selection , "selection" )
2373- self .display_table . param . watch ( _on_tbl_filters , "filters" )
2463+ self ._register_table_watcher ( "selection" , _on_tbl_selection )
2464+ self ._register_table_watcher ( "filters" , _on_tbl_filters )
23742465 self .param .watch (self ._on_map_filter_mode_changed , "map_filters_table" )
23752466
23762467 # Touch-friendly map — tap only, no lasso/box
0 commit comments