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.
- webviz_subsurface/__init__.py +1 -1
- webviz_subsurface/_components/color_picker.py +1 -1
- webviz_subsurface/_datainput/well_completions.py +2 -1
- webviz_subsurface/_providers/ensemble_polygon_provider/__init__.py +3 -0
- webviz_subsurface/_providers/ensemble_polygon_provider/_polygon_discovery.py +97 -0
- webviz_subsurface/_providers/ensemble_polygon_provider/_provider_impl_file.py +226 -0
- webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider.py +53 -0
- webviz_subsurface/_providers/ensemble_polygon_provider/ensemble_polygon_provider_factory.py +99 -0
- webviz_subsurface/_providers/ensemble_polygon_provider/polygon_server.py +125 -0
- webviz_subsurface/plugins/_co2_leakage/_plugin.py +577 -293
- webviz_subsurface/plugins/_co2_leakage/_types.py +7 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/_misc.py +9 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +226 -186
- webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +591 -128
- webviz_subsurface/plugins/_co2_leakage/_utilities/containment_data_provider.py +147 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/containment_info.py +31 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/ensemble_well_picks.py +105 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +170 -2
- webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +199 -97
- webviz_subsurface/plugins/_co2_leakage/_utilities/polygon_handler.py +60 -0
- webviz_subsurface/plugins/_co2_leakage/_utilities/summary_graphs.py +77 -173
- webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +122 -21
- webviz_subsurface/plugins/_co2_leakage/_utilities/unsmry_data_provider.py +108 -0
- webviz_subsurface/plugins/_co2_leakage/views/mainview/mainview.py +44 -19
- webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +944 -359
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/METADATA +2 -2
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/RECORD +33 -20
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/WHEEL +1 -1
- /webviz_subsurface/plugins/_co2_leakage/_utilities/{fault_polygons.py → fault_polygons_handler.py} +0 -0
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/LICENSE +0 -0
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/LICENSE.chromedriver +0 -0
- {webviz_subsurface-0.2.36.dist-info → webviz_subsurface-0.2.38.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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:
|
|
146
|
+
containment_info: ContainmentInfo,
|
|
141
147
|
color_choice: str,
|
|
142
148
|
mark_choice: str,
|
|
143
149
|
) -> Tuple[Dict, List, List]:
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
179
|
-
color_options = list(containment_info
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
326
|
+
table_provider: ContainmentDataProvider,
|
|
244
327
|
realizations: List[int],
|
|
245
328
|
scale: Union[Co2MassScale, Co2VolumeScale],
|
|
246
|
-
containment_info:
|
|
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
|
|
255
|
-
mark_choice = containment_info
|
|
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 =
|
|
265
|
-
df = df[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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
317
|
-
& (df["region"] == containment_info
|
|
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
|
|
325
|
-
& (df["region"] == containment_info
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
495
|
+
table_provider: ContainmentDataProvider,
|
|
406
496
|
realizations: List[int],
|
|
407
497
|
scale: Union[Co2MassScale, Co2VolumeScale],
|
|
408
|
-
containment_info:
|
|
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
|
|
414
|
-
mark_choice = containment_info
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
450
|
-
mark_choice = containment_info
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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"
|
|
546
|
-
f"
|
|
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
|
-
|
|
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:
|
|
729
|
+
table_provider: ContainmentDataProvider,
|
|
559
730
|
realizations: List[int],
|
|
560
731
|
scale: Union[Co2MassScale, Co2VolumeScale],
|
|
561
|
-
containment_info:
|
|
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
|
|
565
|
-
mark_choice = containment_info
|
|
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
|
-
|
|
571
|
-
|
|
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
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
"
|
|
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":
|
|
608
|
-
"
|
|
822
|
+
"name": "",
|
|
823
|
+
"meta": [rlz, name],
|
|
824
|
+
"hovertemplate": hover_template,
|
|
609
825
|
}
|
|
610
|
-
if
|
|
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
|