makoview 0.1.2__py3-none-any.whl
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/__init__.py +0 -0
- makoview/app.py +179 -0
- makoview/plots.py +105 -0
- makoview/styles.css +3 -0
- makoview/wrapper.py +63 -0
- makoview-0.1.2.dist-info/METADATA +14 -0
- makoview-0.1.2.dist-info/RECORD +9 -0
- makoview-0.1.2.dist-info/WHEEL +4 -0
- makoview-0.1.2.dist-info/entry_points.txt +2 -0
makoview/__init__.py
ADDED
|
File without changes
|
makoview/app.py
ADDED
|
@@ -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)
|
makoview/plots.py
ADDED
|
@@ -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
|
makoview/styles.css
ADDED
makoview/wrapper.py
ADDED
|
@@ -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()
|
|
@@ -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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
makoview/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
makoview/app.py,sha256=M_jU2T-c4PpSfSyIqqV5ih3w3k46SQbIkJ264I6xPLs,5234
|
|
3
|
+
makoview/plots.py,sha256=yUQ3BPZmhJPJPF4LCT1qnyiqLSOiTh62dPifYbfFQgU,2970
|
|
4
|
+
makoview/styles.css,sha256=ttB0IB8J7_nxmwmddHoEIOvDB2QaWNoIdIS3OVq80Qo,29
|
|
5
|
+
makoview/wrapper.py,sha256=xaOdKwvobRLs81xktfWTbzZX2JVzwN9yksGV2etISRY,1724
|
|
6
|
+
makoview-0.1.2.dist-info/METADATA,sha256=HxT0Hx08lJsO-njGJxM0_C-1KzGt5_LLIkYmy5JCvc4,358
|
|
7
|
+
makoview-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
makoview-0.1.2.dist-info/entry_points.txt,sha256=ezLJK-IIC64DKimCa4fYFeVEaxHF1l0QFxwC-aL25IM,51
|
|
9
|
+
makoview-0.1.2.dist-info/RECORD,,
|