Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
413 changes: 172 additions & 241 deletions README.md

Large diffs are not rendered by default.

57 changes: 31 additions & 26 deletions examples/wavelet.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
- vtkRTAnalyticSource generates a 3D structured grid with the "RTData" scalar.
- Contour iso-surfaces are clipped by a plane so you can see inside.
- A slice plane through the center shows continuous vs discrete coloring.
- ColormapController manages the color transfer function and wires it to
- Both pipelines are merged via vtkAppendPolyData into a single mapper.
- ColormapConfig manages the color transfer function and wires it to
the mapper.
- The colorbar widget provides an interactive preset picker, scale modes
(linear / log / symlog), discrete banding, and manual range override.
- Four colorbar widgets (top, bottom, left, right) each provide an
interactive preset picker, scale modes (linear / log / symlog),
discrete banding, and manual range override.

Run:
cd <repo_root>
Expand All @@ -20,7 +22,12 @@
from trame.app import TrameApp
from trame.ui.vuetify3 import SinglePageLayout
from vtkmodules.vtkCommonDataModel import vtkPlane
from vtkmodules.vtkFiltersCore import vtkClipPolyData, vtkContourFilter, vtkCutter
from vtkmodules.vtkFiltersCore import (
vtkAppendPolyData,
vtkClipPolyData,
vtkContourFilter,
vtkCutter,
)
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource
from vtkmodules.vtkRenderingCore import (
vtkActor,
Expand All @@ -44,27 +51,27 @@ def __init__(self, server=None):

self.top = colormaps.ColormapConfig(
self.server,
mapper=self.contour_mapper,
mapper=self.mapper,
data_array_fn=self.get_data_array,
).set_data_array("RTData", self.get_data_array, "points")
).set_data_array("RTData", self.get_data_array, "point")

self.right = colormaps.ColormapConfig(
self.server,
mapper=self.contour_mapper,
mapper=self.mapper,
data_array_fn=self.get_data_array,
).set_data_array("RTData", self.get_data_array, "points")
).set_data_array("RTData", self.get_data_array, "point")

self.left = colormaps.ColormapConfig(
self.server,
mapper=self.slice_mapper,
mapper=self.mapper,
data_array_fn=self.get_data_array,
).set_data_array("RTData", self.get_data_array, "points")
).set_data_array("RTData", self.get_data_array, "point")

self.bottom = colormaps.ColormapConfig(
self.server,
mapper=self.slice_mapper,
mapper=self.mapper,
data_array_fn=self.get_data_array,
).set_data_array("RTData", self.get_data_array, "points")
).set_data_array("RTData", self.get_data_array, "point")

# Auto render when mapper update
for colormap in [self.top, self.right, self.bottom, self.left]:
Expand Down Expand Up @@ -94,12 +101,6 @@ def _setup_vtk(self):
clip_contour.SetClipFunction(clip_plane)
clip_contour.Update()

contour_mapper = vtkPolyDataMapper()
contour_mapper.SetInputConnection(clip_contour.GetOutputPort())

contour_actor = vtkActor()
contour_actor.SetMapper(contour_mapper)

# --- Slice plane (perpendicular to clip — shows scalar gradient) ---
slice_plane = vtkPlane()
slice_plane.SetOrigin(0, 0, 0)
Expand All @@ -110,16 +111,21 @@ def _setup_vtk(self):
slicer.SetCutFunction(slice_plane)
slicer.Update()

slice_mapper = vtkPolyDataMapper()
slice_mapper.SetInputConnection(slicer.GetOutputPort())
# --- Merge contour + slice into one mapper ---
append = vtkAppendPolyData()
append.AddInputConnection(clip_contour.GetOutputPort())
append.AddInputConnection(slicer.GetOutputPort())
append.Update()

mapper = vtkPolyDataMapper()
mapper.SetInputConnection(append.GetOutputPort())

slice_actor = vtkActor()
slice_actor.SetMapper(slice_mapper)
actor = vtkActor()
actor.SetMapper(mapper)

# --- Renderer ---
renderer = vtkRenderer()
renderer.AddActor(contour_actor)
renderer.AddActor(slice_actor)
renderer.AddActor(actor)
renderer.SetBackground(0.15, 0.15, 0.15)
renderer.ResetCamera()

Expand All @@ -134,8 +140,7 @@ def _setup_vtk(self):
# Capture variables on class
self.wavelet = wavelet
self.render_window = render_window
self.contour_mapper = contour_mapper
self.slice_mapper = slice_mapper
self.mapper = mapper

def get_data_array(self):
"""Return the active scalar array from the wavelet output."""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "trame-colormaps"
version = "0.1.0"
version = "1.0.0"
description = "Self-contained colormap module for VTK color transfer functions in Trame apps"
readme = "README.md"
license = {text = "BSD-3-Clause"}
Expand Down
5 changes: 5 additions & 0 deletions src/trame/dataclasses/colormaps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""Trame dataclass entry point for trame-colormaps.

Re-exports ColormapConfig for use via ``from trame.dataclasses import colormaps``.
"""

from trame_colormaps.dataclasses import ColormapConfig

__all__ = ["ColormapConfig"]
7 changes: 7 additions & 0 deletions src/trame/widgets/colormaps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""Trame widget entry point for trame-colormaps.

Re-exports all widget classes (ColorMapEditor, HorizontalScalarBar,
VerticalScalarBar) and registers the module with the trame server.
"""

from trame_colormaps.widgets import * # noqa: F403


def initialize(server):
"""Called automatically by trame when this widget module is imported."""
Comment thread
jourdain marked this conversation as resolved.
from trame_colormaps import module # noqa: PLC0415

server.enable_module(module)
15 changes: 8 additions & 7 deletions src/trame_colormaps/core/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Symlog (symmetric log) mapping
- Discrete symlog banding

These functions are designed to be called by the ColormapController but
These functions are designed to be called by ColormapConfig but
have no framework dependencies beyond VTK and numpy.
"""

Expand Down Expand Up @@ -429,7 +429,8 @@ def apply_symlog(ctf, linthresh, linear_rgb_points=None, n_samples=256):
n_samples: Number of uniform samples in symlog space for building the CTF.

Returns:
Base64 PNG colorbar image string, or None if the range is zero.
Tuple of (lut_img_h, lut_img_v) base64 PNG strings, or None if
the range is zero.
"""
x_min, x_max = ctf.GetRange()
data_range = x_max - x_min
Expand Down Expand Up @@ -516,13 +517,13 @@ def apply_discrete_symlog(ctf, linthresh, linear_rgb_points, n_sub=1, n_samples=
n_samples: Number of uniform samples in symlog space for building the CTF.

Returns:
Tuple of (display_rgb_points, discrete_tick_data, lut_img) or
(None, None, None) if the range is zero.
Tuple of (display_rgb_points, discrete_tick_data, lut_img_h,
lut_img_v) or (None, None, None, None) if the range is zero.
"""
x_min, x_max = ctf.GetRange()
data_range = x_max - x_min
if data_range == 0:
return None, None, None
return None, None, None, None

def symlog(v):
"""Symmetric log: sign(v) * log10(1 + |v|/linthresh)."""
Expand Down Expand Up @@ -565,14 +566,14 @@ def symlog(v):
boundaries = sorted(b for b in boundaries if x_min <= b <= x_max)

if len(boundaries) < 2:
return None, None, None
return None, None, None, None

# Symlog range for normalization
s_min = float(symlog(x_min))
s_max = float(symlog(x_max))
s_range = s_max - s_min
if s_range == 0:
return None, None, None
return None, None, None, None

# Store boundary values and their display positions (%) for tick alignment.
all_tick_data = []
Expand Down
22 changes: 8 additions & 14 deletions src/trame_colormaps/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
scale mode, range, discrete settings, and derived display data (lut_img_h/v, ticks).

Can be used standalone or composed into a larger application config.
When used standalone, instantiate ColormapConfig directly. When composed
into an existing config, pass that config object to ColormapController
instead.
"""

import math
Expand Down Expand Up @@ -49,14 +46,9 @@
class ColormapConfig(StateDataModel):
"""Reactive state model for a single colormap instance.

All fields are synced to the Trame client via ``Sync``.
Use this standalone when the colormaps module owns its own state,
or compose these same fields into a larger application config and
pass that object to ``ColormapController`` instead.

Fields fall into three groups:

**User-settable** — bound to UI controls, read by the controller:
**User-settable** — bound to UI controls, trigger reactive updates:

- ``active_presets``: List of preset names available in the picker.
- ``preset``: Active color preset name.
Expand All @@ -72,7 +64,7 @@ class ColormapConfig(StateDataModel):
- ``override_range``: When True, use the manual strings instead of
the data-derived range.

**Derived** — written by the controller, consumed by UI:
**Derived** — computed internally, consumed by UI:

- ``color_range``: Active (min, max) as floats, either from data or
parsed from the manual strings.
Expand All @@ -93,9 +85,11 @@ class ColormapConfig(StateDataModel):

- ``menu``: Whether the preset control panel is open.
- ``search``: Preset search/filter text.
- ``orientation``: Colorbar orientation (``"horizontal"`` or ``"vertical"``).
- ``mapper_change``: Server-only counter incremented on each mapper update.
"""

# --- User-settable (bound to UI, read by controller) ---
# --- User-settable (bound to UI, triggers reactive updates) ---
active_presets: list[str] = Sync(list, DEFAULT_PRESETS)
preset: str = Sync(str, "BuGnYl")
invert: bool = Sync(bool, False)
Expand All @@ -108,7 +102,7 @@ class ColormapConfig(StateDataModel):
color_value_max: str = Sync(str, "1")
override_range: bool = Sync(bool, False)

# --- Derived (written by controller, read by UI) ---
# --- Derived (computed internally, read by UI) ---
color_value_min_valid: bool = Sync(bool, True)
color_value_max_valid: bool = Sync(bool, True)
color_range: list[float] = Sync(tuple[float, float], (0, 1))
Expand Down Expand Up @@ -183,7 +177,7 @@ def _build_lut_lists(self, active_presets):
"""Rebuild the sorted preset picker lists from active_presets.

Filters COLORBAR_CACHE by the given preset names and populates
``config.luts_normal`` and ``config.luts_inverted``.
``self.luts_normal`` and ``self.luts_inverted``.

Args:
active_presets: List of preset names to include.
Expand Down Expand Up @@ -383,7 +377,7 @@ def update_color_preset(
"""Apply a color preset with the specified scale and discrete settings.

Args:
name: Preset name (must exist in PRESET_REGISTRY).
name: Preset name (must exist in COLORBAR_CACHE).
invert: Whether to invert the transfer function.
log_scale: Scale mode — ``"linear"``, ``"log"``, or ``"symlog"``.
discrete_log: Enable discrete (stepped) color banding.
Expand Down
10 changes: 10 additions & 0 deletions src/trame_colormaps/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Trame module definition for trame-colormaps.

This package has no custom client-side components — it uses only
standard vuetify3 and html widgets. The module stub exists so that
``trame.widgets.colormaps.initialize(server)`` works without error.
"""


def setup(app, **kwargs):
"""No-op — nothing to register on the client side."""
12 changes: 5 additions & 7 deletions src/trame_colormaps/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def buttons(name):
{
"icon": (
f"{name}.use_log_scale === 'log' ? 'mdi-math-log' : "
"{name}.use_log_scale === 'symlog' ? 'mdi-sine-wave' : 'mdi-stairs'"
f"{name}.use_log_scale === 'symlog' ? 'mdi-sine-wave' : 'mdi-stairs'"
),
"click": (
f"{name}.use_log_scale = {name}.use_log_scale === 'linear' ? 'log' : "
Expand Down Expand Up @@ -330,13 +330,13 @@ def __init__(self, name, popup_location="top", **kwargs):
"width:100%;"
"transform:translateY(-50%);"
"display:flex;"
"flex-direction:column;"
"flex-direction:row;"
"align-items:center;`",
),
):
html.Div(
style=(
"`height:1.5px;width:30%;background:${tick.color};`",
"`width:100%;height:1.5px;background:${tick.color};`",
),
)
html.Span(
Expand All @@ -347,12 +347,10 @@ def __init__(self, name, popup_location="top", **kwargs):
"white-space: nowrap;"
"color: ${tick.color};"
"writing-mode:vertical-lr;"
"transform: rotate(180deg);`",
"transform: rotate(180deg);"
"padding-left:2px;`",
),
)
html.Div(
style=("`height:1.5px;flex:1;background:${tick.color};`",),
)
# Min label at bottom
html.Div(
f"{{{{ {name}.color_range && {name}.color_range[0] != null"
Expand Down
Loading
Loading