Jupyter widget
Use Megane inside Jupyter notebooks as an interactive widget backed by anywidget. The widget is the MolecularViewer object; megane.view() / view_traj() are convenience wrappers that return one. Works in JupyterLab, Jupyter Notebook, VS Code, and Google Colab.
Installation
pip install megane
Then launch JupyterLab (recommended) or classic Notebook:
jupyter lab
Quick Start
Display a structure
import megane
viewer = megane.view("caffeine.pdb")
viewer # display inline
The last expression in a cell renders the widget. Use display(viewer) to render it multiple times in the same notebook.
Play a trajectory
import megane
viewer = megane.view_traj("protein.pdb", xtc="trajectory.xtc", bonds="structure")
viewer
The timeline scrubber appears automatically when a trajectory is loaded.
Instead of xtc=, pass traj= (ASE .traj), xyz= (multi-frame XYZ) or
lammpstrj= (LAMMPS dump) — only one of them. A self-contained trajectory file
(.traj, multi-frame .xyz / .extxyz, .lammpstrj / .dump / .trj) can be
passed on its own: megane.view_traj("trajectory.traj").
Share a notebook with a playable trajectory
By default the trajectory stays in the Python kernel and the widget asks for
each frame as it is shown. That keeps the notebook small, but a notebook
opened without a running kernel (for example one you send to someone else)
shows a single frame and cannot play. Pass embed_trajectory to send every
frame to the widget up front instead:
viewer = megane.view_traj("protein.pdb", xtc="trajectory.xtc", embed_trajectory=True)
# or, with a pipeline:
viewer.set_pipeline(pipe, embed_trajectory=True)
| Value | Behaviour |
|---|---|
False (default) | Frames stay in the kernel; playback needs a running kernel |
True | Every frame is embedded, whatever the size |
"auto" | Embedded when the encoded frames fit in 20 MB (megane.widget.EMBED_TRAJECTORY_AUTO_LIMIT); otherwise a warning is shown and the frames stay in the kernel |
Embedded frames are stored uncompressed (12 bytes per atom per frame, so 1,000 atoms × 500 frames ≈ 6 MB), and they end up in the notebook file together with the rest of the widget state. For the recipient to play them without running the notebook:
- Save the widget state with the notebook. In JupyterLab turn on
Settings → Save Widget State Automatically before saving;
jupyter nbconvert --to notebook --executestores it by default. - Trust the notebook on the receiving side. Jupyter does not render widgets
from a notebook it did not produce until it is trusted: run
jupyter trust notebook.ipynb, or Trust Notebook from the JupyterLab command palette.
Options
Both view() and view_traj() accept these keyword arguments:
| Parameter | Type | Default | Description |
|---|---|---|---|
bonds | "auto" | "distance" | "structure" | "file" | None | "auto" | Bond detection method. "auto" reads bonds from the file for formats that carry them (PDB, MOL/SDF, LAMMPS data, CML, …) and infers them by distance otherwise — the same default as every other host. "distance" forces distance inference, "structure" (alias "file") forces file bonds, None hides bonds |
perspective | bool | False | Perspective projection instead of orthographic |
cell_axes_visible | bool | True | Show unit cell axes |
Both also add a Symmetry node (as does megane.build_pipeline()), so a CIF asymmetric unit is expanded to the full cell as on the other hosts. Structures without symmetry operations pass through unchanged.
view_traj() additionally accepts embed_trajectory (see Share a notebook with a playable trajectory).
For full control over the visualization (filtering, multi-layer rendering, custom nodes), see Visual Pipeline — Python API.
Widget API
After displaying the widget, control it programmatically:
| Property / Method | Type | Description |
|---|---|---|
viewer.frame_index | int (read/write) | Current trajectory frame (0-based) |
viewer.total_frames | int (read) | Total number of frames |
viewer.selected_atoms | list[int] (read/write) | Indices of currently selected atoms |
viewer.measurement | dict | None (read) | Current measurement result |
viewer.camera_state | dict (read/write) | Camera mode, position, target, zoom and up vector; setting it moves the displayed view, and it is kept in the widget state, so a re-displayed or reopened widget restores its view |
viewer.quick_style | bool (read/write) | Show the Quick Style panel (see Properties) |
viewer.set_pipeline(pipe) | method | Apply a pipeline to the viewer |
viewer.on_event(name, fn) | method | Register an event callback |
viewer.off_event(name, fn) | method | Remove an event callback |
Example — jump to a specific frame:
viewer.frame_index = 100
Example — read the current measurement:
viewer.selected_atoms = [10, 20, 30, 40] # select 4 atoms
print(viewer.measurement) # in a later cell
# {"type": "dihedral", "value": 120.5, "label": "120.5°", "atoms": [10, 20, 30, 40]}
measurement is computed by the displayed widget and sent back to the kernel,
which receives it once the cell that set selected_atoms has finished. Atoms right-clicked in
the viewer update measurement too, but not selected_atoms, which only
changes when Python sets it.
Structure editing: megane.Builder
megane.Builder puts Megane Builder — the structure editor — in
a cell. Python opens files in it and reads the edited structure back:
b = megane.Builder("caffeine.sdf")
b # edit in the cell
b.save("caffeine_edited.pdb") # or b.text("xyz"), b.to_ase(), b.n_atoms
File › Save in the cell writes into the kernel's working directory. See In VS Code, JupyterLab and notebooks.
Event Callbacks
React to user interactions with on_event():
Available events
| Event | Fires when | Payload |
|---|---|---|
frame_change | Frame index changes | {"frame_index": int} |
selection_change | selected_atoms is set from Python | {"atoms": list[int]} |
measurement | A measurement of 2–4 selected atoms (set from Python or right-clicked in the viewer) is computed, changes on a new frame, or is cleared | {"type": str, "value": float, "label": str, "atoms": list[int]} or None |
Registering callbacks
Use on_event() as a decorator or method call:
# Decorator style
@viewer.on_event("frame_change")
def on_frame(data):
print(f"Frame changed to {data['frame_index']}")
# Method-call style
def on_select(data):
print(f"Selected: {data['atoms']}")
viewer.on_event("selection_change", on_select)
Removing callbacks
viewer.off_event("frame_change", on_frame) # remove a specific callback
viewer.off_event("selection_change") # remove all callbacks for this event
Plotly Integration
Combine Megane with Plotly FigureWidget to build interactive analysis dashboards.
Click a chart point to jump to a frame
import plotly.graph_objects as go
import ipywidgets as widgets
import megane
viewer = megane.view_traj("protein.pdb", xtc="trajectory.xtc")
n_frames = viewer.total_frames
energy = ... # one value per frame, e.g. from your simulation log
fig = go.FigureWidget(
data=[
go.Scatter(x=list(range(n_frames)), y=energy, mode="lines+markers"),
go.Scatter(x=[0], y=[energy[0]], mode="markers", marker=dict(size=12, color="red")),
],
layout=go.Layout(xaxis_title="Frame", yaxis_title="Energy (kJ/mol)"),
)
def on_click(trace, points, state):
if points.point_inds:
viewer.frame_index = points.point_inds[0]
fig.data[0].on_click(on_click)
widgets.VBox([fig, viewer])
Sync a Plotly marker with the current frame
marker_trace = fig.data[1]
@viewer.on_event("frame_change")
def update_marker(data):
idx = data["frame_index"]
with fig.batch_update():
marker_trace.x = [idx]
marker_trace.y = [energy[idx]]
Collect dihedral angles across all frames
Measurements are computed in the browser, and the kernel cannot receive them
while a cell is still running, so a loop that sets frame_index and waits for
measurement events collects nothing. Compute the series from the trajectory
in Python instead, and let the viewer highlight the same atoms:
import numpy as np
from megane import load_trajectory
atoms = [10, 20, 30, 40]
viewer.selected_atoms = atoms # live dihedral in the viewer
def dihedral_deg(p0, p1, p2, p3):
b0, b1, b2 = p0 - p1, p2 - p1, p3 - p2
b1n = b1 / np.linalg.norm(b1)
v = b0 - np.dot(b0, b1n) * b1n
w = b2 - np.dot(b2, b1n) * b1n
return float(np.degrees(np.arctan2(np.dot(np.cross(b1n, v), w), np.dot(v, w))))
trajectory = load_trajectory("protein.pdb", "trajectory.xtc")
dihedrals = [
dihedral_deg(*trajectory.get_frame(i)[atoms]) for i in range(trajectory.n_frames)
]
# Plot the result
fig = go.FigureWidget(
data=[go.Scatter(y=dihedrals, mode="lines")],
layout=go.Layout(xaxis_title="Frame", yaxis_title="Dihedral (°)"),
)
fig
Interactive Demos
The following example notebooks (outputs pre-rendered for the docs) can be browsed inline.
Demo Notebook
Basic usage — loading structures, navigating trajectories, and side-by-side viewers.
Advanced: External Events
Event handling, Plotly integration, bidirectional sync, dihedral analysis, and custom widget composition.
Pipeline API Reference
For simple visualization, use megane.view() and megane.view_traj(). For advanced customization (filtering, multi-layer rendering, custom nodes), build a pipeline manually with the Pipeline class. See Visual Pipeline — Python API for the complete list of node classes, port names, and parameters.
All classes and convenience functions are importable from megane:
from megane import (
view, view_traj, build_pipeline, # convenience wrappers
MolecularViewer, Builder, # widgets
Pipeline, # pipeline builder
LoadStructure, LoadTrajectory, LoadVector, Streaming, # source nodes
LoadVolumetric, LoadSpectrum,
Filter, Modify, Color, Representation, # processing nodes
Symmetry, Wrap, Replicate, DrawingBoundary, BoundaryCompletion,
AddBonds, AddLabels, AddCoordination, AddPolyhedra, # overlay nodes
SurfaceMesh,
Isosurface, VectorOverlay, SpectrumPlot, Viewport, # display nodes
)
Quick Reference
Mouse Controls
| Action | Effect |
|---|---|
| Left-drag | Rotate |
| Right-drag / Shift + left-drag | Pan |
| Scroll / Pinch / Middle-drag | Zoom |
| Right-click atom | Select atom |
| Double-click atom or bond | Make it the rotation center (double-click empty space to reset) |
Select 2–4 atoms to measure distances, angles, or dihedral angles; the panel's Clear button deselects them. The buttons in the top-left corner turn the camera to look along ±x / ±y / ±z (and ±a / ±b / ±c when the structure has a cell).
Measurement Types
| Atoms Selected | Measurement |
|---|---|
| 2 | Distance (Å) |
| 3 | Angle (°) |
| 4 | Dihedral angle (°) |
API Reference
Methods
| Method | Description |
|---|---|
set_pipeline(pipeline, embed_trajectory=False) | Apply a Pipeline to the viewer, or None to clear. embed_trajectory (True / "auto") embeds the trajectory so it plays without a kernel |
on_event(name, callback=None) | Register an event callback. Use as @viewer.on_event("name") decorator or viewer.on_event("name", fn) |
off_event(name, callback=None) | Remove a specific callback, or all callbacks for the event if callback is None |
Properties
| Property | Type | Read/Write | Description |
|---|---|---|---|
frame_index | int | R/W | Current trajectory frame (0-based). Setting this fires frame_change |
total_frames | int | R | Total number of frames in the loaded trajectory |
selected_atoms | list[int] | R/W | Indices of selected atoms (the first 4 are used for measurement). Setting this fires selection_change |
measurement | dict | None | R | Current measurement result, or None if fewer than 2 atoms selected |
camera_state | dict | R/W | Camera mode ("orthographic" / "perspective"), position, target, zoom and up, updated as the camera moves and restored when the widget is shown again. Setting it from Python moves the displayed view (up is optional) |
quick_style | bool | R/W | Show the Quick Style panel (style, color, atom size / opacity, bond width / opacity, labels, perspective) in the top-right corner. It edits the pipeline loaded with set_pipeline() (view() / view_traj() do this) and writes the result back; calling set_pipeline() again replaces its edits. view() / view_traj() take no quick_style argument — set viewer.quick_style = True after creating the viewer |
The measurement dict contains: type ("distance", "angle", or "dihedral"), value (float), label (formatted string like "120.5°"), and atoms (list of indices).
Tips
- Re-running cells — calling
viewer.set_pipeline(pipe)again updates the displayed pipeline in-place without re-creating the widget. - JupyterLab vs. classic Notebook — Megane is tested on JupyterLab 4+. In classic Notebook, enable the
anywidgetextension if the viewer appears blank. - Large trajectories — the kernel loads the full trajectory into Python memory, and by default sends the widget one frame at a time. Keep large trajectories out of
embed_trajectory=True(every frame would be written into the notebook), or use"auto", which leaves anything over 20 MB in the kernel. For a trajectory too large for the kernel, open it in the standalone web app, which parses in a Web Worker and streams XTC and LAMMPS dump trajectories of 8 MB or more from the file on demand. See Platform Support — UI features. - Sharing notebooks — to hand over a notebook whose trajectory still plays, embed it (see Share a notebook with a playable trajectory). Exported HTML (
File → Export → HTML) contains the viewer only if the widget state was saved with the notebook; Megane does not add a static image of the view.
Troubleshooting
"Module anywidget, version ~0.9.* is not registered, however, 0.11.0 is"
The notebook was saved by Megane 0.16 or older, which recorded the exact
anywidget minor version it ran with, and the Jupyter opening it has another
one. Megane 0.17 and newer record any anywidget front end from 0.9 on
(megane.widget.ANYWIDGET_FRONTEND_RANGE), so re-run the notebook with an
up-to-date Megane and save it again.
"No version of module anywidget is registered"
The Jupyter server showing the page has no anywidget front end. This happens
when Megane is installed into the kernel's environment but JupyterLab runs from
another one (common on hosted JupyterLab services), or when the page was opened
before pip install megane. Install anywidget where JupyterLab itself runs and
reload the browser tab; the server does not need a restart:
pip install anywidget # in the environment that runs `jupyter lab`
jupyter labextension list, run in that environment, must list anywidget.
VS Code: "Failed to load model class 'AnyModel' from module 'anywidget'"
This is a known issue with VS Code's Jupyter extension and affects many anywidget-based projects.
Workarounds:
-
Reload the VS Code window after installing Megane:
- Open the Command Palette (
Ctrl+Shift+P/Cmd+Shift+P) - Run
Developer: Reload Window
- Open the Command Palette (
-
Ensure ipywidgets v8.x is installed:
pip install "ipywidgets>=8.0.0" -
Update the VS Code Jupyter extension to the latest version.
-
Restart the kernel and re-run the notebook cells.