Skip to content

Commit af7d56f

Browse files
authored
Patch 0.2.1 (#23)
### Fixes - Field names containing spaces are now correctly delt with - The text search now looks for matches inside the values of the tooltip fields, rather than inside the HTML code of the tooltip which included tags and other irrelevant text - Fixed an encoding bug when saving the grid as an HTML file on French Windows, which uses CP-1252 encoding instead of UTF-8
1 parent ac0698b commit af7d56f

File tree

8 files changed

+83
-35
lines changed

8 files changed

+83
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.2.1] - 2022/02/23
8+
### Fixed
9+
- Field names containing spaces are now correctly delt with
10+
- The text search now looks for matches inside the values of the tooltip fields, rather
11+
than inside the HTML code of the tooltip which included tags and other irrelevant text
12+
- Fixed an encoding bug when saving the grid as an HTML file on French Windows, which uses
13+
CP-1252 encoding instead of UTF-8
14+
715
## [0.2.0] - 2022/02/10
816
### Added
917
- `cache_selection=True` allows to retrieve the checkbox state when re-displaying a grid,

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
[![Build status](https://github.com/cbouy/mols2grid/workflows/build/badge.svg)](https://github.com/cbouy/mols2grid/actions/workflows/build.yml)
99

1010
[![Powered by RDKit](https://img.shields.io/static/v1?label=Powered%20by&message=RDKit&color=3838ff&style=flat&logo=data:image/x-icon;base64,AAABAAEAEBAQAAAAAABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAMAABILAAASCwAAAAAAAAAAAADc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/FBT/FBT/FBT/FBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/PBT/PBT/PBT/PBT/PBT/PBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/jIz/jIz/jIz/jIz/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nz/FBT/PBT/ZGT/ZGT/jIz/jIz/jIz/jIz/jIz/jIz/ZGT/ZGT/PBT/FBTc3Nzc3Nz/FBT/PBT/ZGT/ZGT/jIz/jIz/tLT/tLT/jIz/jIz/ZGT/ZGT/PBT/FBTc3Nzc3Nz/FBT/PBT/ZGT/ZGT/jIz/jIz/tLT/tLT/jIz/jIz/ZGT/ZGT/PBT/FBTc3Nzc3Nz/FBT/PBT/ZGT/ZGT/jIz/jIz/jIz/jIz/jIz/jIz/ZGT/ZGT/PBT/FBTc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/jIz/jIz/jIz/jIz/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/PBT/ZGT/ZGT/ZGT/ZGT/ZGT/ZGT/PBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/PBT/PBT/PBT/PBT/PBT/PBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/FBT/FBT/FBT/FBT/FBT/FBTc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz/////+B////AP///gB///wAP//4AB//+AAf//gAH//4AB//+AAf//gAH//8AD///gB///8A////gf////////)](https://www.rdkit.org/)
11+
[![Knime Hub](https://img.shields.io/static/v1?label=Available%20on&message=KNIME&color=ffd500&style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAABdUExURUxpcf/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAP/VAPfHMOMAAAAfdFJOUwCyq6CEYtAEApEspncGDpjxVlAYgDSdiEBHbMrCHtmwXwB/AAAAT3pUWHRSYXcgcHJvZmlsZSB0eXBlIGlwdGMAAHic48osKEnmUgADIwsuYwsTIxNLkxQDEyBEgDTDZAMjs1Qgy9jUyMTMxBzEB8uASKBKLgAolQ7jKiJtHwAAAIxJREFUGNNdjFkSgyAQBYdtYADZVNxz/2NGjSlj+q9fvWqAD1rDk1Ke3nJqH4NnpH7d4iCFvV1XVJ3r7u6URPZiHb8eeFJ25sjDNahlKRDUkq7u5njd32ZC3A433g2+h3bKCuUx9FHOecyV/CzXfi/KSJG9EjJB0lEAS9UxxriINMiOLJim0SfNiYF/3szTBp6mEP9HAAAAAElFTkSuQmCC)](https://hub.knime.com/cbouy/spaces/Private/latest/Interactive%20Grid%20of%20Molecules~OZIyk4YLBNvXq-xU)
1112
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/cbouy/mols2grid/blob/master/demo.ipynb)
1213

1314
mols2grid is an interactive chemical viewer for 2D structures of small molecules, based on RDKit.
@@ -30,7 +31,12 @@ Alternatively, you can also use pip:
3031
pip install mols2grid
3132
```
3233

33-
It is compatible with Jupyter Notebook and Google Colab (Visual Studio notebooks and Jupyterlab are not fully supported) and can run on Streamlit.
34+
It is fully compatible with Jupyter Notebook and Google Colab and can run on Streamlit.
35+
It also works with Visual Studio notebooks and Jupyterlab, except for accessing your selection with `mols2grid.get_selection()` and copying it to the clipboard, but you can still export it as a CSV or SMILES file.
36+
37+
<img alt="knime logo" align="left" style="padding:6px" src="https://www.knime.com/sites/default/files/favicons/favicon-32x32.png"/>
38+
<p>You can also use mols2grid directly in <a href="https://www.knime.com/">KNIME</a>, by looking for the `Interactive Grid of Molecules` component on the Knime HUB.<br/>
39+
Make sure you have setup <a href="https://docs.knime.com/latest/python_installation_guide">Knime's Python integration</a> for the node to work.</p>
3440

3541
## 📜 Usage
3642
---
@@ -152,12 +158,12 @@ You can either:
152158

153159
## 🚀 Resources
154160
---
155-
* [Simple exemple](https://iwatobipen.wordpress.com/2021/06/13/draw-molecules-on-jupyter-notebook-rdkit-mols2grid/) by iwatobipen
161+
* [Simple example](https://iwatobipen.wordpress.com/2021/06/13/draw-molecules-on-jupyter-notebook-rdkit-mols2grid/) by iwatobipen
156162
* Creating a web app with Streamlit for filtering datasets:
157163
* [Blog post](https://blog.reverielabs.com/building-web-applications-from-python-scripts-with-streamlit/) by Justin Chavez
158164
* [Video tutorial](https://www.youtube.com/watch?v=0rqIwSeUImo) by Data Professor
159-
* [Viewing clustered chemical structures](https://practicalcheminformatics.blogspot.com/2021/07/viewing-clustered-chemical-structures.html) by Pat Walters
160-
* [RDKit UGM 2021 notebook](https://colab.research.google.com/github/rdkit/UGM_2021/blob/main/Notebooks/Bouysset_mols2grid.ipynb)
165+
* [Viewing clustered chemical structures](https://practicalcheminformatics.blogspot.com/2021/07/viewing-clustered-chemical-structures.html) and [Exploratory data analysis](https://practicalcheminformatics.blogspot.com/2021/10/exploratory-data-analysis-with.html) by Pat Walters
166+
* [Advanced notebook (RDKit UGM 2021)](https://colab.research.google.com/github/rdkit/UGM_2021/blob/main/Notebooks/Bouysset_mols2grid.ipynb)
161167

162168
## 👏 Acknowledgments
163169
---
@@ -170,7 +176,7 @@ You can either:
170176

171177
Unless otherwise noted, all files in this directory and all subdirectories are distributed under the Apache License, Version 2.0:
172178
```
173-
Copyright 2021 Cédric BOUYSSET
179+
Copyright 2021-2022 Cédric BOUYSSET
174180

175181
Licensed under the Apache License, Version 2.0 (the "License");
176182
you may not use this file except in compliance with the License.

mols2grid/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.0"
1+
__version__ = "0.2.1"

mols2grid/molgrid.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
mol_to_record,
1313
mol_to_smiles,
1414
sdf_to_dataframe,
15-
remove_coordinates)
15+
remove_coordinates,
16+
slugify)
1617
from .select import register
1718
try:
1819
from IPython.display import HTML, Javascript
@@ -397,16 +398,13 @@ def to_pages(self, subset=None, tooltip=None, n_cols=5, n_rows=3,
397398
# define fields that are searchable and sortable
398399
search_cols = [f"data-{col}" for col in subset if col != "img"]
399400
if tooltip:
400-
search_cols.append("mols2grid-tooltip")
401-
sort_cols = search_cols[:-1]
402-
sort_cols.extend([f"data-{col}" for col in tooltip])
401+
search_cols.extend([f"data-{col}" for col in tooltip])
403402
for col in tooltip:
404403
if col not in subset:
405-
s = f'<div class="data data-{col}" style="display: none;"></div>'
404+
s = f'<div class="data data-{slugify(col)}" style="display: none;"></div>'
406405
content.append(s)
407406
column_map[col] = f"data-{col}"
408-
else:
409-
sort_cols = search_cols[:]
407+
sort_cols = search_cols[:]
410408
sort_cols = ["mols2grid-id"] + sort_cols
411409
# get unique list but keep order
412410
sort_cols = list(dict.fromkeys(sort_cols))
@@ -441,15 +439,16 @@ def to_pages(self, subset=None, tooltip=None, n_cols=5, n_rows=3,
441439
'data-toggle="popover" data-content="."></a>')
442440
else:
443441
if style.get(col):
444-
s = f'<div class="data data-{col} style-{col}" style=""></div>'
442+
s = (f'<div class="data data-{slugify(col)} style-{slugify(col)}" '
443+
'style=""></div>')
445444
else:
446-
s = f'<div class="data data-{col}"></div>'
445+
s = f'<div class="data data-{slugify(col)}"></div>'
447446
temp.append(s)
448447
column_map[col] = f"data-{col}"
449448
content = temp + content
450449
# add but hide SMILES div if not present
451450
if smiles not in (subset + tooltip):
452-
s = f'<div class="data data-{smiles}" style="display: none;"></div>'
451+
s = f'<div class="data data-{slugify(smiles)}" style="display: none;"></div>'
453452
content.append(s)
454453
column_map[smiles] = f"data-{smiles}"
455454
# set mapping for list.js
@@ -459,6 +458,7 @@ def to_pages(self, subset=None, tooltip=None, n_cols=5, n_rows=3,
459458
else:
460459
whole_cell_style = False
461460
x = "[{data: ['mols2grid-id']}, "
461+
value_names = [slugify(c) for c in value_names]
462462
value_names = x + str(value_names)[1:]
463463

464464
# apply CSS styles
@@ -467,7 +467,7 @@ def to_pages(self, subset=None, tooltip=None, n_cols=5, n_rows=3,
467467
name = "cellstyle"
468468
df[name] = df.apply(func, axis=1)
469469
else:
470-
name = f"style-{col}"
470+
name = f"style-{slugify(col)}"
471471
df[name] = df[col].apply(func)
472472
final_columns.append(name)
473473
value_names = value_names[:-1] + f", {{ attr: 'style', name: {name!r} }}]"
@@ -523,12 +523,18 @@ def to_pages(self, subset=None, tooltip=None, n_cols=5, n_rows=3,
523523

524524
if sort_by and sort_by != "mols2grid-id":
525525
if sort_by in (subset + tooltip):
526-
sort_by = f"data-{sort_by}"
526+
sort_by = f"data-{slugify(sort_by)}"
527527
else:
528528
raise ValueError(f"{sort_by!r} is not an available field in "
529529
"`subset` or `tooltip`")
530530
else:
531531
sort_by = "mols2grid-id"
532+
533+
# slugify remaining vars
534+
column_map = {k: slugify(v) for k, v in column_map.items()}
535+
sort_cols = [slugify(c) for c in sort_cols]
536+
search_cols = [slugify(c) for c in search_cols]
537+
smiles = slugify(smiles)
532538
df = df[final_columns].rename(columns=column_map).sort_values(sort_by)
533539

534540
template = env.get_template('pages.html')
@@ -750,5 +756,5 @@ def display(self, width="100%", height=None, iframe_allow="clipboard-write",
750756

751757
def save(self, output, **kwargs):
752758
"""Render and save the grid in an HTML document"""
753-
with open(output, "w") as f:
759+
with open(output, "w", encoding="utf-8") as f:
754760
f.write(self.render(**kwargs))

mols2grid/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from functools import wraps, partial
23
from importlib.util import find_spec
34
from jinja2 import Environment, FileSystemLoader
@@ -40,7 +41,8 @@ def tooltip_formatter(s, subset, fmt, style, transform):
4041
items = []
4142
for k, v in s[subset].to_dict().items():
4243
displayed = transform[k](v) if transform.get(k) else v
43-
v = f'<span style="{style[k](v)}">{displayed}</span>' if style.get(k) else displayed
44+
v = (f'<span style="{style[k](v)}">{displayed}</span>'
45+
if style.get(k) else displayed)
4446
items.append(fmt.format(key=k, value=v))
4547
return "<br>".join(items)
4648

@@ -92,3 +94,7 @@ def make_popup_callback(title, html, js="", style=""):
9294
html=html,
9395
title=title,
9496
style=style))
97+
98+
def slugify(string):
99+
"""Replaces whitespaces with hyphens"""
100+
return re.sub(r"\s+", "-", string)

tests/test_interface.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,3 +600,27 @@ def test_subset_gives_rows_order(driver, grid):
600600
assert el.value_of_css_property("display") == "none"
601601
else:
602602
assert name == subset[i]
603+
604+
def test_colname_with_spaces(driver, df):
605+
df = (df
606+
.rename(columns={"SMILES": "Molecule", "_Name": "Molecule name"})
607+
.drop(columns="mol"))
608+
grid = mols2grid.MolGrid(df, smiles_col="Molecule")
609+
doc = get_doc(grid, dict(
610+
subset=["Molecule name", "img"],
611+
tooltip=["Molecule"],
612+
n_rows=1))
613+
driver.get(doc)
614+
driver.wait_for_img_load()
615+
el = driver.find_by_css_selector("#mols2grid .cell .data")
616+
assert el.text == "3-methylpentane"
617+
618+
def test_custom_header(driver, grid):
619+
doc = get_doc(grid, {
620+
"subset": ["img"],
621+
"custom_header": '<script src="https://unpkg.com/@rdkit/rdkit@2021.9.2/Code/MinimalLib/dist/RDKit_minimal.js"></script>',
622+
"n_rows": 1})
623+
driver.get(doc)
624+
driver.wait_for_img_load()
625+
val = driver.execute_script("return RDKit.version();")
626+
assert val == "2021.09.2"

tests/test_molgrid.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -234,21 +234,6 @@ def test_render_wrong_template(grid_otf):
234234
with pytest.raises(ValueError, match="template='foo' not supported"):
235235
grid_otf.render(template="foo")
236236

237-
@pytest.mark.parametrize("kwargs", [
238-
dict(),
239-
dict(subset=["img"]),
240-
dict(tooltip=["ID"]),
241-
dict(selection=False),
242-
dict(style={"ID": lambda x: "color: red" if x == 1 else ""}),
243-
dict(transform={"ID": lambda x: f"Id. #{x}"}),
244-
dict(custom_css="* {color: red;}"),
245-
dict(callback="console.log(JSON.stringify(data));"),
246-
dict(custom_header='<script src="https://unpkg.com/@rdkit/rdkit@2021.3.2/Code/MinimalLib/dist/RDKit_minimal.js"></script>'),
247-
dict(substruct_highlight=False),
248-
])
249-
def test_integration_pages(grid_otf, kwargs):
250-
grid_otf.to_pages(**kwargs)
251-
252237
@pytest.mark.parametrize("kwargs", [
253238
dict(),
254239
dict(subset=["ID"]),

tests/test_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,16 @@ def test_make_popup_callback():
139139
assert '<span id="foo">${title}</span>' in popup
140140
assert '// prerequisite JavaScript code\nvar title = "FOOBAR";' in popup
141141
assert '<div class="modal-dialog" style="max-width: 42%;">' in popup
142+
143+
@pytest.mark.parametrize(["string", "expected"], [
144+
("Mol", "Mol"),
145+
("mol name", "mol-name"),
146+
("mol name", "mol-name"),
147+
("mol-name", "mol-name"),
148+
("mol- name", "mol--name"),
149+
("mol\tname", "mol-name"),
150+
("mol\nname", "mol-name"),
151+
("mol \t\n name", "mol-name"),
152+
])
153+
def test_slugify(string, expected):
154+
assert utils.slugify(string) == expected

0 commit comments

Comments
 (0)