From 6d093ef0473d574edb681c64f1ee04a02175105d Mon Sep 17 00:00:00 2001 From: EirikPeirik <104941095+EirikPeirik@users.noreply.github.com> Date: Thu, 15 Sep 2022 10:16:27 +0200 Subject: [PATCH] Well analysis trail (#1089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactored the code according to the WLF best practice * Removed well attributes filter, and more code improvements * Refactored code again, putting figures in view utils folders * Introduced a view element and got rid of the last store * Simplified tour_steps and removed some imports from init files * Added label to layout options checkbox * Implemented a view element also for the well control view * Added changelog * Removed the error layout * Implemented StrEnum and callback_typecheck * Fixed bug in callback so that only layout is updated if data is unchanged * Implemented new callback_typecheck that handles Optional type * Minor updates to tour_steps Co-authored-by: Eirik Sundby Håland (OG SUB RPE) Co-authored-by: Eirik Sundby Håland (OG SUB RPE) Co-authored-by: Øyvind Lind-Johansen --- CHANGELOG.md | 3 +- .../_well_analysis/_callbacks/__init__.py | 2 - .../_callbacks/well_control_callbacks.py | 91 ----- .../_callbacks/well_overview_callbacks.py | 246 ------------ .../_well_analysis/_figures/__init__.py | 2 - .../_well_analysis/_layout/__init__.py | 4 - .../_layout/clientside_stores.py | 20 - .../_well_analysis/_layout/main_layout.py | 20 - .../_layout/well_control_layout.py | 123 ------ .../_layout/well_overview_layout.py | 214 ---------- .../plugins/_well_analysis/_plugin.py | 92 +++-- .../plugins/_well_analysis/_types.py | 8 +- .../plugins/_well_analysis/_utils/__init__.py | 1 + .../_ensemble_well_analysis_data.py | 2 +- .../plugins/_well_analysis/_views/__init__.py | 0 .../_views/_well_control_view/__init__.py | 2 + .../_well_control_view/_utils/__init__.py | 1 + .../_utils/_well_control_figure.py} | 4 +- .../_views/_well_control_view/_view.py | 321 +++++++++++++++ .../_well_control_view/_view_element.py | 16 + .../_views/_well_overview_view/__init__.py | 2 + .../_well_overview_view/_utils/__init__.py | 1 + .../_utils/_well_overview_figure.py} | 4 +- .../_views/_well_overview_view/_view.py | 365 ++++++++++++++++++ .../_well_overview_view/_view_element.py | 20 + 25 files changed, 801 insertions(+), 763 deletions(-) delete mode 100644 webviz_subsurface/plugins/_well_analysis/_callbacks/__init__.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_callbacks/well_control_callbacks.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_callbacks/well_overview_callbacks.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_figures/__init__.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_layout/__init__.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_layout/clientside_stores.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_layout/main_layout.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_layout/well_control_layout.py delete mode 100644 webviz_subsurface/plugins/_well_analysis/_layout/well_overview_layout.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_utils/__init__.py rename webviz_subsurface/plugins/_well_analysis/{ => _utils}/_ensemble_well_analysis_data.py (99%) create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/__init__.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/__init__.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/__init__.py rename webviz_subsurface/plugins/_well_analysis/{_figures/well_control_figure.py => _views/_well_control_view/_utils/_well_control_figure.py} (99%) create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view_element.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/__init__.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/__init__.py rename webviz_subsurface/plugins/_well_analysis/{_figures/well_overview_figure.py => _views/_well_overview_view/_utils/_well_overview_figure.py} (98%) create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py create mode 100644 webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view_element.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d0a3241..a137e43b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [#1097](https://github.com/equinor/webviz-subsurface/pull/1007) - `smry2arrow_batch` - Now supports an arbitrary number of paths as input, meaning that it is no longer needed to wrap a wildcarded runpath pattern with "". It is though still required if defining a wildcarded eclbase. -- [#1080](https://github.com/equinor/webviz-subsurface/pull/1080) - `GroupTree` - Converted the GroupTree plugin to WLF (Webviz Layout Framework). +- [#1080](https://github.com/equinor/webviz-subsurface/pull/1080) - Converted the `GroupTree` plugin to WLF (Webviz Layout Framework). +- [#1089](https://github.com/equinor/webviz-subsurface/pull/1080) - Converted the `WellAnalysis` plugin to WLF (Webviz Layout Framework). ## [0.2.14] - 2022-06-28 diff --git a/webviz_subsurface/plugins/_well_analysis/_callbacks/__init__.py b/webviz_subsurface/plugins/_well_analysis/_callbacks/__init__.py deleted file mode 100644 index cd89a23d6..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_callbacks/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .well_control_callbacks import well_control_callbacks -from .well_overview_callbacks import well_overview_callbacks diff --git a/webviz_subsurface/plugins/_well_analysis/_callbacks/well_control_callbacks.py b/webviz_subsurface/plugins/_well_analysis/_callbacks/well_control_callbacks.py deleted file mode 100644 index a3cf49981..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_callbacks/well_control_callbacks.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple - -import webviz_core_components as wcc -from dash import Dash, Input, Output, State -from webviz_config import WebvizConfigTheme - -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .._figures import create_well_control_figure -from .._layout import WellControlLayoutElements -from .._types import PressurePlotMode - - -def well_control_callbacks( - app: Dash, - get_uuid: Callable, - data_models: Dict[str, EnsembleWellAnalysisData], - theme: WebvizConfigTheme, -) -> None: - @app.callback( - Output(get_uuid(WellControlLayoutElements.WELL), "options"), - Output(get_uuid(WellControlLayoutElements.WELL), "value"), - Output(get_uuid(WellControlLayoutElements.REAL), "options"), - Output(get_uuid(WellControlLayoutElements.REAL), "value"), - Input(get_uuid(WellControlLayoutElements.ENSEMBLE), "value"), - State(get_uuid(WellControlLayoutElements.WELL), "value"), - State(get_uuid(WellControlLayoutElements.REAL), "value"), - ) - def _update_dropdowns( - ensemble: str, state_well: str, state_real: int - ) -> Tuple[ - List[Dict[str, str]], Optional[str], List[Dict[str, Any]], Optional[int] - ]: - """Updates the well and realization dropdowns with ensemble values""" - wells = data_models[ensemble].wells - reals = data_models[ensemble].realizations - return ( - [{"label": well, "value": well} for well in wells], - state_well if state_well in wells else wells[0], - [{"label": real, "value": real} for real in reals], - state_real if state_real in reals else reals[0], - ) - - @app.callback( - Output(get_uuid(WellControlLayoutElements.GRAPH), "children"), - Input(get_uuid(WellControlLayoutElements.WELL), "value"), - Input(get_uuid(WellControlLayoutElements.INCLUDE_BHP), "value"), - Input(get_uuid(WellControlLayoutElements.PRESSURE_PLOT_MODE), "value"), - Input(get_uuid(WellControlLayoutElements.REAL), "value"), - Input(get_uuid(WellControlLayoutElements.CTRLMODE_BAR), "value"), - Input(get_uuid(WellControlLayoutElements.SHARED_XAXES), "value"), - State(get_uuid(WellControlLayoutElements.ENSEMBLE), "value"), - prevent_initial_call=True, - ) - def _update_figure( - well: str, - include_bhp: List[str], - pressure_plot_mode_string: str, - real: int, - display_ctrlmode_bar: bool, - shared_xaxes: List[str], - ensemble: str, - ) -> List[Optional[Any]]: - """Updates the well control figure""" - pressure_plot_mode = PressurePlotMode(pressure_plot_mode_string) - fig = create_well_control_figure( - data_models[ensemble].get_node_info(well, pressure_plot_mode, real), - data_models[ensemble].summary_data, - pressure_plot_mode, - real, - display_ctrlmode_bar, - "shared_xaxes" in shared_xaxes, - "include_bhp" in include_bhp, - theme, - ) - - return wcc.Graph(style={"height": "87vh"}, figure=fig) - - @app.callback( - Output( - get_uuid(WellControlLayoutElements.SINGLE_REAL_OPTIONS), - component_property="style", - ), - Input(get_uuid(WellControlLayoutElements.PRESSURE_PLOT_MODE), "value"), - ) - def _show_hide_single_real_options(pressure_plot_mode: str) -> Dict[str, str]: - """Hides or unhides the realization dropdown according to whether mean - or single realization is selected. - """ - if PressurePlotMode(pressure_plot_mode) == PressurePlotMode.MEAN: - return {"display": "none"} - return {"display": "block"} diff --git a/webviz_subsurface/plugins/_well_analysis/_callbacks/well_overview_callbacks.py b/webviz_subsurface/plugins/_well_analysis/_callbacks/well_overview_callbacks.py deleted file mode 100644 index f983bed5a..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_callbacks/well_overview_callbacks.py +++ /dev/null @@ -1,246 +0,0 @@ -import datetime -from typing import Callable, Dict, List, Set, Union - -import plotly.graph_objects as go -import webviz_core_components as wcc -from dash import ALL, Dash, Input, Output, State, callback, callback_context, no_update -from webviz_config import WebvizConfigTheme - -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .._figures import WellOverviewFigure, format_well_overview_figure -from .._layout import ClientsideStoreElements, WellOverviewLayoutElements -from .._types import ChartType - - -def well_overview_callbacks( - app: Dash, - get_uuid: Callable, - data_models: Dict[str, EnsembleWellAnalysisData], - theme: WebvizConfigTheme, -) -> None: - @app.callback( - Output(get_uuid(ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED), "data"), - Input( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_BUTTON), - "button": ALL, - }, - "n_clicks", - ), - State( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_BUTTON), - "button": ALL, - }, - "id", - ), - ) - def _update_chart_selected(_apply_click: int, button_ids: list) -> str: - """Stores the selected chart type in ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED""" - ctx = callback_context.triggered[0] - - # handle initial callback - if ctx["prop_id"] == ".": - return ChartType.BAR.value - - for button_id in button_ids: - if button_id["button"] in ctx["prop_id"]: - return ChartType(button_id["button"]).value - raise ValueError("Id not found") - - @callback( - Output( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_BUTTON), - "button": ALL, - }, - "style", - ), - Input(get_uuid(ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED), "data"), - State( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_BUTTON), - "button": ALL, - }, - "id", - ), - ) - def _update_button_style(chart_selected: str, button_ids: list) -> list: - """Updates the styling of the chart type buttons, showing which chart type - is currently selected. - """ - button_styles = { - button["button"]: {"background-color": "#E8E8E8"} for button in button_ids - } - button_styles[chart_selected] = {"background-color": "#7393B3", "color": "#fff"} - - return update_relevant_components( - id_list=button_ids, - update_info=[ - { - "new_value": style, - "conditions": {"button": button}, - } - for button, style in button_styles.items() - ], - ) - - @callback( - Output( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_SETTINGS), - "charttype": ALL, - }, - "style", - ), - Input(get_uuid(ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED), "data"), - State( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_SETTINGS), - "charttype": ALL, - }, - "id", - ), - ) - def _display_charttype_settings( - chart_selected: str, charttype_settings_ids: list - ) -> list: - """Display only the settings relevant for the currently selected chart type.""" - return [ - {"display": "block"} - if settings_id["charttype"] == chart_selected - else {"display": "none"} - for settings_id in charttype_settings_ids - ] - - @app.callback( - Output(get_uuid(WellOverviewLayoutElements.GRAPH_FRAME), "children"), - Input(get_uuid(WellOverviewLayoutElements.ENSEMBLES), "value"), - Input( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_CHECKLIST), - "charttype": ALL, - }, - "value", - ), - Input(get_uuid(WellOverviewLayoutElements.SUMVEC), "value"), - Input(get_uuid(WellOverviewLayoutElements.DATE), "value"), - Input(get_uuid(ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED), "data"), - Input(get_uuid(WellOverviewLayoutElements.WELL_FILTER), "value"), - Input( - { - "id": get_uuid(WellOverviewLayoutElements.WELL_ATTRIBUTES), - "category": ALL, - }, - "value", - ), - State( - { - "id": get_uuid(WellOverviewLayoutElements.CHARTTYPE_CHECKLIST), - "charttype": ALL, - }, - "id", - ), - State( - { - "id": get_uuid(WellOverviewLayoutElements.WELL_ATTRIBUTES), - "category": ALL, - }, - "id", - ), - State(get_uuid(WellOverviewLayoutElements.GRAPH), "figure"), - ) - def _update_graph( - ensembles: List[str], - checklist_values: List[List[str]], - sumvec: str, - prod_after_date: Union[str, None], - chart_selected: str, - wells_selected: List[str], - well_attr_selected: List[str], - checklist_ids: List[Dict[str, str]], - well_attr_ids: List[Dict[str, str]], - current_fig_dict: dict, - ) -> List[wcc.Graph]: - # pylint: disable=too-many-locals - # pylint: disable=too-many-arguments - """Updates the well overview graph with selected input (f.ex chart type)""" - ctx = callback_context.triggered[0]["prop_id"].split(".")[0] - - settings = { - checklist_id["charttype"]: checklist_values[i] - for i, checklist_id in enumerate(checklist_ids) - } - well_attributes_selected: Dict[str, List[str]] = { - well_attr_id["category"]: list(well_attr_selected[i]) - for i, well_attr_id in enumerate(well_attr_ids) - } - - # Make set of wells that match the well_attributes - # Well attributes that does not exist in one ensemble will be ignored - wellattr_filtered_wells: Set[str] = set() - for _, ens_data_model in data_models.items(): - wellattr_filtered_wells = wellattr_filtered_wells.union( - ens_data_model.filter_on_well_attributes(well_attributes_selected) - ) - # Take the intersection with wells_selected. - # this way preserves the order in wells_selected and will not have duplicates - filtered_wells = [ - well for well in wells_selected if well in wellattr_filtered_wells - ] - - # If the event is a plot settings event, then we only update the formatting - # and not the figure data - chart_selected_type = ChartType(chart_selected) - if current_fig_dict is not None and is_plot_settings_event(ctx, get_uuid): - fig_dict = format_well_overview_figure( - go.Figure(current_fig_dict), - chart_selected_type, - settings[chart_selected_type.value], - sumvec, - prod_after_date, - ) - else: - figure = WellOverviewFigure( - ensembles, - data_models, - sumvec, - datetime.datetime.strptime(prod_after_date, "%Y-%m-%d") - if prod_after_date is not None - else None, - chart_selected_type, - filtered_wells, - theme, - ) - - fig_dict = format_well_overview_figure( - figure.figure, - chart_selected_type, - settings[chart_selected_type.value], - sumvec, - prod_after_date, - ) - - return [ - wcc.Graph( - id=get_uuid(WellOverviewLayoutElements.GRAPH), - style={"height": "87vh"}, - figure=fig_dict, - ) - ] - - -def is_plot_settings_event(ctx: str, get_uuid: Callable) -> bool: - if get_uuid(WellOverviewLayoutElements.CHARTTYPE_CHECKLIST) in ctx: - return True - return False - - -def update_relevant_components(id_list: list, update_info: List[dict]) -> list: - output_id_list = [no_update] * len(id_list) - for elm in update_info: - for idx, x in enumerate(id_list): - if all(x[key] == value for key, value in elm["conditions"].items()): - output_id_list[idx] = elm["new_value"] - break - return output_id_list diff --git a/webviz_subsurface/plugins/_well_analysis/_figures/__init__.py b/webviz_subsurface/plugins/_well_analysis/_figures/__init__.py deleted file mode 100644 index b4f8df202..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_figures/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .well_control_figure import create_well_control_figure -from .well_overview_figure import WellOverviewFigure, format_well_overview_figure diff --git a/webviz_subsurface/plugins/_well_analysis/_layout/__init__.py b/webviz_subsurface/plugins/_well_analysis/_layout/__init__.py deleted file mode 100644 index c0232a6ad..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_layout/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .clientside_stores import ClientsideStoreElements, clientside_stores -from .main_layout import main_layout -from .well_control_layout import WellControlLayoutElements -from .well_overview_layout import WellOverviewLayoutElements diff --git a/webviz_subsurface/plugins/_well_analysis/_layout/clientside_stores.py b/webviz_subsurface/plugins/_well_analysis/_layout/clientside_stores.py deleted file mode 100644 index 28eafd758..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_layout/clientside_stores.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Callable - -from dash import dcc, html - - -# pylint: disable = too-few-public-methods -class ClientsideStoreElements: - WELL_OVERVIEW_CHART_SELECTED = "well-overview-chart-selected" - - -def clientside_stores(get_uuid: Callable) -> html.Div: - """Contains the clientside stores""" - return html.Div( - children=[ - dcc.Store( - id=get_uuid(ClientsideStoreElements.WELL_OVERVIEW_CHART_SELECTED), - storage_type="session", - ), - ] - ) diff --git a/webviz_subsurface/plugins/_well_analysis/_layout/main_layout.py b/webviz_subsurface/plugins/_well_analysis/_layout/main_layout.py deleted file mode 100644 index 9a35b5101..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_layout/main_layout.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Callable, Dict - -import webviz_core_components as wcc - -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .well_control_layout import well_control_tab -from .well_overview_layout import well_overview_tab - - -def main_layout( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.Tabs: - """Main layout""" - tabs = [ - wcc.Tab( - label="Well Overview", children=well_overview_tab(get_uuid, data_models) - ), - wcc.Tab(label="Well Control", children=well_control_tab(get_uuid, data_models)), - ] - return wcc.Tabs(children=tabs) diff --git a/webviz_subsurface/plugins/_well_analysis/_layout/well_control_layout.py b/webviz_subsurface/plugins/_well_analysis/_layout/well_control_layout.py deleted file mode 100644 index 9654bd9c9..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_layout/well_control_layout.py +++ /dev/null @@ -1,123 +0,0 @@ -from typing import Callable, Dict - -import webviz_core_components as wcc -from dash import html - -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .._types import PressurePlotMode - - -# pylint: disable = too-few-public-methods -class WellControlLayoutElements: - GRAPH = "well-control-graph" - ENSEMBLE = "well-control-ensemble" - WELL = "well-control-well" - INCLUDE_BHP = "well-control-include-bhp" - PRESSURE_PLOT_MODE = "well-control-pressure-plot-mode" - SINGLE_REAL_OPTIONS = "well-control-single-real-options" - REAL = "well-control-real" - CTRLMODE_BAR = "well-control-ctrlmode-bar" - SHARED_XAXES = "well-control-shared-xaxes" - - -def well_control_tab( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.FlexBox: - return wcc.FlexBox( - children=[ - controls(get_uuid, data_models), - wcc.Frame( - style={"flex": 5, "height": "87vh"}, - color="white", - highlight=False, - id=get_uuid(WellControlLayoutElements.GRAPH), - children=[], - ), - ] - ) - - -def controls( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.Frame: - ensembles = list(data_models.keys()) - return wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=[ - wcc.Selectors( - label="Plot Controls", - children=[ - wcc.Dropdown( - label="Ensemble", - id=get_uuid(WellControlLayoutElements.ENSEMBLE), - options=[{"label": col, "value": col} for col in ensembles], - value=ensembles[0], - multi=False, - ), - wcc.Dropdown( - label="Well", - id=get_uuid(WellControlLayoutElements.WELL), - options=[], - value=None, - multi=False, - ), - ], - ), - wcc.Selectors( - label="Pressure Plot Options", - children=[ - wcc.Checklist( - id=get_uuid(WellControlLayoutElements.INCLUDE_BHP), - options=[{"label": "Include BHP", "value": "include_bhp"}], - value=["include_bhp"], - ), - wcc.RadioItems( - label="Mean or realization", - id=get_uuid(WellControlLayoutElements.PRESSURE_PLOT_MODE), - options=[ - { - "label": "Mean of producing real.", - "value": PressurePlotMode.MEAN.value, - }, - { - "label": "Single realization", - "value": PressurePlotMode.SINGLE_REAL.value, - }, - ], - value=PressurePlotMode.MEAN.value, - ), - html.Div( - id=get_uuid(WellControlLayoutElements.SINGLE_REAL_OPTIONS), - children=[ - wcc.Dropdown( - id=get_uuid(WellControlLayoutElements.REAL), - options=[], - value=None, - multi=False, - ), - wcc.Checklist( - id=get_uuid(WellControlLayoutElements.CTRLMODE_BAR), - options=[ - { - "label": "Display ctrl mode bar", - "value": "ctrlmode_bar", - } - ], - value=["ctrlmode_bar"], - ), - ], - ), - ], - ), - wcc.Selectors( - label="⚙️ Settings", - children=[ - wcc.Checklist( - id=get_uuid(WellControlLayoutElements.SHARED_XAXES), - options=[{"label": "Shared x-axis", "value": "shared_xaxes"}], - value=["shared_xaxes"], - ), - ], - ), - ], - ) diff --git a/webviz_subsurface/plugins/_well_analysis/_layout/well_overview_layout.py b/webviz_subsurface/plugins/_well_analysis/_layout/well_overview_layout.py deleted file mode 100644 index 4a131bbae..000000000 --- a/webviz_subsurface/plugins/_well_analysis/_layout/well_overview_layout.py +++ /dev/null @@ -1,214 +0,0 @@ -import datetime -from typing import Callable, Dict, List, Set - -import webviz_core_components as wcc -from dash import html - -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .._types import ChartType - - -# pylint: disable = too-few-public-methods -class WellOverviewLayoutElements: - GRAPH_FRAME = "well-overview-graph-frame" - GRAPH = "well-overview-graph" - ENSEMBLES = "well-overview-ensembles" - SUMVEC = "well-overview-sumvec" - CHARTTYPE_BUTTON = "well-overview-charttype-button" - CHARTTYPE_SETTINGS = "well-overview-charttype-settings" - CHARTTYPE_CHECKLIST = "well-overview-charttype-checklist" - WELL_FILTER = "well-overview-well-filter" - WELL_ATTRIBUTES = "well-overview-well-attributes" - DATE = "well-overview-date" - - -def well_overview_tab( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.FlexBox: - """Well overview tab""" - return wcc.FlexBox( - children=[ - wcc.Frame( - style={"flex": 1, "height": "87vh"}, - children=[ - buttons(get_uuid), - controls(get_uuid, data_models), - filters(get_uuid, data_models), - plot_settings(get_uuid), - ], - ), - wcc.Frame( - style={"flex": 4, "height": "87vh"}, - color="white", - highlight=False, - id=get_uuid(WellOverviewLayoutElements.GRAPH_FRAME), - children=[ - wcc.Graph( - id=get_uuid(WellOverviewLayoutElements.GRAPH), - ) - ], - ), - ] - ) - - -def buttons(get_uuid: Callable) -> html.Div: - uuid = get_uuid(WellOverviewLayoutElements.CHARTTYPE_BUTTON) - return html.Div( - style={"margin-bottom": "20px"}, - children=[ - html.Button( - "Bar Chart", - className="webviz-inplace-vol-btn", - id={"id": uuid, "button": ChartType.BAR}, - ), - html.Button( - "Pie Chart", - className="webviz-inplace-vol-btn", - id={"id": uuid, "button": ChartType.PIE}, - ), - html.Button( - "Stacked Area Chart", - className="webviz-inplace-vol-btn", - id={"id": uuid, "button": ChartType.AREA}, - ), - ], - ) - - -def controls( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.Selectors: - ensembles = list(data_models.keys()) - dates: Set[datetime.datetime] = set() - for _, ens_data_model in data_models.items(): - dates = dates.union(ens_data_model.dates) - sorted_dates: List[datetime.datetime] = sorted(list(dates)) - - return wcc.Selectors( - label="Plot Controls", - children=[ - wcc.Dropdown( - label="Ensembles", - id=get_uuid(WellOverviewLayoutElements.ENSEMBLES), - options=[{"label": col, "value": col} for col in ensembles], - value=ensembles, - multi=True, - ), - wcc.Dropdown( - label="Response", - id=get_uuid(WellOverviewLayoutElements.SUMVEC), - options=[ - {"label": "Oil production", "value": "WOPT"}, - {"label": "Gas production", "value": "WGPT"}, - {"label": "Water production", "value": "WWPT"}, - ], - value="WOPT", - multi=False, - clearable=False, - ), - wcc.Dropdown( - label="Only Production after date", - id=get_uuid(WellOverviewLayoutElements.DATE), - options=[ - { - "label": dte.strftime("%Y-%m-%d"), - "value": dte.strftime("%Y-%m-%d"), - } - for dte in sorted_dates - ], - multi=False, - ), - ], - ) - - -def filters( - get_uuid: Callable, data_models: Dict[str, EnsembleWellAnalysisData] -) -> wcc.Selectors: - # Collecting wells and well_attributes from all ensembles. - wells = [] - well_attr = {} - for _, ens_data_model in data_models.items(): - wells.extend([well for well in ens_data_model.wells if well not in wells]) - for category, values in ens_data_model.well_attributes.items(): - if category not in well_attr: - well_attr[category] = values - else: - well_attr[category].extend( - [value for value in values if value not in well_attr[category]] - ) - - return wcc.Selectors( - label="Filters", - children=[ - wcc.SelectWithLabel( - label="Well", - size=min(10, len(wells)), - id=get_uuid(WellOverviewLayoutElements.WELL_FILTER), - options=[{"label": well, "value": well} for well in wells], - value=wells, - multi=True, - ) - ] - # Adding well attributes selectors - + [ - wcc.SelectWithLabel( - label=category.capitalize(), - size=min(5, len(values)), - id={ - "id": get_uuid(WellOverviewLayoutElements.WELL_ATTRIBUTES), - "category": category, - }, - options=[{"label": value, "value": value} for value in values], - value=values, - multi=True, - ) - for category, values in well_attr.items() - ], - ) - - -def plot_settings(get_uuid: Callable) -> wcc.Frame: - settings_uuid = get_uuid(WellOverviewLayoutElements.CHARTTYPE_SETTINGS) - checklist_uuid = get_uuid(WellOverviewLayoutElements.CHARTTYPE_CHECKLIST) - return wcc.Selectors( - label="Plot Settings", - children=[ - html.Div( - id={"id": settings_uuid, "charttype": "bar"}, - children=wcc.Checklist( - id={"id": checklist_uuid, "charttype": "bar"}, - options=[ - {"label": "Show legend", "value": "legend"}, - {"label": "Overlay bars", "value": "overlay_bars"}, - {"label": "Show prod as text", "value": "show_prod_text"}, - {"label": "White background", "value": "white_background"}, - ], - value=["legend"], - ), - ), - html.Div( - id={"id": settings_uuid, "charttype": "pie"}, - children=wcc.Checklist( - id={"id": checklist_uuid, "charttype": "pie"}, - options=[ - {"label": "Show legend", "value": "legend"}, - {"label": "Show prod as text", "value": "show_prod_text"}, - ], - value=[], - ), - ), - html.Div( - id={"id": settings_uuid, "charttype": "area"}, - children=wcc.Checklist( - id={"id": checklist_uuid, "charttype": "area"}, - options=[ - {"label": "Show legend", "value": "legend"}, - {"label": "White background", "value": "white_background"}, - ], - value=["legend"], - ), - ), - ], - ) diff --git a/webviz_subsurface/plugins/_well_analysis/_plugin.py b/webviz_subsurface/plugins/_well_analysis/_plugin.py index 42fbc75ec..d614e1676 100644 --- a/webviz_subsurface/plugins/_well_analysis/_plugin.py +++ b/webviz_subsurface/plugins/_well_analysis/_plugin.py @@ -1,11 +1,8 @@ from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple -from dash import Dash, html from webviz_config import WebvizPluginABC, WebvizSettings -from webviz_config.webviz_assets import WEBVIZ_ASSETS - -import webviz_subsurface +from webviz_config.utils import StrEnum from ..._models import GruptreeModel, WellAttributesModel from ..._providers import ( @@ -13,9 +10,9 @@ EnsembleSummaryProviderFactory, Frequency, ) -from ._callbacks import well_control_callbacks, well_overview_callbacks -from ._ensemble_well_analysis_data import EnsembleWellAnalysisData -from ._layout import clientside_stores, main_layout +from ._utils import EnsembleWellAnalysisData +from ._views._well_control_view import WellControlView, WellControlViewElement +from ._views._well_overview_view import WellOverviewView, WellOverviewViewElement class WellAnalysis(WebvizPluginABC): @@ -62,10 +59,12 @@ class WellAnalysis(WebvizPluginABC): """ - # pylint: disable=too-many-arguments + class Ids(StrEnum): + WELL_OVERVIEW = "well-overview" + WELL_CONTROL = "well-control" + def __init__( self, - app: Dash, webviz_settings: WebvizSettings, ensembles: Optional[List[str]] = None, rel_file_pattern: str = "share/results/unsmry/*.arrow", @@ -76,14 +75,6 @@ def __init__( ) -> None: super().__init__() - # This is used to format the buttons in the well overview tab - WEBVIZ_ASSETS.add( - Path(webviz_subsurface.__file__).parent - / "_assets" - / "css" - / "inplace_volumes.css" - ) - self._ensembles = ensembles self._theme = webviz_settings.theme @@ -116,7 +107,59 @@ def __init__( filter_out_startswith=filter_out_startswith, ) - self.set_callbacks(app) + self.add_view( + WellOverviewView(self._data_models, self._theme), + self.Ids.WELL_OVERVIEW, + ) + self.add_view( + WellControlView(self._data_models, self._theme), + self.Ids.WELL_CONTROL, + ) + + @property + def tour_steps(self) -> List[dict]: + return [ + { + "id": self.view(self.Ids.WELL_OVERVIEW) + .view_element(WellOverviewView.Ids.VIEW_ELEMENT) + .component_unique_id(WellOverviewViewElement.Ids.GRAPH), + "content": "Shows production split on well for the various chart types", + }, + { + "id": self.view(self.Ids.WELL_OVERVIEW) + .settings_group(WellOverviewView.Ids.SETTINGS) + .get_unique_id(), + "content": "Choose chart type, ensemble, type of production and other options. " + "You also change the layout of the chart.", + }, + { + "id": self.view(self.Ids.WELL_OVERVIEW) + .settings_group(WellOverviewView.Ids.FILTERS) + .get_unique_id(), + "content": "You can choose to view the production for all the wells or " + "select only the ones you are interested in.", + }, + { + "id": self.view(self.Ids.WELL_CONTROL) + .view_element(WellControlView.Ids.VIEW_ELEMENT) + .component_unique_id(WellControlViewElement.Ids.CHART), + "content": "Shows the number of realizations on different control modes " + "and network pressures.", + }, + { + "id": self.view(self.Ids.WELL_CONTROL) + .settings_group(WellControlView.Ids.SETTINGS) + .get_unique_id(), + "content": "Select the ensemble and well you are interested in." + "The well dropdown is autmatically updated when ensemble is selected", + }, + { + "id": self.view(self.Ids.WELL_CONTROL) + .settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .get_unique_id(), + "content": "Here are some options related only to the Network pressures plot.", + }, + ] def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]: return [ @@ -124,16 +167,3 @@ def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]: for _, ens_data_model in self._data_models.items() for webviz_store_tuple in ens_data_model.webviz_store ] - - @property - def layout(self) -> html.Div: - return html.Div( - children=[ - clientside_stores(get_uuid=self.uuid), - main_layout(self.uuid, self._data_models), - ] - ) - - def set_callbacks(self, app: Dash) -> None: - well_overview_callbacks(app, self.uuid, self._data_models, self._theme) - well_control_callbacks(app, self.uuid, self._data_models, self._theme) diff --git a/webviz_subsurface/plugins/_well_analysis/_types.py b/webviz_subsurface/plugins/_well_analysis/_types.py index 9925c7580..42d34fad6 100644 --- a/webviz_subsurface/plugins/_well_analysis/_types.py +++ b/webviz_subsurface/plugins/_well_analysis/_types.py @@ -1,18 +1,18 @@ -from enum import Enum +from webviz_config.utils import StrEnum -class PressurePlotMode(str, Enum): +class PressurePlotMode(StrEnum): MEAN = "mean" SINGLE_REAL = "single-real" -class NodeType(str, Enum): +class NodeType(StrEnum): WELL = "well" GROUP = "group" WELL_BH = "well-bh" -class ChartType(str, Enum): +class ChartType(StrEnum): BAR = "bar" PIE = "pie" AREA = "area" diff --git a/webviz_subsurface/plugins/_well_analysis/_utils/__init__.py b/webviz_subsurface/plugins/_well_analysis/_utils/__init__.py new file mode 100644 index 000000000..d1ab210a6 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_utils/__init__.py @@ -0,0 +1 @@ +from ._ensemble_well_analysis_data import EnsembleWellAnalysisData diff --git a/webviz_subsurface/plugins/_well_analysis/_ensemble_well_analysis_data.py b/webviz_subsurface/plugins/_well_analysis/_utils/_ensemble_well_analysis_data.py similarity index 99% rename from webviz_subsurface/plugins/_well_analysis/_ensemble_well_analysis_data.py rename to webviz_subsurface/plugins/_well_analysis/_utils/_ensemble_well_analysis_data.py index 3346fffb9..12cf4735a 100644 --- a/webviz_subsurface/plugins/_well_analysis/_ensemble_well_analysis_data.py +++ b/webviz_subsurface/plugins/_well_analysis/_utils/_ensemble_well_analysis_data.py @@ -7,7 +7,7 @@ from webviz_subsurface._models import GruptreeModel, WellAttributesModel from webviz_subsurface._providers import EnsembleSummaryProvider -from ._types import NodeType, PressurePlotMode +from .._types import NodeType, PressurePlotMode class EnsembleWellAnalysisData: diff --git a/webviz_subsurface/plugins/_well_analysis/_views/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/__init__.py new file mode 100644 index 000000000..992537865 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/__init__.py @@ -0,0 +1,2 @@ +from ._view import WellControlView +from ._view_element import WellControlViewElement diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/__init__.py new file mode 100644 index 000000000..ac69567aa --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/__init__.py @@ -0,0 +1 @@ +from ._well_control_figure import create_well_control_figure diff --git a/webviz_subsurface/plugins/_well_analysis/_figures/well_control_figure.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py similarity index 99% rename from webviz_subsurface/plugins/_well_analysis/_figures/well_control_figure.py rename to webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py index d7972f1b8..bb0be2f9d 100644 --- a/webviz_subsurface/plugins/_well_analysis/_figures/well_control_figure.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_utils/_well_control_figure.py @@ -6,8 +6,8 @@ from plotly.subplots import make_subplots from webviz_config import WebvizConfigTheme -from ...._utils.colors import StandardColors -from .._types import NodeType, PressurePlotMode +from ......_utils.colors import StandardColors +from ...._types import NodeType, PressurePlotMode def create_well_control_figure( diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view.py new file mode 100644 index 000000000..d2587c71c --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view.py @@ -0,0 +1,321 @@ +from typing import Any, Dict, List, Optional, Tuple + +import webviz_core_components as wcc +from dash import Input, Output, State, callback, html +from dash.development.base_component import Component +from webviz_config import WebvizConfigTheme +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from ..._types import PressurePlotMode +from ..._utils import EnsembleWellAnalysisData +from ._utils import create_well_control_figure +from ._view_element import WellControlViewElement + + +class WellControlSettings(SettingsGroupABC): + class Ids(StrEnum): + ENSEMBLE = "ensemble" + WELL = "well" + SHARED_X_AXIS = "shared-x-axis" + + def __init__(self, data_models: Dict[str, EnsembleWellAnalysisData]) -> None: + super().__init__("Plot Controls") + self.ensembles = list(data_models.keys()) + self.wells: List[str] = [] + for _, ens_data_model in data_models.items(): + self.wells.extend( + [well for well in ens_data_model.wells if well not in self.wells] + ) + + def layout(self) -> List[Component]: + return [ + wcc.Dropdown( + label="Ensemble", + id=self.register_component_unique_id(WellControlSettings.Ids.ENSEMBLE), + options=[{"label": col, "value": col} for col in self.ensembles], + value=self.ensembles[0], + multi=False, + clearable=False, + ), + wcc.Dropdown( + label="Well", + id=self.register_component_unique_id(WellControlSettings.Ids.WELL), + options=[{"label": well, "value": well} for well in self.wells], + value=self.wells[0], + multi=False, + clearable=False, + ), + wcc.Checklist( + id=self.register_component_unique_id( + WellControlSettings.Ids.SHARED_X_AXIS + ), + options=[{"label": "Shared x-axis", "value": "shared_xaxes"}], + value=["shared_xaxes"], + ), + ] + + +class WellControlPressurePlotOptions(SettingsGroupABC): + class Ids(StrEnum): + INCLUDE_BHP = "include-bhp" + PRESSURE_PLOT_MODE = "pressure-plot-mode" + REALIZATION_BOX = "realization-box" + REALIZATION = "realization" + DISPLAY_CTRL_MODE_BAR = "display-ctrl-mode-bar" + + def __init__(self, data_models: Dict[str, EnsembleWellAnalysisData]) -> None: + + super().__init__("Pressure Plot Options") + self.data_models = data_models + self.ensembles = list(data_models.keys()) + + def layout(self) -> List[Component]: + return [ + wcc.Checklist( + id=self.register_component_unique_id( + WellControlPressurePlotOptions.Ids.INCLUDE_BHP + ), + options=[{"label": "Include BHP", "value": "include_bhp"}], + value=["include_bhp"], + ), + wcc.RadioItems( + label="Mean or realization", + id=self.register_component_unique_id( + WellControlPressurePlotOptions.Ids.PRESSURE_PLOT_MODE + ), + options=[ + { + "label": "Mean of producing real.", + "value": PressurePlotMode.MEAN, + }, + { + "label": "Single realization", + "value": PressurePlotMode.SINGLE_REAL, + }, + ], + value=PressurePlotMode.MEAN, + ), + html.Div( + id=self.register_component_unique_id( + WellControlPressurePlotOptions.Ids.REALIZATION_BOX + ), + children=[ + wcc.Dropdown( + id=self.register_component_unique_id( + WellControlPressurePlotOptions.Ids.REALIZATION + ), + options=[], + value=None, + multi=False, + ), + wcc.Checklist( + id=self.register_component_unique_id( + WellControlPressurePlotOptions.Ids.DISPLAY_CTRL_MODE_BAR + ), + options=[ + { + "label": "Display ctrl mode bar", + "value": "ctrlmode_bar", + } + ], + value=["ctrlmode_bar"], + ), + ], + ), + ] + + +class WellControlView(ViewABC): + class Ids(StrEnum): + SETTINGS = "settings" + PRESSUREPLOT_OPTIONS = "pressure-plot-options" + VIEW_ELEMENT = "view-element" + + def __init__( + self, + data_models: Dict[str, EnsembleWellAnalysisData], + theme: WebvizConfigTheme, + ) -> None: + super().__init__("Well Control") + + self.data_models = data_models + self.theme = theme + + self.add_settings_group( + WellControlSettings(self.data_models), WellControlView.Ids.SETTINGS + ) + self.add_settings_group( + WellControlPressurePlotOptions(self.data_models), + WellControlView.Ids.PRESSUREPLOT_OPTIONS, + ) + + main_column = self.add_column() + main_column.add_view_element(WellControlViewElement(), self.Ids.VIEW_ELEMENT) + + def set_callbacks(self) -> None: + @callback( + Output( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.WELL) + .to_string(), + "options", + ), + Output( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.WELL) + .to_string(), + "value", + ), + Output( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.REALIZATION) + .to_string(), + "options", + ), + Output( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.REALIZATION) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.ENSEMBLE) + .to_string(), + "value", + ), + State( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.WELL) + .to_string(), + "value", + ), + State( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.REALIZATION) + .to_string(), + "value", + ), + ) + def _update_dropdowns( + ensemble: str, state_well: str, state_real: int + ) -> Tuple[ + List[Dict[str, str]], Optional[str], List[Dict[str, Any]], Optional[int] + ]: + """Updates the well and realization dropdowns with ensemble values""" + wells = self.data_models[ensemble].wells + reals = self.data_models[ensemble].realizations + return ( + [{"label": well, "value": well} for well in wells], + state_well if state_well in wells else wells[0], + [{"label": real, "value": real} for real in reals], + state_real if state_real in reals else reals[0], + ) + + @callback( + Output( + self.view_element(self.Ids.VIEW_ELEMENT) + .component_unique_id(WellControlViewElement.Ids.CHART) + .to_string(), + "children", + ), + Input( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.ENSEMBLE) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.WELL) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.INCLUDE_BHP) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id( + WellControlPressurePlotOptions.Ids.PRESSURE_PLOT_MODE + ) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.REALIZATION) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id( + WellControlPressurePlotOptions.Ids.DISPLAY_CTRL_MODE_BAR + ) + .to_string(), + "value", + ), + Input( + self.settings_group(WellControlView.Ids.SETTINGS) + .component_unique_id(WellControlSettings.Ids.SHARED_X_AXIS) + .to_string(), + "value", + ), + ) + @callback_typecheck + def _update_graph( + ensemble: str, + well: str, + include_bhp: List[str], + pressure_plot_mode: PressurePlotMode, + real: int, + display_ctrlmode_bar: List[str], + shared_xaxes: List[str], + ) -> Component: + """Updates the well control figure""" + fig = create_well_control_figure( + self.data_models[ensemble].get_node_info( + well, pressure_plot_mode, real + ), + self.data_models[ensemble].summary_data, + pressure_plot_mode, + real, + "ctrlmode_bar" in display_ctrlmode_bar, + "shared_xaxes" in shared_xaxes, + "include_bhp" in include_bhp, + self.theme, + ) + + return wcc.Graph(style={"height": "87vh"}, figure=fig) + + @callback( + Output( + self.settings_group(self.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id(WellControlPressurePlotOptions.Ids.REALIZATION_BOX) + .to_string(), + component_property="style", + ), + Input( + self.settings_group(self.Ids.PRESSUREPLOT_OPTIONS) + .component_unique_id( + WellControlPressurePlotOptions.Ids.PRESSURE_PLOT_MODE + ) + .to_string(), + "value", + ), + ) + @callback_typecheck + def _show_hide_single_real_options( + pressure_plot_mode: PressurePlotMode, + ) -> Dict[str, str]: + """Hides or unhides the realization dropdown according to whether mean + or single realization is selected. + """ + if pressure_plot_mode == PressurePlotMode.MEAN: + return {"display": "none"} + return {"display": "block"} diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view_element.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view_element.py new file mode 100644 index 000000000..5fc94a23a --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_control_view/_view_element.py @@ -0,0 +1,16 @@ +from dash import html +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class WellControlViewElement(ViewElementABC): + class Ids(StrEnum): + CHART = "chart" + + def __init__(self) -> None: + super().__init__() + + def inner_layout(self) -> html.Div: + return html.Div( + id=self.register_component_unique_id(WellControlViewElement.Ids.CHART) + ) diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/__init__.py new file mode 100644 index 000000000..84e14f7a7 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/__init__.py @@ -0,0 +1,2 @@ +from ._view import WellOverviewView +from ._view_element import WellOverviewViewElement diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/__init__.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/__init__.py new file mode 100644 index 000000000..ff41a7034 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/__init__.py @@ -0,0 +1 @@ +from ._well_overview_figure import WellOverviewFigure, format_well_overview_figure diff --git a/webviz_subsurface/plugins/_well_analysis/_figures/well_overview_figure.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py similarity index 98% rename from webviz_subsurface/plugins/_well_analysis/_figures/well_overview_figure.py rename to webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py index 6e79cc7d0..bc4bd71a1 100644 --- a/webviz_subsurface/plugins/_well_analysis/_figures/well_overview_figure.py +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_utils/_well_overview_figure.py @@ -8,8 +8,8 @@ from plotly.subplots import make_subplots from webviz_config import WebvizConfigTheme -from .._ensemble_well_analysis_data import EnsembleWellAnalysisData -from .._types import ChartType +from ...._types import ChartType +from ...._utils import EnsembleWellAnalysisData class WellOverviewFigure: diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py new file mode 100644 index 000000000..3d5d3c9ab --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view.py @@ -0,0 +1,365 @@ +import datetime +from typing import Dict, List, Optional, Set, Union + +import plotly.graph_objects as go +import webviz_core_components as wcc +from dash import ALL, Input, Output, State, callback, callback_context, html +from dash.development.base_component import Component +from webviz_config import WebvizConfigTheme +from webviz_config.utils import StrEnum, callback_typecheck +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC, ViewABC + +from ..._types import ChartType +from ..._utils import EnsembleWellAnalysisData +from ._utils import WellOverviewFigure, format_well_overview_figure +from ._view_element import WellOverviewViewElement + + +class WellOverviewSettings(SettingsGroupABC): + class Ids(StrEnum): + CHARTTYPE = "charttype" + ENSEMBLES = "ensembles" + RESPONSE = "response" + ONLY_PRODUCTION_AFTER_DATE = "only-production-after-date" + CHARTTYPE_SETTINGS = "charttype-settings" + CHARTTYPE_CHECKLIST = "charttype-checklist" + + def __init__(self, data_models: Dict[str, EnsembleWellAnalysisData]) -> None: + super().__init__("Settings") + + self._ensembles = list(data_models.keys()) + + dates: Set[datetime.datetime] = set() + for _, ens_data_model in data_models.items(): + dates = dates.union(ens_data_model.dates) + self._sorted_dates: List[datetime.datetime] = sorted(list(dates)) + + def layout(self) -> List[Component]: + settings_id = self.register_component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE_SETTINGS + ) + checklist_id = self.register_component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE_CHECKLIST + ) + return [ + wcc.RadioItems( + id=self.register_component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE + ), + label="Chart type", + options=[ + {"label": "Bar chart", "value": ChartType.BAR}, + {"label": "Pie chart", "value": ChartType.PIE}, + {"label": "Stacked area chart", "value": ChartType.AREA}, + ], + value=ChartType.BAR, + vertical=True, + ), + wcc.Dropdown( + label="Ensembles", + id=self.register_component_unique_id( + WellOverviewSettings.Ids.ENSEMBLES + ), + options=[{"label": col, "value": col} for col in self._ensembles], + value=self._ensembles, + multi=True, + ), + wcc.Dropdown( + label="Response", + id=self.register_component_unique_id(WellOverviewSettings.Ids.RESPONSE), + options=[ + {"label": "Oil production", "value": "WOPT"}, + {"label": "Gas production", "value": "WGPT"}, + {"label": "Water production", "value": "WWPT"}, + ], + value="WOPT", + multi=False, + clearable=False, + ), + wcc.Dropdown( + label="Only Production after date", + id=self.register_component_unique_id( + WellOverviewSettings.Ids.ONLY_PRODUCTION_AFTER_DATE + ), + options=[ + { + "label": dte.strftime("%Y-%m-%d"), + "value": dte.strftime("%Y-%m-%d"), + } + for dte in self._sorted_dates + ], + multi=False, + ), + html.Div( + children=[ + html.Div( + id={"id": settings_id, "charttype": ChartType.BAR}, + children=wcc.Checklist( + id={"id": checklist_id, "charttype": ChartType.BAR}, + label="Layout options", + options=[ + {"label": "Show legend", "value": "legend"}, + {"label": "Overlay bars", "value": "overlay_bars"}, + { + "label": "Show prod as text", + "value": "show_prod_text", + }, + { + "label": "White background", + "value": "white_background", + }, + ], + value=["legend"], + ), + ), + html.Div( + id={"id": settings_id, "charttype": ChartType.PIE}, + children=wcc.Checklist( + id={"id": checklist_id, "charttype": ChartType.PIE}, + label="Layout options", + options=[ + {"label": "Show legend", "value": "legend"}, + { + "label": "Show prod as text", + "value": "show_prod_text", + }, + ], + value=[], + ), + ), + html.Div( + id={"id": settings_id, "charttype": ChartType.AREA}, + children=wcc.Checklist( + id={"id": checklist_id, "charttype": ChartType.AREA}, + label="Layout options", + options=[ + {"label": "Show legend", "value": "legend"}, + { + "label": "White background", + "value": "white_background", + }, + ], + value=["legend"], + ), + ), + ], + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output( + { + "id": self.component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE_SETTINGS + ).to_string(), + "charttype": ALL, + }, + "style", + ), + Input( + self.component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE + ).to_string(), + "value", + ), + State( + { + "id": self.component_unique_id( + WellOverviewSettings.Ids.CHARTTYPE_SETTINGS + ).to_string(), + "charttype": ALL, + }, + "id", + ), + ) + @callback_typecheck + def _display_charttype_settings( + chart_selected: ChartType, charttype_settings_ids: list + ) -> list: + """Display only the settings relevant for the currently selected chart type.""" + return [ + {"display": "block"} + if settings_id["charttype"] == chart_selected + else {"display": "none"} + for settings_id in charttype_settings_ids + ] + + +class WellOverviewFilters(SettingsGroupABC): + class Ids(StrEnum): + SELECTED_WELLS = "selected-wells" + + def __init__(self, data_models: Dict[str, EnsembleWellAnalysisData]) -> None: + super().__init__("Filters") + self._wells: List[str] = [] + for ens_data_model in data_models.values(): + self._wells.extend( + [well for well in ens_data_model.wells if well not in self._wells] + ) + + def layout(self) -> List[Component]: + return [ + wcc.SelectWithLabel( + label="Well", + size=min(10, len(self._wells)), + id=self.register_component_unique_id( + WellOverviewFilters.Ids.SELECTED_WELLS + ), + options=[{"label": well, "value": well} for well in self._wells], + value=self._wells, + multi=True, + ) + ] + + +class WellOverviewView(ViewABC): + class Ids(StrEnum): + SETTINGS = "settings" + FILTERS = "filters" + CURRENT_FIGURE = "current-figure" + VIEW_ELEMENT = "view-element" + + def __init__( + self, + data_models: Dict[str, EnsembleWellAnalysisData], + theme: WebvizConfigTheme, + ) -> None: + super().__init__("Well overview") + + self._data_models = data_models + self._theme = theme + + self.add_settings_group( + WellOverviewSettings(self._data_models), self.Ids.SETTINGS + ) + self.add_settings_group( + WellOverviewFilters(self._data_models), self.Ids.FILTERS + ) + main_column = self.add_column() + main_column.add_view_element(WellOverviewViewElement(), self.Ids.VIEW_ELEMENT) + + def set_callbacks(self) -> None: + @callback( + Output( + self.view_element(self.Ids.VIEW_ELEMENT) + .component_unique_id(WellOverviewViewElement.Ids.GRAPH) + .to_string(), + "figure", + ), + Input( + self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.ENSEMBLES) + .to_string(), + "value", + ), + Input( + { + "id": self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.CHARTTYPE_CHECKLIST) + .to_string(), + "charttype": ALL, + }, + "value", + ), + Input( + self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.RESPONSE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SETTINGS) + .component_unique_id( + WellOverviewSettings.Ids.ONLY_PRODUCTION_AFTER_DATE + ) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.CHARTTYPE) + .to_string(), + "value", + ), + Input( + self.settings_group(self.Ids.FILTERS) + .component_unique_id(WellOverviewFilters.Ids.SELECTED_WELLS) + .to_string(), + "value", + ), + State( + { + "id": self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.CHARTTYPE_CHECKLIST) + .to_string(), + "charttype": ALL, + }, + "id", + ), + State( + self.view_element(self.Ids.VIEW_ELEMENT) + .component_unique_id(WellOverviewViewElement.Ids.GRAPH) + .to_string(), + "figure", + ), + ) + @callback_typecheck + def _update_graph( + ensembles: List[str], + checklist_values: List[List[str]], + sumvec: str, + prod_after_date: Union[str, None], + charttype_selected: ChartType, + wells_selected: List[str], + checklist_ids: List[Dict[str, str]], + current_fig_dict: Optional[Dict], + ) -> Component: + # pylint: disable=too-many-locals + # pylint: disable=too-many-arguments + """Updates the well overview graph with selected input (f.ex chart type)""" + ctx = callback_context.triggered[0]["prop_id"].split(".")[0] + + settings = { + checklist_id["charttype"]: checklist_values[i] + for i, checklist_id in enumerate(checklist_ids) + } + + # If the event is a plot settings event, then we only update the formatting + # and not the figure data + if ( + current_fig_dict is not None + and self.settings_group(self.Ids.SETTINGS) + .component_unique_id(WellOverviewSettings.Ids.CHARTTYPE_CHECKLIST) + .to_string() + in ctx + ): + fig_dict = format_well_overview_figure( + go.Figure(current_fig_dict), + charttype_selected, + settings[charttype_selected], + sumvec, + prod_after_date, + ) + else: + figure = WellOverviewFigure( + ensembles, + self._data_models, + sumvec, + datetime.datetime.strptime(prod_after_date, "%Y-%m-%d") + if prod_after_date is not None + else None, + charttype_selected, + wells_selected, + self._theme, + ) + + fig_dict = format_well_overview_figure( + figure.figure, + charttype_selected, + settings[charttype_selected], + sumvec, + prod_after_date, + ) + + return fig_dict diff --git a/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view_element.py b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view_element.py new file mode 100644 index 000000000..bccf42ed1 --- /dev/null +++ b/webviz_subsurface/plugins/_well_analysis/_views/_well_overview_view/_view_element.py @@ -0,0 +1,20 @@ +import webviz_core_components as wcc +from dash import html +from webviz_config.utils import StrEnum +from webviz_config.webviz_plugin_subclasses import ViewElementABC + + +class WellOverviewViewElement(ViewElementABC): + class Ids(StrEnum): + GRAPH = "graph" + + def __init__(self) -> None: + super().__init__() + + def inner_layout(self) -> html.Div: + return html.Div( + children=wcc.Graph( + id=self.register_component_unique_id(WellOverviewViewElement.Ids.GRAPH), + style={"height": "87vh"}, + ), + )