webviz-subsurface 0.2.39__py3-none-any.whl → 0.2.41__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 (105) hide show
  1. webviz_subsurface/_figures/timeseries_figure.py +1 -1
  2. webviz_subsurface/_providers/ensemble_summary_provider/_provider_impl_arrow_lazy.py +3 -1
  3. webviz_subsurface/_providers/ensemble_summary_provider/_provider_impl_arrow_presampled.py +3 -1
  4. webviz_subsurface/_providers/ensemble_table_provider/ensemble_table_provider_impl_arrow.py +3 -1
  5. webviz_subsurface/_utils/dataframe_utils.py +1 -1
  6. webviz_subsurface/_version.py +34 -0
  7. webviz_subsurface/plugins/_bhp_qc/views/_view_functions.py +5 -5
  8. webviz_subsurface/plugins/_co2_migration/__init__.py +1 -0
  9. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_plugin.py +86 -46
  10. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/callbacks.py +53 -30
  11. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/co2volume.py +283 -40
  12. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/color_tables.py +1 -1
  13. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/containment_data_provider.py +6 -4
  14. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/containment_info.py +6 -0
  15. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/ensemble_well_picks.py +1 -1
  16. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/generic.py +59 -6
  17. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/initialization.py +73 -10
  18. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/polygon_handler.py +1 -1
  19. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/summary_graphs.py +20 -18
  20. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/surface_publishing.py +18 -20
  21. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/unsmry_data_provider.py +8 -8
  22. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/views/mainview/mainview.py +98 -44
  23. webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/views/mainview/settings.py +7 -5
  24. webviz_subsurface/plugins/_disk_usage.py +19 -8
  25. webviz_subsurface/plugins/_line_plotter_fmu/controllers/build_figure.py +4 -4
  26. webviz_subsurface/plugins/_map_viewer_fmu/layout.py +2 -1
  27. webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py +1 -1
  28. webviz_subsurface/plugins/_parameter_analysis/_utils/_parameters_model.py +5 -5
  29. webviz_subsurface/plugins/_property_statistics/property_statistics.py +1 -1
  30. webviz_subsurface/plugins/_relative_permeability.py +6 -6
  31. webviz_subsurface/plugins/_reservoir_simulation_timeseries_regional.py +12 -12
  32. webviz_subsurface/plugins/_running_time_analysis_fmu.py +6 -1
  33. webviz_subsurface/plugins/_seismic_misfit.py +2 -3
  34. webviz_subsurface/plugins/_simulation_time_series/_views/_subplot_view/_utils/vector_statistics.py +4 -4
  35. webviz_subsurface/plugins/_structural_uncertainty/views/intersection_and_map.py +1 -1
  36. webviz_subsurface/plugins/_swatinit_qc/_business_logic.py +1 -1
  37. webviz_subsurface-0.2.41.dist-info/METADATA +822 -0
  38. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info}/RECORD +51 -102
  39. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info}/WHEEL +1 -1
  40. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info}/entry_points.txt +1 -1
  41. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info}/top_level.txt +0 -1
  42. tests/integration_tests/test_parameter_filter.py +0 -28
  43. tests/integration_tests/test_surface_selector.py +0 -53
  44. tests/unit_tests/abbreviations_tests/test_reservoir_simulation.py +0 -94
  45. tests/unit_tests/data_input/__init__.py +0 -0
  46. tests/unit_tests/data_input/test_calc_from_cumulatives.py +0 -178
  47. tests/unit_tests/data_input/test_image_processing.py +0 -11
  48. tests/unit_tests/mocks/__init__.py +0 -0
  49. tests/unit_tests/mocks/ensemble_summary_provider_dummy.py +0 -67
  50. tests/unit_tests/model_tests/__init__.py +0 -0
  51. tests/unit_tests/model_tests/test_ensemble_model.py +0 -176
  52. tests/unit_tests/model_tests/test_ensemble_set_model.py +0 -105
  53. tests/unit_tests/model_tests/test_gruptree_model.py +0 -89
  54. tests/unit_tests/model_tests/test_property_statistics_model.py +0 -42
  55. tests/unit_tests/model_tests/test_surface_set_model.py +0 -48
  56. tests/unit_tests/model_tests/test_well_attributes_model.py +0 -110
  57. tests/unit_tests/model_tests/test_well_set_model.py +0 -70
  58. tests/unit_tests/plugin_tests/__init__.py +0 -0
  59. tests/unit_tests/plugin_tests/test_grouptree.py +0 -175
  60. tests/unit_tests/plugin_tests/test_simulation_time_series/__init__.py +0 -0
  61. tests/unit_tests/plugin_tests/test_simulation_time_series/mocks/__init__.py +0 -0
  62. tests/unit_tests/plugin_tests/test_simulation_time_series/mocks/derived_vectors_accessor_ensemble_summary_provider_mock.py +0 -60
  63. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/__init__.py +0 -0
  64. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_create_vector_traces_utils.py +0 -530
  65. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_dataframe_utils.py +0 -119
  66. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_datetime_utils.py +0 -51
  67. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_delta_ensemble_utils.py +0 -222
  68. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_derived_delta_ensemble_vectors_accessor_impl.py +0 -319
  69. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_derived_ensemble_vectors_accessor_impl.py +0 -271
  70. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_derived_ensemble_vectors_accessor_utils.py +0 -78
  71. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_derived_vector_accessor.py +0 -57
  72. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_ensemble_summary_provider_set_utils.py +0 -213
  73. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_from_timeseries_cumulatives.py +0 -322
  74. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_history_vectors.py +0 -201
  75. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_trace_line_shape.py +0 -56
  76. tests/unit_tests/plugin_tests/test_simulation_time_series/test_utils/test_vector_statistics.py +0 -171
  77. tests/unit_tests/plugin_tests/test_tornado_data.py +0 -130
  78. tests/unit_tests/plugin_tests/test_well_completions.py +0 -158
  79. tests/unit_tests/provider_tests/__init__.py +0 -0
  80. tests/unit_tests/provider_tests/test_ensemble_summary_provider.py +0 -255
  81. tests/unit_tests/provider_tests/test_ensemble_summary_provider_impl_arrow_lazy.py +0 -388
  82. tests/unit_tests/provider_tests/test_ensemble_summary_provider_impl_arrow_presampled.py +0 -160
  83. tests/unit_tests/provider_tests/test_ensemble_summary_provider_resampling.py +0 -320
  84. tests/unit_tests/provider_tests/test_ensemble_table_provider.py +0 -190
  85. tests/unit_tests/utils_tests/__init__.py +0 -0
  86. tests/unit_tests/utils_tests/test_dataframe_utils.py +0 -281
  87. tests/unit_tests/utils_tests/test_ensemble_summary_provider_set/__init__.py +0 -0
  88. tests/unit_tests/utils_tests/test_ensemble_summary_provider_set/test_ensemble_summary_provider_set.py +0 -306
  89. tests/unit_tests/utils_tests/test_formatting.py +0 -10
  90. tests/unit_tests/utils_tests/test_simulation_timeseries.py +0 -51
  91. webviz_subsurface/plugins/_co2_leakage/__init__.py +0 -1
  92. webviz_subsurface/plugins/_co2_leakage/_utilities/__init__.py +0 -0
  93. webviz_subsurface/plugins/_co2_leakage/views/__init__.py +0 -0
  94. webviz_subsurface/plugins/_co2_leakage/views/mainview/__init__.py +0 -0
  95. webviz_subsurface-0.2.39.dist-info/METADATA +0 -147
  96. /webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_error.py +0 -0
  97. /webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_types.py +0 -0
  98. {tests/integration_tests → webviz_subsurface/plugins/_co2_migration/_utilities}/__init__.py +0 -0
  99. /webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/_misc.py +0 -0
  100. /webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/fault_polygons_handler.py +0 -0
  101. /webviz_subsurface/plugins/{_co2_leakage → _co2_migration}/_utilities/plume_extent.py +0 -0
  102. {tests/unit_tests → webviz_subsurface/plugins/_co2_migration/views}/__init__.py +0 -0
  103. {tests/unit_tests/abbreviations_tests → webviz_subsurface/plugins/_co2_migration/views/mainview}/__init__.py +0 -0
  104. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info/licenses}/LICENSE +0 -0
  105. {webviz_subsurface-0.2.39.dist-info → webviz_subsurface-0.2.41.dist-info/licenses}/LICENSE.chromedriver +0 -0
@@ -1,5 +1,7 @@
1
1
  # pylint: disable=too-many-lines
2
+ # pylint: disable=C0103
2
3
  # NBNB-AS: We should address this pylint message soon
4
+ import re
3
5
  import warnings
4
6
  from datetime import datetime as dt
5
7
  from typing import Any, Dict, List, Optional, Tuple, Union
@@ -11,13 +13,13 @@ import plotly.graph_objects as go
11
13
 
12
14
  from webviz_subsurface._providers import EnsembleTableProvider
13
15
  from webviz_subsurface._utils.enum_shim import StrEnum
14
- from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
16
+ from webviz_subsurface.plugins._co2_migration._utilities.containment_data_provider import (
15
17
  ContainmentDataProvider,
16
18
  )
17
- from webviz_subsurface.plugins._co2_leakage._utilities.containment_info import (
19
+ from webviz_subsurface.plugins._co2_migration._utilities.containment_info import (
18
20
  ContainmentInfo,
19
21
  )
20
- from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
22
+ from webviz_subsurface.plugins._co2_migration._utilities.generic import (
21
23
  Co2MassScale,
22
24
  Co2VolumeScale,
23
25
  )
@@ -30,14 +32,35 @@ class _Columns(StrEnum):
30
32
  VOLUME_OUTSIDE = "volume_outside"
31
33
 
32
34
 
33
- _COLOR_TOTAL = "#222222"
34
- _COLOR_CONTAINED = "#00aa00"
35
- _COLOR_OUTSIDE = "#006ddd"
36
- _COLOR_HAZARDOUS = "#dd4300"
37
- _COLOR_DISSOLVED = "#208eb7"
38
- _COLOR_GAS = "#C41E3A"
39
- _COLOR_FREE = "#FF2400"
40
- _COLOR_TRAPPED = "#880808"
35
+ class Colors(StrEnum):
36
+ # pylint: disable=invalid-name
37
+ total = "#222222"
38
+ contained = "#00aa00"
39
+ outside = "#006ddd"
40
+ hazardous = "#dd4300"
41
+ dissolved_water = "#208eb7"
42
+ dissolved_oil = "#A0522D"
43
+ gas = "#C41E3A"
44
+ free = "#FF2400"
45
+ trapped = "#880808"
46
+
47
+
48
+ class Marks(StrEnum):
49
+ dissolved_water = "/"
50
+ dissolved_oil = "x"
51
+ gas = ""
52
+ free = ""
53
+ trapped = "."
54
+
55
+
56
+ class Lines(StrEnum):
57
+ dissolved_water = "dash"
58
+ dissolved_oil = "longdash"
59
+ gas = "dot"
60
+ free = "dot"
61
+ trapped = "dashdot"
62
+
63
+
41
64
  _COLOR_ZONES = [
42
65
  "#e91451",
43
66
  "#daa218",
@@ -89,16 +112,13 @@ def _read_dataframe(
89
112
  return df
90
113
 
91
114
 
92
- def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
93
- if split == "containment":
94
- return [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED]
95
- if split == "phase":
96
- if num_cols == 2:
97
- return [_COLOR_GAS, _COLOR_DISSOLVED]
98
- return [_COLOR_FREE, _COLOR_TRAPPED, _COLOR_DISSOLVED]
115
+ def _get_colors(color_options: List[str], split: str) -> List[str]:
116
+ if split in {"containment", "phase"}:
117
+ return [Colors[option] for option in color_options]
99
118
  options = list(_COLOR_ZONES)
100
119
  if split == "region":
101
120
  options.reverse()
121
+ num_cols = len(color_options)
102
122
  if len(options) >= num_cols:
103
123
  return options[:num_cols]
104
124
  num_lengths = int(np.ceil(num_cols / len(options)))
@@ -106,7 +126,8 @@ def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
106
126
  return new_cols[:num_cols]
107
127
 
108
128
 
109
- def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
129
+ def _get_marks(mark_options: List[str], mark_choice: str) -> List[str]:
130
+ num_marks = len(mark_options)
110
131
  if mark_choice == "none":
111
132
  return [""] * num_marks
112
133
  if mark_choice == "containment":
@@ -121,7 +142,7 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
121
142
  )
122
143
  return base_pattern[:num_marks]
123
144
  # mark_choice == "phase":
124
- return ["", "/"] if num_marks == 2 else ["", ".", "/"]
145
+ return [Marks[option] for option in mark_options]
125
146
 
126
147
 
127
148
  def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
@@ -138,7 +159,7 @@ def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
138
159
  )
139
160
  return [options[i % 6] for i in range(len(mark_options))]
140
161
  # mark_choice == "phase":
141
- return ["dot", "dash"] if "gas" in mark_options else ["dot", "dashdot", "dash"]
162
+ return [Lines[option] for option in mark_options]
142
163
 
143
164
 
144
165
  def _prepare_pattern_and_color_options(
@@ -150,10 +171,10 @@ def _prepare_pattern_and_color_options(
150
171
  no_mark = mark_choice == "none"
151
172
  mark_options = [] if no_mark else getattr(containment_info, f"{mark_choice}s")
152
173
  color_options = getattr(containment_info, f"{color_choice}s")
174
+ marks = _get_marks(mark_options, mark_choice)
175
+ colors = _get_colors(color_options, color_choice)
153
176
  num_colors = len(color_options)
154
177
  num_marks = num_colors if no_mark else len(mark_options)
155
- marks = _get_marks(num_marks, mark_choice)
156
- colors = _get_colors(num_colors, color_choice)
157
178
  if no_mark:
158
179
  cat_ord = {"type": color_options}
159
180
  df["type"] = df[color_choice]
@@ -186,7 +207,7 @@ def _prepare_pattern_and_color_options_statistics_plot(
186
207
  num_colors = len(color_options)
187
208
  num_marks = num_colors if no_mark else len(mark_options)
188
209
  line_types = _get_line_types(mark_options, mark_choice)
189
- colors = _get_colors(num_colors, color_choice)
210
+ colors = _get_colors(color_options, color_choice)
190
211
 
191
212
  if mark_choice == "phase":
192
213
  mark_options = ["total"] + mark_options
@@ -273,9 +294,8 @@ def _prepare_line_type_and_color_options(
273
294
  if mark_choice != "none":
274
295
  mark_options = list(getattr(containment_info, f"{mark_choice}s"))
275
296
  color_options = list(getattr(containment_info, f"{color_choice}s"))
276
- num_colors = len(color_options)
277
297
  line_types = _get_line_types(mark_options, mark_choice)
278
- colors = _get_colors(num_colors, color_choice)
298
+ colors = _get_colors(color_options, color_choice)
279
299
 
280
300
  filter_mark = True
281
301
  if mark_choice in ["containment", "phase"]:
@@ -523,8 +543,14 @@ def generate_co2_volume_figure(
523
543
  custom_data=["type", "prop"],
524
544
  )
525
545
  fig.update_traces(
526
- hovertemplate="Type: %{customdata[0]}<br>Amount: %{x:.3f}<br>"
527
- "Realization: %{y}<br>Proportion: %{customdata[1]}<extra></extra>",
546
+ hovertemplate=(
547
+ "<span style='font-family:Courier New;'>"
548
+ "Type : %{customdata[0]}<br>"
549
+ "Amount : %{x:.3f}<br>"
550
+ "Realization: %{y}<br>"
551
+ "Proportion : %{customdata[1]}"
552
+ "</span><extra></extra>"
553
+ ),
528
554
  )
529
555
  if legendonly_traces is not None:
530
556
  _toggle_trace_visibility(fig.data, legendonly_traces)
@@ -577,8 +603,14 @@ def generate_co2_time_containment_one_realization_figure(
577
603
  custom_data=["type", "prop"],
578
604
  )
579
605
  fig.update_traces(
580
- hovertemplate="Type: %{customdata[0]}<br>Date: %{x}<br>"
581
- "Amount: %{y:.3f}<br>Proportion: %{customdata[1]}<extra></extra>",
606
+ hovertemplate=(
607
+ "<span style='font-family:Courier New;'>"
608
+ "Type : %{customdata[0]}<br>"
609
+ "Date : %{x}<br>"
610
+ "Amount : %{y:.3f}<br>"
611
+ "Proportion: %{customdata[1]}"
612
+ "</span><extra></extra>"
613
+ ),
582
614
  )
583
615
  _add_hover_info_in_field(fig, df, cat_ord, colors)
584
616
  fig.layout.yaxis.range = y_limits
@@ -623,7 +655,7 @@ def _add_hover_info_in_field(
623
655
  for date in dates
624
656
  }
625
657
  prev_vals = {date: 0 for date in dates}
626
- date_dict = spaced_dates(dates, 4)
658
+ date_dict = spaced_dates(dates, 4) # type: ignore[arg-type]
627
659
  for name, color in zip(cat_ord["type"], colors):
628
660
  sub_df = df[df["type"] == name]
629
661
  for date in dates:
@@ -640,8 +672,13 @@ def _add_hover_info_in_field(
640
672
  y=y_vals,
641
673
  mode="lines",
642
674
  line=go.scatter.Line(color=color),
643
- text=f"Type: {name}<br>Date: {date_strings[date]}<br>"
644
- f"Amount: {amount:.3f}<br>Proportion: {prop}",
675
+ text=(
676
+ "<span style='font-family:Courier New;'>"
677
+ f"Type : {name}<br>"
678
+ f"Date : {date_strings[date]}<br>"
679
+ f"Amount : {amount:.3f}<br>"
680
+ f"Proportion: {prop}"
681
+ ),
645
682
  opacity=0,
646
683
  hoverinfo="text",
647
684
  hoveron="points",
@@ -768,8 +805,13 @@ def generate_co2_time_containment_figure(
768
805
  fig.add_scatter(y=[0.0], **dummy_args, **args)
769
806
 
770
807
  hover_template = (
771
- "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
772
- "Realization: %{meta[0]}<br>Proportion: %{customdata}"
808
+ "<span style='font-family:Courier New;'>"
809
+ "Type : %{meta[1]}<br>"
810
+ "Date : %{x}<br>"
811
+ "Amount : %{y:.3f}<br>"
812
+ "Realization: %{meta[0]}<br>"
813
+ "Proportion : %{customdata}"
814
+ "</span><extra></extra>"
773
815
  )
774
816
 
775
817
  if containment_info.use_stats:
@@ -795,8 +837,12 @@ def generate_co2_time_containment_figure(
795
837
  )
796
838
  realizations = ["p10", "mean", "p90"] # type: ignore
797
839
  hover_template = (
798
- "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
840
+ "<span style='font-family:Courier New;'>"
841
+ "Type : %{meta[1]}<br>"
842
+ "Date : %{x}<br>"
843
+ "Amount : %{y:.3f}<br>"
799
844
  "Statistic: %{meta[0]}"
845
+ "</span><extra></extra>"
800
846
  )
801
847
  for rlz in realizations:
802
848
  lwd = 1.5 if rlz in ["p10", "p90"] else 2.5
@@ -823,6 +869,9 @@ def generate_co2_time_containment_figure(
823
869
  "meta": [rlz, name],
824
870
  "hovertemplate": hover_template,
825
871
  }
872
+ # Note: The current implementation of CSV export in extract_df_from_fig()
873
+ # uses 'meta' to extract realization number and name.
874
+ # Make sure to keep it in sync when modifying.
826
875
  if not containment_info.use_stats:
827
876
  args["customdata"] = sub_df[sub_df["name"] == name]["prop"]
828
877
  if name in inactive_cols_at_startup:
@@ -859,8 +908,7 @@ def generate_co2_statistics_figure(
859
908
  mark_choice,
860
909
  )
861
910
 
862
- # Remove if we want realization as label?
863
- df = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
911
+ df = df.drop(columns=["REAL"]).reset_index(drop=True)
864
912
  fig = px.ecdf(
865
913
  df,
866
914
  x="amount",
@@ -872,6 +920,7 @@ def generate_co2_statistics_figure(
872
920
  line_dash="type" if mark_choice != "none" else None,
873
921
  line_dash_sequence=line_types,
874
922
  category_orders=cat_ord,
923
+ hover_data=["realization"],
875
924
  )
876
925
 
877
926
  if legend_only_traces is None:
@@ -880,9 +929,18 @@ def generate_co2_statistics_figure(
880
929
  else:
881
930
  _toggle_trace_visibility(fig.data, legend_only_traces)
882
931
 
932
+ # Note: The current implementation of CSV export in extract_df_from_fig()
933
+ # uses the customdata+hovertemplate to extract realization number.
934
+ # Make sure to keep it in sync when modifying.
883
935
  fig.update_traces(
884
- hovertemplate="Type: %{data.name}<br>Amount: %{x:.3f}<br>"
885
- "Probability: %{y:.3f}<extra></extra>",
936
+ hovertemplate=(
937
+ "<span style='font-family:Courier New;'>"
938
+ "Type : %{data.name}<br>"
939
+ "Amount : %{x:.3f}<br>"
940
+ "Probability: %{y:.3f}<br>"
941
+ "Realization: %{customdata[0]}"
942
+ "</span><extra></extra>"
943
+ ),
886
944
  )
887
945
  fig.layout.yaxis.range = [-0.02, 1.02]
888
946
  fig.layout.legend.tracegroupgap = 0
@@ -951,6 +1009,10 @@ def generate_co2_box_plot_figure(
951
1009
  )
952
1010
  )
953
1011
 
1012
+ # Note: The current implementation of CSV export in extract_df_from_fig()
1013
+ # uses the hovertemplate text string to extract the data.
1014
+ # Changing the hovertemplate text string might break the CSV export.
1015
+
954
1016
  fig.add_trace(
955
1017
  go.Bar(
956
1018
  x=[count],
@@ -1081,3 +1143,184 @@ def _toggle_trace_visibility(traces: List, legendonly_names: List[str]) -> None:
1081
1143
  t.visible = "legendonly"
1082
1144
  else:
1083
1145
  t.visible = True
1146
+
1147
+
1148
+ def parse_hover_template(hover_template: str) -> dict:
1149
+ parsed_hover_dict = {}
1150
+ for item in hover_template.split("<br>"):
1151
+ # Remove HTML tags (like <span style='font-family:Courier New;'>)
1152
+ clean_item = re.sub(r"<[^>]*>", "", item)
1153
+ clean_item = clean_item.strip()
1154
+
1155
+ if ":" in clean_item:
1156
+ key, value = clean_item.split(":", 1)
1157
+ key = key.strip()
1158
+ value = value.strip()
1159
+ parsed_hover_dict[key] = value
1160
+ return parsed_hover_dict
1161
+
1162
+
1163
+ def _extract_data_from_box_trace(trace: go.Box) -> dict[str, Any]:
1164
+ if hasattr(trace, "hovertemplate"):
1165
+ hover_dict = parse_hover_template(trace.hovertemplate)
1166
+ trace_name = hover_dict.get("Type", "Unknown")
1167
+ trace_name = trace_name.replace(",", "_").replace(" ", "")
1168
+ return {
1169
+ "type": trace_name,
1170
+ "min": float(hover_dict.get("Min", -1)),
1171
+ "lower_whisker": float(hover_dict.get("Lower whisker", -1)),
1172
+ "p90": float(hover_dict.get("p90 (not shown)", -1)),
1173
+ "q1": float(hover_dict.get("Q1", -1)),
1174
+ "median": float(hover_dict.get("Median", -1)),
1175
+ "q3": float(hover_dict.get("Q3", -1)),
1176
+ "p10": float(hover_dict.get("p10 (not shown)", -1)),
1177
+ "top_whisker": float(hover_dict.get("Top whisker", -1)),
1178
+ "max": float(hover_dict.get("Max", -1)),
1179
+ }
1180
+ return {}
1181
+
1182
+
1183
+ def _extract_data_from_general_trace(
1184
+ trace: Union[go.Box, go.Scatter], plot_choice: str
1185
+ ) -> List[Dict[str, Any]]:
1186
+ records = []
1187
+ if plot_choice == "containment_time_multiple":
1188
+ meta_data = getattr(trace, "meta", None)
1189
+ realization = (
1190
+ meta_data[0] if meta_data is not None and len(meta_data) > 0 else -1
1191
+ )
1192
+ trace_name = (
1193
+ meta_data[1] if meta_data is not None and len(meta_data) > 1 else "Unknown"
1194
+ )
1195
+ else:
1196
+ trace_name = getattr(trace, "name", "Unknown")
1197
+
1198
+ if plot_choice in ["probability", "containment_state"]:
1199
+ if trace.x is not None and "_inputArray" in trace.x:
1200
+ x_data = trace.x["_inputArray"]
1201
+ x_data = {int(k): v for k, v in x_data.items() if str(k).isdigit()}
1202
+ elif plot_choice in [
1203
+ "containment_time_single",
1204
+ "containment_time_multiple",
1205
+ ]:
1206
+ if trace.x is not None:
1207
+ x_data = dict(enumerate(trace.x))
1208
+ if plot_choice in [
1209
+ "probability",
1210
+ "containment_time_single",
1211
+ "containment_time_multiple",
1212
+ ]:
1213
+ if trace.y is not None and "_inputArray" in trace.y:
1214
+ y_data = trace.y["_inputArray"]
1215
+ y_data = {int(k): v for k, v in y_data.items() if str(k).isdigit()}
1216
+ elif plot_choice == "containment_state":
1217
+ if trace.y is not None:
1218
+ y_data = {k: int(v) for k, v in enumerate(trace.y)}
1219
+ xy_data = {k: (x_data[k], y_data[k]) for k in x_data if k in y_data}
1220
+
1221
+ if plot_choice == "probability":
1222
+ custom_data = []
1223
+ if (
1224
+ hasattr(trace, "customdata")
1225
+ and trace.customdata is not None
1226
+ and hasattr(trace, "hovertemplate")
1227
+ and trace.hovertemplate is not None
1228
+ ):
1229
+ match = re.search(r"(\w+)\s*:\s*%\{customdata\[0\]\}", trace.hovertemplate)
1230
+ col_name = match.group(1).lower() if match else "customdata"
1231
+ if col_name == "realization":
1232
+ if "_inputArray" in trace.customdata:
1233
+ custom_data = [x["0"] for x in trace.customdata["_inputArray"]]
1234
+
1235
+ trace_name = trace_name.replace(",", "_").replace(" ", "")
1236
+ for j, (x_val, y_val) in xy_data.items():
1237
+ if plot_choice == "probability":
1238
+ record = {
1239
+ "type": trace_name,
1240
+ "amount": x_val,
1241
+ "probability": y_val,
1242
+ }
1243
+ if custom_data:
1244
+ record["realization"] = custom_data[j]
1245
+ elif plot_choice == "containment_state":
1246
+ record = {
1247
+ "type": trace_name,
1248
+ "amount": x_val,
1249
+ "realization": y_val,
1250
+ }
1251
+ elif plot_choice == "containment_time_single":
1252
+ record = {
1253
+ "type": trace_name,
1254
+ "date": x_val,
1255
+ "amount": y_val,
1256
+ }
1257
+ elif plot_choice == "containment_time_multiple":
1258
+ record = {
1259
+ "type": trace_name,
1260
+ "date": x_val,
1261
+ "amount": y_val,
1262
+ }
1263
+ if realization in ["p10", "p90", "mean"]:
1264
+ col_name = "statistic"
1265
+ else:
1266
+ col_name = "realization"
1267
+ record[col_name] = realization
1268
+ records.append(record)
1269
+ return records
1270
+
1271
+
1272
+ def extract_df_from_fig(fig_data: tuple, plot_choice: str) -> pd.DataFrame:
1273
+ if plot_choice == "containment_time":
1274
+ # Distinguish between single and multiple realizations selected:
1275
+ if hasattr(fig_data[0], "stackgroup") and fig_data[0].stackgroup is not None:
1276
+ plot_choice = "containment_time_single"
1277
+ else:
1278
+ plot_choice = "containment_time_multiple"
1279
+
1280
+ data_records = []
1281
+ for trace in fig_data:
1282
+ if hasattr(trace, "visible") and trace.visible == "legendonly":
1283
+ continue # Skip hidden traces
1284
+ if plot_choice == "containment_time_multiple":
1285
+ is_data_point_trace = (
1286
+ hasattr(trace, "showlegend")
1287
+ and trace.showlegend is False
1288
+ and hasattr(trace, "name")
1289
+ and trace.name == ""
1290
+ )
1291
+ if not is_data_point_trace:
1292
+ # Only keep subset of traces that we want
1293
+ continue
1294
+ elif plot_choice == "containment_time_single":
1295
+ is_line_trace = (
1296
+ hasattr(trace, "showlegend")
1297
+ and trace.showlegend is False
1298
+ and hasattr(trace, "mode")
1299
+ and trace.mode == "lines"
1300
+ )
1301
+ if is_line_trace:
1302
+ continue
1303
+ elif plot_choice == "box":
1304
+ is_invisible_trace = (
1305
+ hasattr(trace, "showlegend")
1306
+ and trace.showlegend is False
1307
+ and hasattr(trace, "opacity")
1308
+ and trace.opacity == 0
1309
+ )
1310
+ if not is_invisible_trace:
1311
+ # Keep only the invisible box traces
1312
+ # They have all the data stored in the hover box,
1313
+ # while the visible traces only got lower/upper whisker and median
1314
+ continue
1315
+
1316
+ if plot_choice == "box":
1317
+ record = _extract_data_from_box_trace(trace)
1318
+ if record:
1319
+ data_records.append(record)
1320
+ else:
1321
+ if hasattr(trace, "x") and hasattr(trace, "y"):
1322
+ records = _extract_data_from_general_trace(trace, plot_choice)
1323
+ if records:
1324
+ data_records += records
1325
+
1326
+ return pd.DataFrame(data_records)
@@ -6,7 +6,7 @@ from webviz_subsurface.plugins._map_viewer_fmu.color_tables import default_color
6
6
  ColorTables = List[Dict[str, Any]]
7
7
 
8
8
 
9
- def co2leakage_color_tables() -> ColorTables:
9
+ def co2migration_color_tables() -> ColorTables:
10
10
  tables = default_color_tables + uniform_color_tables()
11
11
  rev_tables = _reversed_color_tables(tables)
12
12
  return [t for tup in zip(tables, rev_tables) for t in tup]
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
3
3
  import pandas as pd
4
4
 
5
5
  from webviz_subsurface._providers import EnsembleTableProvider
6
- from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
6
+ from webviz_subsurface.plugins._co2_migration._utilities.generic import (
7
7
  Co2MassScale,
8
8
  Co2VolumeScale,
9
9
  MenuOptions,
@@ -105,10 +105,12 @@ class ContainmentDataProvider:
105
105
 
106
106
  plume_groups = sorted(plume_groups, key=plume_sort_key)
107
107
 
108
+ phases = ["total", "gas", "dissolved_water"]
108
109
  if "free_gas" in list(df["phase"]):
109
- phases = ["total", "free_gas", "trapped_gas", "dissolved"]
110
- else:
111
- phases = ["total", "gas", "dissolved"]
110
+ idx = phases.index("gas")
111
+ phases = phases[:idx] + ["free_gas", "trapped_gas"] + phases[idx + 1 :]
112
+ if "dissolved_oil" in list(df["phase"]):
113
+ phases.append("dissolved_oil")
112
114
 
113
115
  dates = df["date"].unique()
114
116
  dates.sort()
@@ -9,6 +9,12 @@ class StatisticsTabOption(StrEnum):
9
9
  BOX_PLOT = "box-plot"
10
10
 
11
11
 
12
+ class MainTabOption(StrEnum):
13
+ CONTAINMENT_STATE = "containment-state"
14
+ CONTAINMENT_OVER_TIME = "containment-over-time"
15
+ STATISTICS = "statistics"
16
+
17
+
12
18
  # pylint: disable=too-many-instance-attributes
13
19
  @dataclass(frozen=True) # NBNB-AS: Removed slots=True (python>=3.10)
14
20
  class ContainmentInfo:
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
6
  from webviz_subsurface._utils.webvizstore_functions import read_csv
7
- from webviz_subsurface.plugins._co2_leakage._utilities._misc import realization_paths
7
+ from webviz_subsurface.plugins._co2_migration._utilities._misc import realization_paths
8
8
  from webviz_subsurface.plugins._map_viewer_fmu._tmp_well_pick_provider import (
9
9
  WellPickProvider,
10
10
  )