diff --git a/crystal_toolkit/components/pourbaix.py b/crystal_toolkit/components/pourbaix.py index 89e63b26..84e3e743 100644 --- a/crystal_toolkit/components/pourbaix.py +++ b/crystal_toolkit/components/pourbaix.py @@ -42,6 +42,7 @@ MAX_PH = 16 MIN_V = -4 MAX_V = 4 +PANEL_LINE_HEIGHT = "2em" class PourbaixDiagramComponent(MPComponent): @@ -127,6 +128,16 @@ class PourbaixDiagramComponent(MPComponent): } ) + @staticmethod + def create_centered_object(content): + return html.Div( + content, + style={ + "display": "flex", + "justifyContent": "center", + }, + ) + @staticmethod def get_figure( pourbaix_diagram: PourbaixDiagram, heatmap_entry=None, show_water_lines=True @@ -440,11 +451,10 @@ def _sub_layouts(self) -> dict[str, Component]: default=self.default_state["filter_solids"], label="Filter Solids", help_str="Whether to filter solid phases by stability on the compositional phase diagram. " - "The practical consequence of this is that highly oxidized or reduced phases that " - "might show up in experiments due to kinetic limitations on oxygen/hydrogen evolution " - "won't appear in the diagram, but they are not actually “stable” (and are frequently " - "overstabilized from DFT errors). Hence, including only the stable solid phases generally " - "leads to the most accurate Pourbaix diagrams.", + "The practical consequence of this is that we only include materials that are predicted to " + "be thermodynamically stable at RT within the limitations of DFT. Notably, there may be " + "disagreements with experiments e.g., highly oxidized or reduced phases, which are kinetically " + "stabilized through surface passivation.", ), html.Div( [ @@ -454,23 +464,56 @@ def _sub_layouts(self) -> dict[str, Component]: id=self.id("invalid-comp-alarm"), message="Illegal composition entry!", ), - html.H5( - "Composition", - id=self.id("composition-title"), - style={"fontWeight": "bold"}, + html.Div( + [ + html.H5( + "Composition Control", + style={ + "fontWeight": "bold", + "textAlign": "center", + "flex": "0 0 100%", + }, + ), + html.H5( + "Composition of", + id=self.id("composition-title"), + # style={"fontWeight": "bold"}, + ), + ], + style={ + "line-height": PANEL_LINE_HEIGHT, + "display": "flex", + "flexWrap": "wrap", + "justifyContent": "center", + }, ), - dcc.Input( - id=self.id("comp-text"), - type="text", - # placeholder="composition e.g. 1:1:1", + PourbaixDiagramComponent.create_centered_object( + dcc.Input( + id=self.id("comp-text"), + className="input", + type="text", + style={ + "textAlign": "center", + "width": "10rem", + "marginRight": "0.2rem", + "marginBottom": "0.2rem", + "height": "36px", + "fontSize": "14px", + }, + ), ), - html.Button( - "Update", - id=self.id("comp-btn"), + ctl.Block( + PourbaixDiagramComponent.create_centered_object( + html.Div( + id=self.id("display-composition"), + ) + ) + ), + html.Hr( + style={ + "backgroundColor": "#C5C5C6", + } ), - ctl.Block(html.Div(id=self.id("display-composition"))), - html.Br(), - html.Br(), dcc.Store(id=self.id("elements-store")), ], id=self.id("comp-panel"), @@ -486,9 +529,22 @@ def _sub_layouts(self) -> dict[str, Component]: id=self.id("conc-panel"), style={"display": "none"}, ), - html.Div(id=self.id("element_specific_controls")), - ] + html.Div( + id=self.id("element_specific_controls"), + ), + PourbaixDiagramComponent.create_centered_object( + html.Button( + "Update", + id=self.id("comp-conc-btn"), + style={"display": "none"}, + ), + ), + ], + style={ + "backgroundColor": "#F1F1F5", + }, ), + html.Br(), self.get_bool_input( "show_heatmap", # kwarg_label # state=self.default_state, @@ -629,6 +685,8 @@ def update_heatmap_choices(entries, mat_detials, filter_solids): Output(self.id("elements-store"), "data"), Output(self.id("comp-text"), "value"), Output(self.id("composition-title"), "children"), + Output(self.id("comp-conc-btn"), "children"), + Output(self.id("comp-conc-btn"), "style"), Input(self.id(), "data"), prevent_initial_call=True, ) @@ -651,57 +709,83 @@ def update_element_specific_sliders( # exclude O and H elements = elements - ELEMENTS_HO - comp_inputs = [] conc_inputs = [] for element in sorted(elements): - conc_input = html.Div( - [ - self.get_numerical_input( - f"conc-{element}", - default=1e-6, - min=MIN_CONCENTRATION, - max=MAX_CONCENTRATION, - label=f"Concentration of {element} ion", - style={"width": "10rem", "fontSize": "14px"}, - ) - ] + conc_input = PourbaixDiagramComponent.create_centered_object( + self.get_numerical_input( + f"conc-{element}", + default=1e-6, + min=MIN_CONCENTRATION, + max=MAX_CONCENTRATION, + label=f"concentration of {element} ion", + style={ + "width": "10rem", + "fontSize": "14px", + }, + ) ) conc_inputs.append(conc_input) comp_conc_controls = [] - comp_conc_controls += comp_inputs ion_label = ( - "Set Ion Concentrations (M)" + "Ion Concentrations Control" if len(elements) > 1 - else "Set Ion Concentration" + else "Ion Concentration Control" + ) + + comp_conc_controls.append( + html.H5( + ion_label, + style={"fontWeight": "bold", "textAlign": "center"}, + ), + ) + comp_conc_controls.append( + PourbaixDiagramComponent.create_centered_object( + html.H6( + f"💡 Set the range between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} (M)" + ) + ) ) - comp_conc_controls.append(ctl.Label(ion_label)) comp_conc_controls += conc_inputs - # + # comp_panel_style comp_panel_style = {"display": "none"} if len(elements) > 1: - comp_panel_style = {"display": "block"} + comp_panel_style = { + "display": "block", + } - # + # elements store elements = [element.symbol for element in elements] - # + # default_comp default_comp = ":".join(["1" for _ in elements]) - # - title = "Composition of " + ":".join(elements) + # composition title + title = "💡 Composition of " + ":".join(elements) + + # update_string + update_string = "Concentration update" + if len(elements) > 1: + update_string = "Composition & concentration update" return ( - html.Div(comp_conc_controls), + html.Div( + comp_conc_controls, + style={ + "line-height": PANEL_LINE_HEIGHT, + }, + ), comp_panel_style, elements, default_comp, title, + update_string, + {"display": "block", "height": "36px"}, ) @cache.memoize(timeout=5 * 60) @@ -715,26 +799,32 @@ def get_pourbaix_diagram(pourbaix_entries, **kwargs): Output(self.id("display-composition"), "children"), Input(self.id(), "data"), Input(self.id("display-composition"), "children"), - Input(self.get_all_kwargs_id(), "value"), - Input(self.id("comp-btn"), "n_clicks"), + State(self.get_all_kwargs_id(), "value"), + Input(self.id("comp-conc-btn"), "n_clicks"), State(self.id("elements-store"), "data"), State(self.id("comp-text"), "value"), Input(self.id("element_specific_controls"), "children"), + Input(self.get_kwarg_id("filter_solids"), "value"), + Input(self.get_kwarg_id("show_heatmap"), "value"), + Input(self.get_kwarg_id("heatmap_choice"), "value"), prevent_initial_call=True, ) def make_figure( pourbaix_entries, - dependency, + display_composition, kwargs, n_clicks, elements, comp_text, - dependency2, + element_specific_controls, + filter_solids, + show_heatmap, + heatmap_choice, ) -> go.Figure: if pourbaix_entries is None: raise PreventUpdate - # check if composition input + # Only update if n_clicks: raw_comp_list = comp_text.split(":") else: @@ -808,6 +898,7 @@ def make_figure( pourbaix_diagram, heatmap_entry=heatmap_entry, ) + return ( self.get_figure_div(figure=figure), False,