webviz-subsurface 0.2.36__py3-none-any.whl → 0.2.37__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 (30) hide show
  1. webviz_subsurface/__init__.py +1 -1
  2. webviz_subsurface/_components/color_picker.py +1 -1
  3. webviz_subsurface/_providers/ensemble_polygon_provider/__init__.py +3 -0
  4. webviz_subsurface/_providers/ensemble_polygon_provider/_polygon_discovery.py +97 -0
  5. webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +226 -0
  6. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider.py +53 -0
  7. webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider_factory.py +99 -0
  8. webviz_subsurface/_providers/ensemble_polygon_provider/polygon_server.py +125 -0
  9. webviz_subsurface/plugins/_co2_leakage/_plugin.py +531 -377
  10. webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
  11. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +169 -173
  12. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +329 -84
  13. webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
  14. webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
  15. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
  16. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +189 -96
  17. webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
  18. webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
  19. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +29 -21
  20. webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
  21. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +30 -18
  22. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +805 -343
  23. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/METADATA +2 -2
  24. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/RECORD +30 -19
  25. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/WHEEL +1 -1
  26. /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
  27. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE +0 -0
  28. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/LICENSE.chromedriver +0 -0
  29. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/entry_points.txt +0 -0
  30. {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.37.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,9 @@ import plotly.graph_objects as go
9
9
 
10
10
  from webviz_subsurface._providers import EnsembleTableProvider
11
11
  from webviz_subsurface._utils.enum_shim import StrEnum
12
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
13
+ ContainmentDataProvider,
14
+ )
12
15
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
13
16
  Co2MassScale,
14
17
  Co2VolumeScale,
@@ -41,6 +44,26 @@ _COLOR_ZONES = [
41
44
  "#34b36f",
42
45
  ]
43
46
 
47
+ _LIGHTER_COLORS = {
48
+ "black": "#909090",
49
+ "#222222": "#909090",
50
+ "#00aa00": "#55ff55",
51
+ "#006ddd": "#6eb6ff",
52
+ "#dd4300": "#ff9a6e",
53
+ "#e91451": "#f589a8",
54
+ "#daa218": "#f2d386",
55
+ "#208eb7": "#81cde9",
56
+ "#84bc04": "#cdfc63",
57
+ "#b74532": "#e19e92",
58
+ "#9a89b4": "#ccc4d9",
59
+ "#8d30ba": "#c891e3",
60
+ "#256b33": "#77d089",
61
+ "#95704d": "#cfb7a1",
62
+ "#1357ca": "#7ba7f3",
63
+ "#f75ef0": "#fbaef7",
64
+ "#34b36f": "#93e0b7",
65
+ }
66
+
44
67
 
45
68
  def _read_dataframe(
46
69
  table_provider: EnsembleTableProvider,
@@ -54,40 +77,6 @@ def _read_dataframe(
54
77
  return df
55
78
 
56
79
 
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
80
  def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
92
81
  if split == "containment":
93
82
  return [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED]
@@ -106,7 +95,7 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
106
95
  return [""] * num_marks
107
96
  if mark_choice == "containment":
108
97
  return ["x", "/", ""]
109
- if mark_choice in ["zone", "region"]:
98
+ if mark_choice in ["zone", "region", "plume_group"]:
110
99
  base_pattern = ["", "/", "x", "-", "\\", "+", "|", "."]
111
100
  if num_marks > len(base_pattern):
112
101
  base_pattern *= int(np.ceil(num_marks / len(base_pattern)))
@@ -123,7 +112,7 @@ def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
123
112
  return ["solid"]
124
113
  if mark_choice == "containment":
125
114
  return ["dash", "dot", "solid"]
126
- if mark_choice in ["zone", "region"]:
115
+ if mark_choice in ["zone", "region", "plume_group"]:
127
116
  if len(mark_options) > 8:
128
117
  warnings.warn(
129
118
  f"Large number of {mark_choice}s might make it hard "
@@ -167,6 +156,93 @@ def _prepare_pattern_and_color_options(
167
156
  return cat_ord, colors, marks
168
157
 
169
158
 
159
+ def _prepare_pattern_and_color_options_statistics_plot(
160
+ df: pd.DataFrame,
161
+ containment_info: Dict,
162
+ color_choice: str,
163
+ mark_choice: str,
164
+ ) -> Tuple[Dict, List, List]:
165
+ mark_options = [] if mark_choice == "none" else containment_info[f"{mark_choice}s"]
166
+ color_options = containment_info[f"{color_choice}s"]
167
+ num_colors = len(color_options)
168
+ num_marks = num_colors if mark_choice == "none" else len(mark_options)
169
+ line_types = _get_line_types(mark_options, mark_choice)
170
+ colors = _get_colors(num_colors, color_choice)
171
+
172
+ if mark_choice == "phase":
173
+ mark_options = ["total"] + mark_options
174
+ line_types = ["solid"] + line_types
175
+ num_marks += 1
176
+ if color_choice == "containment":
177
+ color_options = ["total"] + color_options
178
+ colors = ["black"] + colors
179
+ num_colors += 1
180
+
181
+ filter_mark = mark_choice != "phase"
182
+ filter_color = color_choice not in ["phase", "containment"]
183
+ _filter_rows(df, color_choice, mark_choice, filter_mark, filter_color)
184
+
185
+ if mark_choice == "none":
186
+ cat_ord = {"type": color_options}
187
+ df["type"] = df[color_choice]
188
+ return cat_ord, colors, line_types
189
+ df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
190
+
191
+ if containment_info["sorting"] == "color":
192
+ cat_ord = {
193
+ "type": [", ".join((c, m)) for c in color_options for m in mark_options],
194
+ }
195
+ colors = [c for c in colors for _ in range(num_marks)]
196
+ line_types = line_types * num_colors
197
+ else:
198
+ cat_ord = {
199
+ "type": [", ".join((c, m)) for m in mark_options for c in color_options],
200
+ }
201
+ colors = colors * num_marks
202
+ line_types = [m for m in line_types for _ in range(num_colors)]
203
+
204
+ for m in mark_options + ["total", "all"]:
205
+ df["type"] = df["type"].replace(f"total, {m}", m)
206
+ df["type"] = df["type"].replace(f"all, {m}", m)
207
+ for m in color_options:
208
+ df["type"] = df["type"].replace(f"{m}, total", m)
209
+ df["type"] = df["type"].replace(f"{m}, all", m)
210
+ cat_ord["type"] = [
211
+ label.replace("total, ", "") if "total, " in label else label
212
+ for label in cat_ord["type"]
213
+ ]
214
+ cat_ord["type"] = [
215
+ label.replace("all, ", "") if "all, " in label else label
216
+ for label in cat_ord["type"]
217
+ ]
218
+ cat_ord["type"] = [
219
+ label.replace(", total", "") if ", total" in label else label
220
+ for label in cat_ord["type"]
221
+ ]
222
+ cat_ord["type"] = [
223
+ label.replace(", all", "") if ", all" in label else label
224
+ for label in cat_ord["type"]
225
+ ]
226
+
227
+ return cat_ord, colors, line_types
228
+
229
+
230
+ def _find_default_option_statistics_figure(
231
+ df: pd.DataFrame, categories: List[str]
232
+ ) -> str:
233
+ if "hazardous" in categories:
234
+ default_option = "hazardous"
235
+ else:
236
+ max_value = -999.9
237
+ default_option = categories[0]
238
+ for category in categories:
239
+ df_filtered = df[df["type"] == category]
240
+ if df_filtered["amount"].max() > max_value:
241
+ max_value = df_filtered["amount"].max()
242
+ default_option = category
243
+ return default_option
244
+
245
+
170
246
  def _prepare_line_type_and_color_options(
171
247
  df: pd.DataFrame,
172
248
  containment_info: Dict,
@@ -180,6 +256,7 @@ def _prepare_line_type_and_color_options(
180
256
  num_colors = len(color_options)
181
257
  line_types = _get_line_types(mark_options, mark_choice)
182
258
  colors = _get_colors(num_colors, color_choice)
259
+
183
260
  filter_mark = True
184
261
  if mark_choice == "phase":
185
262
  mark_options = ["total"] + mark_options
@@ -225,22 +302,8 @@ def _prepare_line_type_and_color_options(
225
302
  return options
226
303
 
227
304
 
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
305
  def _read_terminal_co2_volumes(
243
- table_provider: EnsembleTableProvider,
306
+ table_provider: ContainmentDataProvider,
244
307
  realizations: List[int],
245
308
  scale: Union[Co2MassScale, Co2VolumeScale],
246
309
  containment_info: Dict[str, Union[str, None, List[str]]],
@@ -258,11 +321,10 @@ def _read_terminal_co2_volumes(
258
321
  records[color_choice] = []
259
322
  if mark_choice != "none":
260
323
  records[mark_choice] = []
261
- scale_factor = _find_scale_factor(table_provider, scale)
262
324
  data_frame = None
263
325
  for real in realizations:
264
- df = _read_dataframe(table_provider, real, scale_factor)
265
- df = df[df["date"] == np.max(df["date"])]
326
+ df = table_provider.extract_dataframe(real, scale)
327
+ df = df[df["date"] == containment_info["date_option"]]
266
328
  _add_sort_key_and_real(df, str(real), containment_info)
267
329
  _filter_columns(df, color_choice, mark_choice, containment_info)
268
330
  _filter_rows(df, color_choice, mark_choice)
@@ -285,7 +347,7 @@ def _filter_columns(
285
347
  ) -> None:
286
348
  filter_columns = [
287
349
  col
288
- for col in ["phase", "containment", "zone", "region"]
350
+ for col in ["phase", "containment", "zone", "region", "plume_group"]
289
351
  if col not in [mark_choice, color_choice]
290
352
  ]
291
353
  for col in filter_columns:
@@ -298,8 +360,10 @@ def _filter_rows(
298
360
  color_choice: str,
299
361
  mark_choice: str,
300
362
  filter_mark: bool = True,
363
+ filter_color: bool = True,
301
364
  ) -> None:
302
- df.query(f'{color_choice} not in ["total", "all"]', inplace=True)
365
+ if filter_color:
366
+ df.query(f'{color_choice} not in ["total", "all"]', inplace=True)
303
367
  if mark_choice != "none" and filter_mark:
304
368
  df.query(f'{mark_choice} not in ["total", "all"]', inplace=True)
305
369
 
@@ -315,6 +379,7 @@ def _add_sort_key_and_real(
315
379
  & (df["containment"] == "hazardous")
316
380
  & (df["zone"] == containment_info["zone"])
317
381
  & (df["region"] == containment_info["region"])
382
+ & (df["plume_group"] == containment_info["plume_group"])
318
383
  ]["amount"]
319
384
  )
320
385
  sort_value_secondary = np.sum(
@@ -323,6 +388,7 @@ def _add_sort_key_and_real(
323
388
  & (df["containment"] == "outside")
324
389
  & (df["zone"] == containment_info["zone"])
325
390
  & (df["region"] == containment_info["region"])
391
+ & (df["plume_group"] == containment_info["plume_group"])
326
392
  ]["amount"]
327
393
  )
328
394
  df["real"] = [label] * df.shape[0]
@@ -331,14 +397,13 @@ def _add_sort_key_and_real(
331
397
 
332
398
 
333
399
  def _read_co2_volumes(
334
- table_provider: EnsembleTableProvider,
400
+ table_provider: ContainmentDataProvider,
335
401
  realizations: List[int],
336
402
  scale: Union[Co2MassScale, Co2VolumeScale],
337
403
  ) -> pd.DataFrame:
338
- scale_factor = _find_scale_factor(table_provider, scale)
339
404
  return pd.concat(
340
405
  [
341
- _read_dataframe(table_provider, r, scale_factor).assign(realization=r)
406
+ table_provider.extract_dataframe(r, scale).assign(realization=r)
342
407
  for r in realizations
343
408
  ]
344
409
  )
@@ -357,15 +422,21 @@ def _change_names(
357
422
  df["name"] = df["name"].replace(f"{m}, all", m)
358
423
 
359
424
 
360
- def _adjust_figure(fig: go.Figure) -> None:
425
+ def _adjust_figure(fig: go.Figure, plot_title: Optional[str] = None) -> None:
361
426
  fig.layout.legend.orientation = "v"
362
427
  fig.layout.legend.title.text = ""
363
428
  fig.layout.legend.itemwidth = 40
364
429
  fig.layout.xaxis.exponentformat = "power"
365
- fig.layout.title.x = 0.5
430
+ if plot_title is not None:
431
+ fig.layout.title.text = plot_title
432
+ fig.layout.title.font = {"size": 14}
433
+ fig.layout.margin.t = 40
434
+ fig.layout.title.y = 0.95
435
+ else:
436
+ fig.layout.margin.t = 15
437
+ fig.layout.title.x = 0.4
366
438
  fig.layout.paper_bgcolor = "rgba(0,0,0,0)"
367
439
  fig.layout.margin.b = 6
368
- fig.layout.margin.t = 15
369
440
  fig.layout.margin.l = 10
370
441
  fig.layout.margin.r = 10
371
442
  fig.update_layout(
@@ -402,7 +473,7 @@ def _add_prop_to_df(
402
473
 
403
474
 
404
475
  def generate_co2_volume_figure(
405
- table_provider: EnsembleTableProvider,
476
+ table_provider: ContainmentDataProvider,
406
477
  realizations: List[int],
407
478
  scale: Union[Co2MassScale, Co2VolumeScale],
408
479
  containment_info: Dict[str, Any],
@@ -429,17 +500,21 @@ def generate_co2_volume_figure(
429
500
  pattern_shape_sequence=marks,
430
501
  orientation="h",
431
502
  category_orders=cat_ord,
432
- hover_data={"prop": True, "real": False},
503
+ custom_data=["type", "prop"],
504
+ )
505
+ fig.update_traces(
506
+ hovertemplate="Type: %{customdata[0]}<br>Amount: %{x:.3f}<br>"
507
+ "Realization: %{y}<br>Proportion: %{customdata[1]}<extra></extra>",
433
508
  )
434
509
  fig.layout.yaxis.title = "Realization"
435
510
  fig.layout.xaxis.title = scale.value
436
- _adjust_figure(fig)
511
+ _adjust_figure(fig, plot_title=containment_info["date_option"])
437
512
  return fig
438
513
 
439
514
 
440
515
  # pylint: disable=too-many-locals
441
516
  def generate_co2_time_containment_one_realization_figure(
442
- table_provider: EnsembleTableProvider,
517
+ table_provider: ContainmentDataProvider,
443
518
  scale: Union[Co2MassScale, Co2VolumeScale],
444
519
  time_series_realization: int,
445
520
  y_limits: List[Optional[float]],
@@ -477,10 +552,11 @@ def generate_co2_time_containment_one_realization_figure(
477
552
  pattern_shape_sequence=marks,
478
553
  category_orders=cat_ord,
479
554
  range_y=y_limits,
480
- hover_data={
481
- "prop": True,
482
- "amount": ":.3f",
483
- },
555
+ custom_data=["type", "prop"],
556
+ )
557
+ fig.update_traces(
558
+ hovertemplate="Type: %{customdata[0]}<br>Date: %{x}<br>"
559
+ "Amount: %{y:.3f}<br>Proportion: %{customdata[1]}<extra></extra>",
484
560
  )
485
561
  _add_hover_info_in_field(fig, df, cat_ord, colors)
486
562
  fig.layout.yaxis.range = y_limits
@@ -535,15 +611,15 @@ def _add_hover_info_in_field(
535
611
  p15 = prev_val + 0.15 * amount
536
612
  p85 = prev_val + 0.85 * amount
537
613
  y_vals = np.linspace(p15, p85, 8).tolist() * len(date_dict[date])
538
- y_vals.sort()
614
+ y_vals.sort() # type: ignore[attr-defined]
539
615
  fig.add_trace(
540
616
  go.Scatter(
541
617
  x=date_dict[date] * 8,
542
618
  y=y_vals,
543
619
  mode="lines",
544
620
  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}",
621
+ text=f"Type: {name}<br>Date: {date_strings[date]}<br>"
622
+ f"Amount: {amount:.3f}<br>Proportion: {prop}",
547
623
  opacity=0,
548
624
  hoverinfo="text",
549
625
  hoveron="points",
@@ -553,9 +629,82 @@ def _add_hover_info_in_field(
553
629
  prev_vals[date] = prev_val + amount
554
630
 
555
631
 
632
+ def _connect_plume_groups(
633
+ df: pd.DataFrame,
634
+ color_choice: str,
635
+ mark_choice: str,
636
+ ) -> None:
637
+ col_list = ["realization"]
638
+ if color_choice == "plume_group" and mark_choice != "none":
639
+ col_list.append(mark_choice)
640
+ elif mark_choice == "plume_group":
641
+ col_list.append(color_choice)
642
+
643
+ cols: Union[List[str], str] = col_list
644
+ if len(col_list) == 1:
645
+ cols = col_list[0]
646
+ # Find points where plumes start or end, to connect the lines
647
+ end_points = []
648
+ start_points = []
649
+ for plume_name, df_sub in df.groupby("plume_group"):
650
+ if plume_name == "undetermined":
651
+ continue
652
+ for _, df_sub2 in df_sub.groupby(cols):
653
+ # Assumes the data frame is sorted on date
654
+ mask_end = (
655
+ (df_sub2["amount"] == 0.0)
656
+ & (df_sub2["amount"].shift(1) > 0.0)
657
+ & (df_sub2.index > 0)
658
+ )
659
+ mask_start = (
660
+ (df_sub2["amount"] > 0.0)
661
+ & (df_sub2["amount"].shift(1) == 0.0)
662
+ & (df_sub2.index > 0)
663
+ )
664
+ first_index_end = mask_end.idxmax() if mask_end.any() else None
665
+ first_index_start = mask_start.idxmax() if mask_start.any() else None
666
+ transition_row_end = (
667
+ df_sub2.loc[first_index_end] if first_index_end is not None else None
668
+ )
669
+ transition_row_start = (
670
+ df_sub2.loc[first_index_start]
671
+ if first_index_start is not None
672
+ else None
673
+ )
674
+ if transition_row_end is not None:
675
+ end_points.append(transition_row_end)
676
+ # Replace 0 with np.nan for all dates after this
677
+ date = str(transition_row_end["date"])
678
+ df.loc[
679
+ (df["plume_group"] == plume_name)
680
+ & (df["amount"] == 0.0)
681
+ & (df["date"] > date),
682
+ "amount",
683
+ ] = np.nan
684
+ if transition_row_start is not None:
685
+ start_points.append(transition_row_start)
686
+ for end_point in end_points:
687
+ plume1 = end_point["plume_group"]
688
+ row1 = end_point.drop(["amount", "plume_group", "name"])
689
+ for start_point in start_points:
690
+ plume2 = start_point["plume_group"]
691
+ if plume1 in plume2 and len(plume1) < len(plume2):
692
+ row2 = start_point.drop(["amount", "plume_group", "name"])
693
+ if row1.equals(row2):
694
+ row_to_change = df.eq(end_point).all(axis=1)
695
+ if sum(row_to_change) == 1:
696
+ df.loc[row_to_change, "amount"] = start_point["amount"]
697
+ df["is_merged"] = ["+" in x for x in df["plume_group"].values]
698
+ df.loc[
699
+ (df["plume_group"] != "all") & (df["is_merged"]) & (df["amount"] == 0.0),
700
+ "amount",
701
+ ] = np.nan
702
+ df.drop(columns="is_merged", inplace=True)
703
+
704
+
556
705
  # pylint: disable=too-many-locals
557
706
  def generate_co2_time_containment_figure(
558
- table_provider: EnsembleTableProvider,
707
+ table_provider: ContainmentDataProvider,
559
708
  realizations: List[int],
560
709
  scale: Union[Co2MassScale, Co2VolumeScale],
561
710
  containment_info: Dict[str, Any],
@@ -570,6 +719,12 @@ def generate_co2_time_containment_figure(
570
719
  active_cols_at_startup = list(
571
720
  options[options["line_type"].isin(["solid", "0px"])]["name"]
572
721
  )
722
+ if "plume_group" in df:
723
+ try:
724
+ _connect_plume_groups(df, color_choice, mark_choice)
725
+ except ValueError:
726
+ pass
727
+
573
728
  fig = go.Figure()
574
729
  # Generate dummy scatters for legend entries
575
730
  dummy_args = {"x": df["date"], "mode": "lines", "hoverinfo": "none"}
@@ -585,28 +740,65 @@ def generate_co2_time_containment_figure(
585
740
  if name not in active_cols_at_startup:
586
741
  args["visible"] = "legendonly"
587
742
  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]
743
+
744
+ hover_template = (
745
+ "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
746
+ "Realization: %{meta[0]}<br>Proportion: %{customdata}"
747
+ )
748
+
749
+ if containment_info["use_stats"]:
750
+ df_no_real = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
751
+ if mark_choice == "none":
752
+ df_grouped = df_no_real.groupby(
753
+ ["date", "name", color_choice], as_index=False
754
+ )
755
+ else:
756
+ df_grouped = df_no_real.groupby(
757
+ ["date", "name", color_choice, mark_choice], as_index=False
758
+ )
759
+ df_mean = df_grouped.agg(np.mean)
760
+ df_mean["realization"] = ["mean"] * df_mean.shape[0]
761
+ df_p10 = df_grouped.agg(lambda x: np.quantile(x, 0.1))
762
+ df_p10["realization"] = ["p10"] * df_p10.shape[0]
763
+ df_p90 = df_grouped.agg(lambda x: np.quantile(x, 0.9))
764
+ df_p90["realization"] = ["p90"] * df_p90.shape[0]
765
+ df = (
766
+ pd.concat([df_mean, df_p10, df_p90])
767
+ .sort_values(["name", "date"])
768
+ .reset_index(drop=True)
769
+ )
770
+ realizations = ["p10", "mean", "p90"] # type: ignore
771
+ hover_template = (
772
+ "Type: %{meta[1]}<br>Date: %{x}<br>Amount: %{y:.3f}<br>"
773
+ "Statistic: %{meta[0]}"
592
774
  )
775
+ for rlz in realizations:
776
+ lwd = 1.5 if rlz in ["p10", "p90"] else 2.5
777
+ sub_df = df[df["realization"] == rlz].copy().reset_index(drop=True)
778
+ if not containment_info["use_stats"]:
779
+ _add_prop_to_df(
780
+ sub_df, np.unique(df["date"]), "date", [color_choice, mark_choice]
781
+ )
593
782
  common_args = {
594
783
  "x": sub_df["date"],
595
- "hovertemplate": "%{x}: %{y}<br>Realization: %{meta[0]}<br>Prop: %{customdata}%",
596
- "meta": [rlz],
597
784
  "showlegend": False,
598
785
  }
599
786
  for name, color, line_type in zip(
600
787
  options["name"], options["color"], options["line_type"]
601
788
  ):
602
- # NBNB-AS: Check this, mypy complains:
603
789
  args = {
604
790
  "line_dash": line_type,
605
- "marker_color": color,
791
+ "line_width": lwd,
792
+ "marker_color": (
793
+ _LIGHTER_COLORS[color] if rlz in ["p10", "p90"] else color
794
+ ),
606
795
  "legendgroup": name,
607
- "name": name,
608
- "customdata": sub_df[sub_df["name"] == name]["prop"], # type: ignore
796
+ "name": "",
797
+ "meta": [rlz, name],
798
+ "hovertemplate": hover_template,
609
799
  }
800
+ if not containment_info["use_stats"]:
801
+ args["customdata"] = sub_df[sub_df["name"] == name]["prop"]
610
802
  if name not in active_cols_at_startup:
611
803
  args["visible"] = "legendonly"
612
804
  fig.add_scatter(
@@ -618,3 +810,56 @@ def generate_co2_time_containment_figure(
618
810
  fig.layout.yaxis.autorange = True
619
811
  _adjust_figure(fig)
620
812
  return fig
813
+
814
+
815
+ def generate_co2_statistics_figure(
816
+ table_provider: ContainmentDataProvider,
817
+ realizations: List[int],
818
+ scale: Union[Co2MassScale, Co2VolumeScale],
819
+ containment_info: Dict[str, Any],
820
+ ) -> go.Figure:
821
+ date_option = containment_info["date_option"]
822
+ df = _read_co2_volumes(table_provider, realizations, scale)
823
+ df = df[df["date"] == date_option]
824
+ df = df.drop(columns=["date"]).reset_index(drop=True)
825
+ color_choice = containment_info["color_choice"]
826
+ mark_choice = containment_info["mark_choice"]
827
+ _filter_columns(df, color_choice, mark_choice, containment_info)
828
+ cat_ord, colors, line_types = _prepare_pattern_and_color_options_statistics_plot(
829
+ df,
830
+ containment_info,
831
+ color_choice,
832
+ mark_choice,
833
+ )
834
+
835
+ # Remove if we want realization as label?
836
+ df = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
837
+ fig = px.ecdf(
838
+ df,
839
+ x="amount",
840
+ ecdfmode="reversed",
841
+ ecdfnorm="probability",
842
+ markers=True,
843
+ color="type",
844
+ color_discrete_sequence=colors,
845
+ line_dash="type" if mark_choice != "none" else None,
846
+ line_dash_sequence=line_types,
847
+ category_orders=cat_ord,
848
+ )
849
+
850
+ default_option = _find_default_option_statistics_figure(df, cat_ord["type"])
851
+ for trace in fig.data:
852
+ if trace.name != default_option:
853
+ trace.visible = "legendonly"
854
+
855
+ fig.update_traces(
856
+ hovertemplate="Type: %{data.name}<br>Amount: %{x:.3f}<br>"
857
+ "Probability: %{y:.3f}<extra></extra>",
858
+ )
859
+ fig.layout.yaxis.range = [-0.02, 1.02]
860
+ fig.layout.legend.tracegroupgap = 0
861
+ fig.layout.xaxis.title = scale.value
862
+ fig.layout.yaxis.title = "Probability"
863
+ _adjust_figure(fig, plot_title=containment_info["date_option"])
864
+
865
+ return fig