pathview-plus 2.0.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.
- pathview/__init__.py +124 -0
- pathview/color_mapping.py +153 -0
- pathview/constants.py +27 -0
- pathview/databases.py +309 -0
- pathview/examples.py +342 -0
- pathview/highlighting.py +375 -0
- pathview/id_mapping.py +170 -0
- pathview/kegg_api.py +143 -0
- pathview/kgml_parser.py +189 -0
- pathview/mol_data.py +168 -0
- pathview/node_mapping.py +99 -0
- pathview/pathview.py +316 -0
- pathview/rendering.py +409 -0
- pathview/sbgn_parser.py +353 -0
- pathview/splines.py +304 -0
- pathview/svg_rendering.py +305 -0
- pathview/test_all_features.py +343 -0
- pathview/utils.py +80 -0
- pathview_plus-2.0.0.data/scripts/pathview-cli.py +252 -0
- pathview_plus-2.0.0.dist-info/METADATA +661 -0
- pathview_plus-2.0.0.dist-info/RECORD +23 -0
- pathview_plus-2.0.0.dist-info/WHEEL +5 -0
- pathview_plus-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
svg_rendering.py
|
|
3
|
+
Generate SVG (Scalable Vector Graphics) output for KEGG pathways.
|
|
4
|
+
|
|
5
|
+
This complements the existing PNG (pixel-based) and PDF (graph-based) modes
|
|
6
|
+
with native vector graphics that are web-friendly, scalable, and editable.
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
keggview_svg : Render pathway as SVG with data overlay
|
|
11
|
+
render_node_svg : Generate SVG code for a single node
|
|
12
|
+
render_edge_svg : Generate SVG code for a single edge
|
|
13
|
+
|
|
14
|
+
SVG advantages over PNG:
|
|
15
|
+
- Scalable without quality loss
|
|
16
|
+
- Smaller file size for simple diagrams
|
|
17
|
+
- Editable in vector graphics software
|
|
18
|
+
- Web-native format (no conversion needed)
|
|
19
|
+
- Supports CSS styling and JavaScript interaction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import warnings
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional
|
|
27
|
+
from xml.etree import ElementTree as ET
|
|
28
|
+
|
|
29
|
+
import polars as pl
|
|
30
|
+
|
|
31
|
+
from .color_mapping import draw_color_key, make_colormap
|
|
32
|
+
from .utils import wordwrap
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# SVG header and footer
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _svg_header(width: int, height: int, title: str = "") -> str:
|
|
40
|
+
"""Generate SVG document header."""
|
|
41
|
+
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
42
|
+
<svg width="{width}" height="{height}"
|
|
43
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
44
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
45
|
+
viewBox="0 0 {width} {height}">
|
|
46
|
+
<title>{title}</title>
|
|
47
|
+
<defs>
|
|
48
|
+
<style type="text/css">
|
|
49
|
+
.node {{ stroke: #333; stroke-width: 1; }}
|
|
50
|
+
.edge {{ stroke: #666; stroke-width: 1; fill: none; }}
|
|
51
|
+
.label {{ font-family: Arial, sans-serif; font-size: 11px; fill: #000; text-anchor: middle; }}
|
|
52
|
+
</style>
|
|
53
|
+
</defs>
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
def _svg_footer() -> str:
|
|
57
|
+
"""Generate SVG document footer."""
|
|
58
|
+
return "</svg>"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Node rendering (rectangles and ellipses)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def render_node_svg(
|
|
66
|
+
node_id: str,
|
|
67
|
+
x: float,
|
|
68
|
+
y: float,
|
|
69
|
+
width: float,
|
|
70
|
+
height: float,
|
|
71
|
+
shape: str,
|
|
72
|
+
label: str,
|
|
73
|
+
fill_colors: list[str],
|
|
74
|
+
opacity: float = 1.0,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Render a single node as SVG.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
node_id: Unique identifier for the node
|
|
82
|
+
x, y: Center coordinates
|
|
83
|
+
width, height: Node dimensions
|
|
84
|
+
shape: "rectangle", "ellipse", "roundedrectangle"
|
|
85
|
+
label: Text to display on node
|
|
86
|
+
fill_colors: List of hex colors (one per data column/state)
|
|
87
|
+
opacity: Fill opacity (0-1)
|
|
88
|
+
|
|
89
|
+
Returns SVG code string for this node.
|
|
90
|
+
"""
|
|
91
|
+
svg_parts = []
|
|
92
|
+
n_states = len(fill_colors)
|
|
93
|
+
|
|
94
|
+
# Calculate bounding box
|
|
95
|
+
x1 = x - width / 2
|
|
96
|
+
y1 = y - height / 2
|
|
97
|
+
|
|
98
|
+
if shape == "ellipse":
|
|
99
|
+
# Slice ellipse vertically for multi-state
|
|
100
|
+
rx, ry = width / 2, height / 2
|
|
101
|
+
for i, color in enumerate(fill_colors):
|
|
102
|
+
# Create clipped ellipse slices
|
|
103
|
+
clip_x = x1 + (i * width / n_states)
|
|
104
|
+
clip_width = width / n_states
|
|
105
|
+
svg_parts.append(
|
|
106
|
+
f'<g clip-path="url(#clip_{node_id}_{i})">'
|
|
107
|
+
f' <ellipse cx="{x}" cy="{y}" rx="{rx}" ry="{ry}" '
|
|
108
|
+
f' fill="{color}" opacity="{opacity}" class="node"/>'
|
|
109
|
+
f'</g>'
|
|
110
|
+
)
|
|
111
|
+
# Define clip path
|
|
112
|
+
svg_parts.insert(
|
|
113
|
+
0,
|
|
114
|
+
f'<clipPath id="clip_{node_id}_{i}">'
|
|
115
|
+
f' <rect x="{clip_x}" y="{y1}" width="{clip_width}" height="{height}"/>'
|
|
116
|
+
f'</clipPath>'
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
# Rectangle or rounded rectangle
|
|
120
|
+
rx_round = 5 if shape == "roundedrectangle" else 0
|
|
121
|
+
for i, color in enumerate(fill_colors):
|
|
122
|
+
slice_x = x1 + (i * width / n_states)
|
|
123
|
+
slice_width = width / n_states
|
|
124
|
+
svg_parts.append(
|
|
125
|
+
f'<rect x="{slice_x}" y="{y1}" width="{slice_width}" height="{height}" '
|
|
126
|
+
f'rx="{rx_round}" fill="{color}" opacity="{opacity}" class="node"/>'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Add label
|
|
130
|
+
wrapped = wordwrap(label, width=max(8, int(width / 8)))
|
|
131
|
+
lines = wrapped.split("\n")
|
|
132
|
+
y_text = y - (len(lines) - 1) * 5
|
|
133
|
+
for line in lines:
|
|
134
|
+
svg_parts.append(
|
|
135
|
+
f'<text x="{x}" y="{y_text}" class="label">{_escape_xml(line)}</text>'
|
|
136
|
+
)
|
|
137
|
+
y_text += 12
|
|
138
|
+
|
|
139
|
+
return "\n".join(svg_parts)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Edge rendering
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def render_edge_svg(
|
|
147
|
+
source_x: float,
|
|
148
|
+
source_y: float,
|
|
149
|
+
target_x: float,
|
|
150
|
+
target_y: float,
|
|
151
|
+
edge_type: str = "arrow",
|
|
152
|
+
color: str = "#666",
|
|
153
|
+
width: float = 1.5,
|
|
154
|
+
) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Render a single edge as SVG.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
source_x, source_y: Start coordinates
|
|
161
|
+
target_x, target_y: End coordinates
|
|
162
|
+
edge_type: "arrow", "inhibition", "dotted"
|
|
163
|
+
color: Stroke color
|
|
164
|
+
width: Line width
|
|
165
|
+
|
|
166
|
+
Returns SVG code string for this edge.
|
|
167
|
+
"""
|
|
168
|
+
marker_id = f"marker_{edge_type}"
|
|
169
|
+
path_style = f'stroke="{color}" stroke-width="{width}" fill="none" class="edge"'
|
|
170
|
+
|
|
171
|
+
if edge_type == "dotted":
|
|
172
|
+
path_style += ' stroke-dasharray="3,3"'
|
|
173
|
+
|
|
174
|
+
# Define arrow markers
|
|
175
|
+
markers = f'''
|
|
176
|
+
<defs>
|
|
177
|
+
<marker id="{marker_id}" viewBox="0 0 10 10" refX="8" refY="5"
|
|
178
|
+
markerWidth="6" markerHeight="6" orient="auto">
|
|
179
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="{color}"/>
|
|
180
|
+
</marker>
|
|
181
|
+
</defs>
|
|
182
|
+
'''
|
|
183
|
+
|
|
184
|
+
# Draw line
|
|
185
|
+
line = f'<line x1="{source_x}" y1="{source_y}" x2="{target_x}" y2="{target_y}" '\
|
|
186
|
+
f'{path_style} marker-end="url(#{marker_id})"/>'
|
|
187
|
+
|
|
188
|
+
return markers + line
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Main SVG rendering function
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def keggview_svg(
|
|
196
|
+
plot_data_gene: Optional[pl.DataFrame],
|
|
197
|
+
cols_gene: Optional[pl.DataFrame],
|
|
198
|
+
plot_data_cpd: Optional[pl.DataFrame],
|
|
199
|
+
cols_cpd: Optional[pl.DataFrame],
|
|
200
|
+
node_data: pl.DataFrame,
|
|
201
|
+
pathway_name: str,
|
|
202
|
+
kegg_dir: Path = Path("."),
|
|
203
|
+
out_suffix: str = "pathview",
|
|
204
|
+
new_signature: bool = True,
|
|
205
|
+
**kwargs,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Render pathway as SVG with data overlay.
|
|
209
|
+
|
|
210
|
+
This is an alternative to keggview_native (PNG) and keggview_graph (PDF).
|
|
211
|
+
Generates a standalone SVG file with nodes colored by expression data.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
plot_data_gene: Gene node positions + data
|
|
216
|
+
cols_gene: Gene node color assignments
|
|
217
|
+
plot_data_cpd: Compound node positions + data
|
|
218
|
+
cols_cpd: Compound node color assignments
|
|
219
|
+
node_data: All pathway nodes
|
|
220
|
+
pathway_name: Pathway ID
|
|
221
|
+
kegg_dir: Output directory
|
|
222
|
+
out_suffix: Output filename suffix
|
|
223
|
+
new_signature: Add "Rendered by pathview.py" watermark
|
|
224
|
+
"""
|
|
225
|
+
# Determine canvas size from node positions
|
|
226
|
+
max_x = node_data["x"].max() or 1000
|
|
227
|
+
max_y = node_data["y"].max() or 800
|
|
228
|
+
canvas_width = int(max_x + 100)
|
|
229
|
+
canvas_height = int(max_y + 100)
|
|
230
|
+
|
|
231
|
+
svg_code = [_svg_header(canvas_width, canvas_height, pathway_name)]
|
|
232
|
+
|
|
233
|
+
# Render gene nodes
|
|
234
|
+
if plot_data_gene is not None and cols_gene is not None:
|
|
235
|
+
svg_code.append("<!-- Gene nodes -->")
|
|
236
|
+
color_cols = [c for c in cols_gene.columns if c.endswith("_col")]
|
|
237
|
+
for row in plot_data_gene.iter_rows(named=True):
|
|
238
|
+
node_id = row["entry_id"]
|
|
239
|
+
colors = [cols_gene.filter(pl.col("id") == node_id)[c].item()
|
|
240
|
+
for c in color_cols if not cols_gene.filter(pl.col("id") == node_id).is_empty()]
|
|
241
|
+
if not colors or all(c == "transparent" for c in colors):
|
|
242
|
+
colors = ["#CCCCCC"]
|
|
243
|
+
svg_code.append(
|
|
244
|
+
render_node_svg(
|
|
245
|
+
node_id=node_id,
|
|
246
|
+
x=row["x"],
|
|
247
|
+
y=row["y"],
|
|
248
|
+
width=row["width"],
|
|
249
|
+
height=row["height"],
|
|
250
|
+
shape=row.get("shape", "rectangle"),
|
|
251
|
+
label=row.get("label", ""),
|
|
252
|
+
fill_colors=colors,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Render compound nodes
|
|
257
|
+
if plot_data_cpd is not None and cols_cpd is not None:
|
|
258
|
+
svg_code.append("<!-- Compound nodes -->")
|
|
259
|
+
color_cols = [c for c in cols_cpd.columns if c.endswith("_col")]
|
|
260
|
+
for row in plot_data_cpd.iter_rows(named=True):
|
|
261
|
+
node_id = row["entry_id"]
|
|
262
|
+
colors = [cols_cpd.filter(pl.col("id") == node_id)[c].item()
|
|
263
|
+
for c in color_cols if not cols_cpd.filter(pl.col("id") == node_id).is_empty()]
|
|
264
|
+
if not colors or all(c == "transparent" for c in colors):
|
|
265
|
+
colors = ["#DDDDFF"]
|
|
266
|
+
svg_code.append(
|
|
267
|
+
render_node_svg(
|
|
268
|
+
node_id=node_id,
|
|
269
|
+
x=row["x"],
|
|
270
|
+
y=row["y"],
|
|
271
|
+
width=row["width"],
|
|
272
|
+
height=row["height"],
|
|
273
|
+
shape=row.get("shape", "ellipse"),
|
|
274
|
+
label=row.get("label", ""),
|
|
275
|
+
fill_colors=colors,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Add signature
|
|
280
|
+
if new_signature:
|
|
281
|
+
svg_code.append(
|
|
282
|
+
f'<text x="10" y="{canvas_height - 10}" '
|
|
283
|
+
f'style="font-size: 9px; fill: #666;">Rendered by pathview.py</text>'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
svg_code.append(_svg_footer())
|
|
287
|
+
|
|
288
|
+
# Write to file
|
|
289
|
+
out_path = Path(kegg_dir) / f"{pathway_name}.{out_suffix}.svg"
|
|
290
|
+
out_path.write_text("\n".join(svg_code), encoding="utf-8")
|
|
291
|
+
print(f"Info: Written → {out_path}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Utilities
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
def _escape_xml(text: str) -> str:
|
|
299
|
+
"""Escape XML special characters."""
|
|
300
|
+
return (text
|
|
301
|
+
.replace("&", "&")
|
|
302
|
+
.replace("<", "<")
|
|
303
|
+
.replace(">", ">")
|
|
304
|
+
.replace('"', """)
|
|
305
|
+
.replace("'", "'"))
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
test_all_features.py
|
|
4
|
+
====================
|
|
5
|
+
Comprehensive test suite demonstrating all pathview.py features.
|
|
6
|
+
|
|
7
|
+
Tests:
|
|
8
|
+
1. KEGG pathway (PNG)
|
|
9
|
+
2. KEGG pathway (SVG)
|
|
10
|
+
3. Reactome pathway
|
|
11
|
+
4. Multi-condition data
|
|
12
|
+
5. Custom colors
|
|
13
|
+
6. Gene symbols
|
|
14
|
+
7. Compound overlay
|
|
15
|
+
8. Spline curves
|
|
16
|
+
9. Graph layout
|
|
17
|
+
10. Highlighting (API demo)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import polars as pl
|
|
24
|
+
|
|
25
|
+
# Add pathview to path
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
27
|
+
|
|
28
|
+
from pathview import (
|
|
29
|
+
catmull_rom_spline,
|
|
30
|
+
cubic_bezier,
|
|
31
|
+
pathview,
|
|
32
|
+
sim_mol_data,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_1_kegg_png():
|
|
37
|
+
"""Test 1: Basic KEGG pathway with PNG output"""
|
|
38
|
+
print("\n" + "="*70)
|
|
39
|
+
print("TEST 1: KEGG Pathway (PNG)")
|
|
40
|
+
print("="*70)
|
|
41
|
+
|
|
42
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
|
|
43
|
+
|
|
44
|
+
result = pathview(
|
|
45
|
+
pathway_id="04110",
|
|
46
|
+
gene_data=gene_df,
|
|
47
|
+
species="hsa",
|
|
48
|
+
output_format="png",
|
|
49
|
+
out_suffix="test1_png"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print("✓ Generated: hsa04110.test1_png.png")
|
|
53
|
+
return result is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_2_kegg_svg():
|
|
57
|
+
"""Test 2: KEGG pathway with SVG vector output"""
|
|
58
|
+
print("\n" + "="*70)
|
|
59
|
+
print("TEST 2: KEGG Pathway (SVG Vector)")
|
|
60
|
+
print("="*70)
|
|
61
|
+
|
|
62
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
|
|
63
|
+
|
|
64
|
+
result = pathview(
|
|
65
|
+
pathway_id="04110",
|
|
66
|
+
gene_data=gene_df,
|
|
67
|
+
species="hsa",
|
|
68
|
+
output_format="svg",
|
|
69
|
+
out_suffix="test2_svg"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print("✓ Generated: hsa04110.test2_svg.svg")
|
|
73
|
+
print(" → Scalable vector graphics")
|
|
74
|
+
return result is not None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_3_reactome():
|
|
78
|
+
"""Test 3: Reactome SBGN pathway"""
|
|
79
|
+
print("\n" + "="*70)
|
|
80
|
+
print("TEST 3: Reactome SBGN Pathway")
|
|
81
|
+
print("="*70)
|
|
82
|
+
#TODO: Create test for this when server is working
|
|
83
|
+
print("⚠ Requires internet connection to download Reactome pathway")
|
|
84
|
+
print("⚠ Skipping for offline testing - see README for full example")
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_4_multi_condition():
|
|
89
|
+
"""Test 4: Multi-condition visualization"""
|
|
90
|
+
print("\n" + "="*70)
|
|
91
|
+
print("TEST 4: Multi-Condition Visualization")
|
|
92
|
+
print("="*70)
|
|
93
|
+
|
|
94
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=120, n_exp=3)
|
|
95
|
+
gene_df = gene_df.rename({
|
|
96
|
+
"exp1": "Control",
|
|
97
|
+
"exp2": "Treatment_A",
|
|
98
|
+
"exp3": "Treatment_B"
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
result = pathview(
|
|
102
|
+
pathway_id="04010",
|
|
103
|
+
gene_data=gene_df,
|
|
104
|
+
species="hsa",
|
|
105
|
+
out_suffix="test4_multi",
|
|
106
|
+
limit={"gene": 2.0, "cpd": 1.0}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
print("✓ Generated: hsa04010.test4_multi.png")
|
|
110
|
+
print(" → Each node shows 3 colored slices")
|
|
111
|
+
return result is not None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_5_custom_colors():
|
|
115
|
+
"""Test 5: Custom color scheme (ColorBrewer)"""
|
|
116
|
+
print("\n" + "="*70)
|
|
117
|
+
print("TEST 5: Custom Color Scheme")
|
|
118
|
+
print("="*70)
|
|
119
|
+
|
|
120
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
|
|
121
|
+
|
|
122
|
+
result = pathview(
|
|
123
|
+
pathway_id="04151",
|
|
124
|
+
gene_data=gene_df,
|
|
125
|
+
species="hsa",
|
|
126
|
+
out_suffix="test5_colors",
|
|
127
|
+
low={"gene": "#2166AC", "cpd": "#4575B4"},
|
|
128
|
+
mid={"gene": "#F7F7F7", "cpd": "#F7F7F7"},
|
|
129
|
+
high={"gene": "#D6604D", "cpd": "#B2182B"},
|
|
130
|
+
limit={"gene": 2.5, "cpd": 1.5}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
print("✓ Generated: hsa04151.test5_colors.png")
|
|
134
|
+
print(" → ColorBrewer RdBu diverging palette")
|
|
135
|
+
return result is not None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_6_gene_symbols():
|
|
139
|
+
"""Test 6: Gene symbol IDs with auto-conversion"""
|
|
140
|
+
print("\n" + "="*70)
|
|
141
|
+
print("TEST 6: Gene Symbol IDs")
|
|
142
|
+
print("="*70)
|
|
143
|
+
|
|
144
|
+
gene_df = pl.DataFrame({
|
|
145
|
+
"symbol": ["TP53", "EGFR", "KRAS", "PIK3CA", "AKT1"],
|
|
146
|
+
"lfc": [-1.8, 2.4, 1.1, 1.5, 0.9]
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
result = pathview(
|
|
150
|
+
pathway_id="04151",
|
|
151
|
+
gene_data=gene_df,
|
|
152
|
+
species="hsa",
|
|
153
|
+
gene_idtype="SYMBOL",
|
|
154
|
+
out_suffix="test6_symbols"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
print("✓ Generated: hsa04151.test6_symbols.png")
|
|
158
|
+
print(" → Symbols auto-converted via MyGene.info")
|
|
159
|
+
return result is not None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_7_compound_overlay():
|
|
163
|
+
"""Test 7: Gene + compound combined overlay"""
|
|
164
|
+
print("\n" + "="*70)
|
|
165
|
+
print("TEST 7: Gene + Compound Overlay")
|
|
166
|
+
print("="*70)
|
|
167
|
+
|
|
168
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=80, n_exp=1)
|
|
169
|
+
cpd_df = sim_mol_data(mol_type="cpd", n_mol=30, n_exp=1)
|
|
170
|
+
|
|
171
|
+
result = pathview(
|
|
172
|
+
pathway_id="00010",
|
|
173
|
+
gene_data=gene_df,
|
|
174
|
+
cpd_data=cpd_df,
|
|
175
|
+
species="hsa",
|
|
176
|
+
out_suffix="test7_gene_cpd",
|
|
177
|
+
limit={"gene": 2.0, "cpd": 1.5}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
print("✓ Generated: hsa00010.test7_gene_cpd.png")
|
|
181
|
+
print(" → Glycolysis with proteomics + metabolomics")
|
|
182
|
+
return result is not None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_8_splines():
|
|
186
|
+
"""Test 8: Spline curve generation"""
|
|
187
|
+
print("\n" + "="*70)
|
|
188
|
+
print("TEST 8: Spline Curves (Bezier)")
|
|
189
|
+
print("="*70)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
import matplotlib.pyplot as plt
|
|
193
|
+
|
|
194
|
+
# Generate cubic Bezier
|
|
195
|
+
curve = cubic_bezier(
|
|
196
|
+
p0=(0, 0),
|
|
197
|
+
p1=(1, 2),
|
|
198
|
+
p2=(3, 2),
|
|
199
|
+
p3=(4, 0),
|
|
200
|
+
n_points=100
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Generate Catmull-Rom spline
|
|
204
|
+
smooth = catmull_rom_spline(
|
|
205
|
+
[(0, 0), (1, 2), (3, 1), (4, 3)],
|
|
206
|
+
n_points=50
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
|
210
|
+
|
|
211
|
+
ax1.plot(curve[:, 0], curve[:, 1], 'b-', linewidth=2, label='Cubic Bezier')
|
|
212
|
+
ax1.plot([0, 1, 3, 4], [0, 2, 2, 0], 'ro--', alpha=0.5, label='Control points')
|
|
213
|
+
ax1.set_title("Cubic Bezier Curve")
|
|
214
|
+
ax1.legend()
|
|
215
|
+
ax1.grid(True, alpha=0.3)
|
|
216
|
+
|
|
217
|
+
ax2.plot(smooth[:, 0], smooth[:, 1], 'g-', linewidth=2, label='Catmull-Rom')
|
|
218
|
+
ax2.plot([0, 1, 3, 4], [0, 2, 1, 3], 'ro-', alpha=0.5, label='Control points')
|
|
219
|
+
ax2.set_title("Catmull-Rom Spline")
|
|
220
|
+
ax2.legend()
|
|
221
|
+
ax2.grid(True, alpha=0.3)
|
|
222
|
+
|
|
223
|
+
plt.tight_layout()
|
|
224
|
+
plt.savefig("test8_splines.png", dpi=150)
|
|
225
|
+
plt.close()
|
|
226
|
+
|
|
227
|
+
print("✓ Generated: test8_splines.png")
|
|
228
|
+
print(" → Smooth curve demonstrations")
|
|
229
|
+
return True
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(f"⚠ Skipping matplotlib test: {e}")
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_9_graph_layout():
|
|
236
|
+
"""Test 9: Graph layout mode (PDF)"""
|
|
237
|
+
print("\n" + "="*70)
|
|
238
|
+
print("TEST 9: Graph Layout Mode (PDF)")
|
|
239
|
+
print("="*70)
|
|
240
|
+
|
|
241
|
+
gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
|
|
242
|
+
|
|
243
|
+
result = pathview(
|
|
244
|
+
pathway_id="04010",
|
|
245
|
+
gene_data=gene_df,
|
|
246
|
+
species="hsa",
|
|
247
|
+
kegg_native=False,
|
|
248
|
+
output_format="pdf",
|
|
249
|
+
out_suffix="test9_graph"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
print("✓ Generated: hsa04010.test9_graph.pdf")
|
|
253
|
+
print(" → NetworkX layout with Seaborn styling")
|
|
254
|
+
return result is not None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_10_highlighting():
|
|
258
|
+
"""Test 10: Highlighting API demonstration"""
|
|
259
|
+
print("\n" + "="*70)
|
|
260
|
+
print("TEST 10: Highlighting API (Preview)")
|
|
261
|
+
print("="*70)
|
|
262
|
+
|
|
263
|
+
gene_df = pl.DataFrame({
|
|
264
|
+
"entrez": ["1956", "2099", "5594", "207"],
|
|
265
|
+
"lfc": [ 2.3, -1.1, 1.8, -0.5]
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
result = pathview(
|
|
269
|
+
pathway_id="04010",
|
|
270
|
+
gene_data=gene_df,
|
|
271
|
+
species="hsa",
|
|
272
|
+
out_suffix="test10_base"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
print("✓ Generated: hsa04010.test10_base.png")
|
|
276
|
+
print("⚠ Full highlighting implementation:")
|
|
277
|
+
print(" from pathview import highlight_nodes, highlight_edges")
|
|
278
|
+
print(" highlighted = result + highlight_nodes([...]) + highlight_edges([...])")
|
|
279
|
+
return result is not None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main():
|
|
283
|
+
"""Run all tests"""
|
|
284
|
+
print("\n" + "="*70)
|
|
285
|
+
print("PATHVIEW.PY COMPREHENSIVE FEATURE TESTS")
|
|
286
|
+
print("="*70)
|
|
287
|
+
print("\nTesting all features:")
|
|
288
|
+
print(" • KEGG pathways (PNG, SVG, PDF)")
|
|
289
|
+
print(" • Multi-condition visualization")
|
|
290
|
+
print(" • Custom color schemes")
|
|
291
|
+
print(" • Gene symbol IDs")
|
|
292
|
+
print(" • Compound overlays")
|
|
293
|
+
print(" • Spline curves")
|
|
294
|
+
print(" • Graph layouts")
|
|
295
|
+
print(" • Highlighting API")
|
|
296
|
+
|
|
297
|
+
tests = [
|
|
298
|
+
test_1_kegg_png,
|
|
299
|
+
test_2_kegg_svg,
|
|
300
|
+
test_3_reactome,
|
|
301
|
+
test_4_multi_condition,
|
|
302
|
+
test_5_custom_colors,
|
|
303
|
+
test_6_gene_symbols,
|
|
304
|
+
test_7_compound_overlay,
|
|
305
|
+
test_8_splines,
|
|
306
|
+
test_9_graph_layout,
|
|
307
|
+
test_10_highlighting,
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
results = []
|
|
311
|
+
for i, test in enumerate(tests, 1):
|
|
312
|
+
try:
|
|
313
|
+
passed = test()
|
|
314
|
+
results.append((i, test.__doc__.split('\n')[0], passed))
|
|
315
|
+
except Exception as e:
|
|
316
|
+
print(f"\n✗ Test {i} failed: {e}")
|
|
317
|
+
import traceback
|
|
318
|
+
traceback.print_exc()
|
|
319
|
+
results.append((i, test.__doc__.split('\n')[0], False))
|
|
320
|
+
|
|
321
|
+
# Summary
|
|
322
|
+
print("\n" + "="*70)
|
|
323
|
+
print("TEST SUMMARY")
|
|
324
|
+
print("="*70)
|
|
325
|
+
for num, name, passed in results:
|
|
326
|
+
status = "✓ PASS" if passed else "✗ FAIL"
|
|
327
|
+
print(f" {status} {name}")
|
|
328
|
+
|
|
329
|
+
passed_count = sum(1 for _, _, p in results if p)
|
|
330
|
+
print("="*70)
|
|
331
|
+
print(f"Results: {passed_count}/{len(results)} tests passed")
|
|
332
|
+
print("="*70)
|
|
333
|
+
|
|
334
|
+
if passed_count == len(results):
|
|
335
|
+
print("\n🎉 All tests passed!")
|
|
336
|
+
return 0
|
|
337
|
+
else:
|
|
338
|
+
print(f"\n⚠ {len(results) - passed_count} test(s) failed")
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
sys.exit(main())
|