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
@@ -1,3 +1,5 @@
1
+ # pylint: disable=too-many-lines
2
+ # NBNB-AS: We should address this pylint message soon
1
3
  import warnings
2
4
  from datetime import datetime as dt
3
5
  from typing import Any, Dict, List, Optional, Tuple, Union
@@ -9,6 +11,12 @@ import plotly.graph_objects as go
9
11
 
10
12
  from webviz_subsurface._providers import EnsembleTableProvider
11
13
  from webviz_subsurface._utils.enum_shim import StrEnum
14
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
15
+ ContainmentDataProvider,
16
+ )
17
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_info import (
18
+ ContainmentInfo,
19
+ )
12
20
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
13
21
  Co2MassScale,
14
22
  Co2VolumeScale,
@@ -26,6 +34,10 @@ _COLOR_TOTAL = "#222222"
26
34
  _COLOR_CONTAINED = "#00aa00"
27
35
  _COLOR_OUTSIDE = "#006ddd"
28
36
  _COLOR_HAZARDOUS = "#dd4300"
37
+ _COLOR_DISSOLVED = "#208eb7"
38
+ _COLOR_GAS = "#C41E3A"
39
+ _COLOR_FREE = "#FF2400"
40
+ _COLOR_TRAPPED = "#880808"
29
41
  _COLOR_ZONES = [
30
42
  "#e91451",
31
43
  "#daa218",
@@ -41,6 +53,29 @@ _COLOR_ZONES = [
41
53
  "#34b36f",
42
54
  ]
43
55
 
56
+ _LIGHTER_COLORS = {
57
+ "black": "#909090",
58
+ "#222222": "#909090",
59
+ "#00aa00": "#55ff55",
60
+ "#006ddd": "#6eb6ff",
61
+ "#dd4300": "#ff9a6e",
62
+ "#e91451": "#f589a8",
63
+ "#daa218": "#f2d386",
64
+ "#208eb7": "#81cde9",
65
+ "#84bc04": "#cdfc63",
66
+ "#b74532": "#e19e92",
67
+ "#9a89b4": "#ccc4d9",
68
+ "#8d30ba": "#c891e3",
69
+ "#256b33": "#77d089",
70
+ "#95704d": "#cfb7a1",
71
+ "#1357ca": "#7ba7f3",
72
+ "#f75ef0": "#fbaef7",
73
+ "#34b36f": "#93e0b7",
74
+ "#C41E3A": "#E42E5A",
75
+ "#FF2400": "#FF7430",
76
+ "#880808": "#C84848",
77
+ }
78
+
44
79
 
45
80
  def _read_dataframe(
46
81
  table_provider: EnsembleTableProvider,
@@ -54,43 +89,13 @@ def _read_dataframe(
54
89
  return df
55
90
 
56
91
 
57
- def read_menu_options(
58
- table_provider: EnsembleTableProvider,
59
- realization: int,
60
- relpath: str,
61
- ) -> Dict[str, List[str]]:
62
- col_names = table_provider.column_names()
63
- df = table_provider.get_column_data(col_names, [realization])
64
- required_columns = ["date", "amount", "phase", "containment", "zone", "region"]
65
- missing_columns = [col for col in required_columns if col not in col_names]
66
- if len(missing_columns) > 0:
67
- raise KeyError(
68
- f"Missing expected columns {', '.join(missing_columns)} in {relpath}"
69
- f" in realization {realization} (and possibly other csv-files). "
70
- f"Provided files are likely from an old version of ccs-scripts."
71
- )
72
- zones = ["all"]
73
- for zone in list(df["zone"]):
74
- if zone not in zones:
75
- zones.append(zone)
76
- regions = ["all"]
77
- for region in list(df["region"]):
78
- if region not in regions:
79
- regions.append(region)
80
- if "free_gas" in list(df["phase"]):
81
- phases = ["total", "free_gas", "trapped_gas", "aqueous"]
82
- else:
83
- phases = ["total", "gas", "aqueous"]
84
- return {
85
- "zones": zones if len(zones) > 1 else [],
86
- "regions": regions if len(regions) > 1 else [],
87
- "phases": phases,
88
- }
89
-
90
-
91
92
  def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
92
93
  if split == "containment":
93
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]
94
99
  options = list(_COLOR_ZONES)
95
100
  if split == "region":
96
101
  options.reverse()
@@ -106,7 +111,7 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
106
111
  return [""] * num_marks
107
112
  if mark_choice == "containment":
108
113
  return ["x", "/", ""]
109
- if mark_choice in ["zone", "region"]:
114
+ if mark_choice in ["zone", "region", "plume_group"]:
110
115
  base_pattern = ["", "/", "x", "-", "\\", "+", "|", "."]
111
116
  if num_marks > len(base_pattern):
112
117
  base_pattern *= int(np.ceil(num_marks / len(base_pattern)))
@@ -115,6 +120,7 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
115
120
  f"Some {mark_choice}s will share pattern."
116
121
  )
117
122
  return base_pattern[:num_marks]
123
+ # mark_choice == "phase":
118
124
  return ["", "/"] if num_marks == 2 else ["", ".", "/"]
119
125
 
120
126
 
@@ -123,36 +129,37 @@ def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
123
129
  return ["solid"]
124
130
  if mark_choice == "containment":
125
131
  return ["dash", "dot", "solid"]
126
- if mark_choice in ["zone", "region"]:
127
- if len(mark_options) > 8:
132
+ if mark_choice in ["zone", "region", "plume_group"]:
133
+ options = ["solid", "dash", "dot", "dashdot", "longdash", "longdashdot"]
134
+ if len(mark_options) > 6:
128
135
  warnings.warn(
129
136
  f"Large number of {mark_choice}s might make it hard "
130
137
  f"to distinguish different dashed lines."
131
138
  )
132
- return [
133
- f"{round(i / len(mark_options) * 25)}px" for i in range(len(mark_options))
134
- ]
139
+ return [options[i % 6] for i in range(len(mark_options))]
140
+ # mark_choice == "phase":
135
141
  return ["dot", "dash"] if "gas" in mark_options else ["dot", "dashdot", "dash"]
136
142
 
137
143
 
138
144
  def _prepare_pattern_and_color_options(
139
145
  df: pd.DataFrame,
140
- containment_info: Dict,
146
+ containment_info: ContainmentInfo,
141
147
  color_choice: str,
142
148
  mark_choice: str,
143
149
  ) -> Tuple[Dict, List, List]:
144
- mark_options = [] if mark_choice == "none" else containment_info[f"{mark_choice}s"]
145
- color_options = containment_info[f"{color_choice}s"]
150
+ no_mark = mark_choice == "none"
151
+ mark_options = [] if no_mark else getattr(containment_info, f"{mark_choice}s")
152
+ color_options = getattr(containment_info, f"{color_choice}s")
146
153
  num_colors = len(color_options)
147
- num_marks = num_colors if mark_choice == "none" else len(mark_options)
154
+ num_marks = num_colors if no_mark else len(mark_options)
148
155
  marks = _get_marks(num_marks, mark_choice)
149
156
  colors = _get_colors(num_colors, color_choice)
150
- if mark_choice == "none":
157
+ if no_mark:
151
158
  cat_ord = {"type": color_options}
152
159
  df["type"] = df[color_choice]
153
160
  return cat_ord, colors, marks
154
161
  df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
155
- if containment_info["sorting"] == "color":
162
+ if containment_info.sorting == "color":
156
163
  cat_ord = {
157
164
  "type": [", ".join((c, m)) for c in color_options for m in mark_options],
158
165
  }
@@ -167,25 +174,115 @@ def _prepare_pattern_and_color_options(
167
174
  return cat_ord, colors, marks
168
175
 
169
176
 
177
+ def _prepare_pattern_and_color_options_statistics_plot(
178
+ df: pd.DataFrame,
179
+ containment_info: ContainmentInfo,
180
+ color_choice: str,
181
+ mark_choice: str,
182
+ ) -> Tuple[Dict, List, List]:
183
+ no_mark = mark_choice == "none"
184
+ mark_options = [] if no_mark else getattr(containment_info, f"{mark_choice}s")
185
+ color_options = getattr(containment_info, f"{color_choice}s")
186
+ num_colors = len(color_options)
187
+ num_marks = num_colors if no_mark else len(mark_options)
188
+ line_types = _get_line_types(mark_options, mark_choice)
189
+ colors = _get_colors(num_colors, color_choice)
190
+
191
+ if mark_choice == "phase":
192
+ mark_options = ["total"] + mark_options
193
+ line_types = ["solid"] + line_types
194
+ num_marks += 1
195
+ if color_choice in ["containment", "phase"]:
196
+ color_options = ["total"] + color_options
197
+ colors = ["black"] + colors
198
+ num_colors += 1
199
+
200
+ filter_mark = mark_choice != "phase"
201
+ filter_color = color_choice not in ["phase", "containment"]
202
+ _filter_rows(df, color_choice, mark_choice, filter_mark, filter_color)
203
+
204
+ if no_mark:
205
+ cat_ord = {"type": color_options}
206
+ df["type"] = df[color_choice]
207
+ return cat_ord, colors, line_types
208
+ df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
209
+
210
+ if containment_info.sorting == "color":
211
+ cat_ord = {
212
+ "type": [", ".join((c, m)) for c in color_options for m in mark_options],
213
+ }
214
+ colors = [c for c in colors for _ in range(num_marks)]
215
+ line_types = line_types * num_colors
216
+ else:
217
+ cat_ord = {
218
+ "type": [", ".join((c, m)) for m in mark_options for c in color_options],
219
+ }
220
+ colors = colors * num_marks
221
+ line_types = [m for m in line_types for _ in range(num_colors)]
222
+
223
+ for m in mark_options + ["total", "all"]:
224
+ df["type"] = df["type"].replace(f"total, {m}", m)
225
+ df["type"] = df["type"].replace(f"all, {m}", m)
226
+ for m in color_options:
227
+ df["type"] = df["type"].replace(f"{m}, total", m)
228
+ df["type"] = df["type"].replace(f"{m}, all", m)
229
+ cat_ord["type"] = [
230
+ label.replace("total, ", "") if "total, " in label else label
231
+ for label in cat_ord["type"]
232
+ ]
233
+ cat_ord["type"] = [
234
+ label.replace("all, ", "") if "all, " in label else label
235
+ for label in cat_ord["type"]
236
+ ]
237
+ cat_ord["type"] = [
238
+ label.replace(", total", "") if ", total" in label else label
239
+ for label in cat_ord["type"]
240
+ ]
241
+ cat_ord["type"] = [
242
+ label.replace(", all", "") if ", all" in label else label
243
+ for label in cat_ord["type"]
244
+ ]
245
+
246
+ return cat_ord, colors, line_types
247
+
248
+
249
+ def _find_default_legendonly(df: pd.DataFrame, categories: List[str]) -> List[str]:
250
+ if "hazardous" in categories:
251
+ default_option = "hazardous"
252
+ else:
253
+ max_value = -999.9
254
+ default_option = categories[0]
255
+ for category in categories:
256
+ df_filtered = df[df["type"] == category]
257
+ if df_filtered["amount"].max() > max_value:
258
+ max_value = df_filtered["amount"].max()
259
+ default_option = category
260
+
261
+ # The default list should contain all categories HIDDEN in the legend, so we need
262
+ # to create a copy of the list with default_option excluded instead.
263
+ return [c for c in categories if c != default_option]
264
+
265
+
170
266
  def _prepare_line_type_and_color_options(
171
267
  df: pd.DataFrame,
172
- containment_info: Dict,
268
+ containment_info: ContainmentInfo,
173
269
  color_choice: str,
174
270
  mark_choice: str,
175
271
  ) -> pd.DataFrame:
176
272
  mark_options = []
177
273
  if mark_choice != "none":
178
- mark_options = list(containment_info[f"{mark_choice}s"])
179
- color_options = list(containment_info[f"{color_choice}s"])
274
+ mark_options = list(getattr(containment_info, f"{mark_choice}s"))
275
+ color_options = list(getattr(containment_info, f"{color_choice}s"))
180
276
  num_colors = len(color_options)
181
277
  line_types = _get_line_types(mark_options, mark_choice)
182
278
  colors = _get_colors(num_colors, color_choice)
279
+
183
280
  filter_mark = True
184
- if mark_choice == "phase":
281
+ if mark_choice in ["containment", "phase"]:
185
282
  mark_options = ["total"] + mark_options
186
283
  line_types = ["solid"] + line_types
187
284
  filter_mark = False
188
- if color_choice == "containment":
285
+ if color_choice in ["containment", "phase"]:
189
286
  color_options = ["total"] + color_options
190
287
  colors = ["black"] + colors
191
288
  else:
@@ -201,7 +298,7 @@ def _prepare_line_type_and_color_options(
201
298
  )
202
299
  df["name"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
203
300
  _change_names(df, color_options, mark_options)
204
- if containment_info["sorting"] == "color":
301
+ if containment_info.sorting == "color":
205
302
  options = pd.DataFrame(
206
303
  {
207
304
  "name": [
@@ -225,25 +322,11 @@ def _prepare_line_type_and_color_options(
225
322
  return options
226
323
 
227
324
 
228
- def _find_scale_factor(
229
- table_provider: EnsembleTableProvider,
230
- scale: Union[Co2MassScale, Co2VolumeScale],
231
- ) -> float:
232
- if scale in (Co2MassScale.KG, Co2VolumeScale.CUBIC_METERS):
233
- return 1.0
234
- if scale in (Co2MassScale.MTONS, Co2VolumeScale.BILLION_CUBIC_METERS):
235
- return 1e9
236
- if scale in (Co2MassScale.NORMALIZE, Co2VolumeScale.NORMALIZE):
237
- df = table_provider.get_column_data(table_provider.column_names())
238
- return df["total"].max()
239
- return 1.0
240
-
241
-
242
325
  def _read_terminal_co2_volumes(
243
- table_provider: EnsembleTableProvider,
326
+ table_provider: ContainmentDataProvider,
244
327
  realizations: List[int],
245
328
  scale: Union[Co2MassScale, Co2VolumeScale],
246
- containment_info: Dict[str, Union[str, None, List[str]]],
329
+ containment_info: ContainmentInfo,
247
330
  ) -> pd.DataFrame:
248
331
  records: Dict[str, List[Any]] = {
249
332
  "real": [],
@@ -251,18 +334,17 @@ def _read_terminal_co2_volumes(
251
334
  "sort_key": [],
252
335
  "sort_key_secondary": [],
253
336
  }
254
- color_choice = containment_info["color_choice"]
255
- mark_choice = containment_info["mark_choice"]
337
+ color_choice = containment_info.color_choice
338
+ mark_choice = containment_info.mark_choice
256
339
  assert isinstance(color_choice, str)
257
340
  assert isinstance(mark_choice, str)
258
341
  records[color_choice] = []
259
342
  if mark_choice != "none":
260
343
  records[mark_choice] = []
261
- scale_factor = _find_scale_factor(table_provider, scale)
262
344
  data_frame = None
263
345
  for real in realizations:
264
- df = _read_dataframe(table_provider, real, scale_factor)
265
- df = df[df["date"] == np.max(df["date"])]
346
+ df = table_provider.extract_dataframe(real, scale)
347
+ df = df[df["date"] == containment_info.date_option]
266
348
  _add_sort_key_and_real(df, str(real), containment_info)
267
349
  _filter_columns(df, color_choice, mark_choice, containment_info)
268
350
  _filter_rows(df, color_choice, mark_choice)
@@ -281,15 +363,15 @@ def _filter_columns(
281
363
  df: pd.DataFrame,
282
364
  color_choice: str,
283
365
  mark_choice: str,
284
- containment_info: Dict,
366
+ containment_info: ContainmentInfo,
285
367
  ) -> None:
286
368
  filter_columns = [
287
369
  col
288
- for col in ["phase", "containment", "zone", "region"]
370
+ for col in ["phase", "containment", "zone", "region", "plume_group"]
289
371
  if col not in [mark_choice, color_choice]
290
372
  ]
291
373
  for col in filter_columns:
292
- df.query(f'{col} == "{containment_info[col]}"', inplace=True)
374
+ df.query(f'{col} == "{getattr(containment_info, col)}"', inplace=True)
293
375
  df.drop(columns=filter_columns, inplace=True)
294
376
 
295
377
 
@@ -298,8 +380,10 @@ def _filter_rows(
298
380
  color_choice: str,
299
381
  mark_choice: str,
300
382
  filter_mark: bool = True,
383
+ filter_color: bool = True,
301
384
  ) -> None:
302
- df.query(f'{color_choice} not in ["total", "all"]', inplace=True)
385
+ if filter_color:
386
+ df.query(f'{color_choice} not in ["total", "all"]', inplace=True)
303
387
  if mark_choice != "none" and filter_mark:
304
388
  df.query(f'{mark_choice} not in ["total", "all"]', inplace=True)
305
389
 
@@ -307,22 +391,24 @@ def _filter_rows(
307
391
  def _add_sort_key_and_real(
308
392
  df: pd.DataFrame,
309
393
  label: str,
310
- containment_info: Dict,
394
+ containment_info: ContainmentInfo,
311
395
  ) -> None:
312
396
  sort_value = np.sum(
313
397
  df[
314
398
  (df["phase"] == "total")
315
399
  & (df["containment"] == "hazardous")
316
- & (df["zone"] == containment_info["zone"])
317
- & (df["region"] == containment_info["region"])
400
+ & (df["zone"] == containment_info.zone)
401
+ & (df["region"] == containment_info.region)
402
+ & (df["plume_group"] == containment_info.plume_group)
318
403
  ]["amount"]
319
404
  )
320
405
  sort_value_secondary = np.sum(
321
406
  df[
322
407
  (df["phase"] == "total")
323
408
  & (df["containment"] == "outside")
324
- & (df["zone"] == containment_info["zone"])
325
- & (df["region"] == containment_info["region"])
409
+ & (df["zone"] == containment_info.zone)
410
+ & (df["region"] == containment_info.region)
411
+ & (df["plume_group"] == containment_info.plume_group)
326
412
  ]["amount"]
327
413
  )
328
414
  df["real"] = [label] * df.shape[0]
@@ -331,14 +417,13 @@ def _add_sort_key_and_real(
331
417
 
332
418
 
333
419
  def _read_co2_volumes(
334
- table_provider: EnsembleTableProvider,
420
+ table_provider: ContainmentDataProvider,
335
421
  realizations: List[int],
336
422
  scale: Union[Co2MassScale, Co2VolumeScale],
337
423
  ) -> pd.DataFrame:
338
- scale_factor = _find_scale_factor(table_provider, scale)
339
424
  return pd.concat(
340
425
  [
341
- _read_dataframe(table_provider, r, scale_factor).assign(realization=r)
426
+ table_provider.extract_dataframe(r, scale).assign(realization=r)
342
427
  for r in realizations
343
428
  ]
344
429
  )
@@ -357,15 +442,20 @@ def _change_names(
357
442
  df["name"] = df["name"].replace(f"{m}, all", m)
358
443
 
359
444
 
360
- def _adjust_figure(fig: go.Figure) -> None:
445
+ def _adjust_figure(fig: go.Figure, plot_title: str) -> None:
361
446
  fig.layout.legend.orientation = "v"
362
447
  fig.layout.legend.title.text = ""
363
448
  fig.layout.legend.itemwidth = 40
364
449
  fig.layout.xaxis.exponentformat = "power"
365
- fig.layout.title.x = 0.5
450
+
451
+ fig.layout.title.text = plot_title
452
+ fig.layout.title.font = {"size": 14}
453
+ fig.layout.margin.t = 40
454
+ fig.layout.title.y = 0.95
455
+ fig.layout.title.x = 0.4
456
+
366
457
  fig.layout.paper_bgcolor = "rgba(0,0,0,0)"
367
458
  fig.layout.margin.b = 6
368
- fig.layout.margin.t = 15
369
459
  fig.layout.margin.l = 10
370
460
  fig.layout.margin.r = 10
371
461
  fig.update_layout(
@@ -402,16 +492,17 @@ def _add_prop_to_df(
402
492
 
403
493
 
404
494
  def generate_co2_volume_figure(
405
- table_provider: EnsembleTableProvider,
495
+ table_provider: ContainmentDataProvider,
406
496
  realizations: List[int],
407
497
  scale: Union[Co2MassScale, Co2VolumeScale],
408
- containment_info: Dict[str, Any],
498
+ containment_info: ContainmentInfo,
499
+ legendonly_traces: Optional[List[str]],
409
500
  ) -> go.Figure:
410
501
  df = _read_terminal_co2_volumes(
411
502
  table_provider, realizations, scale, containment_info
412
503
  )
413
- color_choice = containment_info["color_choice"]
414
- mark_choice = containment_info["mark_choice"]
504
+ color_choice = containment_info.color_choice
505
+ mark_choice = containment_info.mark_choice
415
506
  _add_prop_to_df(df, [str(r) for r in realizations], "real")
416
507
  cat_ord, colors, marks = _prepare_pattern_and_color_options(
417
508
  df,
@@ -429,28 +520,34 @@ def generate_co2_volume_figure(
429
520
  pattern_shape_sequence=marks,
430
521
  orientation="h",
431
522
  category_orders=cat_ord,
432
- hover_data={"prop": True, "real": False},
523
+ custom_data=["type", "prop"],
524
+ )
525
+ fig.update_traces(
526
+ hovertemplate="Type: %{customdata[0]}<br>Amount: %{x:.3f}<br>"
527
+ "Realization: %{y}<br>Proportion: %{customdata[1]}<extra></extra>",
433
528
  )
529
+ if legendonly_traces is not None:
530
+ _toggle_trace_visibility(fig.data, legendonly_traces)
434
531
  fig.layout.yaxis.title = "Realization"
435
532
  fig.layout.xaxis.title = scale.value
436
- _adjust_figure(fig)
533
+ _adjust_figure(fig, plot_title=_make_title(containment_info))
437
534
  return fig
438
535
 
439
536
 
440
537
  # pylint: disable=too-many-locals
441
538
  def generate_co2_time_containment_one_realization_figure(
442
- table_provider: EnsembleTableProvider,
539
+ table_provider: ContainmentDataProvider,
443
540
  scale: Union[Co2MassScale, Co2VolumeScale],
444
541
  time_series_realization: int,
445
542
  y_limits: List[Optional[float]],
446
- containment_info: Dict[str, Any],
543
+ containment_info: ContainmentInfo,
447
544
  ) -> go.Figure:
448
545
  df = _read_co2_volumes(table_provider, [time_series_realization], scale)
449
- color_choice = containment_info["color_choice"]
450
- mark_choice = containment_info["mark_choice"]
546
+ color_choice = containment_info.color_choice
547
+ mark_choice = containment_info.mark_choice
451
548
  _filter_columns(df, color_choice, mark_choice, containment_info)
452
549
  _filter_rows(df, color_choice, mark_choice)
453
- if containment_info["sorting"] == "marking" and mark_choice != "none":
550
+ if containment_info.sorting == "marking" and mark_choice != "none":
454
551
  sort_order = ["date", mark_choice]
455
552
  else:
456
553
  sort_order = ["date", color_choice]
@@ -477,16 +574,17 @@ def generate_co2_time_containment_one_realization_figure(
477
574
  pattern_shape_sequence=marks,
478
575
  category_orders=cat_ord,
479
576
  range_y=y_limits,
480
- hover_data={
481
- "prop": True,
482
- "amount": ":.3f",
483
- },
577
+ custom_data=["type", "prop"],
578
+ )
579
+ fig.update_traces(
580
+ hovertemplate="Type: %{customdata[0]}<br>Date: %{x}<br>"
581
+ "Amount: %{y:.3f}<br>Proportion: %{customdata[1]}<extra></extra>",
484
582
  )
485
583
  _add_hover_info_in_field(fig, df, cat_ord, colors)
486
584
  fig.layout.yaxis.range = y_limits
487
585
  fig.layout.xaxis.title = "Time"
488
586
  fig.layout.yaxis.title = scale.value
489
- _adjust_figure(fig)
587
+ _adjust_figure(fig, plot_title=_make_title(containment_info, include_date=False))
490
588
  return fig
491
589
 
492
590
 
@@ -535,15 +633,15 @@ def _add_hover_info_in_field(
535
633
  p15 = prev_val + 0.15 * amount
536
634
  p85 = prev_val + 0.85 * amount
537
635
  y_vals = np.linspace(p15, p85, 8).tolist() * len(date_dict[date])
538
- y_vals.sort()
636
+ y_vals.sort() # type: ignore[attr-defined]
539
637
  fig.add_trace(
540
638
  go.Scatter(
541
639
  x=date_dict[date] * 8,
542
640
  y=y_vals,
543
641
  mode="lines",
544
642
  line=go.scatter.Line(color=color),
545
- text=f"type={name}<br>date={date_strings[date]}<br>"
546
- f"amount={amount:.3f}<br>prop={prop}",
643
+ text=f"Type: {name}<br>Date: {date_strings[date]}<br>"
644
+ f"Amount: {amount:.3f}<br>Proportion: {prop}",
547
645
  opacity=0,
548
646
  hoverinfo="text",
549
647
  hoveron="points",
@@ -553,23 +651,106 @@ def _add_hover_info_in_field(
553
651
  prev_vals[date] = prev_val + amount
554
652
 
555
653
 
556
- # pylint: disable=too-many-locals
654
+ def _connect_plume_groups(
655
+ df: pd.DataFrame,
656
+ color_choice: str,
657
+ mark_choice: str,
658
+ ) -> None:
659
+ col_list = ["realization"]
660
+ if color_choice == "plume_group" and mark_choice != "none":
661
+ col_list.append(mark_choice)
662
+ elif mark_choice == "plume_group":
663
+ col_list.append(color_choice)
664
+
665
+ cols: Union[List[str], str] = col_list
666
+ if len(col_list) == 1:
667
+ cols = col_list[0]
668
+ # Find points where plumes start or end, to connect the lines
669
+ end_points = []
670
+ start_points = []
671
+ for plume_name, df_sub in df.groupby("plume_group"):
672
+ if plume_name == "undetermined":
673
+ continue
674
+ for _, df_sub2 in df_sub.groupby(cols):
675
+ # Assumes the data frame is sorted on date
676
+ mask_end = (
677
+ (df_sub2["amount"] == 0.0)
678
+ & (df_sub2["amount"].shift(1) > 0.0)
679
+ & (df_sub2.index > 0)
680
+ )
681
+ mask_start = (
682
+ (df_sub2["amount"] > 0.0)
683
+ & (df_sub2["amount"].shift(1) == 0.0)
684
+ & (df_sub2.index > 0)
685
+ )
686
+ first_index_end = mask_end.idxmax() if mask_end.any() else None
687
+ first_index_start = mask_start.idxmax() if mask_start.any() else None
688
+ transition_row_end = (
689
+ df_sub2.loc[first_index_end] if first_index_end is not None else None
690
+ )
691
+ transition_row_start = (
692
+ df_sub2.loc[first_index_start]
693
+ if first_index_start is not None
694
+ else None
695
+ )
696
+ if transition_row_end is not None:
697
+ end_points.append(transition_row_end)
698
+ # Replace 0 with np.nan for all dates after this
699
+ date = str(transition_row_end["date"])
700
+ df.loc[
701
+ (df["plume_group"] == plume_name)
702
+ & (df["amount"] == 0.0)
703
+ & (df["date"] > date),
704
+ "amount",
705
+ ] = np.nan
706
+ if transition_row_start is not None:
707
+ start_points.append(transition_row_start)
708
+ for end_point in end_points:
709
+ plume1 = end_point["plume_group"]
710
+ row1 = end_point.drop(["amount", "plume_group", "name"])
711
+ for start_point in start_points:
712
+ plume2 = start_point["plume_group"]
713
+ if plume1 in plume2 and len(plume1) < len(plume2):
714
+ row2 = start_point.drop(["amount", "plume_group", "name"])
715
+ if row1.equals(row2):
716
+ row_to_change = df.eq(end_point).all(axis=1)
717
+ if sum(row_to_change) == 1:
718
+ df.loc[row_to_change, "amount"] = start_point["amount"]
719
+ df["is_merged"] = ["+" in x for x in df["plume_group"].values]
720
+ df.loc[
721
+ (df["plume_group"] != "all") & (df["is_merged"]) & (df["amount"] == 0.0),
722
+ "amount",
723
+ ] = np.nan
724
+ df.drop(columns="is_merged", inplace=True)
725
+
726
+
727
+ # pylint: disable=too-many-locals, too-many-statements
557
728
  def generate_co2_time_containment_figure(
558
- table_provider: EnsembleTableProvider,
729
+ table_provider: ContainmentDataProvider,
559
730
  realizations: List[int],
560
731
  scale: Union[Co2MassScale, Co2VolumeScale],
561
- containment_info: Dict[str, Any],
732
+ containment_info: ContainmentInfo,
733
+ legendonly_traces: Optional[List[str]],
562
734
  ) -> go.Figure:
563
735
  df = _read_co2_volumes(table_provider, realizations, scale)
564
- color_choice = containment_info["color_choice"]
565
- mark_choice = containment_info["mark_choice"]
736
+ color_choice = containment_info.color_choice
737
+ mark_choice = containment_info.mark_choice
566
738
  _filter_columns(df, color_choice, mark_choice, containment_info)
567
739
  options = _prepare_line_type_and_color_options(
568
740
  df, containment_info, color_choice, mark_choice
569
741
  )
570
- active_cols_at_startup = list(
571
- options[options["line_type"].isin(["solid", "0px"])]["name"]
572
- )
742
+ if legendonly_traces is None:
743
+ inactive_cols_at_startup = list(
744
+ options[~(options["line_type"].isin(["solid", "0px"]))]["name"]
745
+ )
746
+ else:
747
+ inactive_cols_at_startup = legendonly_traces
748
+ if "plume_group" in df:
749
+ try:
750
+ _connect_plume_groups(df, color_choice, mark_choice)
751
+ except ValueError:
752
+ pass
753
+
573
754
  fig = go.Figure()
574
755
  # Generate dummy scatters for legend entries
575
756
  dummy_args = {"x": df["date"], "mode": "lines", "hoverinfo": "none"}
@@ -582,32 +763,69 @@ def generate_co2_time_containment_figure(
582
763
  "legendgroup": name,
583
764
  "name": name,
584
765
  }
585
- if name not in active_cols_at_startup:
766
+ if name in inactive_cols_at_startup:
586
767
  args["visible"] = "legendonly"
587
768
  fig.add_scatter(y=[0.0], **dummy_args, **args)
588
- for rlz in realizations:
589
- sub_df = df[df["realization"] == rlz].copy().reset_index()
590
- _add_prop_to_df(
591
- sub_df, np.unique(df["date"]), "date", [color_choice, mark_choice]
769
+
770
+ hover_template = (
771
+ "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
772
+ "Realization: %{meta[0]}<br>Proportion: %{customdata}"
773
+ )
774
+
775
+ if containment_info.use_stats:
776
+ df_no_real = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
777
+ if mark_choice == "none":
778
+ df_grouped = df_no_real.groupby(
779
+ ["date", "name", color_choice], as_index=False
780
+ )
781
+ else:
782
+ df_grouped = df_no_real.groupby(
783
+ ["date", "name", color_choice, mark_choice], as_index=False
784
+ )
785
+ df_mean = df_grouped.agg("mean")
786
+ df_mean["realization"] = ["mean"] * df_mean.shape[0]
787
+ df_p10 = df_grouped.agg(lambda x: np.quantile(x, 0.9))
788
+ df_p10["realization"] = ["p10"] * df_p10.shape[0]
789
+ df_p90 = df_grouped.agg(lambda x: np.quantile(x, 0.1))
790
+ df_p90["realization"] = ["p90"] * df_p90.shape[0]
791
+ df = (
792
+ pd.concat([df_mean, df_p10, df_p90])
793
+ .sort_values(["name", "date"])
794
+ .reset_index(drop=True)
795
+ )
796
+ realizations = ["p10", "mean", "p90"] # type: ignore
797
+ hover_template = (
798
+ "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
799
+ "Statistic: %{meta[0]}"
592
800
  )
801
+ for rlz in realizations:
802
+ lwd = 1.5 if rlz in ["p10", "p90"] else 2.5
803
+ sub_df = df[df["realization"] == rlz].copy().reset_index(drop=True)
804
+ if not containment_info.use_stats:
805
+ _add_prop_to_df(
806
+ sub_df, np.unique(df["date"]), "date", [color_choice, mark_choice]
807
+ )
593
808
  common_args = {
594
809
  "x": sub_df["date"],
595
- "hovertemplate": "%{x}: %{y}<br>Realization: %{meta[0]}<br>Prop: %{customdata}%",
596
- "meta": [rlz],
597
810
  "showlegend": False,
598
811
  }
599
812
  for name, color, line_type in zip(
600
813
  options["name"], options["color"], options["line_type"]
601
814
  ):
602
- # NBNB-AS: Check this, mypy complains:
603
815
  args = {
604
816
  "line_dash": line_type,
605
- "marker_color": color,
817
+ "line_width": lwd,
818
+ "marker_color": (
819
+ _LIGHTER_COLORS[color] if rlz in ["p10", "p90"] else color
820
+ ),
606
821
  "legendgroup": name,
607
- "name": name,
608
- "customdata": sub_df[sub_df["name"] == name]["prop"], # type: ignore
822
+ "name": "",
823
+ "meta": [rlz, name],
824
+ "hovertemplate": hover_template,
609
825
  }
610
- if name not in active_cols_at_startup:
826
+ if not containment_info.use_stats:
827
+ args["customdata"] = sub_df[sub_df["name"] == name]["prop"]
828
+ if name in inactive_cols_at_startup:
611
829
  args["visible"] = "legendonly"
612
830
  fig.add_scatter(
613
831
  y=sub_df[sub_df["name"] == name]["amount"], **args, **common_args
@@ -616,5 +834,250 @@ def generate_co2_time_containment_figure(
616
834
  fig.layout.xaxis.title = "Time"
617
835
  fig.layout.yaxis.title = scale.value
618
836
  fig.layout.yaxis.autorange = True
619
- _adjust_figure(fig)
837
+ _adjust_figure(fig, plot_title=_make_title(containment_info, include_date=False))
838
+ return fig
839
+
840
+
841
+ def generate_co2_statistics_figure(
842
+ table_provider: ContainmentDataProvider,
843
+ realizations: List[int],
844
+ scale: Union[Co2MassScale, Co2VolumeScale],
845
+ containment_info: ContainmentInfo,
846
+ legend_only_traces: Optional[List[str]],
847
+ ) -> go.Figure:
848
+ date_option = containment_info.date_option
849
+ df = _read_co2_volumes(table_provider, realizations, scale)
850
+ df = df[df["date"] == date_option]
851
+ df = df.drop(columns=["date"]).reset_index(drop=True)
852
+ color_choice = containment_info.color_choice
853
+ mark_choice = containment_info.mark_choice
854
+ _filter_columns(df, color_choice, mark_choice, containment_info)
855
+ cat_ord, colors, line_types = _prepare_pattern_and_color_options_statistics_plot(
856
+ df,
857
+ containment_info,
858
+ color_choice,
859
+ mark_choice,
860
+ )
861
+
862
+ # Remove if we want realization as label?
863
+ df = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
864
+ fig = px.ecdf(
865
+ df,
866
+ x="amount",
867
+ ecdfmode="reversed",
868
+ ecdfnorm="probability",
869
+ markers=True,
870
+ color="type",
871
+ color_discrete_sequence=colors,
872
+ line_dash="type" if mark_choice != "none" else None,
873
+ line_dash_sequence=line_types,
874
+ category_orders=cat_ord,
875
+ )
876
+
877
+ if legend_only_traces is None:
878
+ default_option = _find_default_legendonly(df, cat_ord["type"])
879
+ _toggle_trace_visibility(fig.data, default_option)
880
+ else:
881
+ _toggle_trace_visibility(fig.data, legend_only_traces)
882
+
883
+ fig.update_traces(
884
+ hovertemplate="Type: %{data.name}<br>Amount: %{x:.3f}<br>"
885
+ "Probability: %{y:.3f}<extra></extra>",
886
+ )
887
+ fig.layout.yaxis.range = [-0.02, 1.02]
888
+ fig.layout.legend.tracegroupgap = 0
889
+ fig.layout.xaxis.title = scale.value
890
+ fig.layout.yaxis.title = "Probability"
891
+ _adjust_figure(fig, plot_title=_make_title(containment_info))
892
+
893
+ return fig
894
+
895
+
896
+ def generate_co2_box_plot_figure(
897
+ table_provider: ContainmentDataProvider,
898
+ realizations: List[int],
899
+ scale: Union[Co2MassScale, Co2VolumeScale],
900
+ containment_info: ContainmentInfo,
901
+ legendonly_traces: Optional[List[str]],
902
+ ) -> go.Figure:
903
+ eps = 0.00001
904
+ date_option = containment_info.date_option
905
+ df = _read_co2_volumes(table_provider, realizations, scale)
906
+ df = df[df["date"] == date_option]
907
+ df = df.drop(columns=["date"]).reset_index(drop=True)
908
+
909
+ color_choice = containment_info.color_choice
910
+ mark_choice = containment_info.mark_choice
911
+ _filter_columns(df, color_choice, mark_choice, containment_info)
912
+ cat_ord, colors, _ = _prepare_pattern_and_color_options_statistics_plot(
913
+ df,
914
+ containment_info,
915
+ color_choice,
916
+ mark_choice,
917
+ )
918
+
919
+ fig = go.Figure()
920
+ for count, type_val in enumerate(cat_ord["type"], 0):
921
+ df_sub = df[df["type"] == type_val]
922
+ if df_sub.size == 0:
923
+ continue
924
+
925
+ values = df_sub["amount"].to_numpy()
926
+ real = df_sub["realization"].to_numpy()
927
+
928
+ median_val = df_sub["amount"].median()
929
+ q1 = _calculate_plotly_quantiles(values, 0.25)
930
+ q3 = _calculate_plotly_quantiles(values, 0.75)
931
+ p10 = np.percentile(values, 90)
932
+ p90 = np.percentile(values, 10)
933
+ min_fence, max_fence = _calculate_plotly_whiskers(values, q1, q3)
934
+
935
+ fig.add_trace(
936
+ go.Box(
937
+ x=[count] * len(values),
938
+ y=values,
939
+ name=type_val,
940
+ marker_color=colors[count],
941
+ boxpoints="all"
942
+ if containment_info.box_show_points == "all_points"
943
+ else "outliers",
944
+ customdata=real,
945
+ hovertemplate="<span style='font-family:Courier New;'>"
946
+ "Type : %{data.name}<br>Amount : %{y:.3f}<br>"
947
+ "Realization: %{customdata}"
948
+ "</span><extra></extra>",
949
+ legendgroup=type_val,
950
+ width=0.55,
951
+ )
952
+ )
953
+
954
+ fig.add_trace(
955
+ go.Bar(
956
+ x=[count],
957
+ y=[values.max() - values.min() + 2 * eps],
958
+ base=[values.min() - eps],
959
+ opacity=0.0,
960
+ hoverinfo="none",
961
+ hovertemplate=(
962
+ "<span style='font-family:Courier New;'>"
963
+ f"Type : {type_val}<br>"
964
+ f"Max : {values.max():.3f}<br>"
965
+ f"Top whisker : {max_fence:.3f}<br>"
966
+ f"p10 (not shown): {p10:.3f}<br>"
967
+ f"Q3 : {q3:.3f}<br>"
968
+ f"Median : {median_val:.3f}<br>"
969
+ f"Q1 : {q1:.3f}<br>"
970
+ f"p90 (not shown): {p90:.3f}<br>"
971
+ f"Lower whisker : {min_fence:.3f}<br>"
972
+ f"Min : {values.min():.3f}"
973
+ "</span><extra></extra>"
974
+ ),
975
+ showlegend=False,
976
+ legendgroup=type_val,
977
+ name=type_val,
978
+ marker_color=colors[count],
979
+ width=0.56,
980
+ )
981
+ )
982
+
983
+ fig.update_layout(
984
+ xaxis={
985
+ "tickmode": "array",
986
+ "tickvals": list(range(len(cat_ord["type"]))),
987
+ "ticktext": cat_ord["type"],
988
+ }
989
+ )
990
+
991
+ if len(cat_ord["type"]) > 20 or legendonly_traces is None:
992
+ default_option = _find_default_legendonly(df, cat_ord["type"])
993
+ _toggle_trace_visibility(fig.data, default_option)
994
+ else:
995
+ _toggle_trace_visibility(fig.data, legendonly_traces)
996
+
997
+ fig.layout.yaxis.autorange = True
998
+ fig.layout.legend.tracegroupgap = 0
999
+ fig.layout.yaxis.title = scale.value
1000
+ _adjust_figure(fig, plot_title=_make_title(containment_info))
1001
+
620
1002
  return fig
1003
+
1004
+
1005
+ # pylint: disable=too-many-branches
1006
+ def _make_title(c_info: ContainmentInfo, include_date: bool = True) -> str:
1007
+ components = []
1008
+ if include_date:
1009
+ components.append(c_info.date_option)
1010
+ if len(c_info.phases) > 0 and "phase" not in [
1011
+ c_info.color_choice,
1012
+ c_info.mark_choice,
1013
+ ]:
1014
+ if c_info.phase is not None and c_info.phase != "total":
1015
+ components.append(c_info.phase.capitalize())
1016
+ else:
1017
+ components.append("Phase: Total")
1018
+ if len(c_info.containments) > 0 and "containment" not in [
1019
+ c_info.color_choice,
1020
+ c_info.mark_choice,
1021
+ ]:
1022
+ if c_info.containment is not None and c_info.containment != "total":
1023
+ components.append(c_info.containment.capitalize())
1024
+ else:
1025
+ components.append("All containments areas")
1026
+ if len(c_info.zones) > 0 and "zone" not in [
1027
+ c_info.color_choice,
1028
+ c_info.mark_choice,
1029
+ ]:
1030
+ if c_info.zone is not None and c_info.zone != "all":
1031
+ components.append(c_info.zone)
1032
+ else:
1033
+ components.append("All zones")
1034
+ if (
1035
+ c_info.regions is not None
1036
+ and len(c_info.regions) > 0
1037
+ and "region"
1038
+ not in [
1039
+ c_info.color_choice,
1040
+ c_info.mark_choice,
1041
+ ]
1042
+ ):
1043
+ if c_info.region is not None and c_info.region != "all":
1044
+ components.append(c_info.region)
1045
+ else:
1046
+ components.append("All regions")
1047
+ if len(c_info.plume_groups) > 0 and "plume_group" not in [
1048
+ c_info.color_choice,
1049
+ c_info.mark_choice,
1050
+ ]:
1051
+ if c_info.plume_group is not None and c_info.plume_group != "all":
1052
+ components.append(c_info.plume_group)
1053
+ else:
1054
+ components.append("All plume groups")
1055
+ return " - ".join(components)
1056
+
1057
+
1058
+ def _calculate_plotly_quantiles(values: np.ndarray, percentile: float) -> float:
1059
+ values_sorted = values.copy()
1060
+ values_sorted.sort()
1061
+ n_val = len(values_sorted)
1062
+ a = n_val * percentile - 0.5
1063
+ if a.is_integer():
1064
+ return float(values_sorted[int(a)])
1065
+ return float(np.interp(a, list(range(0, n_val)), values_sorted))
1066
+
1067
+
1068
+ def _calculate_plotly_whiskers(
1069
+ values: np.ndarray, q1: float, q3: float
1070
+ ) -> Tuple[float, float]:
1071
+ values_sorted = values.copy()
1072
+ values_sorted.sort()
1073
+ a = q1 - 1.5 * (q3 - q1)
1074
+ b = q3 + 1.5 * (q3 - q1)
1075
+ return values[values >= a].min(), values[values <= b].max()
1076
+
1077
+
1078
+ def _toggle_trace_visibility(traces: List, legendonly_names: List[str]) -> None:
1079
+ for t in traces:
1080
+ if t.name in legendonly_names:
1081
+ t.visible = "legendonly"
1082
+ else:
1083
+ t.visible = True