diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a59c539..317b06360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [#1293](https://github.com/equinor/webviz-subsurface/pull/1293) - Added automatic calculation of `STOIIP_TOTAL` / `GIIP_TOTAL` in `VolumetricAnalysis`. +### Changed +- [#1295](https://github.com/equinor/webviz-subsurface/pull/1295) - CO2Leakage: Add support for residual trapping and AMFG migration time. Also changed input format of CSV-files. Various other minor improvements. + ### Fixed - [#1287](https://github.com/equinor/webviz-subsurface/pull/1287) - Fixed bug when grouping on FACIES for non-standard static sources. diff --git a/webviz_subsurface/plugins/_co2_leakage/_plugin.py b/webviz_subsurface/plugins/_co2_leakage/_plugin.py index c2edbda32..9f59e5b6c 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_plugin.py +++ b/webviz_subsurface/plugins/_co2_leakage/_plugin.py @@ -35,10 +35,10 @@ ) from webviz_subsurface.plugins._co2_leakage._utilities.initialization import ( init_map_attribute_names, + init_menu_options, init_surface_providers, init_table_provider, init_well_pick_provider, - init_zone_and_region_options, process_files, ) from webviz_subsurface.plugins._co2_leakage.views.mainview.mainview import ( @@ -166,12 +166,13 @@ def __init__( well_pick_dict, map_surface_names_to_well_pick_names, ) - # Zone and region options - self._zone_and_region_options = init_zone_and_region_options( + # Phase (in case of residual trapping), zone and region options + self._menu_options = init_menu_options( ensemble_paths, self._co2_table_providers, self._co2_actual_volume_table_providers, - self._co2_table_providers, + plume_mass_relpath, + plume_actual_volume_relpath, ) except Exception as err: self._error_message = f"Plugin initialization failed: {err}" @@ -197,7 +198,7 @@ def __init__( self._map_attribute_names, [c["name"] for c in self._color_tables], # type: ignore self._well_pick_names, - self._zone_and_region_options, + self._menu_options, ), self.Ids.MAIN_SETTINGS, ) @@ -286,7 +287,7 @@ def update_graphs( color_choice, mark_choice, sorting, - self._zone_and_region_options[ensemble][source], + self._menu_options[ensemble][source], ) if source in [ GraphSource.CONTAINMENT_MASS, @@ -359,7 +360,10 @@ def set_dates(ensemble: str) -> Tuple[Dict[int, Dict[str, Any]], Optional[int]]: Input(self._settings_component(ViewSettings.Ids.PROPERTY), "value"), ) def toggle_date_slider(attribute: str) -> Dict[str, str]: - if MapAttribute(attribute) == MapAttribute.MIGRATION_TIME: + if MapAttribute(attribute) in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ]: return {"display": "none"} return {} @@ -511,7 +515,6 @@ def update_map_attribute( self._summed_co2, self._visualization_info["unit"], ) - # Plume polygon plume_polygon = None if contour_data is not None: plume_polygon = get_plume_polygon( diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py index 35103c501..7c2f4787d 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py +++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/callbacks.py @@ -56,7 +56,7 @@ def property_origin( return map_attribute_names[MapAttribute.MAX_SGAS] if attribute == MapAttribute.AMFG_PLUME: return map_attribute_names[MapAttribute.MAX_AMFG] - raise AssertionError(f"No origin defined for property: {attribute}") + raise AssertionError(f"Map attribute name not found for property: {attribute}") @dataclass @@ -86,7 +86,9 @@ def from_server( visualization_info, map_attribute_names, ) - assert surf_meta is not None # Should not occur + if surf_meta is None: # Surface file does not exist + return None, None + assert isinstance(img_url, str) value_range = ( 0.0 if np.ma.is_masked(surf_meta.val_min) else surf_meta.val_min, 0.0 if np.ma.is_masked(surf_meta.val_max) else surf_meta.val_max, @@ -132,7 +134,15 @@ def derive_surface_address( threshold=contour_data["threshold"] if contour_data else 0.0, smoothing=contour_data["smoothing"] if contour_data else 0.0, ) - date = None if attribute == MapAttribute.MIGRATION_TIME else date + date = ( + None + if attribute + in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ] + else date + ) if len(realization) == 1: return SimulatedSurfaceAddress( attribute=map_attribute_names[attribute], @@ -151,7 +161,10 @@ def derive_surface_address( def readable_name(attribute: MapAttribute) -> str: unit = "" - if attribute == MapAttribute.MIGRATION_TIME: + if attribute in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ]: unit = " [year]" elif attribute in (MapAttribute.AMFG_PLUME, MapAttribute.SGAS_PLUME): unit = " [# real.]" @@ -198,7 +211,10 @@ def get_plume_polygon( def _find_legend_title(attribute: MapAttribute, unit: str) -> str: - if attribute == MapAttribute.MIGRATION_TIME: + if attribute in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ]: return "years" if attribute in [MapAttribute.MASS, MapAttribute.DISSOLVED, MapAttribute.FREE]: return unit @@ -497,18 +513,22 @@ def process_containment_info( color_choice: str, mark_choice: Optional[str], sorting: str, - zone_and_region_options: Dict[str, List[str]], + menu_options: Dict[str, List[str]], ) -> Dict[str, Union[str, None, List[str], int]]: if mark_choice is None: mark_choice = "phase" - zones = zone_and_region_options["zones"] - regions = zone_and_region_options["regions"] + zones = menu_options["zones"] + regions = menu_options["regions"] if len(zones) > 0: zones = [zone_name for zone_name in zones if zone_name != "all"] if len(regions) > 0: regions = [reg_name for reg_name in regions if reg_name != "all"] containments = ["hazardous", "outside", "contained"] - phases = ["gas", "aqueous"] + phases = [phase for phase in menu_options["phases"] if phase != "total"] + if "zone" in [mark_choice, color_choice]: + region = "all" + if "region" in [mark_choice, color_choice]: + zone = "all" return { "zone": zone, "region": region, diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py index dcb442f0f..d70412e50 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py +++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/co2volume.py @@ -1,8 +1,9 @@ import warnings +from datetime import datetime as dt from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np -import pandas +import pandas as pd import plotly.express as px import plotly.graph_objects as go @@ -45,82 +46,48 @@ def _read_dataframe( table_provider: EnsembleTableProvider, realization: int, scale_factor: float, - containment_info: Dict[str, Union[str, None, List[str]]], -) -> pandas.DataFrame: +) -> pd.DataFrame: df = table_provider.get_column_data(table_provider.column_names(), [realization]) - if any(split in list(df.columns) for split in ["zone", "region"]): - df = _process_containment_information(df, containment_info) if scale_factor == 1.0: return df - for col in df.columns: - if col not in ["date", "zone", "region"]: - df[col] /= scale_factor + df["amount"] /= scale_factor return df -def read_zone_and_region_options( +def read_menu_options( table_provider: EnsembleTableProvider, realization: int, + relpath: str, ) -> Dict[str, List[str]]: - df = table_provider.get_column_data(table_provider.column_names(), [realization]) + col_names = table_provider.column_names() + df = table_provider.get_column_data(col_names, [realization]) + required_columns = ["date", "amount", "phase", "containment", "zone", "region"] + missing_columns = [col for col in required_columns if col not in col_names] + if len(missing_columns) > 0: + raise KeyError( + f"Missing expected columns {', '.join(missing_columns)} in {relpath}" + f" in realization {realization} (and possibly other csv-files). " + f"Provided files are likely from an old version of ccs-scripts." + ) zones = ["all"] - if "zone" in list(df.columns): - for zone in list(df["zone"]): - if zone not in zones: - zones.append(zone) + for zone in list(df["zone"]): + if zone not in zones: + zones.append(zone) regions = ["all"] - if "region" in list(df.columns): - for region in list(df["region"]): - if region not in regions: - regions.append(region) + for region in list(df["region"]): + if region not in regions: + regions.append(region) + if "free_gas" in list(df["phase"]): + phases = ["total", "free_gas", "trapped_gas", "aqueous"] + else: + phases = ["total", "gas", "aqueous"] return { "zones": zones if len(zones) > 1 else [], "regions": regions if len(regions) > 1 else [], + "phases": phases, } -def _process_containment_information( - df: pandas.DataFrame, - containment_info: Dict[str, Union[str, None, List[str]]], -) -> pandas.DataFrame: - choices = [containment_info["color_choice"], containment_info["mark_choice"]] - if "zone" in choices: - return ( - df[df["zone"] != "all"] - .drop(columns="region", errors="ignore") - .reset_index(drop=True) - ) - if "region" in choices: - return ( - df[df["region"] != "all"] - .drop(columns="zone", errors="ignore") - .reset_index(drop=True) - ) - zone = containment_info["zone"] - region = containment_info["region"] - if zone not in ["all", None]: - if zone in list(df["zone"]): - return df[df["zone"] == zone].drop( - columns=["zone", "region"], errors="ignore" - ) - print(f"Zone {zone} not found, using sum for each unique date.") - elif region not in ["all", None]: - if region in list(df["region"]): - return df[df["region"] == region].drop( - columns=["zone", "region"], errors="ignore" - ) - print(f"Region {region} not found, using sum for each unique date.") - if "zone" in list(df.columns): - if "region" in list(df.columns): - return df[ - [a and b for a, b in zip(df["zone"] == "all", df["region"] == "all")] - ].drop(columns=["zone", "region"]) - df = df[df["zone"] == "all"].drop(columns=["zone"]) - elif "region" in list(df.columns): - df = df[df["region"] == "all"].drop(columns=["region"]) - return df - - def _get_colors(num_cols: int = 3, split: str = "zone") -> List[str]: if split == "containment": return [_COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED] @@ -148,133 +115,114 @@ def _get_marks(num_marks: int, mark_choice: str) -> List[str]: f"Some {mark_choice}s will share pattern." ) return base_pattern[:num_marks] - return ["", "/"] + return ["", "/"] if num_marks == 2 else ["", ".", "/"] + + +def _get_line_types(mark_options: List[str], mark_choice: str) -> List[str]: + if mark_choice == "none": + return ["solid"] + if mark_choice == "containment": + return ["dash", "dot", "solid"] + if mark_choice in ["zone", "region"]: + if len(mark_options) > 8: + warnings.warn( + f"Large number of {mark_choice}s might make it hard " + f"to distinguish different dashed lines." + ) + return [ + f"{round(i / len(mark_options) * 25)}px" for i in range(len(mark_options)) + ] + return ["dot", "dash"] if "gas" in mark_options else ["dot", "dashdot", "dash"] def _prepare_pattern_and_color_options( + df: pd.DataFrame, containment_info: Dict, color_choice: str, mark_choice: str, ) -> Tuple[Dict, List, List]: - num_colors = len(containment_info[f"{color_choice}s"]) - num_marks = ( - num_colors - if mark_choice == "none" - else len(containment_info[f"{mark_choice}s"]) - ) + mark_options = [] if mark_choice == "none" else containment_info[f"{mark_choice}s"] + color_options = containment_info[f"{color_choice}s"] + num_colors = len(color_options) + num_marks = num_colors if mark_choice == "none" else len(mark_options) marks = _get_marks(num_marks, mark_choice) colors = _get_colors(num_colors, color_choice) if mark_choice == "none": - cat_ord = {"type": containment_info[f"{color_choice}s"]} + cat_ord = {"type": color_options} + df["type"] = df[color_choice] return cat_ord, colors, marks + df["type"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])] if containment_info["sorting"] == "color": cat_ord = { - "type": [ - ", ".join((c, m)) - for c in containment_info[f"{color_choice}s"] - for m in containment_info[f"{mark_choice}s"] - ], + "type": [", ".join((c, m)) for c in color_options for m in mark_options], } colors = [c for c in colors for _ in range(num_marks)] marks = marks * num_colors else: cat_ord = { - "type": [ - ", ".join((c, m)) - for m in containment_info[f"{mark_choice}s"] - for c in containment_info[f"{color_choice}s"] - ], + "type": [", ".join((c, m)) for m in mark_options for c in color_options], } colors = colors * num_marks marks = [m for m in marks for _ in range(num_colors)] return cat_ord, colors, marks -def _drop_unused_columns( - df: pandas.DataFrame, +def _prepare_line_type_and_color_options( + df: pd.DataFrame, containment_info: Dict, - keep_realization: bool = True, - figure: int = 2, -) -> Tuple[pandas.DataFrame, List[str]]: - color_choice = containment_info["color_choice"] - mark_choice = containment_info["mark_choice"] - containment = containment_info["containment"] - containments = containment_info["containments"] - phase = containment_info["phase"] - phases = containment_info["phases"] - split = _find_split(color_choice, mark_choice) - - cols_to_keep = ["date"] - if keep_realization: - cols_to_keep += ["realization"] - cols_to_return = [] - if split == "standard": - if mark_choice == "none": - cols_to_return += ["_".join((phase, c)) for c in containments] - else: - cols_to_return += [ - "_".join((p, c)) - for c in containments - for p in phases # ["total"] + phases - ] - # cols_to_return += ["total"] + ["_".join(("total", p)) for p in phases] - else: - cols_to_keep += [split] - if mark_choice == split: - cols_to_return += ["_".join((phase, c)) for c in containments] - elif mark_choice == "none": - if containment == "total": - cols_to_return += ( - ["total"] if phase == "total" else ["_".join(("total", phase))] - ) - else: - cols_to_return += ["_".join((phase, containment))] - elif mark_choice == "containment": - cols_to_return += ["_".join((phase, c)) for c in containments] - else: - if figure == 2: - cols_to_return += [ - ( - "total" - if containment == "total" - else "_".join(("total", containment)) - ) - ] - cols_to_return += ( - ["total_gas", "total_aqueous"] - if containment == "total" - else ["_".join((p, containment)) for p in phases] - ) - cols_to_keep += cols_to_return - df = df.drop(columns=[col for col in df.columns if col not in cols_to_keep]) - return df, cols_to_return - - -def _rename_columns_figure2( - part_df: pandas.DataFrame, + color_choice: str, mark_choice: str, - name: str, - colnames: List[str], - containment: str, - phases: List[str], -) -> pandas.DataFrame: +) -> pd.DataFrame: + mark_options = [] + if mark_choice != "none": + mark_options = list(containment_info[f"{mark_choice}s"]) + color_options = list(containment_info[f"{color_choice}s"]) + num_colors = len(color_options) + line_types = _get_line_types(mark_options, mark_choice) + colors = _get_colors(num_colors, color_choice) + filter_mark = True + if mark_choice == "phase": + mark_options = ["total"] + mark_options + line_types = ["solid"] + line_types + filter_mark = False + if color_choice == "containment": + color_options = ["total"] + color_options + colors = ["black"] + colors + else: + _filter_rows(df, color_choice, mark_choice, filter_mark) if mark_choice == "none": - renaming = {c: name for c in colnames} - elif mark_choice == "phase": - if containment == "total": - renaming = { - c: ", ".join((name, p)) for c, p in zip(colnames, ["total"] + phases) + df["name"] = df[color_choice] + return pd.DataFrame( + { + "name": color_options, + "color": colors, + "line_type": line_types * len(colors), } - else: - renaming = { - c: ", ".join((name, c.split("_", maxsplit=1)[0])) for c in colnames + ) + df["name"] = [", ".join((c, m)) for c, m in zip(df[color_choice], df[mark_choice])] + _change_names(df, color_options, mark_options) + if containment_info["sorting"] == "color": + options = pd.DataFrame( + { + "name": [ + ", ".join((c, m)) for c in color_options for m in mark_options + ], + "color": [c for c in colors for _ in mark_options], + "line_type": [l for _ in colors for l in line_types], } - elif mark_choice in ["zone", "region"]: - renaming = {cn: ", ".join((cn.split("_")[1], name)) for cn in colnames} - else: # mark_choice == "containment" - renaming = {cn: ", ".join((name, cn.split("_")[1])) for cn in colnames} - part_df = part_df.rename(columns=renaming).reset_index(drop=True) - return part_df + ) + else: + options = pd.DataFrame( + { + "name": [ + ", ".join((c, m)) for m in mark_options for c in color_options + ], + "color": [c for _ in mark_options for c in colors], + "line_type": [l for l in line_types for _ in colors], + } + ) + _change_names(options, color_options, mark_options) + return options def _find_scale_factor( @@ -296,7 +244,7 @@ def _read_terminal_co2_volumes( realizations: List[int], scale: Union[Co2MassScale, Co2VolumeScale], containment_info: Dict[str, Union[str, None, List[str]]], -) -> pandas.DataFrame: +) -> pd.DataFrame: records: Dict[str, List[Any]] = { "real": [], "amount": [], @@ -311,143 +259,102 @@ def _read_terminal_co2_volumes( if mark_choice != "none": records[mark_choice] = [] scale_factor = _find_scale_factor(table_provider, scale) + data_frame = None for real in realizations: - df = _read_dataframe(table_provider, real, scale_factor, containment_info) - _add_to_records( - records, color_choice, mark_choice, df, str(real), containment_info - ) - df = pandas.DataFrame.from_dict(records) - df.sort_values( + df = _read_dataframe(table_provider, real, scale_factor) + df = df[df["date"] == np.max(df["date"])] + _add_sort_key_and_real(df, str(real), containment_info) + _filter_columns(df, color_choice, mark_choice, containment_info) + _filter_rows(df, color_choice, mark_choice) + if data_frame is None: + data_frame = df + else: + data_frame = pd.concat([data_frame, df]) + assert data_frame is not None + data_frame.sort_values( ["sort_key", "sort_key_secondary"], inplace=True, ascending=[True, True] ) - return df + return data_frame -def _add_to_records( - records: Dict[str, List[Any]], +def _filter_columns( + df: pd.DataFrame, color_choice: str, mark_choice: str, - df: pandas.DataFrame, - label: str, containment_info: Dict, ) -> None: - phase = containment_info["phase"] - containments = containment_info["containments"] - split = _find_split(color_choice, mark_choice) - if split == "standard": - last = df.iloc[np.argmax(df["date"])] - factor = 6 if mark_choice == "phase" else 3 - records["real"] += [label] * factor - if mark_choice == "phase": - record = [ - [last["_".join((p, c))], c, p] - for c in containments - for p in (containment_info["phases"]) - ] - else: - record = [[last["_".join((phase, c))], c] for c in containments] - records["amount"] += [r[0] for r in record] - records["containment"] += [r[1] for r in record] - if mark_choice == "phase": - records["phase"] += [r[2] for r in record] - records["sort_key"] += [last["total_hazardous"]] * factor - records["sort_key_secondary"] += [last["total_outside"]] * factor - else: - containment = containment_info["containment"] - factor = 1 if mark_choice == "none" else 2 if mark_choice == "phase" else 3 - last_ = df[df["date"] == np.max(df["date"])] - records["sort_key"] += ( - [np.sum(last_["total_hazardous"])] * factor * last_.shape[0] - ) - records["sort_key_secondary"] += ( - [np.sum(last_["total_outside"])] * factor * last_.shape[0] - ) - for i in range(last_.shape[0]): - last = last_.iloc[i] - records["real"] += [label] * factor - if mark_choice in ["containment", "zone", "region"]: - records["amount"] += [last["_".join((phase, c))] for c in containments] - records[mark_choice] += ( - containments - if mark_choice == "containment" - else [last[split]] * factor - ) - elif mark_choice == "none": - if containment == "total": - records["amount"] += ( - [last["total"]] - if phase == "total" - else [last["_".join(("total", phase))]] - ) - else: - records["amount"] += [last["_".join((phase, containment))]] - else: # mark_choice == "phase" - if containment == "total": - records["amount"] += [ - last["total_aqueous"], - last["total_gas"], - ] - else: - records["amount"] += [ - last["_".join(("aqueous", containment))], - last["_".join(("gas", containment))], - ] - records[mark_choice] += ["aqueous", "gas"] - records[color_choice] += ( - containments - if mark_choice in ["zone", "region"] - else [last[split]] * factor - ) + filter_columns = [ + col + for col in ["phase", "containment", "zone", "region"] + if col not in [mark_choice, color_choice] + ] + for col in filter_columns: + df.query(f'{col} == "{containment_info[col]}"', inplace=True) + df.drop(columns=filter_columns, inplace=True) + + +def _filter_rows( + df: pd.DataFrame, + color_choice: str, + mark_choice: str, + filter_mark: bool = True, +) -> None: + df.query(f'{color_choice} not in ["total", "all"]', inplace=True) + if mark_choice != "none" and filter_mark: + df.query(f'{mark_choice} not in ["total", "all"]', inplace=True) -def _find_split(color_choice: str, mark_choice: str) -> str: - split = "standard" - if "zone" in [color_choice, mark_choice]: - split = "zone" - elif "region" in [color_choice, mark_choice]: - split = "region" - return split +def _add_sort_key_and_real( + df: pd.DataFrame, + label: str, + containment_info: Dict, +) -> None: + sort_value = np.sum( + df[ + (df["phase"] == "total") + & (df["containment"] == "hazardous") + & (df["zone"] == containment_info["zone"]) + & (df["region"] == containment_info["region"]) + ]["amount"] + ) + sort_value_secondary = np.sum( + df[ + (df["phase"] == "total") + & (df["containment"] == "outside") + & (df["zone"] == containment_info["zone"]) + & (df["region"] == containment_info["region"]) + ]["amount"] + ) + df["real"] = [label] * df.shape[0] + df["sort_key"] = [sort_value] * df.shape[0] + df["sort_key_secondary"] = [sort_value_secondary] * df.shape[0] def _read_co2_volumes( table_provider: EnsembleTableProvider, realizations: List[int], scale: Union[Co2MassScale, Co2VolumeScale], - containment_info: Dict[str, Union[str, None, List[str]]], -) -> pandas.DataFrame: +) -> pd.DataFrame: scale_factor = _find_scale_factor(table_provider, scale) - return pandas.concat( + return pd.concat( [ - _read_dataframe( - table_provider, real, scale_factor, containment_info - ).assign(realization=real) - for real in realizations + _read_dataframe(table_provider, r, scale_factor).assign(realization=r) + for r in realizations ] ) -def _change_type_names(df: pandas.DataFrame) -> None: - df["type"] = df["type"].replace("gas_contained", "contained, gas") - df["type"] = df["type"].replace("gas_outside", "outside, gas") - df["type"] = df["type"].replace("gas_hazardous", "hazardous, gas") - df["type"] = df["type"].replace("aqueous_contained", "contained, aqueous") - df["type"] = df["type"].replace("aqueous_outside", "outside, aqueous") - df["type"] = df["type"].replace("aqueous_hazardous", "hazardous, aqueous") - df["type"] = df["type"].replace("total_contained", "contained, total") - df["type"] = df["type"].replace("total_outside", "outside, total") - df["type"] = df["type"].replace("total_hazardous", "hazardous, total") - - -def _change_type_names_mark_choice_none(df: pandas.DataFrame) -> None: - df["type"] = df["type"].replace("gas_contained", "contained") - df["type"] = df["type"].replace("gas_outside", "outside") - df["type"] = df["type"].replace("gas_hazardous", "hazardous") - df["type"] = df["type"].replace("aqueous_contained", "contained") - df["type"] = df["type"].replace("aqueous_outside", "outside") - df["type"] = df["type"].replace("aqueous_hazardous", "hazardous") - df["type"] = df["type"].replace("total_contained", "contained") - df["type"] = df["type"].replace("total_outside", "outside") - df["type"] = df["type"].replace("total_hazardous", "hazardous") +def _change_names( + df: pd.DataFrame, + color_options: List[str], + mark_options: List[str], +) -> None: + for m in mark_options + ["total", "all"]: + df["name"] = df["name"].replace(f"total, {m}", m) + df["name"] = df["name"].replace(f"all, {m}", m) + for m in color_options: + df["name"] = df["name"].replace(f"{m}, total", m) + df["name"] = df["name"].replace(f"{m}, all", m) def _adjust_figure(fig: go.Figure) -> None: @@ -470,13 +377,21 @@ def _adjust_figure(fig: go.Figure) -> None: def _add_prop_to_df( - df: pandas.DataFrame, + df: pd.DataFrame, list_to_iterate: Union[List, np.ndarray], column: str, + filter_columns: Optional[List[str]] = None, ) -> None: prop = np.zeros(df.shape[0]) for element in list_to_iterate: - summed_amount = np.sum(df.loc[df[column] == element]["amount"]) + if filter_columns is None: + summed_amount = np.sum(df.loc[df[column] == element]["amount"]) + else: + filter_for_sum = df[column] == element + for col in filter_columns: + if col in df.columns: + filter_for_sum &= ~df[col].isin(["total", "all"]) + summed_amount = np.sum(df.loc[filter_for_sum]["amount"]) prop[np.where(df[column] == element)[0]] = summed_amount nonzero = np.where(prop > 0)[0] prop[nonzero] = ( @@ -497,28 +412,23 @@ def generate_co2_volume_figure( ) color_choice = containment_info["color_choice"] mark_choice = containment_info["mark_choice"] + _add_prop_to_df(df, [str(r) for r in realizations], "real") cat_ord, colors, marks = _prepare_pattern_and_color_options( + df, containment_info, color_choice, mark_choice, ) - df["type"] = ( - [", ".join((c, p)) for c, p in zip(df[color_choice], df[mark_choice])] - if mark_choice != "none" - else df[color_choice] - ) - _add_prop_to_df(df, [str(r) for r in realizations], "real") - pattern_shape = "type" if mark_choice != "none" else None fig = px.bar( df, y="real", x="amount", color="type", - pattern_shape=pattern_shape, + color_discrete_sequence=colors, + pattern_shape="type" if mark_choice != "none" else None, pattern_shape_sequence=marks, orientation="h", category_orders=cat_ord, - color_discrete_sequence=colors, hover_data={"prop": True, "real": False}, ) fig.layout.yaxis.title = "Realization" @@ -527,23 +437,6 @@ def generate_co2_volume_figure( return fig -def _rename_columns_figure3( - mark_choice: str, - containment: str, - cols_kept: List[str], -) -> Dict[str, str]: - if mark_choice == "phase": - if containment == "total": - renaming = {"total_gas": "gas", "total_aqueous": "aqueous"} - else: - renaming = {col: col.split("_")[0] for col in cols_kept} - elif mark_choice == "none": - renaming = {} - else: # mark_choice == "containment" - renaming = {col: col.split("_")[1] for col in cols_kept} - return renaming - - # pylint: disable=too-many-locals def generate_co2_time_containment_one_realization_figure( table_provider: EnsembleTableProvider, @@ -552,62 +445,37 @@ def generate_co2_time_containment_one_realization_figure( y_limits: List[Optional[float]], containment_info: Dict[str, Any], ) -> go.Figure: - df = _read_co2_volumes( - table_provider, [time_series_realization], scale, containment_info - ) - df.sort_values(by="date", inplace=True) + df = _read_co2_volumes(table_provider, [time_series_realization], scale) color_choice = containment_info["color_choice"] mark_choice = containment_info["mark_choice"] - split = _find_split(color_choice, mark_choice) - containment = containment_info["containment"] - df, colnames = _drop_unused_columns( - df, containment_info, keep_realization=False, figure=3 - ) - if split == "standard": - df = pandas.melt(df, id_vars=["date"]) - else: - renaming = _rename_columns_figure3( - mark_choice, - containment, - colnames, - ) - df = df.rename(columns=renaming) - df = pandas.melt(df, id_vars=["date", split]) - if mark_choice == "none": - df["variable"] = df[split] - else: - df["variable"] = ( - df["variable"] + ", " + df[split] - if mark_choice == split - else df[split] + ", " + df["variable"] - ) - df = df.drop(columns=[split]) - cat_ord, colors, marks = _prepare_pattern_and_color_options( - containment_info, - color_choice, - mark_choice, - ) - df = df.rename(columns={"value": "amount", "variable": "type"}) - if mark_choice == "none": - _change_type_names_mark_choice_none(df) + _filter_columns(df, color_choice, mark_choice, containment_info) + _filter_rows(df, color_choice, mark_choice) + if containment_info["sorting"] == "marking" and mark_choice != "none": + sort_order = ["date", mark_choice] else: - _change_type_names(df) + sort_order = ["date", color_choice] + df.sort_values(by=sort_order, inplace=True) if y_limits[0] is None and y_limits[1] is not None: y_limits[0] = 0.0 elif y_limits[1] is None and y_limits[0] is not None: y_limits[1] = max(df.groupby("date")["amount"].sum()) * 1.05 _add_prop_to_df(df, np.unique(df["date"]), "date") - pattern_shape = "type" if mark_choice != "none" else None + cat_ord, colors, marks = _prepare_pattern_and_color_options( + df, + containment_info, + color_choice, + mark_choice, + ) fig = px.area( df, x="date", y="amount", color="type", - category_orders=cat_ord, color_discrete_sequence=colors, - pattern_shape=pattern_shape, - pattern_shape_sequence=marks, # ['', '/', '\\', 'x', '-', '|', '+', '.'], + pattern_shape="type" if mark_choice != "none" else None, + pattern_shape_sequence=marks, + category_orders=cat_ord, range_y=y_limits, hover_data={ "prop": True, @@ -622,9 +490,25 @@ def generate_co2_time_containment_one_realization_figure( return fig +def spaced_dates(dates: List[str], num_between: int) -> Dict[str, List[str]]: + dates_list = [dt.strptime(date, "%Y-%m-%d") for date in dates] + date_dict: Dict[str, List[str]] = {date: [] for date in dates} + for i in range(len(dates_list) - 1): + date_dict[dates[i]].append(dates[i]) + delta = (dates_list[i + 1] - dates_list[i]) / (num_between + 1) + for j in range(1, num_between + 1): + new_date = dates_list[i] + delta * j + if j <= num_between / 2: + date_dict[dates[i]].append(new_date.strftime("%Y-%m-%d")) + else: + date_dict[dates[i + 1]].append(new_date.strftime("%Y-%m-%d")) + date_dict[dates[-1]].append(dates[-1]) + return date_dict + + def _add_hover_info_in_field( fig: go.Figure, - df: pandas.DataFrame, + df: pd.DataFrame, cat_ord: Dict, colors: List, ) -> None: @@ -641,163 +525,32 @@ def _add_hover_info_in_field( for date in dates } prev_vals = {date: 0 for date in dates} + date_dict = spaced_dates(dates, 4) for name, color in zip(cat_ord["type"], colors): sub_df = df[df["type"] == name] for date in dates: amount = sub_df[sub_df["date"] == date]["amount"].item() prop = sub_df[sub_df["date"] == date]["prop"].item() prev_val = prev_vals[date] - new_val = prev_val + amount - mid_val = (prev_val + new_val) / 2 + p15 = prev_val + 0.15 * amount + p85 = prev_val + 0.85 * amount + y_vals = np.linspace(p15, p85, 8).tolist() * len(date_dict[date]) + y_vals.sort() fig.add_trace( go.Scatter( - x=[date], - y=[mid_val], + x=date_dict[date] * 8, + y=y_vals, mode="lines", line=go.scatter.Line(color=color), text=f"type={name}
date={date_strings[date]}
" f"amount={amount:.3f}
prop={prop}", + opacity=0, hoverinfo="text", hoveron="points", showlegend=False, ) ) - prev_vals[date] = new_val - - -def _make_cols_to_plot( - split: str, - df_: pandas.DataFrame, - containment_info: Dict, -) -> Tuple[Dict[str, Tuple[str, str, str]], List[str]]: - mark_choice = containment_info["mark_choice"] - color_choice = containment_info["color_choice"] - phase = containment_info["phase"] - phases = containment_info["phases"] - containments = containment_info["containments"] - options = containment_info[f"{split}s"] - if mark_choice in ["phase", "none"]: - colors = _get_colors(len(options), split) - if mark_choice == "none": - line_type = ( - "solid" if phase == "total" else "dot" if phase == "gas" else "dash" - ) - cols_to_plot = { - name: (name, line_type, col) for name, col in zip(options, colors) - } - df_["total"] = df_[options].sum(axis=1) - active_cols_at_startup = options - else: - cols_to_plot = { - ", ".join((name, p)): (", ".join((name, p)), line_type, col) - for name, col in zip(options, colors) - for p, line_type in zip(["total"] + phases, ["solid", "dot", "dash"]) - } - df_["total"] = df_[[", ".join((name, "total")) for name in options]].sum( - axis=1 - ) - active_cols_at_startup = [name + ", total" for name in options] - else: - if mark_choice == split: - colors = _get_colors(split=color_choice) - line_types = [ - f"{round(i / len(options) * 25)}px" for i in range(len(options)) - ] - if len(options) > 8: - warnings.warn( - f"Large number of {split}s might make it hard " - f"to distinguish different dashed lines." - ) - cols_to_plot = { - ", ".join((con, name)): (", ".join((con, name)), line_type, col) - for con, col in zip(containments, colors) - for name, line_type in zip(options, line_types) - } - active_cols_at_startup = ["contained, " + name for name in options] - else: # mark_choice == "containment" - colors = _get_colors(len(options), split) - cols_to_plot = { - ", ".join((name, con)): (", ".join((name, con)), line_type, col) - for name, col in zip(options, colors) - for con, line_type in zip(containments, ["dash", "dot", "solid"]) - } - active_cols_at_startup = [name + ", contained" for name in options] - df_["total"] = df_[cols_to_plot.keys()].sum(axis=1) - return cols_to_plot, active_cols_at_startup - - -def _prepare_time_figure_options( - df: pandas.DataFrame, - containment_info: Dict[str, Any], -) -> Tuple[pandas.DataFrame, Dict[str, Tuple[str, str, str]], List[str]]: - color_choice = containment_info["color_choice"] - mark_choice = containment_info["mark_choice"] - split = _find_split(color_choice, mark_choice) - containment = containment_info["containment"] - containments = containment_info["containments"] - phase = containment_info["phase"] - phases = containment_info["phases"] - if split == "standard": - df.sort_values(by="date", inplace=True) - if mark_choice == "none": - new_names = ["total"] + containments - colors = [_COLOR_TOTAL, _COLOR_HAZARDOUS, _COLOR_OUTSIDE, _COLOR_CONTAINED] - line_type = ( - "solid" if phase == "total" else "dot" if phase == "gas" else "dash" - ) - colnames = ["total"] - colnames += ["_".join((phase, c)) for c in containments] - cols_to_plot = { - new_name: (colname, line_type, color) - for new_name, colname, color in zip(new_names, colnames, colors) - } - cols_to_keep = colnames + ["date", "realization"] - cols_to_keep[0] = ( - "total" if phase == "total" else "_".join(("total", phase)) - ) - df = df.drop(columns=[col for col in df.columns if col not in cols_to_keep]) - df = df.rename(columns={cols_to_keep[0]: "total"}).reset_index(drop=True) - else: - cols_to_plot = { - "total": ("total", "solid", _COLOR_TOTAL), - "hazardous": ("total_hazardous", "solid", _COLOR_HAZARDOUS), - "outside": ("total_outside", "solid", _COLOR_OUTSIDE), - "contained": ("total_contained", "solid", _COLOR_CONTAINED), - "gas": ("total_gas", "dot", _COLOR_TOTAL), - "aqueous": ("total_aqueous", "dash", _COLOR_TOTAL), - "hazardous, gas": ("gas_hazardous", "dot", _COLOR_HAZARDOUS), - "outside, gas": ("gas_outside", "dot", _COLOR_OUTSIDE), - "contained, gas": ("gas_contained", "dot", _COLOR_CONTAINED), - "hazardous, aqueous": ("aqueous_hazardous", "dash", _COLOR_HAZARDOUS), - "contained, aqueous": ("aqueous_contained", "dash", _COLOR_CONTAINED), - "outside, aqueous": ("aqueous_outside", "dash", _COLOR_OUTSIDE), - } - active_cols_at_startup = ["total"] + containments - else: - options = containment_info[f"{split}s"] - df, colnames = _drop_unused_columns(df, containment_info) - df.sort_values(by=["date", "realization"], inplace=True) - df_ = df[["date", "realization"]][df[split] == options[0]].reset_index( - drop=True - ) - for name in options: - part_df = df[colnames][df[split] == name] - part_df = _rename_columns_figure2( - part_df, - mark_choice, - name, - colnames, - containment, - phases, - ) - df_ = pandas.concat([df_, part_df], axis=1) - cols_to_plot, active_cols_at_startup = _make_cols_to_plot( - split, - df_, - containment_info, - ) - df = df_ - return df, cols_to_plot, active_cols_at_startup + prev_vals[date] = prev_val + amount # pylint: disable=too-many-locals @@ -807,53 +560,58 @@ def generate_co2_time_containment_figure( scale: Union[Co2MassScale, Co2VolumeScale], containment_info: Dict[str, Any], ) -> go.Figure: - df = _read_co2_volumes(table_provider, realizations, scale, containment_info) - df, cols_to_plot, active_cols_at_startup = _prepare_time_figure_options( - df, containment_info + df = _read_co2_volumes(table_provider, realizations, scale) + color_choice = containment_info["color_choice"] + mark_choice = containment_info["mark_choice"] + _filter_columns(df, color_choice, mark_choice, containment_info) + options = _prepare_line_type_and_color_options( + df, containment_info, color_choice, mark_choice + ) + active_cols_at_startup = list( + options[options["line_type"].isin(["solid", "0px"])]["name"] ) fig = go.Figure() # Generate dummy scatters for legend entries dummy_args = {"x": df["date"], "mode": "lines", "hoverinfo": "none"} - for col, value in cols_to_plot.items(): + for name, color, line_type in zip( + options["name"], options["color"], options["line_type"] + ): args = { - "line_dash": value[1], - "marker_color": value[2], - "legendgroup": col, - "name": col, + "line_dash": line_type, + "marker_color": color, + "legendgroup": name, + "name": name, } - if col not in active_cols_at_startup: + if name not in active_cols_at_startup: args["visible"] = "legendonly" fig.add_scatter(y=[0.0], **dummy_args, **args) for rlz in realizations: - sub_df = df[df["realization"] == rlz] + sub_df = df[df["realization"] == rlz].copy().reset_index() + _add_prop_to_df( + sub_df, np.unique(df["date"]), "date", [color_choice, mark_choice] + ) common_args = { "x": sub_df["date"], "hovertemplate": "%{x}: %{y}
Realization: %{meta[0]}
Prop: %{customdata}%", "meta": [rlz], "showlegend": False, } - for col, value in cols_to_plot.items(): - prop = np.zeros(sub_df.shape[0]) - nonzero = np.where(np.array(sub_df["total"]) > 0)[0] - prop[nonzero] = ( - np.round( - np.array(sub_df[value[0]])[nonzero] - / np.array(sub_df["total"])[nonzero] - * 1000 - ) - / 10 - ) + for name, color, line_type in zip( + options["name"], options["color"], options["line_type"] + ): # NBNB-AS: Check this, mypy complains: args = { - "line_dash": value[1], - "marker_color": value[2], - "legendgroup": col, - "name": col, - "customdata": prop, # type: ignore + "line_dash": line_type, + "marker_color": color, + "legendgroup": name, + "name": name, + "customdata": sub_df[sub_df["name"] == name]["prop"], # type: ignore } - if col not in active_cols_at_startup: + if name not in active_cols_at_startup: args["visible"] = "legendonly" - fig.add_scatter(y=sub_df[value[0]], **args, **common_args) + fig.add_scatter( + y=sub_df[sub_df["name"] == name]["amount"], **args, **common_args + ) fig.layout.legend.tracegroupgap = 0 fig.layout.xaxis.title = "Time" fig.layout.yaxis.title = scale.value diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py index 261dfc2d1..bac490a4b 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py +++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/generic.py @@ -2,7 +2,8 @@ class MapAttribute(StrEnum): - MIGRATION_TIME = "Migration Time" + MIGRATION_TIME_SGAS = "Migration time (SGAS)" + MIGRATION_TIME_AMFG = "Migration time (AMFG)" MAX_SGAS = "Maximum SGAS" MAX_AMFG = "Maximum AMFG" SGAS_PLUME = "Plume (SGAS)" diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py index 336e5e3ac..97053d1d4 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py +++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/initialization.py @@ -15,7 +15,7 @@ ) from webviz_subsurface._utils.webvizstore_functions import read_csv from webviz_subsurface.plugins._co2_leakage._utilities.co2volume import ( - read_zone_and_region_options, + read_menu_options, ) from webviz_subsurface.plugins._co2_leakage._utilities.generic import ( GraphSource, @@ -35,7 +35,8 @@ def init_map_attribute_names( if mapping is None: # Based on name convention of xtgeoapp_grd3dmaps: return { - MapAttribute.MIGRATION_TIME: "migrationtime", + MapAttribute.MIGRATION_TIME_SGAS: "migrationtime_sgas", + MapAttribute.MIGRATION_TIME_AMFG: "migrationtime_amfg", MapAttribute.MAX_SGAS: "max_sgas", MapAttribute.MAX_AMFG: "max_amfg", MapAttribute.MASS: "co2-mass-total", @@ -118,25 +119,28 @@ def _find_max_file_size_mb(ens_path: str, table_rel_path: str) -> float: return max_size -def init_zone_and_region_options( +def init_menu_options( ensemble_roots: Dict[str, str], mass_table: Dict[str, EnsembleTableProvider], actual_volume_table: Dict[str, EnsembleTableProvider], - co2_table_provider: Dict[str, EnsembleTableProvider], + mass_relpath: str, + volume_relpath: str, ) -> Dict[str, Dict[str, Dict[str, List[str]]]]: options: Dict[str, Dict[str, Dict[str, List[str]]]] = {} for ens in ensemble_roots.keys(): options[ens] = {} - real = co2_table_provider[ens].realizations()[0] - for source, table in zip( + for source, table, relpath in zip( [GraphSource.CONTAINMENT_MASS, GraphSource.CONTAINMENT_ACTUAL_VOLUME], [mass_table, actual_volume_table], + [mass_relpath, volume_relpath], ): - try: - options[ens][source] = read_zone_and_region_options(table[ens], real) - except KeyError: - options[ens][source] = {"zones": [], "regions": []} - options[ens][GraphSource.UNSMRY] = {"zones": [], "regions": []} + real = table[ens].realizations()[0] + options[ens][source] = read_menu_options(table[ens], real, relpath) + options[ens][GraphSource.UNSMRY] = { + "zones": [], + "regions": [], + "phases": ["total", "gas", "aqueous"], + } return options diff --git a/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py b/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py index 83f8b7ae0..a138558fc 100644 --- a/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py +++ b/webviz_subsurface/plugins/_co2_leakage/_utilities/surface_publishing.py @@ -1,3 +1,4 @@ +import warnings from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple, Union @@ -44,7 +45,7 @@ def publish_and_get_surface_metadata( address: Union[SurfaceAddress, TruncatedSurfaceAddress], visualization_info: Dict[str, Any], map_attribute_names: Dict[MapAttribute, str], -) -> Tuple[Optional[SurfaceImageMeta], str, Optional[Any]]: +) -> Tuple[Optional[SurfaceImageMeta], Optional[str], Optional[Any]]: if isinstance(address, TruncatedSurfaceAddress): return _publish_and_get_truncated_surface_metadata(server, provider, address) provider_id: str = provider.provider_id() @@ -53,9 +54,13 @@ def publish_and_get_surface_metadata( summed_mass = None if not surf_meta: # This means we need to compute the surface - surface = provider.get_surface(address) + try: + surface = provider.get_surface(address) + except ValueError: + surface = None if not surface: - raise ValueError(f"Could not get surface for address: {address}") + warnings.warn(f"Could not find surface file with properties: {address}") + return None, None, None if address.attribute in [ map_attribute_names[MapAttribute.MASS], map_attribute_names[MapAttribute.FREE], @@ -64,7 +69,11 @@ def publish_and_get_surface_metadata( surface.values = surface.values / SCALE_DICT[visualization_info["unit"]] summed_mass = np.ma.sum(surface.values) if ( - address.attribute != map_attribute_names[MapAttribute.MIGRATION_TIME] + address.attribute + not in [ + map_attribute_names[MapAttribute.MIGRATION_TIME_SGAS], + map_attribute_names[MapAttribute.MIGRATION_TIME_AMFG], + ] and visualization_info["threshold"] >= 0 ): surface.operation("elile", visualization_info["threshold"]) diff --git a/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py b/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py index e6e4b92ad..784c687ad 100644 --- a/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py +++ b/webviz_subsurface/plugins/_co2_leakage/views/mainview/settings.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines import warnings from typing import Any, Dict, List, Optional, Tuple, Union @@ -78,7 +79,7 @@ def __init__( map_attribute_names: Dict[MapAttribute, str], color_scale_names: List[str], well_names_dict: Dict[str, List[str]], - zone_and_region_options: Dict[str, Dict[str, Dict[str, List[str]]]], + menu_options: Dict[str, Dict[str, Dict[str, List[str]]]], ): super().__init__("Settings") self._ensemble_paths = ensemble_paths @@ -87,15 +88,15 @@ def __init__( self._color_scale_names = color_scale_names self._initial_surface = initial_surface self._well_names_dict = well_names_dict - self._zone_and_region_options = zone_and_region_options + self._menu_options = menu_options self._has_zones = max( len(inner_dict["zones"]) > 0 - for outer_dict in zone_and_region_options.values() + for outer_dict in menu_options.values() for inner_dict in outer_dict.values() ) self._has_regions = max( len(inner_dict["regions"]) > 0 - for outer_dict in zone_and_region_options.values() + for outer_dict in menu_options.values() for inner_dict in outer_dict.values() ) @@ -190,10 +191,14 @@ def set_formations( prop_name = property_origin(MapAttribute(prop), self._map_attribute_names) surfaces = surface_provider.surface_names_for_attribute(prop_name) if len(surfaces) == 0: - warnings.warn( - f"Surface not found for property: {prop}.\n" - f"Expected name: --{prop_name}--.gri" - ) + warning = f"Surface not found for property: {prop}.\n" + warning += f"Expected name: --{prop_name}" + if MapAttribute(prop) not in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ]: + warning += "--" + warnings.warn(warning + ".gri") # Formation names formations = [{"label": v.title(), "value": v} for v in surfaces] picked_formation = None @@ -246,7 +251,10 @@ def set_color_range_data( Input(self.component_unique_id(self.Ids.PROPERTY).to_string(), "value"), ) def set_visualization_threshold(attribute: str) -> bool: - return MapAttribute(attribute) == MapAttribute.MIGRATION_TIME + return MapAttribute(attribute) in [ + MapAttribute.MIGRATION_TIME_SGAS, + MapAttribute.MIGRATION_TIME_AMFG, + ] @callback( Output( @@ -267,6 +275,24 @@ def set_y_min_max( ) -> Tuple[bool, bool]: return len(min_auto) == 1, len(max_auto) == 1 + @callback( + Output(self.component_unique_id(self.Ids.PHASE).to_string(), "options"), + Output(self.component_unique_id(self.Ids.PHASE).to_string(), "value"), + Input(self.component_unique_id(self.Ids.GRAPH_SOURCE).to_string(), "value"), + Input(self.component_unique_id(self.Ids.ENSEMBLE).to_string(), "value"), + State(self.component_unique_id(self.Ids.PHASE).to_string(), "value"), + ) + def set_phases( + source: GraphSource, + ensemble: str, + current_value: str, + ) -> Tuple[List[Dict[str, str]], Union[Any, str]]: + if ensemble is not None: + phases = self._menu_options[ensemble][source]["phases"] + options = [{"label": phase.title(), "value": phase} for phase in phases] + return options, no_update if current_value in phases else "total" + return [], "total" + @callback( Output(self.component_unique_id(self.Ids.ZONE).to_string(), "options"), Output(self.component_unique_id(self.Ids.ZONE).to_string(), "value"), @@ -280,11 +306,11 @@ def set_zones( current_value: str, ) -> Tuple[List[Dict[str, str]], Union[Any, str]]: if ensemble is not None: - zones = self._zone_and_region_options[ensemble][source]["zones"] + zones = self._menu_options[ensemble][source]["zones"] if len(zones) > 0: options = [{"label": zone.title(), "value": zone} for zone in zones] return options, no_update if current_value in zones else "all" - return [], None + return [], "all" @callback( Output(self.component_unique_id(self.Ids.REGION).to_string(), "options"), @@ -299,11 +325,11 @@ def set_regions( current_value: str, ) -> Tuple[List[Dict[str, str]], Union[Any, str]]: if ensemble is not None: - regions = self._zone_and_region_options[ensemble][source]["regions"] + regions = self._menu_options[ensemble][source]["regions"] if len(regions) > 0: options = [{"label": reg.title(), "value": reg} for reg in regions] return options, no_update if current_value in regions else "all" - return [], None + return [], "all" @callback( Output( @@ -336,14 +362,7 @@ def toggle_unit(attribute: str) -> bool: def organize_color_and_mark_menus( color_choice: str, mark_choice: str, - ) -> Tuple[ - List[Dict[str, str]], - str, - Dict[str, str], - Dict[str, str], - Dict[str, str], - Dict[str, str], - ]: + ) -> Tuple[List[Dict], str, Dict, Dict, Dict, Dict]: mark_options = [ {"label": "Phase", "value": "phase"}, {"label": "None", "value": "none"}, @@ -501,7 +520,7 @@ def __init__( wcc.Dropdown( id=property_id, options=_compile_property_options(), - value=MapAttribute.MIGRATION_TIME.value, + value=MapAttribute.MIGRATION_TIME_SGAS.value, clearable=False, ), "Statistic", @@ -693,6 +712,7 @@ def __init__( [ "Zone", wcc.Dropdown( + value="all", id=containment_ids[3], clearable=False, ), @@ -708,6 +728,7 @@ def __init__( [ "Region", wcc.Dropdown( + value="all", id=containment_ids[5], clearable=False, ), @@ -723,11 +744,6 @@ def __init__( [ "Phase", wcc.Dropdown( - options=[ - {"label": "Total", "value": "total"}, - {"label": "Aqueous", "value": "aqueous"}, - {"label": "Gas", "value": "gas"}, - ], value="total", clearable=False, id=containment_ids[8], @@ -862,8 +878,8 @@ def _compile_property_options() -> List[Dict[str, Any]]: "disabled": True, }, { - "label": MapAttribute.MIGRATION_TIME.value, - "value": MapAttribute.MIGRATION_TIME.value, + "label": MapAttribute.MIGRATION_TIME_SGAS.value, + "value": MapAttribute.MIGRATION_TIME_SGAS.value, }, {"label": MapAttribute.MAX_SGAS.value, "value": MapAttribute.MAX_SGAS.value}, { @@ -875,6 +891,10 @@ def _compile_property_options() -> List[Dict[str, Any]]: "value": "", "disabled": True, }, + { + "label": MapAttribute.MIGRATION_TIME_AMFG.value, + "value": MapAttribute.MIGRATION_TIME_AMFG.value, + }, {"label": MapAttribute.MAX_AMFG.value, "value": MapAttribute.MAX_AMFG.value}, { "label": MapAttribute.AMFG_PLUME.value,