genome-spy-python 0.1.0__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.
Files changed (64) hide show
  1. genome_spy/__init__.py +199 -0
  2. genome_spy/_chart_authoring.py +231 -0
  3. genome_spy/_conditions.py +72 -0
  4. genome_spy/_embed.py +87 -0
  5. genome_spy/_expressions.py +271 -0
  6. genome_spy/_parameters.py +267 -0
  7. genome_spy/_render.py +207 -0
  8. genome_spy/_utils.py +75 -0
  9. genome_spy/_widget.py +262 -0
  10. genome_spy/api.py +198 -0
  11. genome_spy/arrow.py +155 -0
  12. genome_spy/channels.py +193 -0
  13. genome_spy/chart.py +1240 -0
  14. genome_spy/data.py +56 -0
  15. genome_spy/data_transformers.py +267 -0
  16. genome_spy/datasets/__init__.py +189 -0
  17. genome_spy/datasets/_airway.py +219 -0
  18. genome_spy/datasets/_annotations.py +37 -0
  19. genome_spy/datasets/_gistic.py +43 -0
  20. genome_spy/datasets/_grammar.py +66 -0
  21. genome_spy/datasets/_hapmap.py +180 -0
  22. genome_spy/datasets/_mutation.py +289 -0
  23. genome_spy/datasets/_oncoprint.py +523 -0
  24. genome_spy/datasets/data/airway_metadata.csv +9 -0
  25. genome_spy/datasets/data/airway_scaledcounts.csv +38695 -0
  26. genome_spy/datasets/data/brca.maf.gz +0 -0
  27. genome_spy/datasets/data/hapmap_gwas.csv +14413 -0
  28. genome_spy/datasets/data/mutation_impact_reference.json +27 -0
  29. genome_spy/datasets/data/oncoprint_dataset3.json +266 -0
  30. genome_spy/datasets/data/p53_sequence_comparison.json.gz +0 -0
  31. genome_spy/datasets/data/pik3ca_mutations.json +1 -0
  32. genome_spy/datasets/data/pik3ca_tcga_brca_lollipop.json +38 -0
  33. genome_spy/datasets/data/refseq_gene_bodies.csv.gz +0 -0
  34. genome_spy/datasets/data/tal1_alphagenome_reference.json.gz +0 -0
  35. genome_spy/datasets/data/tcga.tsv +146 -0
  36. genome_spy/datasets/data/tcga_laml.maf.gz +0 -0
  37. genome_spy/datasets/data/tcga_laml_annot.tsv +201 -0
  38. genome_spy/datasets/data/tcga_laml_combined_oncoplot.json.gz +0 -0
  39. genome_spy/datasets/data/tcga_ov_gistic_lesions.tsv.gz +0 -0
  40. genome_spy/datasets/data/tcga_ov_gistic_scores.tsv.gz +0 -0
  41. genome_spy/helpers.py +185 -0
  42. genome_spy/jupyter.py +5 -0
  43. genome_spy/py.typed +0 -0
  44. genome_spy/schema/__init__.py +784 -0
  45. genome_spy/schema/_kwds.py +1394 -0
  46. genome_spy/schema/_typing.py +186 -0
  47. genome_spy/schema/capabilities.json +593 -0
  48. genome_spy/schema/channels.py +8943 -0
  49. genome_spy/schema/composition.py +1064 -0
  50. genome_spy/schema/core.py +51821 -0
  51. genome_spy/schema/ergonomics.py +2056 -0
  52. genome_spy/schema/expressions.py +476 -0
  53. genome_spy/schema/genome-spy-schema.json +33657 -0
  54. genome_spy/schema/lazy.py +326 -0
  55. genome_spy/schema/mixins.py +11684 -0
  56. genome_spy/schemapi.py +264 -0
  57. genome_spy/static/widget.js +345 -0
  58. genome_spy_python-0.1.0.dist-info/METADATA +185 -0
  59. genome_spy_python-0.1.0.dist-info/RECORD +64 -0
  60. genome_spy_python-0.1.0.dist-info/WHEEL +4 -0
  61. genome_spy_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  62. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/ALTAIR-BSD-3-Clause.txt +27 -0
  63. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/GALLERY-DATA-MIT.txt +22 -0
  64. genome_spy_python-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
@@ -0,0 +1,289 @@
1
+ """Wrangle packaged MAFs for position-based gallery examples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from typing import TYPE_CHECKING, TypedDict, cast
8
+
9
+ from genome_spy.datasets import load_dataset
10
+
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
13
+
14
+
15
+ _COMPLEMENTARY_SUBSTITUTIONS = {
16
+ "A>G": "T>C",
17
+ "T>C": "T>C",
18
+ "C>T": "C>T",
19
+ "G>A": "C>T",
20
+ "A>T": "T>A",
21
+ "T>A": "T>A",
22
+ "A>C": "T>G",
23
+ "T>G": "T>G",
24
+ "C>A": "C>A",
25
+ "G>T": "C>A",
26
+ "C>G": "C>G",
27
+ "G>C": "C>G",
28
+ }
29
+
30
+ _DNMT3A_DOMAINS = (
31
+ {"name": "Dnmt3b_related", "start": 290, "end": 377, "color": "#e78973"},
32
+ {"name": "ADDz_Dnmt3a", "start": 476, "end": 612, "color": "#8094ee"},
33
+ {"name": "AdoMet_MTases", "start": 634, "end": 905, "color": "#f7c57a"},
34
+ )
35
+
36
+
37
+ class Dnmt3aLollipopData(TypedDict):
38
+ """Chart tables and metadata returned for the DNMT3A lollipop."""
39
+
40
+ gene: str
41
+ transcript: str
42
+ protein_length: int
43
+ total_samples: int
44
+ mutated_samples: int
45
+ mutation_rate: float
46
+ features: pd.DataFrame
47
+ domains: pd.DataFrame
48
+ backbone: pd.DataFrame
49
+
50
+
51
+ class BrcaRainfallData(TypedDict):
52
+ """Chart tables and metadata returned for the BRCA rainfall plot."""
53
+
54
+ sample: str
55
+ reference_build: str
56
+ points: pd.DataFrame
57
+ change_points: pd.DataFrame
58
+ y_max: float
59
+
60
+
61
+ class Pik3caLollipopData(TypedDict):
62
+ """Prepared inputs for the TCGA-BRCA PIK3CA lollipop plot."""
63
+
64
+ proteinLength: int
65
+ mutations: list[dict[str, object]]
66
+ domains: list[dict[str, object]]
67
+
68
+
69
+ def pik3ca_lollipop_data() -> Pik3caLollipopData:
70
+ """Load prepared TCGA-BRCA PIK3CA mutations and protein domains.
71
+
72
+ Description:
73
+ Returns the chart-ready named datasets extracted from the official
74
+ GenomeSpy PIK3CA lollipop specification. Statistical aggregation and
75
+ protein-domain curation are intentionally kept out of the gallery
76
+ example so it can focus on declarative visualization.
77
+
78
+ Returns:
79
+ Protein length, recurrent mutation rows, and domain rows.
80
+
81
+ Raises:
82
+ DatasetNotFoundError: If the packaged resource is unavailable.
83
+
84
+ Example:
85
+ >>> pik3ca_lollipop_data()["mutations"][0]["mutation"]
86
+ 'E81K'
87
+ """
88
+ return cast(
89
+ Pik3caLollipopData,
90
+ load_dataset("pik3ca_tcga_brca_lollipop", as_format="json"),
91
+ )
92
+
93
+
94
+ def dnmt3a_lollipop_data() -> Dnmt3aLollipopData:
95
+ """Prepare DNMT3A mutation positions from the packaged TCGA LAML MAF.
96
+
97
+ Description:
98
+ Loads the MAF's ``Hugo_Symbol``, ``Protein_Change``,
99
+ ``Variant_Classification``, and ``Tumor_Sample_Barcode`` columns.
100
+ DNMT3A protein changes are parsed to amino-acid positions and grouped
101
+ into one feature per position. Domains and the protein backbone are
102
+ small display annotations maintained here because they are not in the
103
+ upstream MAF.
104
+
105
+ Returns:
106
+ A mapping containing mutation features, domain/backbone tables, and
107
+ sample-rate metadata.
108
+
109
+ Raises:
110
+ ImportError: If pandas is not installed.
111
+
112
+ Example:
113
+ >>> dnmt3a_lollipop_data()["features"].head()
114
+ """
115
+ import pandas as pd
116
+
117
+ maf = load_dataset("tcga_laml_maf", as_format="dataframe")
118
+ gene_variants = maf.loc[maf["Hugo_Symbol"].eq("DNMT3A")].copy()
119
+ gene_variants["label"] = gene_variants["Protein_Change"].str.removeprefix("p.")
120
+ gene_variants["position"] = gene_variants["label"].map(_protein_position)
121
+ parsed = gene_variants.dropna(subset=["position"]).copy()
122
+ parsed["position"] = parsed["position"].astype(int)
123
+
124
+ # Count calls and choose the most frequent class at each position. Sorting
125
+ # class names descending preserves the previous deterministic tie-break.
126
+ features = parsed.groupby("position", as_index=False).agg(
127
+ count=("label", "size"),
128
+ labels=("label", lambda values: sorted(values.dropna().unique())),
129
+ )
130
+ dominant_class = (
131
+ parsed.groupby(["position", "Variant_Classification"], as_index=False)
132
+ .size()
133
+ .sort_values(
134
+ ["position", "size", "Variant_Classification"],
135
+ ascending=[True, False, False],
136
+ kind="stable",
137
+ )
138
+ .drop_duplicates("position")
139
+ .rename(columns={"Variant_Classification": "class"})[["position", "class"]]
140
+ )
141
+ features = features.merge(dominant_class, on="position", validate="one_to_one")
142
+ features["label"] = features["labels"].map(" / ".join)
143
+ features["is_hotspot"] = features["position"].eq(882)
144
+
145
+ total_samples = int(maf["Tumor_Sample_Barcode"].nunique())
146
+ mutated_samples = int(parsed["Tumor_Sample_Barcode"].nunique())
147
+ return {
148
+ "gene": "DNMT3A",
149
+ "transcript": "NM_022552",
150
+ "protein_length": 912,
151
+ "total_samples": total_samples,
152
+ "mutated_samples": mutated_samples,
153
+ "mutation_rate": round(mutated_samples / total_samples * 100, 2),
154
+ "features": features,
155
+ "domains": pd.DataFrame(_DNMT3A_DOMAINS),
156
+ "backbone": pd.DataFrame([{"start": 1, "end": 912, "color": "#a8b5b6"}]),
157
+ }
158
+
159
+
160
+ def brca_rainfall_data() -> BrcaRainfallData:
161
+ """Prepare a BRCA rainfall plot from the packaged maftools MAF.
162
+
163
+ Description:
164
+ Loads the sample, variant-type, chromosome, position, allele, and gene
165
+ columns. The sample with the most calls is selected, SNPs are ordered
166
+ within chromosomes, substitutions are converted to pyrimidine
167
+ orientation, and inter-event distances are calculated. The kataegis
168
+ detector follows maftools' six-mutation moving-window rule.
169
+
170
+ Returns:
171
+ A mapping containing rainfall points, detected clusters, and display
172
+ metadata.
173
+
174
+ Raises:
175
+ ImportError: If pandas is not installed.
176
+
177
+ Example:
178
+ >>> brca_rainfall_data()["points"].head()
179
+ """
180
+ import pandas as pd
181
+
182
+ maf = load_dataset("brca_maf", as_format="dataframe")
183
+ sample_counts = (
184
+ maf.groupby("Tumor_Sample_Barcode").size().sort_values(ascending=False)
185
+ )
186
+ sample = str(sample_counts.index[0])
187
+ snps = maf.loc[
188
+ maf["Tumor_Sample_Barcode"].eq(sample) & maf["Variant_Type"].eq("SNP")
189
+ ].copy()
190
+ chromosome_order = sorted(snps["Chromosome"].unique(), key=_chromosome_key)
191
+ snps["Chromosome"] = pd.Categorical(
192
+ snps["Chromosome"], categories=chromosome_order, ordered=True
193
+ )
194
+ snps = snps.sort_values(["Chromosome", "Start_Position"], kind="stable")
195
+ snps["distance"] = snps.groupby("Chromosome", observed=True)[
196
+ "Start_Position"
197
+ ].diff()
198
+ substitution = snps["Reference_Allele"] + ">" + snps["Tumor_Seq_Allele2"]
199
+ snps["con_class"] = substitution.map(_COMPLEMENTARY_SUBSTITUTIONS)
200
+ snps["log10_distance"] = snps["distance"].map(
201
+ lambda distance: (
202
+ round(math.log10(distance + 1), 4) if pd.notna(distance) else math.nan
203
+ )
204
+ )
205
+
206
+ points = (
207
+ snps.dropna(subset=["distance", "con_class"])
208
+ .rename(
209
+ columns={
210
+ "Chromosome": "chrom",
211
+ "Start_Position": "pos",
212
+ "Hugo_Symbol": "gene",
213
+ }
214
+ )[["chrom", "pos", "gene", "distance", "log10_distance", "con_class"]]
215
+ .copy()
216
+ )
217
+ points["chrom"] = "chr" + points["chrom"].astype(str).str.removeprefix("chr")
218
+ points["distance"] = points["distance"].astype(int)
219
+
220
+ return {
221
+ "sample": sample,
222
+ # The trimmed maftools example omits NCBI_Build, but its coordinates
223
+ # and Hugo symbols match hg19 (for example, chr8:124090377 TBC1D31).
224
+ "reference_build": "hg19",
225
+ "points": points.reset_index(drop=True),
226
+ "change_points": _detect_kataegis(points),
227
+ "y_max": round(float(points["log10_distance"].max()) + 0.2, 2),
228
+ }
229
+
230
+
231
+ def _protein_position(label: object) -> int | None:
232
+ if not isinstance(label, str):
233
+ return None
234
+ match = re.search(r"\d+", label)
235
+ return int(match.group()) if match else None
236
+
237
+
238
+ def _chromosome_key(chromosome: object) -> tuple[int, str]:
239
+ value = str(chromosome).removeprefix("chr")
240
+ if value.isdigit():
241
+ return int(value), ""
242
+ return {"X": 23, "Y": 24, "MT": 25}.get(value, 26), value
243
+
244
+
245
+ def _detect_kataegis(points: pd.DataFrame) -> pd.DataFrame:
246
+ import pandas as pd
247
+
248
+ records: list[dict[str, object]] = []
249
+ for chromosome, chromosome_points in points.groupby(
250
+ "chrom", observed=True, sort=False
251
+ ):
252
+ chromosome_points = chromosome_points.reset_index(drop=True)
253
+ start_index = 0
254
+ end_index = 6
255
+ while end_index <= len(chromosome_points):
256
+ # maftools starts a cluster when six consecutive mutations average
257
+ # at most 1,000 bp apart, then extends that cluster greedily.
258
+ queue = chromosome_points.iloc[start_index:end_index]
259
+ if len(queue) < 6:
260
+ break
261
+ if queue["distance"].mean() > 1000:
262
+ start_index += 1
263
+ end_index += 1
264
+ continue
265
+
266
+ while end_index <= len(chromosome_points):
267
+ queue = chromosome_points.iloc[start_index:end_index]
268
+ if queue["distance"].mean() > 1000:
269
+ break
270
+ end_index += 1
271
+
272
+ cluster = chromosome_points.iloc[start_index : end_index - 1]
273
+ records.append(
274
+ {
275
+ "chrom": str(chromosome),
276
+ "start": int(cluster["pos"].min()),
277
+ "end": int(cluster["pos"].max()),
278
+ "count": len(cluster),
279
+ "mean_distance": round(float(cluster["distance"].mean()), 2),
280
+ "arrow_y": round(
281
+ max(float(cluster["log10_distance"].min()) - 0.25, 0.1),
282
+ 4,
283
+ ),
284
+ }
285
+ )
286
+ start_index = end_index
287
+ end_index += 6
288
+
289
+ return pd.DataFrame(records)