from dash import html, dcc import pandas as pd import numpy as np from data.functions import * from figure import build_figure import plotly.graph_objects as go # ============================================================ # Small reusable layout helpers # ============================================================ FULL_WIDTH_STYLE = {"width": "100%"} FIELD_LABEL_STYLE = {"marginTop": "4px"} def section_header(text): """Standard section heading.""" return html.H3(text, className="section-header") def field_label(text, first=False): """Standard input label.""" if first: return html.Div(text) return html.Div(text, style=FIELD_LABEL_STYLE) def numeric_input( component_id, value, placeholder=None, disabled=False, ): """Standard full-width numeric input.""" if placeholder is None: placeholder = str(value) return dcc.Input( id=component_id, type="number", placeholder=placeholder, value=value, style=FULL_WIDTH_STYLE, disabled=disabled, required=True, ) # ============================================================ # Placeholder figure # ============================================================ def build_placeholder_figure(): n = len(soluteData) df0 = pd.DataFrame( { "CASRN": soluteData["CASRN"].to_numpy(), "Condition 1": np.zeros(n), "Cond1_err_plus": np.zeros(n), "Cond1_err_minus": np.zeros(n), "Condition 2": np.zeros(n), "Cond2_err_plus": np.zeros(n), "Cond2_err_minus": np.zeros(n), } ) placeholder_fig = build_figure(df0) # Keep the CAS axis layout, but hide the placeholder bar traces. placeholder_fig.update_traces( opacity=0, hoverinfo="skip", showlegend=False, ) placeholder_fig.update_yaxes( range=[-2, 0] ) placeholder_fig.add_annotation( text="Click 'Calculate' to run model", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict( size=22, color="gray", ), align="center", ) return placeholder_fig # ============================================================ # Polymer section # ============================================================ def build_polymer_section(): return html.Div( className="polymer-section", children=[ section_header("Polymer component"), html.Div( className="fields-grid", children=[ # Left column html.Div( children=[ field_label("Matrix", first=True), dcc.Dropdown( id="matrix", options=[ {"label": x, "value": x} for x in polymers ], value=polymers[25], clearable=False, style=FULL_WIDTH_STYLE, ), field_label("Tg (°C)"), numeric_input( component_id="tg", value="0.0", placeholder="0.0", ), field_label("Crystallinity (%)"), numeric_input( component_id="crystal", value="0.0", placeholder="0.0", ), ] ), # Right column html.Div( children=[ field_label("Density (g/cm³)", first=True), numeric_input( component_id="density", value="1.0", placeholder="1.0", ), field_label("Volume (cm³)"), numeric_input( component_id="polymer_volume", value="1.0", placeholder="1.0", ), field_label("Surface area (cm²)"), numeric_input( component_id="surface_area", value="30.0", placeholder="30.0", ), ] ), ], ), ] ) # ============================================================ # Solute section # ============================================================ def build_solute_section(): return html.Div( children=[ section_header("Solutes"), dcc.RadioItems( id="solute_source", options=[ { "label": "Default boundary chemicals", "value": "default", }, { "label": "User-defined chemicals", "value": "upload", }, ], value="default", inline=True, labelStyle={"marginRight": "15px"}, ), # Hidden unless "User-defined chemicals" is selected. html.Div( id="solute_upload_container", children=[ dcc.Upload( id="solute_upload", children=html.Div( [ "Drag and drop or ", html.A("select an Excel file"), ] ), accept=".xlsx,.xls", multiple=False, style={ "width": "100%", "height": "55px", "lineHeight": "55px", "borderWidth": "1px", "borderStyle": "dashed", "borderRadius": "4px", "textAlign": "center", "marginTop": "10px", }, ), html.Div( id="solute_upload_status", style={ "marginTop": "5px", "fontSize": "13px", }, ), html.Div( id="solute_calculate_warning", style={ "marginTop": "6px", "fontSize": "13px", "color": "firebrick", "fontWeight": "bold", }, ), ], style={"display": "none"}, ), dcc.Store(id="uploaded_solute_data"), ] ) # ============================================================ # Condition controls # ============================================================ def build_condition_column( number, default_mode, default_solvent, ): """ Build either Condition #1 or Condition #2. IDs are generated automatically: c1_solvent, c1_volume, ... c2_solvent, c2_volume, ... """ prefix = f"c{number}" return html.Div( children=[ html.H3( f"Condition #{number}", className="section-header", ), html.Div( style={"marginBottom": "10px"}, children=[ dcc.RadioItems( id=f"condition{number}_mode", options=[ { "label": "In vitro", "value": "in_vitro", }, { "label": "In vivo (conservative)", "value": "in_vivo_conservative", }, { "label": "In vivo (representative tissue)", "value": "in_vivo_tissue", }, ], value=default_mode, inline=False, labelStyle={ "display": "block", "marginBottom": "2px", }, ), ], ), field_label("Solvent", first=True), dcc.Dropdown( id=f"{prefix}_solvent", options=[ {"label": x, "value": x} for x in solvents ], value=default_solvent, clearable=False, placeholder="", disabled=True, style=FULL_WIDTH_STYLE, ), field_label("Volume (cm³)"), numeric_input( component_id=f"{prefix}_volume", value="10.0", placeholder="10.0", disabled=True, ), field_label("Swelling ratio (m/m)"), numeric_input( component_id=f"{prefix}_swelling", value="1.0", placeholder="1.0", disabled=True, ), field_label("Extraction T (°C)"), numeric_input( component_id=f"{prefix}_temp", value="50.0", placeholder="50.0", disabled=True, ), field_label("Extraction time (h)"), numeric_input( component_id=f"{prefix}_time", value="24.0", placeholder="24.0", disabled=False, ), field_label("# of iterations"), numeric_input( component_id=f"{prefix}_iterations", value="1", placeholder="1", disabled=True, ), ] ) def build_conditions_section(): return html.Div( className="conditions-grid", children=[ build_condition_column( number=1, default_mode="in_vitro", default_solvent=solvents[9], ), build_condition_column( number=2, default_mode="in_vivo_conservative", default_solvent=solvents[2], ), ], ) # ============================================================ # Monte Carlo section # ============================================================ def build_monte_carlo_section(): return html.Div( children=[ section_header("Monte Carlo"), html.Div( className="fields-grid", children=[ html.Div( children=[ html.Div("Samples"), numeric_input( component_id="samples", value="1000", placeholder="1000", ), ] ), html.Div( children=[ html.Div("Comparison quantity"), dcc.RadioItems( id="mc_option", options=[ { "label": "absolute", "value": "absolute", }, { "label": "ratio", "value": "ratio", }, ], value="absolute", inline=True, labelStyle={"marginRight": "15px"}, ), ] ), ], ), html.Div( className="calculate-row", children=[ html.Button( "Calculate", id="calculate", n_clicks=0, ) ], ), ] ) # ============================================================ # Results / graph panel # ============================================================ def build_results_panel(placeholder_fig): return html.Div( className="results-panel", style={ "flex": "1", "minWidth": 0, "boxSizing": "border-box", "border": "1px solid #D5DADF", "borderRadius": "4px", "backgroundColor": "#FFFFFF", "padding": "16px", "paddingTop": "12px", }, children=[ section_header("Results"), dcc.Loading( id="mm0_loading", type="default", color="#D60036", parent_style={ "position": "relative", "width": "100%", "minWidth": 0, }, children=dcc.Graph( id="mm0_chart", figure=placeholder_fig, responsive=True, config={"displayModeBar": False}, style={ "width": "100%", "height": "600px", }, ), ), html.P( [ "This tool provides an interface to " "physics-based models that can assist " "in selecting extraction conditions. ", "It enables comparison of predicted " "fractional release across in vitro " "and in vivo scenarios. ", "For additional information, please " "refer to the ", dcc.Link("quick start guide", href="/instructions"), ".", ], style={ "margin": "1.25rem 0 0", "lineHeight": "1.4", "color": "#555", }, ), ], ) # ============================================================ # Left-side control panel # ============================================================ def build_controls_panel(): return html.Section( className="controls-panel", **{"aria-label": "Model inputs"}, children=[ build_polymer_section(), build_solute_section(), build_conditions_section(), build_monte_carlo_section(), ], ) # ============================================================ # Main layout # ============================================================ def build_main_layout() -> html.Div: placeholder_fig = build_placeholder_figure() return html.Div( className="model-page", children=[ html.H1( "Extraction condition comparison tool", className="visually-hidden", ), html.Div( className="model-grid", children=[ build_controls_panel(), build_results_panel(placeholder_fig), ], ), ], )