3939 get_wel_period_keys ,
4040 load_wel ,
4141)
42+ from flopy_interactive .utils .perf import perf_call , perf_note , perf_timer
4243from flopy_interactive .viz .color_modes import apply_color_mode
4344
4445
4748SUGGEST_TITLE_FILTER = "Barton Springs Edwards Aquifer"
4849CKAN_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
5155def 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
6367def _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
131141def _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+
337362def _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)
951994def 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
14751558if __name__ == "__main__" :
0 commit comments