Skip to content

Commit 047b263

Browse files
committed
Add performance profiling and dataset title support
Introduces performance profiling utilities and integrates them into key data loading and map rendering functions in dash_app.py. Adds caching for datasets and map figures to improve efficiency. Enhances the UI to support an optional dataset title field and propagates this through callbacks and dataset publishing. Updates CSS for improved layout and panel scrolling. Adds a new RCH file and performance log files.
1 parent cedc1af commit 047b263

9 files changed

Lines changed: 781 additions & 33 deletions

File tree

dash/assets/style.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ body {
88
.app-root {
99
min-height: 100vh;
1010
background: #f4f3ef;
11+
display: flex;
12+
flex-direction: column;
1113
}
1214

1315
.app-shell {
1416
display: flex;
1517
gap: 16px;
1618
padding: 16px;
19+
flex: 1;
20+
align-items: stretch;
1721
}
1822

1923
.header-bar {
@@ -108,6 +112,8 @@ body {
108112
border-radius: 12px;
109113
padding: 16px;
110114
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
115+
max-height: calc(100vh - 140px);
116+
overflow-y: auto;
111117
}
112118

113119
.panel h2 {

dash/dash_app.py

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
get_wel_period_keys,
4040
load_wel,
4141
)
42+
from flopy_interactive.utils.perf import perf_call, perf_note, perf_timer
4243
from flopy_interactive.viz.color_modes import apply_color_mode
4344

4445

@@ -47,6 +48,9 @@
4748
SUGGEST_TITLE_FILTER = "Barton Springs Edwards Aquifer"
4849
CKAN_URL = os.environ.get("FLOPY_CKAN_URL", CKAN_BASE_URL)
4950

51+
_DATASET_CACHE: Dict[str, Dict] = {}
52+
_MAP_FIG_CACHE: Dict[tuple, go.Figure] = {}
53+
5054

5155
def get_datasets() -> List[Dict]:
5256
"""Fetch CKAN datasets for the app session.
@@ -57,7 +61,7 @@ def get_datasets() -> List[Dict]:
5761
Returns:
5862
List of dataset metadata dicts.
5963
"""
60-
return search_ckan_datasets()
64+
return perf_call("search_ckan_datasets", search_ckan_datasets)
6165

6266

6367
def _get_dataset_or_none(name: str | None) -> Dict | None:
@@ -87,6 +91,10 @@ def load_dataset(name: str) -> Dict:
8791
Returns:
8892
Dict with dataset, gdf, wel, rch, lookup, and nlay.
8993
"""
94+
cached = _DATASET_CACHE.get(name)
95+
if cached is not None:
96+
perf_note(f"load_dataset cache hit: {name}")
97+
return cached
9098
dataset = _get_dataset_or_none(name)
9199
if not dataset:
92100
raise ValueError(f"Dataset not found: {name}")
@@ -96,8 +104,8 @@ def load_dataset(name: str) -> Dict:
96104
rch_resource = dataset["matches"]["rch"][0]
97105
wel_path = download_ckan_resource(wel_resource, base_dir / "wel")
98106
rch_path = download_ckan_resource(rch_resource, base_dir / "rch")
99-
gdf = load_grid_resource(grid_resource, base_dir / "grid")
100-
wel = load_wel(wel_path)
107+
gdf = perf_call(f"load_grid_resource:{name}", load_grid_resource, grid_resource, base_dir / "grid")
108+
wel = perf_call(f"load_wel:{name}", load_wel, wel_path)
101109
nrow = int(gdf["ROW"].max())
102110
ncol = int(gdf["COL"].max())
103111
try:
@@ -107,25 +115,27 @@ def load_dataset(name: str) -> Dict:
107115
keys = list(spd.data.keys())
108116
if keys:
109117
nper = int(max(keys)) + 1
110-
rch = load_rch(rch_path, nrow=nrow, ncol=ncol, nper=nper)
118+
rch = perf_call(f"load_rch:{name}", load_rch, rch_path, nrow=nrow, ncol=ncol, nper=nper)
111119
except Exception:
112120
rch = None
113-
cell_id_lookup = build_cell_id_lookup(gdf, wel)
121+
cell_id_lookup = perf_call(f"build_cell_id_lookup:{name}", build_cell_id_lookup, gdf, wel)
114122
nlay = 1
115123
if hasattr(wel, "parent") and wel.parent is not None:
116124
nlay = int(getattr(wel.parent.dis, "nlay", 1))
117125
elif hasattr(wel, "model") and wel.model is not None:
118126
nlay = int(getattr(wel.model.dis, "nlay", 1))
119127
elif hasattr(wel, "_model") and wel._model is not None:
120128
nlay = int(getattr(wel._model.dis, "nlay", 1))
121-
return {
129+
data = {
122130
"dataset": dataset,
123131
"gdf": gdf,
124132
"wel": wel,
125133
"rch": rch,
126134
"cell_id_lookup": cell_id_lookup,
127135
"nlay": nlay,
128136
}
137+
_DATASET_CACHE[name] = data
138+
return data
129139

130140

131141
def _collect_wel_cells_for_periods(
@@ -191,8 +201,10 @@ def _build_map_figure(
191201
"""
192202
gdf_map = gdf[["CELL_ID", "ROW", "COL", "geometry"]].copy()
193203
gdf_valid = gdf_map[gdf_map["geometry"].notna()].copy()
194-
gdf_choro = _downsample_for_choropleth(gdf_valid, gdf, zoom)
195-
grid_geojson = gdf_choro.set_index("CELL_ID").__geo_interface__
204+
with perf_timer("downsample_for_choropleth"):
205+
gdf_choro = _downsample_for_choropleth(gdf_valid, gdf, zoom)
206+
with perf_timer("build_geojson"):
207+
grid_geojson = gdf_choro.set_index("CELL_ID").__geo_interface__
196208
center_lat = float(gdf["_lat"].median())
197209
center_lon = float(gdf["_lon"].median())
198210

@@ -305,15 +317,16 @@ def _build_map_figure(
305317
)
306318

307319
color_gdf = gdf if show_grid else gdf_scatter
308-
apply_color_mode(
309-
fig,
310-
color_gdf,
311-
cells,
312-
color_by,
313-
normalize=False,
314-
flux_label=flux_label,
315-
force_linear=force_linear,
316-
)
320+
with perf_timer("apply_color_mode"):
321+
apply_color_mode(
322+
fig,
323+
color_gdf,
324+
cells,
325+
color_by,
326+
normalize=False,
327+
flux_label=flux_label,
328+
force_linear=force_linear,
329+
)
317330

318331
selected_ids = {int(cid) for cid in selected_ids}
319332
if selected_ids and len(fig.data) > 2:
@@ -334,6 +347,18 @@ def _build_map_figure(
334347
return fig
335348

336349

350+
def _apply_selection(fig: go.Figure, gdf, selected_ids: Iterable[int]) -> None:
351+
"""Apply selected cell overlay to an existing figure."""
352+
if not fig.data or len(fig.data) <= 2:
353+
return
354+
selected_set = {int(cid) for cid in (selected_ids or [])}
355+
if not selected_set:
356+
fig.data[2].update(lon=[], lat=[])
357+
return
358+
selected_rows = gdf[gdf["CELL_ID"].isin(selected_set)]
359+
fig.data[2].update(lon=selected_rows["_lon"], lat=selected_rows["_lat"])
360+
361+
337362
def _dataset_options() -> List[Dict[str, str]]:
338363
"""Build dataset dropdown options from CKAN search.
339364
@@ -485,7 +510,7 @@ def _slugify(value: str) -> str:
485510
dcc.Store(id="update-store", data=0),
486511
dcc.Store(id="loaded-dataset", data=default_dataset),
487512
dcc.Store(id="load-counter", data=0),
488-
dcc.Store(id="ckan-jwt", data=""),
513+
dcc.Store(id="ckan-jwt", data=os.environ.get("FLOPY_CKAN_JWT", "").strip()),
489514
dcc.Store(id="login-message", data=""),
490515
dcc.Store(id="tapis-username", data=""),
491516
dcc.Store(id="name-seed", data=str(uuid.uuid4())),
@@ -628,6 +653,13 @@ def _slugify(value: str) -> str:
628653
options=[{"label": "Add missing wells", "value": "yes"}],
629654
value=[],
630655
),
656+
html.Label("Dataset title"),
657+
dcc.Input(
658+
id="dataset-title",
659+
type="text",
660+
placeholder="Optional display title",
661+
className="login-input",
662+
),
631663
html.Label("New dataset name"),
632664
dcc.Dropdown(
633665
id="dataset-suggestions",
@@ -856,6 +888,15 @@ def login_ckan(n_clicks, username, password):
856888
return token, f"Logged in as {username}", "status status-ok", username, True, True, "Logged in"
857889

858890

891+
@app.callback(
892+
Output("apply-rate", "disabled"),
893+
Input("ckan-jwt", "data"),
894+
)
895+
def toggle_apply_rate(jwt_token):
896+
"""Enable Apply + Save only when authenticated."""
897+
return not bool(jwt_token)
898+
899+
859900
@app.callback(
860901
Output("category-select", "options"),
861902
Output("category-select", "value"),
@@ -927,6 +968,7 @@ def update_dataset_suggestions(username, jwt_token):
927968
Output("output-wel", "value"),
928969
Output("source-url", "value"),
929970
Output("change-summary", "value"),
971+
Output("dataset-title", "value"),
930972
Input("loaded-dataset", "data"),
931973
Input("flux-source", "value"),
932974
Input("rate-mode", "value"),
@@ -947,6 +989,7 @@ def update_dataset_suggestions(username, jwt_token):
947989
State("output-wel", "value"),
948990
State("source-url", "value"),
949991
State("change-summary", "value"),
992+
State("dataset-title", "value"),
950993
)
951994
def suggest_names(
952995
loaded_dataset,
@@ -969,6 +1012,7 @@ def suggest_names(
9691012
current_output_name,
9701013
current_source_url,
9711014
current_change_summary,
1015+
current_dataset_title,
9721016
):
9731017
"""Generate dataset/output names and change summary from UI state.
9741018
@@ -992,10 +1036,16 @@ def suggest_names(
9921036
current_change_summary: Current change summary input.
9931037
9941038
Returns:
995-
Tuple of (dataset name, output filename, source URL, change summary).
1039+
Tuple of (dataset name, output filename, source URL, change summary, dataset title).
9961040
"""
9971041
if not loaded_dataset:
998-
return current_dataset_name, current_output_name, current_source_url, current_change_summary
1042+
return (
1043+
current_dataset_name,
1044+
current_output_name,
1045+
current_source_url,
1046+
current_change_summary,
1047+
current_dataset_title,
1048+
)
9991049
triggered = ctx.triggered_id
10001050
if suggested_name and suggested_name != "__new__":
10011051
current_dataset_name = suggested_name
@@ -1049,7 +1099,18 @@ def suggest_names(
10491099
f"{selection_desc}; {period_desc}; {layer_desc}; "
10501100
f"Rate mode: {rate_mode}, New rate: {new_rate}; {add_desc}"
10511101
)
1052-
return dataset_name, output_name, source_url, change_summary
1102+
dataset_title = current_dataset_title or ""
1103+
if triggered in ("loaded-dataset", "load-counter", "dataset-suggestions") or not dataset_title.strip():
1104+
if loaded_dataset == "gam-carrizo-wilcox-aquifer-central-portion-version-3-02":
1105+
dataset_title = f"Carrizo-Wilcox (v3.02) – updated-{name_seed}"
1106+
else:
1107+
dataset_title = (current_dataset_name or dataset_name or "").strip()
1108+
if not dataset_title and loaded_dataset:
1109+
try:
1110+
dataset_title = str(load_dataset(loaded_dataset)["dataset"].get("title") or "").strip()
1111+
except Exception:
1112+
dataset_title = ""
1113+
return dataset_name, output_name, source_url, change_summary, dataset_title
10531114

10541115

10551116
@app.callback(
@@ -1199,6 +1260,7 @@ def update_output_label(flux_source):
11991260
State("update-store", "data"),
12001261
State("ckan-jwt", "data"),
12011262
State("dataset-name", "value"),
1263+
State("dataset-title", "value"),
12021264
State("output-wel", "value"),
12031265
State("source-url", "value"),
12041266
State("change-summary", "value"),
@@ -1217,6 +1279,7 @@ def apply_rate(
12171279
update_counter,
12181280
jwt_token,
12191281
dataset_name,
1282+
dataset_title,
12201283
output_wel,
12211284
source_url,
12221285
change_summary,
@@ -1237,6 +1300,7 @@ def apply_rate(
12371300
update_counter: Current update counter.
12381301
jwt_token: CKAN JWT token.
12391302
dataset_name: Output dataset name.
1303+
dataset_title: Output dataset title.
12401304
output_wel: Output WEL/RCH filename.
12411305
source_url: Source URL string.
12421306
change_summary: Change summary string.
@@ -1313,6 +1377,7 @@ def apply_rate(
13131377
provenance,
13141378
jwt_token=jwt_token or None,
13151379
new_dataset_name=dataset_name or None,
1380+
new_dataset_title=dataset_title or None,
13161381
source_url=source_url or None,
13171382
change_summary=change_summary or None,
13181383
maintainer_username=tapis_username or None,
@@ -1324,6 +1389,7 @@ def apply_rate(
13241389
provenance,
13251390
jwt_token=jwt_token or None,
13261391
new_dataset_name=dataset_name or None,
1392+
new_dataset_title=dataset_title or None,
13271393
source_url=source_url or None,
13281394
change_summary=change_summary or None,
13291395
maintainer_username=tapis_username or None,
@@ -1419,6 +1485,25 @@ def update_map(
14191485
print(f"[grid] sample geometry: {sample}")
14201486
except Exception as exc:
14211487
print(f"[grid] geometry debug failed: {exc}")
1488+
zoom = None
1489+
if isinstance(relayout, dict):
1490+
zoom = relayout.get("map.zoom")
1491+
if zoom is None:
1492+
zoom = relayout.get("mapbox.zoom")
1493+
cache_key = (
1494+
loaded_dataset,
1495+
flux_source,
1496+
color_by,
1497+
int(color_period) if color_period is not None else None,
1498+
tuple(int(p) for p in (periods or [])),
1499+
float(zoom) if zoom is not None else None,
1500+
int(_update or 0),
1501+
)
1502+
cached_fig = _MAP_FIG_CACHE.get(cache_key)
1503+
if cached_fig is not None:
1504+
fig = go.Figure(cached_fig)
1505+
_apply_selection(fig, gdf, selected_ids)
1506+
return fig
14221507
wel = data["wel"]
14231508
rch = data["rch"]
14241509
periods = periods or []
@@ -1455,21 +1540,19 @@ def update_map(
14551540
f"[flux] period={period_label} cells={count} nonzero={nz_count} "
14561541
f"min={vmin:.6g} max={vmax:.6g}"
14571542
)
1458-
zoom = None
1459-
if isinstance(relayout, dict):
1460-
zoom = relayout.get("map.zoom")
1461-
if zoom is None:
1462-
zoom = relayout.get("mapbox.zoom")
1463-
return _build_map_figure(
1543+
fig = _build_map_figure(
14641544
gdf,
14651545
active_cells,
14661546
label,
14671547
color_by,
14681548
force_linear,
1469-
selected_ids or [],
1549+
[],
14701550
zoom=zoom,
14711551
show_grid=False,
14721552
)
1553+
_MAP_FIG_CACHE[cache_key] = fig
1554+
_apply_selection(fig, gdf, selected_ids)
1555+
return fig
14731556

14741557

14751558
if __name__ == "__main__":

0 commit comments

Comments
 (0)