webviz-subsurface 0.2.37__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.
@@ -2,7 +2,7 @@ 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
@@ -15,6 +15,7 @@ from webviz_subsurface.plugins._co2_leakage._utilities.callbacks import (
15
15
  create_map_layers,
16
16
  create_map_viewports,
17
17
  derive_surface_address,
18
+ extract_legendonly,
18
19
  generate_containment_figures,
19
20
  generate_unsmry_figures,
20
21
  get_plume_polygon,
@@ -56,7 +57,9 @@ from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import (
56
57
  from webviz_subsurface.plugins._co2_leakage.views.mainview.settings import ViewSettings
57
58
 
58
59
  from . import _error
60
+ from ._types import LegendData
59
61
  from ._utilities.color_tables import co2leakage_color_tables
62
+ from ._utilities.containment_info import StatisticsTabOption
60
63
 
61
64
  LOGGER = logging.getLogger(__name__)
62
65
  TABLES_PATH = "share/results/tables"
@@ -64,31 +67,89 @@ TABLES_PATH = "share/results/tables"
64
67
 
65
68
  # pylint: disable=too-many-instance-attributes
66
69
  class CO2Leakage(WebvizPluginABC):
67
- """
68
- Plugin for analyzing CO2 leakage potential across multiple realizations in an FMU
69
- ensemble
70
+ """Plugin for analyzing CO2 leakage potential across multiple realizations in an
71
+ FMU ensemble
72
+
73
+ ---
70
74
 
71
75
  * **`ensembles`:** Which ensembles in `shared_settings` to visualize.
72
76
  * **`well_pick_file`:** Path to a file containing well picks
73
- * **`plume_mass_relpath`:** Path to a table of co2 containment data (amount of
74
- CO2 outside/inside a boundary), for co2 mass. Relative to each realization.
75
- * **`plume_actual_volume_relpath`:** Path to a table of co2 containment data (amount
76
- of CO2 outside/inside a boundary), for co2 volume of type "actual". Relative to each
77
- realization.
78
- * **`unsmry_relpath`:** Relative path to a csv version of a unified summary file
79
- * **`fault_polygon_attribute`:** Polygons with this attribute are used as fault
80
- polygons
81
- * **`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
82
84
  attributes visualized by the plugin and attributes names used by
83
85
  EnsembleSurfaceProvider
84
86
  * **`initial_surface`:** Name of the surface/formation to show when the plugin is
85
- launched. If no name is provided, the first alphabetical surface is shown.
86
- * **`map_surface_names_to_well_pick_names`:** Optional mapping between surface map
87
- names and surface names used in the well pick file
88
- * **`map_surface_names_to_fault_polygons`:** Optional mapping between surface map
89
- names and surface names used by the fault polygons
90
- * **`boundary_settings`:** Settings (paths etc) for polygons representing the
91
- containment and hazardous areas
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.
92
153
  """
93
154
 
94
155
  class Ids(StrEnum):
@@ -104,7 +165,7 @@ class CO2Leakage(WebvizPluginABC):
104
165
  ensembles: List[str],
105
166
  well_pick_file: Optional[str] = None,
106
167
  plume_mass_relpath: str = TABLES_PATH + "/plume_mass.csv",
107
- plume_actual_volume_relpath: str = TABLES_PATH + "/plume_actual_volume.csv",
168
+ plume_actual_volume_relpath: Optional[str] = None,
108
169
  unsmry_relpath: Optional[str] = None,
109
170
  fault_polygon_attribute: str = "dl_extracted_faultlines",
110
171
  initial_surface: Optional[str] = None,
@@ -190,7 +251,6 @@ class CO2Leakage(WebvizPluginABC):
190
251
  "change": False,
191
252
  "unit": "tons",
192
253
  }
193
- self._plot_id = ""
194
254
  self._color_tables = co2leakage_color_tables()
195
255
  self._well_pick_names: Dict[str, List[str]] = {
196
256
  ens: (
@@ -261,504 +321,574 @@ class CO2Leakage(WebvizPluginABC):
261
321
  return dates
262
322
 
263
323
  # Might want to do some refactoring if this gets too big
264
- # pylint: disable=too-many-statements
265
324
  def _set_callbacks(self) -> None:
266
- # Cannot avoid many arguments since all the parameters are needed
267
- # to determine what to plot
268
325
  if self._content["any_table"]:
269
- # pylint: disable=too-many-arguments
270
- @callback(
271
- Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "figure"),
272
- Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "figure"),
273
- Output(
274
- self._view_component(MapViewElement.Ids.STATISTICS_PLOT),
275
- "figure",
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:
345
+ @callback(
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"),
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"),
355
+ Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
356
+ )
357
+ def resize_plots(
358
+ ensemble: str,
359
+ slider_value: float,
360
+ top_style: Dict,
361
+ bottom_style: Dict,
362
+ source: GraphSource,
363
+ ) -> List[Dict]:
364
+ bottom_style["height"] = f"{slider_value}vh"
365
+ top_style["height"] = f"{80 - slider_value}vh"
366
+
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
382
+
383
+ def _add_feedback_dialog_callback(self) -> None:
384
+ @callback(
385
+ Output(ViewSettings.Ids.FEEDBACK, "open"),
386
+ Input(ViewSettings.Ids.FEEDBACK_BUTTON, "n_clicks"),
387
+ )
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
392
+
393
+ def _add_thresholds_dialog_callback(self) -> None:
394
+ @callback(
395
+ Output(ViewSettings.Ids.VISUALIZATION_THRESHOLD_DIALOG, "open"),
396
+ Input(ViewSettings.Ids.VISUALIZATION_THRESHOLD_BUTTON, "n_clicks"),
397
+ )
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
402
+
403
+ def _add_options_dialog_callback(self) -> None:
404
+ @callback(
405
+ Output(ViewSettings.Ids.OPTIONS_DIALOG, "open"),
406
+ Input(ViewSettings.Ids.OPTIONS_DIALOG_BUTTON, "n_clicks"),
407
+ )
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
412
+
413
+ def _add_create_map_callback(self) -> None:
414
+ # Cannot avoid many arguments and/or locals since all layers of the DeckGL map
415
+ # need to be updated simultaneously
416
+ # pylint: disable=too-many-arguments,too-many-locals
417
+ @callback(
418
+ Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "layers"),
419
+ Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "children"),
420
+ Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"),
421
+ inputs={
422
+ "attribute": Input(
423
+ self._settings_component(ViewSettings.Ids.PROPERTY), "value"
276
424
  ),
277
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
278
- Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
279
- Input(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
280
- Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
281
- Input(
282
- self._settings_component(ViewSettings.Ids.Y_MIN_AUTO_GRAPH), "value"
425
+ "date": Input(
426
+ self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"
283
427
  ),
284
- Input(self._settings_component(ViewSettings.Ids.Y_MIN_GRAPH), "value"),
285
- Input(
286
- self._settings_component(ViewSettings.Ids.Y_MAX_AUTO_GRAPH), "value"
428
+ "formation": Input(
429
+ self._settings_component(ViewSettings.Ids.FORMATION), "value"
287
430
  ),
288
- Input(self._settings_component(ViewSettings.Ids.Y_MAX_GRAPH), "value"),
289
- Input(self._settings_component(ViewSettings.Ids.ZONE), "value"),
290
- Input(self._settings_component(ViewSettings.Ids.REGION), "value"),
291
- Input(self._settings_component(ViewSettings.Ids.PHASE), "value"),
292
- Input(self._settings_component(ViewSettings.Ids.CONTAINMENT), "value"),
293
- Input(self._settings_component(ViewSettings.Ids.PLUME_GROUP), "value"),
294
- Input(self._settings_component(ViewSettings.Ids.COLOR_BY), "value"),
295
- Input(self._settings_component(ViewSettings.Ids.MARK_BY), "value"),
296
- Input(self._settings_component(ViewSettings.Ids.SORT_PLOT), "value"),
297
- Input(self._settings_component(ViewSettings.Ids.REAL_OR_STAT), "value"),
298
- Input(self._settings_component(ViewSettings.Ids.DATE_OPTION), "value"),
299
- )
300
- @callback_typecheck
301
- def update_graphs(
302
- ensemble: str,
303
- source: GraphSource,
304
- co2_scale: Union[Co2MassScale, Co2VolumeScale],
305
- realizations: List[int],
306
- y_min_auto: List[str],
307
- y_min_val: Optional[float],
308
- y_max_auto: List[str],
309
- y_max_val: Optional[float],
310
- zone: Optional[str],
311
- region: Optional[str],
312
- phase: str,
313
- containment: str,
314
- plume_group: str,
315
- color_choice: str,
316
- mark_choice: Optional[str],
317
- sorting: str,
318
- lines_to_show: str,
319
- date_option: str,
320
- ) -> Tuple[Dict, go.Figure, go.Figure, go.Figure]:
321
- # pylint: disable=too-many-locals
322
- figs = [no_update] * 3
323
- cont_info = process_containment_info(
324
- zone,
325
- region,
326
- phase,
327
- containment,
328
- plume_group,
329
- color_choice,
330
- mark_choice,
331
- sorting,
332
- lines_to_show,
333
- date_option,
334
- self._menu_options[ensemble][source],
335
- )
336
- if source in [
337
- GraphSource.CONTAINMENT_MASS,
338
- GraphSource.CONTAINMENT_ACTUAL_VOLUME,
339
- ]:
340
- plot_ids = make_plot_ids(
341
- ensemble,
342
- source,
343
- co2_scale,
344
- cont_info,
345
- realizations,
346
- lines_to_show,
347
- len(figs),
348
- )
349
- cont_info["update_first_figure"] = self._plot_id != plot_ids[0]
350
- self._plot_id = plot_ids[0]
351
- y_limits = [
352
- y_min_val if len(y_min_auto) == 0 else None,
353
- y_max_val if len(y_max_auto) == 0 else None,
354
- ]
355
- if (
356
- source == GraphSource.CONTAINMENT_MASS
357
- and ensemble in self._co2_table_providers
358
- ):
359
- figs[: len(figs)] = generate_containment_figures(
360
- self._co2_table_providers[ensemble],
361
- co2_scale,
362
- realizations,
363
- y_limits,
364
- cont_info,
365
- )
366
- elif (
367
- source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
368
- and ensemble in self._co2_actual_volume_table_providers
369
- ):
370
- figs[: len(figs)] = generate_containment_figures(
371
- self._co2_actual_volume_table_providers[ensemble],
372
- co2_scale,
373
- realizations,
374
- y_limits,
375
- cont_info,
376
- )
377
- set_plot_ids(figs, plot_ids)
378
- elif source == GraphSource.UNSMRY:
379
- if self._unsmry_providers is not None:
380
- if ensemble in self._unsmry_providers:
381
- figs[0] = go.Figure()
382
- figs[1] = generate_unsmry_figures(
383
- self._unsmry_providers[ensemble],
384
- co2_scale,
385
- self._co2_table_providers[ensemble],
386
- )
387
- figs[2] = go.Figure()
388
- else:
389
- LOGGER.warning(
390
- """UNSMRY file has not been specified as input.
391
- Please use unsmry_relpath in the configuration."""
392
- )
393
- return figs # type: ignore
394
-
395
- @callback(
396
- Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "options"),
397
- Output(self._settings_component(ViewSettings.Ids.CO2_SCALE), "value"),
398
- Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
399
- )
400
- def make_unit_list(
401
- attribute: str,
402
- ) -> Union[
403
- Tuple[List[Any], Co2MassScale],
404
- Tuple[List[Any], Co2VolumeScale],
405
- ]:
406
- if attribute == GraphSource.CONTAINMENT_ACTUAL_VOLUME:
407
- return list(Co2VolumeScale), Co2VolumeScale.BILLION_CUBIC_METERS
408
- return list(Co2MassScale), Co2MassScale.MTONS
409
-
410
- @callback(
411
- Output(
412
- self._settings_component(ViewSettings.Ids.REAL_OR_STAT), "style"
431
+ "realization": Input(
432
+ self._settings_component(ViewSettings.Ids.REALIZATION), "value"
413
433
  ),
414
- Output(
415
- self._settings_component(ViewSettings.Ids.Y_LIM_OPTIONS), "style"
434
+ "statistic": Input(
435
+ self._settings_component(ViewSettings.Ids.STATISTIC), "value"
416
436
  ),
417
- Input(self._settings_component(ViewSettings.Ids.REALIZATION), "value"),
418
- )
419
- def toggle_time_plot_options_visibility(
420
- realizations: List[int],
421
- ) -> Tuple[Dict[str, str], Dict[str, str]]:
422
- if len(realizations) == 1:
423
- return (
424
- {"display": "none"},
425
- {"display": "flex", "flex-direction": "column"},
426
- )
427
- return (
428
- {"display": "flex", "flex-direction": "row"},
429
- {"display": "none"},
430
- )
431
-
432
- if self._content["maps"]:
433
-
434
- @callback(
435
- Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "marks"),
436
- Output(self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"),
437
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
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
+ },
485
+ )
486
+ def update_map_attribute(
487
+ attribute: MapAttribute,
488
+ date: int,
489
+ formation: str,
490
+ realization: List[int],
491
+ statistic: str,
492
+ color_map_name: str,
493
+ cm_min_auto: List[str],
494
+ cm_min_val: Optional[float],
495
+ cm_max_auto: List[str],
496
+ cm_max_val: Optional[float],
497
+ plume_threshold: Optional[float],
498
+ plume_smoothing: Optional[float],
499
+ visualization_update: int,
500
+ mass_unit: str,
501
+ mass_unit_update: int,
502
+ options_dialog_options: List[int],
503
+ selected_wells: List[str],
504
+ ensemble: str,
505
+ current_views: List[Any],
506
+ thresholds: List[float],
507
+ ) -> Tuple[List[Dict[Any, Any]], Optional[List[Any]], Dict[Any, Any]]:
508
+ # Unable to clear cache (when needed) without the protected member
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
513
+ self._visualization_info = process_visualization_info(
514
+ attribute,
515
+ current_thresholds,
516
+ mass_unit,
517
+ self._visualization_info,
518
+ self._surface_server._image_cache,
438
519
  )
439
- def set_dates(
440
- ensemble: str,
441
- ) -> Tuple[Dict[int, Dict[str, Any]], Optional[int]]:
442
- if ensemble is None:
443
- return {}, None
444
- # Dates
445
- date_list = self._ensemble_dates(ensemble)
446
- dates = {
447
- i: {
448
- "label": f"{d[:4]}",
449
- "style": {"writingMode": "vertical-rl"},
450
- }
451
- for i, d in enumerate(date_list)
520
+ if self._visualization_info["change"]:
521
+ return [], None, no_update
522
+ attribute = MapAttribute(attribute)
523
+ if len(realization) == 0 or ensemble is None:
524
+ raise PreventUpdate
525
+ if isinstance(date, int):
526
+ datestr = self._ensemble_dates(ensemble)[date]
527
+ elif date is None:
528
+ datestr = None
529
+ # Contour data
530
+ contour_data = None
531
+ if MapType[MapAttribute(attribute).name].value == "PLUME":
532
+ contour_data = {
533
+ "property": property_origin(attribute, self._map_attribute_names),
534
+ "threshold": plume_threshold,
535
+ "smoothing": plume_smoothing,
452
536
  }
453
- if len(dates.keys()) > 0:
454
- return dates, max(dates.keys())
455
- return dates, None
456
-
457
- @callback(
458
- Output(self._view_component(MapViewElement.Ids.DATE_WRAPPER), "style"),
459
- Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"),
460
- )
461
- def toggle_date_slider(attribute: str) -> Dict[str, str]:
462
- if MapType[MapAttribute(attribute).name].value == "MIGRATION_TIME":
463
- return {"display": "none"}
464
- return {}
465
-
466
- @callback(
467
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "options"),
468
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"),
469
- Output(ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "style"),
470
- Output(ViewSettings.Ids.WELL_FILTER_HEADER, "style"),
471
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
472
- )
473
- def set_well_options(
474
- ensemble: str,
475
- ) -> Tuple[List[Any], List[str], Dict[Any, Any], Dict[Any, Any]]:
476
- return (
477
- [{"label": i, "value": i} for i in self._well_pick_names[ensemble]],
478
- self._well_pick_names[ensemble],
479
- {
480
- "display": (
481
- "block" if self._well_pick_names[ensemble] else "none"
482
- ),
483
- "height": f"{len(self._well_pick_names[ensemble]) * 22}px",
484
- },
485
- {
486
- "flex": 3,
487
- "minWidth": "20px",
488
- "display": (
489
- "block" if self._well_pick_names[ensemble] else "none"
490
- ),
491
- },
492
- )
493
-
494
- # Cannot avoid many arguments and/or locals since all layers of the DeckGL map
495
- # need to be updated simultaneously
496
- # pylint: disable=too-many-arguments,too-many-locals
497
- @callback(
498
- Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "layers"),
499
- Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "children"),
500
- Output(self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"),
501
- inputs={
502
- "attribute": Input(
503
- self._settings_component(ViewSettings.Ids.PROPERTY), "value"
504
- ),
505
- "date": Input(
506
- self._view_component(MapViewElement.Ids.DATE_SLIDER), "value"
507
- ),
508
- "formation": Input(
509
- self._settings_component(ViewSettings.Ids.FORMATION), "value"
510
- ),
511
- "realization": Input(
512
- self._settings_component(ViewSettings.Ids.REALIZATION), "value"
513
- ),
514
- "statistic": Input(
515
- self._settings_component(ViewSettings.Ids.STATISTIC), "value"
516
- ),
517
- "color_map_name": Input(
518
- self._settings_component(ViewSettings.Ids.COLOR_SCALE), "value"
519
- ),
520
- "cm_min_auto": Input(
521
- self._settings_component(ViewSettings.Ids.CM_MIN_AUTO), "value"
522
- ),
523
- "cm_min_val": Input(
524
- self._settings_component(ViewSettings.Ids.CM_MIN), "value"
525
- ),
526
- "cm_max_auto": Input(
527
- self._settings_component(ViewSettings.Ids.CM_MAX_AUTO), "value"
528
- ),
529
- "cm_max_val": Input(
530
- self._settings_component(ViewSettings.Ids.CM_MAX), "value"
531
- ),
532
- "plume_threshold": Input(
533
- self._settings_component(ViewSettings.Ids.PLUME_THRESHOLD),
534
- "value",
535
- ),
536
- "plume_smoothing": Input(
537
- self._settings_component(ViewSettings.Ids.PLUME_SMOOTHING),
538
- "value",
539
- ),
540
- "visualization_update": Input(
541
- self._settings_component(ViewSettings.Ids.VISUALIZATION_UPDATE),
542
- "n_clicks",
543
- ),
544
- "mass_unit": Input(
545
- self._settings_component(ViewSettings.Ids.MASS_UNIT), "value"
546
- ),
547
- "mass_unit_update": Input(
548
- self._settings_component(ViewSettings.Ids.MASS_UNIT_UPDATE),
549
- "n_clicks",
550
- ),
551
- "options_dialog_options": Input(
552
- ViewSettings.Ids.OPTIONS_DIALOG_OPTIONS, "value"
553
- ),
554
- "selected_wells": Input(
555
- ViewSettings.Ids.OPTIONS_DIALOG_WELL_FILTER, "value"
556
- ),
557
- "ensemble": Input(
558
- self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"
537
+ # Surface
538
+ surf_data, summed_mass = None, None
539
+ if formation is not None and len(realization) > 0:
540
+ surf_data, summed_mass = SurfaceData.from_server(
541
+ server=self._surface_server,
542
+ provider=self._ensemble_surface_providers[ensemble],
543
+ address=derive_surface_address(
544
+ formation,
545
+ attribute,
546
+ datestr,
547
+ realization,
548
+ self._map_attribute_names,
549
+ statistic,
550
+ contour_data,
559
551
  ),
560
- "current_views": State(
561
- self._view_component(MapViewElement.Ids.DECKGL_MAP), "views"
552
+ color_map_range=(
553
+ cm_min_val if len(cm_min_auto) == 0 else None,
554
+ cm_max_val if len(cm_max_auto) == 0 else None,
562
555
  ),
563
- "thresholds": [Input(id, "value") for id in self._threshold_ids],
564
- },
565
- )
566
- def update_map_attribute(
567
- attribute: MapAttribute,
568
- date: int,
569
- formation: str,
570
- realization: List[int],
571
- statistic: str,
572
- color_map_name: str,
573
- cm_min_auto: List[str],
574
- cm_min_val: Optional[float],
575
- cm_max_auto: List[str],
576
- cm_max_val: Optional[float],
577
- plume_threshold: Optional[float],
578
- plume_smoothing: Optional[float],
579
- visualization_update: int,
580
- mass_unit: str,
581
- mass_unit_update: int,
582
- options_dialog_options: List[int],
583
- selected_wells: List[str],
584
- ensemble: str,
585
- current_views: List[Any],
586
- thresholds: List[float],
587
- ) -> Tuple[List[Dict[Any, Any]], Optional[List[Any]], Dict[Any, Any]]:
588
- # Unable to clear cache (when needed) without the protected member
589
- # pylint: disable=protected-access
590
- current_thresholds = dict(zip(self._threshold_ids, thresholds))
591
- assert visualization_update >= 0 # Need the input to trigger callback
592
- assert mass_unit_update >= 0 # These are just to silence pylint
593
- self._visualization_info = process_visualization_info(
594
- attribute,
595
- current_thresholds,
596
- mass_unit,
597
- self._visualization_info,
598
- self._surface_server._image_cache,
556
+ color_map_name=color_map_name,
557
+ readable_name_=readable_name(attribute),
558
+ visualization_info=self._visualization_info,
559
+ map_attribute_names=self._map_attribute_names,
599
560
  )
600
- if self._visualization_info["change"]:
601
- return [], None, no_update
602
- attribute = MapAttribute(attribute)
603
- if len(realization) == 0 or ensemble is None:
604
- raise PreventUpdate
605
- if isinstance(date, int):
606
- datestr = self._ensemble_dates(ensemble)[date]
607
- elif date is None:
608
- datestr = None
609
- # Contour data
610
- contour_data = None
611
- if MapType[MapAttribute(attribute).name].value == "PLUME":
612
- contour_data = {
613
- "property": property_origin(
614
- attribute, self._map_attribute_names
615
- ),
616
- "threshold": plume_threshold,
617
- "smoothing": plume_smoothing,
618
- }
619
- # Surface
620
- surf_data, summed_mass = None, None
621
- if formation is not None and len(realization) > 0:
622
- surf_data, summed_mass = SurfaceData.from_server(
623
- server=self._surface_server,
624
- provider=self._ensemble_surface_providers[ensemble],
625
- address=derive_surface_address(
626
- formation,
627
- attribute,
628
- datestr,
629
- realization,
630
- self._map_attribute_names,
631
- statistic,
632
- contour_data,
633
- ),
634
- color_map_range=(
635
- cm_min_val if len(cm_min_auto) == 0 else None,
636
- cm_max_val if len(cm_max_auto) == 0 else None,
637
- ),
638
- color_map_name=color_map_name,
639
- readable_name_=readable_name(attribute),
640
- visualization_info=self._visualization_info,
641
- map_attribute_names=self._map_attribute_names,
642
- )
643
- assert isinstance(self._visualization_info["unit"], str)
644
- surf_data, self._summed_co2 = process_summed_mass(
645
- formation,
561
+ assert isinstance(self._visualization_info["unit"], str)
562
+ surf_data, self._summed_co2 = process_summed_mass(
563
+ formation,
564
+ realization,
565
+ datestr,
566
+ attribute,
567
+ summed_mass,
568
+ surf_data,
569
+ self._summed_co2,
570
+ self._visualization_info["unit"],
571
+ )
572
+ plume_polygon = None
573
+ if contour_data is not None:
574
+ plume_polygon = get_plume_polygon(
575
+ self._ensemble_surface_providers[ensemble],
646
576
  realization,
577
+ formation,
647
578
  datestr,
648
- attribute,
649
- summed_mass,
650
- surf_data,
651
- self._summed_co2,
652
- self._visualization_info["unit"],
653
- )
654
- plume_polygon = None
655
- if contour_data is not None:
656
- plume_polygon = get_plume_polygon(
657
- self._ensemble_surface_providers[ensemble],
658
- realization,
659
- formation,
660
- datestr,
661
- contour_data,
662
- )
663
- # Create layers and view bounds
664
- fault_polygon_url = self._fault_polygon_handlers[
665
- ensemble
666
- ].extract_fault_polygon_url(formation, realization)
667
- hazardous_polygon_url = self._polygon_handlers[
668
- ensemble
669
- ].extract_hazardous_poly_url(realization)
670
- containment_polygon_url = self._polygon_handlers[
671
- ensemble
672
- ].extract_containment_poly_url(realization)
673
- layers = create_map_layers(
674
- realizations=realization,
675
- formation=formation,
676
- surface_data=surf_data,
677
- fault_polygon_url=fault_polygon_url,
678
- containment_bounds_url=containment_polygon_url,
679
- haz_bounds_url=hazardous_polygon_url,
680
- well_pick_provider=self._well_pick_provider.get(ensemble, None),
681
- plume_extent_data=plume_polygon,
682
- options_dialog_options=options_dialog_options,
683
- selected_wells=selected_wells,
579
+ contour_data,
684
580
  )
685
- annotations = create_map_annotations(
686
- formation=formation,
687
- surface_data=surf_data,
688
- colortables=self._color_tables,
689
- attribute=attribute,
690
- unit=self._visualization_info["unit"],
691
- )
692
- viewports = no_update if current_views else create_map_viewports()
693
- return layers, annotations, viewports
694
-
695
- @callback(
696
- Output(ViewSettings.Ids.OPTIONS_DIALOG, "open"),
697
- Input(ViewSettings.Ids.OPTIONS_DIALOG_BUTTON, "n_clicks"),
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)
591
+ layers = create_map_layers(
592
+ realizations=realization,
593
+ formation=formation,
594
+ surface_data=surf_data,
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),
599
+ plume_extent_data=plume_polygon,
600
+ options_dialog_options=options_dialog_options,
601
+ selected_wells=selected_wells,
698
602
  )
699
- def open_close_options_dialog(_n_clicks: Optional[int]) -> bool:
700
- if _n_clicks is not None:
701
- return _n_clicks > 0
702
- raise PreventUpdate
603
+ annotations = create_map_annotations(
604
+ formation=formation,
605
+ surface_data=surf_data,
606
+ colortables=self._color_tables,
607
+ attribute=attribute,
608
+ unit=self._visualization_info["unit"],
609
+ )
610
+ viewports = no_update if current_views else create_map_viewports()
611
+ return layers, annotations, viewports
703
612
 
704
- @callback(
705
- Output(ViewSettings.Ids.VISUALIZATION_THRESHOLD_DIALOG, "open"),
706
- Input(ViewSettings.Ids.VISUALIZATION_THRESHOLD_BUTTON, "n_clicks"),
613
+ def _add_set_well_options_callback(self) -> None:
614
+ @callback(
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"),
620
+ )
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
+ },
707
636
  )
708
- def open_close_thresholds(_n_clicks: Optional[int]) -> bool:
709
- if _n_clicks is not None:
710
- return _n_clicks > 0
711
- raise PreventUpdate
712
637
 
638
+ def _add_date_slider_visibility_callback(self) -> None:
713
639
  @callback(
714
- Output(ViewSettings.Ids.FEEDBACK, "open"),
715
- 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"),
716
642
  )
717
- def open_close_feedback(_n_clicks: Optional[int]) -> bool:
718
- if _n_clicks is not None:
719
- return _n_clicks > 0
720
- 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 {}
721
647
 
722
- if self._content["maps"] and self._content["any_table"]:
648
+ def _add_set_dates_callback(self) -> None:
649
+ @callback(
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
723
671
 
724
- @callback(
725
- Output(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
726
- Output(
727
- self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"
728
- ),
729
- Output(self._view_component(MapViewElement.Ids.BAR_PLOT), "style"),
730
- Output(self._view_component(MapViewElement.Ids.TIME_PLOT), "style"),
731
- Output(
732
- self._view_component(MapViewElement.Ids.STATISTICS_PLOT), "style"
733
- ),
734
- Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
735
- Input(self._view_component(MapViewElement.Ids.SIZE_SLIDER), "value"),
736
- State(self._view_component(MapViewElement.Ids.TOP_ELEMENT), "style"),
737
- State(self._view_component(MapViewElement.Ids.BOTTOM_ELEMENT), "style"),
738
- Input(self._settings_component(ViewSettings.Ids.GRAPH_SOURCE), "value"),
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"},
739
689
  )
740
- def resize_plots(
741
- ensemble: str,
742
- slider_value: float,
743
- top_style: Dict,
744
- bottom_style: Dict,
745
- source: GraphSource,
746
- ) -> List[Dict]:
747
- bottom_style["height"] = f"{slider_value}vh"
748
- top_style["height"] = f"{80 - slider_value}vh"
749
-
750
- styles = [{"height": f"{slider_value * 0.9 - 4}vh", "width": "90%"}] * 3
751
- if source == GraphSource.UNSMRY and self._unsmry_providers is None:
752
- styles = [{"display": "none"}] * 3
753
- elif (
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"),
711
+ Output(
712
+ self._view_component(MapViewElement.Ids.STATISTICS_PLOT),
713
+ "figure",
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"),
719
+ Input(self._settings_component(ViewSettings.Ids.ENSEMBLE), "value"),
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"),
742
+ )
743
+ @callback_typecheck
744
+ def update_graphs(
745
+ legend_data: LegendData,
746
+ ensemble: str,
747
+ source: GraphSource,
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 (
754
801
  source == GraphSource.CONTAINMENT_MASS
755
- and ensemble not in self._co2_table_providers
802
+ and ensemble in self._co2_table_providers
756
803
  ):
757
- styles = [{"display": "none"}] * 3
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
+ )
758
812
  elif (
759
813
  source == GraphSource.CONTAINMENT_ACTUAL_VOLUME
760
- and ensemble not in self._co2_actual_volume_table_providers
814
+ and ensemble in self._co2_actual_volume_table_providers
761
815
  ):
762
- styles = [{"display": "none"}] * 3
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
841
+
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),
884
+ ):
885
+ if self._is_legend_click_event(stats_event):
886
+ p["stats_legendonly"] = extract_legendonly(stats_figure)
887
+ return p
763
888
 
764
- 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))