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 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
@@ -0,0 +1,3 @@
1
+ .navbar {
2
+ border: none;
3
+ }
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ makoview = makoview.wrapper:main