├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .readthedocs.yaml ├── CHANGELOG.md ├── CITATION.cff ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── babel.config.js ├── codecov.yml ├── css └── widget.css ├── docs ├── Makefile ├── _static │ ├── custom.css │ ├── demo.png │ └── mols2grid_logo.png ├── api │ ├── callbacks.rst │ ├── molgrid.rst │ ├── simple.rst │ └── utils.rst ├── conf.py ├── contents.md ├── environment.yml ├── index.rst ├── make.bat └── notebooks │ ├── callbacks.ipynb │ ├── customization.ipynb │ ├── filtering.ipynb │ └── quickstart.ipynb ├── install.json ├── mols2grid.json ├── mols2grid ├── __init__.py ├── _version.py ├── callbacks.py ├── dispatch.py ├── molgrid.py ├── nbextension │ └── extension.js ├── select.py ├── templates │ ├── __init__.py │ ├── css │ │ ├── common.css │ │ ├── interactive.css │ │ └── static.css │ ├── html │ │ ├── common_header.html │ │ ├── iframe.html │ │ └── interactive_header.html │ ├── interactive.html │ ├── js │ │ ├── callbacks │ │ │ ├── external_link.js │ │ │ └── show_3d.js │ │ ├── draw_mol.js │ │ ├── filter.js │ │ ├── grid_interaction.js │ │ ├── interactive.js │ │ ├── kernel.js │ │ ├── molstorage.js │ │ ├── popup.js │ │ ├── search.js │ │ └── sort.js │ └── static.html ├── utils.py └── widget │ ├── __init__.py │ ├── _frontend.py │ └── widget.py ├── package-lock.json ├── package.json ├── pyproject.toml ├── setup.py ├── src ├── extension.ts ├── index.ts ├── plugin.ts ├── version.ts └── widget.ts ├── tests ├── __init__.py ├── conftest.py ├── environment.yml ├── pytest.ini ├── test_callbacks.py ├── test_interface.py ├── test_molgrid.py ├── test_select.py ├── test_utils.py └── webdriver_utils.py ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "space-before-function-paren": ["error", "never"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | release: 4 | types: [released] 5 | workflow_dispatch: 6 | 7 | defaults: 8 | run: 9 | shell: bash -l {0} 10 | 11 | env: 12 | IS_PRERELEASE: ${{ github.event_name == 'workflow_dispatch' }} 13 | 14 | jobs: 15 | build-n-publish: 16 | name: Build and publish mols2grid 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Get prerelease version tags 24 | if: env.IS_PRERELEASE == 'true' 25 | run: | 26 | py_dirty_tag=$(awk '/__version__ = "[[:digit:]+]\.[[:digit:]+]\.[[:digit:]+]\-.+"/ {print $3}' ./mols2grid/_version.py) 27 | py_is_pre=$(test -z "$py_dirty_tag" && echo "false" || echo "true") 28 | js_version_string=$(grep '"version":' ./package.json) 29 | js_dirty_tag=$(echo "$js_version_string" | cut -d- -f2) 30 | js_is_pre=$(test "$js_version_string" == "$js_dirty_tag" && echo "false" || echo "true") 31 | echo "py_is_pre=$py_is_pre" >> $GITHUB_ENV 32 | echo "js_is_pre=$js_is_pre" >> $GITHUB_ENV 33 | 34 | - name: Fail if prerelease is not correctly versioned 35 | if: (env.IS_PRERELEASE == 'true') && !( env.py_is_pre && env.js_is_pre ) 36 | uses: actions/github-script@v3 37 | with: 38 | script: | 39 | core.setFailed("Versions are not tagged as a prerelease") 40 | 41 | - name: Install node 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: '12.x' 45 | registry-url: 'https://registry.npmjs.org/' 46 | cache: "npm" 47 | 48 | - name: Install python with pip 49 | uses: actions/setup-python@v4 50 | with: 51 | python-version: 3.8 52 | cache: "pip" 53 | 54 | - name: Install dependencies for packaging 55 | run: | 56 | pip install setuptools wheel build virtualenv twine 57 | 58 | - name: Check python installation 59 | run: | 60 | which python 61 | python --version 62 | pip --version 63 | pip list 64 | 65 | - name: Build package 66 | run: | 67 | python -m build . 68 | 69 | - name: Publish the Python package 70 | env: 71 | TWINE_USERNAME: __token__ 72 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 73 | run: twine upload dist/mols2grid*.{tar.gz,whl} 74 | 75 | - name: Publish the NPM package 76 | run: npm publish 77 | env: 78 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 79 | PRE_RELEASE: ${{ env.IS_PRERELEASE }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - "*" 9 | workflow_dispatch: 10 | 11 | defaults: 12 | run: 13 | shell: bash -l {0} 14 | 15 | concurrency: 16 | group: ${{ github.ref }}-${{ github.head_ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | tests: 21 | name: ${{ matrix.label }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | include: 26 | - label: CI-old 27 | os: ubuntu-latest 28 | python-version: 3.8 29 | extra_dependencies: "rdkit==2020.03.1 boost-cpp=1.72.0=h359cf19_6" 30 | - label: CI-edge 31 | os: ubuntu-latest 32 | python-version: "3.10" 33 | extra_dependencies: "rdkit" 34 | - label: CI-py3.8-rdkit2022 35 | os: ubuntu-latest 36 | python-version: 3.8 37 | extra_dependencies: "rdkit==2022.03.1" 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | 42 | - name: Install node 43 | uses: actions/setup-node@v3 44 | with: 45 | node-version: "12.x" 46 | cache: "npm" 47 | 48 | - name: Install Firefox 49 | uses: browser-actions/setup-firefox@latest 50 | 51 | - run: firefox --version 52 | 53 | - name: Prepare Selenium 54 | uses: browser-actions/setup-geckodriver@latest 55 | with: 56 | geckodriver-version: "0.32.0" 57 | 58 | - run: geckodriver --version 59 | 60 | - name: Cache conda 61 | uses: actions/cache@v3 62 | env: 63 | CACHE_NUMBER: 0 64 | with: 65 | path: ~/conda_pkgs_dir 66 | key: 67 | conda-${{ hashFiles('environment.yml') }}-${{ matrix.label }}-${{ env.CACHE_NUMBER }} 68 | 69 | - name: Cache pip 70 | uses: actions/cache@v3 71 | with: 72 | path: ~/.cache/pip 73 | key: pip-${{ hashFiles('setup.cfg') }} 74 | restore-keys: pip- 75 | 76 | - name: Setup Conda 77 | uses: conda-incubator/setup-miniconda@v2 78 | with: 79 | python-version: ${{ matrix.python-version }} 80 | environment-file: tests/environment.yml 81 | use-only-tar-bz2: true 82 | miniforge-variant: Mambaforge 83 | miniforge-version: latest 84 | use-mamba: true 85 | 86 | - name: Check conda and pip 87 | run: | 88 | which python 89 | python --version 90 | pip --version 91 | conda --version 92 | mamba --version 93 | 94 | - name: Install remaining conda dependencies 95 | run: | 96 | mamba install ${{ matrix.extra_dependencies }} 97 | mamba list 98 | 99 | - name: Install package through pip 100 | run: | 101 | pip install .[tests,build] 102 | pip list 103 | 104 | - name: Run tests 105 | run: | 106 | pytest --color=yes --disable-pytest-warnings \ 107 | --cov=mols2grid --cov-report=xml \ 108 | tests/ -m "not webdriver" 109 | 110 | - name: Run webdriver tests 111 | uses: nick-fields/retry@v2 112 | with: 113 | timeout_seconds: 340 114 | max_attempts: 5 115 | retry_on: timeout 116 | shell: bash 117 | command: | 118 | source /usr/share/miniconda/etc/profile.d/conda.sh 119 | conda activate /usr/share/miniconda3/envs/test 120 | pytest --color=yes --disable-pytest-warnings \ 121 | --cov=mols2grid --cov-report=xml --cov-append \ 122 | tests/ -m "webdriver" 123 | 124 | - name: Measure tests coverage 125 | uses: codecov/codecov-action@v3 126 | with: 127 | files: ./coverage.xml 128 | fail_ci_if_error: true 129 | verbose: true 130 | 131 | - name: Prepare for build 132 | run: | 133 | pip uninstall -y mols2grid 134 | python -m build . 135 | echo "$SCRIPT" > test_install.py 136 | cat test_install.py 137 | env: 138 | SCRIPT: | 139 | import mols2grid as mg 140 | from rdkit import RDConfig 141 | sdf = f"{RDConfig.RDDocsDir}/Book/data/solubility.test.sdf" 142 | mg.save(sdf, output="/dev/null") 143 | 144 | - name: Test tar.gz build 145 | run: | 146 | pip install dist/mols2grid-*.tar.gz 147 | python test_install.py 148 | pip uninstall -y mols2grid 149 | 150 | - name: Test wheel build 151 | run: | 152 | pip install dist/mols2grid-*.whl 153 | python test_install.py 154 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # vuepress v2.x temp and cache directory 109 | .temp 110 | 111 | # Docusaurus cache and generated files 112 | .docusaurus 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v2 130 | .yarn/cache 131 | .yarn/unplugged 132 | .yarn/build-state.yml 133 | .yarn/install-state.gz 134 | .pnp.* 135 | 136 | ### Node Patch ### 137 | # Serverless Webpack directories 138 | .webpack/ 139 | 140 | # Optional stylelint cache 141 | 142 | # SvelteKit build / generate output 143 | .svelte-kit 144 | 145 | # End of https://www.toptal.com/developers/gitignore/api/node 146 | 147 | # Created by https://www.toptal.com/developers/gitignore/api/jupyternotebooks,python 148 | # Edit at https://www.toptal.com/developers/gitignore?templates=jupyternotebooks,python 149 | 150 | ### JupyterNotebooks ### 151 | # gitignore template for Jupyter Notebooks 152 | # website: http://jupyter.org/ 153 | 154 | .ipynb_checkpoints 155 | */.ipynb_checkpoints/* 156 | 157 | # IPython 158 | profile_default/ 159 | ipython_config.py 160 | 161 | # Remove previous ipynb_checkpoints 162 | # git rm -r .ipynb_checkpoints/ 163 | 164 | ### Python ### 165 | # Byte-compiled / optimized / DLL files 166 | __pycache__/ 167 | *.py[cod] 168 | *$py.class 169 | 170 | # C extensions 171 | *.so 172 | 173 | # Distribution / packaging 174 | .Python 175 | build/ 176 | develop-eggs/ 177 | dist/ 178 | downloads/ 179 | eggs/ 180 | .eggs/ 181 | lib/ 182 | parts/ 183 | sdist/ 184 | var/ 185 | wheels/ 186 | pip-wheel-metadata/ 187 | share/python-wheels/ 188 | *.egg-info/ 189 | .installed.cfg 190 | *.egg 191 | MANIFEST 192 | 193 | # PyInstaller 194 | # Usually these files are written by a python script from a template 195 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 196 | *.manifest 197 | *.spec 198 | 199 | # Installer logs 200 | pip-log.txt 201 | pip-delete-this-directory.txt 202 | 203 | # Unit test / coverage reports 204 | htmlcov/ 205 | .tox/ 206 | .nox/ 207 | .coverage 208 | .coverage.* 209 | .cache 210 | nosetests.xml 211 | coverage.xml 212 | *.cover 213 | *.py,cover 214 | .hypothesis/ 215 | .pytest_cache/ 216 | pytestdebug.log 217 | 218 | # Translations 219 | *.mo 220 | *.pot 221 | 222 | # Django stuff: 223 | *.log 224 | local_settings.py 225 | db.sqlite3 226 | db.sqlite3-journal 227 | 228 | # Flask stuff: 229 | instance/ 230 | .webassets-cache 231 | 232 | # Scrapy stuff: 233 | .scrapy 234 | 235 | # Sphinx documentation 236 | docs/_build/ 237 | doc/_build/ 238 | 239 | # PyBuilder 240 | target/ 241 | 242 | # Jupyter Notebook 243 | 244 | # IPython 245 | 246 | # pyenv 247 | .python-version 248 | 249 | # pipenv 250 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 251 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 252 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 253 | # install all needed dependencies. 254 | #Pipfile.lock 255 | 256 | # poetry 257 | #poetry.lock 258 | 259 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 260 | __pypackages__/ 261 | 262 | # Celery stuff 263 | celerybeat-schedule 264 | celerybeat.pid 265 | 266 | # SageMath parsed files 267 | *.sage.py 268 | 269 | # Environments 270 | # .env 271 | .env/ 272 | .venv/ 273 | env/ 274 | venv/ 275 | ENV/ 276 | env.bak/ 277 | venv.bak/ 278 | pythonenv* 279 | 280 | # Spyder project settings 281 | .spyderproject 282 | .spyproject 283 | 284 | # Rope project settings 285 | .ropeproject 286 | 287 | # mkdocs documentation 288 | /site 289 | 290 | # mypy 291 | .mypy_cache/ 292 | .dmypy.json 293 | dmypy.json 294 | 295 | # Pyre type checker 296 | .pyre/ 297 | 298 | # pytype static type analyzer 299 | .pytype/ 300 | 301 | # operating system-related files 302 | *.DS_Store #file properties cache/storage on macOS 303 | Thumbs.db #thumbnail cache on Windows 304 | 305 | # profiling data 306 | .prof 307 | 308 | 309 | # End of https://www.toptal.com/developers/gitignore/api/jupyternotebooks,python 310 | 311 | # ========================= 312 | # Operating System Files 313 | # ========================= 314 | 315 | # OSX 316 | # ========================= 317 | 318 | .DS_Store 319 | .AppleDouble 320 | .LSOverride 321 | 322 | # Thumbnails 323 | ._* 324 | 325 | # Files that might appear in the root of a volume 326 | .DocumentRevisions-V100 327 | .fseventsd 328 | .Spotlight-V100 329 | .TemporaryItems 330 | .Trashes 331 | .VolumeIcon.icns 332 | 333 | # Directories potentially created on remote AFP share 334 | .AppleDB 335 | .AppleDesktop 336 | Network Trash Folder 337 | Temporary Items 338 | .apdisk 339 | 340 | # Windows 341 | # ========================= 342 | 343 | # Windows image file caches 344 | Thumbs.db 345 | ehthumbs.db 346 | 347 | # Folder config file 348 | Desktop.ini 349 | 350 | # Recycle Bin used on file shares 351 | $RECYCLE.BIN/ 352 | 353 | # Windows Installer files 354 | *.cab 355 | *.msi 356 | *.msm 357 | *.msp 358 | 359 | # Windows shortcuts 360 | *.lnk 361 | 362 | 363 | # NPM 364 | # ---- 365 | 366 | **/node_modules/ 367 | 368 | # Coverage data 369 | # ------------- 370 | **/coverage/ 371 | 372 | # ====== 373 | # Custom 374 | # ====== 375 | 376 | mols2grid/nbextension/index.* 377 | mols2grid/labextension 378 | .vscode/ 379 | quickstart-grid.html 380 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tests/ 4 | .jshintrc 5 | # Ignore any build output from python: 6 | dist/*.tar.gz 7 | dist/*.wheel 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | mols2grid/templates/interactive.html 2 | mols2grid/templates/static.html 3 | mols2grid/templates/html/interactive_header.html 4 | mols2grid/templates/html/iframe.html -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "preserve", 3 | "printWidth": 110, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "semi": false, 7 | "singleQuote": true, 8 | "quoteProps": "as-needed", 9 | "trailingComma": "es5", 10 | "bracketSpacing": false, 11 | "bracketSameLine": true, 12 | "arrowParens": "avoid", 13 | "htmlWhitespaceSensitivity": "css" 14 | } 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-20.04 10 | tools: 11 | python: "mambaforge-4.10" 12 | 13 | sphinx: 14 | configuration: docs/conf.py 15 | fail_on_warning: false 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | 24 | conda: 25 | environment: docs/environment.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | --- 10 | 11 | ## [2.0.0] - 2023/07/23 12 | 13 | This release is a major change on the UI contributed by @themoenen, refer to 14 | [PR#55](https://github.com/cbouy/mols2grid/pull/55) for the full list of changes: 15 | 16 | ### Added 17 | - `background_color="white"` parameter added to `display` and `save` to control the background 18 | color of each cell. 19 | - Property values displayed via subset or tooltip can now be copied by clicking them. 20 | 21 | ### Changed 22 | - Responsiveness: the grid as well as all UI components are now fully responsive, up until around 23 | ~415px wide. Smaller usage seems unlikely. 24 | - `n_items_per_page` replaces the `n_rows` and `n_cols` parameters. The responsive CSS assumes 25 | results to be a multiple of 12, otherwise a gap is displayed at the end of the grid. 26 | - Hover tooltips: now displayed when hovering the *`i`* icon, and anchored by clicking this icon. 27 | - Save CVS: When exporting as CSV we now use a semicolon `;` as delineator instead of a tab, this 28 | way CSVs are properly previewed on Mac. 29 | - Templates: the `pages`/`table` templates and corresponding functions have been renamed to 30 | `interactive`/`static` for clarity. 31 | - Parameters `width` and `height` have been renamed to `iframe_width` and `iframe_height` to be more 32 | descriptive. 33 | - Improved the sorting UI to be more intuitive. 34 | - You can now toggle between text/SMARTS search instead of having to click a dropdown. 35 | - The checkbox menu icon was replaced with a more standard triple dot menu. 36 | - The mols2grid-id is now permanently displayed next to the checkbox in the corner. 37 | - SVGs are now rendered with a transparent background. 38 | - The entire cell is now clickable, instead of just the tiny checkbox. 39 | - Implemented some basic keyboard navigation: ENTER / ESC for selecting / unselecting, arrows and 40 | TAB for navigation. 41 | - Copy to clipboard: this will now record a tab delineated text which is ready to be pasted into a 42 | spreadsheet, instead of the less useful dictionary format. 43 | - The `tooltip_trigger` parameter has been removed since callback and hover functionalities don't 44 | overlap anymore. 45 | - We're now automatically adding `"img"` to the subset instead of throwing an error. 46 | - A smaller default font size (12px) makes the experience out of the box a bit more practical. 47 | 48 | ### Fixed 49 | - Docstring now mention default values and some inconsistencies have been fixed. 50 | - All UI elements are now neatly aligned and displayed on top so they're accessible without 51 | scrolling. 52 | - Longer property values are now only truncated in the interactive grid, and broken into multiple 53 | lines by default in the static grid, as it is mostly meant for printing. A new parameter 54 | `truncate` lets you override the default truncating behavior. 55 | - The tooltip display zone (around which the tooltip is displayed) is now the entire cell instead 56 | of only the image, so it never overlaps with any of the cell's data or functionality. 57 | - When you download a CSV or SMILES file without any cells selected, you will now download data for 58 | all cells instead of an empty document. 59 | - Parameter `gap` in static template didn't work. 60 | - We no longer resize the iframe if a custom `iframe_height` is set by the display function. 61 | 62 | --- 63 | 64 | ## [1.1.1] - 2023/03/18 65 | 66 | ### Added 67 | - Support for `pathlib.Path` objects as input for `display`, `save`, `MolGrid.from_sdf` 68 | and `sdf_to_dataframe`. 69 | 70 | ### Changed 71 | - The hover tooltip placement has been changed from `"bottom"` to `"auto"`. 72 | - Code and notebook formatting with `black` and `isort`. 73 | - Switched to `hatchling` for the build process, `tbump` for versioning, and migrated to 74 | using only the `pyproject.toml` file. 75 | - Refactored tests to use Pytest's `contest.py` file. 76 | 77 | ### Fixed 78 | - CSV export when sorting the grid was not using the selected molecules. 79 | 80 | --- 81 | 82 | ## [1.1.0] - 2022/12/24 83 | 84 | ### Added 85 | - Predefined JavaScript callbacks in the `mols2grid.callbacks` module. Those can be 86 | extensively configured: 87 | - `info`: displays a bigger image alongside some common descriptors for the molecule 88 | - `show_3d`: displays the molecule in 3D 89 | - `external_link`: opens a new tab. By default, opens [Leruli.com](https://leruli.com/) 90 | using the SMILES of the molecule. 91 | - Support for `tuple` of molecules in `display` and `save`. 92 | 93 | ### Changed 94 | - The `"click"` event is now automatically removed from `tooltip_trigger` when 95 | specifying a callback. 96 | 97 | ### Fixed 98 | - Text searches containing any of the following regex characters `-[]{}()*+?.,\^$|#` 99 | would automatically return an empty grid, preventing searching for CAS numbers and any 100 | other identifier or text containing the above characters. This has been temporarily 101 | patched until a proper fix is released in the underlying `list.js` library. 102 | - The link to the KNIME component on the corresponding badges has been fixed. 103 | 104 | 105 | ## [1.0.0] - 2022/09/04 106 | ### Added 107 | - Notebooks running in VSCode and Jupyter Lab now support accessing selections from 108 | Python, executing Python callback functions, and filtering based on other widgets. 109 | ### Changed 110 | - Python callbacks can now also be `lambda` functions. 111 | - If ``prerender=True``, substructure highlighting will be automatically disabled by 112 | default instead of raising an error. 113 | - When exporting a selection to a SMILES file through the GUI, the output no longer 114 | contains a header. 115 | - Relies on a custom ipywidget to handle communication between the front-end/Javascript 116 | and the back-end/Python. 117 | - When calling `grid.filter` and other filtering methods, mols2grid will now use the 118 | filtering code based on ipywidgets, except for Streamlit where it will use the older 119 | JavaScript version of the code to maintain compatibility. 120 | ### Fixed 121 | - Automatically fitting to the content's height in Streamlit. 122 | ### Removed 123 | - `mapping` argument for renaming fields, replaced by `rename` in `v0.1.0`. 124 | - `mols2grid.selection`, replaced by `mols2grid.get_selection()` in `v0.1.0`. 125 | 126 | 127 | ## [0.2.4] - 2022/05/29 128 | ### Fixed 129 | - Calling `MolGrid.get_selection()` when 2 grids with different names are present should 130 | now display the selection of the grid itself, and not the selection corresponding to 131 | indices of the grid that was last interacted with. 132 | 133 | 134 | ## [0.2.3] - 2022/05/10 135 | ### Fixed 136 | - Doing a substructure search on molecules with explicit hydrogens should now highlight 137 | the correct atoms. 138 | 139 | 140 | ## [0.2.2] - 2022/04/04 141 | ### Added 142 | - A proper documentation page with tutorials can now be accessed online. 143 | - Added a `single_highlight=False` parameter to only highlight a single match per 144 | molecule in substructure queries. 145 | - Added a *"Check matching"* button that only selects items that match the current search 146 | and/or filters. 147 | - Added `custom_css`, `custom_header` and `sort_by` to the "table" template 148 | ### Changed 149 | - Compounds matching a substructure search are now aligned to the query molecule before 150 | rendering the image. 151 | - When doing a substructure search, all matches are now highlighted by default. To only 152 | show a single one, use `single_highlight=True`. 153 | - The *Check all*, *Uncheck all* and *Invert* selection buttons have been fixed. They now 154 | actually check/uncheck ALL items, and not just the ones matching the current search. A 155 | *Check matching* button has been added to reproduce the old behaviour. 156 | - If both `subset` and `tooltip` are `None`, the index and image will be directly 157 | displayed on the grid while the remaining fields will be in the tooltip. This makes the 158 | default representation much more readable. 159 | - The default number of columns is now 5 for `template="table"` (same as the other default 160 | template) 161 | ### Fixed 162 | - `template="table"` now correctly displays images when `prerender=True` (Issue #27) 163 | - Displaying the grid with `template="table"` in a notebook now automatically fits to the 164 | content of the table. 165 | 166 | 167 | ## [0.2.1] - 2022/02/23 168 | ### Fixed 169 | - Field names containing spaces are now correctly delt with 170 | - The text search now looks for matches inside the values of the tooltip fields, rather 171 | than inside the HTML code of the tooltip which included tags and other irrelevant text 172 | - Fixed an encoding bug when saving the grid as an HTML file on French Windows, which uses 173 | CP-1252 encoding instead of UTF-8 174 | 175 | 176 | ## [0.2.0] - 2022/02/10 177 | ### Added 178 | - `cache_selection=True` allows to retrieve the checkbox state when re-displaying a grid, 179 | as long as they have the same name. Fixes #22 180 | - `prerender=False` moves the rendering of molecule images from Python to the browser and 181 | only when the molecule is on the current page, giving a performance boost and allowing 182 | to process much larger files. Fixes #17 183 | - `substruct_highlight=True` highlight the atoms that matched the substructure query when 184 | using the SMARTS search (only available when `prerender=False`). Fixes #18 185 | - Added CSV save option. Exports all the data present in `subset` and `tooltip` for the 186 | current selection 187 | - Support for `.sdf.gz` files 188 | - Added automated tests of the interface, which should prevent future updates from 189 | breaking things 190 | ### Changed 191 | - Python 3.6 is no longer supported 192 | - Molecule images are now generated by the web browser (see `prerender=False` argument) 193 | - The coordinates of the input file are now ignored by default (`use_coords=False`). This 194 | change was made to comply with generating images from SMILES string with the browser by 195 | default. 196 | - Python callbacks are now automatically registered in Google Colab 197 | - Javascript callbacks can access RDKit as either `RDKit` or `RDKitModule` 198 | - The "img" field is now available from the callback data 199 | - The `subset` parameter now throws an error if "img" is not present 200 | - Clicking "Check all"/"Uncheck all" should now be faster 201 | - Bumped RDKit JS version to `2021.9.4` to better support moldrawoptions 202 | - Installation now requires `jinja2>=2.11.0` to prevent an error when given a pathlib.Path 203 | object instead of a string 204 | ### Fixed 205 | - Callbacks now work when `selection=False`. Fixes: Issue #22 206 | - Using both `transform` and `style` should now display the labels as expected in the 207 | tooltip 208 | - Fixed a race condition when clicking checkboxes on different grids 209 | - Fixed the `gap` argument not being properly taken into account 210 | - Automatic resizing of the iframe (used in `mols2Grid.display`) should now work even 211 | better 212 | 213 | 214 | ## [0.1.0] - 2021/10/11 215 | ### Added 216 | - The grid can be filtered using pandas DataFrame's `query` and `loc` logic (mostly 217 | useful to combine with ipywidgets) with `MolGrid.filter_by_index` and `MolGrid.filter`. 218 | - Selections can now be modified (select and unselect all, or invert) and exported (to 219 | clipboard or a SMILES file) even without a notebook kernel. Fixes: Issue #16. 220 | - The grid can be sorted according to the selection status and to values in the tooltips. 221 | - Added tracking the selection in multiple grids at the same time (i.e. it's not a 222 | global object that get's overwritten anymore). 223 | - Added support for executing custom JavaScript code or Python function when clicking on 224 | a molecule's image through the `callback` argument. 225 | - Added the `mols2grid.make_popup_callback` helper function to create a popup-like window 226 | as a JavaScript callback. 227 | - Added styling for the whole cell through `style={"__all__": userfunction}`. 228 | - Added `mols2grid.get_selection()` allowing users to specify which grid selection should 229 | be returned. Without argument, the most recently updated grid is returned. 230 | - Added `mols2grid.list_grids()` to return a list of grid names available. 231 | - Added the `mols2grid.sdf_to_dataframe` function to easily convert an SDF file to a 232 | pandas DataFrame. 233 | - Added the `custom_css` argument to pass custom CSS for the HTML document. 234 | - Added the `sort_by` argument to change how the grid elements are ordered 235 | ### Changed 236 | - The functions in `style` and `transform` are now also applied to tooltips. 237 | - The sizing of the iframe displaying the grid is now fully automated and more precise. 238 | - Reorganized the code to separate the JS, CSS and HTML templates. 239 | ### Fixed 240 | - Fixed `mols2grid.save` that returned an error about missing the `output` argument. 241 | - The tooltip is now compatible with the "focus" mode: `tooltip_trigger="focus"`. 242 | - Fixed rendering SVG images in tooltips. 243 | ### Deprecated 244 | - Deprecated `mols2grid.selection` in favor of `mols2grid.get_selection()`. 245 | - Deprecated `mapping` in favor of `rename` in the MolGrid class and `mols2grid.display`. 246 | 247 | 248 | ## [0.0.6] - 2021/05/14 249 | ### Changed 250 | - Javascript module for RDKit is now sourced from `unpkg.com` and pinned to `v2021.3.2` 251 | 252 | 253 | ## [0.0.5] - 2021/04/08 254 | ### Added 255 | - New `transform` parameter that accepts a dictionary of field-function items where each 256 | function transforms the input value that will be displayed. Fixes: Issue #10 257 | ### Fixed 258 | - Running mols2grid could throw an ImportError (instead of ModuleNotFoundError) if the 259 | `google` module was installed, but not `google.colab`. Solved by PR #11 260 | - Private molecule properties (i.e properties starting with `_`) were not registered when 261 | reading properties from RDKit molecules (SDF or list of mols). 262 | 263 | 264 | ## [0.0.4] - 2021/04/01 265 | ### Changed 266 | - The demo notebook can now be run on Google Colab 267 | ### Fixed 268 | - DataFrames with `NaN` values would previously lead to an **empty grid** as `NaN` were 269 | converted to `nan` (not recognized by JS) instead of `NaN`. 270 | - Selection of molecules in Google Colab now works as expected. 271 | - Saved documents are now displayed properly 272 | 273 | 274 | ## [0.0.3] - 2021/03/31 275 | ### Added 276 | - **SMARTS search**: the "🔎" button now lets users choose between a text search or a 277 | SMARTS search. This relies on RDKit's "MinimalLib" JS wrapper which is still in beta, 278 | and will likely break at some point. Use at your own risk! 279 | - **Sorting**: added a "Sort by" button that lets users choose in which order the 280 | molecules should be listed. Default: by index. Fixes: Issue #7 281 | - `MolDrawOptions` **drawing** parameter: this will allow further customization of the 282 | drawing options. 283 | - **Selection**: added checkboxes to each cell. Clicking on a checkbox will add the 284 | molecule's corresponding index and SMILES to the `mols2grid.selection` dictionary. 285 | - New **input** formats: dict and record (list of dicts) are automatically converted to a 286 | pandas DataFrame when used as input to the MolGrid class. The `mols2grid.display` 287 | function only accepts the dict option (since the list format is already used for lists 288 | of RDKit molecules). 289 | - New **input** options: `mol_col` parameter. Adds the ability to directly use an RDKit 290 | mol instead of relying on a SMILES intermediate. This makes using the 2D coordinates of 291 | the input mol a possibility, instead of systematically generating new ones. It also 292 | allows for adding annotations and highlights on drawings. Introduces 2 new parameters: 293 | - `mol_col=None`: Column of the dataframe containing RDKit molecules 294 | - `use_coords=True`: directly use the coordinates from each molecule, or generate new 295 | ones 296 | ### Changed 297 | - The "mols2grid-id" now keeps track of molecules that could not be read by RDKit. This 298 | makes relying on the index of the corresponding entry more reliable. 299 | ### Fixed 300 | - The "mols2grid-id" field is now correctly set in the internal `DataFrame` and the 301 | JavaScript `List`. 302 | - Using the search bar will now only search inside the fields listed in `subset` and 303 | `tooltip` and exclude the `img` field. 304 | - When using the `display` function, the `height` of the iframe is now automatically set 305 | based on the different parameters, instead of a fixed 600px height. Fixes: Issue #6 306 | 307 | 308 | ## [0.0.2] - 2021/03/23 309 | - First release 310 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: mols2grid - Interactive molecule viewer for 2D structures 6 | message: Please cite this software using these metadata. 7 | type: software 8 | authors: 9 | - given-names: Cédric 10 | family-names: Bouysset 11 | orcid: 'https://orcid.org/0000-0002-7814-8158' 12 | doi: 10.5281/zenodo.6591473 13 | license: Apache-2.0 14 | repository-code: https://github.com/cbouy/mols2grid 15 | date-released: "2021-03-23" 16 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | This is a short guide to setup a dev environment for mols2grid. 2 | 3 | 1. Install conda or mamba 4 | 2. Create a new environment. Python 3.7+ (prefer 3.8): 5 | ``` 6 | conda env create --name mols2grid --file docs/environment.yml 7 | ``` 8 | 3. Install all the package dependencies in editable mode: 9 | ``` 10 | pip install -e .[dev] 11 | ``` 12 | 13 | To run tests locally: 14 | - Install Firefox (needed for UI testing) 15 | - Test your installation: 16 | ``` 17 | pytest tests/ 18 | ``` 19 | - You can select/skip the UI testing by specifying the `webdriver` mark in the pytest 20 | command: `-m webdriver` to select UI tests only, or `-m "not webdriver"` to skip them. 21 | 22 | We use `black` and `isort` for formatting so either install the corresponding extension 23 | from your IDE or install the package with `pip install black isort`. The configuration 24 | is done inside the `pyproject.toml` file. 25 | 26 | Making a pull request will automatically run the tests and documentation build for you. 27 | Don't forget to update the `CHANGELOG.md` file with your changes. 28 | 29 | For versioning, you'll have to update both `package.json` and `mols2grid/_version.py` 30 | files. 31 | 32 | The build and deployment process is run automatically when making a release on 33 | GitHub. 34 | To make a prerelease, bump the versions accordingly (`X.Y.Z-rc1` format) and run 35 | the `build` GitHub Action manually. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![mols2grid logo](https://user-images.githubusercontent.com/27850535/154588465-43dc5d82-ee2d-4178-a2f3-e06000bc87c9.png) mols2grid 2 | 3 | [![Pypi version](https://img.shields.io/pypi/v/mols2grid.svg)](https://pypi.python.org/pypi/mols2grid) 4 | [![Conda version](https://img.shields.io/conda/vn/conda-forge/mols2grid)](https://anaconda.org/conda-forge/mols2grid) 5 | 6 | [![Tests status](https://github.com/cbouy/mols2grid/workflows/CI/badge.svg)](https://github.com/cbouy/mols2grid/actions/workflows/ci.yml) 7 | [![Build status](https://github.com/cbouy/mols2grid/workflows/build/badge.svg)](https://github.com/cbouy/mols2grid/actions/workflows/build.yml) 8 | [![Documentation Status](https://readthedocs.org/projects/mols2grid/badge/?version=latest)](https://mols2grid.readthedocs.io/en/latest/?badge=latest) 9 | [![Code coverage](https://codecov.io/gh/cbouy/mols2grid/branch/master/graph/badge.svg?token=QDI1XQSDUC)](https://codecov.io/gh/cbouy/mols2grid) 10 | 11 | [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/) 12 | [![Knime Hub](https://img.shields.io/badge/Available%20on-KNIME-ffd500.svg?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/Public/latest/Interactive%20Grid%20of%20Molecules) 13 | 14 | **mols2grid** is an interactive molecule viewer for 2D structures, based on RDKit. 15 | 16 | ![Demo image](https://raw.githubusercontent.com/cbouy/mols2grid/master/docs/_static/demo.png) 17 | 18 | ## [Documentation](https://mols2grid.readthedocs.io/en/latest/contents.html) 19 | 20 | Installation, basic usage, links to resources...*etc.* 21 | 22 | ## [Notebooks](https://mols2grid.readthedocs.io/en/latest/notebooks/quickstart.html) 23 | 24 | Showcases examples of most features, with links to run the notebooks on Google Colab 25 | 26 | ## [API Reference](https://mols2grid.readthedocs.io/en/latest/api/simple.html) 27 | 28 | The manual, in all its glory 29 | 30 | ## [Discussions](https://github.com/cbouy/mols2grid/discussions) 31 | 32 | Feature requests or questions on how to use should be posted here 33 | 34 | ## [Issues](https://github.com/cbouy/mols2grid/issues) 35 | 36 | Bug tracker 🐞 37 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceMap: 'inline', 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 85% 6 | patch: 7 | default: 8 | target: 0% 9 | comment: 10 | layout: "diff, sunburst, reach, files" 11 | behavior: default 12 | require_changes: false 13 | require_base: yes 14 | require_head: yes 15 | -------------------------------------------------------------------------------- /css/widget.css: -------------------------------------------------------------------------------- 1 | .mols2grid-widget { 2 | padding: 0; 3 | margin: 0; 4 | width: 0; 5 | height: 0; 6 | max-width: 0; 7 | max-height: 0; 8 | } 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 991px !important; 3 | } -------------------------------------------------------------------------------- /docs/_static/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbouy/mols2grid/d156fc940c3db929409f5ea706b8f598d3eefc13/docs/_static/demo.png -------------------------------------------------------------------------------- /docs/_static/mols2grid_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbouy/mols2grid/d156fc940c3db929409f5ea706b8f598d3eefc13/docs/_static/mols2grid_logo.png -------------------------------------------------------------------------------- /docs/api/callbacks.rst: -------------------------------------------------------------------------------- 1 | Callbacks 2 | ========= 3 | 4 | A collection of functions to easily create JavaScript callbacks. 5 | 6 | .. automodule:: mols2grid.callbacks 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/api/molgrid.rst: -------------------------------------------------------------------------------- 1 | MolGrid object 2 | ============== 3 | 4 | The class that creates and controls the grid of molecules. Allows for a more 5 | advanced usage, like filtering the grid using external controls such as 6 | slider widgets. See the notebooks for examples. 7 | 8 | .. autoclass:: mols2grid.MolGrid 9 | :members: -------------------------------------------------------------------------------- /docs/api/simple.rst: -------------------------------------------------------------------------------- 1 | Simple usage 2 | ============ 3 | 4 | The most straightforward way to use mols2grid! For a more advanced usage, see :class:`~mols2grid.MolGrid`. 5 | 6 | .. autofunction:: mols2grid.display 7 | 8 | .. autofunction:: mols2grid.save 9 | 10 | .. autofunction:: mols2grid.get_selection 11 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | .. autofunction:: mols2grid.sdf_to_dataframe 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath('.')) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'mols2grid' 21 | copyright = '2022, Cédric Bouysset' 22 | author = 'Cédric Bouysset' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | github_doc_root = 'https://github.com/cbouy/mols2grid/tree/master/docs/' 28 | needs_sphinx = '4.5.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 36 | 'sphinx.ext.autosectionlabel', 37 | 'sphinx_rtd_theme', 38 | 'sphinx_mdinclude', 39 | 'nbsphinx', 40 | ] 41 | 42 | autosectionlabel_prefix_document = True 43 | napoleon_google_docstring = False 44 | 45 | source_suffix = ['.rst', '.md'] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = [] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 54 | 55 | 56 | # -- Options for HTML output ------------------------------------------------- 57 | 58 | # The theme to use for HTML and HTML Help pages. See the documentation for 59 | # a list of builtin themes. 60 | # 61 | html_theme = 'sphinx_rtd_theme' 62 | pygments_style = "manni" 63 | html_logo = "_static/mols2grid_logo.png" 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = ["_static"] 69 | 70 | ### 71 | 72 | intersphinx_mapping = {'https://docs.python.org/3/': None, 73 | 'https://numpy.org/doc/stable/': None, 74 | 'https://www.rdkit.org/docs/': None, 75 | 'https://pandas.pydata.org/docs/': None, 76 | 'https://ipython.readthedocs.io/en/stable/': None, 77 | } 78 | 79 | # app setup hook 80 | def setup(app): 81 | # custom css 82 | app.add_css_file('custom.css') 83 | -------------------------------------------------------------------------------- /docs/contents.md: -------------------------------------------------------------------------------- 1 | # 💡 Introduction 2 | 3 | - [![Pypi version](https://img.shields.io/pypi/v/mols2grid.svg)](https://pypi.python.org/pypi/mols2grid) 4 | [![Conda version](https://img.shields.io/conda/vn/conda-forge/mols2grid)](https://anaconda.org/conda-forge/mols2grid) 5 | 6 | - [![Tests status](https://github.com/cbouy/mols2grid/workflows/CI/badge.svg)](https://github.com/cbouy/mols2grid/actions/workflows/ci.yml) 7 | [![Build status](https://github.com/cbouy/mols2grid/workflows/build/badge.svg)](https://github.com/cbouy/mols2grid/actions/workflows/build.yml) 8 | [![Documentation Status](https://readthedocs.org/projects/mols2grid/badge/?version=latest)](https://mols2grid.readthedocs.io/en/latest/?badge=latest) 9 | [![Code coverage](https://codecov.io/gh/cbouy/mols2grid/branch/master/graph/badge.svg?token=QDI1XQSDUC)](https://codecov.io/gh/cbouy/mols2grid) 10 | 11 | - [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/) 12 | [![Knime Hub](https://img.shields.io/badge/Available%20on-KNIME-ffd500.svg?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/Public/latest/Interactive%20Grid%20of%20Molecules) 13 | 14 | **mols2grid** is an interactive molecule viewer for 2D structures, based on RDKit. 15 | 16 | ![Demo showing mols2grid's integration in a Jupyter notebook](_static/demo.png) 17 | 18 | # 🐍 Installation 19 | --- 20 | 21 | mols2grid was developped for Python 3.7+ and requires rdkit (>=2020.03.1), pandas and jinja2 as dependencies. 22 | The easiest way to install it is from conda: 23 | ```shell 24 | conda install -c conda-forge mols2grid 25 | ``` 26 | 27 | Alternatively, you can also use pip: 28 | ```shell 29 | pip install rdkit mols2grid 30 | ``` 31 | 32 | If you notice that the selections, callbacks and interactive filtering aren't working as intended, you may have to manually activate the extension: 33 | - for Jupyter Lab: `jupyter labextension install mols2grid` 34 | - for Jupyter Notebook: `jupyter nbextension install mols2grid` 35 | 36 | **Compatibility** 37 | 38 | mols2grid is mainly meant to be used in notebooks (Jupyter notebooks, Jupyter Lab, and Google Colab) but it can also be used as a standalone HTML page opened with your favorite web browser, or embedded in a Streamlit app. 39 | 40 | Since Streamlit doesn't seem to support ipywidgets yet, some features aren't functional: retrieving the selection from Python (you can still export it from the GUI) and using Python callbacks. 41 | 42 | knime logo 43 |

You can also use mols2grid directly in KNIME, by looking for the `Interactive Grid of Molecules` component on the Knime HUB.
44 | Make sure you have setup Knime's Python integration for the node to work.

45 | 46 | 47 | # 📜 Usage 48 | --- 49 | 50 | You can display a grid from: 51 | 52 | - an SDFile 53 | 54 | ```python 55 | import mols2grid 56 | 57 | mols2grid.display("path/to/molecules.sdf") 58 | ``` 59 | 60 | - a `pandas.DataFrame`: 61 | 62 | ```python 63 | mols2grid.display(df, smiles_col="Smiles") 64 | ``` 65 | 66 | - a list of RDKit molecules: 67 | 68 | ```python 69 | mols2grid.display(mols) 70 | ``` 71 | 72 | Please head to the [notebooks](notebooks/quickstart.html) and [API reference](api/simple.html) sections for a complete overview of the features available. 73 | 74 | # 🚀 Resources 75 | --- 76 | * [Simple example](https://iwatobipen.wordpress.com/2021/06/13/draw-molecules-on-jupyter-notebook-rdkit-mols2grid/) by iwatobipen 77 | * Creating a web app with Streamlit for filtering datasets: 78 | * [Blog post](https://blog.reverielabs.com/building-web-applications-from-python-scripts-with-streamlit/) by Justin Chavez 79 | * [Video tutorial](https://www.youtube.com/watch?v=0rqIwSeUImo) by Data Professor 80 | * [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 81 | * [Advanced notebook (RDKit UGM 2021)](https://colab.research.google.com/github/rdkit/UGM_2021/blob/main/Notebooks/Bouysset_mols2grid.ipynb) 82 | 83 | Feel free to open a pull request if you'd like your snippets to be added to this list! 84 | 85 | # 👏 Acknowledgments 86 | --- 87 | * [@themoenen](https://github.com/themoenen) (contributor) 88 | * [@fredrikw](https://github.com/fredrikw) (contributor) 89 | * [@JustinChavez](https://github.com/JustinChavez) (contributor) 90 | * [@hadim](https://github.com/hadim) (conda feedstock maintainer) 91 | 92 | # 🎓 Citing 93 | --- 94 | You can refer to mols2grid in your research by using the following DOI: 95 | 96 | [![DOI:10.5281/zenodo.6591473](https://zenodo.org/badge/348814588.svg)](https://zenodo.org/badge/latestdoi/348814588) 97 | 98 | # ⚖ License 99 | --- 100 | 101 | Unless otherwise noted, all files in this directory and all subdirectories are distributed under the Apache License, Version 2.0: 102 | ```text 103 | Copyright 2021-2022 Cédric BOUYSSET 104 | 105 | Licensed under the Apache License, Version 2.0 (the "License"); 106 | you may not use this file except in compliance with the License. 107 | You may obtain a copy of the License at 108 | 109 | http://www.apache.org/licenses/LICENSE-2.0 110 | 111 | Unless required by applicable law or agreed to in writing, software 112 | distributed under the License is distributed on an "AS IS" BASIS, 113 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 114 | See the License for the specific language governing permissions and 115 | limitations under the License. 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: mols2grid 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - ipykernel==5.4.3 7 | - ipywidgets==7.6.3 8 | - notebook<7.0.0 9 | - mistune<3.0.0 10 | - jedi==0.17.2 11 | - jinja2==3.0.3 12 | - nbsphinx==0.8.8 13 | - nodejs==17.9.0 14 | - pandas==1.2.1 15 | - rdkit==2022.03.1 16 | - recommonmark==0.7.1 17 | - sphinx==4.5.0 18 | - yarn==1.22.18 19 | - pip: 20 | - py3dmol==1.7.0 21 | - sphinx-rtd-theme==1.0.0 22 | - sphinx-mdinclude==0.5.2 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mols2grid documentation master file, created by 2 | sphinx-quickstart on Fri Apr 1 19:31:58 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to mols2grid's documentation! 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents 12 | 13 | contents 14 | 15 | .. toctree:: 16 | :maxdepth: 3 17 | :caption: Notebooks 18 | 19 | notebooks/quickstart 20 | notebooks/customization 21 | notebooks/filtering 22 | notebooks/callbacks 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: API Reference 27 | 28 | api/simple 29 | api/molgrid 30 | api/callbacks 31 | api/utils 32 | 33 | 34 | Indices and tables 35 | ================== 36 | 37 | * :ref:`genindex` 38 | * :ref:`modindex` 39 | * :ref:`search` 40 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/notebooks/callbacks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Callbacks\n", 8 | "\n", 9 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/cbouy/mols2grid/blob/master/docs/notebooks/callbacks.ipynb)\n", 10 | "\n", 11 | "Callbacks are **functions that are executed when you click on a cell's callback button**. They can be written in *JavaScript* or *Python*.\n", 12 | "\n", 13 | "This functionality can be used to display some additional information on the molecule or run some more complex code such as database queries, docking or machine-learning predictions." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "# uncomment and run if you're on Google Colab\n", 23 | "# !pip install rdkit mols2grid py3Dmol\n", 24 | "# !wget https://raw.githubusercontent.com/rdkit/rdkit/master/Docs/Book/data/solubility.test.sdf" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "import urllib.parse\n", 34 | "import urllib.request\n", 35 | "from pathlib import Path\n", 36 | "from urllib.error import HTTPError\n", 37 | "\n", 38 | "import py3Dmol\n", 39 | "from IPython.display import display\n", 40 | "from ipywidgets import widgets\n", 41 | "from rdkit import RDConfig\n", 42 | "\n", 43 | "import mols2grid\n", 44 | "\n", 45 | "\n", 46 | "SDF_FILE = (\n", 47 | " f\"{RDConfig.RDDocsDir}/Book/data/solubility.test.sdf\"\n", 48 | " if Path(RDConfig.RDDocsDir).is_dir()\n", 49 | " else \"solubility.test.sdf\"\n", 50 | ")\n", 51 | "# Optional: read SDF file and sample 50 mols from it (to keep this notebook light)\n", 52 | "df = mols2grid.sdf_to_dataframe(SDF_FILE).sample(50, random_state=0xac1d1c)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## Python\n", 60 | "\n", 61 | "*Note: if you are reading this from the documentation web page, clicking on the images will not trigger anything. Try running the notebook on Google Colab instead (see link at the top of the page).*\n", 62 | "\n", 63 | "For Python callbacks, you need to declare a function that takes a dictionnary as first argument. This dictionnary contains all the data related to the molecule you've just clicked on. All the data fields are **parsed as strings**, except for the index, \"mols2grid-id\", which is always parsed as an integer.\n", 64 | "\n", 65 | "For example, the SMILES of the molecule will be available as `data[\"SMILES\"]`. If the field contains spaces, they will be converted to hyphens, *i.e.* a field called `mol weight` will be available as `data[\"mol-weight\"]`.\n", 66 | "\n", 67 | "Also, using print or any other \"output\" functions inside the callback will not display anything by default. You need to use ipywidgets's `Output` widget to capture what the function is trying to display, and then show it.\n", 68 | "\n", 69 | "### Basic print example\n", 70 | "\n", 71 | "In this simple example, we'll just show the content of the data dictionnary." 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "output = widgets.Output()\n", 81 | "\n", 82 | "\n", 83 | "# the Output widget let's us capture the output generated by the callback function\n", 84 | "# its presence is mandatory if you want to print/display some info with your callback\n", 85 | "@output.capture(clear_output=True, wait=True)\n", 86 | "def show_data(data):\n", 87 | " data.pop(\"img\")\n", 88 | " for key, value in data.items():\n", 89 | " print(key, value)\n", 90 | "\n", 91 | "\n", 92 | "view = mols2grid.display(\n", 93 | " df,\n", 94 | " callback=show_data,\n", 95 | ")\n", 96 | "display(view)\n", 97 | "output" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "We can also make more complex operations with callbacks.\n", 105 | "\n", 106 | "### Displaying the 3D structure with py3Dmol\n", 107 | "\n", 108 | "Here, we'll query PubChem for the molecule based on its SMILES, then fetch the 3D structure and display it with py3Dmol." 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "output = widgets.Output()\n", 118 | "\n", 119 | "\n", 120 | "@output.capture(clear_output=True, wait=True)\n", 121 | "def show_3d(data):\n", 122 | " \"\"\"Query PubChem to download the SDFile with 3D coordinates and\n", 123 | " display the molecule with py3Dmol\n", 124 | " \"\"\"\n", 125 | " url = \"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{}/SDF?record_type=3d\"\n", 126 | " smi = urllib.parse.quote(data[\"SMILES\"])\n", 127 | " try:\n", 128 | " response = urllib.request.urlopen(url.format(smi))\n", 129 | " except HTTPError:\n", 130 | " print(f\"Could not find corresponding match on PubChem\")\n", 131 | " print(data[\"SMILES\"])\n", 132 | " else:\n", 133 | " sdf = response.read().decode()\n", 134 | " view = py3Dmol.view(height=300, width=800)\n", 135 | " view.addModel(sdf, \"sdf\")\n", 136 | " view.setStyle({\"stick\": {}})\n", 137 | " view.zoomTo()\n", 138 | " view.show()\n", 139 | "\n", 140 | "\n", 141 | "view = mols2grid.display(\n", 142 | " df,\n", 143 | " callback=show_3d,\n", 144 | ")\n", 145 | "display(view)\n", 146 | "output" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "## JavaScript\n", 154 | "\n", 155 | "We can also write JavaScript callbacks, which have the advantage to be able to run on almost any platform.\n", 156 | "\n", 157 | "JS callbacks don't require to declare a function, and you can directly access and use the `data` object similarly to Python in your callback script.\n", 158 | "\n", 159 | "### Basic JS example" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "js_callback = \"\"\"\n", 169 | "// remove image from data\n", 170 | "delete data[\"img\"];\n", 171 | "// convert data object to text\n", 172 | "txt = JSON.stringify(data);\n", 173 | "// show data in alert window\n", 174 | "alert(txt);\n", 175 | "\"\"\"\n", 176 | "\n", 177 | "mols2grid.display(\n", 178 | " df,\n", 179 | " callback=js_callback,\n", 180 | ")" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": {}, 186 | "source": [ 187 | "To display fancy popup windows on click, a helper function is available: `mols2grid.make_popup_callback`.\n", 188 | "\n", 189 | "It requires a title as well as some html code to format and display the information that you'd like to show. All of the values inside the data object can be inserted in the `title` and `html` arguments using `${data[\"field_name\"]}`. Additionally, you can execute a prerequisite JavaScript snippet to create variables that are then also accessible in the html code.\n", 190 | "\n", 191 | "### Display a popup containing descriptors\n", 192 | "\n", 193 | "In the following exemple, we create an RDKit molecule using the SMILES of the molecule (the SMILES field is always present in the data object, no matter your input when creating the grid).\n", 194 | "\n", 195 | "We then create a larger SVG image of the molecule, and calculate some descriptors.\n", 196 | "\n", 197 | "Finally, we inject these variables inside the HTML code. You can also style the popup window through the style argument." 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "js_callback = mols2grid.make_popup_callback(\n", 207 | " title=\"${data['NAME']}\",\n", 208 | " subtitle=\"${data['SMILES']}\",\n", 209 | " svg=\"${svg}\",\n", 210 | " js=\"\"\"\n", 211 | " var mol = RDKit.get_mol(data[\"SMILES\"]);\n", 212 | " var svg = mol.get_svg(400, 300);\n", 213 | " var desc = JSON.parse(mol.get_descriptors());\n", 214 | " var inchikey = RDKit.get_inchikey_for_inchi(mol.get_inchi());\n", 215 | " mol.delete();\n", 216 | " \"\"\",\n", 217 | " html=\"\"\"\n", 218 | " Molecular weight: ${desc.exactmw}
\n", 219 | " HBond Acceptors: ${desc.NumHBA}
\n", 220 | " HBond Donors: ${desc.NumHBD}
\n", 221 | " TPSA: ${desc.tpsa}
\n", 222 | " ClogP: ${desc.CrippenClogP}
\n", 223 | "
\n", 224 | " InChIKey: ${inchikey}\n", 225 | " \"\"\",\n", 226 | " style=\"border-radius: 10px\",\n", 227 | ")\n", 228 | "\n", 229 | "mols2grid.display(\n", 230 | " df,\n", 231 | " callback=js_callback,\n", 232 | ")" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "This functionality is directly available in `mols2grid` by using the `mols2grid.callbacks.info()` function:\n", 240 | "\n", 241 | "```python\n", 242 | "mols2grid.display(\n", 243 | " df, callback=mols2grid.callbacks.info(),\n", 244 | ")\n", 245 | "```" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "It is possible to load additional JS libraries by passing `custom_header=\"\"` to `mols2grid.display`, and they will then be available in the callback.\n", 253 | "\n", 254 | "### Displaying the 3D structure with 3Dmol.js\n", 255 | "\n", 256 | "In the following example, we query PubChem using the SMILES of the molecule (and Cactus as a fallback, but you can also provide another custom REST API), then fetch the 3D structure in SDF format and display it with 3Dmol.js:" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "mols2grid.display(\n", 266 | " df,\n", 267 | " callback=mols2grid.callbacks.show_3d(),\n", 268 | ")" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "### Opening an external link\n", 276 | "\n", 277 | "There is also a function to open a link based on the data in the molecule: `mols2grid.callbacks.external_link`.\n", 278 | "\n", 279 | "By default, it opens the search window of Leruli using the SMILES string, but the URL and data field used can be configured." 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "mols2grid.display(\n", 289 | " df,\n", 290 | " callback=mols2grid.callbacks.external_link(),\n", 291 | ")" 292 | ] 293 | } 294 | ], 295 | "metadata": { 296 | "interpreter": { 297 | "hash": "634da3a3bbf8fbf1ddb65b0056d578c92f3c569db0da492ea274ae9d304e5b24" 298 | }, 299 | "kernelspec": { 300 | "display_name": "Python 3", 301 | "language": "python", 302 | "name": "python3" 303 | }, 304 | "language_info": { 305 | "codemirror_mode": { 306 | "name": "ipython", 307 | "version": 3 308 | }, 309 | "file_extension": ".py", 310 | "mimetype": "text/x-python", 311 | "name": "python", 312 | "nbconvert_exporter": "python", 313 | "pygments_lexer": "ipython3", 314 | "version": "3.7.12" 315 | } 316 | }, 317 | "nbformat": 4, 318 | "nbformat_minor": 2 319 | } 320 | -------------------------------------------------------------------------------- /docs/notebooks/customization.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Customization\n", 9 | "\n", 10 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/cbouy/mols2grid/blob/master/docs/notebooks/customization.ipynb)\n", 11 | "\n", 12 | "The grid can be customized quite extensively, from the content that is displayed to the look of the grid and images." 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "# uncomment and run if you're on Google Colab\n", 22 | "# !pip install rdkit mols2grid\n", 23 | "# !wget https://raw.githubusercontent.com/rdkit/rdkit/master/Docs/Book/data/solubility.test.sdf" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "from pathlib import Path\n", 33 | "\n", 34 | "from rdkit import RDConfig\n", 35 | "\n", 36 | "import mols2grid\n", 37 | "\n", 38 | "\n", 39 | "SDF_FILE = (\n", 40 | " f\"{RDConfig.RDDocsDir}/Book/data/solubility.test.sdf\"\n", 41 | " if Path(RDConfig.RDDocsDir).is_dir()\n", 42 | " else \"solubility.test.sdf\"\n", 43 | ")" 44 | ] 45 | }, 46 | { 47 | "attachments": {}, 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "To display all the arguments available, type `help(mols2grid.display)`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "mols2grid.display(\n", 61 | " SDF_FILE,\n", 62 | " # rename fields for the output document\n", 63 | " rename={\"SOL\": \"Solubility\", \"SOL_classification\": \"Class\", \"NAME\": \"Name\"},\n", 64 | " # set what's displayed on the grid\n", 65 | " subset=[\"ID\", \"img\", \"Solubility\"],\n", 66 | " # set what's displayed on the hover tooltip\n", 67 | " tooltip=[\"Name\", \"SMILES\", \"Class\", \"Solubility\"],\n", 68 | " # style for the grid labels and tooltips\n", 69 | " style={\n", 70 | " \"Solubility\": lambda x: \"color: red; font-weight: bold;\" if x < -3 else \"\",\n", 71 | " \"__all__\": lambda x: \"background-color: azure;\" if x[\"Solubility\"] > -1 else \"\",\n", 72 | " },\n", 73 | " # change the precision and format (or other transformations)\n", 74 | " transform={\"Solubility\": lambda x: round(x, 2)},\n", 75 | " # sort the grid in a different order by default\n", 76 | " sort_by=\"Name\",\n", 77 | " # molecule drawing parameters\n", 78 | " fixedBondLength=25,\n", 79 | " clearBackground=False,\n", 80 | ")" 81 | ] 82 | }, 83 | { 84 | "attachments": {}, 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "See [rdkit.Chem.Draw.rdMolDraw2D.MolDrawOptions](https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html#rdkit.Chem.Draw.rdMolDraw2D.MolDrawOptions) for the molecule drawing options available.\n", 89 | "\n", 90 | "The grid's look can also be customized to an even greater extent:" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "# some unnecessarily complicated CSS stylesheet 🌈\n", 100 | "# use .m2g-cell to select each grid's cell\n", 101 | "# or .data for every data field\n", 102 | "# or .data- for a specific field\n", 103 | "css_style = \"\"\"\n", 104 | "/* rainbow background */\n", 105 | ".m2g-cell {\n", 106 | " background: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3);\n", 107 | " background-size: 500% 500%;\n", 108 | " -webkit-animation: rainbow 10s ease infinite;\n", 109 | " animation: rainbow 10s ease infinite;\n", 110 | "}\n", 111 | ".m2g-cell:hover {\n", 112 | " border-color: red !important;\n", 113 | "}\n", 114 | "/* rainbow font color */\n", 115 | ".data {\n", 116 | " font-weight: bold;\n", 117 | " -webkit-text-stroke: 1px black;\n", 118 | " background: linear-gradient(to right, #ef5350, #f48fb1, #7e57c2, #2196f3, #26c6da, #43a047, #eeff41, #f9a825, #ff5722);\n", 119 | " -webkit-background-clip: text;\n", 120 | " color: transparent;\n", 121 | "}\n", 122 | "/* background animation */\n", 123 | "@-webkit-keyframes rainbow {\n", 124 | " 0% {background-position: 0% 50%}\n", 125 | " 50% {background-position: 100% 50%}\n", 126 | " 100% {background-position: 0% 50%}\n", 127 | "}\n", 128 | "@keyframes rainbow { \n", 129 | " 0% {background-position: 0% 50%}\n", 130 | " 50% {background-position: 100% 50%}\n", 131 | " 100% {background-position: 0% 50%}\n", 132 | "}\n", 133 | "\"\"\"\n", 134 | "\n", 135 | "mols2grid.display(\n", 136 | " SDF_FILE,\n", 137 | " # RDKit drawing options\n", 138 | " comicMode=True,\n", 139 | " fixedBondLength=20,\n", 140 | " bondLineWidth=1,\n", 141 | " # custom atom colour palette (all white)\n", 142 | " atomColourPalette={z: (1, 1, 1) for z in range(1, 295)},\n", 143 | " # mols2grid options\n", 144 | " subset=[\"NAME\", \"img\"],\n", 145 | " custom_css=css_style,\n", 146 | " fontfamily='\"Comic Sans MS\", \"Comic Sans\", cursive;',\n", 147 | " # image size\n", 148 | " size=(130, 80),\n", 149 | " # number of results per page\n", 150 | " n_items_per_page=20,\n", 151 | " # border around each cell\n", 152 | " border=\"5px ridge cyan\",\n", 153 | " # gap between cells\n", 154 | " gap=3,\n", 155 | " # disable selection\n", 156 | " selection=False,\n", 157 | ")" 158 | ] 159 | } 160 | ], 161 | "metadata": { 162 | "interpreter": { 163 | "hash": "634da3a3bbf8fbf1ddb65b0056d578c92f3c569db0da492ea274ae9d304e5b24" 164 | }, 165 | "kernelspec": { 166 | "display_name": "Python 3", 167 | "language": "python", 168 | "name": "python3" 169 | }, 170 | "language_info": { 171 | "codemirror_mode": { 172 | "name": "ipython", 173 | "version": 3 174 | }, 175 | "file_extension": ".py", 176 | "mimetype": "text/x-python", 177 | "name": "python", 178 | "nbconvert_exporter": "python", 179 | "pygments_lexer": "ipython3", 180 | "version": "3.9.13" 181 | } 182 | }, 183 | "nbformat": 4, 184 | "nbformat_minor": 2 185 | } 186 | -------------------------------------------------------------------------------- /docs/notebooks/filtering.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Filtering\n", 8 | "\n", 9 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/cbouy/mols2grid/blob/master/docs/notebooks/filtering.ipynb)\n", 10 | "\n", 11 | "It's possible to integrate the grid with other widgets to complement the searchbar." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# uncomment and run if you're on Google Colab\n", 21 | "# !pip install rdkit mols2grid\n", 22 | "# !wget https://raw.githubusercontent.com/rdkit/rdkit/master/Docs/Book/data/solubility.test.sdf" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from pathlib import Path\n", 32 | "\n", 33 | "from ipywidgets import interact, widgets\n", 34 | "from rdkit import RDConfig\n", 35 | "from rdkit.Chem import Descriptors\n", 36 | "\n", 37 | "import mols2grid\n", 38 | "\n", 39 | "\n", 40 | "SDF_FILE = (\n", 41 | " f\"{RDConfig.RDDocsDir}/Book/data/solubility.test.sdf\"\n", 42 | " if Path(RDConfig.RDDocsDir).is_dir()\n", 43 | " else \"solubility.test.sdf\"\n", 44 | ")" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "We'll use ipywidgets to add sliders for the molecular weight and the other molecular descriptors, and define a function that queries the internal dataframe using the values in the sliders.\n", 52 | "\n", 53 | "Everytime the sliders are moved, the function is called to filter our grid." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "df = mols2grid.sdf_to_dataframe(SDF_FILE)\n", 63 | "# compute some descriptors\n", 64 | "df[\"MolWt\"] = df[\"mol\"].apply(Descriptors.ExactMolWt)\n", 65 | "df[\"LogP\"] = df[\"mol\"].apply(Descriptors.MolLogP)\n", 66 | "df[\"NumHDonors\"] = df[\"mol\"].apply(Descriptors.NumHDonors)\n", 67 | "df[\"NumHAcceptors\"] = df[\"mol\"].apply(Descriptors.NumHAcceptors)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "grid = mols2grid.MolGrid(\n", 77 | " df,\n", 78 | " size=(120, 100),\n", 79 | " name=\"filters\",\n", 80 | ")\n", 81 | "view = grid.display(n_items_per_page=12)\n", 82 | "\n", 83 | "\n", 84 | "@interact(\n", 85 | " MolWt=widgets.IntRangeSlider(value=[0, 600], min=0, max=600, step=10),\n", 86 | " LogP=widgets.IntRangeSlider(value=[-10, 10], min=-10, max=10, step=1),\n", 87 | " NumHDonors=widgets.IntRangeSlider(value=[0, 20], min=0, max=20, step=1),\n", 88 | " NumHAcceptors=widgets.IntRangeSlider(value=[0, 20], min=0, max=20, step=1),\n", 89 | ")\n", 90 | "def filter_grid(MolWt, LogP, NumHDonors, NumHAcceptors):\n", 91 | " results = grid.dataframe.query(\n", 92 | " \"@MolWt[0] <= MolWt <= @MolWt[1] and \"\n", 93 | " \"@LogP[0] <= LogP <= @LogP[1] and \"\n", 94 | " \"@NumHDonors[0] <= NumHDonors <= @NumHDonors[1] and \"\n", 95 | " \"@NumHAcceptors[0] <= NumHAcceptors <= @NumHAcceptors[1]\"\n", 96 | " )\n", 97 | " return grid.filter_by_index(results.index)\n", 98 | "\n", 99 | "\n", 100 | "view" 101 | ] 102 | } 103 | ], 104 | "metadata": { 105 | "kernelspec": { 106 | "display_name": "Python 3.8.12 ('molgrid')", 107 | "language": "python", 108 | "name": "python3" 109 | }, 110 | "language_info": { 111 | "codemirror_mode": { 112 | "name": "ipython", 113 | "version": 3 114 | }, 115 | "file_extension": ".py", 116 | "mimetype": "text/x-python", 117 | "name": "python", 118 | "nbconvert_exporter": "python", 119 | "pygments_lexer": "ipython3", 120 | "version": "3.8.12" 121 | }, 122 | "orig_nbformat": 4, 123 | "vscode": { 124 | "interpreter": { 125 | "hash": "634da3a3bbf8fbf1ddb65b0056d578c92f3c569db0da492ea274ae9d304e5b24" 126 | } 127 | } 128 | }, 129 | "nbformat": 4, 130 | "nbformat_minor": 2 131 | } 132 | -------------------------------------------------------------------------------- /docs/notebooks/quickstart.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Quickstart\n", 8 | "\n", 9 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/cbouy/mols2grid/blob/master/docs/notebooks/quickstart.ipynb)\n", 10 | "\n", 11 | "The easiest way to use mols2grid is through the `mols2grid.display` function. The input can be a DataFrame, a list of RDKit molecules, or an SDFile." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# uncomment and run if you're on Google Colab\n", 21 | "# !pip install rdkit mols2grid\n", 22 | "# !wget https://raw.githubusercontent.com/rdkit/rdkit/master/Docs/Book/data/solubility.test.sdf" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from pathlib import Path\n", 32 | "\n", 33 | "from rdkit import RDConfig\n", 34 | "\n", 35 | "import mols2grid\n", 36 | "\n", 37 | "\n", 38 | "SDF_FILE = (\n", 39 | " f\"{RDConfig.RDDocsDir}/Book/data/solubility.test.sdf\"\n", 40 | " if Path(RDConfig.RDDocsDir).is_dir()\n", 41 | " else \"solubility.test.sdf\"\n", 42 | ")" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "Let's start with an SDFile (`.sdf` and `.sdf.gz` are both supported):" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "mols2grid.display(SDF_FILE)" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "From this interface, you can:\n", 66 | "\n", 67 | "- Make simple text searches using the searchbar on the top right.\n", 68 | "- Make substructure queries by clicking on `SMARTS` instead of `Text` and typing in the searchbar.\n", 69 | "- Sort molecules by clicking on `Sort` and selecting a field (click the arrows on the right side of the `Sort` dropdown to reverse the order).\n", 70 | "- View metadata by hovering your mouse over the *`i`* button of a cell, you can also press that button to anchor the information.\n", 71 | "- Select a couple of molecules (click on a cell or on a checkbox, or navigate using your keyboard arrows and press the `ENTER` key).\n", 72 | "- Export the selection to a SMILES or CSV file, or directly to the clipboard (this last functionality might be blocked depending on how you are running the notebook). If no selection was made, the entire grid is exported." 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "We can also use a pandas DataFrame as input, containing a column of RDKit molecules (specified using `mol_col=...`) or SMILES strings (specified using `smiles_col=...`):" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "df = mols2grid.sdf_to_dataframe(SDF_FILE)\n", 89 | "subset_df = df.sample(50, random_state=0xac1d1c)\n", 90 | "mols2grid.display(subset_df, mol_col=\"mol\")" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "Finally, we can also use a list of RDKit molecules:" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "mols = subset_df[\"mol\"].to_list()\n", 107 | "mols2grid.display(mols)" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "But the main point of mols2grid is that the widget let's you access your selections from Python afterwards:" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "mols2grid.get_selection()" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "If you were using a DataFrame, you can get the subset corresponding to your selection with:" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "df.iloc[list(mols2grid.get_selection().keys())]" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "Finally, you can save the grid as a standalone HTML document. Simply replace `display` by `save` and add the path to the output file with `output=\"path/to/molecules.html\"`" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "mols2grid.save(mols, output=\"quickstart-grid.html\")" 156 | ] 157 | } 158 | ], 159 | "metadata": { 160 | "interpreter": { 161 | "hash": "634da3a3bbf8fbf1ddb65b0056d578c92f3c569db0da492ea274ae9d304e5b24" 162 | }, 163 | "kernelspec": { 164 | "display_name": "Python 3.8.6 ('molgrid')", 165 | "language": "python", 166 | "name": "python3" 167 | }, 168 | "language_info": { 169 | "codemirror_mode": { 170 | "name": "ipython", 171 | "version": 3 172 | }, 173 | "file_extension": ".py", 174 | "mimetype": "text/x-python", 175 | "name": "python", 176 | "nbconvert_exporter": "python", 177 | "pygments_lexer": "ipython3", 178 | "version": "3.8.17" 179 | }, 180 | "orig_nbformat": 4 181 | }, 182 | "nbformat": 4, 183 | "nbformat_minor": 2 184 | } 185 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "mols2grid", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package mols2grid" 5 | } 6 | -------------------------------------------------------------------------------- /mols2grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "mols2grid/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /mols2grid/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .callbacks import make_popup_callback 3 | from .dispatch import display, save 4 | from .molgrid import MolGrid 5 | from .select import get_selection, list_grids 6 | from .utils import sdf_to_dataframe 7 | from .widget import _jupyter_labextension_paths, _jupyter_nbextension_paths 8 | 9 | try: 10 | from google.colab import output 11 | except ImportError: 12 | pass 13 | else: 14 | output.enable_custom_widget_manager() 15 | -------------------------------------------------------------------------------- /mols2grid/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | -------------------------------------------------------------------------------- /mols2grid/callbacks.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Optional 2 | 3 | from .utils import env 4 | 5 | 6 | class _JSCallback(NamedTuple): 7 | """Class that holds JavaScript code for running a callback function. If an external 8 | library is required for the callback to function correctly, it can be passed in 9 | the optional ``library_src`` as a ``""" 138 | return _JSCallback(code=code, library_src=library) 139 | 140 | 141 | def external_link( 142 | url="https://leruli.com/search/{}/home", 143 | field="SMILES", 144 | url_encode=False, 145 | b64_encode=True, 146 | ) -> _JSCallback: 147 | """Opens an external link using ``url`` as a template string and the value in the 148 | corresponding ``field``. The value can be URL-encoded or base64-encoded if needed. 149 | 150 | Parameters 151 | ---------- 152 | url : str 153 | Template string used to generate the URL that will be opened. 154 | field : str 155 | Field name used to generate the URL that will be opened. The value can be 156 | encoded (see below). 157 | url_encode : bool 158 | Encode the value fetched from the specified field to replace characters that are 159 | not allowed in a URL e.g., spaces become ``%20``. 160 | b64_encode : bool 161 | Base64-encode the value fetched from the field. 162 | 163 | Raises 164 | ------ 165 | ValueError : Both ``url_encode`` and ``b64_encode`` have been specified. 166 | """ 167 | if url_encode and b64_encode: 168 | raise ValueError("Setting both URL and B64 encoding is not supported") 169 | code = env.get_template("js/callbacks/external_link.js").render( 170 | url=url, 171 | field=field, 172 | url_encode=url_encode, 173 | b64_encode=b64_encode, 174 | ) 175 | return _JSCallback(code=code) 176 | -------------------------------------------------------------------------------- /mols2grid/dispatch.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import singledispatch 3 | from pathlib import Path 4 | 5 | from pandas import DataFrame, Series 6 | 7 | from .molgrid import MolGrid 8 | 9 | _SIGNATURE = { 10 | method: dict(inspect.signature(getattr(MolGrid, method)).parameters.items()) 11 | for method in ["render", "to_interactive", "to_static", "display"] 12 | } 13 | for method in ["render", "to_interactive", "to_static", "display"]: 14 | _SIGNATURE[method].pop("self") 15 | if method in ["render", "display"]: 16 | _SIGNATURE[method].pop("kwargs") 17 | 18 | 19 | def _prepare_kwargs(kwargs, kind): 20 | """Separate kwargs for the init and render methods of MolGrid""" 21 | template = kwargs.pop("template", _SIGNATURE["render"]["template"].default) 22 | render_kwargs = { 23 | param: kwargs.pop(param, sig.default) 24 | for param, sig in _SIGNATURE[f"to_{template}"].items() 25 | } 26 | if kind == "display": 27 | render_kwargs.update( 28 | { 29 | param: kwargs.pop(param, sig.default) 30 | for param, sig in _SIGNATURE["display"].items() 31 | } 32 | ) 33 | return template, kwargs, render_kwargs 34 | 35 | 36 | @singledispatch 37 | def display(arg, **kwargs): 38 | """Display molecules on an interactive grid. 39 | 40 | Parameters: Data 41 | ---------------- 42 | arg : pandas.DataFrame, SDF file or list of molecules 43 | The input containing your molecules. 44 | smiles_col : str or None, default="SMILES" 45 | If a pandas dataframe is used, name of the column with SMILES. 46 | mol_col : str or None, default=None 47 | If a pandas dataframe is used, name of the column with RDKit molecules. 48 | If available, coordinates and atom/bonds annotations from this will be 49 | used for depiction. 50 | 51 | Parameters: Display 52 | ------------------- 53 | template : str, default="interactive" 54 | Either ``"interactive"`` or ``"static"``. See ``render()`` for more details. 55 | size : tuple, default=(130, 90) 56 | The size of the drawing canvas. The cell minimum width is set to the 57 | width of the image, so if the cell padding is increased, the image will 58 | be displayed smaller. 59 | useSVG : bool, default=True 60 | Use SVG images instead of PNG. 61 | prerender : bool, default=False 62 | Prerender images for the entire dataset, or generate them on-the-fly. 63 | Prerendering is slow and memory-hungry, but required when ``template="static"`` 64 | or ``useSVG=False``. 65 | subset: list or None, default=None 66 | Columns to be displayed in each cell of the grid. Each column's 67 | value will be displayed from top to bottom in the order provided. 68 | The ``"img"`` and ``"mols2grid-id"`` columns are displayed by default, 69 | however you can still add the ``"img"`` column if you wish to change 70 | the display order. 71 | tooltip : list, None or False, default=None 72 | Columns to be displayed inside the tooltip. When no subset is set, 73 | all columns will be listed in the tooltip by default. Use ``False`` 74 | to hide the tooltip. 75 | tooltip_fmt : str, default="{key}: {value}" 76 | Format string of each key/value pair in the tooltip. 77 | tooltip_trigger : str, default="focus" 78 | Only available for the "static" template. 79 | Sequence of triggers for the tooltip: ``click``, ``hover`` or ``focus`` 80 | tooltip_placement : str, default="auto" 81 | Position of the tooltip: ``auto``, ``top``, ``bottom``, ``left`` or 82 | ``right`` 83 | transform : dict or None, default=None 84 | Functions applied to specific items in all cells. The dict must follow 85 | a ``key: function`` structure where the key must correspond to one of 86 | the columns in ``subset`` or ``tooltip``. The function takes the item's 87 | value as input and transforms it, for example:: 88 | 89 | transform={ 90 | "Solubility": lambda x: f"{x:.2f}", 91 | "Melting point": lambda x: f"MP: {5/9*(x-32):.1f}°C" 92 | } 93 | 94 | These transformations only affect columns in ``subset`` and 95 | ``tooltip``, and do not interfere with ``style``. 96 | sort_by : str or None, default=None 97 | Sort the grid according to the following field (which must be 98 | present in ``subset`` or ``tooltip``). 99 | truncate: bool, default=True/False 100 | Whether to truncate the text in each cell if it's too long. 101 | Defaults to ``True`` for interactive grids, ``False`` for static grid. 102 | n_items_per_page, default=24 103 | Only available for the "interactive" template. 104 | Number of items to display per page. A multiple of 12 is recommended 105 | for optimal display. 106 | n_cols : int, default=5 107 | Only available for the "static" template. 108 | Number of columns in the table. 109 | selection : bool, default=True 110 | Only available for the "interactive" template. 111 | Enables the selection of molecules and displays a checkbox at the 112 | top of each cell. In the context of a Jupyter Notebook, this gives 113 | you access to your selection (index and SMILES) through 114 | :func:`mols2grid.get_selection()` or :meth:`MolGrid.get_selection()`. 115 | In all cases, you can export your selection by clicking on the triple-dot menu. 116 | cache_selection : bool, default=False 117 | Only available for the "interactive" template. 118 | Restores the selection from a previous grid with the same name. 119 | use_iframe : bool, default=False 120 | Whether to use an iframe to display the grid. When the grid is displayed 121 | inside a Jupyter Notebook or JupyterLab, this will default to ``True``. 122 | iframe_width : str, default="100% 123 | Width of the iframe 124 | iframe_height : int or None, default=None 125 | Height of the frame. When set to ``None``, the height is set dynamically 126 | based on the content. 127 | 128 | Parameters: Mols 129 | ---------------- 130 | removeHs : bool, default=False 131 | Remove hydrogen atoms from the drawings. 132 | use_coords : bool, default=False 133 | Use the coordinates of the molecules (only relevant when an SDF file, a 134 | list of molecules or a DataFrame of RDKit molecules were used as input.) 135 | coordGen : bool, default=True 136 | Use the CoordGen library instead of the RDKit one to depict the 137 | molecules in 2D. 138 | MolDrawOptions : rdkit.Chem.Draw.rdMolDraw2D.MolDrawOptions or None, default=None 139 | Drawing options. Useful for making highly customized drawings. 140 | substruct_highlight : bool or None, default=None 141 | Highlight substructure when using the SMARTS search. Active by default 142 | when ``prerender=False``. 143 | single_highlight : bool, default=False 144 | Highlight only the first match of the substructure query. 145 | 146 | Parameters: CSS 147 | --------------- 148 | border : str, default="1px solid #cccccc" 149 | Styling of the border around each cell. 150 | gap : int, default=0 151 | Size in pixels of the gap between cells. 152 | pad : int, default=10 153 | Size in pixels of the cell padding. 154 | fontsize : str, default="12px" 155 | Font size of the text displayed in each cell. 156 | fontfamily : str, default="'DejaVu', sans-serif" 157 | Font used for the text in each cell. 158 | textalign : str, default="center" 159 | Alignment of the text in each cell. 160 | background_color : str, default="white" 161 | Only available for the "interactive" template. 162 | Background color of a cell. 163 | hover_color : str, default="rgba(0,0,0,0.05)" 164 | Only available for the "interactive" template. 165 | Background color when hovering a cell 166 | custom_css : str or None, default=None 167 | Custom CSS properties applied to the generated HTML. Please note that 168 | the CSS will apply to the entire page if no iframe is used (see 169 | ``use_iframe`` for more details). 170 | style : dict or None, default=None 171 | CSS styling applied to each item in a cell. The dict must follow a 172 | ``key: function`` structure where the key must correspond to one of the 173 | columns in ``subset`` or ``tooltip``. The function takes the item's 174 | value as input, and outputs a valid CSS styling. For example, if you 175 | want to color the text corresponding to the "Solubility" column in your 176 | dataframe:: 177 | 178 | style={"Solubility": lambda x: "color: red" if x < -5 else ""} 179 | 180 | You can also style a whole cell using the ``__all__`` key, the 181 | corresponding function then has access to all values for each cell:: 182 | 183 | style={"__all__": lambda x: "color: red" if x["Solubility"] < -5 else ""} 184 | 185 | Parameters: Customization 186 | ------------------------- 187 | name : str, default="default" 188 | Name of the grid. Used when retrieving selections from multiple grids 189 | at the same time 190 | rename : dict or None, default=None 191 | Rename the properties in the final document. 192 | custom_header : str or None, default=None 193 | Custom libraries to be loaded in the header of the document. 194 | callback : str, callable or None, default=None 195 | Only available for the "interactive" template. 196 | JavaScript or Python callback to be executed when clicking on an image. 197 | A dictionnary containing the data for the full cell is directly available 198 | as ``data`` in JS. For Python, the callback function must have ``data`` 199 | as the first argument to the function. All the values in the ``data`` dict 200 | are parsed as strings, except "mols2grid-id" which is always an integer. 201 | Note that fields containing spaces in their name will be replaced by 202 | hyphens, i.e. "mol weight" becomes available as ``data["mol-weight"]``. 203 | 204 | Returns 205 | ------- 206 | view : IPython.core.display.HTML 207 | 208 | Notes 209 | ----- 210 | You can also directly use RDKit's :class:`~rdkit.Chem.Draw.rdMolDraw2D.MolDrawOptions` 211 | parameters as arguments. 212 | Additionally, ``atomColourPalette`` is available to customize the atom 213 | palette if you're not prerendering image (``prerender=False``). 214 | 215 | .. versionadded:: 0.1.0 216 | Added ``sort_by``, ``custom_css``, ``custom_header`` and ``callback`` 217 | arguments. Added the ability to style an entire cell with 218 | ``style={"__all__": }``. 219 | 220 | .. versionadded:: 0.2.0 221 | Added ``substruct_highlight`` argument. 222 | 223 | .. versionchanged:: 0.2.2 224 | If both ``subset`` and ``tooltip`` are ``None``, the index and image 225 | will be directly displayed on the grid while the remaining fields will 226 | be in the tooltip. 227 | 228 | .. versionchanged:: 1.0.0 229 | ``callback`` can now be a *lambda* function. If ``prerender=True``, 230 | substructure highlighting will be automatically disabled if it wasn't 231 | explicitely set to ``True`` instead of raising an error. 232 | 233 | """ 234 | raise TypeError(f"No display method registered for type {type(arg)!r}") 235 | 236 | 237 | @display.register(DataFrame) 238 | @display.register(dict) 239 | def _(df, **kwargs): 240 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "display") 241 | return MolGrid(df, **kwargs).display(template=template, **render_kwargs) 242 | 243 | 244 | @display.register(str) 245 | @display.register(Path) 246 | def _(sdf, **kwargs): 247 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "display") 248 | return MolGrid.from_sdf(sdf, **kwargs).display(template=template, **render_kwargs) 249 | 250 | 251 | @display.register(Series) 252 | @display.register(list) 253 | @display.register(tuple) 254 | def _(mols, **kwargs): 255 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "display") 256 | return MolGrid.from_mols(mols, **kwargs).display(template=template, **render_kwargs) 257 | 258 | 259 | @singledispatch 260 | def save(arg, **kwargs): 261 | """Generate an interactive grid of molecules and save it. 262 | 263 | Parameters 264 | ---------- 265 | arg : pandas.DataFrame, SDF file or list of molecules 266 | The input containing your molecules. 267 | output : str 268 | Name and path of the output document. 269 | 270 | Notes 271 | ----- 272 | See :func:`display` for the full list of arguments. 273 | """ 274 | raise TypeError(f"No save method registered for type {type(arg)!r}") 275 | 276 | 277 | @save.register(DataFrame) 278 | def _(df, **kwargs): 279 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "save") 280 | output = kwargs.pop("output") 281 | return MolGrid(df, **kwargs).save(output, template=template, **render_kwargs) 282 | 283 | 284 | @save.register(str) 285 | @save.register(Path) 286 | def _(sdf, **kwargs): 287 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "save") 288 | output = kwargs.pop("output") 289 | return MolGrid.from_sdf(sdf, **kwargs).save( 290 | output, template=template, **render_kwargs 291 | ) 292 | 293 | 294 | @save.register(Series) 295 | @save.register(list) 296 | @save.register(tuple) 297 | def _(mols, **kwargs): 298 | template, kwargs, render_kwargs = _prepare_kwargs(kwargs, "save") 299 | output = kwargs.pop("output") 300 | return MolGrid.from_mols(mols, **kwargs).save( 301 | output, template=template, **render_kwargs 302 | ) 303 | -------------------------------------------------------------------------------- /mols2grid/nbextension/extension.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // 3 | define(function() { 4 | "use strict"; 5 | 6 | window['requirejs'].config({ 7 | map: { 8 | '*': { 9 | 'mols2grid': 'nbextensions/mols2grid/index', 10 | }, 11 | } 12 | }); 13 | // Export the required load_ipython_extension function 14 | return { 15 | load_ipython_extension : function() {} 16 | }; 17 | }); -------------------------------------------------------------------------------- /mols2grid/select.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from ast import literal_eval 3 | 4 | 5 | class SelectionRegister: 6 | """Register for grid selections 7 | 8 | Attributes 9 | ---------- 10 | SELECTIONS : dict 11 | Stores each grid selection according to their name 12 | current_selection : str 13 | Name of the most recently updated grid 14 | """ 15 | 16 | def __init__(self): 17 | self.SELECTIONS = {} 18 | 19 | def _update_current_grid(self, name): 20 | self.current_selection = name 21 | 22 | def _init_grid(self, name): 23 | overwrite = self.SELECTIONS.get(name, False) 24 | if overwrite: 25 | warnings.warn( 26 | f"Overwriting non-empty {name!r} grid selection: {str(overwrite)}" 27 | ) 28 | self.SELECTIONS[name] = {} 29 | self._update_current_grid(name) 30 | 31 | def selection_updated(self, name, event): 32 | self.SELECTIONS[name] = literal_eval(event.new) 33 | self._update_current_grid(name) 34 | 35 | def get_selection(self, name=None): 36 | """Returns the selection for a specific MolGrid instance 37 | 38 | Parameters 39 | ---------- 40 | name : str or None 41 | Name of the grid to fetch the selection from. If `None`, the most 42 | recently updated grid is returned 43 | """ 44 | name = self.current_selection if name is None else name 45 | return self.SELECTIONS[name] 46 | 47 | def list_grids(self): 48 | """Returns a list of grid names""" 49 | return list(self.SELECTIONS.keys()) 50 | 51 | def _clear(self): 52 | """Clears all selections""" 53 | if hasattr(self, "current_selection"): 54 | del self.current_selection 55 | self.SELECTIONS.clear() 56 | 57 | 58 | register = SelectionRegister() 59 | get_selection = register.get_selection 60 | list_grids = register.list_grids 61 | -------------------------------------------------------------------------------- /mols2grid/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbouy/mols2grid/d156fc940c3db929409f5ea706b8f598d3eefc13/mols2grid/templates/__init__.py -------------------------------------------------------------------------------- /mols2grid/templates/css/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * General styling 3 | */ 4 | body { 5 | font-family: {{ fontfamily }}; 6 | } 7 | h1,h2,h3,h4 { 8 | margin: 0 0 10px 0; 9 | } 10 | h1 { 11 | font-size: 26px; 12 | } 13 | h2 { 14 | font-size: 20px; 15 | font-weight: 400; 16 | } 17 | h3 { 18 | font-size: 16px; 19 | } 20 | p { 21 | margin: 0 0 10px 0; 22 | } 23 | 24 | 25 | /* Remove body margin inside iframe */ 26 | body.m2g-inside-iframe { 27 | margin: 0; 28 | } 29 | 30 | /* In-cell text */ 31 | #mols2grid .data:not(.data-img) { 32 | height: 16px; 33 | line-height: 16px; 34 | } 35 | /* Text truncation */ 36 | #mols2grid .data { 37 | /* Break text into multiple lines (default for static)... */ 38 | word-wrap: {{ 'break-word' if not truncate else 'normal' }}; 39 | 40 | /* ...or truncate it (default for interactive). */ 41 | overflow: {{ 'hidden' if truncate else 'visible' }}; 42 | white-space: {{ 'nowrap' if truncate else 'normal' }}; 43 | text-overflow: {{ 'ellipsis' if truncate else 'clip' }}; 44 | } 45 | 46 | 47 | /** 48 | * Popover 49 | * - - - 50 | * Note: this is a bootstrap variable which is not namespaced. 51 | * To avoid any contamination, we only style it when the 52 | * x-placement parameter is set. 53 | */ 54 | .popover[x-placement] { 55 | font-family: {{ fontfamily }}; 56 | background: white; 57 | border: solid 1px rgba(0,0,0,.2); 58 | font-size: {{ fontsize }}; 59 | padding: 10px; 60 | border-radius: 5px; 61 | box-shadow: 0 0 20px rgba(0,0,0,.15); 62 | user-select: none; 63 | } 64 | .popover[x-placement] h3 { 65 | margin: 0; 66 | } 67 | .popover[x-placement] .arrow { 68 | width: 10px; 69 | height: 10px; 70 | background: #fff; 71 | border: solid 1px rgba(0,0,0,.2); 72 | box-sizing: border-box; 73 | position: absolute; 74 | transform-origin: 5px 5px; 75 | clip-path: polygon(0 0, 100% 0, 100% 100%); 76 | } 77 | .popover[x-placement='left'] .arrow { 78 | transform: rotate(45deg); 79 | top: 50%; 80 | right: -5px; 81 | } 82 | .popover[x-placement='right'] .arrow { 83 | transform: rotate(-135deg); 84 | top: 50%; 85 | left: -5px; 86 | } 87 | .popover[x-placement='top'] .arrow { 88 | transform: rotate(135deg); 89 | left: 50%; 90 | bottom: -5px; 91 | } 92 | .popover[x-placement='bottom'] .arrow { 93 | transform: rotate(-45deg); 94 | left: 50%; 95 | top: -5px; 96 | } -------------------------------------------------------------------------------- /mols2grid/templates/css/static.css: -------------------------------------------------------------------------------- 1 | /* Note: #mols2grid is the table element */ 2 | #mols2grid { 3 | border-collapse: {{ 'collapse' if gap == 0 else 'separate' }}; 4 | border-spacing: {{ gap }}px; 5 | /* Compensate for gap so table stays aligned with content above */ 6 | margin: {{ 0 if gap == 0 else -gap }}px; 7 | } 8 | #mols2grid, tr, td { 9 | border: none; 10 | } 11 | #mols2grid tr { 12 | page-break-inside: avoid !important; 13 | } 14 | 15 | /* Cell */ 16 | #mols2grid td { 17 | border: {{ border }}; 18 | text-align: {{ textalign }}; 19 | vertical-align: top; 20 | max-width: {{ cell_width }}px; 21 | width: {{ cell_width }}px; 22 | font-family: {{ fontfamily }}; 23 | font-size: {{ fontsize }}; 24 | margin: 0; 25 | padding: {{ pad }}px; 26 | position: relative; 27 | } 28 | 29 | /* Call focus state */ 30 | #mols2grid td:focus-within, 31 | #mols2grid td:focus { 32 | outline: solid 2px #555; 33 | border-color: transparent; 34 | } 35 | 36 | /* ID */ 37 | #mols2grid .data-mols2grid-id { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | padding: 5px; 42 | } 43 | 44 | /* Tooltip margin */ 45 | /* Adjusted so it plays well with the extruded outline */ 46 | .popover[x-placement] { 47 | margin: 1px 2px 2px 1px; 48 | } 49 | 50 | 51 | #mols2grid td div img { 52 | max-width: {{ image_width }}px; 53 | width: {{ image_width }}px; 54 | height: auto; 55 | padding: 0; 56 | } -------------------------------------------------------------------------------- /mols2grid/templates/html/common_header.html: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /mols2grid/templates/html/iframe.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /mols2grid/templates/html/interactive_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /mols2grid/templates/interactive.html: -------------------------------------------------------------------------------- 1 | {#

Header 1

2 |

Header 2

3 |

Header 3

4 |

Header 2

5 |

Header 2

6 |

Header 2

#} 7 | 8 | {# Iframe HTML wrapper #} 9 | {% if use_iframe %} 10 | 11 | 12 | 13 | 14 | 15 | Document! 16 | {% endif %} 17 | 18 | 19 | 26 | {% include 'html/interactive_header.html' %} 27 | {% include 'html/common_header.html' %} 28 | 29 | 30 | {{ custom_header }} 31 | 32 | 33 | {# Iframe HTML wrapper #} 34 | {% if use_iframe %} 35 | 36 | 37 | {% endif %} 38 | 39 | 40 |
41 | 42 |
43 | {# Rows are used to collapse functions into two rows on smaller screens #} 44 |
45 | 46 |
    47 |
    48 | 49 | 50 |
    51 | 66 |
    67 |
    68 | {{ 'Index' if default_sort == 'mols2grid-id' else default_sort }} 69 |
    70 |
    71 |
    72 |
    73 | 74 |
    75 | 82 |
    83 |
    Text
    84 |
    SMARTS
    85 |
    86 |
    87 | 88 | 89 |
    90 | 100 |
    101 | 102 | 103 | 104 |
    105 |
    106 |
    107 |
    108 | 109 | 110 | {# item template is duplicated using List in interactive.js #} 111 |
    {{ item }}
    112 |
    113 | 116 | 117 | 118 | {# Iframe HTML wrapper #} 119 | {% if use_iframe %} 120 | 121 | 122 | {% endif %} 123 | -------------------------------------------------------------------------------- /mols2grid/templates/js/callbacks/external_link.js: -------------------------------------------------------------------------------- 1 | const field = {{ field | tojson }}; 2 | let value = data[field]; 3 | let url = {{ url | tojson }}; 4 | 5 | {% if url_encode %} 6 | value = encodeURIComponent(value); 7 | {% elif b64_encode %} 8 | value = window.btoa(unescape(encodeURIComponent(value))); 9 | {% endif %} 10 | 11 | url = url.replace("{}", value) 12 | window.open(url, '_blank').focus(); -------------------------------------------------------------------------------- /mols2grid/templates/js/callbacks/show_3d.js: -------------------------------------------------------------------------------- 1 | // fetch file and display with 3Dmol.js 2 | let resolvers = { 3 | "pubchem": { 4 | "url": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{}/SDF?record_type=3d", 5 | "format": "sdf", 6 | "field": "SMILES", 7 | "encode": true, 8 | }, 9 | "cactus": { 10 | "url": "https://cactus.nci.nih.gov/chemical/structure/{}/file?format=sdf", 11 | "format": "sdf", 12 | "field": "SMILES", 13 | "encode": true, 14 | } 15 | } 16 | function get_url(resolver, data) { 17 | let value = data[resolver.field]; 18 | if (resolver.encode) { 19 | value = encodeURIComponent(value) 20 | } 21 | return resolver.url.replace("{}", value); 22 | } 23 | // fetch and display 24 | function show_3d(data, apis_or_custom_resolver, viewer) { 25 | let resolver, api; 26 | if (Array.isArray(apis_or_custom_resolver)) { 27 | // go through list of APIs available 28 | api = apis_or_custom_resolver.shift(); 29 | if (api === undefined) { 30 | console.error("No API left to query...") 31 | return 32 | } 33 | resolver = resolvers[api]; 34 | } else { 35 | // user passed a custom resolver 36 | api = "custom" 37 | resolver = apis_or_custom_resolver; 38 | } 39 | let sdf_url = get_url(resolver, data); 40 | $.ajax(sdf_url, { 41 | success: function(data) { 42 | viewer.addModel(data, resolver.format); 43 | viewer.setStyle({}, {stick: {}}); 44 | viewer.zoomTo(); 45 | viewer.setHoverable( 46 | {}, 1, 47 | function(atom, viewer, event, container) { 48 | if (!atom.label) { 49 | atom.label = viewer.addLabel( 50 | atom.serial + ':' + atom.atom, { 51 | position: atom, 52 | backgroundColor: 'mintcream', 53 | fontColor:'black' 54 | } 55 | ); 56 | } 57 | }, 58 | function(atom, viewer) { 59 | if (atom.label) { 60 | viewer.removeLabel(atom.label); 61 | delete atom.label; 62 | } 63 | } 64 | ); 65 | viewer.render(); 66 | }, 67 | error: function(hdr, status, err) { 68 | console.error( 69 | "Failed to load SDF with " + api 70 | ); 71 | show_3d(data, apis_or_custom_resolver, viewer); 72 | }, 73 | }); 74 | } 75 | $(document).ready(function() { 76 | // 3Dmol.js options 77 | let element = $('#molviewer'); 78 | let config = { backgroundColor: 'white' }; 79 | let viewer = $3Dmol.createViewer(element, config); 80 | // prepare query to fetch 3D SDF from SMILES 81 | let apis_or_custom_resolver = {{ query | tojson }}; 82 | show_3d(data, apis_or_custom_resolver, viewer); 83 | }); -------------------------------------------------------------------------------- /mols2grid/templates/js/draw_mol.js: -------------------------------------------------------------------------------- 1 | // Generate images for the currently displayed molecules. 2 | RDKit.prefer_coordgen({{ prefer_coordGen | tojson }}); 3 | function draw_mol(smiles, index, template_mol) { 4 | var mol = RDKit.get_mol(smiles, '{"removeHs": {{ removeHs | tojson }} }'); 5 | var svg = ""; 6 | if (mol.is_valid()) { 7 | var highlights = smarts_matches[index]; 8 | if (highlights) { 9 | var details = Object.assign({}, draw_opts, highlights); 10 | details = JSON.stringify(details); 11 | mol.generate_aligned_coords(template_mol, {{ prefer_coordGen | tojson }}); 12 | } else { 13 | var details = json_draw_opts; 14 | } 15 | svg = mol.get_svg_with_highlights(details); 16 | } 17 | mol.delete(); 18 | if (svg == "") { 19 | return ''; 20 | } 21 | return svg; 22 | } 23 | 24 | // Update images when the list is updated. 25 | listObj.on("updated", function (list) { 26 | var query = $('#mols2grid .m2g-searchbar').val(); 27 | var template_mol; 28 | if (query === "") { 29 | smarts_matches = {}; 30 | template_mol = null; 31 | } else { 32 | template_mol = RDKit.get_qmol(query); 33 | template_mol.set_new_coords({{ prefer_coordGen | tojson }}); 34 | } 35 | $('#mols2grid .m2g-cell').each(function() { 36 | var $t = $(this); 37 | var smiles = $t.children(".data-{{ smiles_col }}").first().text(); 38 | var index = parseInt(this.getAttribute("data-mols2grid-id")); 39 | var svg = draw_mol(smiles, index, template_mol); 40 | $t.children(".data-img").html(svg); 41 | }); 42 | if (template_mol) { 43 | template_mol.delete(); 44 | } 45 | }); -------------------------------------------------------------------------------- /mols2grid/templates/js/filter.js: -------------------------------------------------------------------------------- 1 | let name = {{ grid_id | tojson }}; 2 | if (typeof mols2grid_lists !== "undefined") { 3 | var listObj = mols2grid_lists[name]; 4 | } else if (typeof window.parent.mols2grid_lists !== "undefined") { 5 | var listObj = window.parent.mols2grid_lists[name]; 6 | } 7 | if (typeof listObj !== "undefined") { 8 | var mask = {{ mask }}; 9 | listObj.filter(function (item) { 10 | return mask[item.values()["mols2grid-id"]]; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /mols2grid/templates/js/grid_interaction.js: -------------------------------------------------------------------------------- 1 | // Check if selection UI is supported. 2 | var supportSelection = eval('{{selection}}'.toLowerCase()); 3 | 4 | listObj.on("updated", initInteraction); 5 | 6 | // (Re)initialiuze all grid interaction every time the grid changes. 7 | function initInteraction(list) { 8 | initCellClick() 9 | initToolTip() 10 | initKeyboard() 11 | if (supportSelection) initCheckbox() 12 | 13 | 14 | // Hide pagination if there is only one page. 15 | if (listObj.matchingItems.length <= listObj.page) { 16 | $('#mols2grid .m2g-pagination').hide() 17 | } else { 18 | $('#mols2grid .m2g-pagination').show() 19 | } 20 | 21 | // Add a bunch of phantom cells. 22 | // These are used as filler to make sure that 23 | // no grid cells need to be resized when there's 24 | // not enough results to fill the row. 25 | $('#mols2grid .m2g-list').append('
    '.repeat(11)); 26 | } 27 | 28 | // Cell click handler. 29 | function initCellClick() { 30 | $('#mols2grid .m2g-cell').off('click').click(function(e) { 31 | if ($(e.target).hasClass('m2g-info') || $(e.target).is(':checkbox')) { 32 | // Info button / Checkbox --> do nothing. 33 | } else if ($(e.target).is('div') && $(e.target).hasClass('data')) { 34 | // Data string --> copy text. 35 | copyOnClick(e.target) 36 | } else if ($(e.target).hasClass('m2g-callback')) { 37 | // Callback button. 38 | onCallbackButtonClick(e.target) 39 | } else { 40 | // Outside checkbox --> toggle the checkbox. 41 | if (supportSelection) { 42 | var chkbox = $(this).find('input:checkbox')[0] 43 | chkbox.checked = !chkbox.checked 44 | $(chkbox).trigger('change') 45 | } 46 | } 47 | }) 48 | } 49 | 50 | // Store an element's text content in the clipboard. 51 | function copyOnClick(target) { 52 | var text = $(target).text() 53 | navigator.clipboard.writeText(text) 54 | 55 | // Blink the cell to indicate that the text was copied. 56 | $(target).addClass('m2g-copy-blink') 57 | setTimeout(function() { 58 | $(target).removeClass('m2g-copy-blink') 59 | }, 450) 60 | } 61 | 62 | // Keyboard actions. 63 | function initKeyboard() { 64 | // Disable scroll when pressing UP/DOWN arrows 65 | $('#mols2grid .m2g-cell').off('keydown').keydown(function(e) { 66 | if (e.which == 38 || e.which == 40) { 67 | e.preventDefault() 68 | } 69 | }) 70 | 71 | $('#mols2grid .m2g-cell').off('keyup').keyup(function(e) { 72 | var chkbox = $(this).find('input:checkbox')[0] 73 | if (e.which == 13) { 74 | // ENTER: toggle 75 | chkbox.checked = !chkbox.checked 76 | $(chkbox).trigger('change') 77 | } else if (e.which == 27 || e.which == 8) { 78 | // ESC/BACKSPACE: unselect 79 | chkbox.checked = false 80 | $(chkbox).trigger('change') 81 | } else if (e.which == 37) { 82 | // LEFT 83 | $(this).prev().focus() 84 | } else if (e.which == 39) { 85 | // RIGHT 86 | $(this).next().focus() 87 | } else if (e.which == 38 || e.which == 40) { 88 | var containerWidth = $(this).parent().outerWidth() 89 | var cellWidth = $(this).outerWidth() + parseInt($(this).css('marginLeft')) * 2 90 | var columns = Math.round(containerWidth / cellWidth) 91 | var index = $(this).index() 92 | if (e.which == 38) { 93 | // UP 94 | var indexAbove = Math.max(index - columns, 0) 95 | $(this).parent().children().eq(indexAbove).focus() 96 | } else if (e.which == 40) { 97 | // DOWN 98 | var total = $(this).parent().children().length 99 | var indexBelow = Math.min(index + columns, total) 100 | $(this).parent().children().eq(indexBelow).focus() 101 | } 102 | } 103 | }) 104 | } 105 | 106 | // Show tooltip when hovering the info icon. 107 | function initToolTip() { 108 | $('#mols2grid .m2g-info').off('mouseenter').off('mouseleave').off('click').mouseenter(function() { 109 | // Show on enter 110 | $(this).closest('.m2g-cell').find('.m2g-tooltip[data-toggle="popover"]').popover('show') 111 | $('body > .popover').click(function(e) { 112 | if ($(e.target).hasClass('copy-me')) { 113 | copyOnClick(e.target) 114 | } else if ($(e.target).is('button')) { 115 | 116 | } 117 | }) 118 | }).mouseleave(function() { 119 | // Hide on leave, unless sticky. 120 | if (!$(this).closest('.m2g-cell').hasClass('m2g-keep-tooltip')) { 121 | $(this).closest('.m2g-cell').find('.m2g-tooltip[data-toggle="popover"]').popover('hide') 122 | } 123 | }).click(function() { 124 | // Toggle sticky on click. 125 | $(this).closest('.m2g-cell').toggleClass('m2g-keep-tooltip') 126 | 127 | // Hide tooltip when sticky was turned off. 128 | if ($(this).closest('.m2g-cell').hasClass('m2g-keep-tooltip')) { 129 | $(this).closest('.m2g-cell').find('.m2g-tooltip[data-toggle="popover"]').popover('show') 130 | } else if (!$(this).closest('.m2g-cell').hasClass('m2g-keep-tooltip')) { 131 | $(this).closest('.m2g-cell').find('.m2g-tooltip[data-toggle="popover"]').popover('hide') 132 | } 133 | }) 134 | } 135 | 136 | // Update selection on checkbox click. 137 | function initCheckbox() { 138 | $("input:checkbox").off('change').change(function() { 139 | var _id = parseInt($(this).closest(".m2g-cell").attr("data-mols2grid-id")); 140 | if (this.checked) { 141 | var _smiles = $($(this).closest(".m2g-cell").children(".data-{{ smiles_col }}")[0]).text(); 142 | add_selection({{ grid_id | tojson }}, [_id], [_smiles]); 143 | } else { 144 | del_selection({{ grid_id | tojson }}, [_id]); 145 | } 146 | }); 147 | } 148 | 149 | // Callback button 150 | function onCallbackButtonClick(target) { 151 | var data = {} 152 | data["mols2grid-id"] = parseInt($(target).closest(".m2g-cell") 153 | .attr("data-mols2grid-id")); 154 | data["img"] = $(target).parent().siblings(".data-img").eq(0).get(0).innerHTML; 155 | $(target).parent().siblings(".data").not(".data-img").each(function() { 156 | let name = this.className.split(" ") 157 | .filter(cls => cls.startsWith("data-"))[0] 158 | .substring(5); 159 | data[name] = this.innerHTML; 160 | }); 161 | 162 | {% if callback_type == "python" %} 163 | // Trigger custom python callback. 164 | let model = window.parent["_MOLS2GRID_" + {{ grid_id | tojson }}]; 165 | if (model) { 166 | model.set("callback_kwargs", JSON.stringify(data)); 167 | model.save_changes(); 168 | } else { 169 | // No kernel detected for callback. 170 | } 171 | {% else %} 172 | // Call custom js callback. 173 | {{ callback }} 174 | {% endif %} 175 | } 176 | 177 | 178 | 179 | /** 180 | * Actions 181 | */ 182 | 183 | // Listen to action dropdown. 184 | $('#mols2grid .m2g-actions select').change(function(e) { 185 | var val = e.target.value 186 | switch(val) { 187 | case 'select-all': 188 | selectAll() 189 | break 190 | case 'select-matching': 191 | selectMatching() 192 | break 193 | case 'unselect-all': 194 | unselectAll() 195 | break 196 | case 'invert': 197 | invertSelection() 198 | break 199 | case 'copy': 200 | copy() 201 | break 202 | case 'save-smiles': 203 | saveSmiles() 204 | break 205 | case 'save-csv': 206 | saveCSV() 207 | break 208 | } 209 | $(this).val('') // Reset dropdown 210 | }) 211 | 212 | // Check all. 213 | function selectAll(e) { 214 | var _id = []; 215 | var _smiles = []; 216 | listObj.items.forEach(function (item) { 217 | if (item.elm) { 218 | item.elm.getElementsByTagName("input")[0].checked = true; 219 | } else { 220 | item.show() 221 | item.elm.getElementsByTagName("input")[0].checked = true; 222 | item.hide() 223 | } 224 | _id.push(item.values()["mols2grid-id"]); 225 | _smiles.push(item.values()["data-{{ smiles_col }}"]); 226 | }); 227 | add_selection({{ grid_id | tojson }}, _id, _smiles); 228 | }; 229 | 230 | 231 | // Check matching. 232 | function selectMatching(e) { 233 | var _id = []; 234 | var _smiles = []; 235 | listObj.matchingItems.forEach(function (item) { 236 | if (item.elm) { 237 | item.elm.getElementsByTagName("input")[0].checked = true; 238 | } else { 239 | item.show() 240 | item.elm.getElementsByTagName("input")[0].checked = true; 241 | item.hide() 242 | } 243 | _id.push(item.values()["mols2grid-id"]); 244 | _smiles.push(item.values()["data-{{ smiles_col }}"]); 245 | }); 246 | add_selection({{ grid_id | tojson }}, _id, _smiles); 247 | }; 248 | 249 | // Uncheck all. 250 | function unselectAll(e) { 251 | var _id = []; 252 | listObj.items.forEach(function (item) { 253 | if (item.elm) { 254 | item.elm.getElementsByTagName("input")[0].checked = false; 255 | } else { 256 | item.show() 257 | item.elm.getElementsByTagName("input")[0].checked = false; 258 | item.hide() 259 | } 260 | _id.push(item.values()["mols2grid-id"]); 261 | }); 262 | del_selection({{ grid_id | tojson }}, _id); 263 | }; 264 | 265 | // Invert selection. 266 | function invertSelection(e) { 267 | var _id_add = []; 268 | var _id_del = []; 269 | var _smiles = []; 270 | listObj.items.forEach(function (item) { 271 | if (item.elm) { 272 | var chkbox = item.elm.getElementsByTagName("input")[0] 273 | chkbox.checked = !chkbox.checked; 274 | } else { 275 | item.show() 276 | var chkbox = item.elm.getElementsByTagName("input")[0] 277 | chkbox.checked = !chkbox.checked; 278 | item.hide() 279 | } 280 | if (chkbox.checked) { 281 | _id_add.push(item.values()["mols2grid-id"]); 282 | _smiles.push(item.values()["data-{{ smiles_col }}"]); 283 | } else { 284 | _id_del.push(item.values()["mols2grid-id"]); 285 | } 286 | }); 287 | del_selection({{ grid_id | tojson }}, _id_del); 288 | add_selection({{ grid_id | tojson }}, _id_add, _smiles); 289 | }; 290 | 291 | // Copy to clipboard. 292 | function copy(e) { 293 | // navigator.clipboard.writeText(SELECTION.to_dict()); 294 | content = _renderCSV('\t') 295 | navigator.clipboard.writeText(content) 296 | }; 297 | 298 | // Export smiles. 299 | function saveSmiles(e) { 300 | var fileName = "selection.smi" 301 | if (SELECTION.size) { 302 | // Download selected smiles 303 | SELECTION.download_smi(fileName); 304 | } else { 305 | // Download all smiles 306 | SELECTION.download_smi(fileName, listObj.items); 307 | } 308 | }; 309 | 310 | // Export CSV. 311 | function saveCSV(e) { 312 | content = _renderCSV(';') 313 | var a = document.createElement("a"); 314 | var file = new Blob([content], {type: "text/csv"}); 315 | a.href = URL.createObjectURL(file); 316 | a.download = "selection.csv"; 317 | a.click(); 318 | a.remove(); 319 | }; 320 | 321 | // Render CSV for export of clipboard. 322 | function _renderCSV(sep) { 323 | // Same order as subset + tooltip 324 | var columns = Array.from(listObj.items[0].elm.querySelectorAll("div.data")) 325 | .map(elm => elm.classList[1]) 326 | .filter(name => name !== "data-img"); 327 | // Remove 'data-' and img 328 | var header = columns.map(name => name.slice(5)); 329 | // CSV content 330 | header = ["index"].concat(header).join(sep); 331 | var content = header + "\n"; 332 | listObj.items.forEach(function (item) { 333 | let data = item.values(); 334 | let index = data["mols2grid-id"]; 335 | if (SELECTION.has(index) || SELECTION.size === 0) { 336 | content += index; 337 | columns.forEach((key) => { 338 | content += sep + data[key]; 339 | }) 340 | content += "\n"; 341 | } 342 | }); 343 | return content 344 | } -------------------------------------------------------------------------------- /mols2grid/templates/js/interactive.js: -------------------------------------------------------------------------------- 1 | // list.js 2 | var listObj = new List('mols2grid', { 3 | listClass: 'm2g-list', 4 | valueNames: {{ value_names }}, 5 | item: {{ item_repr }}, 6 | page: {{ n_items_per_page }}, 7 | pagination: { 8 | paginationClass: "m2g-pagination", 9 | item: '
  • ', 10 | innerWindow: 1, 11 | outerWindow: 1, 12 | }, 13 | }); 14 | listObj.remove("mols2grid-id", "0"); 15 | listObj.add({{ data }}); 16 | 17 | 18 | // filter 19 | if (window.parent.mols2grid_lists === undefined) { 20 | window.parent.mols2grid_lists = {}; 21 | } 22 | window.parent.mols2grid_lists[{{ grid_id | tojson }}] = listObj; 23 | 24 | {% if selection %} 25 | // selection 26 | {% include 'js/molstorage.js' %} 27 | var SELECTION = new MolStorage(); 28 | {% endif %} 29 | 30 | {% if selection or callback %} 31 | // kernel 32 | {% include 'js/kernel.js' %} 33 | {% endif %} 34 | 35 | {% if selection and cached_selection %} 36 | // restore checkbox state 37 | SELECTION.multi_set({{ cached_selection[0] }}, {{ cached_selection[1] }}); 38 | listObj.on("updated", function (list) { 39 | $('#mols2grid .m2g-cell input[checked="false"]').prop("checked", false); 40 | }); 41 | {% endif %} 42 | 43 | // sort 44 | {% include 'js/sort.js' %} 45 | 46 | {% if whole_cell_style %} 47 | // add style for whole cell 48 | listObj.on("updated", function (list) { 49 | $('#mols2grid div.m2g-cell').each(function() { 50 | var $t = $(this); 51 | $t.attr({style: $t.attr('data-cellstyle')}) 52 | .removeAttr('data-cellstyle'); 53 | }); 54 | }); 55 | {% endif %} 56 | 57 | {% if tooltip %} 58 | // tooltips 59 | $.fn.tooltip.Constructor.Default.whiteList.span = ['style'] 60 | listObj.on("updated", function (list) { 61 | $(function () { 62 | // Hide previous popovers. 63 | $('#mols2grid a.page-link').click(function(e) { 64 | $('.m2g-tooltip[data-toggle="popover"]').popover('hide') 65 | }); 66 | // Create new popover. 67 | $('.m2g-tooltip[data-toggle="popover"]').popover({ 68 | placement: {{ tooltip_placement }}, 69 | trigger: 'manual', 70 | html: true, 71 | sanitize: false, 72 | }); 73 | }) 74 | }); 75 | {% endif %} 76 | 77 | // grid interactions (select, click, tooltip, key events) 78 | {% include 'js/grid_interaction.js' %} 79 | 80 | {% if onthefly %} 81 | // generate images for the currently displayed molecules 82 | var draw_opts = {{ json_draw_opts }}; 83 | var json_draw_opts = JSON.stringify(draw_opts); 84 | {% endif %} 85 | var smarts_matches = {}; 86 | 87 | // Load RDKit 88 | window 89 | .initRDKitModule() 90 | .then(function(RDKit) { 91 | console.log('RDKit version: ', RDKit.version()); 92 | window.RDKit = RDKit; 93 | window.RDKitModule = RDKit; 94 | 95 | // Searchbar 96 | {% include 'js/search.js' %} 97 | 98 | {% if onthefly %} 99 | {% include 'js/draw_mol.js' %} 100 | {% endif %} 101 | 102 | // Trigger update to activate tooltips, draw images, setup callbacks... 103 | listObj.update(); 104 | 105 | // Set iframe height to fit content. 106 | fitIframe(window.frameElement); 107 | }); -------------------------------------------------------------------------------- /mols2grid/templates/js/kernel.js: -------------------------------------------------------------------------------- 1 | function add_selection(grid_id, _id, smiles) { 2 | SELECTION.multi_set(_id, smiles); 3 | let model = window.parent["_MOLS2GRID_" + grid_id]; 4 | if (model) { 5 | model.set("selection", SELECTION.to_dict()); 6 | model.save_changes(); 7 | } 8 | } 9 | function del_selection(grid_id, _id) { 10 | SELECTION.multi_del(_id); 11 | let model = window.parent["_MOLS2GRID_" + grid_id]; 12 | if (model) { 13 | model.set("selection", SELECTION.to_dict()); 14 | model.save_changes(); 15 | } 16 | } 17 | if (window.parent.IPython !== undefined) { 18 | // Jupyter notebook 19 | var kernel_env = "jupyter"; 20 | } else if (window.parent.google !== undefined) { 21 | // Google colab 22 | var kernel_env = "colab"; 23 | } else { 24 | var kernel_env = null; 25 | } -------------------------------------------------------------------------------- /mols2grid/templates/js/molstorage.js: -------------------------------------------------------------------------------- 1 | class MolStorage extends Map { 2 | multi_set(_id, _smiles) { 3 | for (let i = 0; i < _id.length; i++) { 4 | this.set(_id[i], _smiles[i]) 5 | } 6 | } 7 | multi_del(_id) { 8 | for (let i = 0; i < _id.length; i++) { 9 | this.delete(_id[i]) 10 | } 11 | } 12 | to_dict() { 13 | var content = '{' 14 | for (let [key, value] of this) { 15 | content += key + ':' + JSON.stringify(value) + ',' 16 | } 17 | content = content.length > 1 ? content.slice(0, -1) : content 18 | content += '}' 19 | return content 20 | } 21 | to_keys() { 22 | var content = [] 23 | for (let [key] of this) { 24 | content.push(key) 25 | } 26 | return content 27 | } 28 | download_smi(fileName, allItems) { 29 | var content = '' 30 | 31 | if (allItems) { 32 | // Gather all smiles 33 | for (var item of allItems) { 34 | var smiles = item.values()['data-SMILES'] 35 | var id = item.values()['mols2grid-id'] 36 | content += smiles + ' ' + id + '\n' 37 | } 38 | } else { 39 | // Gather selected smiles 40 | for (let [key, value] of this) { 41 | content += value + ' ' + key + '\n' 42 | } 43 | } 44 | 45 | var a = document.createElement('a') 46 | var file = new Blob([content], { type: 'text/plain' }) 47 | a.href = URL.createObjectURL(file) 48 | a.download = fileName 49 | a.click() 50 | a.remove() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mols2grid/templates/js/popup.js: -------------------------------------------------------------------------------- 1 | // Prerequisite JavaScript code. 2 | // prettier-ignore 3 | {{ js }} 4 | 5 | // HTML template for the popup. 6 | var html = ` 7 |
    8 | 9 |
    10 | {% if title %}

    {{ title }}

    {% endif %} 11 | {% if subtitle %}

    {{ subtitle }}

    {% endif %} 12 | 13 |
    14 | 15 |
    16 | {% if svg %}
    {{ svg }}
    {% endif %} 17 | {{ html }} 18 |
    19 | 20 |
    21 | ` 22 | // Create container element where the popup will be inserted. 23 | if ($('#m2g-modal-container').length === 0) { 24 | $('').insertAfter('#mols2grid') 25 | } 26 | 27 | // Insert the code inside the container element. 28 | $('#m2g-modal-container').html(html) 29 | 30 | // Show modal. 31 | setTimeout(function () { 32 | $('#m2g-modal-container').addClass('show') 33 | }, 0) 34 | 35 | // Hide modal on close / ESC key. 36 | $('#m2g-modal-container').click(function (e) { 37 | if (e.target.id == 'm2g-modal-container' || e.target.className == 'close') { 38 | closeModal() 39 | } 40 | }) 41 | $(document).keydown(function (e) { 42 | if (e.key == 'Escape') { 43 | closeModal() 44 | e.preventDefault() 45 | } 46 | }) 47 | function closeModal() { 48 | $('#m2g-modal-container').removeClass('show') 49 | setTimeout(function () { 50 | $('#m2g-modal-container').remove() 51 | }, 150 + 10) 52 | } 53 | -------------------------------------------------------------------------------- /mols2grid/templates/js/search.js: -------------------------------------------------------------------------------- 1 | function SmartsSearch(query, columns) { 2 | var smiles_col = columns[0]; 3 | smarts_matches = {}; 4 | var query = $('#mols2grid .m2g-searchbar').val(); 5 | var qmol = RDKit.get_qmol(query); 6 | if (qmol.is_valid()) { 7 | listObj.items.forEach(function (item) { 8 | var smiles = item.values()[smiles_col] 9 | var mol = RDKit.get_mol(smiles, '{"removeHs": {{ removeHs | tojson }} }'); 10 | if (mol.is_valid()) { 11 | var results = mol.get_substruct_matches(qmol); 12 | if (results === "\{\}") { 13 | item.found = false; 14 | } else { 15 | item.found = true; 16 | {% if onthefly and substruct_highlight %} 17 | results = JSON.parse(results); 18 | {% if single_highlight %} 19 | var highlights = results[0] 20 | {% else %} 21 | var highlights = {"atoms": [], "bonds": []}; 22 | results.forEach(function (match) { 23 | highlights["atoms"].push(...match.atoms) 24 | highlights["bonds"].push(...match.bonds) 25 | }); 26 | {% endif %} 27 | var index = item.values()["mols2grid-id"]; 28 | smarts_matches[index] = highlights; 29 | {% endif %} 30 | } 31 | } else { 32 | item.found = false; 33 | } 34 | mol.delete(); 35 | }); 36 | } 37 | qmol.delete(); 38 | } 39 | var search_type = "Text"; 40 | // Temporary fix for regex characters being escaped by list.js 41 | // This extends String.replace to ignore the regex pattern used by list.js and returns 42 | // the string unmodified. Other calls should not be affected, unless they use the exact 43 | // same pattern and replacement value. 44 | // TODO: remove once the issue is fixed in list.js and released 45 | String.prototype.replace = (function(_super) { 46 | return function() { 47 | if ( 48 | (arguments[0].toString() === '/[-[\\]{}()*+?.,\\\\^$|#]/g') 49 | && (arguments[1] === '\\$&') 50 | ) { 51 | if (this.length === 0) { 52 | return '' 53 | } 54 | return this 55 | } 56 | return _super.apply(this, arguments); 57 | }; 58 | })(String.prototype.replace); 59 | 60 | // Switch search type (Text or SMARTS) 61 | $('#mols2grid .m2g-search-options .m2g-option').click(function() { 62 | search_type = $(this).text(); 63 | $('#mols2grid .m2g-search-options .m2g-option.sel').removeClass("sel"); 64 | $(this).addClass("sel"); 65 | }); 66 | 67 | // Searchbar update event handler 68 | $('#mols2grid .m2g-searchbar').on("keyup", function(e) { 69 | var query = e.target.value; 70 | if (search_type === "Text") { 71 | smarts_matches = {}; 72 | listObj.search(query, {{ search_cols }}); 73 | } else { 74 | listObj.search(query, ["data-{{ smiles_col }}"], SmartsSearch); 75 | } 76 | }); -------------------------------------------------------------------------------- /mols2grid/templates/js/sort.js: -------------------------------------------------------------------------------- 1 | var sortField = '{{ sort_by }}' 2 | var sortOrder = 'asc' 3 | 4 | // Sort dropdown 5 | $('#mols2grid .m2g-sort select').change(sort) 6 | 7 | // Sort order 8 | $('#mols2grid .m2g-order').click(flipSort) 9 | 10 | function sort(e) { 11 | if (e) { 12 | sortField = e.target.value 13 | var selectedOption = e.target.options[e.target.selectedIndex] 14 | var sortFieldDisplay = selectedOption.text 15 | } 16 | 17 | // Sort 18 | if (sortField == 'checkbox') { 19 | listObj.sort('mols2grid-id', {order: sortOrder, sortFunction: checkboxSort}) 20 | } else { 21 | listObj.sort(sortField, {order: sortOrder, sortFunction: mols2gridSortFunction}) 22 | } 23 | 24 | // Update UI. 25 | $(this).parent().find('.m2g-display').text(sortFieldDisplay) 26 | } 27 | 28 | // prettier-ignore 29 | function flipSort() { 30 | $(this).parent().removeClass('m2d-arrow-' + sortOrder) 31 | sortOrder = sortOrder === 'desc' ? 'asc' : 'desc' 32 | $(this).parent().addClass('m2d-arrow-' + sortOrder) 33 | sort() 34 | } 35 | 36 | function mols2gridSortFunction(itemA, itemB, options) { 37 | var x = itemA.values()[options.valueName] 38 | var y = itemB.values()[options.valueName] 39 | if (typeof x === 'number') { 40 | if (isFinite(x - y)) { 41 | return x - y 42 | } else { 43 | return isFinite(x) ? -1 : 1 44 | } 45 | } else { 46 | x = x ? x.toLowerCase() : x 47 | y = y ? y.toLowerCase() : y 48 | return x < y ? -1 : x > y ? 1 : 0 49 | } 50 | } 51 | function checkboxSort(itemA, itemB, options) { 52 | if (itemA.elm !== undefined) { 53 | var checkedA = itemA.elm.querySelector('input[type=checkbox]').checked 54 | if (itemB.elm !== undefined) { 55 | var checkedB = itemB.elm.querySelector('input[type=checkbox]').checked 56 | if (checkedA && !checkedB) { 57 | return -1 58 | } else if (!checkedA && checkedB) { 59 | return 1 60 | } else { 61 | return 0 62 | } 63 | } else { 64 | return -1 65 | } 66 | } else if (itemB.elm !== undefined) { 67 | return 1 68 | } else { 69 | return 0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mols2grid/templates/static.html: -------------------------------------------------------------------------------- 1 | {# Iframe HTML wrapper #} 2 | {% if use_iframe %} 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | {% endif %} 10 | 11 | 12 | 19 | 20 | {% include 'html/common_header.html' %} 21 | 22 | 23 | {{ custom_header }} 24 | 25 | 26 | {# Iframe HTML wrapper #} 27 | {% if use_iframe %} 28 | 29 | 30 | {% endif %} 31 | 32 | 33 | 34 | {{ data }} 35 | 36 |
    37 | 38 | 52 | 53 | {# Iframe HTML wrapper #} 54 | {% if use_iframe %} 55 | 56 | 57 | {% endif %} 58 | -------------------------------------------------------------------------------- /mols2grid/utils.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import re 3 | from ast import literal_eval 4 | from functools import partial, wraps 5 | from importlib.util import find_spec 6 | from pathlib import Path 7 | 8 | import pandas as pd 9 | from jinja2 import Environment, FileSystemLoader 10 | from rdkit import Chem 11 | 12 | env = Environment( 13 | loader=FileSystemLoader(Path(__file__).parent / "templates"), autoescape=False 14 | ) 15 | 16 | 17 | def requires(module): 18 | def inner(func): 19 | @wraps(func) 20 | def wrapper(*args, **kwargs): 21 | if find_spec(module): 22 | return func(*args, **kwargs) 23 | raise ModuleNotFoundError( 24 | f"The module {module!r} is required to use {func.__name__!r} " 25 | "but it is not installed!" 26 | ) 27 | 28 | return wrapper 29 | 30 | return inner 31 | 32 | 33 | def tooltip_formatter(s, subset, fmt, style, transform): 34 | """Function to generate tooltips from a pandas Series 35 | 36 | Parameters 37 | ---------- 38 | s : pandas.Series 39 | Row in the internal pandas DataFrame 40 | subset : list 41 | Subset of columns that are used for the tooltip 42 | fmt : str 43 | Format string for each key-value pair of the tooltip 44 | style : dict 45 | CSS styling applied to each item independently 46 | transform : dict 47 | Functions applied to each value before rendering 48 | """ 49 | items = [] 50 | for k, v in s[subset].to_dict().items(): 51 | displayed = transform[k](v) if transform.get(k) else v 52 | v = ( 53 | f'{displayed}' 54 | if style.get(k) 55 | else f'{displayed}' 56 | ) 57 | items.append(fmt.format(key=k, value=v)) 58 | return '
    '.join(items) 59 | 60 | 61 | def mol_to_smiles(mol): 62 | """Returns a SMILES from an RDKit molecule, or None if not an RDKit mol""" 63 | return Chem.MolToSmiles(mol) if mol else None 64 | 65 | 66 | def mol_to_record(mol, mol_col="mol"): 67 | """Function to create a dict of data from an RDKit molecule""" 68 | return {**mol.GetPropsAsDict(includePrivate=True), mol_col: mol} if mol else {} 69 | 70 | 71 | def sdf_to_dataframe(sdf_path, mol_col="mol"): 72 | """Creates a dataframe of molecules from an SDFile. All property fields in 73 | the SDFile are made available in the resulting dataframe 74 | 75 | Parameters 76 | ---------- 77 | sdf_path : str, Path 78 | Path to the SDFile, ending with either ``.sdf`` or ``.sdf.gz`` 79 | mol_col : str 80 | Name of the column containing the RDKit molecules in the dataframe 81 | 82 | Returns 83 | ------- 84 | df : pandas.DataFrame 85 | """ 86 | if str(sdf_path).endswith(".gz"): 87 | read_file = gzip.open 88 | else: 89 | read_file = partial(open, mode="rb") 90 | with read_file(sdf_path) as f: 91 | return pd.DataFrame( 92 | [mol_to_record(mol, mol_col) 93 | for mol in Chem.ForwardSDMolSupplier(f)] 94 | ) 95 | 96 | 97 | def remove_coordinates(mol): 98 | """Removes the existing coordinates from the molecule. The molecule is 99 | modified inplace""" 100 | mol.RemoveAllConformers() 101 | return mol 102 | 103 | 104 | def slugify(string): 105 | """Replaces whitespaces with hyphens""" 106 | return re.sub(r"\s+", "-", string) 107 | 108 | 109 | def callback_handler(callback, event): 110 | """Handler for applying the callback function on change""" 111 | data = literal_eval(event.new) 112 | callback(data) 113 | 114 | 115 | def _get_streamlit_script_run_ctx(): 116 | from streamlit.runtime.scriptrunner import get_script_run_ctx 117 | 118 | return get_script_run_ctx() 119 | 120 | 121 | def is_running_within_streamlit(): 122 | """ 123 | Function to check whether python code is run within streamlit 124 | 125 | Returns 126 | ------- 127 | use_streamlit : boolean 128 | True if code is run within streamlit, else False 129 | """ 130 | try: 131 | ctx = _get_streamlit_script_run_ctx() 132 | except ImportError: 133 | return False 134 | else: 135 | return ctx is not None 136 | -------------------------------------------------------------------------------- /mols2grid/widget/__init__.py: -------------------------------------------------------------------------------- 1 | from .widget import MolGridWidget 2 | 3 | 4 | def _jupyter_labextension_paths(): 5 | """Called by Jupyter Lab Server to detect if it is a valid labextension and 6 | to install the widget 7 | Returns 8 | ======= 9 | src: Source directory name to copy files from. Webpack outputs generated files 10 | into this directory and Jupyter Lab copies from this directory during 11 | widget installation 12 | dest: Destination directory name to install widget files to. Jupyter Lab copies 13 | from `src` directory into /labextensions/ directory 14 | during widget installation 15 | """ 16 | return [ 17 | { 18 | "src": "./labextension", 19 | "dest": "mols2grid", 20 | } 21 | ] 22 | 23 | 24 | def _jupyter_nbextension_paths(): 25 | """Called by Jupyter Notebook Server to detect if it is a valid nbextension and 26 | to install the widget 27 | Returns 28 | ======= 29 | section: The section of the Jupyter Notebook Server to change. 30 | Must be 'notebook' for widget extensions 31 | src: Source directory name to copy files from. Webpack outputs generated files 32 | into this directory and Jupyter Notebook copies from this directory during 33 | widget installation 34 | dest: Destination directory name to install widget files to. Jupyter Notebook copies 35 | from `src` directory into /nbextensions/ directory 36 | during widget installation 37 | require: Path to importable AMD Javascript module inside the 38 | /nbextensions/ directory 39 | """ 40 | return [ 41 | { 42 | "section": "notebook", 43 | "src": "./nbextension", 44 | "dest": "mols2grid", 45 | "require": "mols2grid/extension", 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /mols2grid/widget/_frontend.py: -------------------------------------------------------------------------------- 1 | """ 2 | Information about the frontend package of the widgets. 3 | """ 4 | from .._version import __version__ 5 | 6 | module_name = "mols2grid" 7 | module_version = f"^{__version__}" 8 | -------------------------------------------------------------------------------- /mols2grid/widget/widget.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import DOMWidget, register 2 | from traitlets import Bool, List, Unicode 3 | 4 | from ._frontend import module_name, module_version 5 | 6 | 7 | @register 8 | class MolGridWidget(DOMWidget): 9 | """A custom widget for the MolGrid class. Handles selections and callbacks. 10 | 11 | Attributes 12 | ---------- 13 | grid_id : str 14 | Name of the grid controlling the widget 15 | selection : str 16 | JSON string containing the molecule selection as a dictionnary. Index 17 | are keys and SMILES string are values. 18 | callback_kwargs : str 19 | JSON string containing the keyword arguments with which to call the 20 | callback function. 21 | filter_mask : List[bool] 22 | List stating wether a molecule should be kept (True) or filtered out 23 | (False) 24 | """ 25 | 26 | _model_name = Unicode("MolGridModel").tag(sync=True) 27 | _model_module = Unicode(module_name).tag(sync=True) 28 | _model_module_version = Unicode(module_version).tag(sync=True) 29 | _view_name = Unicode("MolGridView").tag(sync=True) 30 | _view_module = Unicode(module_name).tag(sync=True) 31 | _view_module_version = Unicode(module_version).tag(sync=True) 32 | 33 | grid_id = Unicode("default").tag(sync=True) 34 | selection = Unicode("{}").tag(sync=True) 35 | callback_kwargs = Unicode("{}").tag(sync=True) 36 | filter_mask = List(Bool(), []).tag(sync=True) 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mols2grid", 3 | "version": "2.0.0", 4 | "description": "Custom widget for the Python mols2grid package", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "widgets" 10 | ], 11 | "files": [ 12 | "lib/**/*.js", 13 | "lib/**/*.d.ts", 14 | "dist/*.js", 15 | "dist/*.d.ts", 16 | "dist/*.map", 17 | "css/*.css" 18 | ], 19 | "homepage": "https://github.com/cbouy/mols2grid", 20 | "bugs": { 21 | "url": "https://github.com/cbouy/mols2grid/issues" 22 | }, 23 | "license": "Apache-2.0", 24 | "author": { 25 | "name": "Cedric Bouysset", 26 | "email": "cedric@bouysset.net" 27 | }, 28 | "main": "lib/index.js", 29 | "types": "./lib/index.d.ts", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/cbouy/mols2grid" 33 | }, 34 | "scripts": { 35 | "build": "yarn run build:lib && yarn run build:nbextension && yarn run build:labextension:dev", 36 | "build:prod": "yarn run build:lib && yarn run build:nbextension && yarn run build:labextension", 37 | "build:labextension": "jupyter labextension build .", 38 | "build:labextension:dev": "jupyter labextension build --development True .", 39 | "build:lib": "tsc", 40 | "build:nbextension": "webpack", 41 | "clean": "yarn run clean:lib && yarn run clean:nbextension && yarn run clean:labextension", 42 | "clean:lib": "rimraf lib", 43 | "clean:labextension": "rimraf mols2grid/labextension", 44 | "clean:nbextension": "rimraf mols2grid/nbextension/static/index.js", 45 | "prepack": "yarn run build:lib", 46 | "watch": "npm-run-all -p watch:*", 47 | "watch:lib": "tsc -w", 48 | "watch:nbextension": "webpack --watch --mode=development", 49 | "watch:labextension": "jupyter labextension watch ." 50 | }, 51 | "dependencies": { 52 | "@jupyter-widgets/base": "^1.1.10 || ^2 || ^3 || ^4 || ^5 || ^6" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.5.0", 56 | "@babel/preset-env": "^7.5.0", 57 | "@jupyterlab/builder": "^3.0.0", 58 | "@lumino/application": "^1.6.0", 59 | "@lumino/widgets": "^1.6.0", 60 | "@types/jest": "^26.0.0", 61 | "@types/webpack-env": "^1.13.6", 62 | "@typescript-eslint/eslint-plugin": "^3.6.0", 63 | "@typescript-eslint/parser": "^3.6.0", 64 | "acorn": "^7.2.0", 65 | "css-loader": "^3.2.0", 66 | "eslint": "^7.4.0", 67 | "eslint-config-prettier": "^8.8.0", 68 | "eslint-plugin-prettier": "^4.2.1", 69 | "fs-extra": "^7.0.0", 70 | "identity-obj-proxy": "^3.0.0", 71 | "jest": "^26.0.0", 72 | "mkdirp": "^0.5.1", 73 | "npm-run-all": "^4.1.3", 74 | "prettier": "^2.8.8", 75 | "rimraf": "^2.6.2", 76 | "source-map-loader": "^1.1.3", 77 | "style-loader": "^1.0.0", 78 | "ts-jest": "^26.0.0", 79 | "ts-loader": "^8.0.0", 80 | "typescript": "~4.1.3", 81 | "webpack": "^5.61.0", 82 | "webpack-cli": "^4.0.0" 83 | }, 84 | "jupyterlab": { 85 | "extension": "lib/plugin", 86 | "outputDir": "mols2grid/labextension", 87 | "sharedPackages": { 88 | "@jupyter-widgets/base": { 89 | "bundled": false, 90 | "singleton": true 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling", 4 | "jupyterlab==3.*", 5 | ] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "mols2grid" 10 | description = "Interactive 2D small molecule viewer" 11 | readme = "README.md" 12 | requires-python = ">=3.7" 13 | authors = [ 14 | { name = "Cédric Bouysset", email = "cedric@bouysset.net" }, 15 | ] 16 | keywords = [ 17 | "cheminformatics", 18 | "chemistry", 19 | "jupyter", 20 | "science", 21 | "widgets", 22 | ] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Framework :: Jupyter", 26 | "Intended Audience :: Science/Research", 27 | "License :: OSI Approved :: Apache Software License", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Topic :: Scientific/Engineering :: Chemistry", 35 | ] 36 | dependencies = [ 37 | "ipywidgets>=7,<8", 38 | "jinja2>=2.11.0", 39 | "numpy", 40 | "pandas", 41 | ] 42 | dynamic = ["version"] 43 | 44 | [project.license] 45 | file = "LICENSE" 46 | 47 | [project.optional-dependencies] 48 | build = [ 49 | "build", 50 | ] 51 | tests = [ 52 | "cairosvg==2.5.2", 53 | "flaky==3.7.0", 54 | "pyautogecko==0.1.3", 55 | "imagehash~=4.3", 56 | "ipython==7.12.0", 57 | "pytest-cov==2.12.1", 58 | "pytest==6.2.5", 59 | "selenium==4.10.0", 60 | ] 61 | docs = [ 62 | "mistune<3.0.0", 63 | ] 64 | dev = [ 65 | "mols2grid[build,tests,docs]", 66 | ] 67 | 68 | [project.urls] 69 | Homepage = "https://github.com/cbouy/mols2grid" 70 | Documentation = "https://mols2grid.readthedocs.io/en/latest/" 71 | Discussions = "https://github.com/cbouy/mols2grid/discussions" 72 | Issues = "https://github.com/cbouy/mols2grid/issues" 73 | Changelog = "https://github.com/cbouy/mols2grid/blob/master/CHANGELOG.md" 74 | 75 | [tool.setuptools.dynamic] 76 | version = { attr = "mols2grid._version.__version__" } 77 | 78 | [tool.hatch.version] 79 | path = "mols2grid/_version.py" 80 | 81 | [tool.hatch.build] 82 | artifacts = [ 83 | "mols2grid/nbextension/index.*", 84 | "mols2grid/labextension", 85 | ] 86 | 87 | [tool.hatch.build.targets.wheel.shared-data] 88 | "mols2grid/nbextension" = "share/jupyter/nbextensions/mols2grid" 89 | "mols2grid/labextension" = "share/jupyter/labextensions/mols2grid" 90 | "./install.json" = "share/jupyter/labextensions/mols2grid/install.json" 91 | "./mols2grid.json" = "etc/jupyter/nbconfig/notebook.d/mols2grid.json" 92 | 93 | [tool.hatch.build.targets.sdist] 94 | exclude = [ 95 | ".github", 96 | ] 97 | 98 | [tool.hatch.build.hooks.jupyter-builder] 99 | ensured-targets = [ 100 | "mols2grid/nbextension/index.js", 101 | "mols2grid/labextension/package.json", 102 | ] 103 | skip-if-exists = [ 104 | "mols2grid/nbextension/index.js", 105 | "mols2grid/labextension/package.json", 106 | ] 107 | dependencies = [ 108 | "hatch-jupyter-builder>=0.8.1", 109 | ] 110 | build-function = "hatch_jupyter_builder.npm_builder" 111 | 112 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 113 | path = "." 114 | build_cmd = "build:prod" 115 | 116 | [tool.black] 117 | line-length = 88 118 | extend-exclude = ''' 119 | ( 120 | ^/docs/conf.py 121 | ) 122 | ''' 123 | 124 | [tool.isort] 125 | profile = "black" 126 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // Entry point for the notebook bundle containing custom model definitions. 5 | // 6 | // Setup notebook base URL 7 | // 8 | // Some static assets may be required by the custom widget javascript. The base 9 | // url for the notebook is not known at build time and is therefore computed 10 | // dynamically. 11 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 12 | (window as any).__webpack_public_path__ = 13 | document.querySelector('body')!.getAttribute('data-base-url') + 14 | 'nbextensions/mols2grid'; 15 | 16 | export * from './index'; 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cedric Bouysset 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | export * from './version'; 5 | export * from './widget'; 6 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cedric Bouysset 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { Application, IPlugin } from '@lumino/application'; 5 | 6 | import { Widget } from '@lumino/widgets'; 7 | 8 | import { IJupyterWidgetRegistry } from '@jupyter-widgets/base'; 9 | 10 | import * as widgetExports from './widget'; 11 | 12 | import { MODULE_NAME, MODULE_VERSION } from './version'; 13 | 14 | const EXTENSION_ID = 'mols2grid:plugin'; 15 | 16 | /** 17 | * The example plugin. 18 | */ 19 | const examplePlugin: IPlugin, void> = { 20 | id: EXTENSION_ID, 21 | requires: [IJupyterWidgetRegistry], 22 | activate: activateWidgetExtension, 23 | autoStart: true, 24 | } as unknown as IPlugin, void>; 25 | // the "as unknown as ..." typecast above is solely to support JupyterLab 1 26 | // and 2 in the same codebase and should be removed when we migrate to Lumino. 27 | 28 | export default examplePlugin; 29 | 30 | /** 31 | * Activate the widget extension. 32 | */ 33 | function activateWidgetExtension( 34 | app: Application, 35 | registry: IJupyterWidgetRegistry 36 | ): void { 37 | registry.registerWidget({ 38 | name: MODULE_NAME, 39 | version: MODULE_VERSION, 40 | exports: widgetExports, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cedric Bouysset 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const data = require('../package.json'); 8 | 9 | /** 10 | * The _model_module_version/_view_module_version this package implements. 11 | * 12 | * The html widget manager assumes that this is the same as the npm package 13 | * version number. 14 | */ 15 | export const MODULE_VERSION = data.version; 16 | 17 | /* 18 | * The current package name. 19 | */ 20 | export const MODULE_NAME = data.name; 21 | -------------------------------------------------------------------------------- /src/widget.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cedric Bouysset 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { 5 | DOMWidgetModel, 6 | DOMWidgetView, 7 | ISerializers, 8 | } from '@jupyter-widgets/base'; 9 | 10 | import { MODULE_NAME, MODULE_VERSION } from './version'; 11 | 12 | // Import the CSS 13 | import '../css/widget.css'; 14 | 15 | export class MolGridModel extends DOMWidgetModel { 16 | defaults() { 17 | return { 18 | ...super.defaults(), 19 | _model_name: MolGridModel.model_name, 20 | _model_module: MolGridModel.model_module, 21 | _model_module_version: MolGridModel.model_module_version, 22 | _view_name: MolGridModel.view_name, 23 | _view_module: MolGridModel.view_module, 24 | _view_module_version: MolGridModel.view_module_version, 25 | grid_id: "default", 26 | selection: "{}", 27 | callback_kwargs: "{}", 28 | filter_mask: [], 29 | }; 30 | } 31 | 32 | static serializers: ISerializers = { 33 | ...DOMWidgetModel.serializers, 34 | // Add any extra serializers here 35 | }; 36 | 37 | static model_name = 'MolGridModel'; 38 | static model_module = MODULE_NAME; 39 | static model_module_version = MODULE_VERSION; 40 | static view_name = 'MolGridView'; // Set to null if no view 41 | static view_module = MODULE_NAME; // Set to null if no view 42 | static view_module_version = MODULE_VERSION; 43 | } 44 | 45 | export class MolGridView extends DOMWidgetView { 46 | render() { 47 | this.el.classList.add('mols2grid-widget'); 48 | let grid_id: string = this.model.get('grid_id'); 49 | let name: string = "_MOLS2GRID_" + grid_id; 50 | (window)[name] = this.model; 51 | this.model.on('change:filter_mask', this._trigger_filtering, this); 52 | } 53 | 54 | private _trigger_filtering() { 55 | let grid_id: string = this.model.get('grid_id'); 56 | let listObj: any = undefined; 57 | if (typeof (window).mols2grid_lists !== "undefined") { 58 | listObj = (window).mols2grid_lists[grid_id]; 59 | } else if (typeof (window).parent.mols2grid_lists !== "undefined") { 60 | listObj = (window).parent.mols2grid_lists[grid_id]; 61 | } else { 62 | return; 63 | } 64 | let filter_mask = this.model.get("filter_mask"); 65 | if (filter_mask !== []) { 66 | listObj.filter(function (item: any) { 67 | return filter_mask[item.values()["mols2grid-id"]]; 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbouy/mols2grid/d156fc940c3db929409f5ea706b8f598d3eefc13/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from rdkit import RDConfig 5 | 6 | from mols2grid import MolGrid, sdf_to_dataframe 7 | 8 | 9 | @pytest.fixture(scope="module") 10 | def sdf_path(): 11 | return Path(RDConfig.RDDocsDir) / "Book" / "data" / "solubility.test.sdf" 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def sdf_file(sdf_path): 16 | return str(sdf_path) 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def smiles_records(): 21 | return [{"SMILES": "C" * i, "ID": i} for i in range(1, 5)] 22 | 23 | 24 | @pytest.fixture(scope="module") 25 | def df(sdf_path): 26 | return sdf_to_dataframe(sdf_path).head(30) 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def small_df(df): 31 | return df.head(5) 32 | 33 | 34 | @pytest.fixture(scope="module") 35 | def grid_prerendered(df): 36 | return MolGrid(df, mol_col="mol", prerender=True) 37 | 38 | 39 | @pytest.fixture(scope="module") 40 | def grid(df): 41 | return MolGrid(df, mol_col="mol", size=(160, 120)) 42 | 43 | 44 | @pytest.fixture(scope="module") 45 | def mols(small_df): 46 | return small_df["mol"] 47 | -------------------------------------------------------------------------------- /tests/environment.yml: -------------------------------------------------------------------------------- 1 | name: mols2grid 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - nodejs==17.9.0 7 | - yarn==1.22.18 8 | - pandas 9 | - jinja2>=2.11.0 10 | - jupyterlab 11 | - ipywidgets>=7,<8 12 | - pytest==6.2.5 13 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | webdriver: marks tests requiring a webdriver -------------------------------------------------------------------------------- /tests/test_callbacks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mols2grid import callbacks 4 | 5 | 6 | def test_make_popup_callback(): 7 | popup = callbacks.make_popup_callback( 8 | title="${title}", 9 | subtitle="${title}", 10 | svg="", 11 | html='${title}', 12 | js='var title = "FOOBAR";', 13 | style="max-width: 42%;", 14 | ) 15 | assert '

    ${title}

    ' in popup 16 | assert '

    ${title}

    ' in popup 17 | assert '${title}' in popup 18 | assert '// Prerequisite JavaScript code.\n// prettier-ignore\nvar title = "FOOBAR";' in popup 19 | assert '
    ' in popup 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "title, expected", 24 | [ 25 | ("SMILES", "${data['SMILES']}"), 26 | (None, None), 27 | ], 28 | ) 29 | def test_title_field(title, expected): 30 | assert callbacks._get_title_field(title) == expected 31 | -------------------------------------------------------------------------------- /tests/test_molgrid.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | 3 | import pytest 4 | from numpy.testing import assert_equal 5 | from rdkit import Chem 6 | from rdkit.Chem import Draw 7 | from rdkit.Chem.rdDepictor import Compute2DCoords 8 | 9 | from mols2grid import MolGrid 10 | from mols2grid.select import register 11 | 12 | 13 | def get_grid(df, **kwargs): 14 | kwargs.setdefault("mol_col", "mol") 15 | return MolGrid(df, **kwargs) 16 | 17 | 18 | def test_no_input_specified(small_df): 19 | with pytest.raises( 20 | ValueError, match="One of `smiles_col` or `mol_col` must be set" 21 | ): 22 | get_grid(small_df, smiles_col=None, mol_col=None) 23 | 24 | 25 | def test_name_not_str(small_df): 26 | with pytest.raises( 27 | TypeError, match="`name` must be a string. Currently of type int" 28 | ): 29 | get_grid(small_df, name=0) 30 | 31 | 32 | def test_uses_svg(grid_prerendered): 33 | assert grid_prerendered.useSVG == True 34 | data = grid_prerendered.dataframe.loc[0, "img"] 35 | assert "{key}: {value}", 31 | {}, 32 | {}, 33 | 'SMILES: CCO
    ' 34 | 'ID: 0', 35 | ), 36 | (["ID"], "foo-{value}", {}, {}, 'foo-0'), 37 | ( 38 | ["ID"], 39 | "{value}", 40 | {"ID": lambda x: "color: red"}, 41 | {}, 42 | '0', 43 | ), 44 | ( 45 | ["Activity"], 46 | "{value}", 47 | {}, 48 | {"Activity": lambda x: f"{x:.2f}"}, 49 | '42.01', 50 | ), 51 | ( 52 | ["Activity"], 53 | "{key}: {value}", 54 | {"Activity": lambda x: "color: red" if x > 40 else ""}, 55 | {"Activity": lambda x: f"{x:.2f}"}, 56 | 'Activity: 42.01', 57 | ), 58 | ], 59 | ) 60 | def test_tooltip_formatter(subset, fmt, style, transform, exp): 61 | row = pd.Series( 62 | { 63 | "ID": 0, 64 | "SMILES": "CCO", 65 | "Activity": 42.012345, 66 | } 67 | ) 68 | tooltip = utils.tooltip_formatter(row, subset, fmt, style, transform) 69 | assert tooltip == exp 70 | 71 | 72 | @pytest.mark.parametrize( 73 | ["smi", "exp"], [("CCO", "CCO"), ("blabla", None), (None, None)] 74 | ) 75 | def test_mol_to_smiles(smi, exp): 76 | if smi: 77 | mol = Chem.MolFromSmiles(smi) 78 | else: 79 | mol = smi 80 | assert utils.mol_to_smiles(mol) == exp 81 | 82 | 83 | def test_mol_to_record(): 84 | mol = Chem.MolFromSmiles("CCO") 85 | props = { 86 | "NAME": "ethanol", 87 | "foo": 42, 88 | "_bar": 42.01, 89 | "__baz": 0, 90 | } 91 | for prop, value in props.items(): 92 | if isinstance(value, int): 93 | mol.SetIntProp(prop, value) 94 | elif isinstance(value, float): 95 | mol.SetDoubleProp(prop, value) 96 | else: 97 | mol.SetProp(prop, value) 98 | new = utils.mol_to_record(mol) 99 | assert "mol" in new.keys() 100 | new.pop("mol") 101 | assert new == props 102 | 103 | 104 | def test_mol_to_record_none(): 105 | new = utils.mol_to_record(None) 106 | assert new == {} 107 | 108 | 109 | def test_mol_to_record_overwrite_smiles(): 110 | mol = Chem.MolFromSmiles("CCO") 111 | mol.SetProp("SMILES", "foo") 112 | new = utils.mol_to_record(mol) 113 | assert new["SMILES"] == "foo" 114 | 115 | 116 | def test_mol_to_record_custom_mol_col(): 117 | mol = Chem.MolFromSmiles("CCO") 118 | new = utils.mol_to_record(mol, mol_col="foo") 119 | assert new["foo"] is mol 120 | 121 | 122 | @pytest.mark.parametrize("sdf_source", ["sdf_path", "sdf_file"]) 123 | def test_sdf_to_dataframe(sdf_source, request): 124 | sdf = request.getfixturevalue(sdf_source) 125 | df = utils.sdf_to_dataframe(sdf) 126 | exp = { 127 | "ID": 5, 128 | "NAME": "3-methylpentane", 129 | "SMILES": "CCC(C)CC", 130 | "SOL": -3.68, 131 | "SOL_classification": "(A) low", 132 | "_MolFileComments": "", 133 | "_MolFileInfo": " SciTegic05121109362D", 134 | "_Name": "3-methylpentane", 135 | } 136 | new = df.iloc[0].drop(["mol"]).to_dict() 137 | assert new == exp 138 | 139 | 140 | def test_sdf_to_dataframe_custom_mol_col(sdf_path): 141 | df = utils.sdf_to_dataframe(sdf_path, mol_col="foo") 142 | assert "mol" not in df.columns 143 | assert "foo" in df.columns 144 | 145 | 146 | def test_sdf_to_df_gz(sdf_path, tmp_path): 147 | tmp_file = tmp_path / "mols.sdf.gz" 148 | with open(sdf_path, "rb") as fi, open(tmp_file, "wb") as tf: 149 | gz = gzip.compress(fi.read(), compresslevel=1) 150 | tf.write(gz) 151 | tf.flush() 152 | df = utils.sdf_to_dataframe(tmp_file).drop(columns=["mol"]) 153 | ref = utils.sdf_to_dataframe(sdf_path).drop(columns=["mol"]) 154 | assert (df == ref).values.all() 155 | 156 | 157 | def test_remove_coordinates(): 158 | mol = Chem.MolFromSmiles("CCO") 159 | Compute2DCoords(mol) 160 | mol.GetConformer() 161 | new = utils.remove_coordinates(mol) 162 | assert new is mol 163 | with pytest.raises(ValueError, match="Bad Conformer Id"): 164 | new.GetConformer() 165 | 166 | 167 | @pytest.mark.parametrize( 168 | ["string", "expected"], 169 | [ 170 | ("Mol", "Mol"), 171 | ("mol name", "mol-name"), 172 | ("mol name", "mol-name"), 173 | ("mol-name", "mol-name"), 174 | ("mol- name", "mol--name"), 175 | ("mol\tname", "mol-name"), 176 | ("mol\nname", "mol-name"), 177 | ("mol \t\n name", "mol-name"), 178 | ], 179 | ) 180 | def test_slugify(string, expected): 181 | assert utils.slugify(string) == expected 182 | 183 | 184 | @pytest.mark.parametrize("value", [1, 2]) 185 | def test_callback_handler(value): 186 | callback = lambda x: x + 1 187 | mock = Mock(side_effect=callback) 188 | event = SimpleNamespace(new=str(value)) 189 | utils.callback_handler(mock, event) 190 | mock.assert_called_once_with(value) 191 | 192 | 193 | def test_is_running_within_streamlit(): 194 | assert utils.is_running_within_streamlit() is False 195 | with patch( 196 | "mols2grid.utils._get_streamlit_script_run_ctx", 197 | create=True, 198 | new=lambda: object(), 199 | ): 200 | assert utils.is_running_within_streamlit() is True 201 | with patch( 202 | "mols2grid.utils._get_streamlit_script_run_ctx", create=True, new=lambda: None 203 | ): 204 | assert utils.is_running_within_streamlit() is False 205 | -------------------------------------------------------------------------------- /tests/webdriver_utils.py: -------------------------------------------------------------------------------- 1 | from ast import literal_eval 2 | from base64 import b64decode 3 | from io import BytesIO 4 | 5 | import imagehash 6 | from cairosvg import svg2png 7 | from PIL import Image 8 | from selenium import webdriver 9 | from selenium.common.exceptions import StaleElementReferenceException 10 | from selenium.webdriver.common.action_chains import ActionChains 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.common.keys import Keys 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.webdriver.support.ui import WebDriverWait 15 | 16 | 17 | class selection_available: 18 | def __init__(self, is_empty=False): 19 | self.empty = is_empty 20 | 21 | def __call__(self, driver): 22 | sel = driver.execute_script("return SELECTION.to_dict();") 23 | sel = literal_eval(sel) 24 | if sel == {} and self.empty: 25 | return True 26 | elif sel != {} and not self.empty: 27 | return sel 28 | return False 29 | 30 | 31 | class FirefoxDriver(webdriver.Firefox): 32 | def wait_for_img_load( 33 | self, max_delay=5, selector="#mols2grid .m2g-cell .data-img svg" 34 | ): 35 | return WebDriverWait(self, max_delay).until( 36 | EC.presence_of_element_located((By.CSS_SELECTOR, selector)) 37 | ) 38 | 39 | def wait_for_selection(self, is_empty=False, max_delay=3): 40 | return WebDriverWait(self, max_delay).until(selection_available(is_empty)) 41 | 42 | def wait(self, condition, max_delay=5): 43 | return WebDriverWait( 44 | self, max_delay, ignored_exceptions=[StaleElementReferenceException] 45 | ).until(condition) 46 | 47 | def find_by_id(self, element_id, **kwargs): 48 | condition = EC.presence_of_element_located((By.ID, element_id)) 49 | return self.wait(condition, **kwargs) 50 | 51 | def find_clickable(self, by, selector, **kwargs): 52 | condition = EC.element_to_be_clickable((by, selector)) 53 | return self.wait(condition, **kwargs) 54 | 55 | def find_by_css_selector(self, css_selector, **kwargs): 56 | condition = EC.presence_of_element_located((By.CSS_SELECTOR, css_selector)) 57 | return self.wait(condition, **kwargs) 58 | 59 | def find_by_class_name(self, name, **kwargs): 60 | condition = EC.presence_of_element_located((By.CLASS_NAME, name)) 61 | return self.wait(condition, **kwargs) 62 | 63 | def find_all_by_class_name(self, name, **kwargs): 64 | condition = EC.presence_of_all_elements_located((By.CLASS_NAME, name)) 65 | return self.wait(condition, **kwargs) 66 | 67 | def get_svg_hash(self, *args, **kwargs): 68 | im = next(self.get_imgs_from_svgs(*args, **kwargs)) 69 | return imagehash.average_hash(im, hash_size=16) 70 | 71 | def get_imgs_from_svgs(self, selector="#mols2grid .m2g-cell .data-img"): 72 | condition = EC.presence_of_all_elements_located((By.CSS_SELECTOR, selector)) 73 | svgs = self.wait(condition) 74 | for svg in svgs: 75 | im = svg2png(bytestring=(svg.get_attribute("innerHTML"))) 76 | yield Image.open(BytesIO(im)) 77 | 78 | def get_png_hash(self, selector="#mols2grid .m2g-cell .data-img *"): 79 | img = self.find_by_css_selector(selector) 80 | im = Image.open(BytesIO(b64decode(img.get_attribute("src")[22:]))) 81 | return imagehash.average_hash(im, hash_size=16) 82 | 83 | def substructure_query(self, smarts): 84 | self.find_clickable(By.CSS_SELECTOR, "#mols2grid .m2g-search-smarts").click() 85 | self.find_by_css_selector("#mols2grid .m2g-searchbar").send_keys(smarts) 86 | self.wait_for_img_load() 87 | 88 | def text_search(self, txt): 89 | self.find_clickable(By.CSS_SELECTOR, "#mols2grid .m2g-search-text").click() 90 | self.find_by_css_selector("#mols2grid .m2g-searchbar").send_keys(txt) 91 | self.wait_for_img_load() 92 | 93 | def clear_search(self): 94 | self.find_by_css_selector("#mols2grid .m2g-searchbar").clear() 95 | self.find_by_css_selector("#mols2grid .m2g-searchbar").send_keys(Keys.BACKSPACE) 96 | self.wait_for_img_load() 97 | 98 | def sort_grid(self, field): 99 | self.find_clickable(By.CSS_SELECTOR, "#mols2grid .m2g-sort").click() 100 | self.find_clickable( 101 | By.CSS_SELECTOR, f'#mols2grid .m2g-sort option[value="data-{field}"]' 102 | ).click() 103 | self.wait_for_img_load() 104 | 105 | def invert_sort(self): 106 | self.find_clickable(By.CSS_SELECTOR, "#mols2grid .m2g-sort .m2g-order").click() 107 | self.wait_for_img_load() 108 | 109 | def grid_action(self, action): 110 | self.find_clickable(By.CSS_SELECTOR, "#mols2grid .m2g-actions").click() 111 | self.find_clickable( 112 | By.CSS_SELECTOR, f'#mols2grid .m2g-actions option[value="{action}"]' 113 | ).click() 114 | 115 | def get_tooltip_content(self, pause=0, selector=".m2g-cell .m2g-info"): 116 | ( 117 | ActionChains(self) 118 | .move_to_element(self.find_by_css_selector(f"#mols2grid {selector}")) 119 | .pause(pause) 120 | .perform() 121 | ) 122 | tooltip = self.find_by_css_selector('div.popover[role="tooltip"]') 123 | el = tooltip.find_element(By.CLASS_NAME, "popover-body") 124 | return el.get_attribute("innerHTML") 125 | 126 | def trigger_callback( 127 | self, selector="#mols2grid .m2g-cell .m2g-callback", pause=0.2 128 | ): 129 | self.wait_for_img_load() 130 | el = self.find_clickable(By.CSS_SELECTOR, selector) 131 | (ActionChains(self).move_to_element(el).pause(pause).click().perform()) 132 | 133 | def click_checkbox(self, is_empty=False): 134 | self.find_clickable(By.CSS_SELECTOR, ".m2g-cb").click() 135 | return self.wait_for_selection(is_empty=is_empty) 136 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["es2016", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noUnusedLocals": true, 10 | "outDir": "lib", 11 | "resolveJsonModule": true, 12 | "rootDir": "src", 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "target": "es2016", 18 | "types": [] 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.tsx"] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const version = require('./package.json').version; 3 | 4 | // Custom webpack rules 5 | const rules = [ 6 | { test: /\.ts$/, loader: 'ts-loader' }, 7 | { test: /\.js$/, loader: 'source-map-loader' }, 8 | { test: /\.css$/, use: ['style-loader', 'css-loader']} 9 | ]; 10 | 11 | // Packages that shouldn't be bundled but loaded at runtime 12 | const externals = ['@jupyter-widgets/base']; 13 | 14 | const resolve = { 15 | // Add '.ts' and '.tsx' as resolvable extensions. 16 | extensions: [".webpack.js", ".web.js", ".ts", ".js"] 17 | }; 18 | 19 | module.exports = [ 20 | /** 21 | * Notebook extension 22 | * 23 | * This bundle only contains the part of the JavaScript that is run on load of 24 | * the notebook. 25 | */ 26 | { 27 | entry: './src/extension.ts', 28 | output: { 29 | filename: 'index.js', 30 | path: path.resolve(__dirname, 'mols2grid', 'nbextension'), 31 | libraryTarget: 'amd', 32 | publicPath: '', 33 | }, 34 | module: { 35 | rules: rules 36 | }, 37 | devtool: 'source-map', 38 | externals, 39 | resolve, 40 | }, 41 | 42 | /** 43 | * Embeddable mols2grid bundle 44 | * 45 | * This bundle is almost identical to the notebook extension bundle. The only 46 | * difference is in the configuration of the webpack public path for the 47 | * static assets. 48 | * 49 | * The target bundle is always `dist/index.js`, which is the path required by 50 | * the custom widget embedder. 51 | */ 52 | { 53 | entry: './src/index.ts', 54 | output: { 55 | filename: 'index.js', 56 | path: path.resolve(__dirname, 'dist'), 57 | libraryTarget: 'amd', 58 | library: "mols2grid", 59 | publicPath: 'https://unpkg.com/mols2grid@' + version + '/dist/' 60 | }, 61 | devtool: 'source-map', 62 | module: { 63 | rules: rules 64 | }, 65 | externals, 66 | resolve, 67 | }, 68 | 69 | ]; 70 | --------------------------------------------------------------------------------