4343import urllib .parse
4444from .utils import full_stack
4545from .catalog import DataCatalog , DataReference , CatalogView # noqa: F401 – exposed for subclasses
46+ from .views import ViewsManager , create_views_tab
4647
4748# ---------------------------------------------------------------------------
4849# Standard DWR disclaimer — import and assign to disclaimer_text on any
@@ -414,39 +415,41 @@ def get_table_schema(self, df: pd.DataFrame | None = None) -> dict:
414415 - ``column_widths``: explicit width map (``{"col": "10%"}``)
415416 - ``filters``: explicit filter config map
416417
417- The default implementation preserves existing behavior by deriving
418- widths/filters from legacy hooks.
418+ The default returns an empty schema. Subclasses should override this
419+ directly to declare column ownership. Legacy subclasses that override
420+ ``get_table_column_width_map()`` and ``get_table_filters()`` still work
421+ because those public methods are called directly by the framework.
419422 """
420423 return {
421- "required_columns" : self . get_table_columns () ,
424+ "required_columns" : [] ,
422425 "optional_columns" : [],
423426 "hidden_by_default" : [],
424427 "drop_if_all_null" : False ,
425- "column_widths" : self . get_table_column_width_map () ,
426- "filters" : self . get_table_filters () ,
428+ "column_widths" : {} ,
429+ "filters" : {} ,
427430 }
428431
429432 def get_hidden_table_columns (self , df : pd .DataFrame | None = None ) -> list [str ]:
430433 """Return columns that should be hidden when the table first renders."""
431434 return []
432435
433436 def get_table_column_width_map (self ) -> dict :
434- """
435- Return a dictionary mapping column names to width strings (e.g., '10%').
437+ """Return a dict mapping column names to width strings (e.g. ``'10%'``).
436438
437- You must override this in your subclass.
439+ The default proxies to ``get_table_schema()["column_widths"]``.
440+ Legacy subclasses that override this method directly still work
441+ because Python's MRO will call the override, not this base.
438442 """
439- raise NotImplementedError (
440- "You must implement get_table_column_width_map() in your subclass."
441- )
443+ return dict (self .get_table_schema ().get ("column_widths" , {}))
442444
443445 def get_table_filters (self ) -> dict :
444- """
445- Return a dictionary specifying filter widgets for each column.
446+ """Return a dict specifying filter widgets for each column.
446447
447- You must override this in your subclass.
448+ The default proxies to ``get_table_schema()["filters"]``.
449+ Legacy subclasses that override this method directly still work
450+ because Python's MRO will call the override, not this base.
448451 """
449- raise NotImplementedError ( "You must implement get_table_filters() in your subclass." )
452+ return dict ( self . get_table_schema (). get ( "filters" , {}) )
450453
451454 @lru_cache (maxsize = 128 )
452455 def get_no_selection_message (self ) -> str :
@@ -659,7 +662,9 @@ def __init__(self, dataui_manager, crs=ccrs.PlateCarree(), station_id_column=Non
659662 super ().__init__ (** kwargs )
660663 self ._dataui_manager = dataui_manager
661664 self ._dataui_manager ._dataui = self # insert a reference to self in the dataui_manager for progress bar updates for example
662- self ._dfcat = self ._dataui_manager .get_data_catalog ()
665+ self ._dfcat_full = self ._dataui_manager .get_data_catalog ()
666+ self ._dfcat = self ._dfcat_full
667+ self ._views_manager = ViewsManager ()
663668 self .param .map_color_category .objects = self ._dataui_manager .get_map_color_columns () or []
664669 if self .param .map_color_category .objects :
665670 self .map_color_category = self .param .map_color_category .objects [0 ]
@@ -1498,6 +1503,28 @@ def _setup_action_sidebars(self) -> None:
14981503 "Action sidebar setup failed for %r: %s" , action .get ("name" ), exc
14991504 )
15001505
1506+ def _refresh_table_from_view (self , event = None ) -> None :
1507+ """Update the Tabulator to show only rows matching the active view.
1508+
1509+ Called when :attr:`ViewsManager.active_view` changes or when a view
1510+ definition is modified (Apply button in the Views tab). Resets the
1511+ table selection so stale positional indices don't carry over.
1512+ """
1513+ if not hasattr (self , "display_table" ):
1514+ return
1515+ filtered = self ._views_manager .filter_dataframe (self ._dfcat_full )
1516+ self ._dfcat = filtered
1517+ # Normalise to plain DataFrame (Tabulator can't JSON-serialise geometry)
1518+ try :
1519+ import geopandas as gpd
1520+ tbl_df = pd .DataFrame (filtered ) if isinstance (filtered , gpd .GeoDataFrame ) else filtered
1521+ except ImportError :
1522+ tbl_df = filtered
1523+ # Reindex to exactly the columns the Tabulator was initialised with
1524+ tbl_cols = self .display_table .value .columns
1525+ tbl_df = tbl_df .reindex (columns = tbl_cols )
1526+ self .display_table .param .update (value = tbl_df , selection = [])
1527+
15011528 def show_in_display_panel (self , title , content ):
15021529 """Add *content* as a closable tab in the display panel.
15031530
@@ -1778,7 +1805,7 @@ def _map_callback(
17781805 sizing_mode = "stretch_width" ,
17791806 )
17801807
1781- # Build a flat tab list: Time / Transform / Plot / Map / Table
1808+ # Build a flat tab list: Time / Transform / Plot / Map / Table / Views
17821809 _ctrl = self ._dataui_manager .get_widgets ()
17831810 if isinstance (_ctrl , dict ) and _ctrl :
17841811 _ctrl_tabs = list (_ctrl .items ())
@@ -1788,6 +1815,7 @@ def _map_callback(
17881815 _ctrl_tabs = []
17891816 _ctrl_tabs .append (("Map" , map_view ))
17901817 _ctrl_tabs .append (("Table" , table_options ))
1818+ _ctrl_tabs .append (("Views" , create_views_tab (self )))
17911819 self ._sidebar_tabs = pn .Tabs (* _ctrl_tabs , active = 0 )
17921820 sidebar_view = pn .Column (
17931821 self ._sidebar_tabs ,
@@ -1804,13 +1832,16 @@ def _map_callback(
18041832 else :
18051833 _ctrl_tabs = []
18061834 _ctrl_tabs .append (("Table" , table_options ))
1835+ _ctrl_tabs .append (("Views" , create_views_tab (self )))
18071836 self ._sidebar_tabs = pn .Tabs (* _ctrl_tabs , active = 0 )
18081837 sidebar_view = pn .Column (
18091838 self ._sidebar_tabs ,
18101839 self .progress_bar ,
18111840 self ._status_label ,
18121841 sizing_mode = "stretch_both" ,
18131842 )
1843+ # Wire view switching → table refresh.
1844+ self ._views_manager .param .watch (self ._refresh_table_from_view , "active_view" )
18141845 # Let actions inject their sidebar tabs now that _sidebar_tabs exists.
18151846 self ._setup_action_sidebars ()
18161847 # Append the view-navigation widget to the right end of the action
0 commit comments