@@ -646,6 +646,11 @@ class DataUI(param.Parameterized):
646646 map_default_span = param .Number (default = 15000 , doc = "Default span for map zoom in meters" )
647647 map_non_selection_alpha = param .Number (default = 0.2 , doc = "Non selection alpha" )
648648 map_point_size = param .Number (default = 10 , doc = "Point size for map" )
649+ map_filters_table = param .Boolean (
650+ default = True ,
651+ doc = "When enabled, map selections filter the table rows instead of highlighting them. Table selections do not affect the map." ,
652+ )
653+ _table_selection_for_map = param .List (default = [], precedence = - 1 )
649654
650655 query = param .String (
651656 default = "" ,
@@ -672,6 +677,7 @@ def __init__(self, dataui_manager, crs=ccrs.PlateCarree(), station_id_column=Non
672677 if self .param .map_marker_category .objects :
673678 self .map_marker_category = self .param .map_marker_category .objects [0 ]
674679 self ._dfmapcat = self ._get_map_catalog ()
680+ self ._map_filter_station_ids = None
675681
676682 if isinstance (self ._dfcat , gpd .GeoDataFrame ):
677683 self ._tmap = gv .tile_sources .CartoLight ()
@@ -842,43 +848,54 @@ def update_map_features(
842848 map_default_span ,
843849 map_non_selection_alpha ,
844850 map_point_size ,
851+ map_filters_table = False ,
845852 ):
846853 """Update the map features based on the selection in the table or filters or query. Also updates if the color or marker by columns are changed"""
847854 query = query .strip ()
848855 dfs = self ._get_map_catalog ()
849- # select only those rows in dfs that have station_id_column in self.display_table.current_view
850- if (
851- self ._station_id_column
852- and self ._station_id_column in self .display_table .current_view .columns
853- ):
854- current_view = dfs [
855- dfs [self ._station_id_column ].isin (
856- self .display_table .current_view [self ._station_id_column ]
857- )
858- ]
859- # if current_view is a geodataframe, keep only valid geometries
856+ if map_filters_table :
857+ # In filter mode the map is decoupled from the table: always show the
858+ # full map catalog (not restricted to display_table.current_view) and
859+ # never highlight features based on table selection. _table_selection_for_map
860+ # is never updated while this mode is active, so selection is always [].
861+ current_view = dfs
860862 if isinstance (current_view , gpd .GeoDataFrame ):
861863 current_view = current_view .loc [current_view .is_valid ]
862- current_table_selected = self ._dfcat .iloc [selection ]
863- current_selected = current_view [
864- current_view [self ._station_id_column ].isin (
865- current_table_selected [self ._station_id_column ]
866- )
867- ]
864+ current_selection = []
868865 else :
869- current_view = dfs .loc [self .display_table .current_view .index ]
870- if isinstance (current_view , gpd .GeoDataFrame ):
871- current_view = current_view .loc [current_view .is_valid ]
872- current_table_selected = self ._dfcat .iloc [selection ]
873- current_selected = current_table_selected
874- # Filter out -1 entries: math refs with NaN geometry are absent from
875- # current_view (filtered by .is_valid above) so get_indexer returns -1
876- # for them. Passing -1 to Bokeh's selected= is interpreted as "last row"
877- # (Python-style negative index), selecting the wrong geo point on the map.
878- current_selection = [
879- i for i in current_view .index .get_indexer (current_selected .index ).tolist ()
880- if i >= 0
881- ]
866+ # select only those rows in dfs that have station_id_column in self.display_table.current_view
867+ if (
868+ self ._station_id_column
869+ and self ._station_id_column in self .display_table .current_view .columns
870+ ):
871+ current_view = dfs [
872+ dfs [self ._station_id_column ].isin (
873+ self .display_table .current_view [self ._station_id_column ]
874+ )
875+ ]
876+ # if current_view is a geodataframe, keep only valid geometries
877+ if isinstance (current_view , gpd .GeoDataFrame ):
878+ current_view = current_view .loc [current_view .is_valid ]
879+ current_table_selected = self ._dfcat .iloc [selection ]
880+ current_selected = current_view [
881+ current_view [self ._station_id_column ].isin (
882+ current_table_selected [self ._station_id_column ]
883+ )
884+ ]
885+ else :
886+ current_view = dfs .loc [self .display_table .current_view .index ]
887+ if isinstance (current_view , gpd .GeoDataFrame ):
888+ current_view = current_view .loc [current_view .is_valid ]
889+ current_table_selected = self ._dfcat .iloc [selection ]
890+ current_selected = current_table_selected
891+ # Filter out -1 entries: math refs with NaN geometry are absent from
892+ # current_view (filtered by .is_valid above) so get_indexer returns -1
893+ # for them. Passing -1 to Bokeh's selected= is interpreted as "last row"
894+ # (Python-style negative index), selecting the wrong geo point on the map.
895+ current_selection = [
896+ i for i in current_view .index .get_indexer (current_selected .index ).tolist ()
897+ if i >= 0
898+ ]
882899 try :
883900 if len (query ) > 0 :
884901 current_view = current_view .query (query )
@@ -921,10 +938,92 @@ def update_map_features(
921938 )
922939 return self ._map_features
923940
941+ # ------------------------------------------------------------------
942+ # Map-filter-table helpers
943+ # ------------------------------------------------------------------
944+
945+ def _get_table_df_for_display (self , df ):
946+ """Convert *df* (possibly a GeoDataFrame) to a plain DataFrame with the
947+ same columns as the current Tabulator widget, ready for assignment to
948+ ``display_table.value``."""
949+ try :
950+ tbl_df = pd .DataFrame (df ) if isinstance (df , gpd .GeoDataFrame ) else df
951+ except Exception :
952+ tbl_df = df
953+ tbl_cols = self .display_table .value .columns
954+ return tbl_df .reindex (columns = tbl_cols )
955+
956+ def _refresh_table_with_map_filter (self ):
957+ """Restrict the Tabulator to rows matching ``_map_filter_station_ids``."""
958+ ids = self ._map_filter_station_ids
959+ if ids is None :
960+ return
961+ idcol = self ._station_id_column
962+ if idcol and idcol in self ._dfcat .columns :
963+ filtered = self ._dfcat [self ._dfcat [idcol ].isin (ids )]
964+ else :
965+ # ids is a set of index labels when there is no station_id_column
966+ filtered = self ._dfcat .loc [self ._dfcat .index .isin (ids )]
967+ tbl_df = self ._get_table_df_for_display (filtered )
968+ self .display_table .param .update (value = tbl_df , selection = [])
969+
970+ def _clear_map_filter (self ):
971+ """Remove the active map filter and restore the full catalog in the table."""
972+ if self ._map_filter_station_ids is None :
973+ return
974+ self ._map_filter_station_ids = None
975+ tbl_df = self ._get_table_df_for_display (self ._dfcat )
976+ self .display_table .param .update (value = tbl_df , selection = [])
977+
978+ def _on_table_selection_changed (self , event ):
979+ """Gate table-selection → map sync: only propagate when filter mode is off."""
980+ if not self .map_filters_table :
981+ self ._table_selection_for_map = list (event .new )
982+
983+ def _on_map_filter_mode_changed (self , event ):
984+ """Restore normal table and re-sync map selection when filter mode is toggled off."""
985+ if not event .new :
986+ self ._clear_map_filter ()
987+ # Re-sync proxy param with current table selection so map highlights
988+ # immediately reflect any rows selected while filter mode was active.
989+ self ._table_selection_for_map = list (self .display_table .selection )
990+
991+ def _apply_map_filter (self , index ):
992+ """Apply a filter to the table based on the *index* of selected map features.
993+
994+ Called instead of the normal ``select_data_catalog`` path when
995+ ``map_filters_table`` is True.
996+ """
997+ # Empty selection — clear any active filter
998+ if not index :
999+ self ._clear_map_filter ()
1000+ return
1001+ map_df = self ._map_features .dframe ()
1002+ n_map = len (map_df )
1003+ index = [i for i in index if 0 <= i < n_map ]
1004+ if not index :
1005+ self ._clear_map_filter ()
1006+ return
1007+ idcol = self ._station_id_column
1008+ if idcol and idcol in self ._dfcat .columns :
1009+ self ._map_filter_station_ids = set (map_df .iloc [index ][idcol ].tolist ())
1010+ else :
1011+ # Use the index labels of the matching rows in _dfcat
1012+ dfs = map_df .iloc [index ]
1013+ merged_indices = self ._dfcat .reset_index ().merge (dfs )["index" ].to_list ()
1014+ self ._map_filter_station_ids = set (merged_indices )
1015+ self ._refresh_table_with_map_filter ()
1016+
9241017 def select_data_catalog (self , index = []):
9251018 """Select the rows in the table that correspond to the selected features in the map"""
9261019 if index is None or (len (index ) == 1 and index [0 ] == - 1 ):
9271020 return
1021+
1022+ # In filter mode: filter the table instead of selecting rows
1023+ if self .map_filters_table :
1024+ self ._apply_map_filter (index )
1025+ return
1026+
9281027 idcol = self ._station_id_column
9291028 table = self .display_table
9301029
@@ -1512,6 +1611,10 @@ def _refresh_table_from_view(self, event=None) -> None:
15121611 """
15131612 if not hasattr (self , "display_table" ):
15141613 return
1614+ # Clear any active map filter so it does not persist into the new view.
1615+ if getattr (self , "_map_filter_station_ids" , None ) is not None :
1616+ self ._map_filter_station_ids = None
1617+ notifications .info ("Map filter cleared — catalog view changed." )
15151618 filtered = self ._views_manager .filter_dataframe (self ._dfcat_full )
15161619 self ._dfcat = filtered
15171620 # Normalise to plain DataFrame (Tabulator can't JSON-serialise geometry)
@@ -1712,6 +1815,7 @@ def _on_column_picker_change(event):
17121815 self .param .map_non_selection_alpha ,
17131816 self .param .map_point_size ,
17141817 self .param .query ,
1818+ self .param .map_filters_table ,
17151819 ]
17161820 if _extra_map_widgets is not None :
17171821 _map_option_items .append (_extra_map_widgets )
@@ -1740,11 +1844,15 @@ def _on_column_picker_change(event):
17401844 "map_default_span" ,
17411845 "map_non_selection_alpha" ,
17421846 "map_point_size" ,
1847+ "map_filters_table" ,
1848+ "_table_selection_for_map" ,
17431849 ],
17441850 )
1851+ # selection is intentionally excluded: table row clicks are gated
1852+ # through _table_selection_for_map so filter mode can suppress them.
17451853 _table_stream = streams .Params (
17461854 parameterized = self .display_table ,
1747- parameters = ["filters" , "selection" ],
1855+ parameters = ["filters" ],
17481856 )
17491857
17501858 def _map_callback (
@@ -1756,8 +1864,9 @@ def _map_callback(
17561864 map_default_span ,
17571865 map_non_selection_alpha ,
17581866 map_point_size ,
1867+ map_filters_table ,
1868+ _table_selection_for_map ,
17591869 filters ,
1760- selection ,
17611870 ):
17621871 return self .update_map_features (
17631872 show_color_by = show_map_colors ,
@@ -1766,15 +1875,17 @@ def _map_callback(
17661875 marker_by = map_marker_category ,
17671876 query = query ,
17681877 filters = filters ,
1769- selection = selection ,
1878+ selection = _table_selection_for_map ,
17701879 map_default_span = map_default_span ,
17711880 map_non_selection_alpha = map_non_selection_alpha ,
17721881 map_point_size = map_point_size ,
1882+ map_filters_table = map_filters_table ,
17731883 )
17741884
17751885 self ._map_function = hv .DynamicMap (_map_callback , streams = [_self_stream , _table_stream ])
17761886 self ._station_select .source = self ._map_function
17771887 self ._station_select .param .watch_values (self .select_data_catalog , "index" )
1888+ self .display_table .param .watch (self ._on_table_selection_changed , "selection" )
17781889 map_tooltip = pn .widgets .TooltipIcon (
17791890 value = """Map of geographical features. Click on a feature to see data available in the table. <br/>
17801891 See <a href="https://docs.bokeh.org/en/latest/docs/user_guide/interaction/tools.html">Bokeh Tools</a> for toolbar operation"""
@@ -1842,6 +1953,8 @@ def _map_callback(
18421953 )
18431954 # Wire view switching → table refresh.
18441955 self ._views_manager .param .watch (self ._refresh_table_from_view , "active_view" )
1956+ # Clear map filter when filter mode is toggled off.
1957+ self .param .watch (self ._on_map_filter_mode_changed , "map_filters_table" )
18451958 # Let actions inject their sidebar tabs now that _sidebar_tabs exists.
18461959 self ._setup_action_sidebars ()
18471960 # Append the view-navigation widget to the right end of the action
@@ -2006,27 +2119,31 @@ def create_mobile_view(self, title="Data User Interface"):
20062119 "map_default_span" ,
20072120 "map_non_selection_alpha" ,
20082121 "map_point_size" ,
2122+ "map_filters_table" ,
2123+ "_table_selection_for_map" ,
20092124 ],
20102125 )
20112126 _table_stream = streams .Params (
20122127 parameterized = self .display_table ,
2013- parameters = ["filters" , "selection" ],
2128+ parameters = ["filters" ],
20142129 )
20152130
20162131 def _map_callback (
20172132 show_map_colors , map_color_category ,
20182133 show_map_markers , map_marker_category ,
20192134 query , map_default_span ,
20202135 map_non_selection_alpha , map_point_size ,
2021- filters , selection ,
2136+ map_filters_table , _table_selection_for_map ,
2137+ filters ,
20222138 ):
20232139 return self .update_map_features (
20242140 show_color_by = show_map_colors , color_by = map_color_category ,
20252141 show_marker_by = show_map_markers , marker_by = map_marker_category ,
2026- query = query , filters = filters , selection = selection ,
2142+ query = query , filters = filters , selection = _table_selection_for_map ,
20272143 map_default_span = map_default_span ,
20282144 map_non_selection_alpha = map_non_selection_alpha ,
20292145 map_point_size = map_point_size ,
2146+ map_filters_table = map_filters_table ,
20302147 )
20312148
20322149 self ._map_function = hv .DynamicMap (
0 commit comments