├── .gitattributes ├── MANIFEST.in ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yaml ├── plotmol ├── tests │ ├── __init__.py │ ├── test_utilities.py │ └── test_plotmol.py ├── __init__.py ├── styles.py ├── utilities.py ├── _plotmol.py └── _version.py ├── devtools └── envs │ └── base.yaml ├── .pre-commit-config.yaml ├── Makefile ├── LICENSE ├── pyproject.toml ├── README.md ├── .gitignore └── examples └── scatter-plot.ipynb /.gitattributes: -------------------------------------------------------------------------------- 1 | plotmol/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] __pycache__ *.so -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Provide a brief description of the PR's purpose here. 3 | 4 | ## Status 5 | - [ ] Ready to go -------------------------------------------------------------------------------- /plotmol/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Empty init file in case you choose a package besides PyTest such as Nose which may look for such a file 3 | """ 4 | -------------------------------------------------------------------------------- /plotmol/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | plotmol 3 | 4 | Interactive plotting of data annotated with molecule structures. 5 | """ 6 | 7 | from . import _version 8 | from ._plotmol import InputSizeError, default_tooltip_template, scatter 9 | 10 | __version__ = _version.get_versions()["version"] 11 | __all__ = ["InputSizeError", "default_tooltip_template", "scatter", "__version__"] 12 | -------------------------------------------------------------------------------- /plotmol/tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | import plotmol.styles 2 | import plotmol.utilities 3 | 4 | 5 | def test_smiles_to_svg(): 6 | # It's difficult to test this as it's a graphical function. For now make sure 7 | # it doesn't error and something is produced. 8 | output = plotmol.utilities.smiles_to_svg("CO", plotmol.styles.MoleculeStyle()) 9 | assert len(output) > 0 10 | -------------------------------------------------------------------------------- /devtools/envs/base.yaml: -------------------------------------------------------------------------------- 1 | name: plotmol 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | # Base depends 6 | - python >=3.10 7 | - pip 8 | 9 | # Core 10 | - rdkit 11 | - bokeh 12 | 13 | # Examples 14 | - jupyter 15 | - nbconvert 16 | 17 | # Dev / Testing 18 | - versioneer 19 | 20 | - pre-commit 21 | - isort 22 | - black 23 | - flake8 24 | - flake8-pyproject 25 | - nbqa 26 | 27 | - pytest 28 | - pytest-cov 29 | 30 | - codecov 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | concurrency: 4 | group: ${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: { branches: [ "main" ] } 9 | pull_request: { branches: [ "main" ] } 10 | 11 | jobs: 12 | test: 13 | 14 | runs-on: ubuntu-latest 15 | container: condaforge/mambaforge:latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3.3.0 19 | 20 | - name: Setup Conda Environment 21 | run: | 22 | apt update && apt install -y git make 23 | 24 | make env 25 | make lint 26 | make test 27 | make test-examples 28 | 29 | - name: CodeCov 30 | uses: codecov/codecov-action@v3.1.1 31 | with: 32 | file: ./coverage.xml 33 | flags: unittests 34 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: isort 5 | name: "[Package] Import formatting" 6 | language: system 7 | entry: isort 8 | files: \.py$ 9 | 10 | - id: black 11 | name: "[Package] Code formatting" 12 | language: system 13 | entry: black 14 | files: \.py$ 15 | 16 | - id: flake8 17 | name: "[Package] Linting" 18 | language: system 19 | entry: flake8 20 | files: \.py$ 21 | 22 | - id: isort-examples 23 | name: "[Examples] Import formatting" 24 | language: system 25 | entry: nbqa isort 26 | files: examples/.+\.ipynb$ 27 | 28 | - id: black-examples 29 | name: "[Examples] Code formatting" 30 | language: system 31 | entry: nbqa black 32 | files: examples/.+\.ipynb$ 33 | 34 | - id: flake8-examples 35 | name: "[Examples] Linting" 36 | language: system 37 | entry: nbqa flake8 --ignore=E402 38 | files: examples/.+\.ipynb$ -------------------------------------------------------------------------------- /plotmol/styles.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class MoleculeStyle: 6 | """Options to control the appearance of the 2D structure of the drawn molecule that 7 | appears when a data point is hovered over. 8 | 9 | Attributes: 10 | 11 | image_width: The width [px] to make the image of the 2D structure. 12 | 13 | image_height: The height [px] to make the image of the 2D structure. 14 | 15 | highlight_tagged_atoms: Whether to highlight atoms which have been tagged with 16 | a map index, e.g. whether the oxygen atom should be highlighted for 17 | ``"C[O:1]"``. 18 | 19 | highlight_tagged_bonds: Whether to highlight bonds between atoms which have 20 | been tagged with a map index, e.g. whether the carbon-oxygen bond should be 21 | highlighted for ``"[C:1][O:2]"``. 22 | 23 | """ 24 | 25 | image_width: int = 200 26 | image_height: int = 200 27 | 28 | highlight_tagged_atoms: bool = True 29 | highlight_tagged_bonds: bool = True 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME := plotmol 2 | CONDA_ENV_RUN := conda run --no-capture-output --name $(PACKAGE_NAME) 3 | 4 | EXAMPLES_SKIP := 5 | EXAMPLES := $(filter-out $(EXAMPLES_SKIP), $(wildcard examples/*.ipynb)) 6 | 7 | .PHONY: env lint format test test-examples 8 | 9 | env: 10 | mamba create --name $(PACKAGE_NAME) 11 | mamba env update --name $(PACKAGE_NAME) --file devtools/envs/base.yaml 12 | $(CONDA_ENV_RUN) pip install --no-build-isolation --no-deps -e . 13 | $(CONDA_ENV_RUN) pre-commit install || true 14 | 15 | lint: 16 | $(CONDA_ENV_RUN) isort --check-only $(PACKAGE_NAME) 17 | $(CONDA_ENV_RUN) black --check $(PACKAGE_NAME) 18 | $(CONDA_ENV_RUN) flake8 $(PACKAGE_NAME) 19 | 20 | format: 21 | $(CONDA_ENV_RUN) isort $(PACKAGE_NAME) 22 | $(CONDA_ENV_RUN) black $(PACKAGE_NAME) 23 | $(CONDA_ENV_RUN) flake8 $(PACKAGE_NAME) 24 | 25 | test: 26 | $(CONDA_ENV_RUN) pytest -v --cov=$(PACKAGE_NAME) --cov-report=xml --color=yes $(PACKAGE_NAME)/tests/ 27 | 28 | test-examples: 29 | $(CONDA_ENV_RUN) jupyter nbconvert --to notebook --execute $(EXAMPLES) 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2023 Simon Boothroyd 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel", "versioneer", "rdkit", "bokeh"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "plotmol" 7 | description = "Interactive plotting of data annotated with molecule structures." 8 | authors = [ {name = "Simon Boothroyd"} ] 9 | license = { text = "MIT" } 10 | dynamic = ["version"] 11 | readme = "README.md" 12 | requires-python = ">=3.10" 13 | classifiers = ["Programming Language :: Python :: 3"] 14 | dependencies = ["rdkit", "bokeh"] 15 | 16 | [tool.setuptools] 17 | zip-safe = false 18 | include-package-data = true 19 | 20 | [tool.setuptools.dynamic] 21 | version = {attr = "plotmol.__version__"} 22 | 23 | [tool.setuptools.packages.find] 24 | namespaces = true 25 | where = ["."] 26 | 27 | [tool.versioneer] 28 | VCS = "git" 29 | style = "pep440" 30 | versionfile_source = "plotmol/_version.py" 31 | versionfile_build = "plotmol/_version.py" 32 | tag_prefix = "" 33 | parentdir_prefix = "plotmol-" 34 | 35 | [tool.black] 36 | line-length = 88 37 | 38 | [tool.isort] 39 | profile = "black" 40 | 41 | [tool.flake8] 42 | max-line-length = 88 43 | ignore = ["E203", "E266", "E501", "W503"] 44 | select = ["B","C","E","F","W","T4","B9"] 45 | 46 | [tool.coverage.run] 47 | omit = ["**/tests/*", "**/_version.py"] 48 | 49 | [tool.coverage.report] 50 | exclude_lines = [ 51 | "@overload", 52 | "pragma: no cover", 53 | "raise NotImplementedError", 54 | "if __name__ = .__main__.:", 55 | "if TYPE_CHECKING:", 56 | "if typing.TYPE_CHECKING:", 57 | ] 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive Plots with Molecular Annotations 2 | 3 | [![Test Status](https://github.com/simonboothroyd/plotmol/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/simonboothroyd/plotmol/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/SimonBoothroyd/plotmol/branch/main/graph/badge.svg?token=Aa8STE8WBZ)](https://codecov.io/gh/SimonBoothroyd/plotmol) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | This framework provides a set of functionalities for creating interactive plots that are annotated with molecular 8 | structures using the [`bokeh` library](https://docs.bokeh.org/en/latest/index.html). 9 | 10 | ## Installation 11 | 12 | The package and its dependencies can be installed using the `conda` package manager: 13 | 14 | ```shell 15 | conda install -c conda-forge plotmol 16 | ``` 17 | 18 | ## Getting Started 19 | 20 | See the [examples](examples) directory for an example of this framework in use. 21 | 22 | ## Development 23 | 24 | A development conda environment can be created and activated by running: 25 | 26 | ```shell 27 | make env 28 | conda activate plotmol 29 | ``` 30 | 31 | The environment will include all example and development dependencies, including linters and testing apparatus. 32 | 33 | Unit / example tests can be run using: 34 | 35 | ```shell 36 | make test 37 | make test-examples 38 | ``` 39 | 40 | The codebase can be formatted by running: 41 | 42 | ```shell 43 | make format 44 | ``` 45 | 46 | or checked for lint with: 47 | 48 | ```shell 49 | make lint 50 | ``` 51 | 52 | ## License 53 | 54 | The main package is release under the [MIT license](LICENSE). 55 | 56 | ## Copyright 57 | 58 | Copyright (c) 2023, Simon Boothroyd 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | .pytest_cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # profraw files from LLVM? Unclear exactly what triggers this 105 | # There are reports this comes from LLVM profiling, but also Xcode 9. 106 | *profraw 107 | 108 | # OSX 109 | .DS_Store 110 | 111 | # PyCharm 112 | .idea -------------------------------------------------------------------------------- /plotmol/utilities.py: -------------------------------------------------------------------------------- 1 | """Utility functions""" 2 | import functools 3 | 4 | from rdkit import Chem 5 | from rdkit.Chem.Draw import rdMolDraw2D 6 | 7 | from plotmol.styles import MoleculeStyle 8 | 9 | 10 | @functools.lru_cache(1024) 11 | def smiles_to_svg( 12 | smiles: str, 13 | style: MoleculeStyle, 14 | ) -> str: 15 | """Renders a 2D representation of a molecule based on its SMILES representation as 16 | an SVG string. 17 | 18 | Parameters 19 | ---------- 20 | smiles 21 | The SMILES pattern. 22 | style 23 | Options which control how the structure should be rendered as an image. 24 | 25 | Returns 26 | ------- 27 | The 2D SVG representation. 28 | """ 29 | 30 | # Parse the SMILES into an RDKit molecule 31 | smiles_parser = Chem.rdmolfiles.SmilesParserParams() 32 | smiles_parser.removeHs = False 33 | 34 | rdkit_molecule = Chem.MolFromSmiles(smiles, smiles_parser) 35 | 36 | # look for any tagged atom indices 37 | tagged_atoms = ( 38 | [] 39 | if not style.highlight_tagged_atoms 40 | else [ 41 | atom.GetIdx() 42 | for atom in rdkit_molecule.GetAtoms() 43 | if atom.GetAtomMapNum() != 0 44 | ] 45 | ) 46 | tagged_bonds = ( 47 | [] 48 | if not style.highlight_tagged_bonds 49 | else [ 50 | bond.GetIdx() 51 | for bond in rdkit_molecule.GetBonds() 52 | if bond.GetBeginAtom().GetAtomMapNum() != 0 53 | and bond.GetEndAtom().GetAtomMapNum() != 0 54 | ] 55 | ) 56 | 57 | # Generate a set of 2D coordinates. 58 | if not rdkit_molecule.GetNumConformers(): 59 | Chem.rdDepictor.Compute2DCoords(rdkit_molecule) 60 | 61 | drawer = rdMolDraw2D.MolDraw2DSVG(style.image_width, style.image_height) 62 | rdMolDraw2D.PrepareAndDrawMolecule( 63 | drawer, 64 | rdkit_molecule, 65 | highlightAtoms=tagged_atoms, 66 | highlightBonds=tagged_bonds, 67 | ) 68 | drawer.FinishDrawing() 69 | 70 | svg_content = drawer.GetDrawingText() 71 | return svg_content 72 | -------------------------------------------------------------------------------- /plotmol/tests/test_plotmol.py: -------------------------------------------------------------------------------- 1 | import bokeh.plotting 2 | import pytest 3 | 4 | import plotmol 5 | import plotmol.styles 6 | 7 | 8 | @pytest.fixture() 9 | def bokeh_figure(): 10 | return bokeh.plotting.figure(tooltips=plotmol.default_tooltip_template()) 11 | 12 | 13 | def test_scatter(bokeh_figure): 14 | # It's difficult to test this as it's a graphical function. For now make sure 15 | # it doesn't error and something is produced. 16 | 17 | # Plot the data as an interactive scatter plot 18 | plotmol.scatter( 19 | bokeh_figure, 20 | x=[0.0, 1.0], 21 | y=[0.0, 1.0], 22 | smiles=["C", "CCO"], 23 | marker="x", 24 | marker_size=15, 25 | legend_label="Series A", 26 | ) 27 | 28 | 29 | def test_scatter_tagged_atoms(bokeh_figure): 30 | # again difficult to test but make sure a valid plot is made 31 | plotmol.scatter( 32 | bokeh_figure, 33 | x=[0.0], 34 | y=[0.0], 35 | smiles=["[H][c]1[n][c]([H])[c:2]([O:3][C:4]([H])([H])[H])[c:1]([H])[c]1[H]"], 36 | marker="x", 37 | marker_size=15, 38 | legend_label="Series A", 39 | ) 40 | 41 | 42 | def test_scatter_invalid_input_size(bokeh_figure): 43 | with pytest.raises(plotmol.InputSizeError): 44 | plotmol.scatter(bokeh_figure, x=[0.0], y=[0.0, 1.0], smiles=["C", "CCO"]) 45 | 46 | 47 | def test_scatter_custom_molecule_function(bokeh_figure): 48 | function_called = False 49 | 50 | def molecule_to_svg(smiles, style): 51 | nonlocal function_called 52 | function_called = True 53 | 54 | assert smiles in ["C", "CCO"] 55 | assert style.image_width == 1 56 | 57 | return "" 58 | 59 | plotmol.scatter( 60 | bokeh_figure, 61 | x=[0.0, 1.0], 62 | y=[0.0, 1.0], 63 | smiles=["C", "CCO"], 64 | molecule_style=plotmol.styles.MoleculeStyle(image_width=1), 65 | molecule_to_image_function=molecule_to_svg, 66 | ) 67 | 68 | assert function_called is True 69 | 70 | 71 | def test_scatter_invalid_custom_data_size(bokeh_figure): 72 | expected_match = r"all have the same length: title \(2\)" 73 | 74 | with pytest.raises(plotmol.InputSizeError, match=expected_match): 75 | plotmol.scatter( 76 | bokeh_figure, 77 | x=[0.0], 78 | y=[0.0], 79 | smiles=["C"], 80 | custom_column_data={"title": ["A", "B"]}, 81 | ) 82 | 83 | 84 | def test_scatter_custom_data(bokeh_figure): 85 | glyph = plotmol.scatter( 86 | bokeh_figure, 87 | x=[0.0], 88 | y=[0.0], 89 | smiles=["C"], 90 | marker="x", 91 | marker_size=15, 92 | legend_label="Series A", 93 | custom_column_data={"title": ["A"]}, 94 | ) 95 | 96 | assert "x" in glyph.data_source.data 97 | assert "y" in glyph.data_source.data 98 | assert "smiles" in glyph.data_source.data 99 | assert "title" in glyph.data_source.data 100 | -------------------------------------------------------------------------------- /plotmol/_plotmol.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import typing 3 | 4 | import bokeh.models 5 | import bokeh.plotting 6 | 7 | import plotmol.styles 8 | import plotmol.utilities 9 | 10 | DataType = float | int | str 11 | 12 | MoleculeToImageFunction = typing.Callable[[str, plotmol.styles.MoleculeStyle], str] 13 | 14 | 15 | class InputSizeError(ValueError): 16 | """An error raised when inputs of different sizes are provided to a plot 17 | function.""" 18 | 19 | def __init__(self, sizes: dict[str, int] | None = None): 20 | sizes = {} if sizes is None else sizes 21 | sizes_str = ", ".join(f"{label} ({size})" for label, size in sizes.items()) 22 | 23 | super(InputSizeError, self).__init__( 24 | f"The input data arrays must all have the same length: {sizes_str}." 25 | ) 26 | 27 | 28 | def default_tooltip_template() -> str: 29 | """Returns the default html based template to use for the figure hover pop-up.""" 30 | 31 | return """ 32 |
33 |
34 | 35 |
36 |
37 | """ 38 | 39 | 40 | def scatter( 41 | figure: bokeh.plotting.figure, 42 | x: list[DataType], 43 | y: list[DataType], 44 | smiles: list[str], 45 | legend_label: str | None = None, 46 | marker: str | None = None, 47 | marker_size: int | None = None, 48 | marker_color: str | None = None, 49 | molecule_style: plotmol.styles.MoleculeStyle | None = None, 50 | molecule_to_image_function: MoleculeToImageFunction | None = None, 51 | custom_column_data: dict[str, list[DataType]] | None = None, 52 | **kwargs: dict[str, typing.Any], 53 | ) -> bokeh.models.GlyphRenderer: 54 | """Adds a scatter series to a bokeh figure which will show the molecular 55 | structure associated with a data point when the user hovers over it. 56 | 57 | Args: 58 | figure: The bokeh figure to plot the scatter data on. 59 | x: An array of the x values to plot. 60 | y: An array of the y values to plot. 61 | smiles: An array of the SMILES patterns associated with each (x, y) pair. 62 | legend_label: The label to show in the legend for this data series. 63 | marker: The marker style. 64 | marker_size: The size of the marker to draw. 65 | marker_color: The marker color. 66 | molecule_style: Options which control how the 2D structure which is shown when a 67 | data point is hovered over should be rendered. 68 | molecule_to_image_function: The function which should be used to render the 69 | molecule as a 2D SVG image. This function should accept a string SMILES 70 | pattern and a molecule style object and return a valid SVG string (e.g. 71 | ``"..."``). By default the ``plotmol.rdkit.smiles_to_svg`` 72 | function will be used. 73 | custom_column_data: An optional dictionary of extra entries to include in the 74 | ``ColumnDataSource`` used to render the plot. These custom entries can be 75 | accessed from a figures tooltip and used, for example, to add extra outputs 76 | such as a tooltip title or additional properties associated with a data 77 | point. 78 | 79 | By default the data included are the ``x``, ``y``, and ``smiles`` lists. 80 | 81 | Each key will correspond to a '@key' that will be made available to 82 | the tooltip template (see `the Bokeh documentation 83 | `_ 84 | for more details) and each value must be a list of the corresponding values 85 | with a length equal to ``x`` and ``y``. 86 | kwargs: Extra keyword arguments to pass to the underlying bokeh ``scatter`` 87 | function. 88 | """ 89 | 90 | molecule_style = ( 91 | molecule_style if molecule_style is not None else plotmol.styles.MoleculeStyle() 92 | ) 93 | 94 | molecule_to_image_function = ( 95 | molecule_to_image_function 96 | if molecule_to_image_function is not None 97 | else plotmol.utilities.smiles_to_svg 98 | ) 99 | 100 | # Validate the sizes of the input arrays. 101 | data_sizes = {"x": len(x), "y": len(y), "smiles": len(smiles)} 102 | 103 | if len({*data_sizes.values()}) != 1: 104 | raise InputSizeError(data_sizes) 105 | 106 | # Generate an image for each SMILES pattern. 107 | raw_images = [ 108 | base64.b64encode( 109 | molecule_to_image_function(smiles_pattern, molecule_style).encode() 110 | ).decode() 111 | for smiles_pattern in smiles 112 | ] 113 | images = [f"data:image/svg+xml;base64,{raw_image}" for raw_image in raw_images] 114 | 115 | # Create a custom data source. 116 | column_data = {"x": x, "y": y, "smiles": smiles, "image": images} 117 | 118 | if custom_column_data is not None: 119 | invalid_sizes = { 120 | key: len(entry) 121 | for key, entry in custom_column_data.items() 122 | if len(entry) != len(x) 123 | } 124 | 125 | if len(invalid_sizes) > 0: 126 | raise InputSizeError(invalid_sizes) 127 | 128 | column_data.update(custom_column_data) 129 | 130 | source = bokeh.models.ColumnDataSource(data=column_data) 131 | 132 | # Add the scatter data. 133 | scatter_kwargs = {**kwargs} 134 | 135 | if marker is not None: 136 | scatter_kwargs["marker"] = marker 137 | if marker_size is not None: 138 | scatter_kwargs["size"] = marker_size 139 | if marker_color is not None: 140 | scatter_kwargs["color"] = marker_color 141 | if legend_label is not None: 142 | scatter_kwargs["legend_label"] = legend_label 143 | 144 | return figure.scatter(x="x", y="y", source=source, **scatter_kwargs) 145 | -------------------------------------------------------------------------------- /plotmol/_version.py: -------------------------------------------------------------------------------- 1 | # This file helps to compute a version number in source trees obtained from 2 | # git-archive tarball (such as those provided by githubs download-from-tag 3 | # feature). Distribution tarballs (built by setup.py sdist) and build 4 | # directories (produced by setup.py build) will contain a much shorter file 5 | # that just contains the computed version number. 6 | 7 | # This file is released into the public domain. 8 | # Generated by versioneer-0.29 9 | # https://github.com/python-versioneer/python-versioneer 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import functools 15 | import os 16 | import re 17 | import subprocess 18 | import sys 19 | from collections.abc import Callable 20 | from typing import Any 21 | 22 | 23 | def get_keywords() -> dict[str, str]: 24 | """Get the keywords needed to look up the version information.""" 25 | # these strings will be replaced by git during git-archive. 26 | # setup.py/versioneer.py will grep for the variable names, so they must 27 | # each be defined on a line of their own. _version.py will just call 28 | # get_keywords(). 29 | git_refnames = " (HEAD -> main)" 30 | git_full = "892b0f8647eb6cf64cd4ba21d682b2bab09f1cb1" 31 | git_date = "2023-11-30 22:42:58 +0000" 32 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 33 | return keywords 34 | 35 | 36 | class VersioneerConfig: 37 | """Container for Versioneer configuration parameters.""" 38 | 39 | VCS: str 40 | style: str 41 | tag_prefix: str 42 | parentdir_prefix: str 43 | versionfile_source: str 44 | verbose: bool 45 | 46 | 47 | def get_config() -> VersioneerConfig: 48 | """Create, populate and return the VersioneerConfig() object.""" 49 | # these strings are filled in when 'setup.py versioneer' creates 50 | # _version.py 51 | cfg = VersioneerConfig() 52 | cfg.VCS = "git" 53 | cfg.style = "pep440" 54 | cfg.tag_prefix = "" 55 | cfg.parentdir_prefix = "plotmol-" 56 | cfg.versionfile_source = "plotmol/_version.py" 57 | cfg.verbose = False 58 | return cfg 59 | 60 | 61 | class NotThisMethod(Exception): 62 | """Exception raised if a method is not valid for the current scenario.""" 63 | 64 | 65 | LONG_VERSION_PY: dict[str, str] = {} 66 | HANDLERS: dict[str, dict[str, Callable]] = {} 67 | 68 | 69 | def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator 70 | """Create decorator to mark a method as the handler of a VCS.""" 71 | 72 | def decorate(f: Callable) -> Callable: 73 | """Store f in HANDLERS[vcs][method].""" 74 | if vcs not in HANDLERS: 75 | HANDLERS[vcs] = {} 76 | HANDLERS[vcs][method] = f 77 | return f 78 | 79 | return decorate 80 | 81 | 82 | def run_command( 83 | commands: list[str], 84 | args: list[str], 85 | cwd: str | None = None, 86 | verbose: bool = False, 87 | hide_stderr: bool = False, 88 | env: dict[str, str] | None = None, 89 | ) -> tuple[str | None, int | None]: 90 | """Call the given command(s).""" 91 | assert isinstance(commands, list) 92 | process = None 93 | 94 | popen_kwargs: dict[str, Any] = {} 95 | if sys.platform == "win32": 96 | # This hides the console window if pythonw.exe is used 97 | startupinfo = subprocess.STARTUPINFO() 98 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 99 | popen_kwargs["startupinfo"] = startupinfo 100 | 101 | for command in commands: 102 | try: 103 | dispcmd = str([command] + args) 104 | # remember shell=False, so use git.cmd on windows, not just git 105 | process = subprocess.Popen( 106 | [command] + args, 107 | cwd=cwd, 108 | env=env, 109 | stdout=subprocess.PIPE, 110 | stderr=(subprocess.PIPE if hide_stderr else None), 111 | **popen_kwargs, 112 | ) 113 | break 114 | except OSError as e: 115 | if e.errno == errno.ENOENT: 116 | continue 117 | if verbose: 118 | print("unable to run %s" % dispcmd) 119 | print(e) 120 | return None, None 121 | else: 122 | if verbose: 123 | print("unable to find command, tried {}".format(commands)) 124 | return None, None 125 | stdout = process.communicate()[0].strip().decode() 126 | if process.returncode != 0: 127 | if verbose: 128 | print("unable to run %s (error)" % dispcmd) 129 | print("stdout was %s" % stdout) 130 | return None, process.returncode 131 | return stdout, process.returncode 132 | 133 | 134 | def versions_from_parentdir( 135 | parentdir_prefix: str, 136 | root: str, 137 | verbose: bool, 138 | ) -> dict[str, Any]: 139 | """Try to determine the version from the parent directory name. 140 | 141 | Source tarballs conventionally unpack into a directory that includes both 142 | the project name and a version string. We will also support searching up 143 | two directory levels for an appropriately named parent directory 144 | """ 145 | rootdirs = [] 146 | 147 | for _ in range(3): 148 | dirname = os.path.basename(root) 149 | if dirname.startswith(parentdir_prefix): 150 | return { 151 | "version": dirname[len(parentdir_prefix) :], 152 | "full-revisionid": None, 153 | "dirty": False, 154 | "error": None, 155 | "date": None, 156 | } 157 | rootdirs.append(root) 158 | root = os.path.dirname(root) # up a level 159 | 160 | if verbose: 161 | print( 162 | "Tried directories %s but none started with prefix %s" 163 | % (str(rootdirs), parentdir_prefix) 164 | ) 165 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 166 | 167 | 168 | @register_vcs_handler("git", "get_keywords") 169 | def git_get_keywords(versionfile_abs: str) -> dict[str, str]: 170 | """Extract version information from the given file.""" 171 | # the code embedded in _version.py can just fetch the value of these 172 | # keywords. When used from setup.py, we don't want to import _version.py, 173 | # so we do it with a regexp instead. This function is not used from 174 | # _version.py. 175 | keywords: dict[str, str] = {} 176 | try: 177 | with open(versionfile_abs) as fobj: 178 | for line in fobj: 179 | if line.strip().startswith("git_refnames ="): 180 | mo = re.search(r'=\s*"(.*)"', line) 181 | if mo: 182 | keywords["refnames"] = mo.group(1) 183 | if line.strip().startswith("git_full ="): 184 | mo = re.search(r'=\s*"(.*)"', line) 185 | if mo: 186 | keywords["full"] = mo.group(1) 187 | if line.strip().startswith("git_date ="): 188 | mo = re.search(r'=\s*"(.*)"', line) 189 | if mo: 190 | keywords["date"] = mo.group(1) 191 | except OSError: 192 | pass 193 | return keywords 194 | 195 | 196 | @register_vcs_handler("git", "keywords") 197 | def git_versions_from_keywords( 198 | keywords: dict[str, str], 199 | tag_prefix: str, 200 | verbose: bool, 201 | ) -> dict[str, Any]: 202 | """Get version information from git keywords.""" 203 | if "refnames" not in keywords: 204 | raise NotThisMethod("Short version file found") 205 | date = keywords.get("date") 206 | if date is not None: 207 | # Use only the last line. Previous lines may contain GPG signature 208 | # information. 209 | date = date.splitlines()[-1] 210 | 211 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 212 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 213 | # -like" string, which we must then edit to make compliant), because 214 | # it's been around since git-1.5.3, and it's too difficult to 215 | # discover which version we're using, or to work around using an 216 | # older one. 217 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 218 | refnames = keywords["refnames"].strip() 219 | if refnames.startswith("$Format"): 220 | if verbose: 221 | print("keywords are unexpanded, not using") 222 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 223 | refs = {r.strip() for r in refnames.strip("()").split(",")} 224 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 225 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 226 | TAG = "tag: " 227 | tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} 228 | if not tags: 229 | # Either we're using git < 1.8.3, or there really are no tags. We use 230 | # a heuristic: assume all version tags have a digit. The old git %d 231 | # expansion behaves like git log --decorate=short and strips out the 232 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 233 | # between branches and tags. By ignoring refnames without digits, we 234 | # filter out many common branch names like "release" and 235 | # "stabilization", as well as "HEAD" and "master". 236 | tags = {r for r in refs if re.search(r"\d", r)} 237 | if verbose: 238 | print("discarding '%s', no digits" % ",".join(refs - tags)) 239 | if verbose: 240 | print("likely tags: %s" % ",".join(sorted(tags))) 241 | for ref in sorted(tags): 242 | # sorting will prefer e.g. "2.0" over "2.0rc1" 243 | if ref.startswith(tag_prefix): 244 | r = ref[len(tag_prefix) :] 245 | # Filter out refs that exactly match prefix or that don't start 246 | # with a number once the prefix is stripped (mostly a concern 247 | # when prefix is '') 248 | if not re.match(r"\d", r): 249 | continue 250 | if verbose: 251 | print("picking %s" % r) 252 | return { 253 | "version": r, 254 | "full-revisionid": keywords["full"].strip(), 255 | "dirty": False, 256 | "error": None, 257 | "date": date, 258 | } 259 | # no suitable tags, so version is "0+unknown", but full hex is still there 260 | if verbose: 261 | print("no suitable tags, using unknown + full revision id") 262 | return { 263 | "version": "0+unknown", 264 | "full-revisionid": keywords["full"].strip(), 265 | "dirty": False, 266 | "error": "no suitable tags", 267 | "date": None, 268 | } 269 | 270 | 271 | @register_vcs_handler("git", "pieces_from_vcs") 272 | def git_pieces_from_vcs( 273 | tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command 274 | ) -> dict[str, Any]: 275 | """Get version from 'git describe' in the root of the source tree. 276 | 277 | This only gets called if the git-archive 'subst' keywords were *not* 278 | expanded, and _version.py hasn't already been rewritten with a short 279 | version string, meaning we're inside a checked out source tree. 280 | """ 281 | GITS = ["git"] 282 | if sys.platform == "win32": 283 | GITS = ["git.cmd", "git.exe"] 284 | 285 | # GIT_DIR can interfere with correct operation of Versioneer. 286 | # It may be intended to be passed to the Versioneer-versioned project, 287 | # but that should not change where we get our version from. 288 | env = os.environ.copy() 289 | env.pop("GIT_DIR", None) 290 | runner = functools.partial(runner, env=env) 291 | 292 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) 293 | if rc != 0: 294 | if verbose: 295 | print("Directory %s not under git control" % root) 296 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 297 | 298 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 299 | # if there isn't one, this yields HEX[-dirty] (no NUM) 300 | describe_out, rc = runner( 301 | GITS, 302 | [ 303 | "describe", 304 | "--tags", 305 | "--dirty", 306 | "--always", 307 | "--long", 308 | "--match", 309 | f"{tag_prefix}[[:digit:]]*", 310 | ], 311 | cwd=root, 312 | ) 313 | # --long was added in git-1.5.5 314 | if describe_out is None: 315 | raise NotThisMethod("'git describe' failed") 316 | describe_out = describe_out.strip() 317 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 318 | if full_out is None: 319 | raise NotThisMethod("'git rev-parse' failed") 320 | full_out = full_out.strip() 321 | 322 | pieces: dict[str, Any] = {} 323 | pieces["long"] = full_out 324 | pieces["short"] = full_out[:7] # maybe improved later 325 | pieces["error"] = None 326 | 327 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) 328 | # --abbrev-ref was added in git-1.6.3 329 | if rc != 0 or branch_name is None: 330 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 331 | branch_name = branch_name.strip() 332 | 333 | if branch_name == "HEAD": 334 | # If we aren't exactly on a branch, pick a branch which represents 335 | # the current commit. If all else fails, we are on a branchless 336 | # commit. 337 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 338 | # --contains was added in git-1.5.4 339 | if rc != 0 or branches is None: 340 | raise NotThisMethod("'git branch --contains' returned error") 341 | branches = branches.split("\n") 342 | 343 | # Remove the first line if we're running detached 344 | if "(" in branches[0]: 345 | branches.pop(0) 346 | 347 | # Strip off the leading "* " from the list of branches. 348 | branches = [branch[2:] for branch in branches] 349 | if "master" in branches: 350 | branch_name = "master" 351 | elif not branches: 352 | branch_name = None 353 | else: 354 | # Pick the first branch that is returned. Good or bad. 355 | branch_name = branches[0] 356 | 357 | pieces["branch"] = branch_name 358 | 359 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 360 | # TAG might have hyphens. 361 | git_describe = describe_out 362 | 363 | # look for -dirty suffix 364 | dirty = git_describe.endswith("-dirty") 365 | pieces["dirty"] = dirty 366 | if dirty: 367 | git_describe = git_describe[: git_describe.rindex("-dirty")] 368 | 369 | # now we have TAG-NUM-gHEX or HEX 370 | 371 | if "-" in git_describe: 372 | # TAG-NUM-gHEX 373 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 374 | if not mo: 375 | # unparsable. Maybe git-describe is misbehaving? 376 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 377 | return pieces 378 | 379 | # tag 380 | full_tag = mo.group(1) 381 | if not full_tag.startswith(tag_prefix): 382 | if verbose: 383 | fmt = "tag '%s' doesn't start with prefix '%s'" 384 | print(fmt % (full_tag, tag_prefix)) 385 | pieces["error"] = "tag '{}' doesn't start with prefix '{}'".format( 386 | full_tag, 387 | tag_prefix, 388 | ) 389 | return pieces 390 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 391 | 392 | # distance: number of commits since tag 393 | pieces["distance"] = int(mo.group(2)) 394 | 395 | # commit: short hex revision ID 396 | pieces["short"] = mo.group(3) 397 | 398 | else: 399 | # HEX: no tags 400 | pieces["closest-tag"] = None 401 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 402 | pieces["distance"] = len(out.split()) # total number of commits 403 | 404 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 405 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 406 | # Use only the last line. Previous lines may contain GPG signature 407 | # information. 408 | date = date.splitlines()[-1] 409 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 410 | 411 | return pieces 412 | 413 | 414 | def plus_or_dot(pieces: dict[str, Any]) -> str: 415 | """Return a + if we don't already have one, else return a .""" 416 | if "+" in pieces.get("closest-tag", ""): 417 | return "." 418 | return "+" 419 | 420 | 421 | def render_pep440(pieces: dict[str, Any]) -> str: 422 | """Build up version string, with post-release "local version identifier". 423 | 424 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 425 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 426 | 427 | Exceptions: 428 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 429 | """ 430 | if pieces["closest-tag"]: 431 | rendered = pieces["closest-tag"] 432 | if pieces["distance"] or pieces["dirty"]: 433 | rendered += plus_or_dot(pieces) 434 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 435 | if pieces["dirty"]: 436 | rendered += ".dirty" 437 | else: 438 | # exception #1 439 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 440 | if pieces["dirty"]: 441 | rendered += ".dirty" 442 | return rendered 443 | 444 | 445 | def render_pep440_branch(pieces: dict[str, Any]) -> str: 446 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 447 | 448 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 449 | (a feature branch will appear "older" than the master branch). 450 | 451 | Exceptions: 452 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 453 | """ 454 | if pieces["closest-tag"]: 455 | rendered = pieces["closest-tag"] 456 | if pieces["distance"] or pieces["dirty"]: 457 | if pieces["branch"] != "master": 458 | rendered += ".dev0" 459 | rendered += plus_or_dot(pieces) 460 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 461 | if pieces["dirty"]: 462 | rendered += ".dirty" 463 | else: 464 | # exception #1 465 | rendered = "0" 466 | if pieces["branch"] != "master": 467 | rendered += ".dev0" 468 | rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 469 | if pieces["dirty"]: 470 | rendered += ".dirty" 471 | return rendered 472 | 473 | 474 | def pep440_split_post(ver: str) -> tuple[str, int | None]: 475 | """Split pep440 version string at the post-release segment. 476 | 477 | Returns the release segments before the post-release and the 478 | post-release version number (or -1 if no post-release segment is present). 479 | """ 480 | vc = str.split(ver, ".post") 481 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 482 | 483 | 484 | def render_pep440_pre(pieces: dict[str, Any]) -> str: 485 | """TAG[.postN.devDISTANCE] -- No -dirty. 486 | 487 | Exceptions: 488 | 1: no tags. 0.post0.devDISTANCE 489 | """ 490 | if pieces["closest-tag"]: 491 | if pieces["distance"]: 492 | # update the post release segment 493 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 494 | rendered = tag_version 495 | if post_version is not None: 496 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 497 | else: 498 | rendered += ".post0.dev%d" % (pieces["distance"]) 499 | else: 500 | # no commits, use the tag as the version 501 | rendered = pieces["closest-tag"] 502 | else: 503 | # exception #1 504 | rendered = "0.post0.dev%d" % pieces["distance"] 505 | return rendered 506 | 507 | 508 | def render_pep440_post(pieces: dict[str, Any]) -> str: 509 | """TAG[.postDISTANCE[.dev0]+gHEX] . 510 | 511 | The ".dev0" means dirty. Note that .dev0 sorts backwards 512 | (a dirty tree will appear "older" than the corresponding clean one), 513 | but you shouldn't be releasing software with -dirty anyways. 514 | 515 | Exceptions: 516 | 1: no tags. 0.postDISTANCE[.dev0] 517 | """ 518 | if pieces["closest-tag"]: 519 | rendered = pieces["closest-tag"] 520 | if pieces["distance"] or pieces["dirty"]: 521 | rendered += ".post%d" % pieces["distance"] 522 | if pieces["dirty"]: 523 | rendered += ".dev0" 524 | rendered += plus_or_dot(pieces) 525 | rendered += "g%s" % pieces["short"] 526 | else: 527 | # exception #1 528 | rendered = "0.post%d" % pieces["distance"] 529 | if pieces["dirty"]: 530 | rendered += ".dev0" 531 | rendered += "+g%s" % pieces["short"] 532 | return rendered 533 | 534 | 535 | def render_pep440_post_branch(pieces: dict[str, Any]) -> str: 536 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 537 | 538 | The ".dev0" means not master branch. 539 | 540 | Exceptions: 541 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 542 | """ 543 | if pieces["closest-tag"]: 544 | rendered = pieces["closest-tag"] 545 | if pieces["distance"] or pieces["dirty"]: 546 | rendered += ".post%d" % pieces["distance"] 547 | if pieces["branch"] != "master": 548 | rendered += ".dev0" 549 | rendered += plus_or_dot(pieces) 550 | rendered += "g%s" % pieces["short"] 551 | if pieces["dirty"]: 552 | rendered += ".dirty" 553 | else: 554 | # exception #1 555 | rendered = "0.post%d" % pieces["distance"] 556 | if pieces["branch"] != "master": 557 | rendered += ".dev0" 558 | rendered += "+g%s" % pieces["short"] 559 | if pieces["dirty"]: 560 | rendered += ".dirty" 561 | return rendered 562 | 563 | 564 | def render_pep440_old(pieces: dict[str, Any]) -> str: 565 | """TAG[.postDISTANCE[.dev0]] . 566 | 567 | The ".dev0" means dirty. 568 | 569 | Exceptions: 570 | 1: no tags. 0.postDISTANCE[.dev0] 571 | """ 572 | if pieces["closest-tag"]: 573 | rendered = pieces["closest-tag"] 574 | if pieces["distance"] or pieces["dirty"]: 575 | rendered += ".post%d" % pieces["distance"] 576 | if pieces["dirty"]: 577 | rendered += ".dev0" 578 | else: 579 | # exception #1 580 | rendered = "0.post%d" % pieces["distance"] 581 | if pieces["dirty"]: 582 | rendered += ".dev0" 583 | return rendered 584 | 585 | 586 | def render_git_describe(pieces: dict[str, Any]) -> str: 587 | """TAG[-DISTANCE-gHEX][-dirty]. 588 | 589 | Like 'git describe --tags --dirty --always'. 590 | 591 | Exceptions: 592 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 593 | """ 594 | if pieces["closest-tag"]: 595 | rendered = pieces["closest-tag"] 596 | if pieces["distance"]: 597 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 598 | else: 599 | # exception #1 600 | rendered = pieces["short"] 601 | if pieces["dirty"]: 602 | rendered += "-dirty" 603 | return rendered 604 | 605 | 606 | def render_git_describe_long(pieces: dict[str, Any]) -> str: 607 | """TAG-DISTANCE-gHEX[-dirty]. 608 | 609 | Like 'git describe --tags --dirty --always -long'. 610 | The distance/hash is unconditional. 611 | 612 | Exceptions: 613 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 614 | """ 615 | if pieces["closest-tag"]: 616 | rendered = pieces["closest-tag"] 617 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 618 | else: 619 | # exception #1 620 | rendered = pieces["short"] 621 | if pieces["dirty"]: 622 | rendered += "-dirty" 623 | return rendered 624 | 625 | 626 | def render(pieces: dict[str, Any], style: str) -> dict[str, Any]: 627 | """Render the given version pieces into the requested style.""" 628 | if pieces["error"]: 629 | return { 630 | "version": "unknown", 631 | "full-revisionid": pieces.get("long"), 632 | "dirty": None, 633 | "error": pieces["error"], 634 | "date": None, 635 | } 636 | 637 | if not style or style == "default": 638 | style = "pep440" # the default 639 | 640 | if style == "pep440": 641 | rendered = render_pep440(pieces) 642 | elif style == "pep440-branch": 643 | rendered = render_pep440_branch(pieces) 644 | elif style == "pep440-pre": 645 | rendered = render_pep440_pre(pieces) 646 | elif style == "pep440-post": 647 | rendered = render_pep440_post(pieces) 648 | elif style == "pep440-post-branch": 649 | rendered = render_pep440_post_branch(pieces) 650 | elif style == "pep440-old": 651 | rendered = render_pep440_old(pieces) 652 | elif style == "git-describe": 653 | rendered = render_git_describe(pieces) 654 | elif style == "git-describe-long": 655 | rendered = render_git_describe_long(pieces) 656 | else: 657 | raise ValueError("unknown style '%s'" % style) 658 | 659 | return { 660 | "version": rendered, 661 | "full-revisionid": pieces["long"], 662 | "dirty": pieces["dirty"], 663 | "error": None, 664 | "date": pieces.get("date"), 665 | } 666 | 667 | 668 | def get_versions() -> dict[str, Any]: 669 | """Get version information or return default if unable to do so.""" 670 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 671 | # __file__, we can work backwards from there to the root. Some 672 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 673 | # case we can only use expanded keywords. 674 | 675 | cfg = get_config() 676 | verbose = cfg.verbose 677 | 678 | try: 679 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) 680 | except NotThisMethod: 681 | pass 682 | 683 | try: 684 | root = os.path.realpath(__file__) 685 | # versionfile_source is the relative path from the top of the source 686 | # tree (where the .git directory might live) to this file. Invert 687 | # this to find the root from __file__. 688 | for _ in cfg.versionfile_source.split("/"): 689 | root = os.path.dirname(root) 690 | except NameError: 691 | return { 692 | "version": "0+unknown", 693 | "full-revisionid": None, 694 | "dirty": None, 695 | "error": "unable to find root of source tree", 696 | "date": None, 697 | } 698 | 699 | try: 700 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 701 | return render(pieces, cfg.style) 702 | except NotThisMethod: 703 | pass 704 | 705 | try: 706 | if cfg.parentdir_prefix: 707 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 708 | except NotThisMethod: 709 | pass 710 | 711 | return { 712 | "version": "0+unknown", 713 | "full-revisionid": None, 714 | "dirty": None, 715 | "error": "unable to compute version", 716 | "date": None, 717 | } 718 | -------------------------------------------------------------------------------- /examples/scatter-plot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "In order to use the plotting functionality within a Jupyter notebook we first need to configure\n", 7 | "`bokeh` to run in notebook mode:" 8 | ], 9 | "metadata": { 10 | "collapsed": false 11 | } 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/html": " \n
\n \n Loading BokehJS ...\n
\n" 20 | }, 21 | "metadata": {}, 22 | "output_type": "display_data" 23 | }, 24 | { 25 | "data": { 26 | "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n function drop(id) {\n const view = Bokeh.index.get_by_id(id)\n if (view != null) {\n view.model.document.clear()\n Bokeh.index.delete(view)\n }\n }\n\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n\n // Clean up Bokeh references\n if (id != null) {\n drop(id)\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim()\n drop(id)\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n const el = document.getElementById(\"ec927e22-8cb2-4a50-b7cc-8f7808d90641\");\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.3.1.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\nif (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"ec927e22-8cb2-4a50-b7cc-8f7808d90641\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));", 27 | "application/vnd.bokehjs_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n const el = document.getElementById(\"ec927e22-8cb2-4a50-b7cc-8f7808d90641\");\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.3.1.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\nif (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"ec927e22-8cb2-4a50-b7cc-8f7808d90641\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));" 28 | }, 29 | "metadata": {}, 30 | "output_type": "display_data" 31 | } 32 | ], 33 | "source": [ 34 | "import bokeh.io\n", 35 | "\n", 36 | "bokeh.io.output_notebook()" 37 | ], 38 | "metadata": { 39 | "collapsed": false, 40 | "pycharm": { 41 | "name": "#%%\n" 42 | }, 43 | "ExecuteTime": { 44 | "end_time": "2023-11-29T13:28:35.348274Z", 45 | "start_time": "2023-11-29T13:28:35.336218Z" 46 | } 47 | } 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "source": [ 52 | "Next, similar to with `matplotlib`, we create a figure that we will render to." 53 | ], 54 | "metadata": { 55 | "collapsed": false 56 | } 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "outputs": [], 62 | "source": [ 63 | "import bokeh.plotting\n", 64 | "\n", 65 | "import plotmol\n", 66 | "\n", 67 | "figure = bokeh.plotting.figure(\n", 68 | " # Required to show the molecule structure pop-up.\n", 69 | " tooltips=plotmol.default_tooltip_template(),\n", 70 | " # Plot options. See the ``bokeh`` documentation for more information.\n", 71 | " title=\"My Dummy Data\",\n", 72 | " x_axis_label=\"Dummy X\",\n", 73 | " y_axis_label=\"Dummy Y\",\n", 74 | " width=500,\n", 75 | " height=500,\n", 76 | ")" 77 | ], 78 | "metadata": { 79 | "collapsed": false, 80 | "pycharm": { 81 | "name": "#%%\n" 82 | }, 83 | "ExecuteTime": { 84 | "end_time": "2023-11-29T13:28:36.058676Z", 85 | "start_time": "2023-11-29T13:28:35.349649Z" 86 | } 87 | } 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "source": [ 92 | "Data can then be rendered on the figure similar to `matplotlib` using the `plotmol.scatter` function:" 93 | ], 94 | "metadata": { 95 | "collapsed": false 96 | } 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 3, 101 | "outputs": [ 102 | { 103 | "data": { 104 | "text/plain": "GlyphRenderer(id='p1054', ...)", 105 | "text/html": "
GlyphRenderer(
id = 'p1054', …)
coordinates = None,
data_source = ColumnDataSource(id='p1045', ...),
glyph = Scatter(id='p1051', ...),
group = None,
hover_glyph = None,
js_event_callbacks = {},
js_property_callbacks = {},
level = 'glyph',
muted = False,
muted_glyph = Scatter(id='p1053', ...),
name = None,
nonselection_glyph = Scatter(id='p1052', ...),
propagate_hover = False,
selection_glyph = 'auto',
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
view = CDSView(id='p1055', ...),
visible = True,
x_range_name = 'default',
y_range_name = 'default')
\n\n" 106 | }, 107 | "execution_count": 3, 108 | "metadata": {}, 109 | "output_type": "execute_result" 110 | } 111 | ], 112 | "source": [ 113 | "import bokeh.palettes\n", 114 | "\n", 115 | "import plotmol\n", 116 | "\n", 117 | "# Define a simple color palette.\n", 118 | "palette = bokeh.palettes.Spectral4\n", 119 | "\n", 120 | "# Plot some dummy data to the figure.\n", 121 | "plotmol.scatter(\n", 122 | " figure,\n", 123 | " x=[0.0, 1.0, 2.0, 3.0],\n", 124 | " y=[0.0, 1.0, 2.0, 3.0],\n", 125 | " smiles=[\"C\", \"CO\", \"CC\", \"CCO\"],\n", 126 | " marker=\"x\",\n", 127 | " marker_size=15,\n", 128 | " marker_color=palette[0],\n", 129 | " legend_label=\"Series A\",\n", 130 | ")\n", 131 | "plotmol.scatter(\n", 132 | " figure,\n", 133 | " x=[0.0, 1.0, 2.0, 3.0],\n", 134 | " y=[1.0, 2.0, 3.0, 4.0],\n", 135 | " smiles=[\"C=O\", \"CC=O\", \"COC\", \"CCCO\"],\n", 136 | " marker=\"o\",\n", 137 | " marker_size=15,\n", 138 | " marker_color=palette[1],\n", 139 | " legend_label=\"Series B\",\n", 140 | ")" 141 | ], 142 | "metadata": { 143 | "collapsed": false, 144 | "pycharm": { 145 | "name": "#%%\n" 146 | }, 147 | "ExecuteTime": { 148 | "end_time": "2023-11-29T13:28:36.097706Z", 149 | "start_time": "2023-11-29T13:28:36.077809Z" 150 | } 151 | } 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "source": [ 156 | "We can optionally configure the legend which will be added to the plot. Here we move it to the top left corner\n", 157 | "of the plot and enable the option to toggle data series when they are clicked in the plot legend:" 158 | ], 159 | "metadata": { 160 | "collapsed": false 161 | } 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 4, 166 | "outputs": [], 167 | "source": [ 168 | "# Optionally configure the legend.\n", 169 | "figure.legend.location = \"top_left\"\n", 170 | "figure.legend.click_policy = \"hide\"" 171 | ], 172 | "metadata": { 173 | "collapsed": false, 174 | "pycharm": { 175 | "name": "#%%\n" 176 | }, 177 | "ExecuteTime": { 178 | "end_time": "2023-11-29T13:28:36.097960Z", 179 | "start_time": "2023-11-29T13:28:36.081167Z" 180 | } 181 | } 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "source": [ 186 | "Finally, we show the figure. Hovering over each data point with the mouse should reveal a pop-up containing the\n", 187 | "2D molecular structure associated with a data point." 188 | ], 189 | "metadata": { 190 | "collapsed": false 191 | } 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 5, 196 | "outputs": [ 197 | { 198 | "data": { 199 | "text/html": "\n
\n" 200 | }, 201 | "metadata": {}, 202 | "output_type": "display_data" 203 | }, 204 | { 205 | "data": { 206 | "application/javascript": "(function(root) {\n function embed_document(root) {\n const docs_json = {\"ffe0f9cd-5ead-4ec3-840d-6ff396f0dae7\":{\"version\":\"3.3.1\",\"title\":\"Bokeh Application\",\"roots\":[{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"p1001\",\"attributes\":{\"width\":500,\"height\":500,\"x_range\":{\"type\":\"object\",\"name\":\"DataRange1d\",\"id\":\"p1002\"},\"y_range\":{\"type\":\"object\",\"name\":\"DataRange1d\",\"id\":\"p1003\"},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1011\"},\"y_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1012\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"p1004\",\"attributes\":{\"text\":\"My Dummy Data\",\"text_color\":\"#5B5B5B\",\"text_font\":\"Helvetica\",\"text_font_size\":\"1.15em\"}},\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1040\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1031\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1032\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1033\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[0.0,1.0,2.0,3.0]],[\"y\",[0.0,1.0,2.0,3.0]],[\"smiles\",[\"C\",\"CO\",\"CC\",\"CCO\"]],[\"image\",[\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYXRvbS0wJyBkPSdNIDY2LjEgOTMuMApRIDY2LjEgODYuMCwgNjkuNCA4Mi4zClEgNzIuNyA3OC42LCA3OS4wIDc4LjYKUSA4NC44IDc4LjYsIDg4LjAgODIuNwpMIDg1LjMgODQuOQpRIDgzLjAgODEuOSwgNzkuMCA4MS45ClEgNzQuNyA4MS45LCA3Mi40IDg0LjgKUSA3MC4yIDg3LjYsIDcwLjIgOTMuMApRIDcwLjIgOTguNiwgNzIuNSAxMDEuNQpRIDc0LjkgMTA0LjQsIDc5LjQgMTA0LjQKUSA4Mi42IDEwNC40LCA4Ni4yIDEwMi41CkwgODcuMyAxMDUuNQpRIDg1LjggMTA2LjUsIDgzLjYgMTA3LjAKUSA4MS40IDEwNy42LCA3OC45IDEwNy42ClEgNzIuNyAxMDcuNiwgNjkuNCAxMDMuOApRIDY2LjEgMTAwLjEsIDY2LjEgOTMuMAonIGZpbGw9JyMwMDAwMDAnLz4KPHBhdGggY2xhc3M9J2F0b20tMCcgZD0nTSA5MS43IDc4LjkKTCA5NS42IDc4LjkKTCA5NS42IDkxLjAKTCAxMTAuMSA5MS4wCkwgMTEwLjEgNzguOQpMIDExMy45IDc4LjkKTCAxMTMuOSAxMDcuMgpMIDExMC4xIDEwNy4yCkwgMTEwLjEgOTQuMgpMIDk1LjYgOTQuMgpMIDk1LjYgMTA3LjIKTCA5MS43IDEwNy4yCkwgOTEuNyA3OC45CicgZmlsbD0nIzAwMDAwMCcvPgo8cGF0aCBjbGFzcz0nYXRvbS0wJyBkPSdNIDEzMS42IDExNS4wCkwgMTMzLjkgMTE1LjAKTCAxMzMuOSAxMTcuMQpMIDEzMS42IDExNy4xCkwgMTMxLjYgMTIxLjQKTCAxMjkuMiAxMjEuNApMIDEyOS4yIDExNy4xCkwgMTE5LjUgMTE3LjEKTCAxMTkuNSAxMTUuNApMIDEyNy43IDEwMi43CkwgMTMxLjYgMTAyLjcKTCAxMzEuNiAxMTUuMApNIDEyMi42IDExNS4wCkwgMTI5LjIgMTE1LjAKTCAxMjkuMiAxMDQuNApMIDEyMi42IDExNS4wCicgZmlsbD0nIzAwMDAwMCcvPgo8L3N2Zz4K\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTUuNyw5OS44IEwgNzEuNiw5OS44JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gNzEuNiw5OS44IEwgMTI3LjQsOTkuOCcgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6I0ZGMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2F0b20tMScgZD0nTSAxMzEuNyA5OS45ClEgMTMxLjcgOTMuMSwgMTM1LjEgODkuMwpRIDEzOC40IDg1LjUsIDE0NC43IDg1LjUKUSAxNTEuMCA4NS41LCAxNTQuMyA4OS4zClEgMTU3LjcgOTMuMSwgMTU3LjcgOTkuOQpRIDE1Ny43IDEwNi43LCAxNTQuMyAxMTAuNwpRIDE1MC45IDExNC41LCAxNDQuNyAxMTQuNQpRIDEzOC41IDExNC41LCAxMzUuMSAxMTAuNwpRIDEzMS43IDEwNi44LCAxMzEuNyA5OS45Ck0gMTQ0LjcgMTExLjMKUSAxNDkuMCAxMTEuMywgMTUxLjMgMTA4LjUKUSAxNTMuNyAxMDUuNSwgMTUzLjcgOTkuOQpRIDE1My43IDk0LjMsIDE1MS4zIDkxLjUKUSAxNDkuMCA4OC43LCAxNDQuNyA4OC43ClEgMTQwLjQgODguNywgMTM4LjAgOTEuNQpRIDEzNS43IDk0LjMsIDEzNS43IDk5LjkKUSAxMzUuNyAxMDUuNiwgMTM4LjAgMTA4LjUKUSAxNDAuNCAxMTEuMywgMTQ0LjcgMTExLjMKJyBmaWxsPScjRkYwMDAwJy8+CjxwYXRoIGNsYXNzPSdhdG9tLTEnIGQ9J00gMTYyLjEgODUuOApMIDE2NS45IDg1LjgKTCAxNjUuOSA5Ny44CkwgMTgwLjQgOTcuOApMIDE4MC40IDg1LjgKTCAxODQuMyA4NS44CkwgMTg0LjMgMTE0LjEKTCAxODAuNCAxMTQuMQpMIDE4MC40IDEwMS4wCkwgMTY1LjkgMTAxLjAKTCAxNjUuOSAxMTQuMQpMIDE2Mi4xIDExNC4xCkwgMTYyLjEgODUuOAonIGZpbGw9JyNGRjAwMDAnLz4KPC9zdmc+Cg==\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMzIuNSwxMDAuMCBMIDE2Ny41LDEwMC4wJyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8L3N2Zz4K\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTAuMCwxMTQuOSBMIDgzLjMsNzIuNicgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2JvbmQtMSBhdG9tLTEgYXRvbS0yJyBkPSdNIDgzLjMsNzIuNiBMIDExMy4wLDg5LjgnIHN0eWxlPSdmaWxsOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEnIC8+CjxwYXRoIGNsYXNzPSdib25kLTEgYXRvbS0xIGF0b20tMicgZD0nTSAxMTMuMCw4OS44IEwgMTQyLjcsMTA2LjknIHN0eWxlPSdmaWxsOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiNGRjAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEnIC8+CjxwYXRoIGQ9J00gNzkuNiw3NC43IEwgODMuMyw3Mi42IEwgODQuOCw3My41JyBzdHlsZT0nZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxOycgLz4KPHBhdGggY2xhc3M9J2F0b20tMicgZD0nTSAxNDUuNSAxMTUuMApRIDE0NS41IDEwOS4yLCAxNDguNCAxMDYuMApRIDE1MS4yIDEwMi44LCAxNTYuNSAxMDIuOApRIDE2MS44IDEwMi44LCAxNjQuNyAxMDYuMApRIDE2Ny41IDEwOS4yLCAxNjcuNSAxMTUuMApRIDE2Ny41IDEyMC44LCAxNjQuNiAxMjQuMQpRIDE2MS44IDEyNy40LCAxNTYuNSAxMjcuNApRIDE1MS4yIDEyNy40LCAxNDguNCAxMjQuMQpRIDE0NS41IDEyMC44LCAxNDUuNSAxMTUuMApNIDE1Ni41IDEyNC43ClEgMTYwLjIgMTI0LjcsIDE2Mi4xIDEyMi4yClEgMTY0LjEgMTE5LjgsIDE2NC4xIDExNS4wClEgMTY0LjEgMTEwLjMsIDE2Mi4xIDEwNy45ClEgMTYwLjIgMTA1LjUsIDE1Ni41IDEwNS41ClEgMTUyLjkgMTA1LjUsIDE1MC45IDEwNy45ClEgMTQ4LjkgMTEwLjIsIDE0OC45IDExNS4wClEgMTQ4LjkgMTE5LjgsIDE1MC45IDEyMi4yClEgMTUyLjkgMTI0LjcsIDE1Ni41IDEyNC43CicgZmlsbD0nI0ZGMDAwMCcvPgo8cGF0aCBjbGFzcz0nYXRvbS0yJyBkPSdNIDE3MS4zIDEwMy4xCkwgMTc0LjUgMTAzLjEKTCAxNzQuNSAxMTMuMgpMIDE4Ni44IDExMy4yCkwgMTg2LjggMTAzLjEKTCAxOTAuMCAxMDMuMQpMIDE5MC4wIDEyNy4wCkwgMTg2LjggMTI3LjAKTCAxODYuOCAxMTYuMApMIDE3NC41IDExNi4wCkwgMTc0LjUgMTI3LjAKTCAxNzEuMyAxMjcuMApMIDE3MS4zIDEwMy4xCicgZmlsbD0nI0ZGMDAwMCcvPgo8L3N2Zz4K\"]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1041\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1042\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1037\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"hatch_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"marker\":{\"type\":\"value\",\"value\":\"x\"}}},\"nonselection_glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1038\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.1},\"fill_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.1},\"hatch_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.1},\"marker\":{\"type\":\"value\",\"value\":\"x\"}}},\"muted_glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1039\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.2},\"fill_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.2},\"hatch_color\":{\"type\":\"value\",\"value\":\"#2b83ba\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.2},\"marker\":{\"type\":\"value\",\"value\":\"x\"}}}}},{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1054\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1045\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1046\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1047\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[0.0,1.0,2.0,3.0]],[\"y\",[1.0,2.0,3.0,4.0]],[\"smiles\",[\"C=O\",\"CC=O\",\"COC\",\"CCCO\"]],[\"image\",[\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTMuOSw4Ny44IEwgODQuNCw4Ny44JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gODQuNCw4Ny44IEwgMTU0LjksODcuOCcgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6I0ZGMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2JvbmQtMCBhdG9tLTAgYXRvbS0xJyBkPSdNIDEzLjksMTExLjcgTCA4NC40LDExMS43JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gODQuNCwxMTEuNyBMIDE1NC45LDExMS43JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojRkYwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYXRvbS0xJyBkPSdNIDE2MC4xIDk5LjkKUSAxNjAuMSA5My4xLCAxNjMuNSA4OS4zClEgMTY2LjkgODUuNSwgMTczLjEgODUuNQpRIDE3OS40IDg1LjUsIDE4Mi44IDg5LjMKUSAxODYuMSA5My4xLCAxODYuMSA5OS45ClEgMTg2LjEgMTA2LjcsIDE4Mi43IDExMC43ClEgMTc5LjMgMTE0LjUsIDE3My4xIDExNC41ClEgMTY2LjkgMTE0LjUsIDE2My41IDExMC43ClEgMTYwLjEgMTA2LjgsIDE2MC4xIDk5LjkKTSAxNzMuMSAxMTEuMwpRIDE3Ny41IDExMS4zLCAxNzkuOCAxMDguNQpRIDE4Mi4xIDEwNS41LCAxODIuMSA5OS45ClEgMTgyLjEgOTQuMywgMTc5LjggOTEuNQpRIDE3Ny41IDg4LjcsIDE3My4xIDg4LjcKUSAxNjguOCA4OC43LCAxNjYuNSA5MS41ClEgMTY0LjEgOTQuMywgMTY0LjEgOTkuOQpRIDE2NC4xIDEwNS42LCAxNjYuNSAxMDguNQpRIDE2OC44IDExMS4zLCAxNzMuMSAxMTEuMwonIGZpbGw9JyNGRjAwMDAnLz4KPC9zdmc+Cg==\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTAuMCwxMTQuMiBMIDk1LjQsNjQuOScgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2JvbmQtMSBhdG9tLTEgYXRvbS0yJyBkPSdNIDk1LjQsNjQuOSBMIDEzMC4xLDg0LjknIHN0eWxlPSdmaWxsOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEnIC8+CjxwYXRoIGNsYXNzPSdib25kLTEgYXRvbS0xIGF0b20tMicgZD0nTSAxMzAuMSw4NC45IEwgMTY0LjgsMTA0LjknIHN0eWxlPSdmaWxsOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiNGRjAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEnIC8+CjxwYXRoIGNsYXNzPSdib25kLTEgYXRvbS0xIGF0b20tMicgZD0nTSA5NS40LDgxLjkgTCAxMjIuNyw5Ny43JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0xIGF0b20tMSBhdG9tLTInIGQ9J00gMTIyLjcsOTcuNyBMIDE1Ny40LDExNy43JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojRkYwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBkPSdNIDkxLjIsNjcuMyBMIDk1LjQsNjQuOSBMIDk3LjIsNjUuOScgc3R5bGU9J2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MTsnIC8+CjxwYXRoIGNsYXNzPSdhdG9tLTInIGQ9J00gMTY0LjMgMTIwLjcKUSAxNjQuMyAxMTQuMCwgMTY3LjcgMTEwLjIKUSAxNzEuMCAxMDYuNSwgMTc3LjIgMTA2LjUKUSAxODMuNCAxMDYuNSwgMTg2LjcgMTEwLjIKUSAxOTAuMCAxMTQuMCwgMTkwLjAgMTIwLjcKUSAxOTAuMCAxMjcuNSwgMTg2LjYgMTMxLjMKUSAxODMuMyAxMzUuMSwgMTc3LjIgMTM1LjEKUSAxNzEuMCAxMzUuMSwgMTY3LjcgMTMxLjMKUSAxNjQuMyAxMjcuNSwgMTY0LjMgMTIwLjcKTSAxNzcuMiAxMzIuMApRIDE4MS40IDEzMi4wLCAxODMuNyAxMjkuMgpRIDE4Ni4xIDEyNi4zLCAxODYuMSAxMjAuNwpRIDE4Ni4xIDExNS4yLCAxODMuNyAxMTIuNApRIDE4MS40IDEwOS42LCAxNzcuMiAxMDkuNgpRIDE3Mi45IDEwOS42LCAxNzAuNiAxMTIuNApRIDE2OC4zIDExNS4xLCAxNjguMyAxMjAuNwpRIDE2OC4zIDEyNi4zLCAxNzAuNiAxMjkuMgpRIDE3Mi45IDEzMi4wLCAxNzcuMiAxMzIuMAonIGZpbGw9JyNGRjAwMDAnLz4KPC9zdmc+Cg==\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTAuMCwxMzMuMSBMIDQ2LjgsMTExLjknIHN0eWxlPSdmaWxsOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEnIC8+CjxwYXRoIGNsYXNzPSdib25kLTAgYXRvbS0wIGF0b20tMScgZD0nTSA0Ni44LDExMS45IEwgODMuNiw5MC43JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojRkYwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0xIGF0b20tMSBhdG9tLTInIGQ9J00gMTE2LjQsOTAuNyBMIDE1My4yLDExMS45JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojRkYwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0xIGF0b20tMSBhdG9tLTInIGQ9J00gMTUzLjIsMTExLjkgTCAxOTAuMCwxMzMuMScgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2F0b20tMScgZD0nTSA4Ny4wIDgxLjMKUSA4Ny4wIDc0LjUsIDkwLjQgNzAuNwpRIDkzLjcgNjYuOSwgMTAwLjAgNjYuOQpRIDEwNi4zIDY2LjksIDEwOS42IDcwLjcKUSAxMTMuMCA3NC41LCAxMTMuMCA4MS4zClEgMTEzLjAgODguMSwgMTA5LjYgOTIuMQpRIDEwNi4yIDk1LjksIDEwMC4wIDk1LjkKUSA5My44IDk1LjksIDkwLjQgOTIuMQpRIDg3LjAgODguMiwgODcuMCA4MS4zCk0gMTAwLjAgOTIuNwpRIDEwNC4zIDkyLjcsIDEwNi42IDg5LjkKUSAxMDkuMCA4Ni45LCAxMDkuMCA4MS4zClEgMTA5LjAgNzUuNywgMTA2LjYgNzIuOQpRIDEwNC4zIDcwLjEsIDEwMC4wIDcwLjEKUSA5NS43IDcwLjEsIDkzLjMgNzIuOQpRIDkxLjAgNzUuNywgOTEuMCA4MS4zClEgOTEuMCA4Ny4wLCA5My4zIDg5LjkKUSA5NS43IDkyLjcsIDEwMC4wIDkyLjcKJyBmaWxsPScjRkYwMDAwJy8+Cjwvc3ZnPgo=\",\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0naXNvLTg4NTktMSc/Pgo8c3ZnIHZlcnNpb249JzEuMScgYmFzZVByb2ZpbGU9J2Z1bGwnCiAgICAgICAgICAgICAgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJwogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6cmRraXQ9J2h0dHA6Ly93d3cucmRraXQub3JnL3htbCcKICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJwogICAgICAgICAgICAgICAgICB4bWw6c3BhY2U9J3ByZXNlcnZlJwp3aWR0aD0nMjAwcHgnIGhlaWdodD0nMjAwcHgnIHZpZXdCb3g9JzAgMCAyMDAgMjAwJz4KPCEtLSBFTkQgT0YgSEVBREVSIC0tPgo8cmVjdCBzdHlsZT0nb3BhY2l0eToxLjA7ZmlsbDojRkZGRkZGO3N0cm9rZTpub25lJyB3aWR0aD0nMjAwLjAnIGhlaWdodD0nMjAwLjAnIHg9JzAuMCcgeT0nMC4wJz4gPC9yZWN0Pgo8cGF0aCBjbGFzcz0nYm9uZC0wIGF0b20tMCBhdG9tLTEnIGQ9J00gMTAuMCwxMDUuNCBMIDY0LjYsODIuMicgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2JvbmQtMSBhdG9tLTEgYXRvbS0yJyBkPSdNIDY0LjYsODIuMiBMIDExMi4wLDExNy44JyBzdHlsZT0nZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjBweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxJyAvPgo8cGF0aCBjbGFzcz0nYm9uZC0yIGF0b20tMiBhdG9tLTMnIGQ9J00gMTEyLjAsMTE3LjggTCAxMzQuNCwxMDguMycgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggY2xhc3M9J2JvbmQtMiBhdG9tLTIgYXRvbS0zJyBkPSdNIDEzNC40LDEwOC4zIEwgMTU2LjksOTguNycgc3R5bGU9J2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6I0ZGMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MScgLz4KPHBhdGggZD0nTSA2MS44LDgzLjMgTCA2NC42LDgyLjIgTCA2Ni45LDg0LjAnIHN0eWxlPSdmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjIuMHB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjE7JyAvPgo8cGF0aCBkPSdNIDEwOS42LDExNi4wIEwgMTEyLjAsMTE3LjggTCAxMTMuMSwxMTcuMycgc3R5bGU9J2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6Mi4wcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MTsnIC8+CjxwYXRoIGNsYXNzPSdhdG9tLTMnIGQ9J00gMTU4LjggOTQuNwpRIDE1OC44IDkwLjYsIDE2MC44IDg4LjQKUSAxNjIuOCA4Ni4xLCAxNjYuNSA4Ni4xClEgMTcwLjMgODYuMSwgMTcyLjMgODguNApRIDE3NC4yIDkwLjYsIDE3NC4yIDk0LjcKUSAxNzQuMiA5OC43LCAxNzIuMiAxMDEuMQpRIDE3MC4yIDEwMy40LCAxNjYuNSAxMDMuNApRIDE2Mi44IDEwMy40LCAxNjAuOCAxMDEuMQpRIDE1OC44IDk4LjgsIDE1OC44IDk0LjcKTSAxNjYuNSAxMDEuNQpRIDE2OS4xIDEwMS41LCAxNzAuNSA5OS44ClEgMTcxLjkgOTguMCwgMTcxLjkgOTQuNwpRIDE3MS45IDkxLjQsIDE3MC41IDg5LjcKUSAxNjkuMSA4OC4wLCAxNjYuNSA4OC4wClEgMTY0LjAgODguMCwgMTYyLjYgODkuNwpRIDE2MS4yIDkxLjMsIDE2MS4yIDk0LjcKUSAxNjEuMiA5OC4wLCAxNjIuNiA5OS44ClEgMTY0LjAgMTAxLjUsIDE2Ni41IDEwMS41CicgZmlsbD0nI0ZGMDAwMCcvPgo8cGF0aCBjbGFzcz0nYXRvbS0zJyBkPSdNIDE3Ni45IDg2LjMKTCAxNzkuMSA4Ni4zCkwgMTc5LjEgOTMuNApMIDE4Ny43IDkzLjQKTCAxODcuNyA4Ni4zCkwgMTkwLjAgODYuMwpMIDE5MC4wIDEwMy4xCkwgMTg3LjcgMTAzLjEKTCAxODcuNyA5NS4zCkwgMTc5LjEgOTUuMwpMIDE3OS4xIDEwMy4xCkwgMTc2LjkgMTAzLjEKTCAxNzYuOSA4Ni4zCicgZmlsbD0nI0ZGMDAwMCcvPgo8L3N2Zz4K\"]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1055\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1056\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1051\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"hatch_color\":{\"type\":\"value\",\"value\":\"#abdda4\"}}},\"nonselection_glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1052\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.1},\"fill_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.1},\"hatch_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.1}}},\"muted_glyph\":{\"type\":\"object\",\"name\":\"Scatter\",\"id\":\"p1053\",\"attributes\":{\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"size\":{\"type\":\"value\",\"value\":15},\"line_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.2},\"fill_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.2},\"hatch_color\":{\"type\":\"value\",\"value\":\"#abdda4\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.2}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1010\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"p1023\"},{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"p1024\",\"attributes\":{\"renderers\":\"auto\"}},{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"p1025\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"p1026\",\"attributes\":{\"syncable\":false,\"level\":\"overlay\",\"visible\":false,\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5}}}},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1027\"},{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"p1028\"},{\"type\":\"object\",\"name\":\"HelpTool\",\"id\":\"p1029\"},{\"type\":\"object\",\"name\":\"HoverTool\",\"id\":\"p1030\",\"attributes\":{\"renderers\":\"auto\",\"tooltips\":\"\\n
\\n
\\n \\n
\\n
\\n\"}}]}},\"left\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1018\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1019\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1020\"},\"axis_label\":\"Dummy Y\",\"axis_label_standoff\":10,\"axis_label_text_color\":\"#5B5B5B\",\"axis_label_text_font\":\"Helvetica\",\"axis_label_text_font_size\":\"1.25em\",\"axis_label_text_font_style\":\"normal\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1021\"},\"major_label_text_color\":\"#5B5B5B\",\"major_label_text_font\":\"Helvetica\",\"major_label_text_font_size\":\"1.025em\",\"axis_line_color\":\"#5B5B5B\",\"axis_line_alpha\":0,\"major_tick_line_color\":\"#5B5B5B\",\"major_tick_line_alpha\":0,\"minor_tick_line_color\":\"#5B5B5B\",\"minor_tick_line_alpha\":0}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1013\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1014\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1015\"},\"axis_label\":\"Dummy X\",\"axis_label_standoff\":10,\"axis_label_text_color\":\"#5B5B5B\",\"axis_label_text_font\":\"Helvetica\",\"axis_label_text_font_size\":\"1.25em\",\"axis_label_text_font_style\":\"normal\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1016\"},\"major_label_text_color\":\"#5B5B5B\",\"major_label_text_font\":\"Helvetica\",\"major_label_text_font_size\":\"1.025em\",\"axis_line_color\":\"#5B5B5B\",\"axis_line_alpha\":0,\"major_tick_line_color\":\"#5B5B5B\",\"major_tick_line_alpha\":0,\"minor_tick_line_color\":\"#5B5B5B\",\"minor_tick_line_alpha\":0}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1017\",\"attributes\":{\"axis\":{\"id\":\"p1013\"}}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1022\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"p1018\"}}},{\"type\":\"object\",\"name\":\"Legend\",\"id\":\"p1043\",\"attributes\":{\"location\":\"top_left\",\"border_line_alpha\":0,\"background_fill_alpha\":0.25,\"click_policy\":\"hide\",\"label_text_color\":\"#5B5B5B\",\"label_text_font\":\"Helvetica\",\"label_text_font_size\":\"1.025em\",\"label_standoff\":8,\"glyph_width\":15,\"spacing\":8,\"items\":[{\"type\":\"object\",\"name\":\"LegendItem\",\"id\":\"p1044\",\"attributes\":{\"label\":{\"type\":\"value\",\"value\":\"Series A\"},\"renderers\":[{\"id\":\"p1040\"}]}},{\"type\":\"object\",\"name\":\"LegendItem\",\"id\":\"p1057\",\"attributes\":{\"label\":{\"type\":\"value\",\"value\":\"Series B\"},\"renderers\":[{\"id\":\"p1054\"}]}}]}}]}}]}};\n const render_items = [{\"docid\":\"ffe0f9cd-5ead-4ec3-840d-6ff396f0dae7\",\"roots\":{\"p1001\":\"baf87f57-cdab-45b1-a898-4c666e6d98b1\"},\"root_ids\":[\"p1001\"]}];\n root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n }\n if (root.Bokeh !== undefined) {\n embed_document(root);\n } else {\n let attempts = 0;\n const timer = setInterval(function(root) {\n if (root.Bokeh !== undefined) {\n clearInterval(timer);\n embed_document(root);\n } else {\n attempts++;\n if (attempts > 100) {\n clearInterval(timer);\n console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n }\n }\n }, 10, root)\n }\n})(window);", 207 | "application/vnd.bokehjs_exec.v0+json": "" 208 | }, 209 | "metadata": { 210 | "application/vnd.bokehjs_exec.v0+json": { 211 | "id": "p1001" 212 | } 213 | }, 214 | "output_type": "display_data" 215 | } 216 | ], 217 | "source": [ 218 | "# Show the figure.\n", 219 | "bokeh.plotting.show(figure)" 220 | ], 221 | "metadata": { 222 | "collapsed": false, 223 | "pycharm": { 224 | "name": "#%%\n" 225 | }, 226 | "ExecuteTime": { 227 | "end_time": "2023-11-29T13:28:36.138639Z", 228 | "start_time": "2023-11-29T13:28:36.091322Z" 229 | } 230 | } 231 | } 232 | ], 233 | "metadata": { 234 | "kernelspec": { 235 | "display_name": "Python 3", 236 | "language": "python", 237 | "name": "python3" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 2 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython2", 249 | "version": "2.7.6" 250 | } 251 | }, 252 | "nbformat": 4, 253 | "nbformat_minor": 0 254 | } 255 | --------------------------------------------------------------------------------