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 = []