Source code for movement.napari.layer_styles
"""Dataclasses containing layer styles for napari."""
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
from napari.layers import Shapes
from napari.utils.color import ColorValue
from napari.utils.colormaps import ensure_colormap
DEFAULT_COLORMAP = "turbo"
[docs]
@dataclass
class LayerStyle:
"""Base class for napari layer styles."""
name: str
visible: bool = True
blending: str = "translucent"
[docs]
def as_kwargs(self) -> dict:
"""Return the style properties as a dictionary of kwargs."""
return self.__dict__
[docs]
@dataclass
class PointsStyle(LayerStyle):
"""Style properties for a napari Points layer."""
symbol: str = "disc"
size: int = 10
border_width: int = 0
face_color: str | None = None
face_color_cycle: list[tuple] | None = None
face_colormap: str = DEFAULT_COLORMAP
text: dict = field(
default_factory=lambda: {
"visible": False,
"anchor": "lower_left",
# it actually displays the text in the lower
# _right_ corner of the marker
"translation": 5, # pixels
}
)
[docs]
def set_color_by(
self,
property: str,
properties_df: pd.DataFrame,
cmap: str | None = None,
) -> None:
"""Color markers and text by a column in the properties DataFrame.
Parameters
----------
property
The column name in the properties DataFrame to color by.
properties_df
The properties DataFrame containing the data to color by.
It should contain the column specified in `property`.
cmap
The name of the colormap to use, otherwise use the face_colormap.
"""
# Set points and text to be colored by selected property
self.face_color = property
if "color" in self.text:
self.text["color"].update({"feature": property})
else:
self.text["color"] = {"feature": property}
# Get color cycle
if cmap is None:
cmap = self.face_colormap
n_colors = len(properties_df[property].unique())
color_cycle = _sample_colormap(n_colors, cmap)
# Set color cycle for points and text
self.face_color_cycle = color_cycle
self.text["color"].update({"colormap": color_cycle})
[docs]
def set_text_by(self, property: str) -> None:
"""Set the text property for the points layer.
Parameters
----------
property
The column name in the properties DataFrame to use for text.
"""
self.text["string"] = property
[docs]
@dataclass
class TracksStyle(LayerStyle):
"""Style properties for a napari Tracks layer."""
blending: str = "opaque"
colormap: str = DEFAULT_COLORMAP
color_by: str | None = "track_id"
head_length: int = 0 # frames
tail_length: int = 30 # frames
tail_width: int = 2
[docs]
def set_color_by(self, property: str, cmap: str | None = None) -> None:
"""Color tracks by a column in the properties DataFrame.
Parameters
----------
property
The column name in the properties DataFrame to color by.
cmap
The name of the colormap to use. If not specified,
DEFAULT_COLORMAP is used.
"""
self.color_by = property
# Overwrite colormap if specified
if cmap is not None:
self.colormap = cmap
[docs]
@dataclass
class BoxesStyle(LayerStyle):
"""Style properties for a napari Shapes layer containing bounding boxes."""
edge_width: int = 3
opacity: float = 1.0
shape_type: str = "rectangle"
face_color: str = "#FFFFFF00" # transparent face
edge_colormap: str = DEFAULT_COLORMAP
text: dict = field(
default_factory=lambda: {
"visible": True, # default visible text for bboxes
"anchor": "lower_left",
"translation": 5, # pixels
}
)
[docs]
def set_color_by(
self,
property: str,
properties_df: pd.DataFrame,
cmap: str | None = None,
) -> None:
"""Color boxes and text by chosen column in the properties DataFrame.
Parameters
----------
property
The column name in the properties DataFrame to color shape edges
and associated text by.
properties_df
The properties DataFrame containing the data for generating the
colormap.
cmap
The name of the colormap to use, otherwise use the edge_colormap.
Notes
-----
The input property is expected to be a column in the properties
dataframe and it is used to define the color of the text. A factorized
version of the property ("<property>_factorized") is used to define the
edges color, and is also expected to be present in the properties
dataframe.
"""
# Compute color cycle based on property
if cmap is None:
cmap = self.edge_colormap
n_colors = len(properties_df[property].unique())
color_cycle = _sample_colormap(n_colors, cmap)
# Set color for edges and text
self.edge_color = property + "_factorized"
self.edge_color_cycle = color_cycle
self.text["color"] = {"feature": property}
self.text["color"].update({"colormap": color_cycle})
[docs]
def set_text_by(self, property: str) -> None:
"""Set the text property for the boxes layer.
Parameters
----------
property
The column name in the properties DataFrame to use for text.
"""
self.text["string"] = property
[docs]
@dataclass
class RegionsStyle(LayerStyle):
"""Style properties for a napari Shapes layer containing regions.
The same ``color`` is applied to faces, edges, and text.
The face color alpha is hardcoded to 0.25, while edges and text are
fully opaque. Overall layer opacity is set to 1.0.
"""
name: str = "Regions"
color: str | tuple = "red"
edge_width: float = 5.0
opacity: float = 1.0
text: dict = field(
default_factory=lambda: {
"visible": False,
"anchor": "center",
}
)
@property
def face_color(self) -> ColorValue:
"""Return the face color with transparency applied."""
color = ColorValue(self.color)
color[-1] = 0.25 # this is hardcoded for now
return color
@property
def edge_and_text_color(self) -> ColorValue:
"""Return the opaque color for edges and text."""
color = ColorValue(self.color)
color[-1] = 1.0
return color
[docs]
def set_style_for_new_shapes(self, layer: Shapes) -> None:
"""Set the style that napari will apply to newly drawn shapes.
napari uses current_* properties to style shapes as they are drawn.
"""
layer.current_face_color = self.face_color
layer.current_edge_color = self.edge_and_text_color
layer.current_edge_width = self.edge_width
[docs]
def set_color_all_shapes(self, layer: Shapes) -> None:
"""Set colors on all existing shapes in a napari Shapes layer."""
# Configure text appearance
text_dict = layer.text.dict()
text_dict.update(self.text)
layer.text = text_dict
layer.text.color = self.edge_and_text_color
# Set layer opacity and per-shape face/edge colors
layer.opacity = self.opacity
n_shapes = len(layer.data)
if n_shapes > 0:
layer.face_color = [self.face_color] * n_shapes
layer.edge_color = [self.edge_and_text_color] * n_shapes
layer.edge_width = [self.edge_width] * n_shapes
layer.text.string = "{name}"
layer.text.refresh(layer.features)
self.set_style_for_new_shapes(layer)
def _sample_colormap(n: int, cmap_name: str) -> list[tuple]:
"""Sample n equally-spaced colors from a napari colormap.
This includes the endpoints of the colormap.
"""
cmap = ensure_colormap(cmap_name)
samples = np.linspace(0, len(cmap.colors) - 1, n).astype(int)
return [tuple(cmap.colors[i]) for i in samples]