webviz-subsurface 0.2.37__py3-none-any.whl → 0.2.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -12,6 +14,9 @@ from webviz_subsurface._utils.enum_shim import StrEnum
12
14
  from webviz_subsurface.plugins._co2_leakage._utilities.containment_data_provider import (
13
15
  ContainmentDataProvider,
14
16
  )
17
+ from webviz_subsurface.plugins._co2_leakage._utilities.containment_info import (
18
+ ContainmentInfo,
19
+ )
15
20
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
16
21
  Co2MassScale,
17
22
  Co2VolumeScale,
@@ -29,6 +34,10 @@ _COLOR_TOTAL = "#222222"
29
34
  _COLOR_CONTAINED = "#00aa00"
30
35
  _COLOR_OUTSIDE = "#006ddd"
31
36
  _COLOR_HAZARDOUS = "#dd4300"
37
+ _COLOR_DISSOLVED = "#208eb7"
38
+ _COLOR_GAS = "#C41E3A"
39
+ _COLOR_FREE = "#FF2400"
40
+ _COLOR_TRAPPED = "#880808"
32
41
  _COLOR_ZONES = [
33
42
  "#e91451",
34
43
  "#daa218",
@@ -62,6 +71,9 @@ _LIGHTER_COLORS = {
62
71
  "#1357ca": "#7ba7f3",
63
72
  "#f75ef0": "#fbaef7",
64
73
  "#34b36f": "#93e0b7",
74
+ "#C41E3A": "#E42E5A",
75
+ "#FF2400": "#FF7430",
76
+ "#880808": "#C84848",
65
77
  }
66
78
 
67
79
 
@@ -80,6 +92,10 @@ def _read_dataframe(
80
92
  def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
81
93
  if split == "containment":
82
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]
83
99
  options = list(_COLOR_ZONES)
84
100
  if split == "region":
85
101
  options.reverse()
@@ -104,6 +120,7 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
104
120
  f"Some {mark_choice}s will share pattern."
105
121
  )
106
122
  return base_pattern[:num_marks]
123
+ # mark_choice == "phase":
107
124
  return ["", "/"] if num_marks == 2 else ["", ".", "/"]
108
125
 
109
126
 
@@ -113,35 +130,36 @@ def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
113
130
  if mark_choice == "containment":
114
131
  return ["dash", "dot", "solid"]
115
132
  if mark_choice in ["zone", "region", "plume_group"]:
116
- if len(mark_options) > 8:
133
+ options = ["solid", "dash", "dot", "dashdot", "longdash", "longdashdot"]
134
+ if len(mark_options) > 6:
117
135
  warnings.warn(
118
136
  f"Large number of {mark_choice}s might make it hard "
119
137
  f"to distinguish different dashed lines."
120
138
  )
121
- return [
122
- f"{round(i / len(mark_options) * 25)}px" for i in range(len(mark_options))
123
- ]
139
+ return [options[i % 6] for i in range(len(mark_options))]
140
+ # mark_choice == "phase":
124
141
  return ["dot", "dash"] if "gas" in mark_options else ["dot", "dashdot", "dash"]
125
142
 
126
143
 
127
144
  def _prepare_pattern_and_color_options(
128
145
  df: pd.DataFrame,
129
- containment_info: Dict,
146
+ containment_info: ContainmentInfo,
130
147
  color_choice: str,
131
148
  mark_choice: str,
132
149
  ) -> Tuple[Dict, List, List]:
133
- mark_options = [] if mark_choice == "none" else containment_info[f"{mark_choice}s"]
134
- 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")
135
153
  num_colors = len(color_options)
136
- num_marks = num_colors if mark_choice == "none" else len(mark_options)
154
+ num_marks = num_colors if no_mark else len(mark_options)
137
155
  marks = _get_marks(num_marks, mark_choice)
138
156
  colors = _get_colors(num_colors, color_choice)
139
- if mark_choice == "none":
157
+ if no_mark:
140
158
  cat_ord = {"type": color_options}
141
159
  df["type"] = df[color_choice]
142
160
  return cat_ord, colors, marks
143
161
  df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
144
- if containment_info["sorting"] == "color":
162
+ if containment_info.sorting == "color":
145
163
  cat_ord = {
146
164
  "type": [", ".join((c, m)) for c in color_options for m in mark_options],
147
165
  }
@@ -158,14 +176,15 @@ def _prepare_pattern_and_color_options(
158
176
 
159
177
  def _prepare_pattern_and_color_options_statistics_plot(
160
178
  df: pd.DataFrame,
161
- containment_info: Dict,
179
+ containment_info: ContainmentInfo,
162
180
  color_choice: str,
163
181
  mark_choice: str,
164
182
  ) -> 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"]
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")
167
186
  num_colors = len(color_options)
168
- num_marks = num_colors if mark_choice == "none" else len(mark_options)
187
+ num_marks = num_colors if no_mark else len(mark_options)
169
188
  line_types = _get_line_types(mark_options, mark_choice)
170
189
  colors = _get_colors(num_colors, color_choice)
171
190
 
@@ -173,7 +192,7 @@ def _prepare_pattern_and_color_options_statistics_plot(
173
192
  mark_options = ["total"] + mark_options
174
193
  line_types = ["solid"] + line_types
175
194
  num_marks += 1
176
- if color_choice == "containment":
195
+ if color_choice in ["containment", "phase"]:
177
196
  color_options = ["total"] + color_options
178
197
  colors = ["black"] + colors
179
198
  num_colors += 1
@@ -182,13 +201,13 @@ def _prepare_pattern_and_color_options_statistics_plot(
182
201
  filter_color = color_choice not in ["phase", "containment"]
183
202
  _filter_rows(df, color_choice, mark_choice, filter_mark, filter_color)
184
203
 
185
- if mark_choice == "none":
204
+ if no_mark:
186
205
  cat_ord = {"type": color_options}
187
206
  df["type"] = df[color_choice]
188
207
  return cat_ord, colors, line_types
189
208
  df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
190
209
 
191
- if containment_info["sorting"] == "color":
210
+ if containment_info.sorting == "color":
192
211
  cat_ord = {
193
212
  "type": [", ".join((c, m)) for c in color_options for m in mark_options],
194
213
  }
@@ -227,9 +246,7 @@ def _prepare_pattern_and_color_options_statistics_plot(
227
246
  return cat_ord, colors, line_types
228
247
 
229
248
 
230
- def _find_default_option_statistics_figure(
231
- df: pd.DataFrame, categories: List[str]
232
- ) -> str:
249
+ def _find_default_legendonly(df: pd.DataFrame, categories: List[str]) -> List[str]:
233
250
  if "hazardous" in categories:
234
251
  default_option = "hazardous"
235
252
  else:
@@ -240,29 +257,32 @@ def _find_default_option_statistics_figure(
240
257
  if df_filtered["amount"].max() > max_value:
241
258
  max_value = df_filtered["amount"].max()
242
259
  default_option = category
243
- return default_option
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]
244
264
 
245
265
 
246
266
  def _prepare_line_type_and_color_options(
247
267
  df: pd.DataFrame,
248
- containment_info: Dict,
268
+ containment_info: ContainmentInfo,
249
269
  color_choice: str,
250
270
  mark_choice: str,
251
271
  ) -> pd.DataFrame:
252
272
  mark_options = []
253
273
  if mark_choice != "none":
254
- mark_options = list(containment_info[f"{mark_choice}s"])
255
- 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"))
256
276
  num_colors = len(color_options)
257
277
  line_types = _get_line_types(mark_options, mark_choice)
258
278
  colors = _get_colors(num_colors, color_choice)
259
279
 
260
280
  filter_mark = True
261
- if mark_choice == "phase":
281
+ if mark_choice in ["containment", "phase"]:
262
282
  mark_options = ["total"] + mark_options
263
283
  line_types = ["solid"] + line_types
264
284
  filter_mark = False
265
- if color_choice == "containment":
285
+ if color_choice in ["containment", "phase"]:
266
286
  color_options = ["total"] + color_options
267
287
  colors = ["black"] + colors
268
288
  else:
@@ -278,7 +298,7 @@ def _prepare_line_type_and_color_options(
278
298
  )
279
299
  df["name"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
280
300
  _change_names(df, color_options, mark_options)
281
- if containment_info["sorting"] == "color":
301
+ if containment_info.sorting == "color":
282
302
  options = pd.DataFrame(
283
303
  {
284
304
  "name": [
@@ -306,7 +326,7 @@ def _read_terminal_co2_volumes(
306
326
  table_provider: ContainmentDataProvider,
307
327
  realizations: List[int],
308
328
  scale: Union[Co2MassScale, Co2VolumeScale],
309
- containment_info: Dict[str, Union[str, None, List[str]]],
329
+ containment_info: ContainmentInfo,
310
330
  ) -> pd.DataFrame:
311
331
  records: Dict[str, List[Any]] = {
312
332
  "real": [],
@@ -314,8 +334,8 @@ def _read_terminal_co2_volumes(
314
334
  "sort_key": [],
315
335
  "sort_key_secondary": [],
316
336
  }
317
- color_choice = containment_info["color_choice"]
318
- mark_choice = containment_info["mark_choice"]
337
+ color_choice = containment_info.color_choice
338
+ mark_choice = containment_info.mark_choice
319
339
  assert isinstance(color_choice, str)
320
340
  assert isinstance(mark_choice, str)
321
341
  records[color_choice] = []
@@ -324,7 +344,7 @@ def _read_terminal_co2_volumes(
324
344
  data_frame = None
325
345
  for real in realizations:
326
346
  df = table_provider.extract_dataframe(real, scale)
327
- df = df[df["date"] == containment_info["date_option"]]
347
+ df = df[df["date"] == containment_info.date_option]
328
348
  _add_sort_key_and_real(df, str(real), containment_info)
329
349
  _filter_columns(df, color_choice, mark_choice, containment_info)
330
350
  _filter_rows(df, color_choice, mark_choice)
@@ -343,7 +363,7 @@ def _filter_columns(
343
363
  df: pd.DataFrame,
344
364
  color_choice: str,
345
365
  mark_choice: str,
346
- containment_info: Dict,
366
+ containment_info: ContainmentInfo,
347
367
  ) -> None:
348
368
  filter_columns = [
349
369
  col
@@ -351,7 +371,7 @@ def _filter_columns(
351
371
  if col not in [mark_choice, color_choice]
352
372
  ]
353
373
  for col in filter_columns:
354
- df.query(f'{col} == "{containment_info[col]}"', inplace=True)
374
+ df.query(f'{col} == "{getattr(containment_info, col)}"', inplace=True)
355
375
  df.drop(columns=filter_columns, inplace=True)
356
376
 
357
377
 
@@ -371,24 +391,24 @@ def _filter_rows(
371
391
  def _add_sort_key_and_real(
372
392
  df: pd.DataFrame,
373
393
  label: str,
374
- containment_info: Dict,
394
+ containment_info: ContainmentInfo,
375
395
  ) -> None:
376
396
  sort_value = np.sum(
377
397
  df[
378
398
  (df["phase"] == "total")
379
399
  & (df["containment"] == "hazardous")
380
- & (df["zone"] == containment_info["zone"])
381
- & (df["region"] == containment_info["region"])
382
- & (df["plume_group"] == containment_info["plume_group"])
400
+ & (df["zone"] == containment_info.zone)
401
+ & (df["region"] == containment_info.region)
402
+ & (df["plume_group"] == containment_info.plume_group)
383
403
  ]["amount"]
384
404
  )
385
405
  sort_value_secondary = np.sum(
386
406
  df[
387
407
  (df["phase"] == "total")
388
408
  & (df["containment"] == "outside")
389
- & (df["zone"] == containment_info["zone"])
390
- & (df["region"] == containment_info["region"])
391
- & (df["plume_group"] == containment_info["plume_group"])
409
+ & (df["zone"] == containment_info.zone)
410
+ & (df["region"] == containment_info.region)
411
+ & (df["plume_group"] == containment_info.plume_group)
392
412
  ]["amount"]
393
413
  )
394
414
  df["real"] = [label] * df.shape[0]
@@ -422,19 +442,18 @@ def _change_names(
422
442
  df["name"] = df["name"].replace(f"{m}, all", m)
423
443
 
424
444
 
425
- def _adjust_figure(fig: go.Figure, plot_title: Optional[str] = None) -> None:
445
+ def _adjust_figure(fig: go.Figure, plot_title: str) -> None:
426
446
  fig.layout.legend.orientation = "v"
427
447
  fig.layout.legend.title.text = ""
428
448
  fig.layout.legend.itemwidth = 40
429
449
  fig.layout.xaxis.exponentformat = "power"
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
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
437
455
  fig.layout.title.x = 0.4
456
+
438
457
  fig.layout.paper_bgcolor = "rgba(0,0,0,0)"
439
458
  fig.layout.margin.b = 6
440
459
  fig.layout.margin.l = 10
@@ -476,13 +495,14 @@ def generate_co2_volume_figure(
476
495
  table_provider: ContainmentDataProvider,
477
496
  realizations: List[int],
478
497
  scale: Union[Co2MassScale, Co2VolumeScale],
479
- containment_info: Dict[str, Any],
498
+ containment_info: ContainmentInfo,
499
+ legendonly_traces: Optional[List[str]],
480
500
  ) -> go.Figure:
481
501
  df = _read_terminal_co2_volumes(
482
502
  table_provider, realizations, scale, containment_info
483
503
  )
484
- color_choice = containment_info["color_choice"]
485
- mark_choice = containment_info["mark_choice"]
504
+ color_choice = containment_info.color_choice
505
+ mark_choice = containment_info.mark_choice
486
506
  _add_prop_to_df(df, [str(r) for r in realizations], "real")
487
507
  cat_ord, colors, marks = _prepare_pattern_and_color_options(
488
508
  df,
@@ -506,9 +526,11 @@ def generate_co2_volume_figure(
506
526
  hovertemplate="Type: %{customdata[0]}<br>Amount: %{x:.3f}<br>"
507
527
  "Realization: %{y}<br>Proportion: %{customdata[1]}<extra></extra>",
508
528
  )
529
+ if legendonly_traces is not None:
530
+ _toggle_trace_visibility(fig.data, legendonly_traces)
509
531
  fig.layout.yaxis.title = "Realization"
510
532
  fig.layout.xaxis.title = scale.value
511
- _adjust_figure(fig, plot_title=containment_info["date_option"])
533
+ _adjust_figure(fig, plot_title=_make_title(containment_info))
512
534
  return fig
513
535
 
514
536
 
@@ -518,14 +540,14 @@ def generate_co2_time_containment_one_realization_figure(
518
540
  scale: Union[Co2MassScale, Co2VolumeScale],
519
541
  time_series_realization: int,
520
542
  y_limits: List[Optional[float]],
521
- containment_info: Dict[str, Any],
543
+ containment_info: ContainmentInfo,
522
544
  ) -> go.Figure:
523
545
  df = _read_co2_volumes(table_provider, [time_series_realization], scale)
524
- color_choice = containment_info["color_choice"]
525
- mark_choice = containment_info["mark_choice"]
546
+ color_choice = containment_info.color_choice
547
+ mark_choice = containment_info.mark_choice
526
548
  _filter_columns(df, color_choice, mark_choice, containment_info)
527
549
  _filter_rows(df, color_choice, mark_choice)
528
- if containment_info["sorting"] == "marking" and mark_choice != "none":
550
+ if containment_info.sorting == "marking" and mark_choice != "none":
529
551
  sort_order = ["date", mark_choice]
530
552
  else:
531
553
  sort_order = ["date", color_choice]
@@ -562,7 +584,7 @@ def generate_co2_time_containment_one_realization_figure(
562
584
  fig.layout.yaxis.range = y_limits
563
585
  fig.layout.xaxis.title = "Time"
564
586
  fig.layout.yaxis.title = scale.value
565
- _adjust_figure(fig)
587
+ _adjust_figure(fig, plot_title=_make_title(containment_info, include_date=False))
566
588
  return fig
567
589
 
568
590
 
@@ -702,23 +724,27 @@ def _connect_plume_groups(
702
724
  df.drop(columns="is_merged", inplace=True)
703
725
 
704
726
 
705
- # pylint: disable=too-many-locals
727
+ # pylint: disable=too-many-locals, too-many-statements
706
728
  def generate_co2_time_containment_figure(
707
729
  table_provider: ContainmentDataProvider,
708
730
  realizations: List[int],
709
731
  scale: Union[Co2MassScale, Co2VolumeScale],
710
- containment_info: Dict[str, Any],
732
+ containment_info: ContainmentInfo,
733
+ legendonly_traces: Optional[List[str]],
711
734
  ) -> go.Figure:
712
735
  df = _read_co2_volumes(table_provider, realizations, scale)
713
- color_choice = containment_info["color_choice"]
714
- mark_choice = containment_info["mark_choice"]
736
+ color_choice = containment_info.color_choice
737
+ mark_choice = containment_info.mark_choice
715
738
  _filter_columns(df, color_choice, mark_choice, containment_info)
716
739
  options = _prepare_line_type_and_color_options(
717
740
  df, containment_info, color_choice, mark_choice
718
741
  )
719
- active_cols_at_startup = list(
720
- options[options["line_type"].isin(["solid", "0px"])]["name"]
721
- )
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
722
748
  if "plume_group" in df:
723
749
  try:
724
750
  _connect_plume_groups(df, color_choice, mark_choice)
@@ -737,7 +763,7 @@ def generate_co2_time_containment_figure(
737
763
  "legendgroup": name,
738
764
  "name": name,
739
765
  }
740
- if name not in active_cols_at_startup:
766
+ if name in inactive_cols_at_startup:
741
767
  args["visible"] = "legendonly"
742
768
  fig.add_scatter(y=[0.0], **dummy_args, **args)
743
769
 
@@ -746,7 +772,7 @@ def generate_co2_time_containment_figure(
746
772
  "Realization: %{meta[0]}<br>Proportion: %{customdata}"
747
773
  )
748
774
 
749
- if containment_info["use_stats"]:
775
+ if containment_info.use_stats:
750
776
  df_no_real = df.drop(columns=["REAL", "realization"]).reset_index(drop=True)
751
777
  if mark_choice == "none":
752
778
  df_grouped = df_no_real.groupby(
@@ -756,11 +782,11 @@ def generate_co2_time_containment_figure(
756
782
  df_grouped = df_no_real.groupby(
757
783
  ["date", "name", color_choice, mark_choice], as_index=False
758
784
  )
759
- df_mean = df_grouped.agg(np.mean)
785
+ df_mean = df_grouped.agg("mean")
760
786
  df_mean["realization"] = ["mean"] * df_mean.shape[0]
761
- df_p10 = df_grouped.agg(lambda x: np.quantile(x, 0.1))
787
+ df_p10 = df_grouped.agg(lambda x: np.quantile(x, 0.9))
762
788
  df_p10["realization"] = ["p10"] * df_p10.shape[0]
763
- df_p90 = df_grouped.agg(lambda x: np.quantile(x, 0.9))
789
+ df_p90 = df_grouped.agg(lambda x: np.quantile(x, 0.1))
764
790
  df_p90["realization"] = ["p90"] * df_p90.shape[0]
765
791
  df = (
766
792
  pd.concat([df_mean, df_p10, df_p90])
@@ -775,7 +801,7 @@ def generate_co2_time_containment_figure(
775
801
  for rlz in realizations:
776
802
  lwd = 1.5 if rlz in ["p10", "p90"] else 2.5
777
803
  sub_df = df[df["realization"] == rlz].copy().reset_index(drop=True)
778
- if not containment_info["use_stats"]:
804
+ if not containment_info.use_stats:
779
805
  _add_prop_to_df(
780
806
  sub_df, np.unique(df["date"]), "date", [color_choice, mark_choice]
781
807
  )
@@ -797,9 +823,9 @@ def generate_co2_time_containment_figure(
797
823
  "meta": [rlz, name],
798
824
  "hovertemplate": hover_template,
799
825
  }
800
- if not containment_info["use_stats"]:
826
+ if not containment_info.use_stats:
801
827
  args["customdata"] = sub_df[sub_df["name"] == name]["prop"]
802
- if name not in active_cols_at_startup:
828
+ if name in inactive_cols_at_startup:
803
829
  args["visible"] = "legendonly"
804
830
  fig.add_scatter(
805
831
  y=sub_df[sub_df["name"] == name]["amount"], **args, **common_args
@@ -808,7 +834,7 @@ def generate_co2_time_containment_figure(
808
834
  fig.layout.xaxis.title = "Time"
809
835
  fig.layout.yaxis.title = scale.value
810
836
  fig.layout.yaxis.autorange = True
811
- _adjust_figure(fig)
837
+ _adjust_figure(fig, plot_title=_make_title(containment_info, include_date=False))
812
838
  return fig
813
839
 
814
840
 
@@ -816,14 +842,15 @@ def generate_co2_statistics_figure(
816
842
  table_provider: ContainmentDataProvider,
817
843
  realizations: List[int],
818
844
  scale: Union[Co2MassScale, Co2VolumeScale],
819
- containment_info: Dict[str, Any],
845
+ containment_info: ContainmentInfo,
846
+ legend_only_traces: Optional[List[str]],
820
847
  ) -> go.Figure:
821
- date_option = containment_info["date_option"]
848
+ date_option = containment_info.date_option
822
849
  df = _read_co2_volumes(table_provider, realizations, scale)
823
850
  df = df[df["date"] == date_option]
824
851
  df = df.drop(columns=["date"]).reset_index(drop=True)
825
- color_choice = containment_info["color_choice"]
826
- mark_choice = containment_info["mark_choice"]
852
+ color_choice = containment_info.color_choice
853
+ mark_choice = containment_info.mark_choice
827
854
  _filter_columns(df, color_choice, mark_choice, containment_info)
828
855
  cat_ord, colors, line_types = _prepare_pattern_and_color_options_statistics_plot(
829
856
  df,
@@ -847,10 +874,11 @@ def generate_co2_statistics_figure(
847
874
  category_orders=cat_ord,
848
875
  )
849
876
 
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"
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)
854
882
 
855
883
  fig.update_traces(
856
884
  hovertemplate="Type: %{data.name}<br>Amount: %{x:.3f}<br>"
@@ -860,6 +888,196 @@ def generate_co2_statistics_figure(
860
888
  fig.layout.legend.tracegroupgap = 0
861
889
  fig.layout.xaxis.title = scale.value
862
890
  fig.layout.yaxis.title = "Probability"
863
- _adjust_figure(fig, plot_title=containment_info["date_option"])
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))
864
1001
 
865
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