Skip to content

Commit 74fab5c

Browse files
committed
feat: Implement ViewsManager and ViewDefinition for catalog views with filtering capabilities
1 parent 37d2379 commit 74fab5c

6 files changed

Lines changed: 1012 additions & 19 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ src/dvue/_version.py
6666
.DS_Store
6767
dvue/_version.py
6868
.vscode/settings.json
69+
junit.xml
70+
cache.db

dvue/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from .session_persistence import make_reset_session_button, SessionManager
4141
from .registry import ReaderRegistry
4242
from .registry_ui import RegistryUIManager, RegistryPlotAction
43+
from .views import ViewDefinition, ViewsManager
4344

4445
__all__ = [
4546
# UI layer
@@ -80,4 +81,7 @@
8081
"ReaderRegistry",
8182
"RegistryUIManager",
8283
"RegistryPlotAction",
84+
# Catalog views
85+
"ViewDefinition",
86+
"ViewsManager",
8387
]

dvue/dataui.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import urllib.parse
4444
from .utils import full_stack
4545
from .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

dvue/tsdataui.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,13 @@ def get_time_range(self, dfcat):
328328
raise NotImplementedError("Method get_time_range not implemented")
329329

330330
def get_table_filters(self):
331-
raise NotImplementedError("Method get_table_filters not implemented")
331+
"""Return filter specs for table columns.
332+
333+
The default proxies to ``get_table_schema()["filters"]``.
334+
Legacy subclasses that override this method directly still work;
335+
subclasses that override ``get_table_schema()`` get filters from there.
336+
"""
337+
return dict(self.get_table_schema().get("filters", {}))
332338

333339
def _get_table_column_width_map(self) -> dict:
334340
"""Hook for subclasses to declare column widths.
@@ -837,7 +843,7 @@ def get_table_schema(self, df: pd.DataFrame | None = None) -> dict:
837843
"hidden_by_default": hidden_by_default,
838844
"drop_if_all_null": False,
839845
"column_widths": column_width_map,
840-
"filters": self.get_table_filters(),
846+
"filters": {}, # not populated in default; subclasses provide via get_table_schema()
841847
}
842848

843849
@staticmethod

0 commit comments

Comments
 (0)