├── .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 | [](https://github.com/simonboothroyd/plotmol/actions/workflows/ci.yaml)
4 | [](https://codecov.io/gh/SimonBoothroyd/plotmol)
5 | [](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 \"
re-rerun `output_notebook()` to attempt to load from CDN again, or
\"}};\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 \"
re-rerun `output_notebook()` to attempt to load from CDN again, or