Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve smiles #20

Merged
merged 5 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ def pytest_configure(config):
# sudo apt-get install google-chrome-stable

webdriver = ChromeDriverManager().install()

os.environ["PATH"] += os.pathsep + str(Path(webdriver).parent)
os.environ["PATH"] = str(Path(webdriver).parent) + os.pathsep + os.environ["PATH"]

config.option.webdriver = "Chrome"
config.option.headless = True
Expand Down
49 changes: 16 additions & 33 deletions tests/test_smiles.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,36 @@
import time
import pandas as pd
import dash

from xiplot.setup import setup_xiplot_dash_app
from selenium.webdriver.common.keys import Keys
import dash
import pandas as pd
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import pytest

from tests.util_test import render_plot
from xiplot.plots.smiles import Smiles
from xiplot.setup import setup_xiplot_dash_app

(
render_clicks,
render_hovered,
) = Smiles.register_callbacks(dash.Dash(__name__), lambda x: x, lambda x: x)
update_smiles = Smiles.register_callbacks(dash.Dash(__name__), lambda x: x, lambda x: x)


def test_tesm001_render_smiles(dash_duo):
@pytest.fixture
def driver(dash_duo):
driver = dash_duo.driver
dash_duo.start_server(setup_xiplot_dash_app())
time.sleep(1)
dash_duo.wait_for_page()

render_plot(dash_duo, driver, "Smiles")
return driver

plot = driver.find_element(
By.XPATH,
"//div[@class='plots']",
)

assert "smiles-display" in plot.get_attribute("innerHTML")
def test_tesm001_render_smiles(dash_duo, driver):
plot = driver.find_element(By.XPATH, "//div[@class='plots']")
assert Smiles.get_id(None, "display")["type"] in plot.get_attribute("innerHTML")
assert dash_duo.get_logs() == [], "browser console should contain no error"

driver.close()


def test_tesm002_input_smiles_string(dash_duo):
driver = dash_duo.driver
dash_duo.start_server(setup_xiplot_dash_app())
time.sleep(1)
dash_duo.wait_for_page()

render_plot(dash_duo, driver, "Smiles")

smiles_input = driver.find_element(By.XPATH, "//div[@class='dcc-input']/input")
def test_tesm002_input_smiles_string(driver):
smiles_input = driver.find_element(By.XPATH, "//div[@class='dash-input']/div/input")
smiles_input.clear()
smiles_input.send_keys("O", Keys.RETURN)

Expand All @@ -55,17 +43,12 @@ def test_tesm002_input_smiles_string(dash_duo):
def test_render_clicks():
d = {"col1": [1, 2], "col2": [3, 4], "smiles": ["O", "N"]}
df = pd.DataFrame(data=d)
output = render_clicks(1, ["click"], [""], df)
smiles_string = output["smiles"][0]

smiles_string = update_smiles(1, None, "Click", "smiles", "", df)
assert smiles_string == "N"


def test_render_hovered():
d = {"col1": [1, 2], "col2": [3, 4], "smiles": ["O", "N"]}
df = pd.DataFrame(data=d)
output = render_hovered(1, ["hover"], [""], df)

smiles_string = output["smiles"][0]

smiles_string = update_smiles(None, 1, "Hover", "smiles", "", df)
assert smiles_string == "N"
6 changes: 4 additions & 2 deletions xiplot/assets/html_components.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ button.delete:active,
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
align-items: end;
gap: 10px;
gap: var(--panel-padding);
}

.flex-row .dash-dropdown {
.flex-row .dash-dropdown,
.flex-row .dash-input {
min-width: 12rem;
flex: 1;
}
Expand Down
8 changes: 6 additions & 2 deletions xiplot/assets/stylesheet.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--tab-underline: #adb5bd;
--selected-tab-underline: #198754;
--base-column-size: 30%;
--panel-padding: 10px;
}

[data-theme="dark"] {
Expand Down Expand Up @@ -84,7 +85,9 @@ body {
align-content: center;
flex-wrap: wrap;
gap: 10px;
gap: var(--panel-padding);
margin-top: 10px;
margin-top: var(--panel-padding);
}

.plots {
Expand All @@ -98,11 +101,12 @@ body {
0px 1px 10px 0px hsla(0, 0%, 0%, 0.12),
0px 2px 4px -1px hsla(0, 0%, 0%, 0.2);
padding: 10px;
padding: var(--panel-padding);
}

.smiles-img {
width: 90%;
height: 80%;
width: 100%;
max-width: 80rem;
display: block;
margin-left: auto;
margin-right: auto;
Expand Down
163 changes: 82 additions & 81 deletions xiplot/plots/smiles.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import pandas as pd
import dash
import jsonschema
import pandas as pd
from dash import MATCH, Input, Output, State, dcc, html

from dash import html, dcc, Output, Input, State, MATCH, ALL, ctx
from dash.exceptions import PreventUpdate
from xiplot.utils.components import DeleteButton, PlotData

from xiplot.utils.layouts import layout_wrapper
from xiplot.utils.dataframe import get_smiles_column_name
from xiplot.utils.smiles import get_smiles_inputs
from xiplot.plots import APlot
from xiplot.plugin import STORE_CLICKED_ID, STORE_DATAFRAME_ID, STORE_HOVERED_ID
from xiplot.utils.components import FlexRow, InputText, PlotData
from xiplot.utils.layouts import layout_wrapper


class Smiles(APlot):
@classmethod
def name(cls):
return "SMILES (render molecules)"

@classmethod
def register_callbacks(cls, app, df_from_store, df_to_store):
app.clientside_callback(
Expand Down Expand Up @@ -43,112 +44,112 @@ def register_callbacks(cls, app, df_from_store, df_to_store):
return "data:image/svg+xml;base64," + btoa(svg || INVALID_SVG);
}
""",
Output({"type": "smiles-display", "index": MATCH}, "src"),
Input({"type": "smiles-input", "index": MATCH}, "value"),
Output(cls.get_id(MATCH, "display"), "src"),
Input(cls.get_id(MATCH, "string"), "value"),
prevent_initial_call=False,
)

@app.callback(
output=dict(smiles=Output({"type": "smiles-input", "index": ALL}, "value")),
inputs=[
Input("lastly_clicked_point_store", "data"),
State({"type": "smiles_lock_dropdown", "index": ALL}, "value"),
State({"type": "smiles-input", "index": ALL}, "value"),
State("data_frame_store", "data"),
],
Output(cls.get_id(MATCH, "string"), "value"),
Input(STORE_CLICKED_ID, "data"),
Input(STORE_HOVERED_ID, "data"),
State(cls.get_id(MATCH, "mode"), "value"),
State(cls.get_id(MATCH, "col"), "value"),
State(cls.get_id(MATCH, "string"), "value"),
State(STORE_DATAFRAME_ID, "data"),
prevent_initial_call=True,
)
def render_clicks(row, render_modes, smiles_inputs, df):
def update_smiles(rowc, rowh, mode, col, old, df):
if col is None or col == "":
return dash.no_update
if mode == "Click":
row = rowc
elif mode == "Hover":
row = rowh
else:
raise Exception("Unknown SMILES mode: " + mode)
if row is None:
return dash.no_update
df = df_from_store(df)
smiles_col = get_smiles_column_name(df)

if not smiles_col or render_modes == [None]:
raise PreventUpdate()

smiles_inputs = get_smiles_inputs(
render_modes, "click", smiles_inputs, df, row
)
return dict(smiles=smiles_inputs)

@app.callback(
output=dict(smiles=Output({"type": "smiles-input", "index": ALL}, "value")),
inputs=[
Input("lastly_hovered_point_store", "data"),
State({"type": "smiles_lock_dropdown", "index": ALL}, "value"),
State({"type": "smiles-input", "index": ALL}, "value"),
State("data_frame_store", "data"),
],
)
def render_hovered(row, render_modes, smiles_inputs, df):
df = df_from_store(df)
smiles_col = get_smiles_column_name(df)

if not smiles_col or render_modes == [None]:
raise PreventUpdate()

smiles_inputs = get_smiles_inputs(
render_modes, "hover", smiles_inputs, df, row
)
return dict(smiles=smiles_inputs)
try:
new = df[col][row]
if old != new:
return new
return dash.no_update
except:
return dash.no_update

PlotData.register_callback(
cls.name(),
app,
dict(
mode=Input({"type": "smiles_lock_dropdown", "index": ALL}, "value"),
smiles=Input({"type": "smiles-input", "index": ALL}, "value"),
mode=Input(cls.get_id(MATCH, "mode"), "value"),
smiles=Input(cls.get_id(MATCH, "string"), "value"),
column=Input(cls.get_id(MATCH, "col"), "value"),
),
)

return [render_clicks, render_hovered]
return update_smiles

@classmethod
def create_new_layout(cls, index, df, columns, config=dict()):
def create_layout(cls, index, df: pd.DataFrame, columns, config=dict()):
jsonschema.validate(
instance=config,
schema=dict(
type="object",
properties=dict(
mode=dict(enum=["hover", "click", "lock"]),
mode=dict(enum=["Hover", "Click"]),
smiles=dict(type="string"),
column=dict(type="string"),
),
),
)

render_mode = config.get("mode", "hover")
smiles_input = config.get("smiles", "O.O[Fe]=O")

return html.Div(
children=[
DeleteButton(index),
html.Br(),
html.Img(
id={"type": "smiles-display", "index": index},
className="smiles-img",
),
html.Br(),
cols = [
c
for c in df.select_dtypes([object, "category"])
if isinstance(df[c][0], str)
]
column = next((c for c in cols if "smiles" in c.lower()), "")

render_mode = config.get("mode", "Hover")
smiles_input = config.get("smiles", "")
column = config.get("column", column)

return [
html.Img(
id=cls.get_id(index, "display"),
className="smiles-img",
),
html.Br(),
FlexRow(
layout_wrapper(
dcc.Input(
id={"type": "smiles-input", "index": index},
type="text",
value=smiles_input,
debounce=True,
placeholder="SMILES string",
dcc.Dropdown(
id=cls.get_id(index, "col"), value=column, options=cols
),
css_class="dcc-input",
title="SMILES string",
title="SMILES column",
css_class="dash-dropdown",
),
layout_wrapper(
dcc.Dropdown(
id={"type": "smiles_lock_dropdown", "index": index},
id=cls.get_id(index, "mode"),
value=render_mode,
options=["hover", "click", "lock"],
options=["Hover", "Click"],
clearable=False,
searchable=False,
),
css_class="dd-smiles",
title="Selection mode",
css_class="dash-dropdown",
),
PlotData(index, cls.name()),
],
id={"type": "smiles-container", "index": index},
className="plots",
)
layout_wrapper(
InputText(
id=cls.get_id(index, "string"),
value=smiles_input,
debounce=True,
placeholder="SMILES string, e.g., 'O.O[Fe]=O'",
),
css_class="dash-input",
title="SMILES string",
),
),
]
10 changes: 0 additions & 10 deletions xiplot/plots/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from xiplot.utils.layouts import layout_wrapper
from xiplot.utils.cluster import cluster_colours
from xiplot.utils.dataframe import get_smiles_column_name
from xiplot.utils.table import get_sort_by, get_updated_item, get_updated_item_id
from xiplot.utils.regex import dropdown_regex, get_columns_by_regex
from xiplot.plots import APlot
Expand Down Expand Up @@ -152,15 +151,6 @@ def update_lastly_activated_cell(active_cells, table_row_indices, df):
column = cell["column_id"]
break

smiles_col = get_smiles_column_name(df)

# Try branch for testing
try:
if row is None or column != smiles_col:
raise PreventUpdate()
except:
pass

return dict(cell_store=row, active_cell=[None] * len(active_cells))

@app.callback(
Expand Down
Loading