reflex-niivue
A Reflex component wrapping NiiVue, the WebGL2 viewer for medical images: NIfTI, NRRD, MHA/MHD, MGH, DICOM, meshes (GIfTI, MZ3, OBJ, ...), tractography, connectomes and drawings.
- One general-purpose
niivue.view(...)component: volumes and overlays, label maps, meshes with layers, multiplanar and 3D render views, clip planes, drawing, synced viewers. - Props map 1:1 to NiiVue methods; volumes and meshes are diffed by url, so changing an opacity or a colormap never downloads the image again.
- NiiVue events (
on_location,on_image_loaded, ...) arrive in your Reflex state. - Imperative actions (
save_image,undo_draw,move_crosshair_to_mm, ...) are events you return oryieldfrom a handler. - NiiVue itself (
@niivue/niivue0.69.0) comes from npm when your app builds; nothing is vendored. No image data passes through Reflex state: NiiVue fetches the files itself.
Requires Python 3.11+, Reflex 0.10+ and a browser with WebGL2.
Install
uv add reflex-niivue # or: pip install reflex-niivue
Quick start
import reflex as rx
from reflex_niivue import label_colormap, niivue
IMAGES = "https://niivue.com/demos/images/"
class State(rx.State):
opacity: float = 0.7
mm: list[float] = []
@rx.event
def located(self, mm: list[float], vox: list[int], values: list[dict], frac: list[float]):
self.mm = mm
@rx.event
def save(self):
yield niivue.save_image("mask.nii.gz") # the drawing, as NIfTI
@rx.var
def volumes(self) -> list[dict]:
return [
{"url": IMAGES + "mni152.nii.gz", "name": "t1"},
{
"url": IMAGES + "hippo.nii.gz",
"name": "hippocampus",
"opacity": self.opacity,
"colormap_label": label_colormap({9: ("hippocampus", (255, 70, 70))}),
},
]
def index():
return rx.vstack(
niivue.view(
volumes=State.volumes,
slice_type="multiplanar",
drawing_enabled=True,
pen_value=1,
on_location=State.located,
height="500px",
),
rx.text(State.mm.to_string()),
rx.hstack(
rx.button("Undo", on_click=niivue.undo_draw()),
rx.button("Save drawing", on_click=State.save),
),
)
app = rx.App()
app.add_page(index)
Props
| Prop | Type | NiiVue |
|---|---|---|
volumes |
list[dict] |
loadVolumes / addVolumeFromUrl / removeVolume, setColormap, setOpacity, cal_min/cal_max, setColormapLabel |
meshes |
list[dict] |
addMeshFromUrl / removeMesh, setMeshProperty, setMeshLayerProperty, setMeshShader |
slice_type |
"axial", "coronal", "sagittal", "multiplanar", "render" |
setSliceType |
multiplanar_layout |
"auto", "column", "grid", "row" |
setMultiplanarLayout |
show_3d_crosshair |
bool |
opts.show3Dcrosshair |
crosshair_color, crosshair_width |
RGBA 0..1, float |
setCrosshairColor, setCrosshairWidth |
back_color |
RGBA 0..1 | opts.backColor |
drag_mode |
"none", "contrast", "measurement", "pan", "slicer3d", "angle", "roi_selection", "crosshair", "windowing", "callback_only" |
setDragMode |
is_colorbar, is_orient_cube |
bool |
opts.isColorbar, opts.isOrientCube |
is_radiological |
bool |
setRadiologicalConvention |
clip_plane |
[depth, azimuth, elevation] (depth ≥ 2 = off) |
setClipPlane |
render_azimuth_elevation |
[azimuth, elevation] |
setRenderAzimuthElevation |
selected_volume |
int |
the layer whose colorbar shows; default target of set_frame_4d / save_image |
drawing_enabled |
bool |
setDrawingEnabled |
pen_value |
int (0 erases) |
setPenValue |
draw_opacity |
float |
setDrawOpacity |
drawing_fill_overwrites |
bool |
drawFillOverwrites |
options |
dict (camelCase NVConfigOptions) |
merged into new Niivue(options); the props above win |
sync_group |
str |
viewers in the same group broadcastTo each other (crosshair, pan/zoom, 3D camera) |
sync_opts |
dict |
what a group shares, default {"2d": True, "3d": True} |
height, width, id |
default 480px × 100%; id names the viewer for the helpers |
niivue_options(**props) shows the constructor options a set of literal props produces.
Volume descriptors
{
"url": "https://.../t1.nii.gz", # required (NIfTI, NRRD, MHA, MGH, ...)
"name": "t1", # display name (shown in on_location values)
"colormap": "gray", # see colormaps()
"opacity": 1.0,
"visible": True, # False = opacity 0, the volume stays loaded
"cal_min": 0,
"cal_max": 100, # display range
"colormap_label": label_colormap({1: ("liver", (255, 0, 0))}), # label map
# any other NiiVue ImageFromUrlOptions, camelCase or snake_case:
"colormap_negative": "winter",
"frame_4d": 0,
"headers": {"Authorization": "..."},
}
The first volume is the background. Volumes are matched by url (or name): changing a
property updates the loaded volume; adding volumes after the existing ones appends them;
replacing the background or reordering reloads the list.
Mesh descriptors
{
"url": "https://.../BrainMesh_ICBM152.lh.mz3",
"name": "lh",
"rgba255": [200, 200, 200, 255],
"opacity": 1.0,
"visible": True,
"layers": [
{"url": ".../lh.motor.mz3", "colormap": "warm", "cal_min": 2, "cal_max": 5, "opacity": 0.7}
],
}
Events
| Event | Handler arguments |
|---|---|
on_location |
mm: list[float], vox: list[int], values: list[dict] (name, id, value, vox, mm per volume), frac: list[float] — throttled to 20/s |
on_image_loaded |
name: str, dims: list[int] |
on_mesh_loaded |
name: str |
on_frame_time_ms |
ms: float — mean redraw time, at most once per second |
on_frame_change |
name: str, frame: int (4D volumes) |
on_intensity_change |
volume: dict (name, id, index, cal_min, cal_max) |
on_drawing_changed |
action: str ("draw", "undo", "load", "close") |
on_volume_updated |
— |
on_error |
message: str (no WebGL2, failed download, unreadable file) |
Imperative helpers
Each returns an event: use it as an event trigger, or return/yield it from a handler.
id picks the viewer (its id prop); without it the most recently mounted viewer is used.
rx.button("Undo", on_click=niivue.undo_draw(id="main"))
@rx.event
def save(self):
yield niivue.save_image("mask.nii.gz", id="main")
| Helper | Does |
|---|---|
move_crosshair_to_mm([x, y, z]) |
moves the crosshair (fires on_location) |
set_frame_4d(frame, volume=None) |
shows a frame of a 4D volume |
save_image(filename, drawing=None, volume=None) |
downloads the drawing (default when there is one) or a volume as NIfTI |
save_scene(filename) |
downloads a PNG screenshot |
set_slice_mm(True) |
world-space (True) or voxel-space slices |
undo_draw() / clear_drawing() |
drawing undo / erase |
load_drawing_from_url(url, binarize=False) |
loads a drawing on the background's grid |
reset_view() |
resets pan/zoom, crosshair and 3D camera |
Arguments may be state Vars: niivue.move_crosshair_to_mm(State.target).
Colormaps
from reflex_niivue import colormaps, label_colormap
colormaps() # ["actc", ..., "gray", "hot", ..., "x_rain"]: NiiVue's built-in names
label_colormap({1: ("liver", (255, 0, 0)), 2: ("spleen", "#00ff0080")})
# {"R": [0, 255, 0], "G": [0, 0, 255], "B": [0, 0, 0], "A": [0, 255, 128],
# "I": [0, 1, 2], "labels": ["background", "liver", "spleen"]}
Demo
git clone https://github.com/ecrespo/reflex-niivue && cd reflex-niivue
uv sync
cd niivue_demo && uv run reflex run
Tabs: volume + label overlay (colormap, opacity, label/heat), multiplanar vs 3D render (camera,
clip plane), drawing (pen, undo, clear, save, load), meshes with a scalar layer, two synced
viewers, 4D frames and the imperative helpers, and a panel showing on_location from Python
state. It uses NiiVue's demo images and falls back to a synthetic phantom (numpy + nibabel,
written to niivue_demo/assets/) when they cannot be downloaded; NIIVUE_DEMO_OFFLINE=1 forces
the offline data.
Development
uv sync
uv run pre-commit run --all-files
uv run playwright install chromium # once, for the browser checks
uv run pytest # or: uv run pytest -m "not browser"
uv build
Releasing
Releases are published from GitHub Actions with Trusted Publishing (no API tokens):
| Event | Result |
|---|---|
push to main |
all checks, then the development pre-release 0.1.1.devN on PyPI (pip install --pre reflex-niivue) and an install check |
tag v0.2.0rc1 (or a1, b1) on main |
all checks, then a pre-release on PyPI (pip install --pre reflex-niivue) and a GitHub pre-release |
tag v0.2.0 on main |
all checks, then the release on PyPI and a GitHub release |
git tag v0.1.0rc1 && git push origin v0.1.0rc1 # pre-release
git tag v0.1.0 && git push origin v0.1.0 # release
License
Apache-2.0 (see LICENSE and NOTICE). NiiVue is BSD-2-Clause and is installed from npm.
Metadata
Release files for reflex-niivue 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| reflex_niivue-0.1.0.tar.gz | 100.7 kB | With provenance | Details for reflex_niivue-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| reflex_niivue-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for reflex_niivue-0.1.0-py3-none-any.whl |
Total release size: 130.4 kB
Release files / reflex_niivue-0.1.0.tar.gz
| Download URL | reflex_niivue-0.1.0.tar.gz |
|---|---|
| Size | 100.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55e8c4e4ec2f2a3810fa806b52b861bb9ea7d07ab47e167628b4e79cbe7125a0
|
|
BLAKE2b-256 checksum How to use checksums |
51d38cb27af5cc508d4ffef278278e9a3b07e0531f21bff6cf706be4372f8579
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / reflex_niivue-0.1.0-py3-none-any.whl
| Download URL | reflex_niivue-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7f52e37892be6c6cd85527d53ac7bae4aad7cc0d61378d0057ccaf1dd910db42
|
|
BLAKE2b-256 checksum How to use checksums |
9e293c387832f10c0340c9e0797754c04b089c9f51e561d2207ac7d0eae18d18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logSecurity for reflex-niivue 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for reflex_niivue-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.