webviz-subsurface 0.2.36__py3-none-any.whl → 0.2.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. webviz_subsurface/__init__.py +1 -1
  2. webviz_subsurface/_components/color_picker.py +1 -1
  3. webviz_subsurface/_datainput/well_completions.py +2 -1
  4. webviz_subsurface/_providers/ensemble_polygon_provider/__init__.py +3 -0
  5. webviz_subsurface/_providers/ensemble_polygon_provider/_polygon_discovery.py +97 -0
  6. webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +226 -0
  7. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider.py +53 -0
  8. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider_factory.py +99 -0
  9. webviz_subsurface/_providers/ensemble_polygon_provider/polygon_server.py +125 -0
  10. webviz_subsurface/plugins/_co2_leakage/_plugin.py +577 -293
  11. webviz_subsurface/plugins/_co2_leakage/_types.py +7 -0
  12. webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
  13. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +226 -186
  14. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +591 -128
  15. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
  16. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_info.py +31 -0
  17. webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
  18. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
  19. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +199 -97
  20. webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
  21. webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
  22. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +122 -21
  23. webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
  24. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +44 -19
  25. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +944 -359
  26. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/METADATA +2 -2
  27. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/RECORD +33 -20
  28. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/WHEEL +1 -1
  29. /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
  30. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/LICENSE +0 -0
  31. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/LICENSE.chromedriver +0 -0
  32. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/entry_points.txt +0 -0
  33. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,7 @@
1
+ from typing import List, Optional, TypedDict
2
+
3
+
4
+ class LegendData(TypedDict):
5
+ bar_legendonly: Optional[List[str]]
6
+ time_legendonly: Optional[List[str]]
7
+ stats_legendonly: Optional[List[str]]
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+
4
+ from fmu.ensemble import ScratchEnsemble
5
+
6
+
7
+ def realization_paths(ens_path: str) -> Dict[int, Path]:
8
+ scratch_ensemble = ScratchEnsemble("_", paths=ens_path).filter("OK")
9
+ return {i: Path(r.runpath()) for i, r in scratch_ensemble.realizations.items()}
@@ -11,7 +11,6 @@ from flask_caching import Cache
11
11
 
12
12
  from webviz_subsurface._providers import (
13
13
  EnsembleSurfaceProvider,
14
- EnsembleTableProvider,
15
14
  SimulatedSurfaceAddress,
16
15
  StatisticalSurfaceAddress,
17
16
  SurfaceAddress,
@@ -21,19 +20,35 @@ from webviz_subsurface._providers import (
21
20
  from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_provider import (
22
21
  SurfaceStatistic,
23
22
  )
24
- from webviz_subsurface._utils.webvizstore_functions import read_csv
23
+ from webviz_subsurface.plugins._co2_leakage._types import LegendData
25
24
  from webviz_subsurface.plugins._co2_leakage._utilities import plume_extent
26
25
  from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import (
26
+ generate_co2_box_plot_figure,
27
+ generate_co2_statistics_figure,
27
28
  generate_co2_time_containment_figure,
28
29
  generate_co2_time_containment_one_realization_figure,
29
30
  generate_co2_volume_figure,
30
31
  )
32
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
33
+ ContainmentDataProvider,
34
+ )
35
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_info import (
36
+ ContainmentInfo,
37
+ StatisticsTabOption,
38
+ )
39
+ from webviz_subsurface.plugins._co2_leakage._utilities.ensemble_well_picks import (
40
+ EnsembleWellPicks,
41
+ )
31
42
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
32
43
  Co2MassScale,
33
44
  Co2VolumeScale,
45
+ FilteredMapAttribute,
34
46
  GraphSource,
35
47
  LayoutLabels,
36
48
  MapAttribute,
49
+ MapGroup,
50
+ MapType,
51
+ MenuOptions,
37
52
  )
38
53
  from webviz_subsurface.plugins._co2_leakage._utilities.summary_graphs import (
39
54
  generate_summary_figure,
@@ -42,21 +57,22 @@ from webviz_subsurface.plugins._co2_leakage._utilities.surface_publishing import
42
57
  TruncatedSurfaceAddress,
43
58
  publish_and_get_surface_metadata,
44
59
  )
45
- from webviz_subsurface.plugins._map_viewer_fmu._tmp_well_pick_provider import (
46
- WellPickProvider,
60
+ from webviz_subsurface.plugins._co2_leakage._utilities.unsmry_data_provider import (
61
+ UnsmryDataProvider,
47
62
  )
48
63
 
49
64
 
50
65
  def property_origin(
51
- attribute: MapAttribute, map_attribute_names: Dict[MapAttribute, str]
66
+ attribute: MapAttribute, map_attribute_names: FilteredMapAttribute
52
67
  ) -> str:
53
- if attribute in map_attribute_names:
54
- return map_attribute_names[attribute]
55
- if attribute == MapAttribute.SGAS_PLUME:
56
- return map_attribute_names[MapAttribute.MAX_SGAS]
57
- if attribute == MapAttribute.AMFG_PLUME:
58
- return map_attribute_names[MapAttribute.MAX_AMFG]
59
- raise AssertionError(f"Map attribute name not found for property: {attribute}")
68
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
69
+ return [
70
+ map_attribute_names[attr]
71
+ for attr in MapAttribute
72
+ if MapGroup[attr.name].value == MapGroup[MapAttribute(attribute).name].value
73
+ and MapType[attr.name] == "MAX"
74
+ ][0]
75
+ return map_attribute_names[attribute]
60
76
 
61
77
 
62
78
  @dataclass
@@ -77,7 +93,7 @@ class SurfaceData:
77
93
  color_map_name: str,
78
94
  readable_name_: str,
79
95
  visualization_info: Dict[str, Any],
80
- map_attribute_names: Dict[MapAttribute, str],
96
+ map_attribute_names: FilteredMapAttribute,
81
97
  ) -> Tuple[Any, Optional[Any]]:
82
98
  surf_meta, img_url, summed_mass = publish_and_get_surface_metadata(
83
99
  server,
@@ -110,22 +126,30 @@ class SurfaceData:
110
126
  )
111
127
 
112
128
 
129
+ def extract_legendonly(figure: go.Figure) -> List[str]:
130
+ # Finds the names OR legendgroup of the traces in the figure which have their
131
+ # visibility set to "legendonly". In the figure, these traces are toggled OFF in the
132
+ # legend.
133
+ return [
134
+ d.get("legendgroup", d.get("name"))
135
+ for d in figure["data"]
136
+ if d.get("visible", "") == "legendonly"
137
+ ]
138
+
139
+
113
140
  def derive_surface_address(
114
141
  surface_name: str,
115
142
  attribute: MapAttribute,
116
143
  date: Optional[str],
117
144
  realization: List[int],
118
- map_attribute_names: Dict[MapAttribute, str],
145
+ map_attribute_names: FilteredMapAttribute,
119
146
  statistic: str,
120
147
  contour_data: Optional[Dict[str, Any]],
121
148
  ) -> Union[SurfaceAddress, TruncatedSurfaceAddress]:
122
- if attribute in (MapAttribute.SGAS_PLUME, MapAttribute.AMFG_PLUME):
149
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
150
+ max_attr_name = f"MAX_{MapGroup[MapAttribute(attribute).name]}"
123
151
  assert date is not None
124
- basis = (
125
- MapAttribute.MAX_SGAS
126
- if attribute == MapAttribute.SGAS_PLUME
127
- else MapAttribute.MAX_AMFG
128
- )
152
+ basis = getattr(MapAttribute, max_attr_name)
129
153
  return TruncatedSurfaceAddress(
130
154
  name=surface_name,
131
155
  datestr=date,
@@ -136,11 +160,7 @@ def derive_surface_address(
136
160
  )
137
161
  date = (
138
162
  None
139
- if attribute
140
- in [
141
- MapAttribute.MIGRATION_TIME_SGAS,
142
- MapAttribute.MIGRATION_TIME_AMFG,
143
- ]
163
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME"
144
164
  else date
145
165
  )
146
166
  if len(realization) == 1:
@@ -161,12 +181,9 @@ def derive_surface_address(
161
181
 
162
182
  def readable_name(attribute: MapAttribute) -> str:
163
183
  unit = ""
164
- if attribute in [
165
- MapAttribute.MIGRATION_TIME_SGAS,
166
- MapAttribute.MIGRATION_TIME_AMFG,
167
- ]:
184
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
168
185
  unit = " [year]"
169
- elif attribute in (MapAttribute.AMFG_PLUME, MapAttribute.SGAS_PLUME):
186
+ elif MapType[MapAttribute(attribute).name].value == "PLUME":
170
187
  unit = " [# real.]"
171
188
  return f"{attribute.value}{unit}"
172
189
 
@@ -211,12 +228,9 @@ def get_plume_polygon(
211
228
 
212
229
 
213
230
  def _find_legend_title(attribute: MapAttribute, unit: str) -> str:
214
- if attribute in [
215
- MapAttribute.MIGRATION_TIME_SGAS,
216
- MapAttribute.MIGRATION_TIME_AMFG,
217
- ]:
231
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
218
232
  return "years"
219
- if attribute in [MapAttribute.MASS, MapAttribute.DISSOLVED, MapAttribute.FREE]:
233
+ if MapType[MapAttribute(attribute).name].value == "MASS":
220
234
  return unit
221
235
  return ""
222
236
 
@@ -234,7 +248,8 @@ def create_map_annotations(
234
248
  and surface_data.color_map_range[0] is not None
235
249
  and surface_data.color_map_range[1] is not None
236
250
  ):
237
- num_digits = np.ceil(np.log(surface_data.color_map_range[1]) / np.log(10))
251
+ max_value = surface_data.color_map_range[1]
252
+ num_digits = 4 if max_value < 1 else np.ceil(np.log(max_value) / np.log(10))
238
253
  numbersize = max((6, min((17 - num_digits, 11))))
239
254
  annotations.append(
240
255
  wsc.ViewAnnotation(
@@ -283,12 +298,13 @@ def create_map_viewports() -> Dict:
283
298
 
284
299
  # pylint: disable=too-many-arguments
285
300
  def create_map_layers(
301
+ realizations: List[int],
286
302
  formation: str,
287
303
  surface_data: Optional[SurfaceData],
288
304
  fault_polygon_url: Optional[str],
289
- file_containment_boundary: Optional[str],
290
- file_hazardous_boundary: Optional[str],
291
- well_pick_provider: Optional[WellPickProvider],
305
+ containment_bounds_url: Optional[str],
306
+ haz_bounds_url: Optional[str],
307
+ well_pick_provider: Optional[EnsembleWellPicks],
292
308
  plume_extent_data: Optional[geojson.FeatureCollection],
293
309
  options_dialog_options: List[int],
294
310
  selected_wells: List[str],
@@ -322,8 +338,9 @@ def create_map_layers(
322
338
  "data": fault_polygon_url,
323
339
  }
324
340
  )
341
+
325
342
  if (
326
- file_containment_boundary is not None
343
+ containment_bounds_url is not None
327
344
  and LayoutLabels.SHOW_CONTAINMENT_POLYGON in options_dialog_options
328
345
  ):
329
346
  layers.append(
@@ -331,14 +348,15 @@ def create_map_layers(
331
348
  "@@type": "GeoJsonLayer",
332
349
  "name": "Containment Polygon",
333
350
  "id": "license-boundary-layer",
334
- "data": _parse_polygon_file(file_containment_boundary),
351
+ "data": containment_bounds_url,
335
352
  "stroked": False,
336
353
  "getFillColor": [0, 172, 0, 120],
337
354
  "visible": True,
338
355
  }
339
356
  )
357
+
340
358
  if (
341
- file_hazardous_boundary is not None
359
+ haz_bounds_url is not None
342
360
  and LayoutLabels.SHOW_HAZARDOUS_POLYGON in options_dialog_options
343
361
  ):
344
362
  layers.append(
@@ -346,48 +364,25 @@ def create_map_layers(
346
364
  "@@type": "GeoJsonLayer",
347
365
  "name": "Hazardous Polygon",
348
366
  "id": "hazardous-boundary-layer",
349
- "data": _parse_polygon_file(file_hazardous_boundary),
367
+ "data": haz_bounds_url,
350
368
  "stroked": False,
351
369
  "getFillColor": [200, 0, 0, 120],
352
370
  "visible": True,
353
371
  }
354
372
  )
373
+
355
374
  if (
356
375
  well_pick_provider is not None
357
376
  and formation is not None
377
+ and len(realizations) > 0
358
378
  and LayoutLabels.SHOW_WELLS in options_dialog_options
359
379
  ):
360
- well_data = dict(well_pick_provider.get_geojson(selected_wells, formation))
361
- if "features" in well_data:
362
- if len(well_data["features"]) == 0:
363
- wellstring = "well: " if len(selected_wells) == 1 else "wells: "
364
- wellstring += ", ".join(selected_wells)
365
- warnings.warn(
366
- f"Combination of formation: {formation} and "
367
- f"{wellstring} not found in well picks file."
368
- )
369
- for i in range(len(well_data["features"])):
370
- current_attribute = well_data["features"][i]["properties"]["attribute"]
371
- well_data["features"][i]["properties"]["attribute"] = (
372
- " " + current_attribute
373
- )
374
- layers.append(
375
- {
376
- "@@type": "GeoJsonLayer",
377
- "name": "Well Picks",
378
- "id": "well-picks-layer",
379
- "data": well_data,
380
- "visible": True,
381
- "getText": "@@=properties.attribute",
382
- "getTextSize": 12,
383
- "getTextAnchor": "start",
384
- "pointType": "circle+text",
385
- "lineWidthMinPixels": 2,
386
- "pointRadiusMinPixels": 2,
387
- "pickable": True,
388
- "parameters": {"depthTest": False},
389
- }
380
+ layer = well_pick_provider.geojson_layer(
381
+ realizations[0], selected_wells, formation
390
382
  )
383
+ if layer is not None:
384
+ layers.append(layer)
385
+
391
386
  if plume_extent_data is not None:
392
387
  layers.append(
393
388
  {
@@ -403,28 +398,58 @@ def create_map_layers(
403
398
 
404
399
 
405
400
  def generate_containment_figures(
406
- table_provider: EnsembleTableProvider,
401
+ table_provider: ContainmentDataProvider,
407
402
  co2_scale: Union[Co2MassScale, Co2VolumeScale],
408
- realization: int,
403
+ realizations: List[int],
409
404
  y_limits: List[Optional[float]],
410
- containment_info: Dict[str, Union[str, None, List[str], int]],
405
+ containment_info: ContainmentInfo,
406
+ legenddata: LegendData,
411
407
  ) -> Tuple[go.Figure, go.Figure, go.Figure]:
412
408
  try:
413
409
  fig0 = generate_co2_volume_figure(
414
410
  table_provider,
415
- table_provider.realizations(),
411
+ table_provider.realizations,
416
412
  co2_scale,
417
413
  containment_info,
414
+ legenddata["bar_legendonly"],
418
415
  )
419
- fig1 = generate_co2_time_containment_figure(
420
- table_provider,
421
- table_provider.realizations(),
422
- co2_scale,
423
- containment_info,
424
- )
425
- fig2 = generate_co2_time_containment_one_realization_figure(
426
- table_provider, co2_scale, realization, y_limits, containment_info
416
+ fig1 = (
417
+ generate_co2_time_containment_figure(
418
+ table_provider,
419
+ realizations,
420
+ co2_scale,
421
+ containment_info,
422
+ legenddata["time_legendonly"],
423
+ )
424
+ if len(realizations) > 1
425
+ else generate_co2_time_containment_one_realization_figure(
426
+ table_provider,
427
+ co2_scale,
428
+ realizations[0],
429
+ y_limits,
430
+ containment_info,
431
+ )
427
432
  )
433
+ if (
434
+ containment_info.statistics_tab_option
435
+ == StatisticsTabOption.PROBABILITY_PLOT
436
+ ):
437
+ fig2 = generate_co2_statistics_figure(
438
+ table_provider,
439
+ realizations,
440
+ co2_scale,
441
+ containment_info,
442
+ legenddata["stats_legendonly"],
443
+ )
444
+ else: # "box_plot"
445
+ # Deliberately uses same legend as statistics
446
+ fig2 = generate_co2_box_plot_figure(
447
+ table_provider,
448
+ realizations,
449
+ co2_scale,
450
+ containment_info,
451
+ legenddata["stats_legendonly"],
452
+ )
428
453
  except KeyError as exc:
429
454
  warnings.warn(f"Could not generate CO2 figures: {exc}")
430
455
  raise exc
@@ -432,58 +457,20 @@ def generate_containment_figures(
432
457
 
433
458
 
434
459
  def generate_unsmry_figures(
435
- table_provider_unsmry: EnsembleTableProvider,
460
+ table_provider_unsmry: UnsmryDataProvider,
436
461
  co2_mass_scale: Union[Co2MassScale, Co2VolumeScale],
437
- table_provider_containment: EnsembleTableProvider,
438
- ) -> Tuple[go.Figure]:
439
- return (
440
- generate_summary_figure(
441
- table_provider_unsmry,
442
- table_provider_unsmry.realizations(),
443
- co2_mass_scale,
444
- table_provider_containment,
445
- table_provider_containment.realizations(),
446
- ),
462
+ table_provider_containment: ContainmentDataProvider,
463
+ ) -> go.Figure:
464
+ return generate_summary_figure(
465
+ table_provider_unsmry,
466
+ co2_mass_scale,
467
+ table_provider_containment,
447
468
  )
448
469
 
449
470
 
450
- def _parse_polygon_file(filename: str) -> Dict[str, Any]:
451
- df = read_csv(filename)
452
- if "x" in df.columns:
453
- xyz = df[["x", "y"]].values
454
- elif "X_UTME" in df.columns:
455
- if "POLY_ID" in df.columns:
456
- xyz = [gf[["X_UTME", "Y_UTMN"]].values for _, gf in df.groupby("POLY_ID")]
457
- else:
458
- xyz = df[["X_UTME", "Y_UTMN"]].values
459
- else:
460
- # Attempt to use the first two columns as the x and y coordinates
461
- xyz = df.values[:, :2]
462
- if isinstance(xyz, list):
463
- poly_type = "MultiPolygon"
464
- coords = [[arr.tolist()] for arr in xyz]
465
- else:
466
- poly_type = "Polygon"
467
- coords = [xyz.tolist()]
468
- as_geojson = {
469
- "type": "FeatureCollection",
470
- "features": [
471
- {
472
- "type": "Feature",
473
- "properties": {},
474
- "geometry": {
475
- "type": poly_type,
476
- "coordinates": coords,
477
- },
478
- }
479
- ],
480
- }
481
- return as_geojson
482
-
483
-
484
471
  def process_visualization_info(
485
- n_clicks: int,
486
- threshold: Optional[float],
472
+ attribute: str,
473
+ thresholds: dict,
487
474
  unit: str,
488
475
  stored_info: Dict[str, Any],
489
476
  cache: Cache,
@@ -491,90 +478,147 @@ def process_visualization_info(
491
478
  """
492
479
  Clear surface cache if the threshold for visualization or mass unit is changed
493
480
  """
481
+ stored_info["attribute"] = attribute
494
482
  stored_info["change"] = False
495
- stored_info["n_clicks"] = n_clicks
496
- if unit != stored_info["unit"]:
483
+ if (
484
+ MapType[MapAttribute(attribute).name].value not in ["PLUME", "MIGRATION_TIME"]
485
+ and unit != stored_info["unit"]
486
+ ):
497
487
  stored_info["unit"] = unit
498
488
  stored_info["change"] = True
499
- if threshold is not None and threshold != stored_info["threshold"]:
500
- stored_info["threshold"] = threshold
501
- stored_info["change"] = True
489
+ if thresholds is not None:
490
+ for att in stored_info["thresholds"].keys():
491
+ if stored_info["thresholds"][att] != thresholds[att]:
492
+ stored_info["change"] = True
493
+ stored_info["thresholds"][att] = thresholds[att]
502
494
  if stored_info["change"]:
503
495
  cache.clear()
504
- # stored_info["n_clicks"] = n_clicks
505
496
  return stored_info
506
497
 
507
498
 
499
+ # pylint: disable=too-many-locals
508
500
  def process_containment_info(
509
501
  zone: Optional[str],
510
502
  region: Optional[str],
511
503
  phase: str,
512
504
  containment: str,
505
+ plume_group: str,
513
506
  color_choice: str,
514
507
  mark_choice: Optional[str],
515
508
  sorting: str,
516
- menu_options: Dict[str, List[str]],
517
- ) -> Dict[str, Union[str, None, List[str], int]]:
509
+ lines_to_show: str,
510
+ date_option: str,
511
+ statistics_tab_option: StatisticsTabOption,
512
+ box_show_points: str,
513
+ menu_options: MenuOptions,
514
+ ) -> ContainmentInfo:
518
515
  if mark_choice is None:
519
516
  mark_choice = "phase"
520
517
  zones = menu_options["zones"]
521
518
  regions = menu_options["regions"]
519
+ plume_groups = menu_options["plume_groups"]
522
520
  if len(zones) > 0:
523
521
  zones = [zone_name for zone_name in zones if zone_name != "all"]
524
522
  if len(regions) > 0:
525
523
  regions = [reg_name for reg_name in regions if reg_name != "all"]
526
- containments = ["hazardous", "outside", "contained"]
527
- phases = [phase for phase in menu_options["phases"] if phase != "total"]
524
+ if len(plume_groups) > 0:
525
+ plume_groups = [pg_name for pg_name in plume_groups if pg_name != "all"]
526
+
527
+ def plume_sort_key(name: str) -> int:
528
+ if name == "undetermined":
529
+ return 999
530
+ return name.count("+")
531
+
532
+ plume_groups = sorted(plume_groups, key=plume_sort_key)
533
+
528
534
  if "zone" in [mark_choice, color_choice]:
529
535
  region = "all"
530
536
  if "region" in [mark_choice, color_choice]:
531
537
  zone = "all"
532
- return {
533
- "zone": zone,
534
- "region": region,
535
- "zones": zones,
536
- "regions": regions,
537
- "phase": phase,
538
- "containment": containment,
539
- "color_choice": color_choice,
540
- "mark_choice": mark_choice,
541
- "sorting": sorting,
542
- "phases": phases,
543
- "containments": containments,
544
- }
538
+ return ContainmentInfo(
539
+ zone=zone,
540
+ region=region,
541
+ zones=zones,
542
+ regions=regions,
543
+ phase=phase,
544
+ containment=containment,
545
+ plume_group=plume_group,
546
+ color_choice=color_choice,
547
+ mark_choice=mark_choice,
548
+ sorting=sorting,
549
+ phases=[phase for phase in menu_options["phases"] if phase != "total"],
550
+ containments=["hazardous", "outside", "contained"],
551
+ plume_groups=plume_groups,
552
+ use_stats=lines_to_show == "stat",
553
+ date_option=date_option,
554
+ statistics_tab_option=statistics_tab_option,
555
+ box_show_points=box_show_points,
556
+ )
545
557
 
546
558
 
547
- def set_plot_ids(
548
- figs: List[go.Figure],
559
+ def make_plot_ids(
560
+ ensemble: str,
549
561
  source: GraphSource,
550
562
  scale: Union[Co2MassScale, Co2VolumeScale],
551
- containment_info: Dict,
563
+ containment_info: ContainmentInfo,
552
564
  realizations: List[int],
553
- ) -> None:
554
- if figs[0] != no_update:
555
- zone_str = (
556
- containment_info["zone"] if containment_info["zone"] is not None else "None"
557
- )
558
- region_str = (
559
- containment_info["region"]
560
- if containment_info["region"] is not None
561
- else "None"
562
- )
563
- plot_id = "-".join(
564
- (
565
- source,
566
- scale,
567
- zone_str,
568
- region_str,
569
- str(containment_info["phase"]),
570
- str(containment_info["containment"]),
571
- containment_info["color_choice"],
572
- containment_info["mark_choice"],
573
- )
565
+ # lines_to_show: str,
566
+ num_figs: int,
567
+ ) -> List[str]:
568
+ """
569
+ Removed some keywords from plot id that we don't want to trigger updates for
570
+ with respect to visible legends and potentially zoom level.
571
+
572
+ Note: Currently the legends are reset if you swap to a plot with different plot id
573
+ and back, so it works temporarily, in a sense. This might be good enough for now.
574
+ If we want to store it more extensively, we need to do something like what's been
575
+ outlined in _plugin.py.
576
+ """
577
+ zone_str = containment_info.zone if containment_info.zone is not None else "None"
578
+ region_str = (
579
+ containment_info.region if containment_info.region is not None else "None"
580
+ )
581
+ plume_group_str = (
582
+ containment_info.plume_group
583
+ if containment_info.plume_group is not None
584
+ else "None"
585
+ )
586
+ mark_choice_str = (
587
+ containment_info.mark_choice
588
+ if containment_info.mark_choice is not None
589
+ else "None"
590
+ )
591
+ plot_id = "-".join(
592
+ (
593
+ ensemble,
594
+ source,
595
+ scale,
596
+ zone_str,
597
+ region_str,
598
+ plume_group_str,
599
+ str(containment_info.phase),
600
+ str(containment_info.containment),
601
+ containment_info.color_choice,
602
+ mark_choice_str,
603
+ containment_info.sorting,
604
+ containment_info.date_option,
574
605
  )
575
- for fig in figs:
606
+ )
607
+ ids = [plot_id] * num_figs
608
+ # ids += [plot_id + f"-{realizations}"] * (num_figs - 1)
609
+ # ids[1] += f"-{lines_to_show}"
610
+ ids[1] += "-single" if len(realizations) == 1 else "-multiple"
611
+ ids[2] += f"-{containment_info.statistics_tab_option}"
612
+ return ids
613
+
614
+
615
+ def set_plot_ids(
616
+ figs: List[go.Figure],
617
+ plot_ids: List[str],
618
+ ) -> None:
619
+ for fig, plot_id in zip(figs, plot_ids):
620
+ if fig != no_update:
576
621
  fig["layout"]["uirevision"] = plot_id
577
- figs[-1]["layout"]["uirevision"] += f"-{realizations}"
578
622
 
579
623
 
580
624
  def process_summed_mass(
@@ -589,11 +633,7 @@ def process_summed_mass(
589
633
  ) -> Tuple[Optional[SurfaceData], Dict[str, float]]:
590
634
  summed_co2_key = f"{formation}-{realization[0]}-{datestr}-{attribute}-{unit}"
591
635
  if len(realization) == 1:
592
- if attribute in [
593
- MapAttribute.MASS,
594
- MapAttribute.DISSOLVED,
595
- MapAttribute.FREE,
596
- ]:
636
+ if MapType[MapAttribute(attribute).name].value == "MASS":
597
637
  if summed_mass is not None and summed_co2_key not in summed_co2:
598
638
  summed_co2[summed_co2_key] = summed_mass
599
639
  if summed_co2_key in summed_co2 and surf_data is not None: