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
@@ -13,12 +13,22 @@ from webviz_subsurface._providers.ensemble_surface_provider.ensemble_surface_pro
13
13
  SurfaceStatistic,
14
14
  )
15
15
  from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import property_origin
16
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_info import (
17
+ StatisticsTabOption,
18
+ )
16
19
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
17
20
  Co2MassScale,
21
+ Co2VolumeScale,
22
+ FilteredMapAttribute,
18
23
  GraphSource,
19
24
  LayoutLabels,
20
25
  LayoutStyle,
21
26
  MapAttribute,
27
+ MapGroup,
28
+ MapThresholds,
29
+ MapType,
30
+ MenuOptions,
31
+ map_group_labels,
22
32
  )
23
33
 
24
34
 
@@ -33,6 +43,7 @@ class ViewSettings(SettingsGroupABC):
33
43
  FORMATION = "formation"
34
44
  ENSEMBLE = "ensemble"
35
45
  REALIZATION = "realization"
46
+ ALL_REAL = "all-realizations"
36
47
 
37
48
  PROPERTY = "property"
38
49
  STATISTIC = "statistic"
@@ -48,6 +59,8 @@ class ViewSettings(SettingsGroupABC):
48
59
  Y_MAX_GRAPH = "y-max-graph"
49
60
  Y_MIN_AUTO_GRAPH = "y-min-auto-graph"
50
61
  Y_MAX_AUTO_GRAPH = "y-max-auto-graph"
62
+ Y_LIM_OPTIONS = "y_limit_options"
63
+ REAL_OR_STAT = "realization-or-statistics"
51
64
  COLOR_BY = "color-by"
52
65
  MARK_BY = "mark-by"
53
66
  SORT_PLOT = "sort-plot"
@@ -59,120 +72,166 @@ class ViewSettings(SettingsGroupABC):
59
72
  PHASE = "phase"
60
73
  PHASE_MENU = "phase-menu"
61
74
  CONTAINMENT = "containment"
75
+ PLUME_GROUP = "plume-group"
62
76
  CONTAINMENT_MENU = "containment-menu"
77
+ PLUME_GROUP_MENU = "plume-group-menu"
78
+ DATE_OPTION = "date-option"
79
+ DATE_OPTION_COL = "date-option-column"
80
+ STATISTICS_TAB_OPTION = "statistics-tab-option"
81
+ BOX_SHOW_POINTS = "box-plot-points"
63
82
 
64
83
  PLUME_THRESHOLD = "plume-threshold"
65
84
  PLUME_SMOOTHING = "plume-smoothing"
66
85
 
67
- VISUALIZATION_THRESHOLD = "visualization-threshold"
68
86
  VISUALIZATION_UPDATE = "visualization-update"
87
+ VISUALIZATION_THRESHOLD_BUTTON = "visualization-threshold-button"
88
+ VISUALIZATION_THRESHOLD_DIALOG = "visualization-threshold-dialog"
69
89
  MASS_UNIT = "mass-unit"
90
+ MASS_UNIT_UPDATE = "mass-unit-update"
70
91
 
71
92
  FEEDBACK_BUTTON = "feedback-button"
72
93
  FEEDBACK = "feedback"
73
94
 
95
+ # pylint: disable=too-many-arguments
74
96
  def __init__(
75
97
  self,
76
98
  ensemble_paths: Dict[str, str],
99
+ realizations_per_ensemble: Dict[str, List[int]],
77
100
  ensemble_surface_providers: Dict[str, EnsembleSurfaceProvider],
78
101
  initial_surface: Optional[str],
79
- map_attribute_names: Dict[MapAttribute, str],
102
+ map_attribute_names: FilteredMapAttribute,
103
+ map_thresholds: MapThresholds,
80
104
  color_scale_names: List[str],
81
105
  well_names_dict: Dict[str, List[str]],
82
- menu_options: Dict[str, Dict[str, Dict[str, List[str]]]],
106
+ menu_options: Dict[str, Dict[GraphSource, MenuOptions]],
107
+ content: Dict[str, bool],
83
108
  ):
84
109
  super().__init__("Settings")
85
110
  self._ensemble_paths = ensemble_paths
111
+ self._realizations_per_ensemble = realizations_per_ensemble
86
112
  self._ensemble_surface_providers = ensemble_surface_providers
87
113
  self._map_attribute_names = map_attribute_names
114
+ self._thresholds = map_thresholds
115
+ self._threshold_ids = list(self._thresholds.standard_thresholds.keys())
88
116
  self._color_scale_names = color_scale_names
89
117
  self._initial_surface = initial_surface
90
118
  self._well_names_dict = well_names_dict
91
119
  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
- )
120
+ self._content = content
102
121
 
103
122
  def layout(self) -> List[Component]:
104
- return [
105
- DialogLayout(self._well_names_dict, list(self._ensemble_paths.keys())),
106
- OpenDialogButton(),
123
+ menu_layout = []
124
+ if self._content["maps"]:
125
+ menu_layout += [
126
+ DialogLayout(self._well_names_dict, list(self._ensemble_paths.keys())),
127
+ OpenDialogButton(),
128
+ ]
129
+ menu_layout.append(
107
130
  EnsembleSelectorLayout(
108
131
  self.register_component_unique_id(self.Ids.ENSEMBLE),
109
132
  self.register_component_unique_id(self.Ids.REALIZATION),
133
+ self.register_component_unique_id(self.Ids.ALL_REAL),
110
134
  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
- ),
135
+ )
136
+ )
137
+ if self._content["maps"]:
138
+ menu_layout += [
139
+ FilterSelectorLayout(
140
+ self.register_component_unique_id(self.Ids.FORMATION)
141
+ ),
142
+ VisualizationThresholdsLayout(
143
+ self._threshold_ids,
144
+ self._thresholds,
145
+ self.register_component_unique_id(self.Ids.VISUALIZATION_UPDATE),
146
+ ),
147
+ MapSelectorLayout(
148
+ self._color_scale_names,
149
+ self.register_component_unique_id(self.Ids.PROPERTY),
150
+ self.register_component_unique_id(self.Ids.STATISTIC),
151
+ self.register_component_unique_id(self.Ids.COLOR_SCALE),
152
+ self.register_component_unique_id(self.Ids.CM_MIN),
153
+ self.register_component_unique_id(self.Ids.CM_MAX),
154
+ self.register_component_unique_id(self.Ids.CM_MIN_AUTO),
155
+ self.register_component_unique_id(self.Ids.CM_MAX_AUTO),
156
+ self.register_component_unique_id(self.Ids.MASS_UNIT),
157
+ self.register_component_unique_id(self.Ids.MASS_UNIT_UPDATE),
158
+ self._map_attribute_names,
159
+ ),
160
+ ]
161
+ if self._content["any_table"]:
162
+ menu_layout.append(
163
+ GraphSelectorsLayout(
164
+ self.register_component_unique_id(self.Ids.GRAPH_SOURCE),
165
+ self.register_component_unique_id(self.Ids.CO2_SCALE),
166
+ [
167
+ self.register_component_unique_id(self.Ids.Y_MIN_GRAPH),
168
+ self.register_component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH),
169
+ ],
170
+ [
171
+ self.register_component_unique_id(self.Ids.Y_MAX_GRAPH),
172
+ self.register_component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH),
173
+ ],
174
+ {
175
+ k: self.register_component_unique_id(k)
176
+ for k in [
177
+ self.Ids.COLOR_BY,
178
+ self.Ids.MARK_BY,
179
+ self.Ids.SORT_PLOT,
180
+ self.Ids.ZONE,
181
+ self.Ids.ZONE_COL,
182
+ self.Ids.REGION,
183
+ self.Ids.REGION_COL,
184
+ self.Ids.ZONE_REGION,
185
+ self.Ids.PHASE,
186
+ self.Ids.PHASE_MENU,
187
+ self.Ids.CONTAINMENT,
188
+ self.Ids.CONTAINMENT_MENU,
189
+ self.Ids.PLUME_GROUP,
190
+ self.Ids.PLUME_GROUP_MENU,
191
+ self.Ids.REAL_OR_STAT,
192
+ self.Ids.Y_LIM_OPTIONS,
193
+ self.Ids.DATE_OPTION,
194
+ self.Ids.DATE_OPTION_COL,
195
+ self.Ids.STATISTICS_TAB_OPTION,
196
+ self.Ids.BOX_SHOW_POINTS,
197
+ ]
198
+ },
199
+ self._content,
200
+ )
201
+ )
202
+ if self._content["maps"]:
203
+ menu_layout.append(
204
+ ExperimentalFeaturesLayout(
205
+ self.register_component_unique_id(self.Ids.PLUME_THRESHOLD),
206
+ self.register_component_unique_id(self.Ids.PLUME_SMOOTHING),
207
+ ),
208
+ )
209
+ menu_layout += [
158
210
  FeedbackLayout(),
159
211
  FeedbackButton(),
160
212
  ]
213
+ return menu_layout
161
214
 
215
+ # pylint: disable=too-many-statements
162
216
  def set_callbacks(self) -> None:
217
+ # pylint: disable=unused-argument
163
218
  @callback(
164
219
  Output(
165
220
  self.component_unique_id(self.Ids.REALIZATION).to_string(), "options"
166
221
  ),
167
222
  Output(self.component_unique_id(self.Ids.REALIZATION).to_string(), "value"),
168
223
  Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
224
+ Input(self.component_unique_id(self.Ids.ALL_REAL).to_string(), "n_clicks"),
169
225
  )
170
- def set_realizations(ensemble: str) -> Tuple[List[Dict[str, Any]], List[int]]:
226
+ def set_realizations(
227
+ ensemble: str,
228
+ select_all: int,
229
+ ) -> Tuple[List[Dict[str, Any]], List[int]]:
171
230
  rlz = [
172
231
  {"value": r, "label": str(r)}
173
- for r in self._ensemble_surface_providers[ensemble].realizations()
232
+ for r in self._realizations_per_ensemble[ensemble]
174
233
  ]
175
- return rlz, [rlz[0]["value"]] # type: ignore
234
+ return rlz, self._realizations_per_ensemble[ensemble] # type: ignore
176
235
 
177
236
  @callback(
178
237
  Output(self.component_unique_id(self.Ids.FORMATION).to_string(), "options"),
@@ -193,10 +252,7 @@ class ViewSettings(SettingsGroupABC):
193
252
  if len(surfaces) == 0:
194
253
  warning = f"Surface not found for property: {prop}.\n"
195
254
  warning += f"Expected name: <formation>--{prop_name}"
196
- if MapAttribute(prop) not in [
197
- MapAttribute.MIGRATION_TIME_SGAS,
198
- MapAttribute.MIGRATION_TIME_AMFG,
199
- ]:
255
+ if MapType[MapAttribute(prop).name].value != "MIGRATION_TIME":
200
256
  warning += "--<date>"
201
257
  warnings.warn(warning + ".gri")
202
258
  # Formation names
@@ -215,181 +271,311 @@ class ViewSettings(SettingsGroupABC):
215
271
  )
216
272
  return formations, picked_formation
217
273
 
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
274
+ if self._content["maps"]:
234
275
 
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
276
+ @callback(
277
+ Output(
278
+ self.component_unique_id(self.Ids.STATISTIC).to_string(), "disabled"
279
+ ),
280
+ Input(
281
+ self.component_unique_id(self.Ids.REALIZATION).to_string(), "value"
282
+ ),
283
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
284
+ )
285
+ def toggle_statistics(realizations: List[int], attribute: str) -> bool:
286
+ if len(realizations) <= 1:
287
+ return True
288
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
289
+ return True
290
+ return False
245
291
 
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
- ]
292
+ @callback(
293
+ Output(
294
+ self.component_unique_id(self.Ids.CM_MIN).to_string(), "disabled"
295
+ ),
296
+ Output(
297
+ self.component_unique_id(self.Ids.CM_MAX).to_string(), "disabled"
298
+ ),
299
+ Input(
300
+ self.component_unique_id(self.Ids.CM_MIN_AUTO).to_string(), "value"
301
+ ),
302
+ Input(
303
+ self.component_unique_id(self.Ids.CM_MAX_AUTO).to_string(), "value"
304
+ ),
305
+ )
306
+ def set_color_range_data(
307
+ min_auto: List[str], max_auto: List[str]
308
+ ) -> Tuple[bool, bool]:
309
+ return len(min_auto) == 1, len(max_auto) == 1
258
310
 
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
311
+ @callback(
312
+ Output(
313
+ self.component_unique_id(self.Ids.MASS_UNIT).to_string(), "disabled"
314
+ ),
315
+ Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"),
316
+ )
317
+ def toggle_unit(attribute: str) -> bool:
318
+ if MapType[MapAttribute(attribute).name].value != "MASS":
319
+ return True
320
+ return False
277
321
 
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"
322
+ if self._content["any_table"]:
295
323
 
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"
324
+ @callback(
325
+ Output(
326
+ self.component_unique_id(self.Ids.Y_MIN_GRAPH).to_string(),
327
+ "disabled",
328
+ ),
329
+ Output(
330
+ self.component_unique_id(self.Ids.Y_MAX_GRAPH).to_string(),
331
+ "disabled",
332
+ ),
333
+ Input(
334
+ self.component_unique_id(self.Ids.Y_MIN_AUTO_GRAPH).to_string(),
335
+ "value",
336
+ ),
337
+ Input(
338
+ self.component_unique_id(self.Ids.Y_MAX_AUTO_GRAPH).to_string(),
339
+ "value",
340
+ ),
341
+ )
342
+ def set_y_min_max(
343
+ min_auto: List[str], max_auto: List[str]
344
+ ) -> Tuple[bool, bool]:
345
+ return len(min_auto) == 1, len(max_auto) == 1
314
346
 
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"
347
+ @callback(
348
+ Output(self.component_unique_id(self.Ids.PHASE).to_string(), "options"),
349
+ Output(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
350
+ Input(
351
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
352
+ ),
353
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
354
+ State(self.component_unique_id(self.Ids.PHASE).to_string(), "value"),
355
+ )
356
+ def set_phases(
357
+ source: GraphSource,
358
+ ensemble: str,
359
+ current_value: str,
360
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
361
+ if ensemble is not None:
362
+ phases = self._menu_options[ensemble][source]["phases"]
363
+ options = [
364
+ {"label": phase.title(), "value": phase} for phase in phases
365
+ ]
366
+ return options, no_update if current_value in phases else "total"
367
+ return [{"label": "Total", "value": "total"}], "total"
333
368
 
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
369
+ @callback(
370
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"),
371
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
372
+ Input(
373
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
374
+ ),
375
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
376
+ State(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
377
+ )
378
+ def set_zones(
379
+ source: GraphSource,
380
+ ensemble: str,
381
+ current_value: str,
382
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
383
+ if ensemble is not None:
384
+ zones = self._menu_options[ensemble][source]["zones"]
385
+ if len(zones) > 0:
386
+ options = [
387
+ {"label": zone.title(), "value": zone} for zone in zones
388
+ ]
389
+ return options, no_update if current_value in zones else "all"
390
+ return [{"label": "All", "value": "all"}], "all"
348
391
 
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
392
+ @callback(
393
+ Output(
394
+ self.component_unique_id(self.Ids.REGION).to_string(), "options"
395
+ ),
396
+ Output(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
397
+ Input(
398
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
399
+ ),
400
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
401
+ State(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
382
402
  )
383
- return mark_options, mark_choice, zone, region, phase, containment
403
+ def set_regions(
404
+ source: GraphSource,
405
+ ensemble: str,
406
+ current_value: str,
407
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
408
+ if ensemble is not None:
409
+ regions = self._menu_options[ensemble][source]["regions"]
410
+ if len(regions) > 0:
411
+ options = [
412
+ {"label": reg.title(), "value": reg} for reg in regions
413
+ ]
414
+ return options, no_update if current_value in regions else "all"
415
+ return [{"label": "All", "value": "all"}], "all"
384
416
 
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"
417
+ @callback(
418
+ Output(
419
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(),
420
+ "options",
421
+ ),
422
+ Output(
423
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(), "value"
424
+ ),
425
+ Input(
426
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
427
+ ),
428
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
429
+ State(
430
+ self.component_unique_id(self.Ids.PLUME_GROUP).to_string(), "value"
431
+ ),
432
+ )
433
+ def set_plume_groups(
434
+ source: GraphSource,
435
+ ensemble: str,
436
+ current_value: str,
437
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
438
+ if ensemble is not None:
439
+ plume_groups = self._menu_options[ensemble][source]["plume_groups"]
440
+ if len(plume_groups) > 0:
441
+ options = [
442
+ {"label": x.title(), "value": x} for x in plume_groups
443
+ ]
444
+ return (
445
+ options,
446
+ no_update if current_value in plume_groups else "all",
447
+ )
448
+ return [{"label": "All", "value": "all"}], "all"
449
+
450
+ @callback(
451
+ Output(
452
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(),
453
+ "options",
454
+ ),
455
+ Output(
456
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(), "value"
457
+ ),
458
+ Input(
459
+ self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"
460
+ ),
461
+ Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"),
462
+ State(
463
+ self.component_unique_id(self.Ids.DATE_OPTION).to_string(), "value"
464
+ ),
465
+ )
466
+ def set_date_option(
467
+ source: GraphSource,
468
+ ensemble: str,
469
+ current_value: str,
470
+ ) -> Tuple[List[Dict[str, str]], Union[Any, str]]:
471
+ if ensemble is not None:
472
+ dates = self._menu_options[ensemble][source]["dates"]
473
+ options = [{"label": date.title(), "value": date} for date in dates]
474
+ return options, no_update if current_value in dates else dates[-1]
475
+ return [], None
476
+
477
+ # pylint: disable=too-many-branches
478
+ @callback(
479
+ Output(
480
+ self.component_unique_id(self.Ids.MARK_BY).to_string(), "options"
481
+ ),
482
+ Output(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
483
+ Output(
484
+ self.component_unique_id(self.Ids.ZONE_COL).to_string(), "style"
485
+ ),
486
+ Output(
487
+ self.component_unique_id(self.Ids.REGION_COL).to_string(), "style"
488
+ ),
489
+ Output(
490
+ self.component_unique_id(self.Ids.PHASE_MENU).to_string(), "style"
491
+ ),
492
+ Output(
493
+ self.component_unique_id(self.Ids.CONTAINMENT_MENU).to_string(),
494
+ "style",
495
+ ),
496
+ Output(
497
+ self.component_unique_id(self.Ids.PLUME_GROUP_MENU).to_string(),
498
+ "style",
499
+ ),
500
+ Input(self.component_unique_id(self.Ids.COLOR_BY).to_string(), "value"),
501
+ Input(self.component_unique_id(self.Ids.MARK_BY).to_string(), "value"),
502
+ )
503
+ def organize_color_and_mark_menus(
504
+ color_choice: str,
505
+ mark_choice: str,
506
+ ) -> Tuple[List[Dict], str, Dict, Dict, Dict, Dict, Dict]:
507
+ mark_options = [
508
+ {"label": "None", "value": "none"},
509
+ ]
510
+ if color_choice in ["containment", "phase"]:
511
+ if color_choice == "containment":
512
+ mark_options.append({"label": "Phase", "value": "phase"})
513
+ elif color_choice == "phase":
514
+ mark_options.append(
515
+ {"label": "Containment", "value": "containment"}
516
+ )
517
+ if self._content["zones"]:
518
+ mark_options.append({"label": "Zone", "value": "zone"})
519
+ if self._content["regions"]:
520
+ mark_options.append({"label": "Region", "value": "region"})
521
+ if self._content["plume_groups"]:
522
+ mark_options.append(
523
+ {"label": "Plume group", "value": "plume_group"}
524
+ )
525
+ elif color_choice in ["zone", "region", "plume_group"]:
526
+ mark_options += [
527
+ {"label": "Containment", "value": "containment"},
528
+ {"label": "Phase", "value": "phase"},
529
+ ]
530
+ if (
531
+ color_choice in ["zone", "region"]
532
+ and self._content["plume_groups"]
533
+ ):
534
+ mark_options.append(
535
+ {"label": "Plume group", "value": "plume_group"}
536
+ )
537
+ if color_choice == "plume_group":
538
+ if self._content["zones"]:
539
+ mark_options.append({"label": "Zone", "value": "zone"})
540
+ if self._content["regions"]:
541
+ mark_options.append({"label": "Region", "value": "region"})
542
+ if mark_choice is None or mark_choice == color_choice:
543
+ if color_choice != "phase":
544
+ mark_choice = "phase"
545
+ else:
546
+ mark_choice = "containment"
547
+ if mark_choice in ["zone", "region"] and color_choice in [
548
+ "zone",
549
+ "region",
550
+ ]:
551
+ mark_choice = "phase"
552
+ zone, region, phase, containment, plume_group = _make_styles(
553
+ color_choice,
554
+ mark_choice,
555
+ self._content["zones"],
556
+ self._content["regions"],
557
+ self._content["plume_groups"],
558
+ )
559
+ return (
560
+ mark_options,
561
+ mark_choice,
562
+ zone,
563
+ region,
564
+ phase,
565
+ containment,
566
+ plume_group,
567
+ )
568
+
569
+ @callback(
570
+ Output(self.component_unique_id(self.Ids.ZONE).to_string(), "disabled"),
571
+ Output(
572
+ self.component_unique_id(self.Ids.REGION).to_string(), "disabled"
573
+ ),
574
+ Input(self.component_unique_id(self.Ids.ZONE).to_string(), "value"),
575
+ Input(self.component_unique_id(self.Ids.REGION).to_string(), "value"),
576
+ )
577
+ def disable_zone_or_region(zone: str, region: str) -> Tuple[bool, bool]:
578
+ return region != "all", zone != "all"
393
579
 
394
580
 
395
581
  class OpenDialogButton(html.Button):
@@ -484,6 +670,87 @@ class FilterSelectorLayout(wcc.Selectors):
484
670
  )
485
671
 
486
672
 
673
+ class OpenVisualizationThresholdsButton(html.Button):
674
+ def __init__(self) -> None:
675
+ super().__init__(
676
+ LayoutLabels.VISUALIZATION_THRESHOLDS,
677
+ id=ViewSettings.Ids.VISUALIZATION_THRESHOLD_BUTTON,
678
+ style=LayoutStyle.THRESHOLDS_BUTTON,
679
+ n_clicks=0,
680
+ )
681
+
682
+
683
+ class VisualizationThresholdsLayout(wcc.Dialog):
684
+ """Layout for the visualization thresholds dialog"""
685
+
686
+ def __init__(
687
+ self,
688
+ ids: List[str],
689
+ thresholds: MapThresholds,
690
+ visualization_update_id: str,
691
+ ) -> None:
692
+ standard_thresholds = thresholds.standard_thresholds
693
+
694
+ fields = [
695
+ html.Div(
696
+ "Here you can select a filter for the visualization of the map, "
697
+ "hiding values smaller than the selected minimum cutoff. "
698
+ "After changing the threshold value, press 'Update' to have the map reappear. "
699
+ "A value of -1 can be used to visualize zeros."
700
+ ),
701
+ html.Div("", style={"height": "30px"}),
702
+ html.Div(
703
+ [
704
+ html.Div("Property:", style={"width": "42%"}),
705
+ html.Div("Standard cutoff:", style={"width": "32%"}),
706
+ html.Div("Minimum cutoff:", style={"width": "25%"}),
707
+ ],
708
+ style={"display": "flex", "flex-direction": "row"},
709
+ ),
710
+ ]
711
+ fields += [
712
+ html.Div(
713
+ [
714
+ html.Div(id, style={"width": "42%"}),
715
+ html.Div(standard_thresholds[id], style={"width": "32%"}),
716
+ dcc.Input(
717
+ id=id,
718
+ type="number",
719
+ value=standard_thresholds[id],
720
+ step="0.0005",
721
+ style={"width": "25%"},
722
+ ),
723
+ ],
724
+ style={"display": "flex", "flex-direction": "row"},
725
+ )
726
+ for id in ids
727
+ ]
728
+ fields.append(html.Div(style={"height": "20px"}))
729
+ fields.append(
730
+ html.Div(
731
+ [
732
+ html.Div(style={"width": "80%"}),
733
+ html.Button(
734
+ "Update",
735
+ id=visualization_update_id,
736
+ style=LayoutStyle.VISUALIZATION_BUTTON,
737
+ n_clicks=0,
738
+ ),
739
+ ],
740
+ style={"display": "flex", "flex-direction": "row"},
741
+ )
742
+ )
743
+ super().__init__(
744
+ title=LayoutLabels.VISUALIZATION_THRESHOLDS,
745
+ id=ViewSettings.Ids.VISUALIZATION_THRESHOLD_DIALOG,
746
+ draggable=True,
747
+ open=False,
748
+ children=html.Div(
749
+ fields, style={"flex-direction": "column", "width": "500px"}
750
+ ),
751
+ )
752
+
753
+
487
754
  class MapSelectorLayout(wcc.Selectors):
488
755
  _CM_RANGE = {
489
756
  "display": "flex",
@@ -501,9 +768,9 @@ class MapSelectorLayout(wcc.Selectors):
501
768
  cm_max_id: str,
502
769
  cm_min_auto_id: str,
503
770
  cm_max_auto_id: str,
504
- visualization_threshold_id: str,
505
- visualization_update_id: str,
506
771
  mass_unit_id: str,
772
+ mass_unit_update_id: str,
773
+ map_attribute_names: FilteredMapAttribute,
507
774
  ):
508
775
  default_colormap = (
509
776
  "turbo (Seq)"
@@ -519,8 +786,8 @@ class MapSelectorLayout(wcc.Selectors):
519
786
  "Property",
520
787
  wcc.Dropdown(
521
788
  id=property_id,
522
- options=_compile_property_options(),
523
- value=MapAttribute.MIGRATION_TIME_SGAS.value,
789
+ options=_compile_property_options(map_attribute_names),
790
+ value=next(iter(map_attribute_names.filtered_values)).value,
524
791
  clearable=False,
525
792
  ),
526
793
  "Statistic",
@@ -568,32 +835,28 @@ class MapSelectorLayout(wcc.Selectors):
568
835
  ],
569
836
  style=self._CM_RANGE,
570
837
  ),
571
- "Visualization threshold",
838
+ "Mass unit (for mass maps)",
572
839
  html.Div(
573
840
  [
574
- dcc.Input(
575
- id=visualization_threshold_id,
576
- type="number",
577
- value=-1.0,
578
- style={"width": "70%"},
841
+ html.Div(
842
+ wcc.Dropdown(
843
+ id=mass_unit_id,
844
+ options=["kg", "tons", "M tons"],
845
+ value="tons",
846
+ clearable=False,
847
+ ),
848
+ style={"width": "50%"},
579
849
  ),
580
- html.Div(style={"width": "5%"}),
581
850
  html.Button(
582
- "Update",
583
- id=visualization_update_id,
851
+ "Update unit",
852
+ id=mass_unit_update_id,
584
853
  style=LayoutStyle.VISUALIZATION_BUTTON,
585
854
  n_clicks=0,
586
855
  ),
587
856
  ],
588
857
  style={"display": "flex"},
589
858
  ),
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
- ),
859
+ OpenVisualizationThresholdsButton(),
597
860
  ],
598
861
  )
599
862
  ],
@@ -606,47 +869,65 @@ class GraphSelectorsLayout(wcc.Selectors):
606
869
  "flexDirection": "row",
607
870
  }
608
871
 
872
+ # pylint: disable=too-many-locals
609
873
  def __init__(
610
874
  self,
611
875
  graph_source_id: str,
612
876
  co2_scale_id: str,
613
877
  y_min_ids: List[str],
614
878
  y_max_ids: List[str],
615
- containment_ids: List[str],
616
- has_zones: bool,
617
- has_regions: bool,
879
+ containment_ids: Dict[str, str], # ViewSettings.Ids
880
+ content: Dict[str, bool],
618
881
  ):
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"
626
- color_options = [{"label": "Containment (standard)", "value": "containment"}]
627
- mark_options = [{"label": "Phase", "value": "phase"}]
628
- if has_zones:
882
+ disp_zone = "flex" if content["zones"] else "none"
883
+ disp_region = "flex" if content["regions"] else "none"
884
+ disp_plume_group = "flex" if content["plume_groups"] else "none"
885
+ color_options = [
886
+ {"label": "Containment", "value": "containment"},
887
+ {"label": "Phase", "value": "phase"},
888
+ ]
889
+ mark_options = [
890
+ {"label": "Phase", "value": "phase"},
891
+ {"label": "Containment", "value": "containment"},
892
+ ]
893
+ if content["zones"]:
629
894
  color_options.append({"label": "Zone", "value": "zone"})
630
895
  mark_options.append({"label": "Zone", "value": "zone"})
631
- if has_regions:
896
+ if content["regions"]:
632
897
  color_options.append({"label": "Region", "value": "region"})
633
898
  mark_options.append({"label": "Region", "value": "region"})
899
+ if content["plume_groups"]:
900
+ color_options.append({"label": "Plume group", "value": "plume_group"})
901
+ mark_options.append({"label": "Plume group", "value": "plume_group"})
902
+ source_options = []
903
+ if content["mass"]:
904
+ source_options.append(GraphSource.CONTAINMENT_MASS)
905
+ if content["volume"]:
906
+ source_options.append(GraphSource.CONTAINMENT_ACTUAL_VOLUME)
907
+ if content["unsmry"]:
908
+ source_options.append(GraphSource.UNSMRY)
909
+ unit_options, init_unit = (
910
+ (list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS)
911
+ if source_options[0] == GraphSource.CONTAINMENT_ACTUAL_VOLUME
912
+ else (list(Co2MassScale), Co2MassScale.MTONS)
913
+ )
914
+ ids = ViewSettings.Ids
634
915
  super().__init__(
635
916
  label="Graph Settings",
636
- open_details=False,
917
+ open_details=not content["maps"],
637
918
  children=[
638
919
  "Source",
639
920
  wcc.Dropdown(
640
921
  id=graph_source_id,
641
- options=list(GraphSource),
642
- value=GraphSource.CONTAINMENT_MASS,
922
+ options=source_options,
923
+ value=source_options[0],
643
924
  clearable=False,
644
925
  ),
645
926
  "Unit",
646
927
  wcc.Dropdown(
647
928
  id=co2_scale_id,
648
- options=list(Co2MassScale),
649
- value=Co2MassScale.MTONS,
929
+ options=unit_options,
930
+ value=init_unit,
650
931
  clearable=False,
651
932
  ),
652
933
  html.Div(
@@ -657,7 +938,7 @@ class GraphSelectorsLayout(wcc.Selectors):
657
938
  wcc.Dropdown(
658
939
  options=color_options,
659
940
  value="containment",
660
- id=containment_ids[0],
941
+ id=containment_ids[ids.COLOR_BY],
661
942
  clearable=False,
662
943
  ),
663
944
  ],
@@ -672,7 +953,7 @@ class GraphSelectorsLayout(wcc.Selectors):
672
953
  wcc.Dropdown(
673
954
  options=mark_options,
674
955
  value="phase",
675
- id=containment_ids[1],
956
+ id=containment_ids[ids.MARK_BY],
676
957
  clearable=False,
677
958
  ),
678
959
  ],
@@ -683,7 +964,7 @@ class GraphSelectorsLayout(wcc.Selectors):
683
964
  ),
684
965
  ],
685
966
  style={
686
- "display": "flex", # disp,
967
+ "display": "flex",
687
968
  "flex-direction": "row",
688
969
  "margin-top": "10px",
689
970
  "margin-bottom": "1px",
@@ -695,7 +976,7 @@ class GraphSelectorsLayout(wcc.Selectors):
695
976
  dcc.RadioItems(
696
977
  options=["color", "marking"],
697
978
  value="color",
698
- id=containment_ids[2],
979
+ id=containment_ids[ids.SORT_PLOT],
699
980
  inline=True,
700
981
  ),
701
982
  ],
@@ -712,14 +993,26 @@ class GraphSelectorsLayout(wcc.Selectors):
712
993
  [
713
994
  "Zone",
714
995
  wcc.Dropdown(
996
+ options=[{"label": "All", "value": "all"}],
715
997
  value="all",
716
- id=containment_ids[3],
998
+ id=containment_ids[ids.ZONE],
717
999
  clearable=False,
718
1000
  ),
719
1001
  ],
720
- id=containment_ids[4],
1002
+ id=containment_ids[ids.ZONE_COL],
721
1003
  style={
722
- "width": "50%" if has_regions else "100%",
1004
+ "width": (
1005
+ "33%"
1006
+ if (content["regions"] and content["plume_groups"])
1007
+ else (
1008
+ "50%"
1009
+ if (
1010
+ content["regions"]
1011
+ or content["plume_groups"]
1012
+ )
1013
+ else "100%"
1014
+ )
1015
+ ),
723
1016
  "display": disp_zone,
724
1017
  "flex-direction": "column",
725
1018
  },
@@ -728,14 +1021,23 @@ class GraphSelectorsLayout(wcc.Selectors):
728
1021
  [
729
1022
  "Region",
730
1023
  wcc.Dropdown(
1024
+ options=[{"label": "All", "value": "all"}],
731
1025
  value="all",
732
- id=containment_ids[5],
1026
+ id=containment_ids[ids.REGION],
733
1027
  clearable=False,
734
1028
  ),
735
1029
  ],
736
- id=containment_ids[6],
1030
+ id=containment_ids[ids.REGION_COL],
737
1031
  style={
738
- "width": "50%" if has_zones else "100%",
1032
+ "width": (
1033
+ "33%"
1034
+ if (content["zones"] and content["plume_groups"])
1035
+ else (
1036
+ "50%"
1037
+ if (content["zones"] or content["plume_groups"])
1038
+ else "100%"
1039
+ )
1040
+ ),
739
1041
  "display": disp_region,
740
1042
  "flex-direction": "column",
741
1043
  },
@@ -744,12 +1046,13 @@ class GraphSelectorsLayout(wcc.Selectors):
744
1046
  [
745
1047
  "Phase",
746
1048
  wcc.Dropdown(
1049
+ options=[{"label": "Total", "value": "total"}],
747
1050
  value="total",
748
1051
  clearable=False,
749
- id=containment_ids[8],
1052
+ id=containment_ids[ids.PHASE],
750
1053
  ),
751
1054
  ],
752
- id=containment_ids[9],
1055
+ id=containment_ids[ids.PHASE_MENU],
753
1056
  style={"display": "none"},
754
1057
  ),
755
1058
  html.Div(
@@ -757,50 +1060,161 @@ class GraphSelectorsLayout(wcc.Selectors):
757
1060
  "Containment",
758
1061
  wcc.Dropdown(
759
1062
  options=[
760
- {"label": "Total", "value": "total"},
1063
+ {"label": "All areas", "value": "total"},
761
1064
  {"label": "Contained", "value": "contained"},
762
1065
  {"label": "Outside", "value": "outside"},
763
1066
  {"label": "Hazardous", "value": "hazardous"},
764
1067
  ],
765
1068
  value="total",
766
1069
  clearable=False,
767
- id=containment_ids[10],
1070
+ id=containment_ids[ids.CONTAINMENT],
768
1071
  ),
769
1072
  ],
770
- id=containment_ids[11],
1073
+ id=containment_ids[ids.CONTAINMENT_MENU],
771
1074
  style={"display": "none"},
772
1075
  ),
1076
+ html.Div(
1077
+ [
1078
+ "Plume",
1079
+ wcc.Dropdown(
1080
+ options=[{"label": "All", "value": "all"}],
1081
+ value="all",
1082
+ id=containment_ids[ids.PLUME_GROUP],
1083
+ clearable=False,
1084
+ ),
1085
+ ],
1086
+ id=containment_ids[ids.PLUME_GROUP_MENU],
1087
+ style={
1088
+ "width": (
1089
+ "33%"
1090
+ if (content["zones"] and content["regions"])
1091
+ else (
1092
+ "50%"
1093
+ if (content["zones"] or content["regions"])
1094
+ else "100%"
1095
+ )
1096
+ ),
1097
+ "display": disp_plume_group,
1098
+ "flex-direction": "column",
1099
+ },
1100
+ ),
773
1101
  ],
774
- id=containment_ids[7],
1102
+ id=containment_ids[ids.ZONE_REGION],
775
1103
  style={"display": "flex"},
776
1104
  ),
777
1105
  html.Div(
778
- "Fix y-limits in third plot:",
1106
+ "Time plot options:",
779
1107
  style={"margin-top": "10px"},
780
1108
  ),
781
- "Minimum",
782
1109
  html.Div(
783
1110
  [
784
- dcc.Input(id=y_min_ids[0], type="number"),
785
- dcc.Checklist(
786
- ["Auto"],
787
- ["Auto"],
788
- id=y_min_ids[1],
1111
+ dcc.RadioItems(
1112
+ options=[
1113
+ {"label": "Realizations", "value": "real"},
1114
+ {"label": "Mean/P10/P90", "value": "stat"},
1115
+ ],
1116
+ value="real",
1117
+ id=containment_ids[ids.REAL_OR_STAT],
1118
+ inline=True,
1119
+ ),
1120
+ ],
1121
+ style={
1122
+ "display": "flex",
1123
+ "flex-direction": "row",
1124
+ },
1125
+ ),
1126
+ html.Div(
1127
+ "State at date:",
1128
+ style={"margin-top": "8"},
1129
+ ),
1130
+ html.Div(
1131
+ [
1132
+ wcc.Dropdown(
1133
+ id=containment_ids[ids.DATE_OPTION],
1134
+ clearable=False,
789
1135
  ),
790
1136
  ],
791
- style=self._CM_RANGE,
1137
+ id=containment_ids[ids.DATE_OPTION_COL],
1138
+ style={
1139
+ "width": "100%",
1140
+ "flex-direction": "row",
1141
+ },
792
1142
  ),
793
- "Maximum",
794
1143
  html.Div(
795
1144
  [
796
- dcc.Input(id=y_max_ids[0], type="number"),
797
- dcc.Checklist(
798
- ["Auto"],
799
- ["Auto"],
800
- id=y_max_ids[1],
1145
+ "Fix minimum y-value",
1146
+ html.Div(
1147
+ [
1148
+ dcc.Input(id=y_min_ids[0], type="number"),
1149
+ dcc.Checklist(
1150
+ ["Auto"],
1151
+ ["Auto"],
1152
+ id=y_min_ids[1],
1153
+ ),
1154
+ ],
1155
+ style=self._CM_RANGE,
1156
+ ),
1157
+ "Fix maximum y-value",
1158
+ html.Div(
1159
+ [
1160
+ dcc.Input(id=y_max_ids[0], type="number"),
1161
+ dcc.Checklist(
1162
+ ["Auto"],
1163
+ ["Auto"],
1164
+ id=y_max_ids[1],
1165
+ ),
1166
+ ],
1167
+ style=self._CM_RANGE,
801
1168
  ),
802
1169
  ],
803
- style=self._CM_RANGE,
1170
+ style={
1171
+ "display": "flex",
1172
+ "flex-direction": "column",
1173
+ },
1174
+ id=containment_ids[ids.Y_LIM_OPTIONS],
1175
+ ),
1176
+ html.Div(
1177
+ "Statistics tab:",
1178
+ style={"margin-top": "10px"},
1179
+ ),
1180
+ html.Div(
1181
+ [
1182
+ dcc.RadioItems(
1183
+ options=[
1184
+ {
1185
+ "label": "Probability plot",
1186
+ "value": StatisticsTabOption.PROBABILITY_PLOT,
1187
+ },
1188
+ {
1189
+ "label": "Box plot",
1190
+ "value": StatisticsTabOption.BOX_PLOT,
1191
+ },
1192
+ ],
1193
+ value=StatisticsTabOption.PROBABILITY_PLOT,
1194
+ id=containment_ids[ids.STATISTICS_TAB_OPTION],
1195
+ ),
1196
+ ],
1197
+ ),
1198
+ html.Div(
1199
+ "Box plot points to show:",
1200
+ style={"margin-top": "10px"},
1201
+ ),
1202
+ html.Div(
1203
+ [
1204
+ dcc.RadioItems(
1205
+ options=[
1206
+ {"label": "All", "value": "all_points"},
1207
+ {"label": "Outliers", "value": "only_outliers"},
1208
+ ],
1209
+ value="only_outliers",
1210
+ id=containment_ids[ids.BOX_SHOW_POINTS],
1211
+ inline=True,
1212
+ ),
1213
+ ],
1214
+ style={
1215
+ "display": "flex",
1216
+ "flex-direction": "row",
1217
+ },
804
1218
  ),
805
1219
  ],
806
1220
  )
@@ -848,7 +1262,13 @@ class ExperimentalFeaturesLayout(wcc.Selectors):
848
1262
 
849
1263
 
850
1264
  class EnsembleSelectorLayout(wcc.Selectors):
851
- def __init__(self, ensemble_id: str, realization_id: str, ensembles: List[str]):
1265
+ def __init__(
1266
+ self,
1267
+ ensemble_id: str,
1268
+ realization_id: str,
1269
+ all_real_id: str,
1270
+ ensembles: List[str],
1271
+ ):
852
1272
  super().__init__(
853
1273
  label="Ensemble",
854
1274
  open_details=True,
@@ -860,7 +1280,23 @@ class EnsembleSelectorLayout(wcc.Selectors):
860
1280
  value=ensembles[0],
861
1281
  clearable=False,
862
1282
  ),
863
- "Realization",
1283
+ html.Div(
1284
+ [
1285
+ html.Div("Realization", style={"width": "50%"}),
1286
+ html.Button(
1287
+ "Select all",
1288
+ id=all_real_id,
1289
+ style=LayoutStyle.ALL_REAL_BUTTON,
1290
+ n_clicks=0,
1291
+ ),
1292
+ ],
1293
+ style={
1294
+ "display": "flex",
1295
+ "flex-direction": "row",
1296
+ "margin-top": "3px",
1297
+ "margin-bottom": "3px",
1298
+ },
1299
+ ),
864
1300
  wcc.SelectWithLabel(
865
1301
  id=realization_id,
866
1302
  value=[],
@@ -870,44 +1306,34 @@ class EnsembleSelectorLayout(wcc.Selectors):
870
1306
  )
871
1307
 
872
1308
 
873
- def _compile_property_options() -> List[Dict[str, Any]]:
1309
+ def _create_left_side_menu(
1310
+ map_group: str, map_attribute_names: FilteredMapAttribute
1311
+ ) -> List:
1312
+ title = {
1313
+ "label": html.Span([f"{map_group}:"], style={"text-decoration": "underline"}),
1314
+ "value": "",
1315
+ "disabled": True,
1316
+ }
1317
+ map_attribute_list = [
1318
+ {"label": MapAttribute[key.name].value, "value": MapAttribute[key.name].value}
1319
+ for key in map_attribute_names.filtered_values.keys()
1320
+ if map_group_labels[MapGroup[key.name].value] == map_group
1321
+ ]
1322
+ return [title] + map_attribute_list
1323
+
1324
+
1325
+ def _compile_property_options(
1326
+ map_attribute_names: FilteredMapAttribute,
1327
+ ) -> List[Dict[str, Any]]:
1328
+ requested_map_groups = [
1329
+ map_group_labels[MapGroup[key.name].value]
1330
+ for key in map_attribute_names.filtered_values.keys()
1331
+ ]
1332
+ unique_requested_map_groups = list(set(requested_map_groups))
874
1333
  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},
1334
+ element
1335
+ for group in unique_requested_map_groups
1336
+ for element in _create_left_side_menu(group, map_attribute_names)
911
1337
  ]
912
1338
 
913
1339
 
@@ -962,7 +1388,7 @@ def get_emails() -> str:
962
1388
  for i, m in enumerate(
963
1389
  [
964
1390
  "GLLNAdpthons/bnl",
965
- "OLCIKBgswklmp,amo",
1391
+ "`ijBgswklmp,amo",
966
1392
  "pfhCmq-ml",
967
1393
  "bjarnajDjv*jk",
968
1394
  "vlfdfmdEkw+kj",
@@ -972,45 +1398,204 @@ def get_emails() -> str:
972
1398
  return ";".join(emails[:2]) + "?cc=" + ";".join(emails[2:])
973
1399
 
974
1400
 
1401
+ # pylint: disable=too-many-statements, too-many-branches
975
1402
  def _make_styles(
976
1403
  color_choice: str,
977
1404
  mark_choice: str,
978
1405
  has_zones: bool,
979
1406
  has_regions: bool,
1407
+ has_plume_groups: bool,
980
1408
  ) -> List[Dict[str, str]]:
981
1409
  zone = {"display": "none", "flex-direction": "column", "width": "100%"}
982
1410
  region = {"display": "none", "flex-direction": "column", "width": "100%"}
983
1411
  phase = {"display": "none", "flex-direction": "column", "width": "100%"}
984
1412
  containment = {"display": "none", "flex-direction": "column", "width": "100%"}
1413
+ plume_group = {"display": "none", "flex-direction": "column", "width": "100%"}
985
1414
  if color_choice == "containment":
986
1415
  if mark_choice == "phase":
987
- zone["width"] = "50%" if has_regions else "100%"
988
1416
  zone["display"] = "flex" if has_zones else "none"
989
- region["width"] = "50%" if has_zones else "100%"
990
1417
  region["display"] = "flex" if has_regions else "none"
1418
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1419
+ n_categories = has_regions + has_zones + has_plume_groups
1420
+ if n_categories == 3:
1421
+ zone["width"] = region["width"] = plume_group["width"] = "33%"
1422
+ elif n_categories == 2:
1423
+ zone["width"] = region["width"] = plume_group["width"] = "50%"
1424
+ else:
1425
+ zone["width"] = region["width"] = plume_group["width"] = "100%"
1426
+ elif mark_choice == "plume_group":
1427
+ zone["display"] = "flex" if has_zones else "none"
1428
+ region["display"] = "flex" if has_regions else "none"
1429
+ phase["display"] = "flex"
1430
+ n_categories = 1 + has_regions + has_zones
1431
+ if n_categories == 3:
1432
+ zone["width"] = region["width"] = phase["width"] = "33%"
1433
+ elif n_categories == 2:
1434
+ zone["width"] = region["width"] = phase["width"] = "50%"
1435
+ else:
1436
+ zone["width"] = region["width"] = phase["width"] = "100%"
991
1437
  elif mark_choice == "none":
992
- zone["width"] = "33%" if has_regions else "50%"
993
1438
  zone["display"] = "flex" if has_zones else "none"
994
- region["width"] = "33%" if has_zones else "50%"
995
1439
  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
- )
1440
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1003
1441
  phase["display"] = "flex"
1442
+ n_categories = 1 + has_regions + has_zones + has_plume_groups
1443
+ if n_categories == 4:
1444
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1445
+ "width"
1446
+ ] = "25%"
1447
+ elif n_categories == 3:
1448
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1449
+ "width"
1450
+ ] = "33%"
1451
+ elif n_categories == 2:
1452
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1453
+ "width"
1454
+ ] = "50%"
1455
+ else:
1456
+ phase["width"] = zone["width"] = region["width"] = plume_group[
1457
+ "width"
1458
+ ] = "100%"
1459
+ else: # mark_choice == "zone" / "region"
1460
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1461
+ n_categories = 1 + has_plume_groups
1462
+ if n_categories == 2:
1463
+ phase["width"] = plume_group["width"] = "50%"
1464
+ else:
1465
+ phase["width"] = plume_group["width"] = "100%"
1466
+ phase["display"] = "flex"
1467
+ elif color_choice == "phase":
1468
+ if mark_choice == "containment":
1469
+ zone["display"] = "flex" if has_zones else "none"
1470
+ region["display"] = "flex" if has_regions else "none"
1471
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1472
+ n_categories = has_regions + has_zones + has_plume_groups
1473
+ if n_categories == 3:
1474
+ zone["width"] = region["width"] = plume_group["width"] = "33%"
1475
+ elif n_categories == 2:
1476
+ zone["width"] = region["width"] = plume_group["width"] = "50%"
1477
+ else:
1478
+ zone["width"] = region["width"] = plume_group["width"] = "100%"
1479
+ elif mark_choice == "plume_group":
1480
+ zone["display"] = "flex" if has_zones else "none"
1481
+ region["display"] = "flex" if has_regions else "none"
1482
+ containment["display"] = "flex"
1483
+ n_categories = 1 + has_regions + has_zones
1484
+ if n_categories == 3:
1485
+ zone["width"] = region["width"] = containment["width"] = "33%"
1486
+ elif n_categories == 2:
1487
+ zone["width"] = region["width"] = containment["width"] = "50%"
1488
+ else:
1489
+ zone["width"] = region["width"] = containment["width"] = "100%"
1490
+ elif mark_choice == "none":
1491
+ zone["display"] = "flex" if has_zones else "none"
1492
+ region["display"] = "flex" if has_regions else "none"
1493
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1494
+ containment["display"] = "flex"
1495
+ n_categories = 1 + has_regions + has_zones + has_plume_groups
1496
+ if n_categories == 4:
1497
+ containment["width"] = zone["width"] = region["width"] = plume_group[
1498
+ "width"
1499
+ ] = "25%"
1500
+ elif n_categories == 3:
1501
+ containment["width"] = zone["width"] = region["width"] = plume_group[
1502
+ "width"
1503
+ ] = "33%"
1504
+ elif n_categories == 2:
1505
+ containment["width"] = zone["width"] = region["width"] = plume_group[
1506
+ "width"
1507
+ ] = "50%"
1508
+ else:
1509
+ containment["width"] = zone["width"] = region["width"] = plume_group[
1510
+ "width"
1511
+ ] = "100%"
1004
1512
  else: # mark_choice == "zone" / "region"
1513
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1514
+ n_categories = 1 + has_plume_groups
1515
+ if n_categories == 2:
1516
+ containment["width"] = plume_group["width"] = "50%"
1517
+ else:
1518
+ containment["width"] = plume_group["width"] = "100%"
1519
+ containment["display"] = "flex"
1520
+ elif color_choice == "plume_group":
1521
+ if mark_choice == "phase":
1522
+ zone["display"] = "flex" if has_zones else "none"
1523
+ region["display"] = "flex" if has_regions else "none"
1524
+ containment["display"] = "flex"
1525
+ n_categories = 1 + has_zones + has_regions
1526
+ if n_categories == 3:
1527
+ zone["width"] = region["width"] = containment["width"] = "33%"
1528
+ elif n_categories == 2:
1529
+ zone["width"] = region["width"] = containment["width"] = "50%"
1530
+ else:
1531
+ zone["width"] = region["width"] = containment["width"] = "100%"
1532
+ elif mark_choice == "containment":
1533
+ zone["display"] = "flex" if has_zones else "none"
1534
+ region["display"] = "flex" if has_regions else "none"
1005
1535
  phase["display"] = "flex"
1536
+ n_categories = 1 + has_zones + has_regions
1537
+ if n_categories == 3:
1538
+ zone["width"] = region["width"] = phase["width"] = "33%"
1539
+ elif n_categories == 2:
1540
+ zone["width"] = region["width"] = phase["width"] = "50%"
1541
+ else:
1542
+ zone["width"] = region["width"] = phase["width"] = "100%"
1543
+ elif mark_choice == "none":
1544
+ zone["display"] = "flex" if has_zones else "none"
1545
+ region["display"] = "flex" if has_regions else "none"
1546
+ phase["display"] = "flex"
1547
+ containment["display"] = "flex"
1548
+ n_categories = 2 + has_zones + has_regions
1549
+ if n_categories == 4:
1550
+ zone["width"] = region["width"] = phase["width"] = containment[
1551
+ "width"
1552
+ ] = "25%"
1553
+ elif n_categories == 3:
1554
+ zone["width"] = region["width"] = phase["width"] = containment[
1555
+ "width"
1556
+ ] = "33%"
1557
+ elif n_categories == 2:
1558
+ zone["width"] = region["width"] = phase["width"] = containment[
1559
+ "width"
1560
+ ] = "50%"
1561
+ else:
1562
+ zone["width"] = region["width"] = phase["width"] = containment[
1563
+ "width"
1564
+ ] = "100%"
1565
+ else: # mark == "zone/region"
1566
+ phase["display"] = "flex"
1567
+ containment["display"] = "flex"
1568
+ phase["width"] = containment["width"] = "50%"
1006
1569
  else: # color_choice == "zone" / "region"
1007
1570
  if mark_choice == "phase":
1571
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1008
1572
  containment["display"] = "flex"
1573
+ n_categories = 1 + has_plume_groups
1574
+ if n_categories == 2:
1575
+ plume_group["width"] = containment["width"] = "50%"
1576
+ else:
1577
+ plume_group["width"] = containment["width"] = "100%"
1578
+ elif mark_choice == "plume_group":
1579
+ containment["display"] = "flex"
1580
+ phase["display"] = "flex"
1581
+ phase["width"] = containment["width"] = "50%"
1009
1582
  elif mark_choice == "none":
1010
- containment["width"] = "50%"
1583
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1011
1584
  containment["display"] = "flex"
1012
- phase["width"] = "50%"
1013
1585
  phase["display"] = "flex"
1586
+ n_categories = 2 + has_plume_groups
1587
+ if n_categories == 3:
1588
+ plume_group["width"] = containment["width"] = phase["width"] = "33%"
1589
+ elif n_categories == 2:
1590
+ plume_group["width"] = containment["width"] = phase["width"] = "50%"
1591
+ else:
1592
+ plume_group["width"] = containment["width"] = phase["width"] = "100%"
1014
1593
  else: # mark == "containment"
1594
+ plume_group["display"] = "flex" if has_plume_groups else "none"
1015
1595
  phase["display"] = "flex"
1016
- return [zone, region, phase, containment]
1596
+ n_categories = 1 + has_plume_groups
1597
+ if n_categories == 2:
1598
+ plume_group["width"] = phase["width"] = "50%"
1599
+ else:
1600
+ plume_group["width"] = phase["width"] = "100%"
1601
+ return [zone, region, phase, containment, plume_group]