diff --git a/webviz_subsurface/containers/_parameter_correlation.py b/webviz_subsurface/containers/_parameter_correlation.py index b8f35d29f..a53f63c04 100644 --- a/webviz_subsurface/containers/_parameter_correlation.py +++ b/webviz_subsurface/containers/_parameter_correlation.py @@ -150,9 +150,9 @@ def control_div(self): "grid-template-columns": "3fr 1fr 4fr", }, children=[ - # pylint: disable=no-member - # ToggleSwitch module attribute in dash-daq is set at runtime html.Label("Show density plot", style={"font-weight": "bold"}), + # ToggleSwitch module attribute in dash-daq is set at runtime + # pylint: disable=no-member, not-callable daq.ToggleSwitch(id=self.density_id, value=True), ], ), diff --git a/webviz_subsurface/containers/_reservoir_simulation_timeseries_onebyone.py b/webviz_subsurface/containers/_reservoir_simulation_timeseries_onebyone.py index 73ece413e..c3c52fa92 100644 --- a/webviz_subsurface/containers/_reservoir_simulation_timeseries_onebyone.py +++ b/webviz_subsurface/containers/_reservoir_simulation_timeseries_onebyone.py @@ -45,10 +45,10 @@ class ReservoirSimulationTimeSeriesOneByOne(WebvizContainerABC): ] TABLE_STAT = [ - "Sens Name", - "Sens Case", + "Sensitivity", + "Case", "Mean", - "Stddev", + "Standard Deviation", "Minimum", "P90", "P10", @@ -102,7 +102,7 @@ def __init__( def make_uuids(self): uuid = f"{uuid4()}" self.smry_col_id = f"smry-col-{uuid}" - self.date_label = f"date-label{uuid}" + self.store_date_id = f"date-store{uuid}" self.ensemble_id = f"ensemble-{uuid}" self.table_id = f"table-{uuid}" self.graph_id = f"graph-{uuid}" @@ -112,10 +112,8 @@ def make_uuids(self): @property def tour_steps(self): return [ - {"id": self.smry_col_id, "content": "Select time series"}, - {"id": self.ensemble_id, "content": "Select ensemble"}, { - "id": self.graph_id, + "id": self.graph_wrapper_id, "content": ( "Selected time series displayed per realization. " "Click in the plot to calculate tornadoplot for the " @@ -132,6 +130,8 @@ def tour_steps(self): "relevant realizations in the main chart." ), }, + {"id": self.smry_col_id, "content": "Select time series"}, + {"id": self.ensemble_id, "content": "Select ensemble"}, ] @property @@ -159,7 +159,7 @@ def smry_selector(self): return html.Div( style={"paddingBottom": "30px"}, children=[ - html.Label("Vector"), + html.Label("Time Series"), dcc.Dropdown( id=self.smry_col_id, options=[{"label": i, "value": i} for i in self.smry_cols], @@ -214,7 +214,7 @@ def layout(self): children=[ self.ensemble_selector, self.smry_selector, - html.Label(id=self.date_label), + dcc.Store(id=self.store_date_id), ], ), html.Div( @@ -234,6 +234,12 @@ def layout(self): {"name": i, "id": i} for i in ReservoirSimulationTimeSeriesOneByOne.TABLE_STAT ], + style_cell_conditional=[ + { + "if": {"column_id": "Standard Deviation"}, + "width": "5%", + } + ], ), ] ), @@ -247,9 +253,14 @@ def set_callbacks(self, app): @app.callback( Output(self.graph_wrapper_id, "children"), [Input(self.ensemble_id, "value"), Input(self.smry_col_id, "value")], + [State(self.graph_id, "figure"), State(self.store_date_id, "children")], ) - def _render_lines(ensemble, vector): - """Callback to update graph, and tornado""" + def _render_lines(ensemble, vector, figure, date): + """Callback to update graph, and tornado + Since it is not possible to use the same Output object in two different + callbacks, a parent div is used to re-render the graph when changing + vector or ensemble + """ # Filter dataframe based on dropdown choices data = filter_ensemble(self.data, ensemble, vector) @@ -265,23 +276,28 @@ def _render_lines(ensemble, vector): for r, df in data.groupby(["REAL"]) ] traces[0]["hoverinfo"] = "x" + + # Check if a data has been clicked previously + # If so, add the vertical line to the figure + if date: + ymin = min([min(trace["y"]) for trace in figure["data"]]) + ymax = max([max(trace["y"]) for trace in figure["data"]]) + date = json.loads(date) + layout = { + "shapes": [ + {"type": "line", "x0": date, "x1": date, "y0": ymin, "y1": ymax} + ], + "showlegend": False, + } + else: + layout = {"showlegend": False} return [ - wcc.Graph( - id=self.graph_id, - figure={ - "data": traces, - "layout": { - "title": "Click on a date to calculate tornado plot. " - + "Click on a bar in tornadoplot to highlight relevant realizations", - "showlegend": False, - }, - }, - ) + wcc.Graph(id=self.graph_id, figure={"data": traces, "layout": layout}) ] @app.callback( [ - Output(self.date_label, "children"), + Output(self.store_date_id, "children"), Output(self.table_id, "data"), Output(self.tornadoplot.storage_id, "children"), ], @@ -291,16 +307,19 @@ def _render_lines(ensemble, vector): Input(self.smry_col_id, "value"), ], ) - def _render_tornado(ensemble, clickdata, vector): + def _render_date(ensemble, clickdata, vector): + """Store selected date and tornado input. Write statistics + to table""" try: date = clickdata["points"][0]["x"] except TypeError: raise PreventUpdate + data = filter_ensemble(self.data, ensemble, vector) data = data.loc[data["DATE"].astype(str) == date] table_rows = calculate_table_rows(data, vector) return ( - f"Selected data {date}", + json.dumps(f"{date}"), table_rows, json.dumps( { @@ -312,12 +331,17 @@ def _render_tornado(ensemble, clickdata, vector): @app.callback( Output(self.graph_id, "figure"), - [Input(self.tornadoplot.click_id, "children")], + [ + Input(self.tornadoplot.click_id, "children"), + Input(self.store_date_id, "children"), + ], [State(self.graph_id, "figure")], ) - def _render_tornado(clickdata, figure): + def _render_tornado(clickdata, date, figure): + """Update graph with line coloring, vertical line and title""" if not clickdata: return figure + clickdata = json.loads(clickdata) for trace in figure["data"]: if trace["customdata"] in clickdata["real_low"]: @@ -329,10 +353,21 @@ def _render_tornado(clickdata, figure): else: trace["marker"] = {"color": "grey"} trace["opacity"] = 0.02 - figure["layout"]["title"] = "" + if date: + ymin = min([min(trace["y"]) for trace in figure["data"]]) + ymax = max([max(trace["y"]) for trace in figure["data"]]) + date = json.loads(date) + figure["layout"]["shapes"] = [ + {"type": "line", "x0": date, "x1": date, "y0": ymin, "y1": ymax} + ] + figure["layout"][ + "title" + ] = f"Date: {date}, sensitivity: {clickdata['sens_name']}" + return figure +@CACHE.memoize(timeout=CACHE.TIMEOUT) def calculate_table_rows(df, vector): table = [] for (sensname, senscase), dframe in df.groupby(["SENSNAME", "SENSCASE"]): @@ -340,12 +375,12 @@ def calculate_table_rows(df, vector): try: table.append( { - "Sens Name": str(sensname), - "Sens Case": str(senscase), + "Sensitivity": str(sensname), + "Case": str(senscase), "Minimum": f"{values.min():.2e}", "Maximum": f"{values.max():.2e}", "Mean": f"{values.mean():.2e}", - "Stddev": f"{values.std():.2e}", + "Standard Deviation": f"{values.std():.2e}", "P10": f"{np.percentile(values, 90):.2e}", "P90": f"{np.percentile(values, 10):.2e}", } diff --git a/webviz_subsurface/private_containers/_tornado_plot.py b/webviz_subsurface/private_containers/_tornado_plot.py index bb4d3111b..f7b62b596 100644 --- a/webviz_subsurface/private_containers/_tornado_plot.py +++ b/webviz_subsurface/private_containers/_tornado_plot.py @@ -163,7 +163,14 @@ def _save_hover_data(data): try: real_low = data["points"][0]["customdata"] real_high = data["points"][1]["customdata"] - return json.dumps({"real_low": real_low, "real_high": real_high}) + sens_name = data["points"][0]["y"] + return json.dumps( + { + "real_low": real_low, + "real_high": real_high, + "sens_name": sens_name, + } + ) except TypeError: raise PreventUpdate @@ -176,7 +183,14 @@ def _save_hover_data(data): try: real_low = data["points"][0]["customdata"] real_high = data["points"][1]["customdata"] - return json.dumps({"real_low": real_low, "real_high": real_high}) + sens_name = data["points"][0]["y"] + return json.dumps( + { + "real_low": real_low, + "real_high": real_high, + "sens_name": sens_name, + } + ) except TypeError: raise PreventUpdate