├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── deno.json ├── example.ipynb ├── pyproject.toml ├── src └── pygv │ ├── __init__.py │ ├── _api.py │ ├── _browser.py │ ├── _config.py │ ├── _tracks.py │ ├── _version.py │ └── static │ └── widget.js ├── tests ├── __init__.py └── test_pygv.py └── uv.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: astral-sh/setup-uv@v4 18 | with: 19 | version: "0.5.x" 20 | - run: | 21 | uv run ruff format --check 22 | uv run ruff check 23 | 24 | Test: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | python-version: ["3.9", "3.10", "3.11", "3.12"] 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: astral-sh/setup-uv@v4 33 | with: 34 | version: "0.5.x" 35 | 36 | - run: uv run pytest ./tests --color=yes 37 | env: 38 | UV_PYTHON: ${{ matrix.python-version }} 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | 10 | Release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | # IMPORTANT: this permission is mandatory for trusted publishing 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: astral-sh/setup-uv@v4 18 | with: 19 | version: "0.5.x" 20 | 21 | - run: uv build 22 | - name: Publish distribution 📦 to PyPI 23 | uses: pypa/gh-action-pypi-publish@release/v1 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .venv 3 | dist 4 | .DS_Store 5 | 6 | # Python 7 | __pycache__ 8 | .ipynb_checkpoints 9 | 10 | 11 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Trevor Manz 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygv 2 | 3 | a minimal, scriptable genome browser for python built with 4 | [anywidget](https://github.com/manzt/anywidget) 5 | 6 | ## installation 7 | 8 | **Here be dragons 🐉** 9 | 10 | ```sh 11 | pip install pygv 12 | ``` 13 | 14 | ## usage 15 | 16 | ```py 17 | import pygv 18 | 19 | # set the reference genome 20 | pygv.ref("mm10") 21 | 22 | # set the locus 23 | pygv.locus("chr17:31,531,100-31,531,259") 24 | 25 | # create a browser instance 26 | pygv.browse("fragments.bed", "10x_cov.bw") 27 | ``` 28 | 29 | ![igv.js in Jupyter Notebook](https://github.com/manzt/anywidget/assets/24403730/8aa77384-6d7c-422f-9238-37e06a0272f6) 30 | 31 | That's it. By default, `pygv` infers the track and data-types by file extension. 32 | If a file format has an index file, it must be specified as a tuple (remote URLs 33 | also work): 34 | 35 | ```py 36 | pygv.browse( 37 | ( 38 | "https://example.com/example.bam", # data file 39 | "https://example.com/example.bam.bai" # index file 40 | ) 41 | ) 42 | ``` 43 | 44 | You can use `track` to adjust track properties beyond the defaults: 45 | 46 | ```py 47 | pygv.browse( 48 | pygv.track("10x_cov.bw", name="10x coverage", autoscale=True), 49 | ) 50 | ``` 51 | 52 | Multiple tracks are supported by adding to the `browse` call: 53 | 54 | ```py 55 | pygv.browse( 56 | # track 1 57 | ( 58 | "https://example.com/example.bam", 59 | "https://example.com/example.bam.bai" 60 | ), 61 | # track 2 62 | pygv.track("10x_cov.bw", name="10x coverage", autoscale=True), 63 | # track 3 64 | pygv.track("genes.bed", name="Genes", color="blue"), 65 | ) 66 | ``` 67 | 68 | ## development 69 | 70 | development requires [uv](https://astral.sh/uv) 71 | 72 | ```sh 73 | uv run jupyter lab # open notebook with editable install 74 | ``` 75 | 76 | ```sh 77 | uv run pytest # testing 78 | uv run ruff check # linting 79 | uv run ruff format # formatting 80 | ``` 81 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "compilerOptions": { 4 | "checkJs": true, 5 | "allowJs": true, 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ] 11 | }, 12 | "fmt": { 13 | "exclude": [ 14 | ".venv" 15 | ] 16 | }, 17 | "lint": { 18 | "exclude": [ 19 | ".venv" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext autoreload\n", 10 | "%autoreload 2\n", 11 | "%env ANYWIDGET_HMR=1" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import pygv\n", 21 | "\n", 22 | "pygv.ref(\"mm10\")\n", 23 | "pygv.locus(\"chr17:31,531,100-31,531,259\")\n", 24 | "pygv.browse(\"fragments.bed\", \"10x_cov.bw\")" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [] 33 | } 34 | ], 35 | "metadata": { 36 | "kernelspec": { 37 | "display_name": "Python 3 (ipykernel)", 38 | "language": "python", 39 | "name": "python3" 40 | }, 41 | "language_info": { 42 | "codemirror_mode": { 43 | "name": "ipython", 44 | "version": 3 45 | }, 46 | "file_extension": ".py", 47 | "mimetype": "text/x-python", 48 | "name": "python", 49 | "nbconvert_exporter": "python", 50 | "pygments_lexer": "ipython3", 51 | "version": "3.12.2" 52 | } 53 | }, 54 | "nbformat": 4, 55 | "nbformat_minor": 4 56 | } 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pygv" 7 | version = "0.2.2" 8 | dependencies = ["anywidget>=0.9.3", "servir>=0.2.1", "msgspec>=0.18.6"] 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | 12 | [dependency-groups] 13 | dev = [ 14 | "inline-snapshot>=0.17.1", 15 | "jupyterlab>=4.3.4", 16 | "pytest>=8.3.4", 17 | "ruff>=0.8.3", 18 | ] 19 | 20 | [tool.ruff.lint] 21 | pydocstyle = { convention = "numpy" } 22 | select = ["ALL"] 23 | ignore = ["COM812", "ISC001"] 24 | 25 | [tool.ruff.lint.per-file-ignores] 26 | "tests/*.py" = ["D", "S", "PLR", "ANN"] 27 | "src/pygv/_tracks.py" = ["UP007"] 28 | -------------------------------------------------------------------------------- /src/pygv/__init__.py: -------------------------------------------------------------------------------- 1 | """A minimal, scriptable genome browser for python.""" 2 | 3 | from ._api import Browser, Config, browse, load, loads, locus, ref, track # noqa: F401 4 | from ._version import __version__ # noqa: F401 5 | -------------------------------------------------------------------------------- /src/pygv/_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses 4 | import json 5 | import pathlib 6 | import typing 7 | 8 | from ._browser import Browser 9 | from ._config import Config, is_href 10 | from ._tracks import BaseTrack, Track 11 | 12 | __all__ = ["Browser", "browse", "load", "loads", "locus", "ref", "track"] 13 | 14 | 15 | @dataclasses.dataclass 16 | class Context: 17 | genome: str = "hg38" 18 | locus: str | None = None 19 | current: Browser | None = None 20 | 21 | 22 | _CONTEXT = Context() 23 | 24 | FilePathOrUrl = typing.Union[str, pathlib.Path] 25 | TrackArgument = typing.Union[ 26 | FilePathOrUrl, 27 | tuple[FilePathOrUrl, FilePathOrUrl], 28 | Track, 29 | ] 30 | 31 | 32 | def locus(locus: str) -> None: 33 | """Set the initial locus for the browsers in this session.""" 34 | _CONTEXT.locus = locus 35 | 36 | 37 | def ref(genome: str) -> None: 38 | """Set the reference genome for the browsers in this session.""" 39 | _CONTEXT.genome = genome 40 | 41 | 42 | def track(targ: TrackArgument | None = None, /, **kwargs) -> Track: # noqa: ANN003 43 | if isinstance(targ, BaseTrack): 44 | return targ 45 | 46 | if targ is None: 47 | url = kwargs["url"] 48 | elif isinstance(targ, (str, pathlib.Path)): 49 | url = kwargs["url"] = str(targ) 50 | else: 51 | url = kwargs["url"] = str(targ[0]) 52 | kwargs["indexURL"] = str(targ[1]) 53 | 54 | if "name" not in kwargs: 55 | kwargs["name"] = url if is_href(url) else pathlib.Path(url).name 56 | 57 | return Config.from_dict({"tracks": [kwargs]}).tracks[0] 58 | 59 | 60 | def browse(*tracks: TrackArgument) -> Browser: 61 | """Create a new genome browser instance. 62 | 63 | Parameters 64 | ---------- 65 | tracks : tuple[TrackArgument, ...] 66 | A list of tracks to display in the browser. 67 | 68 | Returns 69 | ------- 70 | Browser 71 | The browser widget. 72 | """ 73 | config = Config( 74 | genome=_CONTEXT.genome, 75 | tracks=[track(t) for t in tracks], 76 | ) 77 | if _CONTEXT.locus: 78 | config.locus = _CONTEXT.locus 79 | _CONTEXT.current = Browser(config) 80 | return _CONTEXT.current 81 | 82 | 83 | def load(file: typing.IO[str]) -> Browser: 84 | """Load an existing IGV configuration from a file-like.""" 85 | return loads(file.read()) 86 | 87 | 88 | def loads(json_config: str) -> Browser: 89 | """Load a JSON-encoded IGV configuration.""" 90 | config = json.loads(json_config) 91 | _CONTEXT.current = Browser(Config.from_dict(config)) 92 | return _CONTEXT.current 93 | -------------------------------------------------------------------------------- /src/pygv/_browser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | 5 | import anywidget 6 | import msgspec 7 | import traitlets 8 | 9 | from ._config import Config 10 | 11 | 12 | class Browser(anywidget.AnyWidget): 13 | _esm = pathlib.Path(__file__).parent / "static" / "widget.js" 14 | config = traitlets.Instance(Config).tag( 15 | sync=True, to_json=lambda x, _: msgspec.to_builtins(x) 16 | ) 17 | 18 | def __init__(self, config: Config) -> None: 19 | super().__init__(config=config.servable()) 20 | -------------------------------------------------------------------------------- /src/pygv/_config.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import pathlib 3 | import typing as t 4 | 5 | import msgspec 6 | import servir 7 | from msgspec import UNSET, Struct, UnsetType 8 | 9 | from ._tracks import Track 10 | 11 | _PROVIDER = servir.Provider() 12 | _RESOURCES = set() 13 | 14 | FilePathOrUrl = t.Union[str, pathlib.Path] 15 | 16 | 17 | class Config(Struct, rename="camel", repr_omit_defaults=True, omit_defaults=True): 18 | """An IGV configuration.""" 19 | 20 | genome: t.Union[str, UnsetType] = UNSET # noqa: FA100 21 | locus: t.Union[str, list[str], UnsetType] = UNSET # noqa: FA100 22 | show_sample_names: t.Union[bool, UnsetType] = UNSET # noqa: FA100 23 | tracks: list[Track] = [] 24 | 25 | @classmethod 26 | def from_dict( 27 | cls, 28 | config: dict, 29 | ) -> "Config": 30 | config = copy.deepcopy(config) 31 | 32 | for track in config.get("tracks", []): 33 | track["type"] = resolve_track_type( 34 | track.get("type"), 35 | track.get("format", guess_format(track.get("url"))), 36 | ) 37 | 38 | return msgspec.convert(config, type=Config) 39 | 40 | def servable(self) -> "Config": 41 | """Returns a new config with tracks that are ensured to be servable.""" # noqa: D401 42 | copy = msgspec.convert(msgspec.to_builtins(self), type=Config) 43 | 44 | for track in copy.tracks: 45 | if track.url != msgspec.UNSET: 46 | track.url = resolve_file_or_url(track.url) 47 | 48 | if track.index_url != msgspec.UNSET: 49 | track.index_url = resolve_file_or_url(track.index_url) 50 | 51 | return copy 52 | 53 | 54 | def is_href(s: str) -> bool: 55 | return s.startswith(("http", "https")) 56 | 57 | 58 | def resolve_file_or_url(path_or_url: t.Union[str, pathlib.Path]) -> str: # noqa: FA100 59 | """Resolve a file path or URL to a URL. 60 | 61 | Parameters 62 | ---------- 63 | path_or_url : str | pathlib.Path 64 | A file path or URL. 65 | 66 | Returns 67 | ------- 68 | str 69 | A URL. If `path_or_url` is a URL, it is returned as-is, otherwise 70 | a file resource is created and the URL is returned. 71 | """ 72 | normalized = str(path_or_url) 73 | if is_href(normalized): 74 | return normalized 75 | path = pathlib.Path(normalized).resolve() 76 | if not path.is_file() or not path.exists(): 77 | raise FileNotFoundError(path) 78 | resource = _PROVIDER.create(path) 79 | _RESOURCES.add(resource) 80 | return resource.url 81 | 82 | 83 | def resolve_track_type( # noqa: C901, PLR0911, PLR0912 84 | type_: t.Union[str, None], # noqa: FA100 85 | format_: t.Union[str, None], # noqa: FA100 86 | ) -> str: 87 | if type_ == "annotation" or format_ in {"bed", "gff", "gff3", "gtf", "bedpe"}: 88 | return "annotation" 89 | 90 | if type_ == "wig" or format_ in {"bigWig", "bw", "bg", "bedGraph"}: 91 | return "wig" 92 | 93 | if type_ == "alignment" or format_ in {"bam", "cram"}: 94 | return "alignment" 95 | 96 | if type_ == "variant" or format_ in {"vcf"}: 97 | return "variant" 98 | 99 | if type_ == "mut" or format_ in {"mut", "maf"}: 100 | return "mut" 101 | 102 | if type_ == "seg" or format_ in {"mut", "seg"}: 103 | return "seg" 104 | 105 | if type_ == "gwas" or format_ in {"bed", "gwas"}: 106 | return "gwas" 107 | 108 | if type_ == "interact" or format_ in {"bedpe", "interact", "bigInteract"}: 109 | return "interact" 110 | 111 | if type_ == "qtl" or format_ in {"qtl"}: 112 | return "qtl" 113 | 114 | if type_ == "junction" or format_ in {"bed"}: 115 | return "junction" 116 | 117 | if type_ == "cnvpytor" or format_ in {"pytor", "vcf"}: 118 | return "cnvpytor" 119 | 120 | if type_ == "arc" or format_ in {"bp", "bed"}: 121 | return "arc" 122 | 123 | if type_ == "merged": 124 | return "merged" 125 | 126 | msg = "Unknown track type, got: {}" 127 | raise ValueError(msg) 128 | 129 | 130 | def guess_format(filename: t.Union[str, None]) -> t.Union[str, None]: # noqa: FA100 131 | if filename is None: 132 | return None 133 | parts = filename.split(".") 134 | filetype = parts[-1].lower() 135 | if filetype == "gz": 136 | filetype = parts[-2].lower() 137 | return filetype 138 | -------------------------------------------------------------------------------- /src/pygv/_tracks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | 5 | from msgspec import UNSET, Struct, UnsetType, field 6 | 7 | __all__ = [ 8 | "AlignmentTrack", 9 | "AnnotationTrack", 10 | "ArcTrack", 11 | "CnvPytorTrack", 12 | "InteractTrack", 13 | "MergedTrack", 14 | "MutationTrack", 15 | "QtlTrack", 16 | "SegmentedCopyNumberTrack", 17 | "SpliceJunctionTrack", 18 | "Track", 19 | "VariantTrack", 20 | "WigTrack", 21 | ] 22 | 23 | 24 | class BaseTrack(Struct, rename="camel", repr_omit_defaults=True, omit_defaults=True): 25 | """Represents a browser track. 26 | 27 | For a full configuration options, see the [IGV.js docs](https://igv.org/doc/igvjs/#tracks/Tracks) 28 | """ 29 | 30 | url: t.Union[str, UnsetType] = UNSET 31 | """URL to the track data resource, such as a file or webservice, or a data URI.""" 32 | 33 | name: t.Union[str, UnsetType] = UNSET 34 | """Display name (label). Required.""" 35 | 36 | index_url: t.Union[str, UnsetType] = field(default=UNSET, name="indexURL") 37 | """URL to a file index, such as a BAM .bai, tabix .tbi, or tribble .idx file. 38 | 39 | For indexed file access the index URL is required, if absent the entire file 40 | will be read. 41 | """ 42 | 43 | source_type: t.Union[t.Literal["file", "htsget", "custom"], UnsetType] = UNSET 44 | """Type of data source.""" 45 | 46 | format: t.Union[str, UnsetType] = UNSET 47 | """No default. If not specified, format is inferred from file name extension.""" 48 | 49 | indexed: t.Union[bool, UnsetType] = UNSET 50 | """Explicitly indicate whether the resource is indexed. 51 | 52 | This flag is redundant if `index_url` is provided. It can be used to load small 53 | BAM files without an index by setting to `False` 54 | """ 55 | 56 | order: t.Union[int, UnsetType] = UNSET 57 | """Integer value specifying relative order of track position on the screen. 58 | 59 | To pin a track to the bottom use a very large value. 60 | If no order is specified, tracks appear in order of their addition. 61 | """ 62 | 63 | color: t.Union[str, UnsetType] = UNSET 64 | """CSS color value for track features, e.g. "#ff0000" or "rgb(100,0,100)".""" 65 | 66 | height: t.Union[int, UnsetType] = UNSET 67 | """Initial height of track viewport in pixels. Default 50.""" 68 | 69 | min_height: t.Union[int, UnsetType] = UNSET 70 | """Minimum height of track in pixels. Default 50.""" 71 | 72 | max_height: t.Union[int, UnsetType] = UNSET 73 | """Maximum height of track in pixels. Default 500.""" 74 | 75 | visibility_window: t.Union[int, str, UnsetType] = UNSET 76 | """Maximum window size in base pairs for which indexed annotations or 77 | variants are displayed. 78 | 79 | 1 MB for variants, 30 KB for alignments, whole chromosome for 80 | other track types. 81 | """ 82 | 83 | removable: t.Union[bool, UnsetType] = UNSET 84 | """If true a "remove" item is included in the track menu. Default `True`.""" 85 | 86 | headers: t.Union[dict[str, str], UnsetType] = UNSET 87 | """HTTP headers to include with each request. 88 | 89 | For example `{"Authorization": "Bearer cn389ncoiwuencr"}`. 90 | """ 91 | 92 | oauth_token: t.Union[str, UnsetType] = UNSET 93 | """OAuth token, or function returning an OAuth token. 94 | 95 | The value will be included as a Bearer token with each request. 96 | """ 97 | 98 | id: t.Union[str, UnsetType] = UNSET 99 | """An identifier for this track.""" 100 | 101 | samples: t.Union[list[str], UnsetType] = UNSET 102 | """Sample names.""" 103 | 104 | 105 | class AnnotationTrack(BaseTrack, tag="annotation"): 106 | """Display views of genomic annotations. 107 | 108 | Associated file formats: bed, gff, gff3, gtf, bedpe (and more). 109 | 110 | Ref: https://igv.org/doc/igvjs/#tracks/Annotation-Track 111 | 112 | Example: 113 | ```py 114 | AnnotationTrack( 115 | name="Color by attribute biotype", 116 | format="gff3", 117 | display_mode="EXPANDED", 118 | height=300, 119 | url="https://s3.amazonaws.com/igv.org.genomes/hg38/Homo_sapiens.GRCh38.94.chr.gff3.gz", 120 | index_url="https://s3.amazonaws.com/igv.org.genomes/hg38/Homo_sapiens.GRCh38.94.chr.gff3.gz.tbi", 121 | visibility_window=1000000, 122 | color_by="biotype", 123 | color_table={ 124 | "antisense": "blueviolet", 125 | "protein_coding": "blue", 126 | "retained_intron": "rgb(0, 150, 150)", 127 | "processed_transcript": "purple", 128 | "processed_pseudogene": "#7fff00", 129 | "unprocessed_pseudogene": "#d2691e", 130 | "*": "black" 131 | } 132 | ) 133 | ``` 134 | """ 135 | 136 | display_mode: t.Union[t.Literal["COLLAPSED", "EXPANDED", "SQUISHED"], UnsetType] = ( 137 | UNSET 138 | ) 139 | """Annotation track display mode. Default `"COLLAPSED"`.""" 140 | 141 | expanded_row_height: t.Union[int, UnsetType] = UNSET 142 | """Height of each row of features in `"EXPANDED"` mode. Default `30`.""" 143 | 144 | squished_row_height: t.Union[int, UnsetType] = UNSET 145 | """Height of each row of features in `"SQUISHED"` mode. Default `15`.""" 146 | 147 | name_field: t.Union[str, UnsetType] = UNSET 148 | """For GFF/GTF file formats. Name of column 9 to be used for feature label.""" 149 | 150 | max_rows: t.Union[int, UnsetType] = UNSET 151 | """Maximum number of rows of features to display. Default `500`.""" 152 | 153 | searchable: t.Union[bool, UnsetType] = UNSET 154 | """Whether feature names for this track can be searched. Default `False`. 155 | 156 | Does not work for indexed tracks. Use with caution; it is memory intensive. 157 | """ 158 | 159 | searchable_fields: t.Union[list[str], UnsetType] = UNSET 160 | """Field (column 9) names to be included in feature searches. 161 | 162 | For use with the `searchable` option in conjunction with GFF files. 163 | 164 | When searching for feature attributes spaces need to be escaped with a "+" 165 | sign or percent encoded ("%20). 166 | """ 167 | 168 | filter_types: t.Union[list[str], UnsetType] = UNSET 169 | """GFF feature types to filter from display. Default `["chromosome", "gene"]`.""" 170 | 171 | color: t.Union[str, UnsetType] = UNSET 172 | """CSS color value for features. Default `"rgb(0,0,150)"` (i.e. `"#000096"`).""" 173 | 174 | alt_color: t.Union[str, UnsetType] = UNSET 175 | """If supplied, used for features on negative strand.""" 176 | 177 | color_by: t.Union[str, UnsetType] = UNSET 178 | """Used with GFF/GTF files. Name of column 9 attribute to color features by.""" 179 | 180 | color_table: t.Union[dict[str, str], UnsetType] = UNSET 181 | """Maps attribute values to CSS colors. 182 | 183 | Used in conjunction with the `color_by` to assign specific colors to attributes. 184 | """ 185 | 186 | 187 | class GuideLine(Struct): 188 | """Represents a horizontal guide line.""" 189 | 190 | color: str 191 | """A CSS color value.""" 192 | dotted: bool 193 | """Whether the line should be dashed.""" 194 | y: int 195 | """The y position. Should be between min and max.""" 196 | 197 | 198 | class WigTrack(BaseTrack, tag="wig"): 199 | """Displays quantititive data as either a bar chart, line plot, or points. 200 | 201 | Associated file formats: wig, bigWig, bedGraph. 202 | 203 | Ref: https://igv.org/doc/igvjs/#tracks/Wig-Track/ 204 | 205 | Example: 206 | ```py 207 | WigTrack( 208 | name="CTCF", 209 | url="https://www.encodeproject.org/files/ENCFF356YES/@@download/ENCFF356YES.bigWig", 210 | min="0", 211 | max="30", 212 | color="rgb(0, 0, 150)", 213 | guide_lines=[ 214 | GuideLine(color="green", dotted=True, y=25), 215 | GuideLine(color="red", dotted=False, y=5), 216 | ] 217 | ) 218 | ``` 219 | """ 220 | 221 | autoscale: t.Union[bool, None] = None 222 | """Autoscale track to maximum value in view.""" 223 | 224 | autoscale_group: t.Union[str, UnsetType] = UNSET 225 | """An identifier for an autoscale group. 226 | 227 | Tracks with the same identifier are autoscaled together. 228 | """ 229 | 230 | min: t.Union[int, UnsetType] = UNSET 231 | """Minimum value for the data (y-axis) scale. Usually zero.""" 232 | 233 | max: t.Union[int, UnsetType] = UNSET 234 | """Maximum value for the data (y-axis) scale. Ignored if `autoscale` is `True`.""" 235 | 236 | color: t.Union[str, UnsetType] = UNSET 237 | """CSS color value. Default `"rgb(150,150,150)"`.""" 238 | 239 | alt_color: t.Union[str, UnsetType] = UNSET 240 | """If supplied, used for negative values.""" 241 | 242 | color_scale: t.Union[dict, UnsetType] = UNSET 243 | """Color scale for heatmap (graphType = "heatmap" ). 244 | 245 | Ref: https://igv.org/doc/igvjs/#tracks/Wig-Track/#color-scale-objects 246 | """ 247 | 248 | guide_lines: t.Union[list[GuideLine], UnsetType] = UNSET 249 | """Draw a horizontal line for each object in the given array.""" 250 | 251 | graph_type: t.Union[t.Literal["bar", "points", "heatmap", "line"], UnsetType] = ( 252 | UNSET 253 | ) 254 | """Type of graph. Default `"bar"`.""" 255 | 256 | flip_axis: t.Union[bool, UnsetType] = UNSET 257 | """Whether the track is drawn "upside down" with zero at top. Default `False`.""" 258 | 259 | window_function: t.Union[t.Literal["min", "max", "mean"], UnsetType] = UNSET 260 | """Governs how data is summarized when zooming out. Default `"mean"`. 261 | 262 | Applicable to tracks created from bigwig and tdf files. 263 | """ 264 | 265 | 266 | class AlignmentSorting(Struct, rename="camel"): 267 | """Represents initial sort order of packed alignment rows.""" 268 | 269 | chr: str 270 | """Sequence (chromosome) name.""" 271 | 272 | pos: int 273 | """Genomic position.""" 274 | 275 | option: t.Literal["BASE", "STRAND", "INSERT_SIZE", "MATE_CHR", "MQ", "TAG"] 276 | """Parameter to sort by.""" 277 | 278 | tag: t.Union[str, UnsetType] = UNSET 279 | """Tag name to sort by. Include only if option = 'TAG""" 280 | 281 | direction: t.Literal["ASC", "DESC"] = "ASC" 282 | """Sort directions.""" 283 | 284 | 285 | class AlignmentFiltering(Struct, rename="camel"): 286 | """Represents filtering options for alignments.""" 287 | 288 | vendor_failed: bool = True 289 | """Filter alignments marked as failing vendor quality checks (bit 0x200).""" 290 | 291 | duplicates: bool = True 292 | """Filter alignments marked as a duplicate (bit 0x400).""" 293 | 294 | secondary: bool = False 295 | """Filter alignments marked as secondary (bit 0x100).""" 296 | 297 | supplementary: bool = False 298 | """Filter alignments marked as supplementary (bit 0x800).""" 299 | 300 | mq: int = 0 301 | """Filter alignments with mapping quality less than the supplied value.""" 302 | 303 | readgroups: t.Union[set[str], UnsetType] = UNSET 304 | """Read groups ('RG' tag). If present, filter alignments not matching this set.""" 305 | 306 | 307 | class AlignmentTrack(BaseTrack, tag="alignment"): 308 | """Display views of read alignments from BAM or CRAM files. 309 | 310 | Associated file formats: bam, cram. 311 | 312 | Ref: https://igv.org/doc/igvjs/#tracks/Alignment-Track 313 | 314 | Example: 315 | ```py 316 | AlignmentTrack( 317 | format="bam", 318 | name="NA12878", 319 | url="gs://genomics-public-data/platinum-genomes/bam/NA12878_S1.bam", 320 | index_url="gs://genomics-public-data/platinum-genomes/bam/NA12878_S1.bam.bai", 321 | ) 322 | ``` 323 | """ 324 | 325 | show_coverage: t.Union[bool, UnsetType] = UNSET 326 | """Show coverage depth track. Default `True`.""" 327 | 328 | show_alignments: t.Union[bool, UnsetType] = UNSET 329 | """Show individual alignments. Default `True`.""" 330 | 331 | view_as_pairs: t.Union[bool, UnsetType] = UNSET 332 | """Whether paired reads are drawn connected with a line. Default `False`.""" 333 | 334 | pairs_supported: t.Union[bool, UnsetType] = UNSET 335 | """Whether paired mate info is ignored during downsampling. Default `True`.""" 336 | 337 | color: t.Union[str, UnsetType] = UNSET 338 | """Default color of alignment blocks. Default `"rgb(170, 170, 170)"`.""" 339 | 340 | deletion_color: t.Union[str, UnsetType] = UNSET 341 | """Color of line representing a deletion. Default `"black"`.""" 342 | 343 | skipped_color: t.Union[str, UnsetType] = UNSET 344 | """Color of line representing a skipped region (e.g., splice junction). 345 | 346 | Default `"rgb(150, 170, 170)"`. 347 | """ 348 | 349 | insertion_color: t.Union[str, UnsetType] = UNSET 350 | """Color of marker for insertions. Default `"rgb(138, 94, 161)"`.""" 351 | 352 | neg_strand_color: t.Union[str, UnsetType] = UNSET 353 | """Color of alignment on negative strand. Default `"rgba(150, 150, 230, 0.75)"`. 354 | 355 | Applicable if `color_by` = `"strand"`. 356 | """ 357 | 358 | pos_strand_color: t.Union[str, UnsetType] = UNSET 359 | """Color of alignment or position strand. Default `"rgba(230, 150, 150, 0.75)"`. 360 | 361 | Applicable if `color_by` = `"strand"`. 362 | """ 363 | 364 | pair_connector_color: t.Union[str, UnsetType] = UNSET 365 | """Color of connector line between read pairs ("view as pairs" mode). 366 | 367 | Defaults to the alignment color. 368 | """ 369 | 370 | color_by: t.Union[str, UnsetType] = UNSET 371 | """Color alignment by property. Default `"unexpectedPair"`. 372 | 373 | See: https://igv.org/doc/igvjs/#tracks/Alignment-Track/#colorby-options 374 | """ 375 | 376 | group_by: t.Union[str, UnsetType] = UNSET 377 | """Group alignments by property. 378 | 379 | See: https://igv.org/doc/igvjs/#tracks/Alignment-Track/#groupby-options 380 | """ 381 | 382 | sampling_window_size: t.Union[int, UnsetType] = UNSET 383 | """Window (bucket) size for alignment downsampling in base pairs. Default `100`.""" 384 | 385 | sampling_depth: t.Union[int, UnsetType] = UNSET 386 | """Number of alignments to keep per bucket. Default 100. 387 | 388 | WARNING: Setting to a high value can freeze the browser when 389 | viewing areas of deep coverage. 390 | """ 391 | 392 | readgroup: t.Union[str, UnsetType] = UNSET 393 | """Readgroup ID value (tag 'RG').""" 394 | 395 | sort: t.Union[AlignmentSorting, UnsetType] = UNSET 396 | """Initial sort option. 397 | 398 | See: https://igv.org/doc/igvjs/#tracks/Alignment-Track/#sort-option 399 | """ 400 | 401 | filter: t.Union[AlignmentFiltering, UnsetType] = UNSET 402 | """Alignment filter options. 403 | 404 | See: https://igv.org/doc/igvjs/#tracks/Alignment-Track/#filter-options 405 | """ 406 | 407 | show_soft_clips: t.Union[bool, UnsetType] = UNSET 408 | """Show soft-clipped regions. Default `False`.""" 409 | 410 | show_mismatches: t.Union[bool, UnsetType] = UNSET 411 | """Highlight alignment bases which do not match the reference. Default `True`.""" 412 | 413 | show_all_bases: t.Union[bool, UnsetType] = UNSET 414 | """Show all bases of the read sequence. Default `False`.""" 415 | 416 | show_insertion_text: t.Union[bool, UnsetType] = UNSET 417 | """Show number of bases for insertions inline when zoomed in. Default `False`.""" 418 | 419 | insertion_text_color: t.Union[str, UnsetType] = UNSET 420 | """Color for insertion count text. Default `"white"`.""" 421 | 422 | show_deletion_text: t.Union[bool, UnsetType] = UNSET 423 | """Show number of bases deleted inline when zoomed in. Default `False`.""" 424 | 425 | deletion_text_color: t.Union[str, UnsetType] = UNSET 426 | """Color for deletion count text. Default `"black"`.""" 427 | 428 | display_mode: t.Union[t.Literal["EXPANDED", "SQUISHED", "FULL"], UnsetType] = UNSET 429 | """Display mode for the track. Deault `"EXPANDED"`. 430 | 431 | * `EXPANDED` - Pack alignments densely and draw at `alignment_row_height` 432 | * `SQUISHED` - Pack alignments densely and draw at `squished_row_height` 433 | * `FULL` - Draw 1 alignment per row at `alignment_row_height`. 434 | """ 435 | 436 | alignment_row_height: t.Union[int, UnsetType] = UNSET 437 | """Pixel height for each alignment row in `"EXPANDED"` or `"FULL"` display mode. 438 | 439 | Default `14`. 440 | """ 441 | 442 | squished_row_height: t.Union[int, UnsetType] = UNSET 443 | """Pixel height for each alignment row in `"SQUISHED"` display mode. Default `3`.""" 444 | 445 | coverage_color: t.Union[str, UnsetType] = UNSET 446 | """Color of coverage track. Default `"rgb(150, 150, 150)"`.""" 447 | 448 | coverage_track_height: t.Union[int, UnsetType] = UNSET 449 | """Height in pixels of the coverage track. Default `3`.""" 450 | 451 | autoscale: t.Union[bool, UnsetType] = UNSET 452 | """Autoscale coverage track to maximum value in view. `True` unless `max` is set.""" 453 | 454 | autoscale_group: t.Union[str, UnsetType] = UNSET 455 | """An identifier for an autoscale group for the coverage track. 456 | 457 | Tracks with the same identifier are autoscaled together. 458 | """ 459 | 460 | min: t.Union[int, UnsetType] = UNSET 461 | """Minimum value for the data (y-axis) scale. Usually zero.""" 462 | 463 | max: t.Union[int, UnsetType] = UNSET 464 | """Maximum value for the data (y-axis) scale. Ignored if `autoscale` is `True`.""" 465 | 466 | 467 | class VariantTrack(BaseTrack, tag="variant"): 468 | """Displays variant records from "VCF" files or equivalents. 469 | 470 | Associated file formats: vcf. 471 | 472 | Ref: https://igv.org/doc/igvjs/#tracks/Variant-Track/ 473 | 474 | Example: 475 | ```py 476 | # Basic 477 | 478 | VariantTrack( 479 | format="vcf", 480 | url="https://s3.amazonaws.com/1000genomes/release/20130502/ALL.chr22.phase3_shapeit2_mvncall_integrated_v5a.20130502.genotypes.vcf.gz", 481 | index_url="https://s3.amazonaws.com/1000genomes/release/20130502/ALL.chr22.phase3_shapeit2_mvncall_integrated_v5a.20130502.genotypes.vcf.gz.tbi", 482 | name="1KG variants (chr22)", 483 | squished_call_height=1, 484 | expanded_call_height=4, 485 | display_mode="SQUISHED", 486 | visibility_window=1000 487 | ) 488 | 489 | # Color-by info field with color table 490 | 491 | VariantTrack( 492 | url="https://s3.amazonaws.com/igv.org.demo/nstd186.GRCh38.variant_call.vcf.gz", 493 | index_url="https://s3.amazonaws.com/igv.org.demo/nstd186.GRCh38.variant_call.vcf.gz.tbi", 494 | name="Color by table, SVTYPE", 495 | visibility_window=-1, 496 | color_by="SVTYPE", 497 | color_table={ 498 | "DEL": "#ff2101", 499 | "INS": "#001888", 500 | "DUP": "#028401", 501 | "INV": "#008688", 502 | "CNV": "#8931ff", 503 | "BND": "#891100", 504 | "*": "#002eff", 505 | }, 506 | ) 507 | ``` 508 | """ 509 | 510 | display_mode: t.Union[t.Literal["COLLAPSED", "EXPANDED", "SQUISHED"], UnsetType] = ( 511 | UNSET 512 | ) 513 | """Display option. Default `"EXPANDED"`. 514 | 515 | * 'COLLAPSED' => show variants only 516 | * 'SQUISHED' and 'EXPANDED' => show calls. 517 | """ 518 | 519 | squished_call_height: t.Union[int, UnsetType] = UNSET 520 | """Height of genotype call rows in `"SQUISHED"` mode. Default `1`.""" 521 | 522 | expanded_call_height: t.Union[int, UnsetType] = UNSET 523 | """Height of genotype call rows in EXPANDED mode. Default `10`.""" 524 | 525 | # Variant color options 526 | 527 | color: t.Union[str, UnsetType] = UNSET 528 | """A CSS color value for a variant.""" 529 | 530 | color_by: t.Union[str, UnsetType] = UNSET 531 | """Specify an `INFO` field to color variants by. 532 | 533 | Optional, if specified takes precedence over `color` property. 534 | """ 535 | 536 | color_table: t.Union[dict[str, str], UnsetType] = UNSET 537 | """Color table mapping `INFO` field values to colors. 538 | 539 | Use in conjunction with `color_by`. 540 | 541 | Optional, if not specified a color table will be generated. 542 | """ 543 | 544 | # Genotype color options 545 | 546 | no_call_color: t.Union[str, UnsetType] = UNSET 547 | """Color for no-calls. Default `"rgb(250, 250, 250)"`.""" 548 | 549 | homevar_color: t.Union[str, UnsetType] = UNSET 550 | """CSS color for homozygous non-reference calls. Default `"rgb(17,248,254)"`""" 551 | 552 | hetvar_color: t.Union[str, UnsetType] = UNSET 553 | """CSS color for heterozygous calls. Default `"rgb(34,12,253)"`.""" 554 | 555 | homref_color: t.Union[str, UnsetType] = UNSET 556 | """CSS color for homozygous reference calls. Default `"rgb(200, 200, 200)"`.""" 557 | 558 | 559 | class MutationTrack(BaseTrack, tag="mut"): 560 | """Displays data from the National Cancer Institute's "mut" and "maf" file formats. 561 | 562 | Associated file formats: mut, maf. 563 | 564 | Ref: https://igv.org/doc/igvjs/#tracks/Mutation-Track 565 | 566 | Example: 567 | ```py 568 | MutationTrack( 569 | format="maf", 570 | url="https://s3.amazonaws.com/igv.org.demo/TCGA.BRCA.mutect.995c0111-d90b-4140-bee7-3845436c3b42.DR-10.0.somatic.maf.gz", 571 | indexed=False, 572 | height=700, 573 | display_mode="EXPANDED", 574 | ) 575 | ``` 576 | """ 577 | 578 | display_mode: t.Union[t.Literal["EXPANDED", "SQUISHED", "COLLAPSED"], UnsetType] = ( 579 | UNSET 580 | ) 581 | """The track display mode. Default `"EXPANDED"`.""" 582 | 583 | 584 | class SegmentedCopyNumberSorting(Struct, rename="camel"): 585 | """Represents initial sort order of segmented copy number rows.""" 586 | 587 | chr: str 588 | """Sequence (chromosome) name.""" 589 | 590 | start: int 591 | """Position start.""" 592 | 593 | end: int 594 | """Position end.""" 595 | 596 | direction: t.Literal["ASC", "DESC"] 597 | """Sort direction.""" 598 | 599 | 600 | class SegmentedCopyNumberTrack(BaseTrack, tag="seg"): 601 | """Displays segmented copy number values as a heatmap. 602 | 603 | Associated file formats: seg. 604 | 605 | Ref: https://igv.org/doc/igvjs/#tracks/Seg-Track 606 | 607 | * Red = amplifications 608 | * Blue = deletions 609 | 610 | There are 2 common conventions for values in segmented copy number files, 611 | the copy number itself, and a log score computed from: 612 | 613 | ```py 614 | score = 2 * np.log2(copy_number / 2) 615 | ``` 616 | 617 | The value type is indicated by the `is_log` property. 618 | 619 | If no value is set for `is_log`, it is inferred by the values in the file: 620 | 621 | * all positive values => `is_log` = `False` 622 | * any negative values => `is_log` = `True` 623 | 624 | Example: 625 | ```py 626 | SegmentedCopyNumberTrack( 627 | format="seg", 628 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 629 | indexed=False, 630 | is_log=True, 631 | name="GBM Copy # (TCGA Broad GDAC)", 632 | sort=SegmentedCopyNumberSorting( 633 | direction="DESC", 634 | chr="chr7", 635 | start=55174641, 636 | end=55175252, 637 | ), 638 | ) 639 | ``` 640 | """ 641 | 642 | display_mode: t.Union[t.Literal["EXPANDED", "SQUISHED", "FILL"], UnsetType] = UNSET 643 | """Track display mode. 644 | 645 | Affects the sample height (height of each row). The "FILL" value will result in all 646 | samples visible in the track view. 647 | """ 648 | 649 | sort: t.Union[SegmentedCopyNumberSorting, UnsetType] = UNSET 650 | """The initial sort order.""" 651 | 652 | 653 | class GwasColumns(Struct, rename="camel"): 654 | """Declaration of column number for chrom, position, & value.""" 655 | 656 | chromosome: int 657 | """Chromosome number""" 658 | position: int 659 | """Genomic position""" 660 | value: int 661 | """Value.""" 662 | 663 | 664 | class GwasTrack(BaseTrack, tag="gwas"): 665 | """Display genome wide association data as a "manhattan" style plot. 666 | 667 | Associated file formats: bed, gwas. 668 | 669 | Ref: https://igv.org/doc/igvjs/#tracks/GWAS 670 | 671 | Example: 672 | ```py 673 | GwasTrack( 674 | format="gwas", 675 | name="GWAS sample", 676 | url="https://s3.amazonaws.com/igv.org.demo/gwas_sample.tsv.gz", 677 | indexed=False, 678 | columns=GwasColumns( 679 | chromosome=12, 680 | position=13, 681 | value=28, 682 | ), 683 | ) 684 | ``` 685 | """ 686 | 687 | min: t.Union[int, UnsetType] = UNSET 688 | """Sets the minimum value for the data (y-axis) scale. Default `0`.""" 689 | 690 | max: t.Union[int, UnsetType] = UNSET 691 | """Sets the maximum value for the data (y-axis) scale. 692 | 693 | Default `25` for p-value (-log10(pvalue)), `1` for posterior probability. 694 | """ 695 | 696 | posterior_probability: t.Union[bool, UnsetType] = UNSET 697 | """Whether to interpret values as probabilities within range 0-1. Default `False`. 698 | 699 | By default values are treated as p-values and plotted as -log10(P-value). 700 | """ 701 | 702 | dot_size: t.Union[int, UnsetType] = UNSET 703 | """Diameter of dots in pixels. Default `3`.""" 704 | 705 | columns: t.Union[GwasColumns, UnsetType] = UNSET 706 | """Declaration of column number for chromosome, position, and value. 707 | 708 | For gwas format only. 709 | """ 710 | 711 | color_table: t.Union[dict[str, str], UnsetType] = UNSET 712 | """Object mapping chromosome names -> colors. 713 | 714 | If supplied all chromosomes in data should be included. 715 | 716 | See defaults: https://igv.org/doc/igvjs/#tracks/GWAS/#default-color-table 717 | """ 718 | 719 | 720 | class InteractTrack(BaseTrack, tag="interact"): 721 | """Display pairwise interactions between genome regions as arcs. 722 | 723 | Associated file formats: bedpe, interact, bigInteract 724 | 725 | Ref: https://igv.org/doc/igvjs/#tracks/Interact 726 | 727 | Example: 728 | ```py 729 | Config( 730 | genome="hg38", 731 | locus="chr2:65,489,209-65,795,733", 732 | tracks=[ 733 | InteractTrack( 734 | url="https://s3.amazonaws.com/igv.org.demo/GSM1872886_GM12878_CTCF_PET.bedpe.txt", 735 | format="bedpe", 736 | name="CTCF PET - proportional", 737 | arc_type="proportional", 738 | arc_orientation="UP", 739 | color="rgb(0,200,0)", 740 | log_scale=true, 741 | max=80, 742 | visibility_window=10_000_000, 743 | ), 744 | InteractTrack( 745 | url="https://s3.amazonaws.com/igv.org.demo/GSM1872886_GM12878_CTCF_PET.bedpe.txt", 746 | format="bedpe", 747 | name="CTCF PET - nested", 748 | arc_type="nested", 749 | arc_orientation="DOWN", 750 | color="blue", 751 | alpha=0.15, 752 | visibility_window=10_000_000, 753 | ), 754 | ], 755 | ) 756 | ``` 757 | """ 758 | 759 | arc_type: t.Union[ 760 | t.Literal["nested", "proportional", "inView", "partialInView"], UnsetType 761 | ] = UNSET 762 | """The arc type. Default `"nested"`. 763 | 764 | * nested - Arc height is proportional to feature width. 765 | * propotional - Arc height is proportional to feature score. 766 | * inView - Proportional, only draw arcs that are completely in view. 767 | * partialInView - Proportional, only draw arcs that are whole or partially in view. 768 | """ 769 | 770 | arc_orientation: t.Union[t.Literal["UP", "DOWN"], UnsetType] = UNSET 771 | """Direction of arcs ("UP" or "DOWN"). Default `"UP"`.""" 772 | 773 | alpha: t.Union[float, UnsetType] = UNSET 774 | """Alpha transparency to apply to arcs that extend beyond viewport. 775 | 776 | Must be between `0` and `1`. Default `0.5`. 777 | """ 778 | 779 | thickness: t.Union[int, UnsetType] = UNSET 780 | """Line thickness. Default `2`.""" 781 | 782 | 783 | class QtlTrack(BaseTrack, tag="qtl"): 784 | """Displays xQTL data. 785 | 786 | Associated file formats: qtl 787 | 788 | Ref: https://igv.org/doc/igvjs/#tracks/QTL-Track 789 | 790 | Example: 791 | ```py 792 | QtlTrack( 793 | format="qtl", 794 | name="B cell eQTL", 795 | url="https://igv-genepattern-org.s3.amazonaws.com/test/qtl/B.cell_eQTL.tsv.gz", 796 | index_url="https://igv-genepattern-org.s3.amazonaws.com/test/qtl/B.cell_eQTL.tsv.gz.tbi", 797 | visibility_window=4_000_000, 798 | ) 799 | ``` 800 | """ 801 | 802 | min: t.Union[float, UnsetType] = UNSET 803 | """Minimum value of y-axis in -log10 units. Default `3.5`.""" 804 | 805 | max: t.Union[float, UnsetType] = UNSET 806 | """Maximum value of y-axis in -log10 units. 807 | 808 | Optional, if not specified max is set as a percentile of values in view. 809 | """ 810 | 811 | autoscale_percentile: t.Union[float, UnsetType] = UNSET 812 | """Upper percentile for setting max value when autoscaling. 813 | 814 | Number between `0` and `100`. Default `98`. 815 | """ 816 | 817 | 818 | class SpliceJunctionTrack(BaseTrack, tag="junction"): 819 | """Displays splice junction information. 820 | 821 | Associated file formats: bed. 822 | 823 | Ref: https://igv.org/doc/igvjs/#tracks/Splice-Junctions 824 | 825 | Example: 826 | ```python 827 | SpliceJunctionTrack( 828 | name="Junctions", 829 | format="bed", 830 | url="https://www.dropbox.com/s/nvmy55hhe24plpv/splice_junction_track_test_cases_sampleA.chr15-92835700-93031800.SJ.out.bed.gz?dl=0", 831 | index_url="https://www.dropbox.com/s/iv5tcg3t8v3xu23/splice_junction_track_test_cases_sampleA.chr15-92835700-93031800.SJ.out.bed.gz.tbi?dl=0", 832 | display_mode="COLLAPSED", 833 | min_uniquely_mapped_reads=1, 834 | min_total_reads=1, 835 | max_fraction_multi_mapped_reads=1, 836 | min_spliced_alignment_overhang=0, 837 | thickness_based_on="numUniqueReads", 838 | bounce_height_based_on="random", 839 | color_by="isAnnotatedJunction", 840 | label_unique_read_count=True, 841 | label_multi_mapped_read_count=True, 842 | label_total_read_count=False, 843 | label_motif=False, 844 | label_is_annotated_junction=" [A]", 845 | hide_annotated_junctions=False, 846 | hide_unannotated_junctions=False, 847 | hide_motifs=["GT/AT", "non-canonical"], 848 | ) 849 | ``` 850 | """ 851 | 852 | # Display Options 853 | 854 | color_by: t.Union[ 855 | t.Literal[ 856 | "numUniqueReads", "numReads", "isAnnotatedJunction", "strand", "motif" 857 | ], 858 | UnsetType, 859 | ] = UNSET 860 | """Splice junction color. Default `"numUniqueReads"`.""" 861 | 862 | color_by_num_reads_threshold: t.Union[int, UnsetType] = UNSET 863 | """Threshold for `color_by`. Default `5`. 864 | 865 | If `color_by` is set to `"numUniqueReads"` or `"numReads"`, junction color will 866 | be darker when number of reads exceeds this threshold. 867 | """ 868 | 869 | thickness_based_on: t.Union[ 870 | t.Literal["numUniqueReads", "numReads", "isAnnotatedJunction"], UnsetType 871 | ] = UNSET 872 | """Splice junction line thickness. Default `"numUniqueReads"`.""" 873 | 874 | bounce_height_based_on: t.Union[ 875 | t.Literal["random", "distance", "thickness"], UnsetType 876 | ] = UNSET 877 | """Splice junction curve height. Default `"random"`.""" 878 | 879 | label_unique_read_count: t.Union[bool, UnsetType] = UNSET 880 | """Add unique read counts to splice junction label. Default `True`.""" 881 | 882 | label_multi_mapped_read_count: t.Union[bool, UnsetType] = UNSET 883 | """Add multi-mapped read counts to splice junction label. Default `True`.""" 884 | 885 | label_total_read_count: t.Union[bool, UnsetType] = UNSET 886 | """Add total read counts to splice junction label. Default `False`.""" 887 | 888 | label_motif: t.Union[bool, UnsetType] = UNSET 889 | """Add splice junction motif to its label. Default `False`.""" 890 | 891 | label_annotated_junction: t.Union[str, UnsetType] = UNSET 892 | """Label annotation for junction. 893 | 894 | If defined, the string will be appended to the labels of splice junctions that exist 895 | in known gene models. 896 | """ 897 | 898 | # Filtering Options 899 | 900 | min_uniquely_mapped_reads: t.Union[int, UnsetType] = UNSET 901 | """Junction must be supported by at least this many uniquely-mapped reads. 902 | 903 | Default `0`. 904 | """ 905 | 906 | min_total_reads: t.Union[int, UnsetType] = UNSET 907 | """Junction must be supported by at least this many uniquely-mapped + multi-mapped reads. 908 | 909 | Default `0`. 910 | """ # noqa: E501 911 | 912 | max_fraction_multi_mapped_reads: t.Union[float, UnsetType] = UNSET 913 | """(Uniquely-mapped reads) / (Total reads) must be <= this threshold. 914 | 915 | Default `1`. 916 | """ 917 | 918 | min_spliced_alignment_overhang: t.Union[int, UnsetType] = UNSET 919 | """Minimum spliced alignment overhang in base pairs. Default `0`. 920 | 921 | See [STAR aligner docs](https://github.com/alexdobin/STAR/blob/master/doc/STARmanual.pdf) for details. 922 | """ # noqa: E501 923 | 924 | hide_strand: t.Union[t.Literal["+", "-"], UnsetType] = UNSET 925 | """Set to "+" or "-" to hide junctions on the plus or minus strand.""" 926 | 927 | hide_annotated_junctions: t.Union[bool, UnsetType] = UNSET 928 | """Whether to hide annotated junctions. Default `False`. 929 | 930 | If `True`, only novel junctions will be shown (e.g., those not found in gene models passed to the aligner). 931 | """ # noqa: E501 932 | 933 | hide_unannotated_junctions: t.Union[bool, UnsetType] = UNSET 934 | """Whether to hide unannotated junctions. Default `False`. 935 | 936 | If `True`, only annotated junctions will be shown (eg. those found in gene models passed to the aligner). 937 | """ # noqa: E501 938 | 939 | hide_motifs: t.Union[list[str], UnsetType] = UNSET 940 | """A list of strings for motif values to hide. 941 | 942 | For example: ["GT/AT", "non-canonical"] 943 | """ 944 | 945 | 946 | class CnvPytorTrack(BaseTrack, tag="cnvpytor"): 947 | """Displays read depth and B-allele frequency (BAF) of variants. 948 | 949 | Associated file formats: pytor, vcf 950 | 951 | Ref: https://igv.org/doc/igvjs/#tracks/CNVPytor 952 | 953 | Example: 954 | ```python 955 | CnvPytorTrack( 956 | id="pytor_track", 957 | name="HepG2 pytor", 958 | url="https://storage.googleapis.com/cnvpytor_data/HepG2_WGS.pytor", 959 | ) 960 | ``` 961 | """ 962 | 963 | signal_name: t.Union[t.Literal["rd_snp", "rd", "snp"], UnsetType] = field( 964 | default=UNSET, name="signal_name" 965 | ) 966 | """Signal name. Default `"rd_nsp"` 967 | 968 | * rd_snp : Read Depth and BAF Likelihood 969 | * rd : Read depth 970 | * snp : BAF likelihood 971 | """ 972 | 973 | cnv_caller: t.Union[t.Literal["ReadDepth", "2D"], UnsetType] = field( 974 | default=UNSET, name="cnv_caller" 975 | ) 976 | """Name of CNV caller. Default `"2D"`. 977 | 978 | Shows data based on available caller data. 979 | 980 | * ReadDepth: Uses Read depth information only 981 | * 2D: Uses both Read depth and BAF information 982 | """ 983 | 984 | bin_size: t.Union[int, UnsetType] = field(default=UNSET, name="bin_size") 985 | """Bin size. Default `100_000`. 986 | 987 | * pytor file: Bin size should be avialable in the pytor file 988 | * vcf: Bin size should be multiple of 10,000 989 | """ 990 | 991 | colors: t.Union[list[str], UnsetType] = UNSET 992 | """Color of the signals. Signal details are in file format section. 993 | 994 | Default `["gray", "black", "green", "blue"]`. 995 | """ 996 | 997 | 998 | class MergedTrack(BaseTrack, tag="merged"): 999 | """Overlay multiple wig tracks. 1000 | 1001 | Ref: https://igv.org/doc/igvjs/#tracks/Merged/ 1002 | 1003 | Example: 1004 | ```py 1005 | MergedTrack( 1006 | name="Merged", 1007 | height=50, 1008 | alpha=0.5, 1009 | tracks=[ 1010 | WigTrack( 1011 | format="bigwig", 1012 | url="https://www.encodeproject.org/files/ENCFF000ASJ/@@download/ENCFF000ASJ.bigWig", 1013 | color="red", 1014 | ), 1015 | WigTrack( 1016 | format="bigwig", 1017 | url="https://www.encodeproject.org/files/ENCFF351WPV/@@download/ENCFF351WPV.bigWig", 1018 | color="green", 1019 | ), 1020 | ], 1021 | ) 1022 | ``` 1023 | """ 1024 | 1025 | tracks: list[WigTrack] = [] 1026 | """Child wig tracks.""" 1027 | 1028 | alpha: t.Union[float, UnsetType] = UNSET 1029 | """Alpha transparency to apply to individual track colors. 1030 | 1031 | Number between `0` and `1`. Default `0.5`. 1032 | """ 1033 | 1034 | 1035 | class ArcTrack(BaseTrack, tag="arc"): 1036 | """Displays RNA secondary structures in arcs connecting base pairs. 1037 | 1038 | Associated file formats: bp, bed 1039 | 1040 | Ref: https://igv.org/doc/igvjs/#tracks/Arc-Track 1041 | 1042 | Alternative structures, where one nucleotide is involved in more than one base pair, 1043 | and pseudo knots, where arcs cross, can be accommodated. 1044 | 1045 | Example: 1046 | ```py 1047 | ArcTrack(format="bp", name="RNA Struct BP", url="example.bp") 1048 | ``` 1049 | """ 1050 | 1051 | arc_orientation: t.Union[t.Literal["UP", "DOWN"], UnsetType] = UNSET 1052 | """Direction of arcs ("UP" or "DOWN"). Default `"UP"`.""" 1053 | 1054 | 1055 | Track = t.Union[ 1056 | AnnotationTrack, 1057 | WigTrack, 1058 | AlignmentTrack, 1059 | VariantTrack, 1060 | MutationTrack, 1061 | SegmentedCopyNumberTrack, 1062 | InteractTrack, 1063 | QtlTrack, 1064 | SpliceJunctionTrack, 1065 | CnvPytorTrack, 1066 | MergedTrack, 1067 | ArcTrack, 1068 | ] 1069 | -------------------------------------------------------------------------------- /src/pygv/_version.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | try: 4 | __version__ = importlib.metadata.version("pygv") 5 | except importlib.metadata.PackageNotFoundError: 6 | __version__ = "unknown" 7 | -------------------------------------------------------------------------------- /src/pygv/static/widget.js: -------------------------------------------------------------------------------- 1 | import igv from "https://esm.sh/igv@3.1.2"; 2 | 3 | /** 4 | * @typedef Config 5 | * @property {string} genome 6 | * @property {(string | Array)=} locus 7 | * @property {Array>} tracks 8 | */ 9 | 10 | /** 11 | * @typedef Model 12 | * @property {Config} config 13 | */ 14 | 15 | /** @type {import("npm:@anywidget/types").Render} */ 16 | async function render({ model, el }) { 17 | const browser = await igv.createBrowser(el, model.get("config")); 18 | return () => { 19 | igv.removeBrowser(browser); 20 | }; 21 | } 22 | 23 | export default { render }; 24 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manzt/pygv/e2a33595c34706364d761dfff289dceb5b92760a/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_pygv.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | 4 | import pytest 5 | from inline_snapshot import snapshot 6 | 7 | import pygv 8 | from pygv import Config 9 | from pygv._api import load 10 | from pygv._tracks import ( 11 | AlignmentTrack, 12 | AnnotationTrack, 13 | MergedTrack, 14 | SegmentedCopyNumberTrack, 15 | VariantTrack, 16 | WigTrack, 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def config_dict(): 22 | return { 23 | "genome": "hg38", 24 | "locus": "chr8:127,736,588-127,739,371", 25 | "tracks": [ 26 | { 27 | "name": "HG00103", 28 | "url": "https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram", 29 | "indexURL": "https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram.crai", 30 | "format": "cram", 31 | }, 32 | ], 33 | } 34 | 35 | 36 | def test_loads_config(config_dict: dict) -> None: 37 | assert Config.from_dict(config_dict) == snapshot( 38 | Config( 39 | genome="hg38", 40 | locus="chr8:127,736,588-127,739,371", 41 | tracks=[ 42 | AlignmentTrack( 43 | name="HG00103", 44 | url="https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram", 45 | index_url="https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram.crai", 46 | format="cram", 47 | ) 48 | ], 49 | ) 50 | ) 51 | 52 | 53 | def test_loads_file(config_dict: dict, tmp_path: pathlib.Path) -> None: 54 | path = tmp_path / "config.json" 55 | path.write_text(json.dumps(config_dict), encoding="utf-8") 56 | 57 | with path.open() as f: 58 | browser = load(f) 59 | 60 | assert browser.config == snapshot( 61 | Config( 62 | genome="hg38", 63 | locus="chr8:127,736,588-127,739,371", 64 | tracks=[ 65 | AlignmentTrack( 66 | url="https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram", 67 | name="HG00103", 68 | index_url="https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram.crai", 69 | format="cram", 70 | ) 71 | ], 72 | ) 73 | ) 74 | 75 | 76 | def test_merged() -> None: 77 | assert Config.from_dict( 78 | { 79 | "genome": "hg19", 80 | "locus": "chr4:40,174,668-40,221,204", 81 | "tracks": [ 82 | { 83 | "name": "Merged - individual autoscaled", 84 | "type": "merged", 85 | "tracks": [ 86 | { 87 | "type": "wig", 88 | "format": "bigwig", 89 | "url": "https://www.encodeproject.org/files/ENCFF000ASJ/@@download/ENCFF000ASJ.bigWig", 90 | "color": "red", 91 | "autoscale": True, 92 | }, 93 | { 94 | "type": "wig", 95 | "format": "bigwig", 96 | "url": "https://www.encodeproject.org/files/ENCFF351WPV/@@download/ENCFF351WPV.bigWig", 97 | "color": "green", 98 | "autoscale": True, 99 | }, 100 | ], 101 | }, 102 | ], 103 | } 104 | ) == snapshot( 105 | Config( 106 | genome="hg19", 107 | locus="chr4:40,174,668-40,221,204", 108 | tracks=[ 109 | MergedTrack( 110 | name="Merged - individual autoscaled", 111 | tracks=[ 112 | WigTrack( 113 | url="https://www.encodeproject.org/files/ENCFF000ASJ/@@download/ENCFF000ASJ.bigWig", 114 | format="bigwig", 115 | color="red", 116 | autoscale=True, 117 | ), 118 | WigTrack( 119 | url="https://www.encodeproject.org/files/ENCFF351WPV/@@download/ENCFF351WPV.bigWig", 120 | format="bigwig", 121 | color="green", 122 | autoscale=True, 123 | ), 124 | ], 125 | ) 126 | ], 127 | ) 128 | ) 129 | 130 | 131 | def test_seg() -> None: 132 | assert Config.from_dict( 133 | { 134 | "genome": "hg19", 135 | "showSampleNames": True, 136 | "tracks": [ 137 | { 138 | "name": "Explicit Samples", 139 | "type": "seg", 140 | "format": "seg", 141 | "samples": [ 142 | "TCGA-06-0168-01A-02D-0236-01", 143 | "TCGA-02-0115-01A-01D-0193-01", 144 | "TCGA-02-2485-01A-01D-0784-01", 145 | "TCGA-06-0151-01A-01D-0236-01", 146 | ], 147 | "url": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 148 | "height": 100, 149 | }, 150 | { 151 | "name": "Segmented Copy Number", 152 | "type": "seg", 153 | "format": "seg", 154 | "url": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 155 | }, 156 | { 157 | "name": "Indexed", 158 | "type": "seg", 159 | "format": "seg", 160 | "url": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 161 | "indexURL": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz.tbi", 162 | }, 163 | { 164 | "name": "Indexed with visibility window", 165 | "type": "seg", 166 | "format": "seg", 167 | "url": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 168 | "indexURL": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz.tbi", 169 | "visibilityWindow": "100000000", 170 | }, 171 | ], 172 | } 173 | ) == snapshot( 174 | Config( 175 | genome="hg19", 176 | show_sample_names=True, 177 | tracks=[ 178 | SegmentedCopyNumberTrack( 179 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 180 | name="Explicit Samples", 181 | format="seg", 182 | height=100, 183 | samples=[ 184 | "TCGA-06-0168-01A-02D-0236-01", 185 | "TCGA-02-0115-01A-01D-0193-01", 186 | "TCGA-02-2485-01A-01D-0784-01", 187 | "TCGA-06-0151-01A-01D-0236-01", 188 | ], 189 | ), 190 | SegmentedCopyNumberTrack( 191 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 192 | name="Segmented Copy Number", 193 | format="seg", 194 | ), 195 | SegmentedCopyNumberTrack( 196 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 197 | name="Indexed", 198 | index_url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz.tbi", 199 | format="seg", 200 | ), 201 | SegmentedCopyNumberTrack( 202 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 203 | name="Indexed with visibility window", 204 | index_url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz.tbi", 205 | format="seg", 206 | visibility_window="100000000", 207 | ), 208 | ], 209 | ) 210 | ) 211 | 212 | 213 | def test_basic_config(): 214 | assert Config.from_dict( 215 | { 216 | "genome": "hg19", 217 | "locus": "chr1:155,160,475-155,184,282", 218 | "tracks": [ 219 | { 220 | "url": "https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 221 | "name": "GBM Copy # (TCGA Broad GDAC)", 222 | }, 223 | { 224 | "type": "annotation", 225 | "format": "bed", 226 | "url": "https://data.broadinstitute.org/igvdata/annotations/hg19/dbSnp/snp137.hg19.bed.gz", 227 | "indexURL": "https://data.broadinstitute.org/igvdata/annotations/hg19/dbSnp/snp137.hg19.bed.gz.tbi", 228 | "visibilityWindow": 200_000, 229 | "name": "dbSNP 137", 230 | }, 231 | { 232 | "type": "wig", 233 | "format": "bigwig", 234 | "url": "https://s3.amazonaws.com/igv.broadinstitute.org/data/hg19/encode/wgEncodeBroadHistoneGm12878H3k4me3StdSig.bigWig", 235 | "name": "Gm12878H3k4me3", 236 | }, 237 | { 238 | "type": "alignment", 239 | "format": "bam", 240 | "url": "https://1000genomes.s3.amazonaws.com/phase3/data/HG02450/alignment/HG02450.mapped.ILLUMINA.bwa.ACB.low_coverage.20120522.bam", 241 | "indexURL": "https://1000genomes.s3.amazonaws.com/phase3/data/HG02450/alignment/HG02450.mapped.ILLUMINA.bwa.ACB.low_coverage.20120522.bam.bai", 242 | "name": "HG02450", 243 | }, 244 | ], 245 | } 246 | ) == snapshot( 247 | Config( 248 | genome="hg19", 249 | locus="chr1:155,160,475-155,184,282", 250 | tracks=[ 251 | SegmentedCopyNumberTrack( 252 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 253 | name="GBM Copy # (TCGA Broad GDAC)", 254 | ), 255 | AnnotationTrack( 256 | url="https://data.broadinstitute.org/igvdata/annotations/hg19/dbSnp/snp137.hg19.bed.gz", 257 | name="dbSNP 137", 258 | index_url="https://data.broadinstitute.org/igvdata/annotations/hg19/dbSnp/snp137.hg19.bed.gz.tbi", 259 | format="bed", 260 | visibility_window=200000, 261 | ), 262 | WigTrack( 263 | url="https://s3.amazonaws.com/igv.broadinstitute.org/data/hg19/encode/wgEncodeBroadHistoneGm12878H3k4me3StdSig.bigWig", 264 | name="Gm12878H3k4me3", 265 | format="bigwig", 266 | ), 267 | AlignmentTrack( 268 | url="https://1000genomes.s3.amazonaws.com/phase3/data/HG02450/alignment/HG02450.mapped.ILLUMINA.bwa.ACB.low_coverage.20120522.bam", 269 | name="HG02450", 270 | index_url="https://1000genomes.s3.amazonaws.com/phase3/data/HG02450/alignment/HG02450.mapped.ILLUMINA.bwa.ACB.low_coverage.20120522.bam.bai", 271 | format="bam", 272 | ), 273 | ], 274 | ) 275 | ) 276 | 277 | 278 | def test_simple_api(): 279 | browser = pygv.browse( 280 | "https://example.com/example.vcf", 281 | ("https://example.com/example.bam", "https://example.com/example.bam.bai"), 282 | pygv.track( 283 | "https://example.com/10x_cov.bw", name="10x coverage", autoscale=True 284 | ), 285 | pygv.track( 286 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 287 | name="GBM Copy # (TCGA Broad GDAC)", 288 | indexed=False, 289 | is_log=True, 290 | color="blue", 291 | ), 292 | ) 293 | 294 | assert browser.config == snapshot( 295 | Config( 296 | genome="hg38", 297 | tracks=[ 298 | VariantTrack( 299 | url="https://example.com/example.vcf", 300 | name="https://example.com/example.vcf", 301 | ), 302 | AlignmentTrack( 303 | url="https://example.com/example.bam", 304 | name="https://example.com/example.bam", 305 | index_url="https://example.com/example.bam.bai", 306 | ), 307 | WigTrack( 308 | url="https://example.com/10x_cov.bw", 309 | name="10x coverage", 310 | autoscale=True, 311 | ), 312 | SegmentedCopyNumberTrack( 313 | url="https://s3.amazonaws.com/igv.org.demo/GBM-TP.seg.gz", 314 | name="GBM Copy # (TCGA Broad GDAC)", 315 | indexed=False, 316 | color="blue", 317 | ), 318 | ], 319 | ) 320 | ) 321 | --------------------------------------------------------------------------------