webviz-subsurface 0.2.35__py3-none-any.whl → 0.2.37__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 (32) hide show
  1. webviz_subsurface/__init__.py +1 -1
  2. webviz_subsurface/_components/color_picker.py +1 -1
  3. webviz_subsurface/_providers/ensemble_polygon_provider/__init__.py +3 -0
  4. webviz_subsurface/_providers/ensemble_polygon_provider/_polygon_discovery.py +97 -0
  5. webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +226 -0
  6. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider.py +53 -0
  7. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider_factory.py +99 -0
  8. webviz_subsurface/_providers/ensemble_polygon_provider/polygon_server.py +125 -0
  9. webviz_subsurface/_providers/ensemble_summary_provider/_provider_impl_arrow_lazy.py +1 -1
  10. webviz_subsurface/plugins/_co2_leakage/_plugin.py +531 -377
  11. webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
  12. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +169 -173
  13. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +329 -84
  14. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
  15. webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
  16. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
  17. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +189 -96
  18. webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
  19. webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
  20. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +29 -21
  21. webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
  22. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +30 -18
  23. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +805 -343
  24. webviz_subsurface/plugins/_relative_permeability.py +1 -1
  25. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/METADATA +2 -2
  26. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/RECORD +32 -21
  27. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/WHEEL +1 -1
  28. /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
  29. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE +0 -0
  30. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE.chromedriver +0 -0
  31. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/entry_points.txt +0 -0
  32. {webviz_subsurface-0.2.35.dist-info → webviz_subsurface-0.2.37.dist-info}/top_level.txt +0 -0
@@ -15,10 +15,17 @@ from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_pro
15
15
  from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import property_origin
16
16
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
17
17
  Co2MassScale,
18
+ Co2VolumeScale,
19
+ FilteredMapAttribute,
18
20
  GraphSource,
19
21
  LayoutLabels,
20
22
  LayoutStyle,
21
23
  MapAttribute,
24
+ MapGroup,
25
+ MapThresholds,
26
+ MapType,
27
+ MenuOptions,
28
+ map_group_labels,
22
29
  )
23
30
 
24
31
 
@@ -33,6 +40,7 @@ class ViewSettings(SettingsGroupABC):
33
40
  FORMATION = "formation"
34
41
  ENSEMBLE = "ensemble"
35
42
  REALIZATION = "realization"
43
+ ALL_REAL = "all-realizations"
36
44
 
37
45
  PROPERTY = "property"
38
46
  STATISTIC = "statistic"
@@ -48,6 +56,8 @@ class ViewSettings(SettingsGroupABC):
48
56
  Y_MAX_GRAPH = "y-max-graph"
49
57
  Y_MIN_AUTO_GRAPH = "y-min-auto-graph"
50
58
  Y_MAX_AUTO_GRAPH = "y-max-auto-graph"
59
+ Y_LIM_OPTIONS = "y_limit_options"
60
+ REAL_OR_STAT = "realization-or-statistics"
51
61
  COLOR_BY = "color-by"
52
62
  MARK_BY = "mark-by"
53
63
  SORT_PLOT = "sort-plot"
@@ -59,120 +69,159 @@ class ViewSettings(SettingsGroupABC):
59
69
  PHASE = "phase"
60
70
  PHASE_MENU = "phase-menu"
61
71
  CONTAINMENT = "containment"
72
+ PLUME_GROUP = "plume-group"
62
73
  CONTAINMENT_MENU = "containment-menu"
74
+ PLUME_GROUP_MENU = "plume-group-menu"
75
+ DATE_OPTION = "date-option"
76
+ DATE_OPTION_COL = "date-option-column"
63
77
 
64
78
  PLUME_THRESHOLD = "plume-threshold"
65
79
  PLUME_SMOOTHING = "plume-smoothing"
66
80
 
67
- VISUALIZATION_THRESHOLD = "visualization-threshold"
68
81
  VISUALIZATION_UPDATE = "visualization-update"
82
+ VISUALIZATION_THRESHOLD_BUTTON = "visualization-threshold-button"
83
+ VISUALIZATION_THRESHOLD_DIALOG = "visualization-threshold-dialog"
69
84
  MASS_UNIT = "mass-unit"
85
+ MASS_UNIT_UPDATE = "mass-unit-update"
70
86
 
71
87
  FEEDBACK_BUTTON = "feedback-button"
72
88
  FEEDBACK = "feedback"
73
89
 
90
+ # pylint: disable=too-many-arguments
74
91
  def __init__(
75
92
  self,
76
93
  ensemble_paths: Dict[str, str],
94
+ realizations_per_ensemble: Dict[str, List[int]],
77
95
  ensemble_surface_providers: Dict[str, EnsembleSurfaceProvider],
78
96
  initial_surface: Optional[str],
79
- map_attribute_names: Dict[MapAttribute, str],
97
+ map_attribute_names: FilteredMapAttribute,
98
+ map_thresholds: MapThresholds,
80
99
  color_scale_names: List[str],
81
100
  well_names_dict: Dict[str, List[str]],
82
- menu_options: Dict[str, Dict[str, Dict[str, List[str]]]],
101
+ menu_options: Dict[str, Dict[GraphSource, MenuOptions]],
102
+ content: Dict[str, bool],
83
103
  ):
84
104
  super().__init__("Settings")
85
105
  self._ensemble_paths = ensemble_paths
106
+ self._realizations_per_ensemble = realizations_per_ensemble
86
107
  self._ensemble_surface_providers = ensemble_surface_providers
87
108
  self._map_attribute_names = map_attribute_names
109
+ self._thresholds = map_thresholds
110
+ self._threshold_ids = list(self._thresholds.standard_thresholds.keys())
88
111
  self._color_scale_names = color_scale_names
89
112
  self._initial_surface = initial_surface
90
113
  self._well_names_dict = well_names_dict
91
114
  self._menu_options = menu_options
92
- self._has_zones = max(
93
- len(inner_dict["zones"]) > 0
94
- for outer_dict in menu_options.values()
95
- for inner_dict in outer_dict.values()
96
- )
97
- self._has_regions = max(
98
- len(inner_dict["regions"]) > 0
99
- for outer_dict in menu_options.values()
100
- for inner_dict in outer_dict.values()
101
- )
115
+ self._content = content
102
116
 
103
117
  def layout(self) -> List[Component]:
104
- return [
105
- DialogLayout(self._well_names_dict, list(self._ensemble_paths.keys())),
106
- OpenDialogButton(),
118
+ menu_layout = []
119
+ if self._content["maps"]:
120
+ menu_layout += [
121
+ DialogLayout(self._well_names_dict, list(self._ensemble_paths.keys())),
122
+ OpenDialogButton(),
123
+ ]
124
+ menu_layout.append(
107
125
  EnsembleSelectorLayout(
108
126
  self.register_component_unique_id(self.Ids.ENSEMBLE),
109
127
  self.register_component_unique_id(self.Ids.REALIZATION),
128
+ self.register_component_unique_id(self.Ids.ALL_REAL),
110
129
  list(self._ensemble_paths.keys()),
111
- ),
112
- FilterSelectorLayout(self.register_component_unique_id(self.Ids.FORMATION)),
113
- MapSelectorLayout(
114
- self._color_scale_names,
115
- self.register_component_unique_id(self.Ids.PROPERTY),
116
- self.register_component_unique_id(self.Ids.STATISTIC),
117
- self.register_component_unique_id(self.Ids.COLOR_SCALE),
118
- self.register_component_unique_id(self.Ids.CM_MIN),
119
- self.register_component_unique_id(self.Ids.CM_MAX),
120
- self.register_component_unique_id(self.Ids.CM_MIN_AUTO),
121
- self.register_component_unique_id(self.Ids.CM_MAX_AUTO),
122
- self.register_component_unique_id(self.Ids.VISUALIZATION_THRESHOLD),
123
- self.register_component_unique_id(self.Ids.VISUALIZATION_UPDATE),
124
- self.register_component_unique_id(self.Ids.MASS_UNIT),
125
- ),
126
- GraphSelectorsLayout(
127
- self.register_component_unique_id(self.Ids.GRAPH_SOURCE),
128
- self.register_component_unique_id(self.Ids.CO2_SCALE),
129
- [
130
- self.register_component_unique_id(self.Ids.Y_MIN_GRAPH),
131
- self.register_component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH),
132
- ],
133
- [
134
- self.register_component_unique_id(self.Ids.Y_MAX_GRAPH),
135
- self.register_component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH),
136
- ],
137
- [
138
- self.register_component_unique_id(self.Ids.COLOR_BY),
139
- self.register_component_unique_id(self.Ids.MARK_BY),
140
- self.register_component_unique_id(self.Ids.SORT_PLOT),
141
- self.register_component_unique_id(self.Ids.ZONE),
142
- self.register_component_unique_id(self.Ids.ZONE_COL),
143
- self.register_component_unique_id(self.Ids.REGION),
144
- self.register_component_unique_id(self.Ids.REGION_COL),
145
- self.register_component_unique_id(self.Ids.ZONE_REGION),
146
- self.register_component_unique_id(self.Ids.PHASE),
147
- self.register_component_unique_id(self.Ids.PHASE_MENU),
148
- self.register_component_unique_id(self.Ids.CONTAINMENT),
149
- self.register_component_unique_id(self.Ids.CONTAINMENT_MENU),
150
- ],
151
- self._has_zones,
152
- self._has_regions,
153
- ),
154
- ExperimentalFeaturesLayout(
155
- self.register_component_unique_id(self.Ids.PLUME_THRESHOLD),
156
- self.register_component_unique_id(self.Ids.PLUME_SMOOTHING),
157
- ),
130
+ )
131
+ )
132
+ if self._content["maps"]:
133
+ menu_layout += [
134
+ FilterSelectorLayout(
135
+ self.register_component_unique_id(self.Ids.FORMATION)
136
+ ),
137
+ VisualizationThresholdsLayout(
138
+ self._threshold_ids,
139
+ self._thresholds,
140
+ self.register_component_unique_id(self.Ids.VISUALIZATION_UPDATE),
141
+ ),
142
+ MapSelectorLayout(
143
+ self._color_scale_names,
144
+ self.register_component_unique_id(self.Ids.PROPERTY),
145
+ self.register_component_unique_id(self.Ids.STATISTIC),
146
+ self.register_component_unique_id(self.Ids.COLOR_SCALE),
147
+ self.register_component_unique_id(self.Ids.CM_MIN),
148
+ self.register_component_unique_id(self.Ids.CM_MAX),
149
+ self.register_component_unique_id(self.Ids.CM_MIN_AUTO),
150
+ self.register_component_unique_id(self.Ids.CM_MAX_AUTO),
151
+ self.register_component_unique_id(self.Ids.MASS_UNIT),
152
+ self.register_component_unique_id(self.Ids.MASS_UNIT_UPDATE),
153
+ self._map_attribute_names,
154
+ ),
155
+ ]
156
+ if self._content["any_table"]:
157
+ menu_layout.append(
158
+ GraphSelectorsLayout(
159
+ self.register_component_unique_id(self.Ids.GRAPH_SOURCE),
160
+ self.register_component_unique_id(self.Ids.CO2_SCALE),
161
+ [
162
+ self.register_component_unique_id(self.Ids.Y_MIN_GRAPH),
163
+ self.register_component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH),
164
+ ],
165
+ [
166
+ self.register_component_unique_id(self.Ids.Y_MAX_GRAPH),
167
+ self.register_component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH),
168
+ ],
169
+ [
170
+ self.register_component_unique_id(self.Ids.COLOR_BY),
171
+ self.register_component_unique_id(self.Ids.MARK_BY),
172
+ self.register_component_unique_id(self.Ids.SORT_PLOT),
173
+ self.register_component_unique_id(self.Ids.ZONE),
174
+ self.register_component_unique_id(self.Ids.ZONE_COL),
175
+ self.register_component_unique_id(self.Ids.REGION),
176
+ self.register_component_unique_id(self.Ids.REGION_COL),
177
+ self.register_component_unique_id(self.Ids.ZONE_REGION),
178
+ self.register_component_unique_id(self.Ids.PHASE),
179
+ self.register_component_unique_id(self.Ids.PHASE_MENU),
180
+ self.register_component_unique_id(self.Ids.CONTAINMENT),
181
+ self.register_component_unique_id(self.Ids.CONTAINMENT_MENU),
182
+ self.register_component_unique_id(self.Ids.PLUME_GROUP),
183
+ self.register_component_unique_id(self.Ids.PLUME_GROUP_MENU),
184
+ self.register_component_unique_id(self.Ids.REAL_OR_STAT),
185
+ self.register_component_unique_id(self.Ids.Y_LIM_OPTIONS),
186
+ self.register_component_unique_id(self.Ids.DATE_OPTION),
187
+ self.register_component_unique_id(self.Ids.DATE_OPTION_COL),
188
+ ],
189
+ self._content,
190
+ )
191
+ )
192
+ if self._content["maps"]:
193
+ menu_layout.append(
194
+ ExperimentalFeaturesLayout(
195
+ self.register_component_unique_id(self.Ids.PLUME_THRESHOLD),
196
+ self.register_component_unique_id(self.Ids.PLUME_SMOOTHING),
197
+ ),
198
+ )
199
+ menu_layout += [
158
200
  FeedbackLayout(),
159
201
  FeedbackButton(),
160
202
  ]
203
+ return menu_layout
161
204
 
205
+ # pylint: disable=too-many-statements
162
206
  def set_callbacks(self) -> None:
207
+ # pylint: disable=unused-argument
163
208
  @callback(
164
209
  Output(
165
210
  self.component_unique_id(self.Ids.REALIZATION).to_string(), "options"
166
211
  ),
167
212
  Output(self.component_unique_id(self.Ids.REALIZATION).to_string(), "value"),
168
213
  Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
214
+ Input(self.component_unique_id(self.Ids.ALL_REAL).to_string(), "n_clicks"),
169
215
  )
170
- def set_realizations(ensemble: str) -> Tuple[List[Dict[str, Any]], List[int]]:
216
+ def set_realizations(
217
+ ensemble: str,
218
+ select_all: int,
219
+ ) -> Tuple[List[Dict[str, Any]], List[int]]:
171
220
  rlz = [
172
221
  {"value": r, "label": str(r)}
173
- for r in self._ensemble_surface_providers[ensemble].realizations()
222
+ for r in self._realizations_per_ensemble[ensemble]
174
223
  ]
175
- return rlz, [rlz[0]["value"]] # type: ignore
224
+ return rlz, self._realizations_per_ensemble[ensemble] # type: ignore
176
225
 
177
226
  @callback(
178
227
  Output(self.component_unique_id(self.Ids.FORMATION).to_string(), "options"),
@@ -193,10 +242,7 @@ class ViewSettings(SettingsGroupABC):
193
242
  if len(surfaces) == 0:
194
243
  warning = f"Surface not found for property: {prop}.\n"
195
244
  warning += f"Expected name: <formation>--{prop_name}"
196
- if MapAttribute(prop) not in [
197
- MapAttribute.MIGRATION_TIME_SGAS,
198
- MapAttribute.MIGRATION_TIME_AMFG,
199
- ]:
245
+ if MapType[MapAttribute(prop).name].value != "MIGRATION_TIME":
200
246
  warning += "--<date>"
201
247
  warnings.warn(warning + ".gri")
202
248
  # Formation names
@@ -215,181 +261,300 @@ class ViewSettings(SettingsGroupABC):
215
261
  )
216
262
  return formations, picked_formation
217
263
 
218
- @callback(
219
- Output(
220
- self.component_unique_id(self.Ids.STATISTIC).to_string(), "disabled"
221
- ),
222
- Input(self.component_unique_id(self.Ids.REALIZATION).to_string(), "value"),
223
- Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
224
- )
225
- def toggle_statistics(realizations: List[int], attribute: str) -> bool:
226
- if len(realizations) <= 1:
227
- return True
228
- if MapAttribute(attribute) in (
229
- MapAttribute.SGAS_PLUME,
230
- MapAttribute.AMFG_PLUME,
231
- ):
232
- return True
233
- return False
264
+ if self._content["maps"]:
234
265
 
235
- @callback(
236
- Output(self.component_unique_id(self.Ids.CM_MIN).to_string(), "disabled"),
237
- Output(self.component_unique_id(self.Ids.CM_MAX).to_string(), "disabled"),
238
- Input(self.component_unique_id(self.Ids.CM_MIN_AUTO).to_string(), "value"),
239
- Input(self.component_unique_id(self.Ids.CM_MAX_AUTO).to_string(), "value"),
240
- )
241
- def set_color_range_data(
242
- min_auto: List[str], max_auto: List[str]
243
- ) -> Tuple[bool, bool]:
244
- return len(min_auto) == 1, len(max_auto) == 1
266
+ @callback(
267
+ Output(
268
+ self.component_unique_id(self.Ids.STATISTIC).to_string(), "disabled"
269
+ ),
270
+ Input(
271
+ self.component_unique_id(self.Ids.REALIZATION).to_string(), "value"
272
+ ),
273
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
274
+ )
275
+ def toggle_statistics(realizations: List[int], attribute: str) -> bool:
276
+ if len(realizations) <= 1:
277
+ return True
278
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
279
+ return True
280
+ return False
245
281
 
246
- @callback(
247
- Output(
248
- self.component_unique_id(self.Ids.VISUALIZATION_THRESHOLD).to_string(),
249
- "disabled",
250
- ),
251
- Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
252
- )
253
- def set_visualization_threshold(attribute: str) -> bool:
254
- return MapAttribute(attribute) in [
255
- MapAttribute.MIGRATION_TIME_SGAS,
256
- MapAttribute.MIGRATION_TIME_AMFG,
257
- ]
282
+ @callback(
283
+ Output(
284
+ self.component_unique_id(self.Ids.CM_MIN).to_string(), "disabled"
285
+ ),
286
+ Output(
287
+ self.component_unique_id(self.Ids.CM_MAX).to_string(), "disabled"
288
+ ),
289
+ Input(
290
+ self.component_unique_id(self.Ids.CM_MIN_AUTO).to_string(), "value"
291
+ ),
292
+ Input(
293
+ self.component_unique_id(self.Ids.CM_MAX_AUTO).to_string(), "value"
294
+ ),
295
+ )
296
+ def set_color_range_data(
297
+ min_auto: List[str], max_auto: List[str]
298
+ ) -> Tuple[bool, bool]:
299
+ return len(min_auto) == 1, len(max_auto) == 1
258
300
 
259
- @callback(
260
- Output(
261
- self.component_unique_id(self.Ids.Y_MIN_GRAPH).to_string(), "disabled"
262
- ),
263
- Output(
264
- self.component_unique_id(self.Ids.Y_MAX_GRAPH).to_string(), "disabled"
265
- ),
266
- Input(
267
- self.component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH).to_string(), "value"
268
- ),
269
- Input(
270
- self.component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH).to_string(), "value"
271
- ),
272
- )
273
- def set_y_min_max(
274
- min_auto: List[str], max_auto: List[str]
275
- ) -> Tuple[bool, bool]:
276
- return len(min_auto) == 1, len(max_auto) == 1
301
+ @callback(
302
+ Output(
303
+ self.component_unique_id(self.Ids.MASS_UNIT).to_string(), "disabled"
304
+ ),
305
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
306
+ )
307
+ def toggle_unit(attribute: str) -> bool:
308
+ if MapType[MapAttribute(attribute).name].value != "MASS":
309
+ return True
310
+ return False
277
311
 
278
- @callback(
279
- Output(self.component_unique_id(self.Ids.PHASE).to_string(), "options"),
280
- Output(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
281
- Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"),
282
- Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
283
- State(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
284
- )
285
- def set_phases(
286
- source: GraphSource,
287
- ensemble: str,
288
- current_value: str,
289
- ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
290
- if ensemble is not None:
291
- phases = self._menu_options[ensemble][source]["phases"]
292
- options = [{"label": phase.title(), "value": phase} for phase in phases]
293
- return options, no_update if current_value in phases else "total"
294
- return [], "total"
312
+ if self._content["any_table"]:
295
313
 
296
- @callback(
297
- Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"),
298
- Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
299
- Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"),
300
- Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
301
- State(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
302
- )
303
- def set_zones(
304
- source: GraphSource,
305
- ensemble: str,
306
- current_value: str,
307
- ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
308
- if ensemble is not None:
309
- zones = self._menu_options[ensemble][source]["zones"]
310
- if len(zones) > 0:
311
- options = [{"label": zone.title(), "value": zone} for zone in zones]
312
- return options, no_update if current_value in zones else "all"
313
- return [], "all"
314
+ @callback(
315
+ Output(
316
+ self.component_unique_id(self.Ids.Y_MIN_GRAPH).to_string(),
317
+ "disabled",
318
+ ),
319
+ Output(
320
+ self.component_unique_id(self.Ids.Y_MAX_GRAPH).to_string(),
321
+ "disabled",
322
+ ),
323
+ Input(
324
+ self.component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH).to_string(),
325
+ "value",
326
+ ),
327
+ Input(
328
+ self.component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH).to_string(),
329
+ "value",
330
+ ),
331
+ )
332
+ def set_y_min_max(
333
+ min_auto: List[str], max_auto: List[str]
334
+ ) -> Tuple[bool, bool]:
335
+ return len(min_auto) == 1, len(max_auto) == 1
314
336
 
315
- @callback(
316
- Output(self.component_unique_id(self.Ids.REGION).to_string(), "options"),
317
- Output(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
318
- Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"),
319
- Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
320
- State(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
321
- )
322
- def set_regions(
323
- source: GraphSource,
324
- ensemble: str,
325
- current_value: str,
326
- ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
327
- if ensemble is not None:
328
- regions = self._menu_options[ensemble][source]["regions"]
329
- if len(regions) > 0:
330
- options = [{"label": reg.title(), "value": reg} for reg in regions]
331
- return options, no_update if current_value in regions else "all"
332
- return [], "all"
337
+ @callback(
338
+ Output(self.component_unique_id(self.Ids.PHASE).to_string(), "options"),
339
+ Output(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
340
+ Input(
341
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
342
+ ),
343
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
344
+ State(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
345
+ )
346
+ def set_phases(
347
+ source: GraphSource,
348
+ ensemble: str,
349
+ current_value: str,
350
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
351
+ if ensemble is not None:
352
+ phases = self._menu_options[ensemble][source]["phases"]
353
+ options = [
354
+ {"label": phase.title(), "value": phase} for phase in phases
355
+ ]
356
+ return options, no_update if current_value in phases else "total"
357
+ return [{"label": "Total", "value": "total"}], "total"
333
358
 
334
- @callback(
335
- Output(
336
- self.component_unique_id(self.Ids.MASS_UNIT).to_string(), "disabled"
337
- ),
338
- Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
339
- )
340
- def toggle_unit(attribute: str) -> bool:
341
- if MapAttribute(attribute) not in (
342
- MapAttribute.MASS,
343
- MapAttribute.FREE,
344
- MapAttribute.DISSOLVED,
345
- ):
346
- return True
347
- return False
359
+ @callback(
360
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"),
361
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
362
+ Input(
363
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
364
+ ),
365
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
366
+ State(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
367
+ )
368
+ def set_zones(
369
+ source: GraphSource,
370
+ ensemble: str,
371
+ current_value: str,
372
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
373
+ if ensemble is not None:
374
+ zones = self._menu_options[ensemble][source]["zones"]
375
+ if len(zones) > 0:
376
+ options = [
377
+ {"label": zone.title(), "value": zone} for zone in zones
378
+ ]
379
+ return options, no_update if current_value in zones else "all"
380
+ return [{"label": "All", "value": "all"}], "all"
348
381
 
349
- @callback(
350
- Output(self.component_unique_id(self.Ids.MARK_BY).to_string(), "options"),
351
- Output(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
352
- Output(self.component_unique_id(self.Ids.ZONE_COL).to_string(), "style"),
353
- Output(self.component_unique_id(self.Ids.REGION_COL).to_string(), "style"),
354
- Output(self.component_unique_id(self.Ids.PHASE_MENU).to_string(), "style"),
355
- Output(
356
- self.component_unique_id(self.Ids.CONTAINMENT_MENU).to_string(),
357
- "style",
358
- ),
359
- Input(self.component_unique_id(self.Ids.COLOR_BY).to_string(), "value"),
360
- Input(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
361
- )
362
- def organize_color_and_mark_menus(
363
- color_choice: str,
364
- mark_choice: str,
365
- ) -> Tuple[List[Dict], str, Dict, Dict, Dict, Dict]:
366
- mark_options = [
367
- {"label": "Phase", "value": "phase"},
368
- {"label": "None", "value": "none"},
369
- ]
370
- if self._has_zones and color_choice == "containment":
371
- mark_options.append({"label": "Zone", "value": "zone"})
372
- if self._has_regions and color_choice == "containment":
373
- mark_options.append({"label": "Region", "value": "region"})
374
- if color_choice in ["zone", "region"]:
375
- mark_options.append({"label": "Containment", "value": "containment"})
376
- if mark_choice is None or mark_choice == color_choice:
377
- mark_choice = "phase"
378
- if mark_choice in ["zone", "region"] and color_choice in ["zone", "region"]:
379
- mark_choice = "phase"
380
- zone, region, phase, containment = _make_styles(
381
- color_choice, mark_choice, self._has_zones, self._has_regions
382
+ @callback(
383
+ Output(
384
+ self.component_unique_id(self.Ids.REGION).to_string(), "options"
385
+ ),
386
+ Output(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
387
+ Input(
388
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
389
+ ),
390
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
391
+ State(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
382
392
  )
383
- return mark_options, mark_choice, zone, region, phase, containment
393
+ def set_regions(
394
+ source: GraphSource,
395
+ ensemble: str,
396
+ current_value: str,
397
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
398
+ if ensemble is not None:
399
+ regions = self._menu_options[ensemble][source]["regions"]
400
+ if len(regions) > 0:
401
+ options = [
402
+ {"label": reg.title(), "value": reg} for reg in regions
403
+ ]
404
+ return options, no_update if current_value in regions else "all"
405
+ return [{"label": "All", "value": "all"}], "all"
384
406
 
385
- @callback(
386
- Output(self.component_unique_id(self.Ids.ZONE).to_string(), "disabled"),
387
- Output(self.component_unique_id(self.Ids.REGION).to_string(), "disabled"),
388
- Input(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
389
- Input(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
390
- )
391
- def disable_zone_or_region(zone: str, region: str) -> Tuple[bool, bool]:
392
- return region != "all", zone != "all"
407
+ @callback(
408
+ Output(
409
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(),
410
+ "options",
411
+ ),
412
+ Output(
413
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(), "value"
414
+ ),
415
+ Input(
416
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
417
+ ),
418
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
419
+ State(
420
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(), "value"
421
+ ),
422
+ )
423
+ def set_plume_groups(
424
+ source: GraphSource,
425
+ ensemble: str,
426
+ current_value: str,
427
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
428
+ if ensemble is not None:
429
+ plume_groups = self._menu_options[ensemble][source]["plume_groups"]
430
+ if len(plume_groups) > 0:
431
+ options = [
432
+ {"label": x.title(), "value": x} for x in plume_groups
433
+ ]
434
+ return (
435
+ options,
436
+ no_update if current_value in plume_groups else "all",
437
+ )
438
+ return [{"label": "All", "value": "all"}], "all"
439
+
440
+ @callback(
441
+ Output(
442
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(),
443
+ "options",
444
+ ),
445
+ Output(
446
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(), "value"
447
+ ),
448
+ Input(
449
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
450
+ ),
451
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
452
+ State(
453
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(), "value"
454
+ ),
455
+ )
456
+ def set_date_option(
457
+ source: GraphSource,
458
+ ensemble: str,
459
+ current_value: str,
460
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
461
+ if ensemble is not None:
462
+ dates = self._menu_options[ensemble][source]["dates"]
463
+ options = [{"label": date.title(), "value": date} for date in dates]
464
+ return options, no_update if current_value in dates else dates[-1]
465
+ return [], None
466
+
467
+ @callback(
468
+ Output(
469
+ self.component_unique_id(self.Ids.MARK_BY).to_string(), "options"
470
+ ),
471
+ Output(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
472
+ Output(
473
+ self.component_unique_id(self.Ids.ZONE_COL).to_string(), "style"
474
+ ),
475
+ Output(
476
+ self.component_unique_id(self.Ids.REGION_COL).to_string(), "style"
477
+ ),
478
+ Output(
479
+ self.component_unique_id(self.Ids.PHASE_MENU).to_string(), "style"
480
+ ),
481
+ Output(
482
+ self.component_unique_id(self.Ids.CONTAINMENT_MENU).to_string(),
483
+ "style",
484
+ ),
485
+ Output(
486
+ self.component_unique_id(self.Ids.PLUME_GROUP_MENU).to_string(),
487
+ "style",
488
+ ),
489
+ Input(self.component_unique_id(self.Ids.COLOR_BY).to_string(), "value"),
490
+ Input(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
491
+ )
492
+ def organize_color_and_mark_menus(
493
+ color_choice: str,
494
+ mark_choice: str,
495
+ ) -> Tuple[List[Dict], str, Dict, Dict, Dict, Dict, Dict]:
496
+ mark_options = [
497
+ {"label": "Phase", "value": "phase"},
498
+ {"label": "None", "value": "none"},
499
+ ]
500
+ if self._content["zones"] and color_choice == "containment":
501
+ mark_options.append({"label": "Zone", "value": "zone"})
502
+ if self._content["regions"] and color_choice == "containment":
503
+ mark_options.append({"label": "Region", "value": "region"})
504
+ if self._content["plume_groups"] and color_choice == "containment":
505
+ mark_options.append(
506
+ {"label": "Plume group", "value": "plume_group"}
507
+ )
508
+ if color_choice in ["zone", "region", "plume_group"]:
509
+ mark_options.append(
510
+ {"label": "Containment", "value": "containment"}
511
+ )
512
+ if (
513
+ color_choice in ["zone", "region"]
514
+ and self._content["plume_groups"]
515
+ ):
516
+ mark_options.append(
517
+ {"label": "Plume group", "value": "plume_group"}
518
+ )
519
+ if color_choice == "plume_group":
520
+ if self._content["zones"]:
521
+ mark_options.append({"label": "Zone", "value": "zone"})
522
+ if self._content["regions"]:
523
+ mark_options.append({"label": "Region", "value": "region"})
524
+ if mark_choice is None or mark_choice == color_choice:
525
+ mark_choice = "phase"
526
+ if mark_choice in ["zone", "region"] and color_choice in [
527
+ "zone",
528
+ "region",
529
+ ]:
530
+ mark_choice = "phase"
531
+ zone, region, phase, containment, plume_group = _make_styles(
532
+ color_choice,
533
+ mark_choice,
534
+ self._content["zones"],
535
+ self._content["regions"],
536
+ self._content["plume_groups"],
537
+ )
538
+ return (
539
+ mark_options,
540
+ mark_choice,
541
+ zone,
542
+ region,
543
+ phase,
544
+ containment,
545
+ plume_group,
546
+ )
547
+
548
+ @callback(
549
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "disabled"),
550
+ Output(
551
+ self.component_unique_id(self.Ids.REGION).to_string(), "disabled"
552
+ ),
553
+ Input(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
554
+ Input(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
555
+ )
556
+ def disable_zone_or_region(zone: str, region: str) -> Tuple[bool, bool]:
557
+ return region != "all", zone != "all"
393
558
 
394
559
 
395
560
  class OpenDialogButton(html.Button):
@@ -484,6 +649,87 @@ class FilterSelectorLayout(wcc.Selectors):
484
649
  )
485
650
 
486
651
 
652
+ class OpenVisualizationThresholdsButton(html.Button):
653
+ def __init__(self) -> None:
654
+ super().__init__(
655
+ LayoutLabels.VISUALIZATION_THRESHOLDS,
656
+ id=ViewSettings.Ids.VISUALIZATION_THRESHOLD_BUTTON,
657
+ style=LayoutStyle.THRESHOLDS_BUTTON,
658
+ n_clicks=0,
659
+ )
660
+
661
+
662
+ class VisualizationThresholdsLayout(wcc.Dialog):
663
+ """Layout for the visualization thresholds dialog"""
664
+
665
+ def __init__(
666
+ self,
667
+ ids: List[str],
668
+ thresholds: MapThresholds,
669
+ visualization_update_id: str,
670
+ ) -> None:
671
+ standard_thresholds = thresholds.standard_thresholds
672
+
673
+ fields = [
674
+ html.Div(
675
+ "Here you can select a filter for the visualization of the map, "
676
+ "hiding values smaller than the selected minimum cutoff. "
677
+ "After changing the threshold value, press 'Update' to have the map reappear. "
678
+ "A value of -1 can be used to visualize zeros."
679
+ ),
680
+ html.Div("", style={"height": "30px"}),
681
+ html.Div(
682
+ [
683
+ html.Div("Property:", style={"width": "42%"}),
684
+ html.Div("Standard cutoff:", style={"width": "32%"}),
685
+ html.Div("Minimum cutoff:", style={"width": "25%"}),
686
+ ],
687
+ style={"display": "flex", "flex-direction": "row"},
688
+ ),
689
+ ]
690
+ fields += [
691
+ html.Div(
692
+ [
693
+ html.Div(id, style={"width": "42%"}),
694
+ html.Div(standard_thresholds[id], style={"width": "32%"}),
695
+ dcc.Input(
696
+ id=id,
697
+ type="number",
698
+ value=standard_thresholds[id],
699
+ step="0.0005",
700
+ style={"width": "25%"},
701
+ ),
702
+ ],
703
+ style={"display": "flex", "flex-direction": "row"},
704
+ )
705
+ for id in ids
706
+ ]
707
+ fields.append(html.Div(style={"height": "20px"}))
708
+ fields.append(
709
+ html.Div(
710
+ [
711
+ html.Div(style={"width": "80%"}),
712
+ html.Button(
713
+ "Update",
714
+ id=visualization_update_id,
715
+ style=LayoutStyle.VISUALIZATION_BUTTON,
716
+ n_clicks=0,
717
+ ),
718
+ ],
719
+ style={"display": "flex", "flex-direction": "row"},
720
+ )
721
+ )
722
+ super().__init__(
723
+ title=LayoutLabels.VISUALIZATION_THRESHOLDS,
724
+ id=ViewSettings.Ids.VISUALIZATION_THRESHOLD_DIALOG,
725
+ draggable=True,
726
+ open=False,
727
+ children=html.Div(
728
+ fields, style={"flex-direction": "column", "width": "500px"}
729
+ ),
730
+ )
731
+
732
+
487
733
  class MapSelectorLayout(wcc.Selectors):
488
734
  _CM_RANGE = {
489
735
  "display": "flex",
@@ -501,9 +747,9 @@ class MapSelectorLayout(wcc.Selectors):
501
747
  cm_max_id: str,
502
748
  cm_min_auto_id: str,
503
749
  cm_max_auto_id: str,
504
- visualization_threshold_id: str,
505
- visualization_update_id: str,
506
750
  mass_unit_id: str,
751
+ mass_unit_update_id: str,
752
+ map_attribute_names: FilteredMapAttribute,
507
753
  ):
508
754
  default_colormap = (
509
755
  "turbo (Seq)"
@@ -519,8 +765,8 @@ class MapSelectorLayout(wcc.Selectors):
519
765
  "Property",
520
766
  wcc.Dropdown(
521
767
  id=property_id,
522
- options=_compile_property_options(),
523
- value=MapAttribute.MIGRATION_TIME_SGAS.value,
768
+ options=_compile_property_options(map_attribute_names),
769
+ value=next(iter(map_attribute_names.filtered_values)).value,
524
770
  clearable=False,
525
771
  ),
526
772
  "Statistic",
@@ -568,32 +814,28 @@ class MapSelectorLayout(wcc.Selectors):
568
814
  ],
569
815
  style=self._CM_RANGE,
570
816
  ),
571
- "Visualization threshold",
817
+ "Mass unit (for mass maps)",
572
818
  html.Div(
573
819
  [
574
- dcc.Input(
575
- id=visualization_threshold_id,
576
- type="number",
577
- value=-1.0,
578
- style={"width": "70%"},
820
+ html.Div(
821
+ wcc.Dropdown(
822
+ id=mass_unit_id,
823
+ options=["kg", "tons", "M tons"],
824
+ value="tons",
825
+ clearable=False,
826
+ ),
827
+ style={"width": "50%"},
579
828
  ),
580
- html.Div(style={"width": "5%"}),
581
829
  html.Button(
582
- "Update",
583
- id=visualization_update_id,
830
+ "Update unit",
831
+ id=mass_unit_update_id,
584
832
  style=LayoutStyle.VISUALIZATION_BUTTON,
585
833
  n_clicks=0,
586
834
  ),
587
835
  ],
588
836
  style={"display": "flex"},
589
837
  ),
590
- "Mass unit (for mass maps)",
591
- wcc.Dropdown(
592
- id=mass_unit_id,
593
- options=["kg", "tons", "M tons"],
594
- value="kg",
595
- clearable=False,
596
- ),
838
+ OpenVisualizationThresholdsButton(),
597
839
  ],
598
840
  )
599
841
  ],
@@ -613,40 +855,50 @@ class GraphSelectorsLayout(wcc.Selectors):
613
855
  y_min_ids: List[str],
614
856
  y_max_ids: List[str],
615
857
  containment_ids: List[str],
616
- has_zones: bool,
617
- has_regions: bool,
858
+ content: Dict[str, bool],
618
859
  ):
619
- disp_zone = "flex" if has_zones else "none"
620
- disp_region = "flex" if has_regions else "none"
621
- header = "Containment for specific"
622
- if has_zones and not has_regions:
623
- header += " zone"
624
- elif has_regions and not has_zones:
625
- header += " region"
860
+ disp_zone = "flex" if content["zones"] else "none"
861
+ disp_region = "flex" if content["regions"] else "none"
862
+ disp_plume_group = "flex" if content["plume_groups"] else "none"
626
863
  color_options = [{"label": "Containment (standard)", "value": "containment"}]
627
864
  mark_options = [{"label": "Phase", "value": "phase"}]
628
- if has_zones:
865
+ if content["zones"]:
629
866
  color_options.append({"label": "Zone", "value": "zone"})
630
867
  mark_options.append({"label": "Zone", "value": "zone"})
631
- if has_regions:
868
+ if content["regions"]:
632
869
  color_options.append({"label": "Region", "value": "region"})
633
870
  mark_options.append({"label": "Region", "value": "region"})
871
+ if content["plume_groups"]:
872
+ color_options.append({"label": "Plume group", "value": "plume_group"})
873
+ mark_options.append({"label": "Plume group", "value": "plume_group"})
874
+ source_options = []
875
+ if content["mass"]:
876
+ source_options.append(GraphSource.CONTAINMENT_MASS)
877
+ if content["volume"]:
878
+ source_options.append(GraphSource.CONTAINMENT_ACTUAL_VOLUME)
879
+ if content["unsmry"]:
880
+ source_options.append(GraphSource.UNSMRY)
881
+ unit_options, init_unit = (
882
+ (list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS)
883
+ if source_options[0] == GraphSource.CONTAINMENT_ACTUAL_VOLUME
884
+ else (list(Co2MassScale), Co2MassScale.MTONS)
885
+ )
634
886
  super().__init__(
635
887
  label="Graph Settings",
636
- open_details=False,
888
+ open_details=not content["maps"],
637
889
  children=[
638
890
  "Source",
639
891
  wcc.Dropdown(
640
892
  id=graph_source_id,
641
- options=list(GraphSource),
642
- value=GraphSource.CONTAINMENT_MASS,
893
+ options=source_options,
894
+ value=source_options[0],
643
895
  clearable=False,
644
896
  ),
645
897
  "Unit",
646
898
  wcc.Dropdown(
647
899
  id=co2_scale_id,
648
- options=list(Co2MassScale),
649
- value=Co2MassScale.MTONS,
900
+ options=unit_options,
901
+ value=init_unit,
650
902
  clearable=False,
651
903
  ),
652
904
  html.Div(
@@ -683,7 +935,7 @@ class GraphSelectorsLayout(wcc.Selectors):
683
935
  ),
684
936
  ],
685
937
  style={
686
- "display": "flex", # disp,
938
+ "display": "flex",
687
939
  "flex-direction": "row",
688
940
  "margin-top": "10px",
689
941
  "margin-bottom": "1px",
@@ -712,6 +964,7 @@ class GraphSelectorsLayout(wcc.Selectors):
712
964
  [
713
965
  "Zone",
714
966
  wcc.Dropdown(
967
+ options=[{"label": "All", "value": "all"}],
715
968
  value="all",
716
969
  id=containment_ids[3],
717
970
  clearable=False,
@@ -719,7 +972,18 @@ class GraphSelectorsLayout(wcc.Selectors):
719
972
  ],
720
973
  id=containment_ids[4],
721
974
  style={
722
- "width": "50%" if has_regions else "100%",
975
+ "width": (
976
+ "33%"
977
+ if (content["regions"] and content["plume_groups"])
978
+ else (
979
+ "50%"
980
+ if (
981
+ content["regions"]
982
+ or content["plume_groups"]
983
+ )
984
+ else "100%"
985
+ )
986
+ ),
723
987
  "display": disp_zone,
724
988
  "flex-direction": "column",
725
989
  },
@@ -728,6 +992,7 @@ class GraphSelectorsLayout(wcc.Selectors):
728
992
  [
729
993
  "Region",
730
994
  wcc.Dropdown(
995
+ options=[{"label": "All", "value": "all"}],
731
996
  value="all",
732
997
  id=containment_ids[5],
733
998
  clearable=False,
@@ -735,7 +1000,15 @@ class GraphSelectorsLayout(wcc.Selectors):
735
1000
  ],
736
1001
  id=containment_ids[6],
737
1002
  style={
738
- "width": "50%" if has_zones else "100%",
1003
+ "width": (
1004
+ "33%"
1005
+ if (content["zones"] and content["plume_groups"])
1006
+ else (
1007
+ "50%"
1008
+ if (content["zones"] or content["plume_groups"])
1009
+ else "100%"
1010
+ )
1011
+ ),
739
1012
  "display": disp_region,
740
1013
  "flex-direction": "column",
741
1014
  },
@@ -744,6 +1017,7 @@ class GraphSelectorsLayout(wcc.Selectors):
744
1017
  [
745
1018
  "Phase",
746
1019
  wcc.Dropdown(
1020
+ options=[{"label": "Total", "value": "total"}],
747
1021
  value="total",
748
1022
  clearable=False,
749
1023
  id=containment_ids[8],
@@ -770,37 +1044,105 @@ class GraphSelectorsLayout(wcc.Selectors):
770
1044
  id=containment_ids[11],
771
1045
  style={"display": "none"},
772
1046
  ),
1047
+ html.Div(
1048
+ [
1049
+ "Plume",
1050
+ wcc.Dropdown(
1051
+ options=[{"label": "All", "value": "all"}],
1052
+ value="all",
1053
+ id=containment_ids[12],
1054
+ clearable=False,
1055
+ ),
1056
+ ],
1057
+ id=containment_ids[13],
1058
+ style={
1059
+ "width": (
1060
+ "33%"
1061
+ if (content["zones"] and content["regions"])
1062
+ else (
1063
+ "50%"
1064
+ if (content["zones"] or content["regions"])
1065
+ else "100%"
1066
+ )
1067
+ ),
1068
+ "display": disp_plume_group,
1069
+ "flex-direction": "column",
1070
+ },
1071
+ ),
773
1072
  ],
774
1073
  id=containment_ids[7],
775
1074
  style={"display": "flex"},
776
1075
  ),
777
1076
  html.Div(
778
- "Fix y-limits in third plot:",
1077
+ "Time plot options:",
779
1078
  style={"margin-top": "10px"},
780
1079
  ),
781
- "Minimum",
782
1080
  html.Div(
783
1081
  [
784
- dcc.Input(id=y_min_ids[0], type="number"),
785
- dcc.Checklist(
786
- ["Auto"],
787
- ["Auto"],
788
- id=y_min_ids[1],
1082
+ dcc.RadioItems(
1083
+ options=[
1084
+ {"label": "Realizations", "value": "real"},
1085
+ {"label": "Mean/P10/P90", "value": "stat"},
1086
+ ],
1087
+ value="real",
1088
+ id=containment_ids[14],
1089
+ inline=True,
789
1090
  ),
790
1091
  ],
791
- style=self._CM_RANGE,
1092
+ style={
1093
+ "display": "flex",
1094
+ "flex-direction": "row",
1095
+ },
1096
+ ),
1097
+ html.Div(
1098
+ "State at date:",
1099
+ style={"margin-top": "8"},
792
1100
  ),
793
- "Maximum",
794
1101
  html.Div(
795
1102
  [
796
- dcc.Input(id=y_max_ids[0], type="number"),
797
- dcc.Checklist(
798
- ["Auto"],
799
- ["Auto"],
800
- id=y_max_ids[1],
1103
+ wcc.Dropdown(
1104
+ id=containment_ids[16],
1105
+ clearable=False,
801
1106
  ),
802
1107
  ],
803
- style=self._CM_RANGE,
1108
+ id=containment_ids[17],
1109
+ style={
1110
+ "width": "100%",
1111
+ "flex-direction": "row",
1112
+ },
1113
+ ),
1114
+ html.Div(
1115
+ [
1116
+ "Fix minimum y-value",
1117
+ html.Div(
1118
+ [
1119
+ dcc.Input(id=y_min_ids[0], type="number"),
1120
+ dcc.Checklist(
1121
+ ["Auto"],
1122
+ ["Auto"],
1123
+ id=y_min_ids[1],
1124
+ ),
1125
+ ],
1126
+ style=self._CM_RANGE,
1127
+ ),
1128
+ "Fix maximum y-value",
1129
+ html.Div(
1130
+ [
1131
+ dcc.Input(id=y_max_ids[0], type="number"),
1132
+ dcc.Checklist(
1133
+ ["Auto"],
1134
+ ["Auto"],
1135
+ id=y_max_ids[1],
1136
+ ),
1137
+ ],
1138
+ style=self._CM_RANGE,
1139
+ ),
1140
+ ],
1141
+ style={
1142
+ "display": "flex",
1143
+ "flex-direction": "column",
1144
+ },
1145
+ id=containment_ids[15],
804
1146
  ),
805
1147
  ],
806
1148
  )
@@ -848,7 +1190,13 @@ class ExperimentalFeaturesLayout(wcc.Selectors):
848
1190
 
849
1191
 
850
1192
  class EnsembleSelectorLayout(wcc.Selectors):
851
- def __init__(self, ensemble_id: str, realization_id: str, ensembles: List[str]):
1193
+ def __init__(
1194
+ self,
1195
+ ensemble_id: str,
1196
+ realization_id: str,
1197
+ all_real_id: str,
1198
+ ensembles: List[str],
1199
+ ):
852
1200
  super().__init__(
853
1201
  label="Ensemble",
854
1202
  open_details=True,
@@ -860,7 +1208,23 @@ class EnsembleSelectorLayout(wcc.Selectors):
860
1208
  value=ensembles[0],
861
1209
  clearable=False,
862
1210
  ),
863
- "Realization",
1211
+ html.Div(
1212
+ [
1213
+ html.Div("Realization", style={"width": "50%"}),
1214
+ html.Button(
1215
+ "Select all",
1216
+ id=all_real_id,
1217
+ style=LayoutStyle.ALL_REAL_BUTTON,
1218
+ n_clicks=0,
1219
+ ),
1220
+ ],
1221
+ style={
1222
+ "display": "flex",
1223
+ "flex-direction": "row",
1224
+ "margin-top": "3px",
1225
+ "margin-bottom": "3px",
1226
+ },
1227
+ ),
864
1228
  wcc.SelectWithLabel(
865
1229
  id=realization_id,
866
1230
  value=[],
@@ -870,44 +1234,34 @@ class EnsembleSelectorLayout(wcc.Selectors):
870
1234
  )
871
1235
 
872
1236
 
873
- def _compile_property_options() -> List[Dict[str, Any]]:
1237
+ def _create_left_side_menu(
1238
+ map_group: str, map_attribute_names: FilteredMapAttribute
1239
+ ) -> List:
1240
+ title = {
1241
+ "label": html.Span([f"{map_group}:"], style={"text-decoration": "underline"}),
1242
+ "value": "",
1243
+ "disabled": True,
1244
+ }
1245
+ map_attribute_list = [
1246
+ {"label": MapAttribute[key.name].value, "value": MapAttribute[key.name].value}
1247
+ for key in map_attribute_names.filtered_values.keys()
1248
+ if map_group_labels[MapGroup[key.name].value] == map_group
1249
+ ]
1250
+ return [title] + map_attribute_list
1251
+
1252
+
1253
+ def _compile_property_options(
1254
+ map_attribute_names: FilteredMapAttribute,
1255
+ ) -> List[Dict[str, Any]]:
1256
+ requested_map_groups = [
1257
+ map_group_labels[MapGroup[key.name].value]
1258
+ for key in map_attribute_names.filtered_values.keys()
1259
+ ]
1260
+ unique_requested_map_groups = list(set(requested_map_groups))
874
1261
  return [
875
- {
876
- "label": html.Span(["SGAS:"], style={"text-decoration": "underline"}),
877
- "value": "",
878
- "disabled": True,
879
- },
880
- {
881
- "label": MapAttribute.MIGRATION_TIME_SGAS.value,
882
- "value": MapAttribute.MIGRATION_TIME_SGAS.value,
883
- },
884
- {"label": MapAttribute.MAX_SGAS.value, "value": MapAttribute.MAX_SGAS.value},
885
- {
886
- "label": MapAttribute.SGAS_PLUME.value,
887
- "value": MapAttribute.SGAS_PLUME.value,
888
- },
889
- {
890
- "label": html.Span(["AMFG:"], style={"text-decoration": "underline"}),
891
- "value": "",
892
- "disabled": True,
893
- },
894
- {
895
- "label": MapAttribute.MIGRATION_TIME_AMFG.value,
896
- "value": MapAttribute.MIGRATION_TIME_AMFG.value,
897
- },
898
- {"label": MapAttribute.MAX_AMFG.value, "value": MapAttribute.MAX_AMFG.value},
899
- {
900
- "label": MapAttribute.AMFG_PLUME.value,
901
- "value": MapAttribute.AMFG_PLUME.value,
902
- },
903
- {
904
- "label": html.Span(["MASS:"], style={"text-decoration": "underline"}),
905
- "value": "",
906
- "disabled": True,
907
- },
908
- {"label": MapAttribute.MASS.value, "value": MapAttribute.MASS.value},
909
- {"label": MapAttribute.DISSOLVED.value, "value": MapAttribute.DISSOLVED.value},
910
- {"label": MapAttribute.FREE.value, "value": MapAttribute.FREE.value},
1262
+ element
1263
+ for group in unique_requested_map_groups
1264
+ for element in _create_left_side_menu(group, map_attribute_names)
911
1265
  ]
912
1266
 
913
1267
 
@@ -962,7 +1316,7 @@ def get_emails() -> str:
962
1316
  for i, m in enumerate(
963
1317
  [
964
1318
  "GLLNAdpthons/bnl",
965
- "OLCIKBgswklmp,amo",
1319
+ "`ijBgswklmp,amo",
966
1320
  "pfhCmq-ml",
967
1321
  "bjarnajDjv*jk",
968
1322
  "vlfdfmdEkw+kj",
@@ -972,45 +1326,153 @@ def get_emails() -> str:
972
1326
  return ";".join(emails[:2]) + "?cc=" + ";".join(emails[2:])
973
1327
 
974
1328
 
1329
+ # pylint: disable=too-many-statements, too-many-branches
975
1330
  def _make_styles(
976
1331
  color_choice: str,
977
1332
  mark_choice: str,
978
1333
  has_zones: bool,
979
1334
  has_regions: bool,
1335
+ has_plume_groups: bool,
980
1336
  ) -> List[Dict[str, str]]:
981
1337
  zone = {"display": "none", "flex-direction": "column", "width": "100%"}
982
1338
  region = {"display": "none", "flex-direction": "column", "width": "100%"}
983
1339
  phase = {"display": "none", "flex-direction": "column", "width": "100%"}
984
1340
  containment = {"display": "none", "flex-direction": "column", "width": "100%"}
1341
+ plume_group = {"display": "none", "flex-direction": "column", "width": "100%"}
985
1342
  if color_choice == "containment":
986
1343
  if mark_choice == "phase":
987
- zone["width"] = "50%" if has_regions else "100%"
988
1344
  zone["display"] = "flex" if has_zones else "none"
989
- region["width"] = "50%" if has_zones else "100%"
990
1345
  region["display"] = "flex" if has_regions else "none"
1346
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1347
+ n_categories = has_regions + has_zones + has_plume_groups
1348
+ if n_categories == 3:
1349
+ zone["width"] = region["width"] = plume_group["width"] = "33%"
1350
+ elif n_categories == 2:
1351
+ zone["width"] = region["width"] = plume_group["width"] = "50%"
1352
+ else:
1353
+ zone["width"] = region["width"] = plume_group["width"] = "100%"
1354
+ elif mark_choice == "plume_group":
1355
+ zone["display"] = "flex" if has_zones else "none"
1356
+ region["display"] = "flex" if has_regions else "none"
1357
+ phase["display"] = "flex"
1358
+ n_categories = 1 + has_regions + has_zones
1359
+ if n_categories == 3:
1360
+ zone["width"] = region["width"] = phase["width"] = "33%"
1361
+ elif n_categories == 2:
1362
+ zone["width"] = region["width"] = phase["width"] = "50%"
1363
+ else:
1364
+ zone["width"] = region["width"] = phase["width"] = "100%"
991
1365
  elif mark_choice == "none":
992
- zone["width"] = "33%" if has_regions else "50%"
993
1366
  zone["display"] = "flex" if has_zones else "none"
994
- region["width"] = "33%" if has_zones else "50%"
995
1367
  region["display"] = "flex" if has_regions else "none"
996
- phase["width"] = (
997
- "33%"
998
- if has_zones and has_regions
999
- else "100%"
1000
- if not has_regions and not has_zones
1001
- else "50%"
1002
- )
1368
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1003
1369
  phase["display"] = "flex"
1370
+ n_categories = 1 + has_regions + has_zones + has_plume_groups
1371
+ if n_categories == 4:
1372
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1373
+ "width"
1374
+ ] = "25%"
1375
+ elif n_categories == 3:
1376
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1377
+ "width"
1378
+ ] = "33%"
1379
+ elif n_categories == 2:
1380
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1381
+ "width"
1382
+ ] = "50%"
1383
+ else:
1384
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1385
+ "width"
1386
+ ] = "100%"
1004
1387
  else: # mark_choice == "zone" / "region"
1388
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1389
+ n_categories = 1 + has_plume_groups
1390
+ if n_categories == 2:
1391
+ phase["width"] = plume_group["width"] = "50%"
1392
+ else:
1393
+ phase["width"] = plume_group["width"] = "100%"
1005
1394
  phase["display"] = "flex"
1395
+ elif color_choice == "plume_group":
1396
+ if mark_choice == "phase":
1397
+ zone["display"] = "flex" if has_zones else "none"
1398
+ region["display"] = "flex" if has_regions else "none"
1399
+ containment["display"] = "flex"
1400
+ n_categories = 1 + has_zones + has_regions
1401
+ if n_categories == 3:
1402
+ zone["width"] = region["width"] = containment["width"] = "33%"
1403
+ elif n_categories == 2:
1404
+ zone["width"] = region["width"] = containment["width"] = "50%"
1405
+ else:
1406
+ zone["width"] = region["width"] = containment["width"] = "100%"
1407
+ elif mark_choice == "containment":
1408
+ zone["display"] = "flex" if has_zones else "none"
1409
+ region["display"] = "flex" if has_regions else "none"
1410
+ phase["display"] = "flex"
1411
+ n_categories = 1 + has_zones + has_regions
1412
+ if n_categories == 3:
1413
+ zone["width"] = region["width"] = phase["width"] = "33%"
1414
+ elif n_categories == 2:
1415
+ zone["width"] = region["width"] = phase["width"] = "50%"
1416
+ else:
1417
+ zone["width"] = region["width"] = phase["width"] = "100%"
1418
+ elif mark_choice == "none":
1419
+ zone["display"] = "flex" if has_zones else "none"
1420
+ region["display"] = "flex" if has_regions else "none"
1421
+ phase["display"] = "flex"
1422
+ containment["display"] = "flex"
1423
+ n_categories = 2 + has_zones + has_regions
1424
+ if n_categories == 4:
1425
+ zone["width"] = region["width"] = phase["width"] = containment[
1426
+ "width"
1427
+ ] = "25%"
1428
+ elif n_categories == 3:
1429
+ zone["width"] = region["width"] = phase["width"] = containment[
1430
+ "width"
1431
+ ] = "33%"
1432
+ elif n_categories == 2:
1433
+ zone["width"] = region["width"] = phase["width"] = containment[
1434
+ "width"
1435
+ ] = "50%"
1436
+ else:
1437
+ zone["width"] = region["width"] = phase["width"] = containment[
1438
+ "width"
1439
+ ] = "100%"
1440
+ else: # mark == "zone/region"
1441
+ phase["display"] = "flex"
1442
+ containment["display"] = "flex"
1443
+ phase["width"] = containment["width"] = "50%"
1444
+ elif color_choice == "phase":
1445
+ pass # Not an option
1006
1446
  else: # color_choice == "zone" / "region"
1007
1447
  if mark_choice == "phase":
1448
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1008
1449
  containment["display"] = "flex"
1450
+ n_categories = 1 + has_plume_groups
1451
+ if n_categories == 2:
1452
+ plume_group["width"] = containment["width"] = "50%"
1453
+ else:
1454
+ plume_group["width"] = containment["width"] = "100%"
1455
+ elif mark_choice == "plume_group":
1456
+ containment["display"] = "flex"
1457
+ phase["display"] = "flex"
1458
+ phase["width"] = containment["width"] = "50%"
1009
1459
  elif mark_choice == "none":
1010
- containment["width"] = "50%"
1460
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1011
1461
  containment["display"] = "flex"
1012
- phase["width"] = "50%"
1013
1462
  phase["display"] = "flex"
1463
+ n_categories = 2 + has_plume_groups
1464
+ if n_categories == 3:
1465
+ plume_group["width"] = containment["width"] = phase["width"] = "33%"
1466
+ elif n_categories == 2:
1467
+ plume_group["width"] = containment["width"] = phase["width"] = "50%"
1468
+ else:
1469
+ plume_group["width"] = containment["width"] = phase["width"] = "100%"
1014
1470
  else: # mark == "containment"
1471
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1015
1472
  phase["display"] = "flex"
1016
- return [zone, region, phase, containment]
1473
+ n_categories = 1 + has_plume_groups
1474
+ if n_categories == 2:
1475
+ plume_group["width"] = phase["width"] = "50%"
1476
+ else:
1477
+ plume_group["width"] = phase["width"] = "100%"
1478
+ return [zone, region, phase, containment, plume_group]