diff --git a/CHANGELOG.md b/CHANGELOG.md index b3038cd10..19c6c4f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] - YYYY-MM-DD ### Added +- [#825](https://github.com/equinor/webviz-subsurface/pull/825) - Added options to create separate tornado's for e.g Region/Zone in `VolumetricAnalysis`. As well as various improvements to the tornado figure. - [#734](https://github.com/equinor/webviz-subsurface/pull/645) - New plugin, SeismicMisfit, for comparing observed and modelled seismic attributes. Multiple views, including misfit quantification and coverage plots. - [#809](https://github.com/equinor/webviz-subsurface/pull/809) - `GroupTree` - added more statistical options (P10, P90, P50/Median, Max, Min). Some improvements to the menu layout and behaviour diff --git a/tests/integration_tests/plugin_tests/test_volumetric_analysis.py b/tests/integration_tests/plugin_tests/test_volumetric_analysis.py index f06dd90cc..abaccc4f7 100644 --- a/tests/integration_tests/plugin_tests/test_volumetric_analysis.py +++ b/tests/integration_tests/plugin_tests/test_volumetric_analysis.py @@ -6,7 +6,6 @@ def test_volumetrics_no_sens(dash_duo, app, shared_settings) -> None: plugin = VolumetricAnalysis( - app, shared_settings["HM_SETTINGS"], ensembles=shared_settings["HM_ENSEMBLES"], volfiles={"geogrid": "geogrid--vol.csv", "simgrid": "simgrid--vol.csv"}, @@ -24,7 +23,6 @@ def test_volumetrics_no_sens(dash_duo, app, shared_settings) -> None: def test_volumetrics_sens(dash_duo, app, shared_settings) -> None: plugin = VolumetricAnalysis( - app, shared_settings["SENS_SETTINGS"], ensembles=shared_settings["SENS_ENSEMBLES"], volfiles={"geogrid": "geogrid--vol.csv", "simgrid": "simgrid--vol.csv"}, diff --git a/webviz_subsurface/_components/tornado/_tornado_bar_chart.py b/webviz_subsurface/_components/tornado/_tornado_bar_chart.py index 4cebe18d8..2855d2290 100644 --- a/webviz_subsurface/_components/tornado/_tornado_bar_chart.py +++ b/webviz_subsurface/_components/tornado/_tornado_bar_chart.py @@ -4,7 +4,6 @@ import plotly.graph_objects as go from webviz_subsurface._abbreviations.number_formatting import si_prefixed -from webviz_subsurface._utils.formatting import printable_int_list from ._tornado_data import TornadoData @@ -12,7 +11,7 @@ class TornadoBarChart: """Creates a plotly bar figure from a TornadoData instance""" - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, tornado_data: TornadoData, @@ -25,6 +24,9 @@ def __init__( spaced: bool = True, use_true_base: bool = False, show_realization_points: bool = True, + show_reference: bool = True, + color_by_sensitivity: bool = False, + sensitivity_color_map: dict = None, ) -> None: self._tornadotable = tornado_data.tornadotable self._realtable = self.make_points(tornado_data.real_df) @@ -37,6 +39,7 @@ def __init__( self._locked_si_prefix_relative: Optional[int] self._scale = tornado_data.scale self._use_true_base = use_true_base + self._show_reference = show_reference if self._scale == "Percentage": self._unit_x = "%" self._locked_si_prefix_relative = 0 @@ -46,6 +49,15 @@ def __init__( self._figure_height = figure_height self._label_options = label_options self._show_scatter = show_realization_points + self._color_by_sens = color_by_sensitivity + self._sens_color_map = sensitivity_color_map + + def create_color_list(self, sensitivities: list) -> list: + return ( + [self._sens_color_map.get(sensname, "grey") for sensname in sensitivities] + if self._sens_color_map is not None + else self._plotly_theme["layout"]["colorway"] + ) def make_points(self, realdf: pd.DataFrame) -> pd.DataFrame: dfs = [] @@ -105,21 +117,30 @@ def bar_labels(self, case: str) -> List: return [ f"{self._set_si_prefix_relative(x)}, " f"True: {self._set_si_prefix(val)}, " - f"
Case: {label}, " - f"Reals: {printable_int_list(reals)}" - if reals - else None - for x, label, val, reals in zip( + f"
Case: {label} " + for x, label, val in zip( self._tornadotable[f"{case}_tooltip"], self._tornadotable[f"{case}_label"], self._tornadotable[f"true_{case}"], - self._tornadotable[f"{case}_reals"], ) ] return [] + def hover_label(self) -> List: + return [ + f"Sensname: {sens}:
" + f"Low: {self._set_si_prefix_relative(low)}, " + f"High: {self._set_si_prefix_relative(high)}, " + for low, high, sens in zip( + self._tornadotable["low"], + self._tornadotable["high"], + self._tornadotable["sensname"], + ) + ] + @property def data(self) -> List: + colors = self.create_color_list(self._tornadotable["sensname"].unique()) return [ dict( type="bar", @@ -133,9 +154,13 @@ def data(self) -> List: text=self.bar_labels("low"), textposition="auto", insidetextanchor="middle", - hoverinfo="none", + hoverinfo="text", + hovertext=self.hover_label(), orientation="h", - marker={"line": {"width": 1.5, "color": "black"}}, + marker={ + "line": {"width": 1.5, "color": "black"}, + "color": colors if self._color_by_sens else None, + }, ), dict( type="bar", @@ -149,9 +174,13 @@ def data(self) -> List: text=self.bar_labels("high"), textposition="auto", insidetextanchor="middle", - hoverinfo="none", + hoverinfo="text", + hovertext=self.hover_label(), orientation="h", - marker={"line": {"width": 1.5, "color": "black"}}, + marker={ + "line": {"width": 1.5, "color": "black"}, + "color": colors if self._color_by_sens else None, + }, ), ] @@ -173,7 +202,8 @@ def scatter_data(self) -> List[Dict]: "y": df["sensname"], "x": self.calculate_scatter_value(df["VALUE"]), "text": df["REAL"], - "hoverinfo": "none", + "hovertemplate": "REAL: %{text}
" + + "X: %{x:.1f} ", "marker": { "size": 15, "color": self._plotly_theme["layout"]["colorway"][0] @@ -208,7 +238,9 @@ def layout(self) -> Dict: "title": self._scale, "range": self.range, "autorange": self._show_scatter or self._tornadotable.empty, - "showgrid": False, + "gridwidth": 1, + "gridcolor": "whitesmoke", + "showgrid": True, "zeroline": False, "linecolor": "black", "showline": True, @@ -227,7 +259,8 @@ def layout(self) -> Dict: "tickfont": {"size": 15}, }, "showlegend": False, - "hovermode": "y", + "hovermode": "closest", + "hoverlabel": {"bgcolor": "white", "font_size": 16}, "annotations": [ { "x": 0 if not self._use_true_base else self._reference_average, @@ -239,7 +272,9 @@ def layout(self) -> Dict: "showarrow": False, "align": "center", } - ], + ] + if self._show_reference + else None, "shapes": [ { "type": "line", @@ -258,9 +293,7 @@ def layout(self) -> Dict: @property def figure(self) -> go.Figure: - data = self.data - - fig = go.Figure({"data": data, "layout": self.layout}) + fig = go.Figure({"data": self.data, "layout": self.layout}) if self._show_scatter: fig.update_traces(marker_opacity=0.4, text=None) for trace in self.scatter_data: diff --git a/webviz_subsurface/_components/tornado/_tornado_data.py b/webviz_subsurface/_components/tornado/_tornado_data.py index e82b70183..6c798bc1e 100644 --- a/webviz_subsurface/_components/tornado/_tornado_data.py +++ b/webviz_subsurface/_components/tornado/_tornado_data.py @@ -210,6 +210,7 @@ def _cut_sensitivities_by_ref(self) -> None: self._tornadotable = self._tornadotable.loc[ ((self._tornadotable["low"] - self._tornadotable["high"]) != 0) + | (self._tornadotable["sensname"] == self._reference) ] def _sort_sensitivities_by_max(self) -> None: diff --git a/webviz_subsurface/_components/tornado/tornado_widget.py b/webviz_subsurface/_components/tornado/tornado_widget.py index 477ab850c..0cfb0ee07 100644 --- a/webviz_subsurface/_components/tornado/tornado_widget.py +++ b/webviz_subsurface/_components/tornado/tornado_widget.py @@ -249,6 +249,10 @@ def settings_layout(self) -> html.Div: "label": "Show realization points", "value": "Show realization points", }, + { + "label": "Color bars by sensitivity", + "value": "Color bars by sensitivity", + }, ], value=[], labelStyle={"display": "block"}, @@ -439,6 +443,7 @@ def _calc_tornado( locked_si_prefix=data.get("locked_si_prefix", None), use_true_base=scale == "True value", show_realization_points="Show realization points" in plot_options, + color_by_sensitivity="Color bars by sensitivity" in plot_options, ) tornado_table = TornadoTable(tornado_data=tornado_data) return ( @@ -455,15 +460,20 @@ def _calc_tornado( [ Input(self.ids("tornado-graph"), "clickData"), Input(self.ids("reset"), "n_clicks"), + State(self.ids("high-low-storage"), "data"), ], ) - def _save_click_data(data: dict, nclicks: Optional[int]) -> str: - if callback_context.triggered is None: + def _save_click_data( + data: dict, nclicks: Optional[int], sens_reals: dict + ) -> str: + if ( + callback_context.triggered is None + or sens_reals is None + or data is None + ): raise PreventUpdate - ctx = callback_context.triggered[0]["prop_id"].split(".")[0] if ctx == self.ids("reset") and nclicks: - return json.dumps( { "real_low": [], @@ -471,21 +481,13 @@ def _save_click_data(data: dict, nclicks: Optional[int]) -> str: "sens_name": None, } ) - try: - - real_low = next( - x["customdata"] for x in data["points"] if x["curveNumber"] == 0 - ) - real_high = next( - x["customdata"] for x in data["points"] if x["curveNumber"] == 1 - ) - sens_name = data["points"][0]["y"] - return json.dumps( - { - "real_low": real_low, - "real_high": real_high, - "sens_name": sens_name, - } - ) - except TypeError as exc: - raise PreventUpdate from exc + sensname = data["points"][0]["y"] + real_high = sens_reals[sensname]["real_high"] + real_low = sens_reals[sensname]["real_low"] + return json.dumps( + { + "real_low": real_low, + "real_high": real_high, + "sens_name": sensname, + } + ) diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/__init__.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/__init__.py index 028298428..dcbd3edb6 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/__init__.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/__init__.py @@ -4,3 +4,4 @@ from .fipfile_qc_controller import fipfile_qc_controller from .layout_controllers import layout_controllers from .selections_controllers import selections_controllers +from .tornado_controllers import tornado_controllers diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/comparison_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/comparison_controllers.py index 86966821e..a7df87c38 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/comparison_controllers.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/comparison_controllers.py @@ -4,7 +4,7 @@ import pandas as pd import plotly.express as px import plotly.graph_objects as go -from dash import Dash, Input, Output, State, callback_context, dash_table, html +from dash import Input, Output, State, callback, callback_context, dash_table, html from dash.exceptions import PreventUpdate from webviz_subsurface._figures import create_figure @@ -24,11 +24,10 @@ # pylint: disable=too-many-locals def comparison_controllers( - app: Dash, get_uuid: Callable, volumemodel: InplaceVolumesModel, ) -> None: - @app.callback( + @callback( Output({"id": get_uuid("main-src-comp"), "wrapper": "table"}, "children"), Input(get_uuid("selections"), "data"), Input({"id": get_uuid("main-src-comp"), "element": "display-option"}, "value"), @@ -56,7 +55,7 @@ def _update_page_src_comp( display_option=display_option, ) - @app.callback( + @callback( Output({"id": get_uuid("main-ens-comp"), "wrapper": "table"}, "children"), Input(get_uuid("selections"), "data"), Input({"id": get_uuid("main-ens-comp"), "element": "display-option"}, "value"), diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/distribution_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/distribution_controllers.py index 3ad3dfae0..cc135c539 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/distribution_controllers.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/distribution_controllers.py @@ -4,18 +4,14 @@ import pandas as pd import plotly.express as px import plotly.graph_objects as go -from dash import ALL, Dash, Input, Output, State, html, no_update +from dash import ALL, Input, Output, State, callback, html, no_update from dash.exceptions import PreventUpdate from pandas.api.types import is_numeric_dtype -from webviz_config import WebvizConfigTheme from webviz_subsurface._abbreviations.volume_terminology import ( volume_description, volume_unit, ) -from webviz_subsurface._components.tornado._tornado_bar_chart import TornadoBarChart -from webviz_subsurface._components.tornado._tornado_data import TornadoData -from webviz_subsurface._components.tornado._tornado_table import TornadoTable from webviz_subsurface._figures import create_figure from webviz_subsurface._models import InplaceVolumesModel @@ -29,12 +25,9 @@ # pylint: disable=too-many-statements, too-many-branches def distribution_controllers( - app: Dash, - get_uuid: Callable, - volumemodel: InplaceVolumesModel, - theme: WebvizConfigTheme, + get_uuid: Callable, volumemodel: InplaceVolumesModel ) -> None: - @app.callback( + @callback( Output( {"id": get_uuid("main-voldist"), "element": "graph", "page": ALL}, "figure" ), @@ -171,7 +164,7 @@ def _update_page_1p1t_and_custom( ) ) - @app.callback( + @callback( Output( {"id": get_uuid("main-table"), "wrapper": "table", "page": "table"}, "children", @@ -213,7 +206,7 @@ def _update_page_tables( selections=selections, ) - @app.callback( + @callback( Output( { "id": get_uuid("main-voldist"), @@ -291,7 +284,7 @@ def _update_page_per_zr( output_figs.append(figs[fig_id["selector"]][fig_id["chart"]]) return output_figs - @app.callback( + @callback( Output( {"id": get_uuid("main-voldist"), "element": "plot", "page": "conv"}, "figure", @@ -362,125 +355,6 @@ def _update_page_conv(selections: dict, page_selected: str) -> go.Figure: figure.update_yaxes(dict(matches=None)) return figure - @app.callback( - Output( - { - "id": get_uuid("main-tornado"), - "element": "bulktornado", - "page": "tornado", - }, - "figure", - ), - Output( - { - "id": get_uuid("main-tornado"), - "element": "inplacetornado", - "page": "tornado", - }, - "figure", - ), - Output( - { - "id": get_uuid("main-tornado"), - "wrapper": "table", - "page": "tornado", - }, - "children", - ), - Input(get_uuid("selections"), "data"), - State(get_uuid("page-selected"), "data"), - ) - def _update_page_tornado(selections: dict, page_selected: str) -> go.Figure: - - if page_selected != "tornado": - raise PreventUpdate - - selections = selections[page_selected] - if not selections["update"]: - raise PreventUpdate - - filters = selections["filters"].copy() - - figures = [] - tables = [] - for plot_id in ["left", "right"]: - response = selections[f"Response {plot_id}"] - sensfilter = selections[f"Sensitivities {plot_id}"] - - if selections["Reference"] not in sensfilter: - sensfilter.append(selections["Reference"]) - - filters.update(SENSNAME=sensfilter) - - groups = ["REAL", "ENSEMBLE", "SENSNAME", "SENSCASE", "SENSTYPE"] - df_for_tornado = volumemodel.get_df(filters=filters, groups=groups) - df_for_tornado.rename(columns={response: "VALUE"}, inplace=True) - - tornado_data = TornadoData( - dframe=df_for_tornado, - reference=selections["Reference"], - response_name=response, - scale=selections["Scale"], - cutbyref=bool(selections["Remove no impact"]), - ) - figure = TornadoBarChart( - tornado_data=tornado_data, - plotly_theme=theme.plotly_theme, - label_options=selections["labeloptions"], - number_format="#.3g", - use_true_base=selections["Scale"] == "True", - show_realization_points=bool(selections["real_scatter"]), - ).figure - - figure.update_xaxes( - gridwidth=1, - gridcolor="whitesmoke", - showgrid=True, - side="bottom", - title=None, - ).update_layout( - title=dict( - text=f"Tornadoplot for {response}
" - + f"Fluid zone: {(' + ').join(selections['filters']['FLUID_ZONE'])}", - font=dict(size=18), - ), - margin={"t": 70}, - hovermode="closest", - ).update_traces( - hovertemplate="REAL: %{text}", - selector={"type": "scatter"}, - ) - - figures.append(figure) - - tornado_table = TornadoTable(tornado_data=tornado_data) - table_data = tornado_table.as_plotly_table - for data in table_data: - data["Reference"] = tornado_data.reference_average - tables.append(table_data) - - return ( - figures[0], - figures[1], - html.Div( - children=[ - html.Div( - style={"margin-top": "20px"}, - children=create_data_table( - columns=tornado_table.columns - + create_table_columns( - columns=["Reference"], use_si_format=["Reference"] - ), - data=table, - height="20vh", - table_id={"table_id": f"{page_selected}-table{idx}"}, - ), - ) - for idx, table in enumerate(tables) - ] - ), - ) - # pylint: disable=too-many-locals def make_table_wrapper_children( diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/export_data_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/export_data_controllers.py index 8bbaa32e7..61e0851b8 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/export_data_controllers.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/export_data_controllers.py @@ -1,12 +1,12 @@ from typing import Callable import pandas as pd -from dash import ALL, Dash, Input, Output, State, callback_context, dcc +from dash import ALL, Input, Output, State, callback, callback_context, dcc from dash.exceptions import PreventUpdate -def export_data_controllers(app: Dash, get_uuid: Callable) -> None: - @app.callback( +def export_data_controllers(get_uuid: Callable) -> None: + @callback( Output(get_uuid("download-dataframe"), "data"), Input({"request": "table_data", "table_id": ALL}, "data_requested"), State({"table_id": ALL}, "data"), diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/fipfile_qc_controller.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/fipfile_qc_controller.py index 674a5da4c..e0275eb20 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/fipfile_qc_controller.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/fipfile_qc_controller.py @@ -4,18 +4,14 @@ import plotly.express as px import plotly.graph_objects as go import webviz_core_components as wcc -from dash import Dash, Input, Output, html +from dash import Input, Output, callback, html from dash.exceptions import PreventUpdate from ..utils.table_and_figure_utils import create_data_table, create_table_columns -def fipfile_qc_controller( - app: Dash, - get_uuid: Callable, - disjoint_set_df: pd.DataFrame, -) -> None: - @app.callback( +def fipfile_qc_controller(get_uuid: Callable, disjoint_set_df: pd.DataFrame) -> None: + @callback( Output(get_uuid("main-fipqc"), "children"), Input(get_uuid("selections"), "data"), Input(get_uuid("page-selected"), "data"), diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/layout_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/layout_controllers.py index 3c35937ba..250d25ac2 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/layout_controllers.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/layout_controllers.py @@ -1,56 +1,97 @@ -from typing import Callable, Tuple +from typing import Callable -from dash import ALL, Dash, Input, Output, State, callback_context, no_update +from dash import ALL, Input, Output, State, callback, callback_context, no_update +from dash.exceptions import PreventUpdate +from ..utils.utils import update_relevant_components -def layout_controllers(app: Dash, get_uuid: Callable) -> None: - @app.callback( - Output({"id": get_uuid("selections"), "button": ALL}, "style"), + +def layout_controllers(get_uuid: Callable) -> None: + @callback( Output(get_uuid("page-selected"), "data"), - Output({"id": get_uuid("main-voldist"), "page": ALL}, "style"), Output(get_uuid("voldist-page-selected"), "data"), Input({"id": get_uuid("selections"), "button": ALL}, "n_clicks"), Input(get_uuid("tabs"), "value"), State({"id": get_uuid("selections"), "button": ALL}, "id"), - State({"id": get_uuid("main-voldist"), "page": ALL}, "id"), State(get_uuid("voldist-page-selected"), "data"), ) def _selected_page_controllers( _apply_click: int, tab_selected: str, - button_ids: dict, - main_layout_ids: list, - prev_voldist_page: str, - ) -> Tuple[list, str, list, str]: + button_ids: list, + previous_page: dict, + ) -> tuple: ctx = callback_context.triggered[0] + initial_pages = {"voldist": "1p1t", "tornado": "torn_multi"} - if ( - tab_selected != "voldist" - or "tabs" in ctx["prop_id"] - and prev_voldist_page is not None - ): - return ( - [no_update] * len(button_ids), - tab_selected if tab_selected != "voldist" else prev_voldist_page, - [no_update] * len(main_layout_ids), - no_update, + # handle initial callback + if ctx["prop_id"] == ".": + page_selected = ( + tab_selected + if not tab_selected in initial_pages + else initial_pages[tab_selected] ) + previous_page = initial_pages - button_styles = [] - for button_id in button_ids: - if button_id["button"] in ctx["prop_id"]: - button_styles.append({"background-color": "#7393B3", "color": "#fff"}) - page_selected = button_id["button"] - else: - button_styles.append({"background-color": "#E8E8E8"}) - if ( - ctx["prop_id"] == "." - or "tabs" in ctx["prop_id"] - and prev_voldist_page is None - ): - page_selected = button_ids[0]["button"] - button_styles[0] = {"background-color": "#7393B3", "color": "#fff"} + elif "tabs" in ctx["prop_id"]: + page_selected = ( + tab_selected + if not tab_selected in initial_pages + else previous_page[tab_selected] + ) + previous_page = no_update + + else: + for button_id in button_ids: + if button_id["button"] in ctx["prop_id"]: + page_selected = previous_page[tab_selected] = button_id["button"] + + return page_selected, previous_page + + @callback( + Output({"id": get_uuid("selections"), "button": ALL}, "style"), + Input(get_uuid("page-selected"), "data"), + State(get_uuid("tabs"), "value"), + State({"id": get_uuid("selections"), "button": ALL}, "id"), + ) + def _update_button_style( + page_selected: str, tab_selected: str, button_ids: list + ) -> list: + + if tab_selected not in ["voldist", "tornado"]: + raise PreventUpdate + + button_styles = { + button["button"]: {"background-color": "#E8E8E8"} for button in button_ids + } + button_styles[page_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("main-voldist"), "page": ALL}, "style"), + Input(get_uuid("page-selected"), "data"), + State({"id": get_uuid("main-voldist"), "page": ALL}, "id"), + State(get_uuid("tabs"), "value"), + ) + def _main_voldist_display( + page_selected: str, + main_layout_ids: list, + tab_selected: str, + ) -> list: + + if tab_selected != "voldist": + raise PreventUpdate voldist_layout = [] for page_id in main_layout_ids: @@ -58,10 +99,32 @@ def _selected_page_controllers( voldist_layout.append({"display": "block"}) else: voldist_layout.append({"display": "none"}) + return voldist_layout - return button_styles, page_selected, voldist_layout, page_selected + @callback( + Output({"id": get_uuid("main-tornado"), "page": ALL}, "style"), + Input(get_uuid("page-selected"), "data"), + State({"id": get_uuid("main-tornado"), "page": ALL}, "id"), + State(get_uuid("tabs"), "value"), + ) + def _main_tornado_display( + page_selected: str, + main_layout_ids: list, + tab_selected: str, + ) -> list: + + if tab_selected != "tornado": + raise PreventUpdate + + main_layout = [] + for page_id in main_layout_ids: + if page_id["page"] == page_selected: + main_layout.append({"display": "block"}) + else: + main_layout.append({"display": "none"}) + return main_layout - @app.callback( + @callback( Output( { "id": get_uuid("main-voldist"), diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/selections_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/selections_controllers.py index 0173f475c..e172e194f 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/controllers/selections_controllers.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/selections_controllers.py @@ -1,19 +1,20 @@ -from typing import Any, Callable, Optional +from typing import Any, Callable, Dict, Optional import webviz_core_components as wcc -from dash import ALL, Dash, Input, Output, State, callback_context, no_update +from dash import ALL, Input, Output, State, callback, callback_context, no_update from dash.exceptions import PreventUpdate from webviz_subsurface._models import InplaceVolumesModel +from webviz_subsurface._utils.formatting import printable_int_list -from ..utils.utils import create_range_string, update_relevant_components +from ..utils.utils import update_relevant_components # pylint: disable=too-many-statements,too-many-arguments def selections_controllers( - app: Dash, get_uuid: Callable, volumemodel: InplaceVolumesModel + get_uuid: Callable, volumemodel: InplaceVolumesModel ) -> None: - @app.callback( + @callback( Output(get_uuid("selections"), "data"), Input({"id": get_uuid("selections"), "tab": ALL, "selector": ALL}, "value"), Input( @@ -21,7 +22,7 @@ def selections_controllers( "value", ), Input( - {"id": get_uuid("selections"), "tab": "voldist", "settings": "Colorscale"}, + {"id": get_uuid("selections"), "tab": ALL, "settings": "Colorscale"}, "colorscale", ), Input(get_uuid("initial-load-info"), "data"), @@ -62,7 +63,7 @@ def _update_selections( if id_value["tab"] == selected_tab } - page_selections.update(Colorscale=colorscale) + page_selections.update(Colorscale=colorscale[0] if colorscale else None) page_selections.update(ctx_clicked=ctx["prop_id"]) # check if a page needs to be updated due to page refresh or @@ -81,7 +82,7 @@ def _update_selections( previous_selection[selected_page] = page_selections return previous_selection - @app.callback( + @callback( Output(get_uuid("initial-load-info"), "data"), Input(get_uuid("page-selected"), "data"), State(get_uuid("initial-load-info"), "data"), @@ -92,7 +93,7 @@ def _store_initial_load_info(page_selected: str, initial_load: dict) -> dict: initial_load[page_selected] = page_selected not in initial_load return initial_load - @app.callback( + @callback( Output( {"id": get_uuid("selections"), "tab": "voldist", "selector": ALL}, "disabled", @@ -115,9 +116,6 @@ def _store_initial_load_info(page_selected: str, initial_load: dict) -> dict: State( {"id": get_uuid("selections"), "tab": "voldist", "selector": ALL}, "value" ), - State( - {"id": get_uuid("selections"), "tab": "voldist", "selector": ALL}, "options" - ), State({"id": get_uuid("selections"), "tab": "voldist", "selector": ALL}, "id"), State(get_uuid("selections"), "data"), State(get_uuid("tabs"), "value"), @@ -128,7 +126,6 @@ def _plot_options( selected_page: str, selected_color_by: list, selector_values: list, - selector_options: list, selector_ids: list, previous_selection: Optional[dict], selected_tab: str, @@ -141,26 +138,16 @@ def _plot_options( ): raise PreventUpdate - initial_page_load = selected_page not in previous_selection - selections: Any = {} - if initial_page_load: - selections = { - id_value["selector"]: options[0]["value"] - if id_value["selector"] in ["Plot type", "X Response"] - else None - for id_value, options in zip(selector_ids, selector_options) + selections: Any = ( + previous_selection.get(selected_page) + if "page-selected" in ctx["prop_id"] and selected_page in previous_selection + else { + id_value["selector"]: values + for id_value, values in zip(selector_ids, selector_values) } - else: - selections = ( - previous_selection.get(selected_page) - if "page-selected" in ctx["prop_id"] - else { - id_value["selector"]: values - for id_value, values in zip(selector_ids, selector_values) - } - ) + ) - selectors_disable_in_pages = { + selectors_disable_in_pages: Dict[str, list] = { "Plot type": ["per_zr", "conv"], "Y Response": ["per_zr", "conv"], "X Response": [], @@ -168,26 +155,22 @@ def _plot_options( "Subplots": ["per_zr", "1p1t"], } - settings = {} + settings: Dict[str, dict] = {} for selector, disable_in_pages in selectors_disable_in_pages.items(): - disable = selected_page in disable_in_pages # type: ignore + disable = selected_page in disable_in_pages or ( + selector == "Y Response" + and selections["Plot type"] in ["distribution", "histogram"] + ) value = None if disable else selections.get(selector) - settings[selector] = { - "disable": disable, - "value": value, - } - - if settings["Plot type"]["value"] in ["distribution", "histogram"]: - settings["Y Response"]["disable"] = True - settings["Y Response"]["value"] = None + settings[selector] = {"disable": disable, "value": value} # update dropdown options based on plot type - if settings["Plot type"]["value"] == "scatter": + if selections["Plot type"] == "scatter": y_elm = x_elm = ( volumemodel.responses + volumemodel.selectors + volumemodel.parameters ) - elif settings["Plot type"]["value"] in ["box", "bar"]: + elif selections["Plot type"] in ["box", "bar"]: y_elm = x_elm = volumemodel.responses + volumemodel.selectors if selections.get("Y Response") is None: settings["Y Response"]["value"] = selected_color_by @@ -223,7 +206,7 @@ def _plot_options( for prop in ["disable", "value", "options"] ) - @app.callback( + @callback( Output( {"id": get_uuid("filters"), "tab": ALL, "selector": ALL, "type": "undef"}, "multi", @@ -249,8 +232,9 @@ def _plot_options( "id", ), ) + # pylint: disable=too-many-locals def _update_filter_options( - _selected_page: str, + selected_page: str, selectors: list, selector_ids: list, selected_tab: str, @@ -304,11 +288,9 @@ def _update_filter_options( # filter tornado on correct fluid based on volume response chosen output["FLUID_ZONE"] = {} - if selected_tab == "tornado" and page_selections["mode"] == "locked": + if selected_page == "torn_bulk_inplace": output["FLUID_ZONE"] = { - "values": [ - "oil" if page_selections["Response right"] == "STOIIP" else "gas" - ] + "values": ["oil" if page_selections["Response"] == "STOIIP" else "gas"] } return ( @@ -334,7 +316,7 @@ def _update_filter_options( ), ) - @app.callback( + @callback( Output( {"id": get_uuid("filters"), "tab": ALL, "selector": ALL, "type": "REAL"}, "value", @@ -367,9 +349,6 @@ def _update_realization_filter_and_text( if reals_ids[index]["component_type"] == "range": real_list = list(range(real_list[0], real_list[1] + 1)) - text = f"{real_list[0]}-{real_list[-1]}" - else: - text = create_range_string(real_list) return ( update_relevant_components( @@ -380,11 +359,16 @@ def _update_realization_filter_and_text( ), update_relevant_components( id_list=real_string_ids, - update_info=[{"new_value": text, "conditions": {"tab": selected_tab}}], + update_info=[ + { + "new_value": printable_int_list(real_list), + "conditions": {"tab": selected_tab}, + } + ], ), ) - @app.callback( + @callback( Output( { "id": get_uuid("filters"), @@ -474,61 +458,7 @@ def _update_realization_selected_info( update_info=[{"new_value": component, "conditions": {"tab": selected_tab}}], ) - @app.callback( - Output( - {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "options" - ), - Output( - {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "value" - ), - Output( - {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, - "disabled", - ), - Input( - {"id": get_uuid("selections"), "selector": "mode", "tab": "tornado"}, - "value", - ), - State({"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "id"), - ) - def _update_tornado_selections_from_mode(mode: str, selector_ids: list) -> tuple: - settings = {} - if mode == "custom": - responses = [x for x in volumemodel.responses if x not in ["BO", "BG"]] - settings["Response left"] = settings["Response right"] = { - "options": [{"label": i, "value": i} for i in responses], - "disabled": False, - } - else: - volume_options = [ - x for x in ["STOIIP", "GIIP"] if x in volumemodel.responses - ] - settings["Response left"] = { - "options": [{"label": "BULK", "value": "BULK"}], - "value": "BULK", - "disabled": True, - } - settings["Response right"] = { - "options": [{"label": i, "value": i} for i in volume_options], - "value": volume_options[0], - "disabled": len(volume_options) == 1, - } - - return tuple( - update_relevant_components( - id_list=selector_ids, - update_info=[ - { - "new_value": values.get(prop, no_update), - "conditions": {"selector": selector}, - } - for selector, values in settings.items() - ], - ) - for prop in ["options", "value", "disabled"] - ) - - @app.callback( + @callback( Output( {"id": get_uuid("filters"), "tab": ALL, "selector": ALL, "type": "region"}, "value", @@ -624,7 +554,7 @@ def update_region_filters( ), ) - @app.callback( + @callback( Output( {"id": get_uuid("selections"), "tab": "src-comp", "selector": "Ignore <"}, "value", @@ -638,7 +568,7 @@ def _reset_ignore_value_source_comparison(_response_change: str) -> float: """reset ignore value when new response is selected""" return 0 - @app.callback( + @callback( Output( {"id": get_uuid("selections"), "tab": "ens-comp", "selector": "Ignore <"}, "value", diff --git a/webviz_subsurface/plugins/_volumetric_analysis/controllers/tornado_controllers.py b/webviz_subsurface/plugins/_volumetric_analysis/controllers/tornado_controllers.py new file mode 100644 index 000000000..f88ce62d6 --- /dev/null +++ b/webviz_subsurface/plugins/_volumetric_analysis/controllers/tornado_controllers.py @@ -0,0 +1,269 @@ +from typing import Any, Callable, List, Optional, Tuple + +import pandas as pd +import plotly.graph_objects as go +from dash import ALL, Input, Output, State, callback, callback_context, no_update +from dash.exceptions import PreventUpdate +from webviz_config import WebvizConfigTheme + +from webviz_subsurface._components.tornado._tornado_bar_chart import TornadoBarChart +from webviz_subsurface._components.tornado._tornado_data import TornadoData +from webviz_subsurface._components.tornado._tornado_table import TornadoTable +from webviz_subsurface._models import InplaceVolumesModel + +from ..utils.table_and_figure_utils import create_data_table, create_table_columns +from ..utils.utils import update_relevant_components +from ..views.tornado_view import tornado_plots_layout + + +# pylint: disable=too-many-locals +def tornado_controllers( + get_uuid: Callable, volumemodel: InplaceVolumesModel, theme: WebvizConfigTheme +) -> None: + @callback( + Output({"id": get_uuid("main-tornado"), "page": ALL}, "children"), + Input(get_uuid("selections"), "data"), + State(get_uuid("page-selected"), "data"), + State({"id": get_uuid("main-tornado"), "page": ALL}, "id"), + ) + def _update_tornado_pages( + selections: dict, page_selected: str, id_list: list + ) -> go.Figure: + + if page_selected not in ["torn_multi", "torn_bulk_inplace"]: + raise PreventUpdate + + selections = selections[page_selected] + if not selections["update"]: + raise PreventUpdate + + subplots = selections["Subplots"] is not None + groups = ["REAL", "ENSEMBLE", "SENSNAME", "SENSCASE", "SENSTYPE"] + if subplots and selections["Subplots"] not in groups: + groups.append(selections["Subplots"]) + + filters = selections["filters"].copy() + + figures = [] + tables = [] + responses = ( + ["BULK", selections["Response"]] + if page_selected == "torn_bulk_inplace" + else [selections["Response"]] + ) + for response in responses: + if not (response == "BULK" and page_selected == "torn_bulk_inplace"): + if selections["Reference"] not in selections["Sensitivities"]: + selections["Sensitivities"].append(selections["Reference"]) + filters.update(SENSNAME=selections["Sensitivities"]) + + dframe = volumemodel.get_df(filters=filters, groups=groups) + dframe.rename(columns={response: "VALUE"}, inplace=True) + + df_groups = ( + dframe.groupby(selections["Subplots"]) if subplots else [(None, dframe)] + ) + + for group, df in df_groups: + figure, table, columns = tornado_figure_and_table( + df=df, + response=response, + selections=selections, + theme=theme, + sensitivity_colors=sens_colors(), + font_size=max((20 - (0.4 * len(df_groups))), 10), + group=group, + ) + figures.append(figure) + if selections["tornado_table"]: + tables.append(table) + + if selections["Shared axis"] and selections["Scale"] != "True": + x_absmax = max([max(abs(trace.x)) for fig in figures for trace in fig.data]) + for fig in figures: + fig.update_layout(xaxis_range=[-x_absmax, x_absmax]) + + return update_relevant_components( + id_list=id_list, + update_info=[ + { + "new_value": tornado_plots_layout( + figures=figures, + table=create_data_table( + columns=columns, + selectors=[selections["Subplots"]] if subplots else [], + data=[x for table in tables for x in table], + height="42vh", + table_id={"table_id": f"{page_selected}-torntable"}, + ), + ), + "conditions": {"page": page_selected}, + } + ], + ) + + @callback( + Output( + {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "options" + ), + Output( + {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "value" + ), + Output( + {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, + "disabled", + ), + Input(get_uuid("page-selected"), "data"), + State(get_uuid("tabs"), "value"), + State({"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "id"), + State( + {"id": get_uuid("selections"), "selector": ALL, "tab": "tornado"}, "value" + ), + State(get_uuid("selections"), "data"), + ) + def _update_tornado_selections( + selected_page: str, + selected_tab: str, + selector_ids: list, + selector_values: list, + previous_selection: Optional[dict], + ) -> tuple: + if selected_tab != "tornado" or previous_selection is None: + raise PreventUpdate + + ctx = callback_context.triggered[0] + initial_page_load = selected_page not in previous_selection + + selections: Any = ( + previous_selection.get(selected_page) + if "page-selected" in ctx["prop_id"] and selected_page in previous_selection + else { + id_value["selector"]: values + for id_value, values in zip(selector_ids, selector_values) + } + ) + + settings = {} + if selected_page == "torn_bulk_inplace": + volume_options = [ + x for x in ["STOIIP", "GIIP"] if x in volumemodel.responses + ] + settings["Response"] = { + "options": [{"label": i, "value": i} for i in volume_options], + "value": volume_options[0] + if initial_page_load + else selections["Response"], + "disabled": len(volume_options) == 1, + } + else: + responses = [x for x in volumemodel.responses if x not in ["BO", "BG"]] + settings["Response"] = { + "options": [{"label": i, "value": i} for i in responses], + "disabled": False, + "value": selections["Response"], + } + + settings["tornado_table"] = {"value": selections["tornado_table"]} + + disable_subplots = selected_page != "torn_multi" + settings["Subplots"] = { + "disabled": disable_subplots, + "value": None if disable_subplots else selections["Subplots"], + } + + return tuple( + update_relevant_components( + id_list=selector_ids, + update_info=[ + { + "new_value": values.get(prop, no_update), + "conditions": {"selector": selector}, + } + for selector, values in settings.items() + ], + ) + for prop in ["options", "value", "disabled"] + ) + + def sens_colors() -> dict: + colors = [ + "#FF1243", + "#243746", + "#007079", + "#80B7BC", + "#919BA2", + "#BE8091", + "#B2D4D7", + "#FF597B", + "#BDC3C7", + "#D8B2BD", + "#FFE7D6", + "#D5EAF4", + "#FF88A1", + ] + sensitivities = volumemodel.dataframe["SENSNAME"].unique() + return dict(zip(sensitivities, colors * 10)) + + +def tornado_figure_and_table( + df: pd.DataFrame, + response: str, + selections: dict, + theme: WebvizConfigTheme, + sensitivity_colors: dict, + font_size: float, + group: Optional[str] = None, +) -> Tuple[go.Figure, List[dict], List[dict]]: + + tornado_data = TornadoData( + dframe=df, + reference=selections["Reference"], + response_name=response, + scale=selections["Scale"], + cutbyref=bool(selections["Remove no impact"]), + ) + figure = TornadoBarChart( + tornado_data=tornado_data, + plotly_theme=theme.plotly_theme, + label_options=selections["labeloptions"], + number_format="#.3g", + use_true_base=selections["Scale"] == "True", + show_realization_points=bool(selections["real_scatter"]), + show_reference=not ( + selections["Subplots"] is not None and selections["tornado_table"] + ), + color_by_sensitivity=selections["color_by_sens"], + sensitivity_color_map=sensitivity_colors, + ).figure + + figure.update_xaxes(side="bottom", title=None).update_layout( + title_text=f"Tornadoplot for {response}
" + + f"Fluid zone: {(' + ').join(selections['filters']['FLUID_ZONE'])}" + if group is None + else f"{response} {group}", + title_font_size=font_size, + margin={"t": 70}, + ) + + table_data, columns = create_tornado_table( + tornado_data, subplots=selections["Subplots"], group=group + ) + return figure, table_data, columns + + +def create_tornado_table( + tornado_data: TornadoData, subplots: str, group: Optional[str] +) -> Tuple[List[dict], List[dict]]: + tornado_table = TornadoTable(tornado_data=tornado_data) + table_data = tornado_table.as_plotly_table + for data in table_data: + data["Reference"] = tornado_data.reference_average + if group is not None: + data[subplots] = group + + columns = create_table_columns(columns=[subplots]) if subplots is not None else [] + columns.extend(tornado_table.columns) + columns.extend( + create_table_columns(columns=["Reference"], use_si_format=["Reference"]) + ) + return table_data, columns diff --git a/webviz_subsurface/plugins/_volumetric_analysis/utils/table_and_figure_utils.py b/webviz_subsurface/plugins/_volumetric_analysis/utils/table_and_figure_utils.py index 4f406845e..2d28c69d3 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/utils/table_and_figure_utils.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/utils/table_and_figure_utils.py @@ -1,3 +1,4 @@ +import math from typing import List, Optional, Union import plotly.graph_objects as go @@ -133,3 +134,15 @@ def add_correlation_line(figure: go.Figure, xy_min: float, xy_max: float) -> go. y1=xy_max, line=dict(color="black", width=2, dash="dash"), ) + + +def create_figure_matrix(figures: List[go.Figure]) -> List[List[go.Figure]]: + """Convert a list of figures into a matrix for display""" + figs_in_row = min( + min([x for x in range(100) if (x * (x + 1)) > len(figures)]), + 20, + ) + len_of_matrix = figs_in_row * math.ceil(len(figures) / figs_in_row) + # extend figure list with None to fit size of matrix + figures.extend([None] * (len_of_matrix - len(figures))) + return [figures[i : i + figs_in_row] for i in range(0, len_of_matrix, figs_in_row)] diff --git a/webviz_subsurface/plugins/_volumetric_analysis/utils/utils.py b/webviz_subsurface/plugins/_volumetric_analysis/utils/utils.py index 3cfc6efa8..2e9d4aa65 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/utils/utils.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/utils/utils.py @@ -3,21 +3,6 @@ import dash -def create_range_string(real_list: list) -> str: - idx = 0 - ranges = [[real_list[0], real_list[0]]] - for real in list(real_list): - if ranges[idx][1] in (real, real - 1): - ranges[idx][1] = real - else: - ranges.append([real, real]) - idx += 1 - - return ", ".join( - map(lambda p: f"{p[0]}-{p[1]}" if p[0] != p[1] else str(p[0]), ranges) - ) - - def update_relevant_components(id_list: list, update_info: List[dict]) -> list: output_id_list = [dash.no_update] * len(id_list) for elm in update_info: diff --git a/webviz_subsurface/plugins/_volumetric_analysis/views/main_view.py b/webviz_subsurface/plugins/_volumetric_analysis/views/main_view.py index df1a4cd09..ae68864bf 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/views/main_view.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/views/main_view.py @@ -16,8 +16,7 @@ fipfile_qc_selections, ) from .selections_view import selections_layout, table_selections_layout -from .tornado_layout import tornado_main_layout -from .tornado_selections_view import tornado_selections_layout +from .tornado_view import tornado_main_layout, tornado_selections_layout def main_view( diff --git a/webviz_subsurface/plugins/_volumetric_analysis/views/selections_view.py b/webviz_subsurface/plugins/_volumetric_analysis/views/selections_view.py index 34d97449c..f9387d2b3 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/views/selections_view.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/views/selections_view.py @@ -96,10 +96,7 @@ def table_selections_layout( }, options=[{"label": i, "value": i} for i in responses], value=responses, - size=min( - 20, - len(responses), - ), + size=min(20, len(responses)), ), ], ) @@ -122,19 +119,19 @@ def plot_selector_dropdowns( ]: if selector == "Plot type": elements = ["histogram", "scatter", "distribution", "box", "bar"] - value = elements[0] if not volumemodel.sensrun else "box" + value = elements[0] if selector == "X Response": elements = volumemodel.responses - value = elements[0] if not volumemodel.sensrun else "SENSNAME" + value = elements[0] if selector == "Y Response": elements = volumemodel.responses - value = None if not volumemodel.sensrun else elements[0] + value = None if selector == "Subplots": elements = [x for x in volumemodel.selectors if x != "REAL"] value = None if selector == "Color by": elements = volumemodel.selectors - value = "ENSEMBLE" if not volumemodel.sensrun else "SENSCASE" + value = "ENSEMBLE" dropdowns.append( wcc.Dropdown( @@ -143,8 +140,7 @@ def plot_selector_dropdowns( options=[{"label": elm, "value": elm} for elm in elements], value=value, clearable=selector in ["Subplots", "Color by", "Y Response"], - disabled=selector == "Subplots" - or (selector == "Y Response" and not volumemodel.sensrun), + disabled=selector in ["Subplots", "Y Response"], ) ) return dropdowns diff --git a/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_layout.py b/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_layout.py deleted file mode 100644 index cec33c810..000000000 --- a/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_layout.py +++ /dev/null @@ -1,61 +0,0 @@ -import webviz_core_components as wcc -from dash import html - - -def tornado_main_layout(uuid: str) -> html.Div: - return html.Div( - id={"id": uuid, "page": "tornado"}, - style={"display": "block"}, - children=tornado_plots_layout(uuid), - ) - - -def tornado_plots_layout(uuid: str) -> html.Div: - return html.Div( - children=[ - wcc.Frame( - color="white", - highlight=False, - style={"height": "44vh"}, - children=[ - wcc.FlexBox( - children=[ - html.Div( - style={"flex": 1}, - children=wcc.Graph( - id={ - "id": uuid, - "element": "bulktornado", - "page": "tornado", - }, - config={"displayModeBar": False}, - style={"height": "42vh"}, - ), - ), - html.Div( - style={"flex": 1}, - children=wcc.Graph( - id={ - "id": uuid, - "element": "inplacetornado", - "page": "tornado", - }, - config={"displayModeBar": False}, - style={"height": "42vh"}, - ), - ), - ] - ) - ], - ), - wcc.Frame( - color="white", - highlight=False, - style={"height": "44vh"}, - children=html.Div( - id={"id": uuid, "wrapper": "table", "page": "tornado"}, - style={"display": "block"}, - ), - ), - ] - ) diff --git a/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_selections_view.py b/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_selections_view.py deleted file mode 100644 index 03e158289..000000000 --- a/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_selections_view.py +++ /dev/null @@ -1,154 +0,0 @@ -import webviz_core_components as wcc -from dash import html - -from webviz_subsurface._models import InplaceVolumesModel - - -def tornado_selections_layout( - uuid: str, volumemodel: InplaceVolumesModel, tab: str -) -> html.Div: - """Layout for selecting tornado data""" - return html.Div( - children=[ - tornado_controls_layout(uuid, tab, volumemodel), - settings_layout(uuid, tab, volumemodel), - ] - ) - - -def tornado_controls_layout( - uuid: str, tab: str, volumemodel: InplaceVolumesModel -) -> wcc.Selectors: - mode_options = [{"label": "Custom", "value": "custom"}] - if "BULK" in volumemodel.responses: - mode_options.append({"label": "Bulk vs STOIIP/GIIP", "value": "locked"}) - return wcc.Selectors( - label="TORNADO CONTROLS", - children=[ - wcc.RadioItems( - label="Mode:", - id={"id": uuid, "tab": tab, "selector": "mode"}, - options=mode_options, - value="custom", - ), - tornado_selection( - position="left", uuid=uuid, tab=tab, volumemodel=volumemodel - ), - tornado_selection( - position="right", uuid=uuid, tab=tab, volumemodel=volumemodel - ), - ], - ) - - -def tornado_selection( - position: str, uuid: str, tab: str, volumemodel: InplaceVolumesModel -) -> html.Div: - return html.Div( - style={"margin-top": "10px"}, - children=[ - html.Label( - children=f"Tornado ({position})", - style={"display": "block"}, - className="webviz-underlined-label", - ), - wcc.Dropdown( - id={"id": uuid, "tab": tab, "selector": f"Response {position}"}, - clearable=False, - value=volumemodel.responses[0], - ), - html.Details( - open=False, - children=[ - html.Summary("Sensitivity filter"), - wcc.Select( - id={ - "id": uuid, - "tab": tab, - "selector": f"Sensitivities {position}", - }, - options=[ - {"label": i, "value": i} for i in volumemodel.sensitivities - ], - value=volumemodel.sensitivities, - size=min(15, len(volumemodel.sensitivities)), - ), - ], - ), - ], - ) - - -def settings_layout( - uuid: str, tab: str, volumemodel: InplaceVolumesModel -) -> wcc.Selectors: - return wcc.Selectors( - label="⚙️ SETTINGS", - open_details=True, - children=[ - wcc.Dropdown( - label="Scale:", - id={"id": uuid, "tab": tab, "selector": "Scale"}, - options=[ - {"label": "Relative value (%)", "value": "Percentage"}, - {"label": "Relative value", "value": "Absolute"}, - {"label": "True value", "value": "True"}, - ], - value="Percentage", - clearable=False, - ), - html.Div( - style={"margin-top": "10px"}, - children=wcc.Checklist( - id={"id": uuid, "tab": tab, "selector": "real_scatter"}, - options=[{"label": "Show realization points", "value": "Show"}], - value=[], - ), - ), - cut_by_ref(uuid, tab), - labels_display(uuid, tab), - wcc.Dropdown( - label="Reference:", - id={"id": uuid, "tab": tab, "selector": "Reference"}, - options=[ - {"label": elm, "value": elm} for elm in volumemodel.sensitivities - ], - value="rms_seed" - if "rms_seed" in volumemodel.sensitivities - else volumemodel.sensitivities[0], - clearable=False, - ), - ], - ) - - -def cut_by_ref(uuid: str, tab: str) -> html.Div: - return html.Div( - style={"margin-bottom": "10px"}, - children=wcc.Checklist( - id={"id": uuid, "tab": tab, "selector": "Remove no impact"}, - options=[ - {"label": "Remove sensitivities with no impact", "value": "Remove"} - ], - value=["Remove"], - ), - ) - - -def labels_display(uuid: str, tab: str) -> html.Div: - return html.Div( - style={"margin-bottom": "10px"}, - children=[ - wcc.RadioItems( - label="Label options:", - id={"id": uuid, "tab": tab, "selector": "labeloptions"}, - options=[ - {"label": "detailed", "value": "detailed"}, - {"label": "simple", "value": "simple"}, - {"label": "hide", "value": "hide"}, - ], - vertical=False, - value="detailed", - ), - ], - ) diff --git a/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_view.py b/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_view.py new file mode 100644 index 000000000..a6b344f2e --- /dev/null +++ b/webviz_subsurface/plugins/_volumetric_analysis/views/tornado_view.py @@ -0,0 +1,218 @@ +import webviz_core_components as wcc +from dash import html + +from webviz_subsurface._models import InplaceVolumesModel + +from ..utils.table_and_figure_utils import create_figure_matrix + + +def tornado_main_layout(uuid: str) -> html.Div: + return html.Div( + children=[ + html.Div( + id={"id": uuid, "page": "torn_multi"}, + style={"display": "block"}, + ), + html.Div( + id={"id": uuid, "page": "torn_bulk_inplace"}, + style={"display": "none"}, + ), + ] + ) + + +def tornado_plots_layout(figures: list, table: list) -> html.Div: + matrix = create_figure_matrix(figures) + max_height = 42 if table else 88 + + return html.Div( + children=[ + wcc.Frame( + color="white", + highlight=False, + style={"height": "44vh" if table else "91vh"}, + children=[ + wcc.FlexBox( + children=[ + html.Div( + style={"flex": 1}, + children=wcc.Graph( + config={"displayModeBar": False}, + style={"height": f"{max_height/len(matrix)}vh"}, + figure=fig, + ) + if fig is not None + else [], + ) + for fig in row + ] + ) + for row in matrix + ], + ), + wcc.Frame( + color="white", + highlight=False, + style={"height": "44vh", "display": "block" if table else "none"}, + children=html.Div(style={"margin-top": "20px"}, children=table), + ), + ] + ) + + +def tornado_selections_layout( + uuid: str, volumemodel: InplaceVolumesModel, tab: str +) -> html.Div: + """Layout for selecting tornado data""" + return html.Div( + children=[ + page_buttons(uuid, volumemodel), + wcc.Selectors( + label="TORNADO CONTROLS", + children=tornado_controls_layout(uuid, tab, volumemodel), + ), + wcc.Selectors( + label="⚙️ SETTINGS", + open_details=True, + children=[ + scale_selector(uuid, tab), + checkboxes_settings(uuid, tab), + labels_display(uuid, tab), + reference_selector(uuid, tab, volumemodel), + ], + ), + ] + ) + + +def page_buttons(uuid: str, volumemodel: InplaceVolumesModel) -> html.Div: + no_bulk = "BULK" not in volumemodel.responses + return html.Div( + style={"margin-bottom": "20px", "display": "none" if no_bulk else "block"}, + children=[ + button(uuid=uuid, title="Custom", page_id="torn_multi"), + button( + uuid=uuid, + title="Bulk vs STOIIP/GIIP", + page_id="torn_bulk_inplace", + ), + ], + ) + + +def button(uuid: str, title: str, page_id: str) -> html.Button: + return html.Button( + title, + className="webviz-inplace-vol-btn", + id={"id": uuid, "button": page_id}, + ) + + +def tornado_controls_layout( + uuid: str, tab: str, volumemodel: InplaceVolumesModel +) -> wcc.Selectors: + return [ + wcc.Dropdown( + label="Response", + id={"id": uuid, "tab": tab, "selector": "Response"}, + clearable=False, + options=[{"label": i, "value": i} for i in volumemodel.responses], + value=volumemodel.responses[0], + ), + wcc.SelectWithLabel( + label="Sensitivity filter", + collapsible=True, + open_details=False, + id={"id": uuid, "tab": tab, "selector": "Sensitivities"}, + options=[{"label": i, "value": i} for i in volumemodel.sensitivities], + value=volumemodel.sensitivities, + size=min(15, len(volumemodel.sensitivities)), + ), + wcc.Dropdown( + label="Subplots", + id={"id": uuid, "tab": tab, "selector": "Subplots"}, + clearable=True, + options=[ + {"label": i, "value": i} + for i in [ + x + for x in volumemodel.selectors + if x not in ["REAL", "SENSNAME", "SENSCASE", "SENSTYPE"] + ] + ], + ), + html.Div( + style={"margin-top": "10px"}, + children=wcc.Checklist( + id={"id": uuid, "tab": tab, "selector": "tornado_table"}, + options=[{"label": "Show tornado table", "value": "selected"}], + value=["selected"], + ), + ), + ] + + +def checkboxes_settings(uuid: str, tab: str) -> html.Div: + return html.Div( + style={"margin-top": "10px", "margin-bottom": "10px"}, + children=[ + wcc.Checklist( + id={"id": uuid, "tab": tab, "selector": selector}, + options=[{"label": label, "value": "selected"}], + value=["selected"] if selected else [], + ) + for label, selector, selected in [ + ("Color by sensitivity", "color_by_sens", True), + ("Shared subplot X axis", "Shared axis", False), + ("Show realization points", "real_scatter", False), + ("Remove sensitivities with no impact", "Remove no impact", True), + ] + ], + ) + + +def labels_display(uuid: str, tab: str) -> html.Div: + return html.Div( + style={"margin-bottom": "10px"}, + children=[ + wcc.RadioItems( + label="Label options:", + id={"id": uuid, "tab": tab, "selector": "labeloptions"}, + options=[ + {"label": "detailed", "value": "detailed"}, + {"label": "simple", "value": "simple"}, + {"label": "hide", "value": "hide"}, + ], + vertical=False, + value="simple", + ), + ], + ) + + +def reference_selector( + uuid: str, tab: str, volumemodel: InplaceVolumesModel +) -> wcc.Dropdown: + return wcc.Dropdown( + label="Reference:", + id={"id": uuid, "tab": tab, "selector": "Reference"}, + options=[{"label": elm, "value": elm} for elm in volumemodel.sensitivities], + value="rms_seed" + if "rms_seed" in volumemodel.sensitivities + else volumemodel.sensitivities[0], + clearable=False, + ) + + +def scale_selector(uuid: str, tab: str) -> wcc.Dropdown: + return wcc.Dropdown( + label="Scale:", + id={"id": uuid, "tab": tab, "selector": "Scale"}, + options=[ + {"label": "Relative value (%)", "value": "Percentage"}, + {"label": "Relative value", "value": "Absolute"}, + {"label": "True value", "value": "True"}, + ], + value="Percentage", + clearable=False, + ) diff --git a/webviz_subsurface/plugins/_volumetric_analysis/volumetric_analysis.py b/webviz_subsurface/plugins/_volumetric_analysis/volumetric_analysis.py index b25e8ad92..1820afba0 100644 --- a/webviz_subsurface/plugins/_volumetric_analysis/volumetric_analysis.py +++ b/webviz_subsurface/plugins/_volumetric_analysis/volumetric_analysis.py @@ -2,7 +2,7 @@ from typing import Callable, List, Optional, Tuple import pandas as pd -from dash import Dash, html +from dash import html from webviz_config import WebvizPluginABC, WebvizSettings from webviz_config.common_cache import CACHE from webviz_config.webviz_assets import WEBVIZ_ASSETS @@ -23,6 +23,7 @@ fipfile_qc_controller, layout_controllers, selections_controllers, + tornado_controllers, ) from .views import clientside_stores, main_view from .volume_validator_and_combinator import VolumeValidatorAndCombinator @@ -137,7 +138,6 @@ class VolumetricAnalysis(WebvizPluginABC): # pylint: disable=too-many-arguments def __init__( self, - app: Dash, webviz_settings: WebvizSettings, csvfile_vol: Path = None, csvfile_parameters: Path = None, @@ -195,7 +195,7 @@ def __init__( volume_type=vcomb.volume_type, ) self.theme = webviz_settings.theme - self.set_callbacks(app) + self.set_callbacks() @property def layout(self) -> html.Div: @@ -211,17 +211,16 @@ def layout(self) -> html.Div: ], ) - def set_callbacks(self, app: Dash) -> None: - selections_controllers(app=app, get_uuid=self.uuid, volumemodel=self.volmodel) - distribution_controllers( - app=app, get_uuid=self.uuid, volumemodel=self.volmodel, theme=self.theme - ) - comparison_controllers(app=app, get_uuid=self.uuid, volumemodel=self.volmodel) - layout_controllers(app=app, get_uuid=self.uuid) - export_data_controllers(app=app, get_uuid=self.uuid) - fipfile_qc_controller( - app=app, get_uuid=self.uuid, disjoint_set_df=self.disjoint_set_df + def set_callbacks(self) -> None: + selections_controllers(get_uuid=self.uuid, volumemodel=self.volmodel) + distribution_controllers(get_uuid=self.uuid, volumemodel=self.volmodel) + tornado_controllers( + get_uuid=self.uuid, volumemodel=self.volmodel, theme=self.theme ) + comparison_controllers(get_uuid=self.uuid, volumemodel=self.volmodel) + layout_controllers(get_uuid=self.uuid) + export_data_controllers(get_uuid=self.uuid) + fipfile_qc_controller(get_uuid=self.uuid, disjoint_set_df=self.disjoint_set_df) def add_webvizstore(self) -> List[Tuple[Callable, list]]: store_functions = []