From d2428d02d5df134007e7473907c2dca81649aa57 Mon Sep 17 00:00:00 2001 From: Xinyang Li Date: Thu, 2 Oct 2025 15:12:08 -0400 Subject: [PATCH 1/4] Move in-line style to CSS Most of the in-line style has been moved to Bootstrap predefined classes or a customized CSS file in `assets/css/styles.css`. --- app.py | 3 +- assets/css/styles.css | 38 +++++++++++++++++ obsidian/dash/inputs_config.py | 11 ++--- obsidian/dash/inputs_data.py | 10 +++-- obsidian/dash/optimize.py | 2 +- obsidian/dash/predict.py | 14 +++--- obsidian/dash/utils.py | 78 +++++++++++++++++++++------------- 7 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 assets/css/styles.css diff --git a/app.py b/app.py index 74ed3df..3f4d2a3 100755 --- a/app.py +++ b/app.py @@ -20,7 +20,8 @@ app_image = html.Div(html.Img(src=logo, style={'width': '5%', 'height': '5%'}), style={'textAlign': 'center'}) app_title = html.Div([html.H1(children='obsidian'), html.H5(children='Algorithmic Process Optimization and Experiment Design', - style={'font-style': 'italic', 'color': '#AAAAAA'})], + className="fst-italic", + style={'color': '#AAAAAA'})], style={'textAlign': 'center'}) app_infobar = html.Div(id='root-infobar') app_tabs = dbc.Tabs(children=[], id="root-tabs") diff --git a/assets/css/styles.css b/assets/css/styles.css new file mode 100644 index 0000000..a4f5f92 --- /dev/null +++ b/assets/css/styles.css @@ -0,0 +1,38 @@ +:root { + /* global defaults for Obsidian typography */ + --obsd-text-color: var(--bs-primary); + --obsd-form-font-size: 0.7em; + --obsd-form-lg-font-size: 0.8em; + --obsd-form-xl-font-size: 1em; + --obsd-info-color: var(--bs-info, #0dcaf0); +} + +/* Base Class */ +.obsd-form-text { + font-size: var(--obsd-form-font-size); + color: var(--obsd-text-color); +} + +/* Utility Classes */ +.obsd-text-lg { + font-size: var(--obsd-form-lg-font-size); +} + +.obsd-text-xl { + font-size: var(--obsd-form-xl-font-size); +} + +.obsd-text-normal-style { + font-style: var(--obsd-help-font-style); + /* 'normal' */ +} + +.obsd-input-row { + margin-bottom: 1rem; +} + +.obsd-form-label { + font-weight: bold; + color: var(--obsd-text-color); + font-size: var(--obsd-form-lg-font-size); +} \ No newline at end of file diff --git a/obsidian/dash/inputs_config.py b/obsidian/dash/inputs_config.py index 86d499a..01eec4a 100644 --- a/obsidian/dash/inputs_config.py +++ b/obsidian/dash/inputs_config.py @@ -25,7 +25,8 @@ def make_acquisition(index, delete=True): className='me-2', color='danger', n_clicks=0)) columns.append(dbc.Col([dbc.InputGroup(hyper_input), - dbc.FormText('Input a hyperparameter (optional)', style={'font-size': '0.7em'})])) + dbc.FormText('Input a hyperparameter (optional)', + className="obsd-form-text")])) return dbc.Row(id={'type': 'div-acquisition', 'index': index}, children=columns, align='center') @@ -62,19 +63,19 @@ def setup_config(app, app_tabs): acquisitions = dbc.Container(dbc.Card(id='aq_inputs', children=[ dbc.CardHeader(['Acquisition Functions', html.Div(dbc.FormText('Objective function of the experiment\ search/selection', - style={'font-size': '0.7em'}))]), + className="obsd-form-text"))]), dbc.CardBody(html.Div(id='div-acquisition_all', children=[make_acquisition(index=0, delete=False)])), dbc.CardFooter(dbc.Button('Add', id='button-acquisition_add', className='me-2', color='secondary', n_clicks=0)) - ], style={'margin-top': '15px'})) + ], className="mt-3")) general_options = dbc.Container(dbc.Card(dbc.CardBody( children=[input_surrogate, input_m_batch, input_optim_sequential])), - style={'margin-top': '15px'}) + className="mt-3") advanced_options = dbc.Container(make_collapse('adv_options', [input_optimizer_seed, input_f_transform, input_optim_restarts], 'Advanced Options'), - style={'margin-top': '15px'}) + className="mt-3") # Config store config = dcc.Store(id='store-config') diff --git a/obsidian/dash/inputs_data.py b/obsidian/dash/inputs_data.py index bdfa024..aaedd96 100644 --- a/obsidian/dash/inputs_data.py +++ b/obsidian/dash/inputs_data.py @@ -42,7 +42,9 @@ def setup_data(app, app_tabs, default_data, default_Xspace): parameters and response variable(s), with one row per observation. Download\ template data (left) for example.', target='info-data', placement='top', style={'text-transform': 'none'}), - dbc.FormText('Example Data', color='info', style={'font-size': '1em', 'font-style': 'italic'}, + dbc.FormText('Example Data', + color="info", + className="obsd-form-text obsd-text-xl fst-italic", id='table-X0-footer') ], style={'textAlign': 'center'})) @@ -50,7 +52,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace): preview_uploader = dbc.Row([dbc.Col([uploader, template_downloader], width=4), dbc.Col(preview, width=8)], - style={'margin-top': '15px'}) + className="mt-3") # Data store storage_X0 = dcc.Store(id='store-X0', data=default_data.to_dict()) @@ -180,7 +182,7 @@ def update_xspace_types(data, ycol, Xspace_save): ], color='primary', outline=True), width=2)) - return dbc.Container(dbc.Row(cols, style={'margin-top': '15px'}, + return dbc.Container(dbc.Row(cols, className="mt-3", justify='center'), fluid=True) @@ -298,7 +300,7 @@ def preview_cats(current_cats): preview = [] for cat in cat_list: - preview += [html.Div(cat, style={'font-size': '0.8em'})] + preview += [html.Div(cat, className="obsd-form-text obsd-text-lg")] return preview @app.callback( diff --git a/obsidian/dash/optimize.py b/obsidian/dash/optimize.py index 23836de..cf1554f 100644 --- a/obsidian/dash/optimize.py +++ b/obsidian/dash/optimize.py @@ -58,7 +58,7 @@ def setup_optimize(app, app_tabs): dbc.Button('Download Suggested Candidates', id='button-download_candidates', className='me-2', color='primary'), dcc.Download(id='downloader-candidates')], - style={'textAlign': 'center', 'margin-top': '15px'}) + style={'textAlign': 'center'}, className="mt-3") # Add all of these elements to the app columns = dbc.Row([dbc.Col(fit_div, width=6), dbc.Col([predict_div, candidates_downloader], width=6)]) diff --git a/obsidian/dash/predict.py b/obsidian/dash/predict.py index 9d55492..6d3312c 100644 --- a/obsidian/dash/predict.py +++ b/obsidian/dash/predict.py @@ -20,7 +20,7 @@ def setup_predict(app, app_tabs): dbc.Card([ dbc.CardHeader('Parameter Space'), dbc.CardBody([ - html.Div(id='div-xspace_df', children=[], style={'overflow-x': 'scroll'}) + html.Div(id='div-xspace_df', children=[], className="overflow-x-scroll") ]), ]), ], @@ -34,7 +34,7 @@ def setup_predict(app, app_tabs): dbc.Card([ dbc.CardHeader('Example Prediction Input'), dbc.CardBody([ - html.Div(id='div-template', children=[], style={'overflow-x': 'scroll'}) + html.Div(id='div-template', children=[], className="overflow-x-scroll") ]), ]), ], @@ -47,7 +47,7 @@ def setup_predict(app, app_tabs): template_downloader = html.Div(children=[dbc.Button('Download Template Candidates', id='button-download_template', className='me-2', color='primary'), dcc.Download(id='downloader-template')], - style={'textAlign': 'center', 'margin-top': '15px'}) + style={'textAlign': 'center'}, className="mt-3") default_data = pd.DataFrame() @@ -78,15 +78,17 @@ def setup_predict(app, app_tabs): parameters and response variable(s), with one row per observation. Download\ template data (left) for example.', target='info-data_1', placement='top', style={'text-transform': 'none'}), - dbc.FormText('Example Data', color='info', style={'font-size': '1em', 'font-style': 'italic'}, + dbc.FormText('Example Data', + color="info", + className="obsd-form-text obsd-text-xl fst-italic", id='table-X1-footer') ], style={'textAlign': 'center'})) ])) row1 = dbc.Row([dbc.Col([xspace_df_div], width=4), - dbc.Col([template_div, template_downloader], width=8)], style={'margin-top': '15px'}) - row2 = dbc.Row([uploader_1, preview_1], style={'margin-top': '15px'}) + dbc.Col([template_div, template_downloader], width=8)], className="mt-3") + row2 = dbc.Row([uploader_1, preview_1], className="mt-3") # Add all of these elements to the app elements = [html.Br(), store_template, row1, row2, storage_X1] diff --git a/obsidian/dash/utils.py b/obsidian/dash/utils.py index 9bf06be..5d8bea7 100644 --- a/obsidian/dash/utils.py +++ b/obsidian/dash/utils.py @@ -28,57 +28,75 @@ def add_tab(target, elements, id, label): target.children.append(tab) return target.children - -def make_input(property_name, help_text, default_value=None, id=None, kwargs={}, required=True): +def make_input( + property_name, help_text, default_value=None, id=None, kwargs={}, required=True +): components = [ - dbc.Label(property_name, style={'font-weight': 'bold', 'font-size': '0.8em'}), - dbc.Input(value=default_value, id=f'input-[{property_name}]' if id is None else id, - debounce=True, required=required, **kwargs), - html.Div(f'{help_text}', style={'font-size': '0.7em'}), + dbc.Label(property_name, className="obsd-form-label"), + dbc.Input( + value=default_value, + id=f"input-[{property_name}]" if id is None else id, + debounce=True, + required=required, + **kwargs, + ), + html.Div(help_text, className="obsd-help-text"), ] - - return html.Div(components, className='mb-4') + return html.Div(components, className="mb-4 obsd-input-row") def make_dropdown(property_name, help_text, options=[], id=None, kwargs={}): components = [ - dbc.Label(property_name, style={'font-weight': 'bold', 'font-size': '0.8em'}), - dcc.Dropdown(options, id=f'input-[{property_name}]' if id is None else id, clearable=False, **kwargs), - dbc.FormText(f'{help_text}', style={'font-size': '0.7em'}), - ] - - return html.Div(components, className='mb-4') + dbc.Label(property_name, className="obsd-form-label"), + dcc.Dropdown( + options, + id=f"input-[{property_name}]" if id is None else id, + clearable=False, + **kwargs, + ), + dbc.FormText(help_text, className="obsd-help-text"), + ] + return html.Div(components, className="mb-4 obsd-input-row") def make_switch(property_name, help_text, id=None, kwargs={}): components = [ - dbc.Label(property_name, style={'font-weight': 'bold', 'font-size': '0.8em'}), - html.Div(dbc.Switch(id=f'toggle-[{property_name}]' if id is None else id, value=True, **kwargs)), - dbc.FormText(f'{help_text}', style={'font-size': '0.7em'}), + dbc.Label(property_name, className="obsd-form-label"), + html.Div( + dbc.Switch( + id=f"toggle-[{property_name}]" if id is None else id, + value=True, + **kwargs, + ) + ), + dbc.FormText(help_text, className="obsd-help-text"), ] - - return html.Div(components) + return html.Div(components, className="obsd-input-row") def make_slider(property_name, help_text, min, max, id=None, kwargs={}): components = [ - dbc.Label(property_name, style={'font-weight': 'bold', 'font-size': '0.8em'}), - dcc.Slider(min, max, id=f'input-[{property_name}]' if id is None else id, **kwargs), - dbc.FormText(f'{help_text}', style={'font-size': '0.7em'}), + dbc.Label(property_name, className="obsd-form-label"), + dcc.Slider( + min, max, id=f"input-[{property_name}]" if id is None else id, **kwargs + ), + dbc.FormText(help_text, className="obsd-help-text"), ] - - return html.Div(components) + return html.Div(components, className="obsd-input-row") def make_knob(property_name, help_text, min, max, id=None, kwargs={}): components = [ - dbc.Label(property_name, style={'font-weight': 'bold', 'font-size': '0.8em'}), - daq.Knob(min=min, max=max, id=f'input-[{property_name}]' if id is None else id, **kwargs), - dbc.FormText(f'{help_text}', style={'font-size': '0.7em'}), + dbc.Label(property_name, className="obsd-form-label"), + daq.Knob( + min=min, + max=max, + id=f"input-[{property_name}]" if id is None else id, + **kwargs, + ), + dbc.FormText(help_text, className="obsd-help-text"), ] - - return html.Div(components) - + return html.Div(components, className="obsd-input-row") def make_table(df, fill_width=False): table = html.Div([dash_table.DataTable(data=df.to_dict('records'), From 73187af0d9863c656a8d9fe3ba2d4a34327b6023 Mon Sep 17 00:00:00 2001 From: Xinyang Li Date: Mon, 6 Oct 2025 16:02:48 -0400 Subject: [PATCH 2/4] Fix input config update handling --- obsidian/dash/inputs_data.py | 89 +++++++++++++++++++++++++++++------- obsidian/dash/utils.py | 17 +++++++ 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/obsidian/dash/inputs_data.py b/obsidian/dash/inputs_data.py index aaedd96..a32e2fa 100644 --- a/obsidian/dash/inputs_data.py +++ b/obsidian/dash/inputs_data.py @@ -1,7 +1,7 @@ from .utils import add_tab, make_input, make_dropdown, make_switch, make_slider, make_knob, make_table, make_collapse -from .utils import center +from .utils import center, error_message_handling, is_input_empty import dash_bootstrap_components as dbc -from dash import dcc, html, Dash, dash_table, callback, Output, Input, State, ALL, MATCH +from dash import dcc, html, Dash, dash_table, callback, Output, Input, State, ALL, MATCH, no_update import pandas as pd import base64 import io @@ -80,18 +80,43 @@ def setup_data(app, app_tabs, default_data, default_Xspace): kwargs={'value': default_data.columns[-1]})])) ]), width=4), justify='center') + input_container = dbc.Row( + dbc.Col( + dbc.Card( + [ + dbc.CardHeader( + [ + html.I( + id="search-space-block", + className="bi bi-info-circle-fill me-2", + ), + dbc.Tooltip( + "The search space defines the range and types of values for each parameter that the optimizer will explore. These parameter types and ranges are initially inferred from the uploaded data. Please verify they are appropriate for your use case. While you can update the parameter space during optimization, do so only if you understand the implications.", + target="search-space-block", + placement="top", + style={"text-transform": "none"}, + ), + "Search Space Configuration", + ] + ), + dbc.CardBody(xspace), + ] + ), + ), + justify="center", + className="m-3", + ) # Extra div for printing outputs, for troubleshooting troubleshoot = html.Div(id='debug-print-data') # Add all of these elements to the app - elements = [html.Br(), preview_uploader, html.Hr(), ycol, xspace, storage_X0, + elements = [html.Br(), preview_uploader, html.Hr(), ycol, input_container, storage_X0, storage_X0_template, storage_Xspace, html.Hr(), troubleshoot] add_tab(app_tabs, elements, 'tab-data', 'Data') setup_data_callbacks(app) return - def setup_data_callbacks(app): # Save the uploaded data into the data-store @@ -235,25 +260,55 @@ def update_xspace_vals(param_type, param_id, data, ycol): Input({'type': 'input-param_min', 'index': MATCH}, 'value'), Input({'type': 'input-param_max', 'index': MATCH}, 'value'), Input({'type': 'store-param_categories', 'index': MATCH}, 'data'), + State('store-config', 'data'), prevent_initial_call=True # It takes a second for these input matches to show up ) - def update_param_save(param_id, param_type, param_min, param_max, param_cat): - - name = param_id['index'] - + def update_param_save( + param_id, param_type, param_min, param_max, param_cat, config + ): + """ + Build and serialize a single-parameter ParamSpace for this control. + On exception, do not update the store (return no_update) to avoid triggering downstream aggregation. + """ + + name = param_id["index"] + # Placeholder for verbosity setting from config + # It should be configurable in the future + verbosity = (config or {}).get('verbose', 1) try: - if param_type == 'Numeric': + # Explicit conversion/validation + if param_type == "Numeric": + # Allow empty/incomplete during edit: keep previous store value + if not (param_min and param_max): + return no_update + param_min = float(param_min) + param_max = float(param_max) param = Param_Continuous(name, param_min, param_max) - elif param_type == 'Categorical': - param = Param_Categorical(name, param_cat) - elif param_type == 'Ordinal': - param = Param_Ordinal(name, param_cat) - + + elif param_type in ("Categorical", "Ordinal"): + # Expect a comma-separated string or list; accept both + if is_input_empty(param_cat): + return no_update + if param_type == "Categorical": + param = Param_Categorical(name, param_cat) + else: + param = Param_Ordinal(name, param_cat) + + else: + error_message_handling(name, f'unknown param_type "{param_type}"', verbosity) + return no_update + single_param = ParamSpace([param]) return single_param.save_state() - - except TypeError: - return None + + except Exception as e: + error_message_handling( + name, + f"please double check the inputs. Error: {e}", + verbosity, + tb="traceback", + ) + return no_update # Category management for categorical variables @app.callback( diff --git a/obsidian/dash/utils.py b/obsidian/dash/utils.py index 5d8bea7..8faba0e 100644 --- a/obsidian/dash/utils.py +++ b/obsidian/dash/utils.py @@ -1,3 +1,5 @@ +import traceback + import dash_bootstrap_components as dbc from dash import dcc, html, Dash, dash_table, callback, Output, Input, State, ALL, MATCH from dash.dash_table.Format import Format, Scheme @@ -119,3 +121,18 @@ def make_collapse(id, contents, label): dbc.Collapse(contents, id=f'collapse-{id}', is_open=False) ] return dbc.Card(dbc.CardBody(components)) + +def is_input_empty(value): + if isinstance(value, (list, tuple)): + return len(value) == 0 + if isinstance(value, str): + return value.strip() == '' + +def error_message_handling(name, message, verbosity=1, tb=None): + if verbosity >= 1: + print(f'Updating "{name}" failed: {message}') + if tb is not None and verbosity > 1: + if isinstance(tb, str): + print(tb) + else: + traceback.print_exc() \ No newline at end of file From 4c4ef92237041a3bdd1fb1b7db89e7e9011bbda4 Mon Sep 17 00:00:00 2001 From: Xinyang Li Date: Tue, 7 Oct 2025 14:35:04 -0400 Subject: [PATCH 3/4] Migrate inline style to css All occurrences of inline style `style=""`, except for `dash_table.DataTable`, have been migrated to CSS classes (Bootstrap 5 built-in preferred, otherwise defined in `assets/css/styles.css`). --- assets/css/styles.css | 21 +++++++++++++++++++++ obsidian/dash/infobar.py | 11 ++++------- obsidian/dash/inputs_data.py | 19 +++++++------------ obsidian/dash/optimize.py | 15 +++++++-------- obsidian/dash/predict.py | 25 ++++++++++--------------- obsidian/dash/utils.py | 4 ++-- 6 files changed, 51 insertions(+), 44 deletions(-) diff --git a/assets/css/styles.css b/assets/css/styles.css index a4f5f92..47d56fe 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -7,6 +7,14 @@ --obsd-info-color: var(--bs-info, #0dcaf0); } +.obsd-dashed-box { + height: 60px; + line-height: 60px; + border: 1px dashed; + border-radius: 5px; + box-sizing: border-box; +} + /* Base Class */ .obsd-form-text { font-size: var(--obsd-form-font-size); @@ -35,4 +43,17 @@ font-weight: bold; color: var(--obsd-text-color); font-size: var(--obsd-form-lg-font-size); +} + +.text-transform-none { + text-transform: none !important; +} + +.d-inline-block { + display: inline-block; +} + +.overflow-x-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } \ No newline at end of file diff --git a/obsidian/dash/infobar.py b/obsidian/dash/infobar.py index b43f9fd..697e11f 100644 --- a/obsidian/dash/infobar.py +++ b/obsidian/dash/infobar.py @@ -16,12 +16,9 @@ def setup_infobar(app, app_infobar): app_infobar.children = dbc.Container([ html.Br(), dbc.Row([ - dbc.Col(dbc.Button('Help', outline='True', color='warning', className='me-1', id='button-help'), - style={'textAlign': 'left'}, width={'size': 2}), - dbc.Col(html.Div(dbc.Badge(f'v{obsidian.__version__}', color='primary', className='me-1'), style={'textAlign': 'center'}), - width={'size': 2}), - dbc.Col(dbc.Button('Contact', outline='True', color='secondary', className='me-1', id='button-contact'), - style={'textAlign': 'right'}, width={'size': 2}), + dbc.Col(dbc.Button('Help', outline='True', color='warning', className='me-1', id='button-help'), className="text-start", width={'size': 2}), + dbc.Col(html.Div(dbc.Badge(f'v{obsidian.__version__}', color='primary', className='me-1 text-center')), className="text-center", width={'size': 2}), + dbc.Col(dbc.Button('Contact', outline='True', color='secondary', className='me-1', id='button-contact'), className="text-end", width={'size': 2}), ], justify='center'), # Pop-up for Contact Us dbc.Modal([ @@ -31,7 +28,7 @@ def setup_infobar(app, app_infobar): color='primary', className='me-1'), dbc.Button('Visit Our Site', href='https://msdllcpapers.github.io/obsidian/', target='_blank', external_link=True, color='primary', className='me-1') - ], style={'textAlign': 'center'}), + ], className="text-center"), dbc.ModalFooter([]) ], id='modal-contact', is_open=False, size='xl', centered=True), # Pop-up for Help diff --git a/obsidian/dash/inputs_data.py b/obsidian/dash/inputs_data.py index a32e2fa..6664c72 100644 --- a/obsidian/dash/inputs_data.py +++ b/obsidian/dash/inputs_data.py @@ -16,12 +16,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace): uploader = dcc.Upload(id='uploader-X0', children=html.Div(['Upload Data: Drag and Drop or ', html.A('Select Files')]), - style={ - 'width': '100%', 'height': '60px', - 'lineHeight': '60px', 'borderWidth': '1px', - 'borderStyle': 'dashed', 'borderRadius': '5px', - 'textAlign': 'center', 'margin': '10px' - }, + className="w-100 m-2 text-center obsd-dashed-box", multiple=False, filename='Example Data' ) @@ -30,7 +25,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace): template_downloader = html.Div(children=[dbc.Button('Download Template Data', id='button-download_data', className='me-2', color='primary'), dcc.Download(id='downloader-X0_template')], - style={'textAlign': 'center'}) + className="text-center") # Data preview preview = html.Div(id='table-X0', children=dbc.Card( @@ -41,13 +36,13 @@ def setup_data(app, app_tabs, default_data, default_Xspace): dbc.Tooltip('Input data must be a CSV file and must include column headers for the input\ parameters and response variable(s), with one row per observation. Download\ template data (left) for example.', - target='info-data', placement='top', style={'text-transform': 'none'}), + target='info-data', placement='top', className="text-transform-none"), dbc.FormText('Example Data', color="info", className="obsd-form-text obsd-text-xl fst-italic", id='table-X0-footer') ], - style={'textAlign': 'center'})) + className="text-center")) ])) preview_uploader = dbc.Row([dbc.Col([uploader, template_downloader], width=4), @@ -70,7 +65,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace): it is the target of the objective function before\ including optional transformations.', target='info-response_col', placement='top', - style={'text-transform': 'none'}), + className="text-transform-none"), 'Response Selection']), dbc.CardBody(html.Div(id='div-response_name', children=[make_dropdown('Data Column', @@ -94,7 +89,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace): "The search space defines the range and types of values for each parameter that the optimizer will explore. These parameter types and ranges are initially inferred from the uploaded data. Please verify they are appropriate for your use case. While you can update the parameter space during optimization, do so only if you understand the implications.", target="search-space-block", placement="top", - style={"text-transform": "none"}, + className="text-transform-none", ), "Search Space Configuration", ] @@ -245,7 +240,7 @@ def update_xspace_vals(param_type, param_id, data, ycol): className='me-2', color='danger')]), html.Hr(), html.Div(id={'type': 'div-param_categories', 'index': x}, children=None, - style={'textAlign': 'center'})])), + className="text-center")])), dcc.Store(id={'type': 'store-param_categories', 'index': x}, data=', '.join(list(ser_x.sort_values().astype('str').unique()))), html.Div(id={'type': 'input-param_min', 'index': x}), diff --git a/obsidian/dash/optimize.py b/obsidian/dash/optimize.py index cf1554f..bb1bb5b 100644 --- a/obsidian/dash/optimize.py +++ b/obsidian/dash/optimize.py @@ -29,7 +29,7 @@ def setup_optimize(app, app_tabs): ]), html.Div(id='graph-parity', children=[]) ], - style={'textAlign': 'center'} + className="text-center" ) predict_div = dbc.Container([ @@ -41,11 +41,11 @@ def setup_optimize(app, app_tabs): dbc.Card([ dbc.CardHeader('Optimal Experiments'), dbc.CardBody([ - html.Div(id='div-predict', children=[], style={}) + html.Div(id="div-predict", children=[], className="overflow-x-scroll") ]), ]), ], - style={'textAlign': 'center'} + className="text-center" ) storage_fit = dcc.Store(id='store-fit', data=None) @@ -57,8 +57,7 @@ def setup_optimize(app, app_tabs): candidates_downloader = html.Div(children=[ dbc.Button('Download Suggested Candidates', id='button-download_candidates', className='me-2', color='primary'), - dcc.Download(id='downloader-candidates')], - style={'textAlign': 'center'}, className="mt-3") + dcc.Download(id='downloader-candidates')], className="text-center mt-3") # Add all of these elements to the app columns = dbc.Row([dbc.Col(fit_div, width=6), dbc.Col([predict_div, candidates_downloader], width=6)]) @@ -142,7 +141,7 @@ def graph_parity_plot(opt_save, config): @app.callback( Output('div-predict', 'children'), - Output('div-predict', 'style'), + Output('div-predict', 'className'), Output('button-predict', 'n_clicks'), Output('store-candidates', 'data'), Input('button-predict', 'n_clicks'), @@ -157,7 +156,7 @@ def predict_optimizer(predict_clicked, config, opt_save): alert_color = 'danger' else: alert_color = 'info' - return dbc.Alert('Model must be fit first', color=alert_color), {}, predict_clicked, {} + return dbc.Alert('Model must be fit first', color=alert_color), "", predict_clicked, {} optimizer = load_optimizer(config, opt_save) X_suggest, eval_suggest = optimizer.suggest(**config['aq_params']) @@ -165,7 +164,7 @@ def predict_optimizer(predict_clicked, config, opt_save): df_suggest.insert(loc=0, column='CandidatesID', value=df_suggest.index) tables = [center(make_table(df_suggest))] - return tables, {'overflow-x': 'scroll'}, 0, df_suggest.to_dict() + return tables, "overflow-x-scroll", 0, df_suggest.to_dict() # Download Suggested Candidates @app.callback( diff --git a/obsidian/dash/predict.py b/obsidian/dash/predict.py index 6d3312c..c7ad1f9 100644 --- a/obsidian/dash/predict.py +++ b/obsidian/dash/predict.py @@ -24,7 +24,7 @@ def setup_predict(app, app_tabs): ]), ]), ], - style={'textAlign': 'center'} + className="text-center" ) template_div = dbc.Container([ @@ -38,7 +38,7 @@ def setup_predict(app, app_tabs): ]), ]), ], - style={'textAlign': 'center'} + className="text-center" ) # candidates store store_template = dcc.Store(id='store-template', data={}) @@ -47,7 +47,7 @@ def setup_predict(app, app_tabs): template_downloader = html.Div(children=[dbc.Button('Download Template Candidates', id='button-download_template', className='me-2', color='primary'), dcc.Download(id='downloader-template')], - style={'textAlign': 'center'}, className="mt-3") + className="text-center mt-3") default_data = pd.DataFrame() @@ -55,12 +55,7 @@ def setup_predict(app, app_tabs): uploader_1 = dcc.Upload(id='uploader-X1', children=html.Div(['Upload Data: Drag and Drop or ', html.A('Select Files')]), - style={ - 'width': '100%', 'height': '60px', - 'lineHeight': '60px', 'borderWidth': '1px', - 'borderStyle': 'dashed', 'borderRadius': '5px', - 'textAlign': 'center', 'margin': '10px' - }, + className="m-2 text-center obsd-dashed-box", multiple=False, filename='Example Data' ) @@ -77,14 +72,14 @@ def setup_predict(app, app_tabs): dbc.Tooltip('Input data must be a CSV file and must include column headers for the input\ parameters and response variable(s), with one row per observation. Download\ template data (left) for example.', - target='info-data_1', placement='top', style={'text-transform': 'none'}), + target='info-data_1', placement='top', className="text-transform-none"), dbc.FormText('Example Data', color="info", className="obsd-form-text obsd-text-xl fst-italic", id='table-X1-footer') ], - style={'textAlign': 'center'})) - ])) + className="text-center")) + ], className="m-2")) row1 = dbc.Row([dbc.Col([xspace_df_div], width=4), dbc.Col([template_div, template_downloader], width=8)], className="mt-3") @@ -103,14 +98,14 @@ def setup_predict_callbacks(app): @app.callback( Output('button-config', 'n_clicks'), Output('div-xspace_df', 'children'), - Output('div-xspace_df', 'style'), + Output('div-xspace_df', 'className'), Input('button-config', 'n_clicks'), State('store-Xspace', 'data'), prevent_initial_call=True ) def config_tableView(clicked, Xspace_save): if not Xspace_save: - return 0, None, {'overflow-x': 'scroll'} + return 0, None, "overflow-x-scroll" Xspace_list = [] for param in Xspace_save.keys(): @@ -123,7 +118,7 @@ def config_tableView(clicked, Xspace_save): df_xspace = pd.DataFrame(Xspace_list) tables = [center(make_table(df_xspace))] - return 0, tables, {'overflow-x': 'scroll'} + return 0, tables, "overflow-x-scroll" # Generate New Data template according to x_space @app.callback( diff --git a/obsidian/dash/utils.py b/obsidian/dash/utils.py index 8faba0e..22857f0 100644 --- a/obsidian/dash/utils.py +++ b/obsidian/dash/utils.py @@ -11,7 +11,7 @@ def center(element): - return html.Div(html.Div(element, style={'display': 'inline-block'}), style={'textAlign': 'center'}) + return html.Div(html.Div(element, className="d-inline-block"), className="text-center") def load_optimizer(config, opt_save): @@ -117,7 +117,7 @@ def make_table(df, fill_width=False): def make_collapse(id, contents, label): components = [ html.Div(dbc.Button(label, id=f'button-collapse-{id}', className='mb-3', color='primary', n_clicks=0), - style={'textAlign': 'center'}), + className="text-center"), dbc.Collapse(contents, id=f'collapse-{id}', is_open=False) ] return dbc.Card(dbc.CardBody(components)) From 60e48557bdbe356f04e3c7c9b9a8616c94e12546 Mon Sep 17 00:00:00 2001 From: Xinyang Li Date: Tue, 7 Oct 2025 15:15:24 -0400 Subject: [PATCH 4/4] Fix wrong CSS class names used --- assets/css/styles.css | 7 +++---- obsidian/dash/utils.py | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/assets/css/styles.css b/assets/css/styles.css index 47d56fe..c54ca57 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -1,6 +1,6 @@ :root { - /* global defaults for Obsidian typography */ - --obsd-text-color: var(--bs-primary); + --obsd-label-color: var(--bs-primary); + --obsd-text-color: var(--bs-body-color); --obsd-form-font-size: 0.7em; --obsd-form-lg-font-size: 0.8em; --obsd-form-xl-font-size: 1em; @@ -32,7 +32,6 @@ .obsd-text-normal-style { font-style: var(--obsd-help-font-style); - /* 'normal' */ } .obsd-input-row { @@ -41,7 +40,7 @@ .obsd-form-label { font-weight: bold; - color: var(--obsd-text-color); + color: var(--obsd-label-color); font-size: var(--obsd-form-lg-font-size); } diff --git a/obsidian/dash/utils.py b/obsidian/dash/utils.py index 22857f0..3f24a21 100644 --- a/obsidian/dash/utils.py +++ b/obsidian/dash/utils.py @@ -42,7 +42,7 @@ def make_input( required=required, **kwargs, ), - html.Div(help_text, className="obsd-help-text"), + html.Div(help_text, className="obsd-form-text"), ] return html.Div(components, className="mb-4 obsd-input-row") @@ -56,7 +56,7 @@ def make_dropdown(property_name, help_text, options=[], id=None, kwargs={}): clearable=False, **kwargs, ), - dbc.FormText(help_text, className="obsd-help-text"), + dbc.FormText(help_text, className="obsd-form-text"), ] return html.Div(components, className="mb-4 obsd-input-row") @@ -71,7 +71,7 @@ def make_switch(property_name, help_text, id=None, kwargs={}): **kwargs, ) ), - dbc.FormText(help_text, className="obsd-help-text"), + dbc.FormText(help_text, className="obsd-form-text"), ] return html.Div(components, className="obsd-input-row") @@ -82,7 +82,7 @@ def make_slider(property_name, help_text, min, max, id=None, kwargs={}): dcc.Slider( min, max, id=f"input-[{property_name}]" if id is None else id, **kwargs ), - dbc.FormText(help_text, className="obsd-help-text"), + dbc.FormText(help_text, className="obsd-form-text"), ] return html.Div(components, className="obsd-input-row") @@ -96,7 +96,7 @@ def make_knob(property_name, help_text, min, max, id=None, kwargs={}): id=f"input-[{property_name}]" if id is None else id, **kwargs, ), - dbc.FormText(help_text, className="obsd-help-text"), + dbc.FormText(help_text, className="obsd-form-text"), ] return html.Div(components, className="obsd-input-row")