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
@@ -2,21 +2,24 @@ import logging
2
2
  from typing import Any, Dict, List, Optional, Tuple, Union
3
3
 
4
4
  import plotly.graph_objects as go
5
- from dash import Dash, Input, Output, State, callback, html, no_update
5
+ from dash import Dash, Input, Output, Patch, State, callback, ctx, html, no_update
6
6
  from dash.exceptions import PreventUpdate
7
7
  from webviz_config import WebvizPluginABC, WebvizSettings
8
8
  from webviz_config.utils import StrEnum, callback_typecheck
9
9
 
10
10
  from webviz_subsurface._providers import FaultPolygonsServer, SurfaceImageServer
11
+ from webviz_subsurface._providers.ensemble_polygon_provider import PolygonServer
11
12
  from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import (
12
13
  SurfaceData,
13
14
  create_map_annotations,
14
15
  create_map_layers,
15
16
  create_map_viewports,
16
17
  derive_surface_address,
18
+ extract_legendonly,
17
19
  generate_containment_figures,
18
20
  generate_unsmry_figures,
19
21
  get_plume_polygon,
22
+ make_plot_ids,
20
23
  process_containment_info,
21
24
  process_summed_mass,
22
25
  process_visualization_info,
@@ -24,22 +27,28 @@ from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import (
24
27
  readable_name,
25
28
  set_plot_ids,
26
29
  )
27
- from webviz_subsurface.plugins._co2_leakage._utilities.fault_polygons import (
30
+ from webviz_subsurface.plugins._co2_leakage._utilities.fault_polygons_handler import (
28
31
  FaultPolygonsHandler,
29
32
  )
30
33
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
34
+ BoundarySettings,
31
35
  Co2MassScale,
32
36
  Co2VolumeScale,
33
37
  GraphSource,
34
38
  MapAttribute,
39
+ MapThresholds,
40
+ MapType,
35
41
  )
36
42
  from webviz_subsurface.plugins._co2_leakage._utilities.initialization import (
43
+ init_containment_data_providers,
44
+ init_dictionary_of_content,
37
45
  init_map_attribute_names,
38
46
  init_menu_options,
47
+ init_polygon_provider_handlers,
48
+ init_realizations,
39
49
  init_surface_providers,
40
- init_table_provider,
50
+ init_unsmry_data_providers,
41
51
  init_well_pick_provider,
42
- process_files,
43
52
  )
44
53
  from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
45
54
  MainView,
@@ -48,7 +57,9 @@ from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
48
57
  from webviz_subsurface.plugins._co2_leakage.views.mainview.settings import ViewSettings
49
58
 
50
59
  from . import _error
60
+ from ._types import LegendData
51
61
  from ._utilities.color_tables import co2leakage_color_tables
62
+ from ._utilities.containment_info import StatisticsTabOption
52
63
 
53
64
  LOGGER = logging.getLogger(__name__)
54
65
  TABLES_PATH = "share/results/tables"
@@ -56,31 +67,89 @@ TABLES_PATH = "share/results/tables"
56
67
 
57
68
  # pylint: disable=too-many-instance-attributes
58
69
  class CO2Leakage(WebvizPluginABC):
59
- """
60
- Plugin for analyzing CO2 leakage potential across multiple realizations in an FMU
61
- ensemble
70
+ """Plugin for analyzing CO2 leakage potential across multiple realizations in an
71
+ FMU ensemble
72
+
73
+ ---
62
74
 
63
75
  * **`ensembles`:** Which ensembles in `shared_settings` to visualize.
64
- * **`file_containment_boundary`:** Path to a polygon representing the containment area
65
- * **`file_hazardous_boundary`:** Path to a polygon representing the hazardous area
66
76
  * **`well_pick_file`:** Path to a file containing well picks
67
- * **`plume_mass_relpath`:** Path to a table of co2 containment data (amount of
68
- CO2 outside/inside a boundary), for co2 mass. Relative to each realization.
69
- * **`plume_actual_volume_relpath`:** Path to a table of co2 containment data (amount
70
- of CO2 outside/inside a boundary), for co2 volume of type "actual". Relative to each
71
- realization.
72
- * **`unsmry_relpath`:** Relative path to a csv version of a unified summary file
73
- * **`fault_polygon_attribute`:** Polygons with this attribute are used as fault
74
- polygons
75
- * **`map_attribute_names`:** Dictionary for overriding the default mapping between
77
+ * **`plume_mass_relpath`:** Path to a table of co2 _containment data_ for co2 mass
78
+ * **`plume_actual_volume_relpath`:** Path to a table of co2 _containment data_ for co2
79
+ volume of type "actual"
80
+ * **`unsmry_relpath`:** Path to a csv/arrow version of a Cirrus/Eclipse unified summary
81
+ file
82
+ * **`fault_polygon_attribute`:** Polygons with this attribute are used as fault polygons
83
+ * **`map_attribute_names`:** Key-value pairs for overriding the default mapping between
76
84
  attributes visualized by the plugin and attributes names used by
77
85
  EnsembleSurfaceProvider
78
86
  * **`initial_surface`:** Name of the surface/formation to show when the plugin is
79
- launched. If no name is provided, the first alphabetical surface is shown.
80
- * **`map_surface_names_to_well_pick_names`:** Optional mapping between surface map
81
- names and surface names used in the well pick file
82
- * **`map_surface_names_to_fault_polygons`:** Optional mapping between surface map
83
- names and surface names used by the fault polygons
87
+ launched. If not provided, the first alphabetical surface is shown.
88
+ * **`map_surface_names_to_well_pick_names`:** Mapping between surface map names and
89
+ surface names used in the well pick file
90
+ * **`map_surface_names_to_fault_polygons`:** Mapping between surface map names and
91
+ surface names used by the fault polygons
92
+ * **`boundary_settings`:** Settings for polygons representing the containment and
93
+ hazardous areas
94
+ ---
95
+
96
+ This plugin is tightly linked to the FMU CCS post-process available in the ccs-scripts
97
+ repository. If the ccs-scripts workflow is executed without alterations, there is no
98
+ need for any configuration except the `ensembles` keyword. If any steps of the
99
+ post-process are skipped, the plugin will exclude the respective results and
100
+ functionality.
101
+
102
+ Even though the workflow is standardized, it is sometimes necessary to override
103
+ specific settings. For all path settings, these are interpreted relative to the
104
+ realization root, but can also be absolute paths. Their default values are
105
+ - `well_pick_file`: `share/results/well_picks.csv`
106
+ - `plume_mass_relpath`: `share/results/tables/plume_mass.csv`
107
+ - `plume_actual_volume_relpath`: No value
108
+ - `unsmry_relpath`: No value
109
+
110
+ Fault polygons are assumed to be stored in the `share/results/polygons` folder, and
111
+ with a `dl_extracted_faultlines` attribute. This attribute can be overridden with
112
+ `fault_polygon_attribute`, but the relative path to the polygons cannot.
113
+
114
+ `map_attribute_names` can be used to override how attributes are mapped to specific
115
+ features of the plugin. For instance, the attribute `migration_time_sgas` is mapped to
116
+ the Migration Time (SGAS) visualization, but this can be overridden by specifying
117
+ ```
118
+ map_attribute_names:
119
+ MIGRATION_TIME_SGAS: mig_time
120
+ ```
121
+
122
+ The following keys are allowed: `MIGRATION_TIME_SGAS, MIGRATION_TIME_AMFG,
123
+ MIGRATION_TIME_XMF2, MAX_SGAS, MAX_AMFG, MAX_XMF2, MAX_SGSTRAND, MAX_SGTRH, MASS,
124
+ DISSOLVED, FREE, FREE_GAS, TRAPPED_GAS`
125
+
126
+ Well pick files and fault polygons might name surfaces differently than the ones
127
+ generated by the ccs-scripts workflow. The options
128
+ `map_surface_names_to_well_pick_names` and `map_surface_names_to_fault_polygons` can
129
+ be used to specify this mapping explicitly. For instance, if the well pick file
130
+ contains a surface called `top_sgas`, but the ccs-scripts workflow generated a
131
+ surface called `top_sgas_2`, this can be specified with
132
+ ```
133
+ map_surface_names_to_well_pick_names:
134
+ top_sgas_2: top_sgas
135
+ ```
136
+ Similar for `map_surface_names_to_fault_polygons`.
137
+
138
+ `boundary_settings` is the final override option, and it can be used to specify
139
+ polygons representing the containment and hazardous areas. By default, the polygons are
140
+ expected to be named:
141
+ - `share/results/polygons/containment--boundary.csv`
142
+ - `share/results/polygons/hazarduous--boundary.csv`
143
+
144
+ This corresponds to the following input:
145
+ ```
146
+ boundary_settings:
147
+ polygon_file_pattern: share/results/polygons/*.csv
148
+ attribute: boundary
149
+ hazardous_name: hazardous
150
+ containment_name: containment
151
+ ```
152
+ All four settings are optional, and if not specified, the default values are used.
84
153
  """
85
154
 
86
155
  class Ids(StrEnum):
@@ -94,17 +163,16 @@ class CO2Leakage(WebvizPluginABC):
94
163
  app: Dash,
95
164
  webviz_settings: WebvizSettings,
96
165
  ensembles: List[str],
97
- file_containment_boundary: Optional[str] = None,
98
- file_hazardous_boundary: Optional[str] = None,
99
166
  well_pick_file: Optional[str] = None,
100
167
  plume_mass_relpath: str = TABLES_PATH + "/plume_mass.csv",
101
- plume_actual_volume_relpath: str = TABLES_PATH + "/plume_actual_volume.csv",
168
+ plume_actual_volume_relpath: Optional[str] = None,
102
169
  unsmry_relpath: Optional[str] = None,
103
170
  fault_polygon_attribute: str = "dl_extracted_faultlines",
104
171
  initial_surface: Optional[str] = None,
105
172
  map_attribute_names: Optional[Dict[str, str]] = None,
106
173
  map_surface_names_to_well_pick_names: Optional[Dict[str, str]] = None,
107
174
  map_surface_names_to_fault_polygons: Optional[Dict[str, str]] = None,
175
+ boundary_settings: Optional[BoundarySettings] = None,
108
176
  ):
109
177
  super().__init__()
110
178
  self._error_message = ""
@@ -115,21 +183,14 @@ class CO2Leakage(WebvizPluginABC):
115
183
  ]
116
184
  for ensemble_name in ensembles
117
185
  }
118
- (
119
- containment_poly_dict,
120
- hazardous_poly_dict,
121
- well_pick_dict,
122
- ) = process_files(
123
- file_containment_boundary,
124
- file_hazardous_boundary,
125
- well_pick_file,
126
- ensemble_paths,
127
- )
128
- self._polygon_files = [containment_poly_dict, hazardous_poly_dict]
186
+ self._realizations_per_ensemble = init_realizations(ensemble_paths)
129
187
  self._surface_server = SurfaceImageServer.instance(app)
130
188
  self._polygons_server = FaultPolygonsServer.instance(app)
131
-
132
- self._map_attribute_names = init_map_attribute_names(map_attribute_names)
189
+ self._map_attribute_names = init_map_attribute_names(
190
+ webviz_settings, ensembles, map_attribute_names
191
+ )
192
+ self._map_thresholds = MapThresholds(self._map_attribute_names)
193
+ self._threshold_ids = list(self._map_thresholds.standard_thresholds.keys())
133
194
  # Surfaces
134
195
  self._ensemble_surface_providers = init_surface_providers(
135
196
  webviz_settings, ensembles
@@ -145,25 +206,27 @@ class CO2Leakage(WebvizPluginABC):
145
206
  for ens in ensembles
146
207
  }
147
208
  # CO2 containment
148
- self._co2_table_providers = init_table_provider(
209
+ self._co2_table_providers = init_containment_data_providers(
149
210
  ensemble_paths,
150
211
  plume_mass_relpath,
151
212
  )
152
- self._co2_actual_volume_table_providers = init_table_provider(
213
+ self._co2_actual_volume_table_providers = init_containment_data_providers(
153
214
  ensemble_paths,
154
215
  plume_actual_volume_relpath,
155
216
  )
156
- self._unsmry_providers = (
157
- init_table_provider(
158
- ensemble_paths,
159
- unsmry_relpath,
160
- )
161
- if unsmry_relpath is not None
162
- else None
217
+ self._unsmry_providers = init_unsmry_data_providers(
218
+ ensemble_paths,
219
+ unsmry_relpath,
220
+ )
221
+ self._polygon_handlers = init_polygon_provider_handlers(
222
+ PolygonServer.instance(app),
223
+ ensemble_paths,
224
+ boundary_settings,
163
225
  )
164
226
  # Well picks
165
227
  self._well_pick_provider = init_well_pick_provider(
166
- well_pick_dict,
228
+ ensemble_paths,
229
+ well_pick_file,
167
230
  map_surface_names_to_well_pick_names,
168
231
  )
169
232
  # Phase (in case of residual trapping), zone and region options
@@ -171,8 +234,11 @@ class CO2Leakage(WebvizPluginABC):
171
234
  ensemble_paths,
172
235
  self._co2_table_providers,
173
236
  self._co2_actual_volume_table_providers,
174
- plume_mass_relpath,
175
- plume_actual_volume_relpath,
237
+ self._unsmry_providers,
238
+ )
239
+ self._content = init_dictionary_of_content(
240
+ self._menu_options,
241
+ len(self._map_attribute_names.mapping) > 0,
176
242
  )
177
243
  except Exception as err:
178
244
  self._error_message = f"Plugin initialization failed: {err}"
@@ -180,29 +246,36 @@ class CO2Leakage(WebvizPluginABC):
180
246
 
181
247
  self._summed_co2: Dict[str, Any] = {}
182
248
  self._visualization_info = {
183
- "threshold": -1.0,
249
+ "thresholds": self._map_thresholds.standard_thresholds,
184
250
  "n_clicks": 0,
185
251
  "change": False,
186
- "unit": "kg",
252
+ "unit": "tons",
187
253
  }
188
254
  self._color_tables = co2leakage_color_tables()
189
- self._well_pick_names = {
190
- ens: prov.well_names() if prov is not None else []
191
- for ens, prov in self._well_pick_provider.items()
255
+ self._well_pick_names: Dict[str, List[str]] = {
256
+ ens: (
257
+ self._well_pick_provider[ens].well_names
258
+ if ens in self._well_pick_provider
259
+ else []
260
+ )
261
+ for ens in ensembles
192
262
  }
193
263
  self.add_shared_settings_group(
194
264
  ViewSettings(
195
265
  ensemble_paths,
266
+ self._realizations_per_ensemble,
196
267
  self._ensemble_surface_providers,
197
268
  initial_surface,
198
269
  self._map_attribute_names,
270
+ self._map_thresholds,
199
271
  [c["name"] for c in self._color_tables], # type: ignore
200
272
  self._well_pick_names,
201
273
  self._menu_options,
274
+ self._content,
202
275
  ),
203
276
  self.Ids.MAIN_SETTINGS,
204
277
  )
205
- self.add_view(MainView(self._color_tables), self.Ids.MAIN_VIEW)
278
+ self.add_view(MainView(self._color_tables, self._content), self.Ids.MAIN_VIEW)
206
279
 
207
280
  @property
208
281
  def layout(self) -> html.Div:
@@ -225,184 +298,119 @@ class CO2Leakage(WebvizPluginABC):
225
298
 
226
299
  def _ensemble_dates(self, ens: str) -> List[str]:
227
300
  surface_provider = self._ensemble_surface_providers[ens]
228
- att_name = self._map_attribute_names[MapAttribute.MAX_SGAS]
229
- dates = surface_provider.surface_dates_for_attribute(att_name)
301
+ date_map_attribute = next(
302
+ (
303
+ k
304
+ for k in self._map_attribute_names.filtered_values
305
+ if MapType[k.name].value != "MIGRATION_TIME"
306
+ ),
307
+ None,
308
+ )
309
+ att_name = (
310
+ self._map_attribute_names[date_map_attribute]
311
+ if date_map_attribute is not None
312
+ else None
313
+ )
314
+ dates = (
315
+ None
316
+ if att_name is None
317
+ else surface_provider.surface_dates_for_attribute(att_name)
318
+ )
230
319
  if dates is None:
231
320
  raise ValueError(f"Failed to fetch dates for attribute '{att_name}'")
232
321
  return dates
233
322
 
234
323
  # Might want to do some refactoring if this gets too big
235
- # pylint: disable=too-many-statements
236
324
  def _set_callbacks(self) -> None:
237
- # Cannot avoid many arguments since all the parameters are needed
238
- # to determine what to plot
239
- # pylint: disable=too-many-arguments
325
+ if self._content["any_table"]:
326
+ self._add_graph_callback()
327
+ self._add_legend_change_callback()
328
+ self._add_set_unit_list_callback()
329
+ self._add_time_plot_visibility_callback()
330
+
331
+ if self._content["maps"]:
332
+ self._add_set_dates_callback()
333
+ self._add_date_slider_visibility_callback()
334
+ self._add_set_well_options_callback()
335
+ self._add_create_map_callback()
336
+ self._add_options_dialog_callback()
337
+ self._add_thresholds_dialog_callback()
338
+
339
+ self._add_feedback_dialog_callback()
340
+
341
+ if self._content["maps"] and self._content["any_table"]:
342
+ self._add_resize_plot_callback()
343
+
344
+ def _add_resize_plot_callback(self) -> None:
240
345
  @callback(
241
- Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
242
- Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
243
- Output(
244
- self._view_component(MapViewElement.Ids.TIME_PLOT_ONE_REAL), "figure"
245
- ),
346
+ Output(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
347
+ Output(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
348
+ Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "style"),
349
+ Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "style"),
350
+ Output(self._view_component(MapViewElement.Ids.STATISTICS_PLOT), "style"),
246
351
  Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
352
+ Input(self._view_component(MapViewElement.Ids.SIZE_SLIDER), "value"),
353
+ State(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
354
+ State(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
247
355
  Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
248
- Input(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
249
- Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
250
- Input(self._settings_component(ViewSettings.Ids.Y_MIN_AUTO_GRAPH), "value"),
251
- Input(self._settings_component(ViewSettings.Ids.Y_MIN_GRAPH), "value"),
252
- Input(self._settings_component(ViewSettings.Ids.Y_MAX_AUTO_GRAPH), "value"),
253
- Input(self._settings_component(ViewSettings.Ids.Y_MAX_GRAPH), "value"),
254
- Input(self._settings_component(ViewSettings.Ids.ZONE), "value"),
255
- Input(self._settings_component(ViewSettings.Ids.REGION), "value"),
256
- Input(self._settings_component(ViewSettings.Ids.PHASE), "value"),
257
- Input(self._settings_component(ViewSettings.Ids.CONTAINMENT), "value"),
258
- Input(self._settings_component(ViewSettings.Ids.COLOR_BY), "value"),
259
- Input(self._settings_component(ViewSettings.Ids.MARK_BY), "value"),
260
- Input(self._settings_component(ViewSettings.Ids.SORT_PLOT), "value"),
261
356
  )
262
- @callback_typecheck
263
- def update_graphs(
357
+ def resize_plots(
264
358
  ensemble: str,
359
+ slider_value: float,
360
+ top_style: Dict,
361
+ bottom_style: Dict,
265
362
  source: GraphSource,
266
- co2_scale: Union[Co2MassScale, Co2VolumeScale],
267
- realizations: List[int],
268
- y_min_auto: List[str],
269
- y_min_val: Optional[float],
270
- y_max_auto: List[str],
271
- y_max_val: Optional[float],
272
- zone: Optional[str],
273
- region: Optional[str],
274
- phase: str,
275
- containment: str,
276
- color_choice: str,
277
- mark_choice: Optional[str],
278
- sorting: str,
279
- ) -> Tuple[Dict, go.Figure, go.Figure, go.Figure]:
280
- # pylint: disable=too-many-locals
281
- figs = [no_update] * 3
282
- cont_info = process_containment_info(
283
- zone,
284
- region,
285
- phase,
286
- containment,
287
- color_choice,
288
- mark_choice,
289
- sorting,
290
- self._menu_options[ensemble][source],
291
- )
292
- if source in [
293
- GraphSource.CONTAINMENT_MASS,
294
- GraphSource.CONTAINMENT_ACTUAL_VOLUME,
295
- ]:
296
- y_limits = [
297
- y_min_val if len(y_min_auto) == 0 else None,
298
- y_max_val if len(y_max_auto) == 0 else None,
299
- ]
300
- if (
301
- source == GraphSource.CONTAINMENT_MASS
302
- and ensemble in self._co2_table_providers
303
- ):
304
- figs[: len(figs)] = generate_containment_figures(
305
- self._co2_table_providers[ensemble],
306
- co2_scale,
307
- realizations[0],
308
- y_limits,
309
- cont_info,
310
- )
311
- elif (
312
- source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
313
- and ensemble in self._co2_actual_volume_table_providers
314
- ):
315
- figs[: len(figs)] = generate_containment_figures(
316
- self._co2_actual_volume_table_providers[ensemble],
317
- co2_scale,
318
- realizations[0],
319
- y_limits,
320
- cont_info,
321
- )
322
- set_plot_ids(figs, source, co2_scale, cont_info, realizations)
323
- elif source == GraphSource.UNSMRY:
324
- if self._unsmry_providers is not None:
325
- if ensemble in self._unsmry_providers:
326
- u_figs = generate_unsmry_figures(
327
- self._unsmry_providers[ensemble],
328
- co2_scale,
329
- self._co2_table_providers[ensemble],
330
- )
331
- figs = list(u_figs)
332
- else:
333
- LOGGER.warning(
334
- """UNSMRY file has not been specified as input.
335
- Please use unsmry_relpath in the configuration."""
336
- )
337
- return figs # type: ignore
363
+ ) -> List[Dict]:
364
+ bottom_style["height"] = f"{slider_value}vh"
365
+ top_style["height"] = f"{80 - slider_value}vh"
338
366
 
339
- @callback(
340
- Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
341
- Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
342
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
343
- )
344
- def set_dates(ensemble: str) -> Tuple[Dict[int, Dict[str, Any]], Optional[int]]:
345
- if ensemble is None:
346
- return {}, None
347
- # Dates
348
- date_list = self._ensemble_dates(ensemble)
349
- dates = {
350
- i: {
351
- "label": f"{d[:4]}",
352
- "style": {"writingMode": "vertical-rl"},
353
- }
354
- for i, d in enumerate(date_list)
355
- }
356
- return dates, max(dates.keys())
367
+ styles = [{"height": f"{slider_value * 0.9 - 4}vh", "width": "90%"}] * 3
368
+ if source == GraphSource.UNSMRY and self._unsmry_providers is None:
369
+ styles = [{"display": "none"}] * 3
370
+ elif (
371
+ source == GraphSource.CONTAINMENT_MASS
372
+ and ensemble not in self._co2_table_providers
373
+ ):
374
+ styles = [{"display": "none"}] * 3
375
+ elif (
376
+ source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
377
+ and ensemble not in self._co2_actual_volume_table_providers
378
+ ):
379
+ styles = [{"display": "none"}] * 3
380
+
381
+ return [top_style, bottom_style] + styles
357
382
 
383
+ def _add_feedback_dialog_callback(self) -> None:
358
384
  @callback(
359
- Output(self._view_component(MapViewElement.Ids.DATE_WRAPPER), "style"),
360
- Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
385
+ Output(ViewSettings.Ids.FEEDBACK, "open"),
386
+ Input(ViewSettings.Ids.FEEDBACK_BUTTON, "n_clicks"),
361
387
  )
362
- def toggle_date_slider(attribute: str) -> Dict[str, str]:
363
- if MapAttribute(attribute) in [
364
- MapAttribute.MIGRATION_TIME_SGAS,
365
- MapAttribute.MIGRATION_TIME_AMFG,
366
- ]:
367
- return {"display": "none"}
368
- return {}
388
+ def open_close_feedback(_n_clicks: Optional[int]) -> bool:
389
+ if _n_clicks is not None:
390
+ return _n_clicks > 0
391
+ raise PreventUpdate
369
392
 
393
+ def _add_thresholds_dialog_callback(self) -> None:
370
394
  @callback(
371
- Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "options"),
372
- Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
373
- Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
395
+ Output(ViewSettings.Ids.VISUALIZATION_THRESHOLD_DIALOG, "open"),
396
+ Input(ViewSettings.Ids.VISUALIZATION_THRESHOLD_BUTTON, "n_clicks"),
374
397
  )
375
- def make_unit_list(
376
- attribute: str,
377
- ) -> Union[Tuple[List[Any], Co2MassScale], Tuple[List[Any], Co2VolumeScale],]:
378
- if attribute == GraphSource.CONTAINMENT_ACTUAL_VOLUME:
379
- return list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS
380
- return list(Co2MassScale), Co2MassScale.MTONS
398
+ def open_close_thresholds(_n_clicks: Optional[int]) -> bool:
399
+ if _n_clicks is not None:
400
+ return _n_clicks > 0
401
+ raise PreventUpdate
381
402
 
403
+ def _add_options_dialog_callback(self) -> None:
382
404
  @callback(
383
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "options"),
384
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
385
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "style"),
386
- Output(ViewSettings.Ids.WELL_FILTER_HEADER, "style"),
387
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
405
+ Output(ViewSettings.Ids.OPTIONS_DIALOG, "open"),
406
+ Input(ViewSettings.Ids.OPTIONS_DIALOG_BUTTON, "n_clicks"),
388
407
  )
389
- def set_well_options(
390
- ensemble: str,
391
- ) -> Tuple[List[Any], List[str], Dict[Any, Any], Dict[Any, Any]]:
392
- return (
393
- [{"label": i, "value": i} for i in self._well_pick_names[ensemble]],
394
- self._well_pick_names[ensemble],
395
- {
396
- "display": "block" if self._well_pick_names[ensemble] else "none",
397
- "height": f"{len(self._well_pick_names[ensemble]) * 22}px",
398
- },
399
- {
400
- "flex": 3,
401
- "minWidth": "20px",
402
- "display": "block" if self._well_pick_names[ensemble] else "none",
403
- },
404
- )
408
+ def open_close_options_dialog(_n_clicks: Optional[int]) -> bool:
409
+ if _n_clicks is not None:
410
+ return _n_clicks > 0
411
+ raise PreventUpdate
405
412
 
413
+ def _add_create_map_callback(self) -> None:
406
414
  # Cannot avoid many arguments and/or locals since all layers of the DeckGL map
407
415
  # need to be updated simultaneously
408
416
  # pylint: disable=too-many-arguments,too-many-locals
@@ -410,31 +418,70 @@ class CO2Leakage(WebvizPluginABC):
410
418
  Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "layers"),
411
419
  Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "children"),
412
420
  Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"),
413
- Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
414
- Input(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
415
- Input(self._settings_component(ViewSettings.Ids.FORMATION), "value"),
416
- Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
417
- Input(self._settings_component(ViewSettings.Ids.STATISTIC), "value"),
418
- Input(self._settings_component(ViewSettings.Ids.COLOR_SCALE), "value"),
419
- Input(self._settings_component(ViewSettings.Ids.CM_MIN_AUTO), "value"),
420
- Input(self._settings_component(ViewSettings.Ids.CM_MIN), "value"),
421
- Input(self._settings_component(ViewSettings.Ids.CM_MAX_AUTO), "value"),
422
- Input(self._settings_component(ViewSettings.Ids.CM_MAX), "value"),
423
- Input(self._settings_component(ViewSettings.Ids.PLUME_THRESHOLD), "value"),
424
- Input(self._settings_component(ViewSettings.Ids.PLUME_SMOOTHING), "value"),
425
- Input(
426
- self._settings_component(ViewSettings.Ids.VISUALIZATION_THRESHOLD),
427
- "value",
428
- ),
429
- Input(
430
- self._settings_component(ViewSettings.Ids.VISUALIZATION_UPDATE),
431
- "n_clicks",
432
- ),
433
- Input(self._settings_component(ViewSettings.Ids.MASS_UNIT), "value"),
434
- Input(ViewSettings.Ids.OPTIONS_DIALOG_OPTIONS, "value"),
435
- Input(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
436
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
437
- State(self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"),
421
+ inputs={
422
+ "attribute": Input(
423
+ self._settings_component(ViewSettings.Ids.PROPERTY), "value"
424
+ ),
425
+ "date": Input(
426
+ self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"
427
+ ),
428
+ "formation": Input(
429
+ self._settings_component(ViewSettings.Ids.FORMATION), "value"
430
+ ),
431
+ "realization": Input(
432
+ self._settings_component(ViewSettings.Ids.REALIZATION), "value"
433
+ ),
434
+ "statistic": Input(
435
+ self._settings_component(ViewSettings.Ids.STATISTIC), "value"
436
+ ),
437
+ "color_map_name": Input(
438
+ self._settings_component(ViewSettings.Ids.COLOR_SCALE), "value"
439
+ ),
440
+ "cm_min_auto": Input(
441
+ self._settings_component(ViewSettings.Ids.CM_MIN_AUTO), "value"
442
+ ),
443
+ "cm_min_val": Input(
444
+ self._settings_component(ViewSettings.Ids.CM_MIN), "value"
445
+ ),
446
+ "cm_max_auto": Input(
447
+ self._settings_component(ViewSettings.Ids.CM_MAX_AUTO), "value"
448
+ ),
449
+ "cm_max_val": Input(
450
+ self._settings_component(ViewSettings.Ids.CM_MAX), "value"
451
+ ),
452
+ "plume_threshold": Input(
453
+ self._settings_component(ViewSettings.Ids.PLUME_THRESHOLD),
454
+ "value",
455
+ ),
456
+ "plume_smoothing": Input(
457
+ self._settings_component(ViewSettings.Ids.PLUME_SMOOTHING),
458
+ "value",
459
+ ),
460
+ "visualization_update": Input(
461
+ self._settings_component(ViewSettings.Ids.VISUALIZATION_UPDATE),
462
+ "n_clicks",
463
+ ),
464
+ "mass_unit": Input(
465
+ self._settings_component(ViewSettings.Ids.MASS_UNIT), "value"
466
+ ),
467
+ "mass_unit_update": Input(
468
+ self._settings_component(ViewSettings.Ids.MASS_UNIT_UPDATE),
469
+ "n_clicks",
470
+ ),
471
+ "options_dialog_options": Input(
472
+ ViewSettings.Ids.OPTIONS_DIALOG_OPTIONS, "value"
473
+ ),
474
+ "selected_wells": Input(
475
+ ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"
476
+ ),
477
+ "ensemble": Input(
478
+ self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"
479
+ ),
480
+ "current_views": State(
481
+ self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"
482
+ ),
483
+ "thresholds": [Input(id, "value") for id in self._threshold_ids],
484
+ },
438
485
  )
439
486
  def update_map_attribute(
440
487
  attribute: MapAttribute,
@@ -449,32 +496,39 @@ class CO2Leakage(WebvizPluginABC):
449
496
  cm_max_val: Optional[float],
450
497
  plume_threshold: Optional[float],
451
498
  plume_smoothing: Optional[float],
452
- visualization_threshold: Optional[float],
453
499
  visualization_update: int,
454
500
  mass_unit: str,
501
+ mass_unit_update: int,
455
502
  options_dialog_options: List[int],
456
503
  selected_wells: List[str],
457
504
  ensemble: str,
458
505
  current_views: List[Any],
459
- ) -> Tuple[List[Dict[Any, Any]], List[Any], Dict[Any, Any]]:
506
+ thresholds: List[float],
507
+ ) -> Tuple[List[Dict[Any, Any]], Optional[List[Any]], Dict[Any, Any]]:
460
508
  # Unable to clear cache (when needed) without the protected member
461
509
  # pylint: disable=protected-access
510
+ current_thresholds = dict(zip(self._threshold_ids, thresholds))
511
+ assert visualization_update >= 0 # Need the input to trigger callback
512
+ assert mass_unit_update >= 0 # These are just to silence pylint
462
513
  self._visualization_info = process_visualization_info(
463
- visualization_update,
464
- visualization_threshold,
514
+ attribute,
515
+ current_thresholds,
465
516
  mass_unit,
466
517
  self._visualization_info,
467
518
  self._surface_server._image_cache,
468
519
  )
469
520
  if self._visualization_info["change"]:
470
- return [], no_update, no_update
521
+ return [], None, no_update
471
522
  attribute = MapAttribute(attribute)
472
523
  if len(realization) == 0 or ensemble is None:
473
524
  raise PreventUpdate
474
- datestr = self._ensemble_dates(ensemble)[date]
525
+ if isinstance(date, int):
526
+ datestr = self._ensemble_dates(ensemble)[date]
527
+ elif date is None:
528
+ datestr = None
475
529
  # Contour data
476
530
  contour_data = None
477
- if attribute in (MapAttribute.SGAS_PLUME, MapAttribute.AMFG_PLUME):
531
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
478
532
  contour_data = {
479
533
  "property": property_origin(attribute, self._map_attribute_names),
480
534
  "threshold": plume_threshold,
@@ -525,18 +579,23 @@ class CO2Leakage(WebvizPluginABC):
525
579
  contour_data,
526
580
  )
527
581
  # Create layers and view bounds
582
+ fault_polygon_url = self._fault_polygon_handlers[
583
+ ensemble
584
+ ].extract_fault_polygon_url(formation, realization)
585
+ hazardous_polygon_url = self._polygon_handlers[
586
+ ensemble
587
+ ].extract_hazardous_poly_url(realization)
588
+ containment_polygon_url = self._polygon_handlers[
589
+ ensemble
590
+ ].extract_containment_poly_url(realization)
528
591
  layers = create_map_layers(
592
+ realizations=realization,
529
593
  formation=formation,
530
594
  surface_data=surf_data,
531
- fault_polygon_url=(
532
- self._fault_polygon_handlers[ensemble].extract_fault_polygon_url(
533
- formation,
534
- realization,
535
- )
536
- ),
537
- file_containment_boundary=self._polygon_files[0][ensemble],
538
- file_hazardous_boundary=self._polygon_files[1][ensemble],
539
- well_pick_provider=self._well_pick_provider[ensemble],
595
+ fault_polygon_url=fault_polygon_url,
596
+ containment_bounds_url=containment_polygon_url,
597
+ haz_bounds_url=hazardous_polygon_url,
598
+ well_pick_provider=self._well_pick_provider.get(ensemble, None),
540
599
  plume_extent_data=plume_polygon,
541
600
  options_dialog_options=options_dialog_options,
542
601
  selected_wells=selected_wells,
@@ -551,60 +610,285 @@ class CO2Leakage(WebvizPluginABC):
551
610
  viewports = no_update if current_views else create_map_viewports()
552
611
  return layers, annotations, viewports
553
612
 
613
+ def _add_set_well_options_callback(self) -> None:
554
614
  @callback(
555
- Output(ViewSettings.Ids.OPTIONS_DIALOG, "open"),
556
- Input(ViewSettings.Ids.OPTIONS_DIALOG_BUTTON, "n_clicks"),
615
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "options"),
616
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
617
+ Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "style"),
618
+ Output(ViewSettings.Ids.WELL_FILTER_HEADER, "style"),
619
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
557
620
  )
558
- def open_close_options_dialog(_n_clicks: Optional[int]) -> bool:
559
- if _n_clicks is not None:
560
- return _n_clicks > 0
561
- raise PreventUpdate
621
+ def set_well_options(
622
+ ensemble: str,
623
+ ) -> Tuple[List[Any], List[str], Dict[Any, Any], Dict[Any, Any]]:
624
+ return (
625
+ [{"label": i, "value": i} for i in self._well_pick_names[ensemble]],
626
+ self._well_pick_names[ensemble],
627
+ {
628
+ "display": ("block" if self._well_pick_names[ensemble] else "none"),
629
+ "height": f"{len(self._well_pick_names[ensemble]) * 22}px",
630
+ },
631
+ {
632
+ "flex": 3,
633
+ "minWidth": "20px",
634
+ "display": ("block" if self._well_pick_names[ensemble] else "none"),
635
+ },
636
+ )
562
637
 
638
+ def _add_date_slider_visibility_callback(self) -> None:
563
639
  @callback(
564
- Output(ViewSettings.Ids.FEEDBACK, "open"),
565
- Input(ViewSettings.Ids.FEEDBACK_BUTTON, "n_clicks"),
640
+ Output(self._view_component(MapViewElement.Ids.DATE_WRAPPER), "style"),
641
+ Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
566
642
  )
567
- def open_close_feedback(_n_clicks: Optional[int]) -> bool:
568
- if _n_clicks is not None:
569
- return _n_clicks > 0
570
- raise PreventUpdate
643
+ def toggle_date_slider(attribute: str) -> Dict[str, str]:
644
+ if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
645
+ return {"display": "none"}
646
+ return {}
571
647
 
648
+ def _add_set_dates_callback(self) -> None:
572
649
  @callback(
573
- Output(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
574
- Output(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
575
- Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "style"),
576
- Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "style"),
650
+ Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
651
+ Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
652
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
653
+ )
654
+ def set_dates(
655
+ ensemble: str,
656
+ ) -> Tuple[Dict[int, Dict[str, Any]], Optional[int]]:
657
+ if ensemble is None:
658
+ return {}, None
659
+ # Dates
660
+ date_list = self._ensemble_dates(ensemble)
661
+ dates = {
662
+ i: {
663
+ "label": f"{d[:4]}",
664
+ "style": {"writingMode": "vertical-rl"},
665
+ }
666
+ for i, d in enumerate(date_list)
667
+ }
668
+ if len(dates.keys()) > 0:
669
+ return dates, max(dates.keys())
670
+ return dates, None
671
+
672
+ def _add_time_plot_visibility_callback(self) -> None:
673
+ @callback(
674
+ Output(self._settings_component(ViewSettings.Ids.REAL_OR_STAT), "style"),
675
+ Output(self._settings_component(ViewSettings.Ids.Y_LIM_OPTIONS), "style"),
676
+ Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
677
+ )
678
+ def toggle_time_plot_options_visibility(
679
+ realizations: List[int],
680
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
681
+ if len(realizations) == 1:
682
+ return (
683
+ {"display": "none"},
684
+ {"display": "flex", "flex-direction": "column"},
685
+ )
686
+ return (
687
+ {"display": "flex", "flex-direction": "row"},
688
+ {"display": "none"},
689
+ )
690
+
691
+ def _add_set_unit_list_callback(self) -> None:
692
+ @callback(
693
+ Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "options"),
694
+ Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
695
+ Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
696
+ )
697
+ def make_unit_list(
698
+ attribute: str,
699
+ ) -> Union[Tuple[List[Any], Co2MassScale], Tuple[List[Any], Co2VolumeScale],]:
700
+ if attribute == GraphSource.CONTAINMENT_ACTUAL_VOLUME:
701
+ return list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS
702
+ return list(Co2MassScale), Co2MassScale.MTONS
703
+
704
+ def _add_graph_callback(self) -> None:
705
+ # Cannot avoid many arguments since all the parameters are needed
706
+ # to determine what to plot
707
+ # pylint: disable=too-many-arguments
708
+ @callback(
709
+ Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
710
+ Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
577
711
  Output(
578
- self._view_component(MapViewElement.Ids.TIME_PLOT_ONE_REAL), "style"
712
+ self._view_component(MapViewElement.Ids.STATISTICS_PLOT),
713
+ "figure",
579
714
  ),
715
+ # LEGEND_DATA_STORE is updated whenever the legend is clicked. However,
716
+ # there is not need to update the plots based on this change, since that
717
+ # is done by plotly internally. We therefore use State instead of Input
718
+ State(self._view_component(MapViewElement.Ids.LEGEND_DATA_STORE), "data"),
580
719
  Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
581
- Input(self._view_component(MapViewElement.Ids.SIZE_SLIDER), "value"),
582
- State(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
583
- State(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
584
720
  Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
721
+ Input(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
722
+ Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
723
+ Input(self._settings_component(ViewSettings.Ids.Y_MIN_AUTO_GRAPH), "value"),
724
+ Input(self._settings_component(ViewSettings.Ids.Y_MIN_GRAPH), "value"),
725
+ Input(self._settings_component(ViewSettings.Ids.Y_MAX_AUTO_GRAPH), "value"),
726
+ Input(self._settings_component(ViewSettings.Ids.Y_MAX_GRAPH), "value"),
727
+ Input(self._settings_component(ViewSettings.Ids.ZONE), "value"),
728
+ Input(self._settings_component(ViewSettings.Ids.REGION), "value"),
729
+ Input(self._settings_component(ViewSettings.Ids.PHASE), "value"),
730
+ Input(self._settings_component(ViewSettings.Ids.CONTAINMENT), "value"),
731
+ Input(self._settings_component(ViewSettings.Ids.PLUME_GROUP), "value"),
732
+ Input(self._settings_component(ViewSettings.Ids.COLOR_BY), "value"),
733
+ Input(self._settings_component(ViewSettings.Ids.MARK_BY), "value"),
734
+ Input(self._settings_component(ViewSettings.Ids.SORT_PLOT), "value"),
735
+ Input(self._settings_component(ViewSettings.Ids.REAL_OR_STAT), "value"),
736
+ Input(self._settings_component(ViewSettings.Ids.DATE_OPTION), "value"),
737
+ Input(
738
+ self._settings_component(ViewSettings.Ids.STATISTICS_TAB_OPTION),
739
+ "value",
740
+ ),
741
+ Input(self._settings_component(ViewSettings.Ids.BOX_SHOW_POINTS), "value"),
585
742
  )
586
- def resize_plots(
743
+ @callback_typecheck
744
+ def update_graphs(
745
+ legend_data: LegendData,
587
746
  ensemble: str,
588
- slider_value: float,
589
- top_style: Dict,
590
- bottom_style: Dict,
591
747
  source: GraphSource,
592
- ) -> List[Dict]:
593
- bottom_style["height"] = f"{slider_value}vh"
594
- top_style["height"] = f"{80 - slider_value}vh"
748
+ co2_scale: Union[Co2MassScale, Co2VolumeScale],
749
+ realizations: List[int],
750
+ y_min_auto: List[str],
751
+ y_min_val: Optional[float],
752
+ y_max_auto: List[str],
753
+ y_max_val: Optional[float],
754
+ zone: Optional[str],
755
+ region: Optional[str],
756
+ phase: str,
757
+ containment: str,
758
+ plume_group: str,
759
+ color_choice: str,
760
+ mark_choice: Optional[str],
761
+ sorting: str,
762
+ lines_to_show: str,
763
+ date_option: str,
764
+ statistics_tab_option: StatisticsTabOption,
765
+ box_show_points: str,
766
+ ) -> Tuple[Dict, go.Figure, go.Figure, go.Figure]:
767
+ # pylint: disable=too-many-locals
768
+ figs = [no_update] * 3
769
+ cont_info = process_containment_info(
770
+ zone,
771
+ region,
772
+ phase,
773
+ containment,
774
+ plume_group,
775
+ color_choice,
776
+ mark_choice,
777
+ sorting,
778
+ lines_to_show,
779
+ date_option,
780
+ statistics_tab_option,
781
+ box_show_points,
782
+ self._menu_options[ensemble][source],
783
+ )
784
+ if source in [
785
+ GraphSource.CONTAINMENT_MASS,
786
+ GraphSource.CONTAINMENT_ACTUAL_VOLUME,
787
+ ]:
788
+ plot_ids = make_plot_ids(
789
+ ensemble,
790
+ source,
791
+ co2_scale,
792
+ cont_info,
793
+ realizations,
794
+ len(figs),
795
+ )
796
+ y_limits = [
797
+ y_min_val if len(y_min_auto) == 0 else None,
798
+ y_max_val if len(y_max_auto) == 0 else None,
799
+ ]
800
+ if (
801
+ source == GraphSource.CONTAINMENT_MASS
802
+ and ensemble in self._co2_table_providers
803
+ ):
804
+ figs[: len(figs)] = generate_containment_figures(
805
+ self._co2_table_providers[ensemble],
806
+ co2_scale,
807
+ realizations,
808
+ y_limits,
809
+ cont_info,
810
+ legend_data,
811
+ )
812
+ elif (
813
+ source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
814
+ and ensemble in self._co2_actual_volume_table_providers
815
+ ):
816
+ figs[: len(figs)] = generate_containment_figures(
817
+ self._co2_actual_volume_table_providers[ensemble],
818
+ co2_scale,
819
+ realizations,
820
+ y_limits,
821
+ cont_info,
822
+ legend_data,
823
+ )
824
+ set_plot_ids(figs, plot_ids)
825
+ elif source == GraphSource.UNSMRY:
826
+ if self._unsmry_providers is not None:
827
+ if ensemble in self._unsmry_providers:
828
+ figs[0] = go.Figure()
829
+ figs[1] = generate_unsmry_figures(
830
+ self._unsmry_providers[ensemble],
831
+ co2_scale,
832
+ self._co2_table_providers[ensemble],
833
+ )
834
+ figs[2] = go.Figure()
835
+ else:
836
+ LOGGER.warning(
837
+ """UNSMRY file has not been specified as input.
838
+ Please use unsmry_relpath in the configuration."""
839
+ )
840
+ return figs # type: ignore
595
841
 
596
- styles = [{"height": f"{slider_value * 0.9 - 4}vh", "width": "90%"}] * 3
597
- if source == GraphSource.UNSMRY and self._unsmry_providers is None:
598
- styles = [{"display": "none"}] * 3
599
- elif (
600
- source == GraphSource.CONTAINMENT_MASS
601
- and ensemble not in self._co2_table_providers
602
- ):
603
- styles = [{"display": "none"}] * 3
604
- elif (
605
- source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
606
- and ensemble not in self._co2_actual_volume_table_providers
842
+ def _add_legend_change_callback(self) -> None:
843
+ @callback(
844
+ Output(self._view_component(MapViewElement.Ids.LEGEND_DATA_STORE), "data"),
845
+ Input(self._view_component(MapViewElement.Ids.BAR_PLOT), "restyleData"),
846
+ State(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
847
+ Input(self._view_component(MapViewElement.Ids.TIME_PLOT), "restyleData"),
848
+ State(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
849
+ Input(
850
+ self._view_component(MapViewElement.Ids.STATISTICS_PLOT), "restyleData"
851
+ ),
852
+ State(self._view_component(MapViewElement.Ids.STATISTICS_PLOT), "figure"),
853
+ Input(
854
+ self._settings_component(ViewSettings.Ids.STATISTICS_TAB_OPTION),
855
+ "value",
856
+ ),
857
+ )
858
+ def on_bar_legend_update(
859
+ bar_event: List[Any],
860
+ bar_figure: go.Figure,
861
+ time_event: List[Any],
862
+ time_figure: go.Figure,
863
+ stats_event: List[Any],
864
+ stats_figure: go.Figure,
865
+ _: StatisticsTabOption,
866
+ ) -> Patch:
867
+ # We cannot subscribe to a legend click event directly, but we can subscribe
868
+ # to the more general "restyleData" event, and then try to identify if this
869
+ # was a click event or not. If yes, we update the appropriate store component
870
+ p = Patch()
871
+ _id = ctx.triggered_id
872
+ if _id is None:
873
+ return p
874
+
875
+ if _id == self._view_component(MapViewElement.Ids.BAR_PLOT):
876
+ if self._is_legend_click_event(bar_event):
877
+ p["bar_legendonly"] = extract_legendonly(bar_figure)
878
+ elif _id == self._view_component(MapViewElement.Ids.TIME_PLOT):
879
+ if self._is_legend_click_event(time_event):
880
+ p["time_legendonly"] = extract_legendonly(time_figure)
881
+ elif _id in (
882
+ self._view_component(MapViewElement.Ids.STATISTICS_PLOT),
883
+ self._settings_component(ViewSettings.Ids.STATISTICS_TAB_OPTION),
607
884
  ):
608
- styles = [{"display": "none"}] * 3
885
+ if self._is_legend_click_event(stats_event):
886
+ p["stats_legendonly"] = extract_legendonly(stats_figure)
887
+ return p
609
888
 
610
- return [top_style, bottom_style] + styles
889
+ @staticmethod
890
+ def _is_legend_click_event(event: List[Any]) -> bool:
891
+ # A typical legend click event would be: [{'visible': ['legendonly']}, [1]]
892
+ if event is None or not isinstance(event, list):
893
+ return False
894
+ return any("visible" in e for e in event if isinstance(e, dict))