webviz-subsurface 0.2.30__py3-none-any.whl → 0.2.31__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 (19) hide show
  1. webviz_subsurface/_components/tornado/_tornado_data.py +3 -0
  2. webviz_subsurface/_providers/ensemble_surface_provider/surface_array_server.py +0 -1
  3. webviz_subsurface/_providers/ensemble_surface_provider/surface_image_server.py +0 -1
  4. webviz_subsurface/plugins/_co2_leakage/_plugin.py +79 -37
  5. webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +99 -38
  6. webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +417 -355
  7. webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +2 -7
  8. webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +15 -11
  9. webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +13 -4
  10. webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +93 -33
  11. webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +301 -116
  12. webviz_subsurface/plugins/_volumetric_analysis/controllers/tornado_controllers.py +5 -1
  13. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/METADATA +34 -34
  14. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/RECORD +19 -19
  15. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/WHEEL +1 -1
  16. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/LICENSE +0 -0
  17. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/LICENSE.chromedriver +0 -0
  18. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/entry_points.txt +0 -0
  19. {webviz_subsurface-0.2.30.dist-info → webviz_subsurface-0.2.31.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
+ import warnings
2
+ from datetime import datetime as dt
1
3
  from typing import Any, Dict, List, Optional, Tuple, Union
2
4
 
3
5
  import numpy as np
4
- import pandas
6
+ import pandas as pd
5
7
  import plotly.express as px
6
8
  import plotly.graph_objects as go
7
9
 
@@ -10,7 +12,6 @@ from webviz_subsurface._utils.enum_shim import StrEnum
10
12
  from webviz_subsurface.plugins._co2_leakage._utilities.generic import (
11
13
  Co2MassScale,
12
14
  Co2VolumeScale,
13
- ContainmentViews,
14
15
  )
15
16
 
16
17
 
@@ -45,100 +46,51 @@ def _read_dataframe(
45
46
  table_provider: EnsembleTableProvider,
46
47
  realization: int,
47
48
  scale_factor: float,
48
- containment_info: Dict[str, Union[str, None, List[str]]],
49
- ) -> pandas.DataFrame:
49
+ ) -> pd.DataFrame:
50
50
  df = table_provider.get_column_data(table_provider.column_names(), [realization])
51
- if any(split in list(df.columns) for split in ["zone", "region"]):
52
- df = _process_containment_information(df, containment_info)
53
- if containment_info["containment_view"] != ContainmentViews.CONTAINMENTSPLIT:
54
- df["aqueous"] = (
55
- df["aqueous_contained"]
56
- + df["aqueous_outside"]
57
- + df["aqueous_hazardous"]
58
- )
59
- df["gas"] = df["gas_contained"] + df["gas_outside"] + df["gas_hazardous"]
60
- df = df.drop(
61
- columns=[
62
- "aqueous_contained",
63
- "aqueous_outside",
64
- "aqueous_hazardous",
65
- "gas_contained",
66
- "gas_outside",
67
- "gas_hazardous",
68
- ]
69
- )
70
51
  if scale_factor == 1.0:
71
52
  return df
72
- for col in df.columns:
73
- if col not in ["date", "zone", "region"]:
74
- df[col] /= scale_factor
53
+ df["amount"] /= scale_factor
75
54
  return df
76
55
 
77
56
 
78
- def read_zone_and_region_options(
57
+ def read_menu_options(
79
58
  table_provider: EnsembleTableProvider,
80
59
  realization: int,
60
+ relpath: str,
81
61
  ) -> Dict[str, List[str]]:
82
- df = table_provider.get_column_data(table_provider.column_names(), [realization])
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
+ )
83
72
  zones = ["all"]
84
- if "zone" in list(df.columns):
85
- for zone in list(df["zone"]):
86
- if zone not in zones:
87
- zones.append(zone)
73
+ for zone in list(df["zone"]):
74
+ if zone not in zones:
75
+ zones.append(zone)
88
76
  regions = ["all"]
89
- if "region" in list(df.columns):
90
- for region in list(df["region"]):
91
- if region not in regions:
92
- regions.append(region)
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"]
93
84
  return {
94
85
  "zones": zones if len(zones) > 1 else [],
95
86
  "regions": regions if len(regions) > 1 else [],
87
+ "phases": phases,
96
88
  }
97
89
 
98
90
 
99
- def _process_containment_information(
100
- df: pandas.DataFrame,
101
- containment_info: Dict[str, Union[str, None, List[str]]],
102
- ) -> pandas.DataFrame:
103
- view = containment_info["containment_view"]
104
- if view == ContainmentViews.ZONESPLIT:
105
- return (
106
- df[df["zone"] != "all"]
107
- .drop(columns="region", errors="ignore")
108
- .reset_index(drop=True)
109
- )
110
- if view == ContainmentViews.REGIONSPLIT:
111
- return (
112
- df[df["region"] != "all"]
113
- .drop(columns="zone", errors="ignore")
114
- .reset_index(drop=True)
115
- )
116
- zone = containment_info["zone"]
117
- region = containment_info["region"]
118
- if zone not in ["all", None]:
119
- if zone in list(df["zone"]):
120
- return df[df["zone"] == zone].drop(
121
- columns=["zone", "region"], errors="ignore"
122
- )
123
- print(f"Zone {zone} not found, using sum for each unique date.")
124
- elif region not in ["all", None]:
125
- if region in list(df["region"]):
126
- return df[df["region"] == region].drop(
127
- columns=["zone", "region"], errors="ignore"
128
- )
129
- print(f"Region {region} not found, using sum for each unique date.")
130
- if "zone" in list(df.columns):
131
- if "region" in list(df.columns):
132
- return df[
133
- [a and b for a, b in zip(df["zone"] == "all", df["region"] == "all")]
134
- ].drop(columns=["zone", "region"])
135
- df = df[df["zone"] == "all"].drop(columns=["zone"])
136
- elif "region" in list(df.columns):
137
- df = df[df["region"] == "all"].drop(columns=["region"])
138
- return df
139
-
140
-
141
- def _split_colors(num_cols: int, split: str = "zone") -> List[str]:
91
+ def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]:
92
+ if split == "containment":
93
+ return [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED]
142
94
  options = list(_COLOR_ZONES)
143
95
  if split == "region":
144
96
  options.reverse()
@@ -149,6 +101,130 @@ def _split_colors(num_cols: int, split: str = "zone") -> List[str]:
149
101
  return new_cols[:num_cols]
150
102
 
151
103
 
104
+ def _get_marks(num_marks: int, mark_choice: str) -> List[str]:
105
+ if mark_choice == "none":
106
+ return [""] * num_marks
107
+ if mark_choice == "containment":
108
+ return ["x", "/", ""]
109
+ if mark_choice in ["zone", "region"]:
110
+ base_pattern = ["", "/", "x", "-", "\\", "+", "|", "."]
111
+ if num_marks > len(base_pattern):
112
+ base_pattern *= int(np.ceil(num_marks / len(base_pattern)))
113
+ warnings.warn(
114
+ f"More {mark_choice}s than pattern options. "
115
+ f"Some {mark_choice}s will share pattern."
116
+ )
117
+ return base_pattern[:num_marks]
118
+ return ["", "/"] if num_marks == 2 else ["", ".", "/"]
119
+
120
+
121
+ def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]:
122
+ if mark_choice == "none":
123
+ return ["solid"]
124
+ if mark_choice == "containment":
125
+ return ["dash", "dot", "solid"]
126
+ if mark_choice in ["zone", "region"]:
127
+ if len(mark_options) > 8:
128
+ warnings.warn(
129
+ f"Large number of {mark_choice}s might make it hard "
130
+ f"to distinguish different dashed lines."
131
+ )
132
+ return [
133
+ f"{round(i / len(mark_options) * 25)}px" for i in range(len(mark_options))
134
+ ]
135
+ return ["dot", "dash"] if "gas" in mark_options else ["dot", "dashdot", "dash"]
136
+
137
+
138
+ def _prepare_pattern_and_color_options(
139
+ df: pd.DataFrame,
140
+ containment_info: Dict,
141
+ color_choice: str,
142
+ mark_choice: str,
143
+ ) -> 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"]
146
+ num_colors = len(color_options)
147
+ num_marks = num_colors if mark_choice == "none" else len(mark_options)
148
+ marks = _get_marks(num_marks, mark_choice)
149
+ colors = _get_colors(num_colors, color_choice)
150
+ if mark_choice == "none":
151
+ cat_ord = {"type": color_options}
152
+ df["type"] = df[color_choice]
153
+ return cat_ord, colors, marks
154
+ df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
155
+ if containment_info["sorting"] == "color":
156
+ cat_ord = {
157
+ "type": [", ".join((c, m)) for c in color_options for m in mark_options],
158
+ }
159
+ colors = [c for c in colors for _ in range(num_marks)]
160
+ marks = marks * num_colors
161
+ else:
162
+ cat_ord = {
163
+ "type": [", ".join((c, m)) for m in mark_options for c in color_options],
164
+ }
165
+ colors = colors * num_marks
166
+ marks = [m for m in marks for _ in range(num_colors)]
167
+ return cat_ord, colors, marks
168
+
169
+
170
+ def _prepare_line_type_and_color_options(
171
+ df: pd.DataFrame,
172
+ containment_info: Dict,
173
+ color_choice: str,
174
+ mark_choice: str,
175
+ ) -> pd.DataFrame:
176
+ mark_options = []
177
+ if mark_choice != "none":
178
+ mark_options = list(containment_info[f"{mark_choice}s"])
179
+ color_options = list(containment_info[f"{color_choice}s"])
180
+ num_colors = len(color_options)
181
+ line_types = _get_line_types(mark_options, mark_choice)
182
+ colors = _get_colors(num_colors, color_choice)
183
+ filter_mark = True
184
+ if mark_choice == "phase":
185
+ mark_options = ["total"] + mark_options
186
+ line_types = ["solid"] + line_types
187
+ filter_mark = False
188
+ if color_choice == "containment":
189
+ color_options = ["total"] + color_options
190
+ colors = ["black"] + colors
191
+ else:
192
+ _filter_rows(df, color_choice, mark_choice, filter_mark)
193
+ if mark_choice == "none":
194
+ df["name"] = df[color_choice]
195
+ return pd.DataFrame(
196
+ {
197
+ "name": color_options,
198
+ "color": colors,
199
+ "line_type": line_types * len(colors),
200
+ }
201
+ )
202
+ df["name"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])]
203
+ _change_names(df, color_options, mark_options)
204
+ if containment_info["sorting"] == "color":
205
+ options = pd.DataFrame(
206
+ {
207
+ "name": [
208
+ ", ".join((c, m)) for c in color_options for m in mark_options
209
+ ],
210
+ "color": [c for c in colors for _ in mark_options],
211
+ "line_type": [l for _ in colors for l in line_types],
212
+ }
213
+ )
214
+ else:
215
+ options = pd.DataFrame(
216
+ {
217
+ "name": [
218
+ ", ".join((c, m)) for m in mark_options for c in color_options
219
+ ],
220
+ "color": [c for _ in mark_options for c in colors],
221
+ "line_type": [l for l in line_types for _ in colors],
222
+ }
223
+ )
224
+ _change_names(options, color_options, mark_options)
225
+ return options
226
+
227
+
152
228
  def _find_scale_factor(
153
229
  table_provider: EnsembleTableProvider,
154
230
  scale: Union[Co2MassScale, Co2VolumeScale],
@@ -168,110 +244,161 @@ def _read_terminal_co2_volumes(
168
244
  realizations: List[int],
169
245
  scale: Union[Co2MassScale, Co2VolumeScale],
170
246
  containment_info: Dict[str, Union[str, None, List[str]]],
171
- ) -> pandas.DataFrame:
172
- view = containment_info["containment_view"]
247
+ ) -> pd.DataFrame:
173
248
  records: Dict[str, List[Any]] = {
174
249
  "real": [],
175
250
  "amount": [],
176
- "phase": [],
177
251
  "sort_key": [],
178
252
  "sort_key_secondary": [],
179
253
  }
180
- if view == ContainmentViews.ZONESPLIT:
181
- records["zone"] = []
182
- elif view == ContainmentViews.REGIONSPLIT:
183
- records["region"] = []
184
- else:
185
- records["containment"] = []
254
+ color_choice = containment_info["color_choice"]
255
+ mark_choice = containment_info["mark_choice"]
256
+ assert isinstance(color_choice, str)
257
+ assert isinstance(mark_choice, str)
258
+ records[color_choice] = []
259
+ if mark_choice != "none":
260
+ records[mark_choice] = []
186
261
  scale_factor = _find_scale_factor(table_provider, scale)
262
+ data_frame = None
187
263
  for real in realizations:
188
- df = _read_dataframe(table_provider, real, scale_factor, containment_info)
189
- if view != ContainmentViews.CONTAINMENTSPLIT:
190
- split = "zone" if view == ContainmentViews.ZONESPLIT else "region"
191
- last_ = df[df["date"] == np.max(df["date"])]
192
- for i in range(last_.shape[0]):
193
- last = last_.iloc[i]
194
- label = str(real)
195
-
196
- records["real"] += [label] * 2
197
- records["amount"] += [
198
- last["aqueous"],
199
- last["gas"],
200
- ]
201
- records["phase"] += ["aqueous", "gas"]
202
- records[split] += [last[split]] * 2
203
- records["sort_key"] += [label] * 2
204
- records["sort_key_secondary"] += [last[split]] * 2
264
+ df = _read_dataframe(table_provider, real, scale_factor)
265
+ df = df[df["date"] == np.max(df["date"])]
266
+ _add_sort_key_and_real(df, str(real), containment_info)
267
+ _filter_columns(df, color_choice, mark_choice, containment_info)
268
+ _filter_rows(df, color_choice, mark_choice)
269
+ if data_frame is None:
270
+ data_frame = df
205
271
  else:
206
- last = df.iloc[np.argmax(df["date"])]
207
- label = str(real)
208
-
209
- records["real"] += [label] * 6
210
- records["amount"] += [
211
- last["aqueous_contained"],
212
- last["gas_contained"],
213
- last["aqueous_outside"],
214
- last["gas_outside"],
215
- last["aqueous_hazardous"],
216
- last["gas_hazardous"],
217
- ]
218
- records["containment"] += [
219
- "contained",
220
- "contained",
221
- "outside",
222
- "outside",
223
- "hazardous",
224
- "hazardous",
225
- ]
226
- records["phase"] += ["aqueous", "gas", "aqueous", "gas", "aqueous", "gas"]
227
- records["sort_key"] += [last["gas_hazardous"]] * 6
228
- records["sort_key_secondary"] += [last["gas_outside"]] * 6
229
- df = pandas.DataFrame.from_dict(records)
230
- df.sort_values(
272
+ data_frame = pd.concat([data_frame, df])
273
+ assert data_frame is not None
274
+ data_frame.sort_values(
231
275
  ["sort_key", "sort_key_secondary"], inplace=True, ascending=[True, True]
232
276
  )
233
- return df
277
+ return data_frame
278
+
279
+
280
+ def _filter_columns(
281
+ df: pd.DataFrame,
282
+ color_choice: str,
283
+ mark_choice: str,
284
+ containment_info: Dict,
285
+ ) -> None:
286
+ filter_columns = [
287
+ col
288
+ for col in ["phase", "containment", "zone", "region"]
289
+ if col not in [mark_choice, color_choice]
290
+ ]
291
+ for col in filter_columns:
292
+ df.query(f'{col} == "{containment_info[col]}"', inplace=True)
293
+ df.drop(columns=filter_columns, inplace=True)
294
+
295
+
296
+ def _filter_rows(
297
+ df: pd.DataFrame,
298
+ color_choice: str,
299
+ mark_choice: str,
300
+ filter_mark: bool = True,
301
+ ) -> None:
302
+ df.query(f'{color_choice} not in ["total", "all"]', inplace=True)
303
+ if mark_choice != "none" and filter_mark:
304
+ df.query(f'{mark_choice} not in ["total", "all"]', inplace=True)
305
+
306
+
307
+ def _add_sort_key_and_real(
308
+ df: pd.DataFrame,
309
+ label: str,
310
+ containment_info: Dict,
311
+ ) -> None:
312
+ sort_value = np.sum(
313
+ df[
314
+ (df["phase"] == "total")
315
+ & (df["containment"] == "hazardous")
316
+ & (df["zone"] == containment_info["zone"])
317
+ & (df["region"] == containment_info["region"])
318
+ ]["amount"]
319
+ )
320
+ sort_value_secondary = np.sum(
321
+ df[
322
+ (df["phase"] == "total")
323
+ & (df["containment"] == "outside")
324
+ & (df["zone"] == containment_info["zone"])
325
+ & (df["region"] == containment_info["region"])
326
+ ]["amount"]
327
+ )
328
+ df["real"] = [label] * df.shape[0]
329
+ df["sort_key"] = [sort_value] * df.shape[0]
330
+ df["sort_key_secondary"] = [sort_value_secondary] * df.shape[0]
234
331
 
235
332
 
236
333
  def _read_co2_volumes(
237
334
  table_provider: EnsembleTableProvider,
238
335
  realizations: List[int],
239
336
  scale: Union[Co2MassScale, Co2VolumeScale],
240
- containment_info: Dict[str, Union[str, None, List[str]]],
241
- ) -> pandas.DataFrame:
337
+ ) -> pd.DataFrame:
242
338
  scale_factor = _find_scale_factor(table_provider, scale)
243
- return pandas.concat(
339
+ return pd.concat(
244
340
  [
245
- _read_dataframe(
246
- table_provider, real, scale_factor, containment_info
247
- ).assign(realization=real)
248
- for real in realizations
341
+ _read_dataframe(table_provider, r, scale_factor).assign(realization=r)
342
+ for r in realizations
249
343
  ]
250
344
  )
251
345
 
252
346
 
253
- def _change_type_names(df: pandas.DataFrame) -> None:
254
- df["type"] = df["type"].replace("total", "Total")
255
- df["type"] = df["type"].replace("total_contained", "Contained")
256
- df["type"] = df["type"].replace("total_outside", "Outside")
257
- df["type"] = df["type"].replace("total_hazardous", "Hazardous")
258
- df["type"] = df["type"].replace("total_gas", "Gas")
259
- df["type"] = df["type"].replace("total_aqueous", "Aqueous")
260
- df["type"] = df["type"].replace("gas_contained", "Contained mobile gas")
261
- df["type"] = df["type"].replace("gas_outside", "Outside mobile gas")
262
- df["type"] = df["type"].replace("gas_hazardous", "Hazardous mobile gas")
263
- df["type"] = df["type"].replace("aqueous_contained", "Contained aqueous")
264
- df["type"] = df["type"].replace("aqueous_outside", "Outside aqueous")
265
- df["type"] = df["type"].replace("aqueous_hazardous", "Hazardous aqueous")
347
+ def _change_names(
348
+ df: pd.DataFrame,
349
+ color_options: List[str],
350
+ mark_options: List[str],
351
+ ) -> None:
352
+ for m in mark_options + ["total", "all"]:
353
+ df["name"] = df["name"].replace(f"total, {m}", m)
354
+ df["name"] = df["name"].replace(f"all, {m}", m)
355
+ for m in color_options:
356
+ df["name"] = df["name"].replace(f"{m}, total", m)
357
+ df["name"] = df["name"].replace(f"{m}, all", m)
266
358
 
267
359
 
268
360
  def _adjust_figure(fig: go.Figure) -> None:
361
+ fig.layout.legend.orientation = "v"
362
+ fig.layout.legend.title.text = ""
363
+ fig.layout.legend.itemwidth = 40
364
+ fig.layout.xaxis.exponentformat = "power"
269
365
  fig.layout.title.x = 0.5
270
366
  fig.layout.paper_bgcolor = "rgba(0,0,0,0)"
271
367
  fig.layout.margin.b = 6
272
- fig.layout.margin.t = 40
368
+ fig.layout.margin.t = 15
273
369
  fig.layout.margin.l = 10
274
370
  fig.layout.margin.r = 10
371
+ fig.update_layout(
372
+ legend={
373
+ "x": 1.05,
374
+ "xanchor": "left",
375
+ }
376
+ )
377
+
378
+
379
+ def _add_prop_to_df(
380
+ df: pd.DataFrame,
381
+ list_to_iterate: Union[List, np.ndarray],
382
+ column: str,
383
+ filter_columns: Optional[List[str]] = None,
384
+ ) -> None:
385
+ prop = np.zeros(df.shape[0])
386
+ for element in list_to_iterate:
387
+ if filter_columns is None:
388
+ summed_amount = np.sum(df.loc[df[column] == element]["amount"])
389
+ else:
390
+ filter_for_sum = df[column] == element
391
+ for col in filter_columns:
392
+ if col in df.columns:
393
+ filter_for_sum &= ~df[col].isin(["total", "all"])
394
+ summed_amount = np.sum(df.loc[filter_for_sum]["amount"])
395
+ prop[np.where(df[column] == element)[0]] = summed_amount
396
+ nonzero = np.where(prop > 0)[0]
397
+ prop[nonzero] = (
398
+ np.round(np.array(df["amount"])[nonzero] / prop[nonzero] * 1000) / 10
399
+ )
400
+ df["prop"] = prop
401
+ df["prop"] = df["prop"].map(lambda p: str(p) + "%")
275
402
 
276
403
 
277
404
  def generate_co2_volume_figure(
@@ -283,43 +410,34 @@ def generate_co2_volume_figure(
283
410
  df = _read_terminal_co2_volumes(
284
411
  table_provider, realizations, scale, containment_info
285
412
  )
286
- if containment_info["containment_view"] == ContainmentViews.ZONESPLIT:
287
- color = "zone"
288
- cat_ord = {"zone": containment_info["zones"], "phase": ["gas", "aqueous"]}
289
- colors = _split_colors(len(containment_info["zones"]))
290
- elif containment_info["containment_view"] == ContainmentViews.REGIONSPLIT:
291
- color = "region"
292
- cat_ord = {"region": containment_info["regions"], "phase": ["gas", "aqueous"]}
293
- colors = _split_colors(len(containment_info["regions"]), "region")
294
- else:
295
- color = "containment"
296
- cat_ord = {
297
- "containment": ["hazardous", "outside", "contained"],
298
- "phase": ["gas", "aqueous"],
299
- }
300
- colors = [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED]
413
+ color_choice = containment_info["color_choice"]
414
+ mark_choice = containment_info["mark_choice"]
415
+ _add_prop_to_df(df, [str(r) for r in realizations], "real")
416
+ cat_ord, colors, marks = _prepare_pattern_and_color_options(
417
+ df,
418
+ containment_info,
419
+ color_choice,
420
+ mark_choice,
421
+ )
301
422
  fig = px.bar(
302
423
  df,
303
424
  y="real",
304
425
  x="amount",
305
- color=color,
306
- pattern_shape="phase",
307
- title="End-state CO<sub>2</sub> containment (all realizations)",
426
+ color="type",
427
+ color_discrete_sequence=colors,
428
+ pattern_shape="type" if mark_choice != "none" else None,
429
+ pattern_shape_sequence=marks,
308
430
  orientation="h",
309
431
  category_orders=cat_ord,
310
- color_discrete_sequence=colors,
432
+ hover_data={"prop": True, "real": False},
311
433
  )
312
- fig.layout.legend.title.text = ""
313
- fig.layout.legend.orientation = "h"
314
- fig.layout.legend.y = -0.3
315
- fig.layout.legend.font = {"size": 8}
316
434
  fig.layout.yaxis.title = "Realization"
317
- fig.layout.xaxis.exponentformat = "power"
318
435
  fig.layout.xaxis.title = scale.value
319
436
  _adjust_figure(fig)
320
437
  return fig
321
438
 
322
439
 
440
+ # pylint: disable=too-many-locals
323
441
  def generate_co2_time_containment_one_realization_figure(
324
442
  table_provider: EnsembleTableProvider,
325
443
  scale: Union[Co2MassScale, Co2VolumeScale],
@@ -327,232 +445,176 @@ def generate_co2_time_containment_one_realization_figure(
327
445
  y_limits: List[Optional[float]],
328
446
  containment_info: Dict[str, Any],
329
447
  ) -> go.Figure:
330
- df = _read_co2_volumes(
331
- table_provider, [time_series_realization], scale, containment_info
332
- )
333
- df.sort_values(by="date", inplace=True)
334
- df = df.drop(
335
- columns=[
336
- "realization",
337
- "REAL",
338
- "total",
339
- "total_contained",
340
- "total_outside",
341
- "total_hazardous",
342
- "total_gas",
343
- "total_aqueous",
344
- ]
345
- )
346
- if containment_info["containment_view"] == ContainmentViews.ZONESPLIT:
347
- df = pandas.melt(df, id_vars=["date", "zone"])
348
- df["variable"] = df["zone"] + ", " + df["variable"]
349
- df = df.drop(columns=["zone"])
350
- cat_ord = {
351
- "type": [
352
- zone_name + ", " + phase
353
- for zone_name in containment_info["zones"]
354
- for phase in ["gas", "aqueous"]
355
- ]
356
- }
357
- pattern = ["", "/"] * len(containment_info["zones"])
358
- colors = [
359
- col
360
- for col in _split_colors(len(containment_info["zones"]))
361
- for i in range(2)
362
- ]
363
- elif containment_info["containment_view"] == ContainmentViews.REGIONSPLIT:
364
- df = pandas.melt(df, id_vars=["date", "region"])
365
- df["variable"] = df["region"] + ", " + df["variable"]
366
- df = df.drop(columns=["region"])
367
- cat_ord = {
368
- "type": [
369
- region_name + ", " + phase
370
- for region_name in containment_info["regions"]
371
- for phase in ["gas", "aqueous"]
372
- ]
373
- }
374
- pattern = ["", "/"] * len(containment_info["regions"])
375
- colors = [
376
- col
377
- for col in _split_colors(len(containment_info["regions"]), "region")
378
- for i in range(2)
379
- ]
448
+ df = _read_co2_volumes(table_provider, [time_series_realization], scale)
449
+ color_choice = containment_info["color_choice"]
450
+ mark_choice = containment_info["mark_choice"]
451
+ _filter_columns(df, color_choice, mark_choice, containment_info)
452
+ _filter_rows(df, color_choice, mark_choice)
453
+ if containment_info["sorting"] == "marking" and mark_choice != "none":
454
+ sort_order = ["date", mark_choice]
380
455
  else:
381
- df = pandas.melt(df, id_vars=["date"])
382
- cat_ord = {
383
- "type": [
384
- "Hazardous mobile gas",
385
- "Hazardous aqueous",
386
- "Outside mobile gas",
387
- "Outside aqueous",
388
- "Contained mobile gas",
389
- "Contained aqueous",
390
- ]
391
- }
392
- pattern = ["", "/"] * 3
393
- colors = [
394
- _COLOR_HAZARDOUS,
395
- _COLOR_HAZARDOUS,
396
- _COLOR_OUTSIDE,
397
- _COLOR_OUTSIDE,
398
- _COLOR_CONTAINED,
399
- _COLOR_CONTAINED,
400
- ]
401
- df = df.rename(columns={"value": "mass", "variable": "type"})
402
- df.sort_values(by="date", inplace=True)
403
- _change_type_names(df)
456
+ sort_order = ["date", color_choice]
457
+ df.sort_values(by=sort_order, inplace=True)
404
458
  if y_limits[0] is None and y_limits[1] is not None:
405
459
  y_limits[0] = 0.0
406
460
  elif y_limits[1] is None and y_limits[0] is not None:
407
- y_limits[1] = max(df.groupby("date")["mass"].sum()) * 1.05
461
+ y_limits[1] = max(df.groupby("date")["amount"].sum()) * 1.05
462
+
463
+ _add_prop_to_df(df, np.unique(df["date"]), "date")
464
+ cat_ord, colors, marks = _prepare_pattern_and_color_options(
465
+ df,
466
+ containment_info,
467
+ color_choice,
468
+ mark_choice,
469
+ )
408
470
  fig = px.area(
409
471
  df,
410
472
  x="date",
411
- y="mass",
473
+ y="amount",
412
474
  color="type",
413
- category_orders=cat_ord,
414
475
  color_discrete_sequence=colors,
415
- pattern_shape="type",
416
- pattern_shape_sequence=pattern, # ['', '/', '\\', 'x', '-', '|', '+', '.'],
476
+ pattern_shape="type" if mark_choice != "none" else None,
477
+ pattern_shape_sequence=marks,
478
+ category_orders=cat_ord,
417
479
  range_y=y_limits,
480
+ hover_data={
481
+ "prop": True,
482
+ "amount": ":.3f",
483
+ },
418
484
  )
485
+ _add_hover_info_in_field(fig, df, cat_ord, colors)
419
486
  fig.layout.yaxis.range = y_limits
420
- fig.layout.legend.orientation = "h"
421
- fig.layout.legend.title.text = ""
422
- fig.layout.legend.y = -0.3
423
- fig.layout.legend.font = {"size": 8}
424
- fig.layout.title = "CO<sub>2</sub> containment for realization: " + str(
425
- time_series_realization
426
- )
427
487
  fig.layout.xaxis.title = "Time"
428
488
  fig.layout.yaxis.title = scale.value
429
- fig.layout.yaxis.exponentformat = "power"
430
489
  _adjust_figure(fig)
431
490
  return fig
432
491
 
433
492
 
434
- def _prepare_time_figure_options(
435
- df: pandas.DataFrame,
436
- containment_info: Dict[str, Any],
437
- ) -> Tuple[pandas.DataFrame, Dict[str, Tuple[str, str, str]], List[str]]:
438
- view = containment_info["containment_view"]
439
- if view != ContainmentViews.CONTAINMENTSPLIT:
440
- split = "zone" if view == ContainmentViews.ZONESPLIT else "region"
441
- options = (
442
- containment_info["zones"]
443
- if split == "zone"
444
- else containment_info["regions"]
445
- )
446
- df = df.drop(
447
- columns=[
448
- "REAL",
449
- "total_gas",
450
- "total_aqueous",
451
- "total_contained",
452
- "total_outside",
453
- "total_hazardous",
454
- "region" if split == "zone" else "zone",
455
- ],
456
- errors="ignore",
457
- )
458
- df.sort_values(by=["date", "realization"], inplace=True)
459
- df_ = df[["date", "realization"]][df[split] == options[0]].reset_index(
460
- drop=True
461
- )
462
- for name in options:
463
- part_df = df[["total", "gas", "aqueous"]][df[split] == name]
464
- part_df = part_df.rename(
465
- columns={
466
- "total": name + ", total",
467
- "gas": name + ", gas",
468
- "aqueous": name + ", aqueous",
469
- }
470
- ).reset_index(drop=True)
471
- df_ = pandas.concat([df_, part_df], axis=1)
472
- colors = _split_colors(len(options), split)
473
- cols_to_plot = {}
474
- for phase, line_type in zip(
475
- ["total", "gas", "aqueous"], ["solid", "dot", "dash"]
476
- ):
477
- for name, col in zip(options, colors):
478
- cols_to_plot[name + ", " + phase] = (
479
- name + ", " + phase,
480
- line_type,
481
- col,
493
+ def spaced_dates(dates: List[str], num_between: int) -> Dict[str, List[str]]:
494
+ dates_list = [dt.strptime(date, "%Y-%m-%d") for date in dates]
495
+ date_dict: Dict[str, List[str]] = {date: [] for date in dates}
496
+ for i in range(len(dates_list) - 1):
497
+ date_dict[dates[i]].append(dates[i])
498
+ delta = (dates_list[i + 1] - dates_list[i]) / (num_between + 1)
499
+ for j in range(1, num_between + 1):
500
+ new_date = dates_list[i] + delta * j
501
+ if j <= num_between / 2:
502
+ date_dict[dates[i]].append(new_date.strftime("%Y-%m-%d"))
503
+ else:
504
+ date_dict[dates[i + 1]].append(new_date.strftime("%Y-%m-%d"))
505
+ date_dict[dates[-1]].append(dates[-1])
506
+ return date_dict
507
+
508
+
509
+ def _add_hover_info_in_field(
510
+ fig: go.Figure,
511
+ df: pd.DataFrame,
512
+ cat_ord: Dict,
513
+ colors: List,
514
+ ) -> None:
515
+ """
516
+ Plots additional, invisible points in the middle of each field in the third plot,
517
+ solely to display hover information inside the fields
518
+ (which is not possible directly with plotly.express.area)
519
+ """
520
+ months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
521
+ months += ["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
522
+ dates = np.unique(df["date"])
523
+ date_strings = {
524
+ date: f"{months[int(date.split('-')[1]) - 1]} {date.split('-')[0]}"
525
+ for date in dates
526
+ }
527
+ prev_vals = {date: 0 for date in dates}
528
+ date_dict = spaced_dates(dates, 4)
529
+ for name, color in zip(cat_ord["type"], colors):
530
+ sub_df = df[df["type"] == name]
531
+ for date in dates:
532
+ amount = sub_df[sub_df["date"] == date]["amount"].item()
533
+ prop = sub_df[sub_df["date"] == date]["prop"].item()
534
+ prev_val = prev_vals[date]
535
+ p15 = prev_val + 0.15 * amount
536
+ p85 = prev_val + 0.85 * amount
537
+ y_vals = np.linspace(p15, p85, 8).tolist() * len(date_dict[date])
538
+ y_vals.sort()
539
+ fig.add_trace(
540
+ go.Scatter(
541
+ x=date_dict[date] * 8,
542
+ y=y_vals,
543
+ mode="lines",
544
+ 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}",
547
+ opacity=0,
548
+ hoverinfo="text",
549
+ hoveron="points",
550
+ showlegend=False,
482
551
  )
483
- active_cols_at_startup = [name + ", total" for name in options]
484
- df = df_
485
- else:
486
- df.sort_values(by="date", inplace=True)
487
- cols_to_plot = {
488
- "Total": ("total", "solid", _COLOR_TOTAL),
489
- "Contained": ("total_contained", "solid", _COLOR_CONTAINED),
490
- "Outside": ("total_outside", "solid", _COLOR_OUTSIDE),
491
- "Hazardous": ("total_hazardous", "solid", _COLOR_HAZARDOUS),
492
- "Gas": ("total_gas", "dot", _COLOR_TOTAL),
493
- "Aqueous": ("total_aqueous", "dash", _COLOR_TOTAL),
494
- "Contained mobile gas": ("gas_contained", "dot", _COLOR_CONTAINED),
495
- "Outside mobile gas": ("gas_outside", "dot", _COLOR_OUTSIDE),
496
- "Hazardous mobile gas": ("gas_hazardous", "dot", _COLOR_HAZARDOUS),
497
- "Contained aqueous": ("aqueous_contained", "dash", _COLOR_CONTAINED),
498
- "Outside aqueous": ("aqueous_outside", "dash", _COLOR_OUTSIDE),
499
- "Hazardous aqueous": ("aqueous_hazardous", "dash", _COLOR_HAZARDOUS),
500
- }
501
- active_cols_at_startup = ["Total", "Outside", "Hazardous"]
502
- return df, cols_to_plot, active_cols_at_startup
552
+ )
553
+ prev_vals[date] = prev_val + amount
503
554
 
504
555
 
556
+ # pylint: disable=too-many-locals
505
557
  def generate_co2_time_containment_figure(
506
558
  table_provider: EnsembleTableProvider,
507
559
  realizations: List[int],
508
560
  scale: Union[Co2MassScale, Co2VolumeScale],
509
561
  containment_info: Dict[str, Any],
510
562
  ) -> go.Figure:
511
- df = _read_co2_volumes(table_provider, realizations, scale, containment_info)
512
- df, cols_to_plot, active_cols_at_startup = _prepare_time_figure_options(
513
- df, containment_info
563
+ df = _read_co2_volumes(table_provider, realizations, scale)
564
+ color_choice = containment_info["color_choice"]
565
+ mark_choice = containment_info["mark_choice"]
566
+ _filter_columns(df, color_choice, mark_choice, containment_info)
567
+ options = _prepare_line_type_and_color_options(
568
+ df, containment_info, color_choice, mark_choice
569
+ )
570
+ active_cols_at_startup = list(
571
+ options[options["line_type"].isin(["solid", "0px"])]["name"]
514
572
  )
515
573
  fig = go.Figure()
516
574
  # Generate dummy scatters for legend entries
517
575
  dummy_args = {"x": df["date"], "mode": "lines", "hoverinfo": "none"}
518
- for col, value in cols_to_plot.items():
576
+ for name, color, line_type in zip(
577
+ options["name"], options["color"], options["line_type"]
578
+ ):
519
579
  args = {
520
- "line_dash": value[1],
521
- "marker_color": value[2],
522
- "legendgroup": col,
523
- "name": col,
580
+ "line_dash": line_type,
581
+ "marker_color": color,
582
+ "legendgroup": name,
583
+ "name": name,
524
584
  }
525
- if col not in active_cols_at_startup:
585
+ if name not in active_cols_at_startup:
526
586
  args["visible"] = "legendonly"
527
587
  fig.add_scatter(y=[0.0], **dummy_args, **args)
528
588
  for rlz in realizations:
529
- sub_df = df[df["realization"] == rlz]
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]
592
+ )
530
593
  common_args = {
531
594
  "x": sub_df["date"],
532
- "hovertemplate": "%{x}: %{y}<br>Realization: %{meta[0]}",
595
+ "hovertemplate": "%{x}: %{y}<br>Realization: %{meta[0]}<br>Prop: %{customdata}%",
533
596
  "meta": [rlz],
534
597
  "showlegend": False,
535
598
  }
536
- for col, value in cols_to_plot.items():
599
+ for name, color, line_type in zip(
600
+ options["name"], options["color"], options["line_type"]
601
+ ):
602
+ # NBNB-AS: Check this, mypy complains:
537
603
  args = {
538
- "line_dash": value[1],
539
- "marker_color": value[2],
540
- "legendgroup": col,
541
- "name": col,
604
+ "line_dash": line_type,
605
+ "marker_color": color,
606
+ "legendgroup": name,
607
+ "name": name,
608
+ "customdata": sub_df[sub_df["name"] == name]["prop"], # type: ignore
542
609
  }
543
- if col not in active_cols_at_startup:
610
+ if name not in active_cols_at_startup:
544
611
  args["visible"] = "legendonly"
545
- fig.add_scatter(y=sub_df[value[0]], **args, **common_args)
546
- fig.layout.legend.orientation = "h"
547
- fig.layout.legend.title.text = ""
548
- fig.layout.legend.y = -0.3
549
- fig.layout.legend.font = {"size": 8}
612
+ fig.add_scatter(
613
+ y=sub_df[sub_df["name"] == name]["amount"], **args, **common_args
614
+ )
550
615
  fig.layout.legend.tracegroupgap = 0
551
- fig.layout.title = "CO<sub>2</sub> containment (all realizations)"
552
616
  fig.layout.xaxis.title = "Time"
553
617
  fig.layout.yaxis.title = scale.value
554
- fig.layout.yaxis.exponentformat = "power"
555
618
  fig.layout.yaxis.autorange = True
556
619
  _adjust_figure(fig)
557
- # fig.update_layout(legend=dict(font=dict(size=8)), legend_tracegroupgap=0)
558
620
  return fig