Skip to main content

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.

A live Megane widget showing caffeine solvated in water — drag to rotate, scroll to zoom. This is the same renderer the notebook cell mounts.

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)
ValueBehaviour
False (default)Frames stay in the kernel; playback needs a running kernel
TrueEvery 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 --execute stores 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:

ParameterTypeDefaultDescription
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
perspectiveboolFalsePerspective projection instead of orthographic
cell_axes_visibleboolTrueShow 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 / MethodTypeDescription
viewer.frame_indexint (read/write)Current trajectory frame (0-based)
viewer.total_framesint (read)Total number of frames
viewer.selected_atomslist[int] (read/write)Indices of currently selected atoms
viewer.measurementdict | None (read)Current measurement result
viewer.camera_statedict (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_stylebool (read/write)Show the Quick Style panel (see Properties)
viewer.set_pipeline(pipe)methodApply a pipeline to the viewer
viewer.on_event(name, fn)methodRegister an event callback
viewer.off_event(name, fn)methodRemove 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​

EventFires whenPayload
frame_changeFrame index changes{"frame_index": int}
selection_changeselected_atoms is set from Python{"atoms": list[int]}
measurementA 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.

Loading notebook...

Advanced: External Events​

Event handling, Plotly integration, bidirectional sync, dihedral analysis, and custom widget composition.

Loading notebook...

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​

ActionEffect
Left-dragRotate
Right-drag / Shift + left-dragPan
Scroll / Pinch / Middle-dragZoom
Right-click atomSelect atom
Double-click atom or bondMake 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 SelectedMeasurement
2Distance (Å)
3Angle (°)
4Dihedral angle (°)

API Reference​

Methods​

MethodDescription
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​

PropertyTypeRead/WriteDescription
frame_indexintR/WCurrent trajectory frame (0-based). Setting this fires frame_change
total_framesintRTotal number of frames in the loaded trajectory
selected_atomslist[int]R/WIndices of selected atoms (the first 4 are used for measurement). Setting this fires selection_change
measurementdict | NoneRCurrent measurement result, or None if fewer than 2 atoms selected
camera_statedictR/WCamera 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_styleboolR/WShow 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 anywidget extension 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:

  1. Reload the VS Code window after installing Megane:

    • Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
    • Run Developer: Reload Window
  2. Ensure ipywidgets v8.x is installed:

    pip install "ipywidgets>=8.0.0"
  3. Update the VS Code Jupyter extension to the latest version.

  4. Restart the kernel and re-run the notebook cells.