Skip to content

Commit e05cadf

Browse files
committed
Add Source Compare functionality and related tests
- Introduce SourceCompareAction to facilitate cross-source comparison in the catalog. - Merge Transform → Ref and Source Compare actions into a single MenuButton in the TimeSeriesDataUIManager. - Implement helper functions for detecting varying columns and grouping by identity. - Add tests for SourceCompareAction, covering varying column detection, identity grouping, and reference creation. - Update __init__.py to include new actions and enhance the DataUIManager with new parameters.
1 parent 94f0ea6 commit e05cadf

6 files changed

Lines changed: 953 additions & 1 deletion

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
description: "Use when creating a subclass of dvue's TimeSeriesDataUIManager, DataUIManager, or DataUI. Covers required method overrides, initialization order, catalog patterns, identity_key_columns, and NaN-safety for mixed catalogs. Relevant for any new data UI manager in pydelmod, schismviz, or downstream apps."
3+
---
4+
# dvue Subclassing Guide
5+
6+
## Choose Your Base Class
7+
8+
| Base Class | Use When |
9+
|---|---|
10+
| `TimeSeriesDataUIManager` | Time-indexed data (most common) |
11+
| `DataUIManager` | Non-time-series tabular data |
12+
| `DataUI` | Low-level: custom view only, no manager |
13+
14+
## Initialization Order (TimeSeriesDataUIManager)
15+
16+
Always follow this order — violations cause param errors or empty UI:
17+
18+
```python
19+
class MyManager(TimeSeriesDataUIManager):
20+
identity_key_columns = param.List(default=["station", "variable"])
21+
22+
def __init__(self, *data_files, **kwargs):
23+
self._data_files = data_files
24+
self._build_catalog() # 1. Build internal state
25+
super().__init__(url_column="filename", # 2. Call super (triggers get_data_catalog,
26+
url_num_column="url_num", # get_time_range, populates params)
27+
**kwargs)
28+
self.color_cycle_column = "variable" # 3. Set param defaults AFTER super()
29+
self.dashed_line_cycle_column = "source"
30+
self.marker_cycle_column = "station"
31+
```
32+
33+
## Required Method Overrides
34+
35+
All of these are abstract — the class will not instantiate without them:
36+
37+
```python
38+
# --- Catalog ---
39+
def get_data_catalog(self) -> pd.DataFrame: ...
40+
def get_time_range(self, dfcat: pd.DataFrame) -> tuple: ... # (start, end) Timestamps
41+
42+
# --- Row-level data access ---
43+
def is_irregular(self, row: pd.Series) -> bool: ...
44+
def build_station_name(self, row: pd.Series) -> str: ...
45+
46+
# --- One of these two patterns (see below) ---
47+
def get_data_for_time_range(self, row, time_range) -> tuple: ... # (df, unit, ptype)
48+
# OR: implement data_catalog property + get_data_reference()
49+
50+
# --- UI config ---
51+
def get_table_column_width_map(self) -> dict: ... # {"col": "15%", ...}
52+
def get_table_filters(self) -> dict: ... # {"col": {"type": "input", "func": "like"}}
53+
def get_tooltips(self) -> list: ... # [("Label", "@col"), ...]
54+
def get_map_color_columns(self) -> list: ...
55+
def get_name_to_color(self) -> dict: ...
56+
def get_map_marker_columns(self) -> list: ...
57+
def get_name_to_marker(self) -> dict: ...
58+
```
59+
60+
## Two Catalog Patterns
61+
62+
### Pattern A — DataCatalog (preferred for reactive/math refs)
63+
64+
```python
65+
def _build_catalog(self):
66+
self._dvue_catalog = DataCatalog()
67+
for ...:
68+
self._dvue_catalog.add(DataReference(reader=MyReader(...), name=ref_name, **attrs))
69+
70+
@property
71+
def data_catalog(self) -> DataCatalog:
72+
return self._dvue_catalog
73+
74+
def get_data_catalog(self) -> pd.DataFrame:
75+
return super().get_data_catalog() # Delegates to DataCatalog.to_dataframe()
76+
77+
def get_data_reference(self, row: pd.Series) -> DataReference:
78+
# Guard against NaN for mixed catalogs — see NaN Safety below
79+
if "name" in row.index and not pd.isna(row.get("name")):
80+
return self._dvue_catalog.get(row["name"])
81+
return self._dvue_catalog.get(f"{row['filename']}::{row['station']}")
82+
```
83+
84+
### Pattern B — plain DataFrame (simpler, legacy)
85+
86+
```python
87+
def get_data_catalog(self) -> pd.DataFrame:
88+
return self._dfcat # Plain DataFrame built in __init__
89+
90+
def get_data_for_time_range(self, row, time_range):
91+
# Read data for the row from source
92+
df = my_read_function(row["filename"], row["path"], time_range)
93+
return df, row["unit"], None # (DataFrame, unit_str, ptype or None)
94+
```
95+
96+
## Identity Key Columns (for Transform → Catalog names)
97+
98+
Set `identity_key_columns` so `TransformToCatalogAction` generates readable short names:
99+
100+
```python
101+
class MyManager(TimeSeriesDataUIManager):
102+
identity_key_columns = param.List(default=["station", "variable"])
103+
```
104+
105+
Or per-reference (takes precedence over manager-level param):
106+
107+
```python
108+
ref.set_key_attributes(["station", "variable"])
109+
```
110+
111+
Without this, the full `ref_key()` is used as the name — always valid but verbose.
112+
113+
## NaN Safety for Mixed Catalogs
114+
115+
When the catalog contains both raw `DataReference` and `MathDataReference` rows, file/source columns are `NaN` for math rows. Two required guards:
116+
117+
**In `get_data_reference`:**
118+
```python
119+
def get_data_reference(self, row):
120+
filename = row.get("filename", None)
121+
if pd.isna(filename):
122+
return self._dvue_catalog.get(row["name"])
123+
return self._dvue_catalog.get(self._build_ref_key(row))
124+
```
125+
126+
**In any code using `get_unique_short_names` (file indexing):**
127+
```python
128+
valid_files = [f for f in df["filename"].unique() if not pd.isna(f)]
129+
short_names = get_unique_short_names(valid_files)
130+
```
131+
132+
Skipping either guard raises `KeyError: "No DataReference named 'nan::...'"` or `TypeError` from `os.path.normpath(NaN)`.
133+
134+
## Key Visual Styling Params
135+
136+
Set after `super().__init__()`:
137+
138+
| Param | Controls |
139+
|---|---|
140+
| `color_cycle_column` | Line colors |
141+
| `dashed_line_cycle_column` | Dash patterns |
142+
| `marker_cycle_column` | Marker shapes |
143+
| `plot_group_by_column` | Plot grouping (None = group by unit) |
144+
| `identity_key_columns` | TransformToCatalogAction naming |
145+
146+
## Custom Plot Action
147+
148+
Override `TimeSeriesPlotAction` and wire it in:
149+
150+
```python
151+
class MyPlotAction(TimeSeriesPlotAction):
152+
def create_curve(self, data, row, unit, file_index=""):
153+
label = row["station"]
154+
if file_index:
155+
label = f"{file_index}:{label}"
156+
return hv.Curve(data.iloc[:, [0]], label=label)
157+
158+
class MyManager(TimeSeriesDataUIManager):
159+
def _make_plot_action(self):
160+
return MyPlotAction()
161+
```
162+
163+
## Checklist
164+
165+
- [ ] Subclass `TimeSeriesDataUIManager` (not `DataUIManager` for time-series)
166+
- [ ] Build catalog **before** `super().__init__()`
167+
- [ ] Set `color_cycle_column` etc. **after** `super().__init__()`
168+
- [ ] Implement all abstract methods
169+
- [ ] Choose Pattern A (DataCatalog) or Pattern B (plain DataFrame) — not both
170+
- [ ] Set `identity_key_columns` for readable TransformToCatalog names
171+
- [ ] Add NaN guards in `get_data_reference` if supporting math references
172+
- [ ] Return `(df, unit_str, ptype_or_None)` from `get_data_for_time_range`
173+
174+
## References
175+
176+
- [dvue/AGENTS.md](../../AGENTS.md) — core design rules and mixed-catalog pitfalls
177+
- [dvue/tsdataui.py](../../dvue/tsdataui.py) — full `TimeSeriesDataUIManager` implementation
178+
- [dvue/dataui.py](../../dvue/dataui.py)`DataUIManager` base
179+
- [dvue/catalog.py](../../dvue/catalog.py)`DataCatalog`, `DataReference`, `MathDataReference`
180+
- [dvue/actions.py](../../dvue/actions.py)`TimeSeriesPlotAction` and `TransformToCatalogAction`
181+
- Real subclass examples: `pydelmod/dssui.py`, `schismviz/schismui.py`

dvue/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
PermalinkAction,
1111
DownloadDataAction,
1212
DownloadDataCatalogAction,
13+
TransformToCatalogAction,
14+
SourceCompareAction,
1315
)
1416
from .math_ref_editor import MathRefEditorAction
1517
from .fullscreen import FullScreen
@@ -44,6 +46,8 @@
4446
"PermalinkAction",
4547
"DownloadDataAction",
4648
"DownloadDataCatalogAction",
49+
"TransformToCatalogAction",
50+
"SourceCompareAction",
4751
"MathRefEditorAction",
4852
"FullScreen",
4953
# DataReferenceReader hierarchy

0 commit comments

Comments
 (0)