seqeyes-python 0.2.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.
- seqeyes/__init__.py +32 -0
- seqeyes/_plot.py +221 -0
- seqeyes/_renderer.py +236 -0
- seqeyes/resources/pulseq-bundle.js +2296 -0
- seqeyes/resources/viewer.html +1589 -0
- seqeyes_python-0.2.0.dist-info/METADATA +124 -0
- seqeyes_python-0.2.0.dist-info/RECORD +8 -0
- seqeyes_python-0.2.0.dist-info/WHEEL +4 -0
seqeyes/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SeqEyes — Interactive Pulseq MRI Sequence Viewer
|
|
3
|
+
=================================================
|
|
4
|
+
|
|
5
|
+
Drop‑in replacement for ``pypulseq.Sequence.plot()``.
|
|
6
|
+
|
|
7
|
+
Quick start
|
|
8
|
+
-----------
|
|
9
|
+
>>> import seqeyes
|
|
10
|
+
>>> seqeyes.set(theme="dark") # enable SeqEyes + set defaults
|
|
11
|
+
|
|
12
|
+
>>> seq.plot() # interactive viewer in Jupyter
|
|
13
|
+
>>> seq.plot(show_blocks=True) # per‑call overrides
|
|
14
|
+
>>> seq.plot(time_range=(0, 0.05)) # zoom to first 50 ms
|
|
15
|
+
|
|
16
|
+
>>> seqeyes.reset() # back to matplotlib
|
|
17
|
+
|
|
18
|
+
In a plain ``.py`` script (no Jupyter):
|
|
19
|
+
>>> seq.plot() # opens the viewer in your default web browser
|
|
20
|
+
|
|
21
|
+
API
|
|
22
|
+
---
|
|
23
|
+
- :func:`set` — enable SeqEyes + configure defaults
|
|
24
|
+
- :func:`reset` — restore matplotlib
|
|
25
|
+
- :class:`SeqEyesViewer` — low‑level viewer
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from seqeyes._plot import set, reset
|
|
29
|
+
from seqeyes._renderer import SeqEyesViewer
|
|
30
|
+
|
|
31
|
+
__all__ = ["set", "reset", "SeqEyesViewer"]
|
|
32
|
+
__version__ = "0.2.0"
|
seqeyes/_plot.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_plot.py — SeqEyes plotting API.
|
|
3
|
+
|
|
4
|
+
Call ``seqeyes.set()`` to switch ``seq.plot()`` to the interactive
|
|
5
|
+
SeqEyes viewer. Call ``seqeyes.reset()`` to restore matplotlib.
|
|
6
|
+
|
|
7
|
+
In Jupyter the viewer renders inline. In a ``.py`` script it opens
|
|
8
|
+
a native desktop pop‑up window (requires ``pywebview``: ``pip install pywebview``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Optional, Tuple, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Module state ──────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
_defaults: dict[str, Any] = {}
|
|
22
|
+
_original_plot = None
|
|
23
|
+
_patched = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
def set(
|
|
29
|
+
*,
|
|
30
|
+
theme: Optional[str] = None,
|
|
31
|
+
show_blocks: Optional[bool] = None,
|
|
32
|
+
time_disp: Optional[str] = None,
|
|
33
|
+
grad_disp: Optional[str] = None,
|
|
34
|
+
time_range: Optional[Tuple[float, float]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Enable SeqEyes for all ``seq.plot()`` calls and set global defaults.
|
|
37
|
+
|
|
38
|
+
Call this once at the top of your notebook / script — with or
|
|
39
|
+
without arguments. Any keywords become defaults applied to every
|
|
40
|
+
subsequent ``seq.plot()`` (overridable per‑call).
|
|
41
|
+
|
|
42
|
+
>>> seqeyes.set() # enable, system defaults
|
|
43
|
+
>>> seqeyes.set(theme="dark", time_disp="ms") # enable + defaults
|
|
44
|
+
>>> seq.plot() # uses dark + ms
|
|
45
|
+
>>> seq.plot(theme="light") # overrides theme only
|
|
46
|
+
"""
|
|
47
|
+
_store_defaults(theme, show_blocks, time_disp, grad_disp, time_range)
|
|
48
|
+
_ensure_patched()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reset() -> None:
|
|
52
|
+
"""Restore matplotlib ``seq.plot()`` and clear all SeqEyes defaults.
|
|
53
|
+
|
|
54
|
+
Call ``seqeyes.set()`` again to re‑enable at any time.
|
|
55
|
+
"""
|
|
56
|
+
global _patched
|
|
57
|
+
_defaults.clear()
|
|
58
|
+
_patched = False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
from pypulseq import Sequence as _Seq
|
|
62
|
+
except ImportError:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if _original_plot is not None:
|
|
66
|
+
_Seq.plot = _original_plot # type: ignore[attr-defined]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Internal helpers ──────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def _store_defaults(
|
|
72
|
+
theme: Optional[str],
|
|
73
|
+
show_blocks: Optional[bool],
|
|
74
|
+
time_disp: Optional[str],
|
|
75
|
+
grad_disp: Optional[str],
|
|
76
|
+
time_range: Optional[Tuple[float, float]],
|
|
77
|
+
) -> None:
|
|
78
|
+
for name, val in [
|
|
79
|
+
("theme", theme),
|
|
80
|
+
("show_blocks", show_blocks),
|
|
81
|
+
("time_disp", time_disp),
|
|
82
|
+
("grad_disp", grad_disp),
|
|
83
|
+
("time_range", time_range),
|
|
84
|
+
]:
|
|
85
|
+
if val is not None:
|
|
86
|
+
_defaults[name] = val
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ensure_patched() -> None:
|
|
90
|
+
global _patched, _original_plot
|
|
91
|
+
|
|
92
|
+
if _patched:
|
|
93
|
+
return
|
|
94
|
+
_patched = True
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from pypulseq import Sequence as _Seq
|
|
98
|
+
except ImportError:
|
|
99
|
+
raise ImportError("pypulseq is not installed. Install with: pip install pypulseq")
|
|
100
|
+
|
|
101
|
+
if _original_plot is None:
|
|
102
|
+
_original_plot = getattr(_Seq, "plot", None)
|
|
103
|
+
|
|
104
|
+
# ── Replacement .plot() ──────────────────────────────────────────
|
|
105
|
+
def _seqeyes_plot(
|
|
106
|
+
self: object,
|
|
107
|
+
label: str = "",
|
|
108
|
+
show_blocks: bool = False,
|
|
109
|
+
time_range: Any = (0, float("inf")),
|
|
110
|
+
time_disp: str = "s",
|
|
111
|
+
grad_disp: str = "kHz/m",
|
|
112
|
+
**kwargs: Any,
|
|
113
|
+
) -> None:
|
|
114
|
+
# Merge globals — per‑call args take precedence
|
|
115
|
+
sb = _defaults.get("show_blocks", show_blocks)
|
|
116
|
+
td = _defaults.get("time_disp", time_disp)
|
|
117
|
+
gd = _defaults.get("grad_disp", grad_disp)
|
|
118
|
+
tr = _defaults.get("time_range", time_range)
|
|
119
|
+
th = str(kwargs.pop("theme", _defaults.get("theme", "system")))
|
|
120
|
+
|
|
121
|
+
seq_text = _seq_to_text(self)
|
|
122
|
+
if not seq_text:
|
|
123
|
+
raise RuntimeError("Could not write .seq text from the pypulseq Sequence.")
|
|
124
|
+
|
|
125
|
+
from seqeyes._renderer import SeqEyesViewer
|
|
126
|
+
|
|
127
|
+
viewer = SeqEyesViewer(
|
|
128
|
+
seq_text, label=label, show_blocks=sb,
|
|
129
|
+
time_range=tr, time_disp=td, grad_disp=gd, theme=th,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if _in_jupyter():
|
|
133
|
+
from IPython.display import display
|
|
134
|
+
display(viewer)
|
|
135
|
+
else:
|
|
136
|
+
_open_in_browser(viewer)
|
|
137
|
+
|
|
138
|
+
# Preserve original as _plot_matplotlib
|
|
139
|
+
if _original_plot is not None and not hasattr(_Seq, "_plot_matplotlib"):
|
|
140
|
+
_Seq._plot_matplotlib = _original_plot # type: ignore[attr-defined]
|
|
141
|
+
|
|
142
|
+
_Seq.plot = _seqeyes_plot # type: ignore[attr-defined]
|
|
143
|
+
|
|
144
|
+
# _repr_html_ so bare ``seq`` auto‑renders in Jupyter
|
|
145
|
+
def _seq_repr_html_(self: object) -> str:
|
|
146
|
+
from seqeyes._renderer import SeqEyesViewer
|
|
147
|
+
return SeqEyesViewer(_seq_to_text(self))._repr_html_()
|
|
148
|
+
|
|
149
|
+
_Seq._repr_html_ = _seq_repr_html_ # type: ignore[attr-defined]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _in_jupyter() -> bool:
|
|
153
|
+
try:
|
|
154
|
+
from IPython import get_ipython
|
|
155
|
+
s = get_ipython()
|
|
156
|
+
return s is not None and ("ZMQ" in s.__class__.__name__ or "Shell" in s.__class__.__name__)
|
|
157
|
+
except ImportError:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _open_in_browser(viewer: "SeqEyesViewer") -> None: # noqa: F821
|
|
162
|
+
"""Open the viewer in a native desktop pop‑up window (pywebview).
|
|
163
|
+
|
|
164
|
+
Spawns a subprocess so ``webview.start()`` runs on its own main
|
|
165
|
+
thread without blocking the calling script. Falls back to the
|
|
166
|
+
system browser if pywebview is not installed.
|
|
167
|
+
"""
|
|
168
|
+
html = viewer.to_html()
|
|
169
|
+
|
|
170
|
+
# Write HTML to a temp file — file:// URLs are more reliable with
|
|
171
|
+
# pywebview than inline html= on some backends.
|
|
172
|
+
p = os.path.join(tempfile.gettempdir(), "seqeyes_viewer.html")
|
|
173
|
+
Path(p).write_text(html, encoding="utf-8")
|
|
174
|
+
url = f"file:///{p.replace(os.sep, '/')}"
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
import webview # type: ignore[import-untyped]
|
|
178
|
+
except ImportError:
|
|
179
|
+
_fallback_browser(url)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# Run webview in a subprocess so it gets its own main thread.
|
|
183
|
+
# We shell out to a tiny inline script — this avoids multiprocessing
|
|
184
|
+
# "spawn" issues (re‑importing the parent script on Windows).
|
|
185
|
+
import subprocess
|
|
186
|
+
import sys
|
|
187
|
+
|
|
188
|
+
script = (
|
|
189
|
+
"import webview;"
|
|
190
|
+
f"webview.create_window(title='SeqEyes — Pulseq MRI Sequence Viewer',url={url!r},width=1280,height=800,resizable=True,easy_drag=False);"
|
|
191
|
+
"webview.start()"
|
|
192
|
+
)
|
|
193
|
+
try:
|
|
194
|
+
subprocess.Popen(
|
|
195
|
+
[sys.executable, "-c", script],
|
|
196
|
+
stdout=subprocess.DEVNULL,
|
|
197
|
+
stderr=subprocess.DEVNULL,
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
_fallback_browser(url)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _fallback_browser(url: str) -> None:
|
|
204
|
+
"""Last resort: open the viewer in the system web browser."""
|
|
205
|
+
import webbrowser
|
|
206
|
+
webbrowser.open(url)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _seq_to_text(seq: object) -> str:
|
|
210
|
+
if hasattr(seq, "write"):
|
|
211
|
+
with tempfile.NamedTemporaryFile(mode="r", suffix=".seq", delete=False, encoding="utf-8") as f:
|
|
212
|
+
tp = f.name
|
|
213
|
+
try:
|
|
214
|
+
seq.write(tp) # type: ignore[union-attr]
|
|
215
|
+
return Path(tp).read_text(encoding="utf-8")
|
|
216
|
+
finally:
|
|
217
|
+
try:
|
|
218
|
+
os.unlink(tp)
|
|
219
|
+
except OSError:
|
|
220
|
+
pass
|
|
221
|
+
return ""
|
seqeyes/_renderer.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_renderer.py — Jupyter HTML renderer for the SeqEyes interactive viewer.
|
|
3
|
+
|
|
4
|
+
Generates a self‑contained HTML string that Jupyter displays inline via
|
|
5
|
+
``_repr_html_()``, exactly the same mechanism Plotly uses. The viewer is
|
|
6
|
+
an iframe embedding the SeqEyes web UI with sequence data injected.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Paths ─────────────────────────────────────────────────────────────────
|
|
19
|
+
_RESOURCES_DIR = Path(__file__).resolve().parent / "resources"
|
|
20
|
+
_VIEWER_TEMPLATE_PATH = _RESOURCES_DIR / "viewer.html"
|
|
21
|
+
|
|
22
|
+
# pulseq-bundle.js lives in the repo root; during dev we read it directly.
|
|
23
|
+
# When installed as a package, a copy should be placed in resources/.
|
|
24
|
+
_BUNDLE_CANDIDATES = [
|
|
25
|
+
_RESOURCES_DIR / "pulseq-bundle.js",
|
|
26
|
+
Path(__file__).resolve().parent.parent.parent.parent / "web" / "pulseq-bundle.js",
|
|
27
|
+
Path.cwd() / "web" / "pulseq-bundle.js",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _find_bundle() -> str:
|
|
32
|
+
"""Locate and read the pulseq-bundle.js file."""
|
|
33
|
+
for p in _BUNDLE_CANDIDATES:
|
|
34
|
+
if p.is_file():
|
|
35
|
+
return p.read_text(encoding="utf-8")
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
"pulseq-bundle.js not found. "
|
|
38
|
+
"Build it with: npm run build:web (from the repo root), "
|
|
39
|
+
"or copy it to python/src/seqeyes/resources/"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_viewer_template() -> str:
|
|
44
|
+
"""Read the standalone viewer HTML template."""
|
|
45
|
+
if _VIEWER_TEMPLATE_PATH.is_file():
|
|
46
|
+
return _VIEWER_TEMPLATE_PATH.read_text(encoding="utf-8")
|
|
47
|
+
alt = Path.cwd() / "python" / "src" / "seqeyes" / "resources" / "viewer.html"
|
|
48
|
+
if alt.is_file():
|
|
49
|
+
return alt.read_text(encoding="utf-8")
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
f"Viewer template not found at {_VIEWER_TEMPLATE_PATH}. "
|
|
52
|
+
"Ensure the package is installed correctly."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _build_html(
|
|
57
|
+
seq_text: str,
|
|
58
|
+
*,
|
|
59
|
+
theme: Optional[str] = None,
|
|
60
|
+
inject_bundle: bool = True,
|
|
61
|
+
label: str = "",
|
|
62
|
+
show_blocks: bool = False,
|
|
63
|
+
time_range: tuple = (0, float("inf")),
|
|
64
|
+
time_disp: str = "s",
|
|
65
|
+
grad_disp: str = "kHz/m",
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Assemble the complete viewer HTML.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
seq_text : str
|
|
72
|
+
Raw .seq file content.
|
|
73
|
+
theme : str or None
|
|
74
|
+
CSS theme class to apply to ``<body>``.
|
|
75
|
+
inject_bundle : bool
|
|
76
|
+
If True, inline the pulseq-bundle.js parser into the HTML.
|
|
77
|
+
label : str
|
|
78
|
+
Display label (injected as JS variable, not yet rendered).
|
|
79
|
+
show_blocks : bool
|
|
80
|
+
Initial block‑boundary visibility.
|
|
81
|
+
time_range : tuple[float, float]
|
|
82
|
+
Initial viewport range in seconds.
|
|
83
|
+
time_disp : str
|
|
84
|
+
Initial time display unit (``"s"``, ``"ms"``, ``"us"``).
|
|
85
|
+
grad_disp : str
|
|
86
|
+
Initial gradient display unit (``"Hz/m"``, ``"kHz/m"``,
|
|
87
|
+
``"mT/m"``, ``"G/cm"``).
|
|
88
|
+
"""
|
|
89
|
+
template = _read_viewer_template()
|
|
90
|
+
seq_b64 = base64.b64encode(seq_text.encode("utf-8")).decode("ascii")
|
|
91
|
+
|
|
92
|
+
# 1. Inject the pulseq-bundle.js
|
|
93
|
+
bundle_placeholder = "<!-- PULSEQ_BUNDLE_PLACEHOLDER -->"
|
|
94
|
+
if inject_bundle and bundle_placeholder in template:
|
|
95
|
+
bundle_js = _find_bundle()
|
|
96
|
+
template = template.replace(
|
|
97
|
+
bundle_placeholder,
|
|
98
|
+
f"<script>\n{bundle_js}\n</script>",
|
|
99
|
+
)
|
|
100
|
+
elif bundle_placeholder in template:
|
|
101
|
+
template = template.replace(bundle_placeholder, "")
|
|
102
|
+
|
|
103
|
+
# 2. Build options injection block (sequence data + display opts)
|
|
104
|
+
opts = [
|
|
105
|
+
f"window.SEQEYES_RAW_B64 = {json.dumps(seq_b64)};",
|
|
106
|
+
f"window.SEQEYES_LABEL = {json.dumps(label)};",
|
|
107
|
+
f"window.SEQEYES_SHOW_BLOCKS = {json.dumps(show_blocks)};",
|
|
108
|
+
f"window.SEQEYES_TIME_RANGE = {json.dumps(list(time_range))};",
|
|
109
|
+
f"window.SEQEYES_TIME_DISP = {json.dumps(time_disp)};",
|
|
110
|
+
f"window.SEQEYES_GRAD_DISP = {json.dumps(grad_disp)};",
|
|
111
|
+
]
|
|
112
|
+
opts_block = "\n".join(opts)
|
|
113
|
+
|
|
114
|
+
data_placeholder = "/* SEQEYES_DATA_PLACEHOLDER */"
|
|
115
|
+
if data_placeholder in template:
|
|
116
|
+
template = template.replace(data_placeholder, opts_block)
|
|
117
|
+
else:
|
|
118
|
+
template = template.replace(
|
|
119
|
+
"</body>",
|
|
120
|
+
f"<script>{opts_block}</script>\n</body>",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# 3. Apply theme if specified
|
|
124
|
+
if theme:
|
|
125
|
+
template = template.replace(
|
|
126
|
+
"<body>",
|
|
127
|
+
f'<body class="theme-{theme}">',
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return template
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SeqEyesViewer:
|
|
134
|
+
"""Interactive Pulseq sequence viewer for Jupyter notebook output.
|
|
135
|
+
|
|
136
|
+
Normally you don't create this directly — call ``seqeyes.set()``
|
|
137
|
+
once, then use ``seq.plot(...)`` on any ``pypulseq.Sequence``.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
seq_text : str
|
|
142
|
+
Raw .seq file content as a string.
|
|
143
|
+
label : str
|
|
144
|
+
Display label (not yet rendered on the viewer).
|
|
145
|
+
show_blocks : bool
|
|
146
|
+
Whether to show block‑boundary lines initially.
|
|
147
|
+
time_range : tuple[float, float]
|
|
148
|
+
Initial time viewport ``(start_sec, end_sec)``. ``(0, inf)``
|
|
149
|
+
shows the whole sequence.
|
|
150
|
+
time_disp : str
|
|
151
|
+
Time axis unit — ``"s"``, ``"ms"``, or ``"us"``.
|
|
152
|
+
grad_disp : str
|
|
153
|
+
Gradient axis unit — ``"Hz/m"``, ``"kHz/m"``, ``"mT/m"``, ``"G/cm"``.
|
|
154
|
+
theme : str or None
|
|
155
|
+
Colour theme.
|
|
156
|
+
width : str
|
|
157
|
+
CSS width of the iframe (e.g. ``"100%"``).
|
|
158
|
+
height : str
|
|
159
|
+
CSS height of the iframe (e.g. ``"550px"``).
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
seq_text: str,
|
|
165
|
+
*,
|
|
166
|
+
label: str = "",
|
|
167
|
+
show_blocks: bool = False,
|
|
168
|
+
time_range: tuple = (0, float("inf")),
|
|
169
|
+
time_disp: str = "s",
|
|
170
|
+
grad_disp: str = "kHz/m",
|
|
171
|
+
theme: Optional[str] = None,
|
|
172
|
+
width: str = "100%",
|
|
173
|
+
height: str = "550px",
|
|
174
|
+
) -> None:
|
|
175
|
+
self._seq_text = seq_text
|
|
176
|
+
self._label = label
|
|
177
|
+
self._show_blocks = show_blocks
|
|
178
|
+
self._time_range = time_range
|
|
179
|
+
self._time_disp = time_disp
|
|
180
|
+
self._grad_disp = grad_disp
|
|
181
|
+
self._theme = theme
|
|
182
|
+
self._width = width
|
|
183
|
+
self._height = height
|
|
184
|
+
|
|
185
|
+
# ── Jupyter integration ───────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _repr_html_(self) -> str:
|
|
188
|
+
"""Return an HTML iframe that Jupyter renders inline."""
|
|
189
|
+
html = _build_html(
|
|
190
|
+
self._seq_text,
|
|
191
|
+
theme=self._theme,
|
|
192
|
+
label=self._label,
|
|
193
|
+
show_blocks=self._show_blocks,
|
|
194
|
+
time_range=self._time_range,
|
|
195
|
+
time_disp=self._time_disp,
|
|
196
|
+
grad_disp=self._grad_disp,
|
|
197
|
+
)
|
|
198
|
+
b64 = base64.b64encode(html.encode("utf-8")).decode("ascii")
|
|
199
|
+
return (
|
|
200
|
+
f'<iframe src="data:text/html;base64,{b64}" '
|
|
201
|
+
f'width="{self._width}" height="{self._height}" '
|
|
202
|
+
f'frameborder="0" '
|
|
203
|
+
f'style="border:none;max-width:100%;overflow:hidden" '
|
|
204
|
+
f'title="SeqEyes Viewer">'
|
|
205
|
+
f"</iframe>"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _ipython_display_(self) -> None:
|
|
209
|
+
"""IPython display hook."""
|
|
210
|
+
from IPython.display import HTML, display
|
|
211
|
+
|
|
212
|
+
display(HTML(self._repr_html_()))
|
|
213
|
+
|
|
214
|
+
# ── Convenience ────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
def show(self) -> "SeqEyesViewer":
|
|
217
|
+
"""Display the viewer (useful in IPython when auto‑display is off)."""
|
|
218
|
+
from IPython.display import display
|
|
219
|
+
|
|
220
|
+
display(self)
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
# ── Direct HTML access (for debugging / custom embedding) ──────────
|
|
224
|
+
|
|
225
|
+
def to_html(self, *, inject_bundle: bool = True) -> str:
|
|
226
|
+
"""Return the full standalone HTML string (for saving to a file)."""
|
|
227
|
+
return _build_html(
|
|
228
|
+
self._seq_text,
|
|
229
|
+
theme=self._theme,
|
|
230
|
+
inject_bundle=inject_bundle,
|
|
231
|
+
label=self._label,
|
|
232
|
+
show_blocks=self._show_blocks,
|
|
233
|
+
time_range=self._time_range,
|
|
234
|
+
time_disp=self._time_disp,
|
|
235
|
+
grad_disp=self._grad_disp,
|
|
236
|
+
)
|