makoview 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- makoview-0.1.2/.github/workflows/publish-to-pypi.yml +75 -0
- makoview-0.1.2/.gitignore +220 -0
- makoview-0.1.2/PKG-INFO +14 -0
- makoview-0.1.2/README.md +1 -0
- makoview-0.1.2/pyproject.toml +31 -0
- makoview-0.1.2/src/makoview/__init__.py +0 -0
- makoview-0.1.2/src/makoview/app.py +179 -0
- makoview-0.1.2/src/makoview/plots.py +105 -0
- makoview-0.1.2/src/makoview/styles.css +3 -0
- makoview-0.1.2/src/makoview/wrapper.py +63 -0
- makoview-0.1.2/uv.lock +1068 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
|
|
2
|
+
|
|
3
|
+
on: push
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
name: Build distribution 📦
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
with:
|
|
13
|
+
persist-credentials: false
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v7
|
|
17
|
+
|
|
18
|
+
- name: Install Python 3.13
|
|
19
|
+
run: uv python install 3.13
|
|
20
|
+
|
|
21
|
+
- name: Build
|
|
22
|
+
run: uv build
|
|
23
|
+
|
|
24
|
+
- name: Store the distribution packages
|
|
25
|
+
uses: actions/upload-artifact@v5
|
|
26
|
+
with:
|
|
27
|
+
name: python-package-distributions
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish-to-pypi:
|
|
31
|
+
name: >-
|
|
32
|
+
Publish Python 🐍 distribution 📦 to PyPI
|
|
33
|
+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
|
|
34
|
+
needs:
|
|
35
|
+
- build
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment:
|
|
38
|
+
name: pypi
|
|
39
|
+
url: https://pypi.org/p/makoview # Replace <package-name> with your PyPI project name
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Download all the dists
|
|
45
|
+
uses: actions/download-artifact@v6
|
|
46
|
+
with:
|
|
47
|
+
name: python-package-distributions
|
|
48
|
+
path: dist/
|
|
49
|
+
- name: Publish distribution 📦 to PyPI
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
51
|
+
|
|
52
|
+
publish-to-testpypi:
|
|
53
|
+
name: Publish Python 🐍 distribution 📦 to TestPyPI
|
|
54
|
+
needs:
|
|
55
|
+
- build
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
|
|
58
|
+
environment:
|
|
59
|
+
name: testpypi
|
|
60
|
+
url: https://test.pypi.org/p/makoview
|
|
61
|
+
|
|
62
|
+
permissions:
|
|
63
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
- name: Download all the dists
|
|
67
|
+
uses: actions/download-artifact@v6
|
|
68
|
+
with:
|
|
69
|
+
name: python-package-distributions
|
|
70
|
+
path: dist/
|
|
71
|
+
- name: Publish distribution 📦 to TestPyPI
|
|
72
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
73
|
+
with:
|
|
74
|
+
repository-url: https://test.pypi.org/legacy/
|
|
75
|
+
verbose: true
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
|
|
204
|
+
# Ruff stuff:
|
|
205
|
+
.ruff_cache/
|
|
206
|
+
|
|
207
|
+
# PyPI configuration file
|
|
208
|
+
.pypirc
|
|
209
|
+
|
|
210
|
+
# Marimo
|
|
211
|
+
marimo/_static/
|
|
212
|
+
marimo/_lsp/
|
|
213
|
+
__marimo__/
|
|
214
|
+
|
|
215
|
+
# Streamlit
|
|
216
|
+
.streamlit/secrets.toml
|
|
217
|
+
|
|
218
|
+
# DuckDB database files used in testing
|
|
219
|
+
**/*.duckdb
|
|
220
|
+
**/.DS_Store
|
makoview-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: makoview
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Visualise the results from Mako
|
|
5
|
+
Requires-Python: <3.13,>=3.9
|
|
6
|
+
Requires-Dist: duckdb>=1.0.0
|
|
7
|
+
Requires-Dist: matplotlib>=3.9.4
|
|
8
|
+
Requires-Dist: pandas>=2.3.3
|
|
9
|
+
Requires-Dist: seaborn>=0.13.2
|
|
10
|
+
Requires-Dist: shiny>=0.10.0
|
|
11
|
+
Requires-Dist: shinyswatch>=0.9.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# makoview
|
makoview-0.1.2/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# makoview
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "makoview"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Visualise the results from Mako"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9,<3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"shiny>=0.10.0",
|
|
9
|
+
"duckdb>=1.0.0",
|
|
10
|
+
"pandas>=2.3.3",
|
|
11
|
+
"matplotlib>=3.9.4",
|
|
12
|
+
"seaborn>=0.13.2",
|
|
13
|
+
"shinyswatch>=0.9.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
build-backend = 'hatchling.build'
|
|
18
|
+
requires = ['hatchling']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
[tool.uv_build]
|
|
22
|
+
src-layout = true
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[tool.uv]
|
|
26
|
+
dev-dependencies = []
|
|
27
|
+
package = true
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
makoview = "makoview.wrapper:main"
|
|
File without changes
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from shiny.types import SilentException
|
|
2
|
+
from shiny.express import input, render, ui
|
|
3
|
+
from shiny import reactive
|
|
4
|
+
import duckdb
|
|
5
|
+
import os
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import shinyswatch
|
|
10
|
+
|
|
11
|
+
import plots
|
|
12
|
+
|
|
13
|
+
_db_path = os.environ["MAKO_DIFFERENTIAL_DB"]
|
|
14
|
+
_reads_path = os.environ["MAKO_MODIFICATION_DB"]
|
|
15
|
+
|
|
16
|
+
# Validate that database files exist
|
|
17
|
+
if not os.path.exists(_db_path):
|
|
18
|
+
raise FileNotFoundError(f"Differential sites database not found: {_db_path}")
|
|
19
|
+
if not os.path.exists(_reads_path):
|
|
20
|
+
raise FileNotFoundError(f"Modification database not found: {_reads_path}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# load from db into a Pandas dataframe in-memory for faster access
|
|
24
|
+
def load_data_into_memory(db_path) -> pd.DataFrame:
|
|
25
|
+
conn = duckdb.connect(database=db_path, read_only=True)
|
|
26
|
+
df = conn.execute("SELECT * FROM sites").fetchdf()
|
|
27
|
+
conn.close()
|
|
28
|
+
return df
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
df = load_data_into_memory(_db_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# UI Definition
|
|
35
|
+
ui.page_opts(title="makoview", theme=shinyswatch.theme.lumen)
|
|
36
|
+
ui.include_css(Path(__file__).parent / "styles.css")
|
|
37
|
+
# ui.page_opts(title="Mako modification lookup", fillable=True)
|
|
38
|
+
|
|
39
|
+
read_cache: reactive.Value[pd.DataFrame] = reactive.value()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
with ui.sidebar(width="400px", open="always"):
|
|
43
|
+
with ui.card(fill=False):
|
|
44
|
+
ui.card_header("Search for a transcript...")
|
|
45
|
+
# with ui.layout_columns(col_widths=(8, 4), gap="0.75rem", row_heights="auto"):
|
|
46
|
+
ui.input_text(
|
|
47
|
+
"transcript_id",
|
|
48
|
+
"Transcript ID",
|
|
49
|
+
placeholder="e.g. ENST00000000233.10",
|
|
50
|
+
width="100%",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
ui.input_selectize(
|
|
54
|
+
"transcript_position",
|
|
55
|
+
"Transcript Position",
|
|
56
|
+
choices=[],
|
|
57
|
+
multiple=False,
|
|
58
|
+
options={
|
|
59
|
+
"placeholder": "transcript not found...",
|
|
60
|
+
"dropdownParent": "body",
|
|
61
|
+
},
|
|
62
|
+
width="100%",
|
|
63
|
+
)
|
|
64
|
+
ui.input_action_button(
|
|
65
|
+
"search_btn", "Search", class_="btn-primary", style="margin-top: 0.5rem;"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@render.data_frame
|
|
69
|
+
@reactive.event(input.search_btn, ignore_none=False)
|
|
70
|
+
def on_search():
|
|
71
|
+
"""Generate boxplot of probability_modified by group and sample."""
|
|
72
|
+
transcript_id: str = input.transcript_id()
|
|
73
|
+
transcript_position: int = input.transcript_position()
|
|
74
|
+
|
|
75
|
+
if not transcript_id or not transcript_id.strip():
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Get matching reads as a dataframe
|
|
79
|
+
reads_df = get_matching_reads(transcript_id.strip(), int(transcript_position))
|
|
80
|
+
|
|
81
|
+
site_df = df.loc[
|
|
82
|
+
(df["transcript_id"] == transcript_id.strip())
|
|
83
|
+
& (df["transcript_position"] == int(transcript_position))
|
|
84
|
+
]
|
|
85
|
+
site_df_tidy = site_df.melt(var_name="column", value_name="value")
|
|
86
|
+
|
|
87
|
+
read_cache.set(reads_df)
|
|
88
|
+
|
|
89
|
+
return render.DataGrid(data=site_df_tidy, width="100%", height="auto")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_matching_reads(transcript_id: str, transcript_position: int) -> pd.DataFrame:
|
|
93
|
+
"""
|
|
94
|
+
Find rnames for the given transcript_id and transcript_position,
|
|
95
|
+
then query reads database for matching non-ignored reads.
|
|
96
|
+
"""
|
|
97
|
+
# Find matching rnames from the sites dataframe
|
|
98
|
+
matching_rnames = (
|
|
99
|
+
df.loc[
|
|
100
|
+
(df["transcript_id"] == transcript_id)
|
|
101
|
+
& (df["transcript_position"] == transcript_position),
|
|
102
|
+
"rname",
|
|
103
|
+
]
|
|
104
|
+
.unique()
|
|
105
|
+
.tolist()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
print(matching_rnames)
|
|
109
|
+
|
|
110
|
+
if not matching_rnames:
|
|
111
|
+
return pd.DataFrame()
|
|
112
|
+
|
|
113
|
+
# Query reads database for matching, non-ignored reads
|
|
114
|
+
conn = duckdb.connect(database=_reads_path, read_only=True)
|
|
115
|
+
|
|
116
|
+
# Use parameterized query to avoid SQL injection
|
|
117
|
+
placeholders = ", ".join(["?" for _ in matching_rnames])
|
|
118
|
+
query = f"""
|
|
119
|
+
SELECT * FROM reads
|
|
120
|
+
WHERE rname IN ({placeholders})
|
|
121
|
+
AND transcript_position = {transcript_position}
|
|
122
|
+
AND ignored = FALSE
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
reads_df = conn.execute(query, matching_rnames).fetchdf()
|
|
126
|
+
conn.close()
|
|
127
|
+
|
|
128
|
+
return reads_df
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Server logic
|
|
132
|
+
@reactive.effect
|
|
133
|
+
@reactive.event(input.transcript_id)
|
|
134
|
+
def update_transcript_pos():
|
|
135
|
+
"""Update the transcript position options based on the entered transcript ID."""
|
|
136
|
+
transcript_id: str = input.transcript_id()
|
|
137
|
+
|
|
138
|
+
if transcript_id and transcript_id.strip():
|
|
139
|
+
positions = df.loc[
|
|
140
|
+
df["transcript_id"] == transcript_id.strip(), "transcript_position"
|
|
141
|
+
].unique()
|
|
142
|
+
positions = sorted(positions.tolist())
|
|
143
|
+
else:
|
|
144
|
+
positions = []
|
|
145
|
+
|
|
146
|
+
ui.update_selectize(
|
|
147
|
+
"transcript_position",
|
|
148
|
+
choices=positions,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@render.data_frame
|
|
153
|
+
def plot_counts():
|
|
154
|
+
try:
|
|
155
|
+
subset = read_cache.get()
|
|
156
|
+
except SilentException:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
binarized_df = (
|
|
160
|
+
subset.groupby(["sample_name", "group_name"])
|
|
161
|
+
.agg(
|
|
162
|
+
successes=("probability_modified", lambda x: (x >= 0.5).sum()),
|
|
163
|
+
failures=("probability_modified", lambda x: (x < 0.5).sum()),
|
|
164
|
+
)
|
|
165
|
+
.sort_values(["group_name", "sample_name"])
|
|
166
|
+
.reset_index()
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return binarized_df
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@render.plot(height=750)
|
|
173
|
+
def plot_modification():
|
|
174
|
+
try:
|
|
175
|
+
subset = read_cache.get()
|
|
176
|
+
except SilentException:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
return plots.plot_binarised_violin_by_site(subset)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import seaborn as sns
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def plot_binarised_violin_by_site(subset):
|
|
8
|
+
# Binarise probability (threshold 0.5)
|
|
9
|
+
subset["binarised_probability_modified"] = (
|
|
10
|
+
subset["probability_modified"] >= 0.5
|
|
11
|
+
).astype(int)
|
|
12
|
+
|
|
13
|
+
# Add combined label and sort
|
|
14
|
+
subset["label"] = subset["sample_name"] + " (" + subset["group_name"] + ")"
|
|
15
|
+
subset.sort_values(["group_name", "sample_name"], inplace=True)
|
|
16
|
+
label_order = subset["label"].unique()
|
|
17
|
+
|
|
18
|
+
# Compute counts per label
|
|
19
|
+
counts = subset.groupby("label")["binarised_probability_modified"]
|
|
20
|
+
n = counts.size().reindex(label_order)
|
|
21
|
+
T = counts.sum().reindex(label_order)
|
|
22
|
+
F = n - T
|
|
23
|
+
T_over_n = (T / n).round(3)
|
|
24
|
+
|
|
25
|
+
label_order_with_stats = [
|
|
26
|
+
f"{lbl}\n(n={n[lbl]}, T={T[lbl]}, F={F[lbl]}, T/n={T_over_n[lbl]})"
|
|
27
|
+
for lbl in label_order
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Plot
|
|
31
|
+
fig, ax = plt.subplots(1, 1, figsize=(max(10, len(label_order) * 1.5), 8))
|
|
32
|
+
|
|
33
|
+
sns.violinplot(
|
|
34
|
+
data=subset,
|
|
35
|
+
x="label",
|
|
36
|
+
y="binarised_probability_modified",
|
|
37
|
+
order=label_order,
|
|
38
|
+
inner=None,
|
|
39
|
+
density_norm="width",
|
|
40
|
+
cut=0,
|
|
41
|
+
color="skyblue",
|
|
42
|
+
ax=ax,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sns.boxplot(
|
|
46
|
+
data=subset,
|
|
47
|
+
x="label",
|
|
48
|
+
y="binarised_probability_modified",
|
|
49
|
+
order=label_order,
|
|
50
|
+
showcaps=True,
|
|
51
|
+
width=0.15,
|
|
52
|
+
boxprops={"facecolor": "white", "edgecolor": "black", "linewidth": 1},
|
|
53
|
+
whiskerprops={"color": "black", "linewidth": 1},
|
|
54
|
+
capprops={"color": "black", "linewidth": 1},
|
|
55
|
+
medianprops={"color": "black", "linewidth": 1},
|
|
56
|
+
showfliers=False,
|
|
57
|
+
ax=ax,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
sns.stripplot(
|
|
61
|
+
data=subset,
|
|
62
|
+
x="label",
|
|
63
|
+
y="binarised_probability_modified",
|
|
64
|
+
order=label_order,
|
|
65
|
+
color="black",
|
|
66
|
+
size=3,
|
|
67
|
+
jitter=True,
|
|
68
|
+
alpha=0.5,
|
|
69
|
+
ax=ax,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Mean ± SD
|
|
73
|
+
stats = (
|
|
74
|
+
subset.groupby("label")["binarised_probability_modified"]
|
|
75
|
+
.agg(["mean", "std"])
|
|
76
|
+
.reindex(label_order)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
for j, label in enumerate(label_order):
|
|
80
|
+
mean_val = stats.loc[label, "mean"]
|
|
81
|
+
std_val = stats.loc[label, "std"]
|
|
82
|
+
|
|
83
|
+
ax.plot(j, mean_val, "o", color="red", markersize=6)
|
|
84
|
+
|
|
85
|
+
ymin = max(0, mean_val - std_val)
|
|
86
|
+
ymax = min(1, mean_val + std_val)
|
|
87
|
+
ax.errorbar(
|
|
88
|
+
j,
|
|
89
|
+
mean_val,
|
|
90
|
+
yerr=[[mean_val - ymin], [ymax - mean_val]],
|
|
91
|
+
fmt="none",
|
|
92
|
+
ecolor="red",
|
|
93
|
+
elinewidth=1,
|
|
94
|
+
capsize=5,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
ax.set_xticks(range(len(label_order)))
|
|
98
|
+
ax.set_xticklabels(label_order_with_stats, rotation=45, ha="right")
|
|
99
|
+
ax.set_xlabel("Sample (Group)")
|
|
100
|
+
ax.set_ylabel("Binarised Probability Modified")
|
|
101
|
+
ax.set_title("Violin Plot of Binarised Probability Modified for each Sample")
|
|
102
|
+
ax.set_ylim(-0.05, 1.05)
|
|
103
|
+
ax.grid(True, linestyle="--", alpha=0.6)
|
|
104
|
+
|
|
105
|
+
return fig
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from shiny import run_app
|
|
2
|
+
import os
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
"""Launch the MakoView Shiny visualization application."""
|
|
9
|
+
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
description="Launch makoview",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--differential-results",
|
|
16
|
+
type=Path,
|
|
17
|
+
required=True,
|
|
18
|
+
dest="differential_results",
|
|
19
|
+
help="Path to differential sites database file",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--modification-db",
|
|
24
|
+
type=Path,
|
|
25
|
+
required=True,
|
|
26
|
+
dest="modification_db",
|
|
27
|
+
help="Path to modification database file",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--port",
|
|
32
|
+
type=int,
|
|
33
|
+
default=8000,
|
|
34
|
+
help="Port for the Shiny application (default: 8000)",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
# Use explicitly provided paths
|
|
40
|
+
diff_db_path = args.differential_results
|
|
41
|
+
mod_db_path = args.modification_db
|
|
42
|
+
|
|
43
|
+
# Validate files exist
|
|
44
|
+
if not diff_db_path.exists():
|
|
45
|
+
parser.error(f"Differential results file not found: {diff_db_path}")
|
|
46
|
+
if not mod_db_path.exists():
|
|
47
|
+
parser.error(f"Modification database file not found: {mod_db_path}")
|
|
48
|
+
|
|
49
|
+
# Set environment variables for app.py to consume
|
|
50
|
+
os.environ["MAKO_DIFFERENTIAL_DB"] = str(diff_db_path.absolute())
|
|
51
|
+
os.environ["MAKO_MODIFICATION_DB"] = str(mod_db_path.absolute())
|
|
52
|
+
|
|
53
|
+
print(f"Starting Mako Shiny app on port {args.port}...")
|
|
54
|
+
print(f" Differential DB: {diff_db_path.absolute()}")
|
|
55
|
+
print(f" Modification DB: {mod_db_path.absolute()}")
|
|
56
|
+
|
|
57
|
+
path = Path(__file__).parent.resolve()
|
|
58
|
+
|
|
59
|
+
run_app(str(path / "app.py"), port=args.port) # type: ignore[call-non-callable]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|