diff --git a/docs/source/api/graphic_features/SizeSpace.rst b/docs/source/api/graphic_features/SizeSpace.rst new file mode 100644 index 000000000..0bca1ecc8 --- /dev/null +++ b/docs/source/api/graphic_features/SizeSpace.rst @@ -0,0 +1,35 @@ +.. _api.SizeSpace: + +SizeSpace +********* + +========= +SizeSpace +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: SizeSpace_api + + SizeSpace + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: SizeSpace_api + + SizeSpace.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: SizeSpace_api + + SizeSpace.add_event_handler + SizeSpace.block_events + SizeSpace.clear_event_handlers + SizeSpace.remove_event_handler + SizeSpace.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index ea3ce8903..dc88e97d6 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -7,6 +7,7 @@ Graphic Features VertexColors UniformColor UniformSize + SizeSpace Thickness VertexPositions PointsSizesFeature diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index cb924b4dc..4302ab56c 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -31,6 +31,7 @@ Properties LineGraphic.offset LineGraphic.right_click_menu LineGraphic.rotation + LineGraphic.size_space LineGraphic.supported_events LineGraphic.thickness LineGraphic.visible diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 8f15e827a..83e734c61 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -31,6 +31,7 @@ Properties ScatterGraphic.offset ScatterGraphic.right_click_menu ScatterGraphic.rotation + ScatterGraphic.size_space ScatterGraphic.sizes ScatterGraphic.supported_events ScatterGraphic.visible diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 4f9013425..a1915bbe9 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -2,6 +2,7 @@ VertexColors, UniformColor, UniformSize, + SizeSpace, Thickness, VertexPositions, PointsSizesFeature, @@ -42,6 +43,7 @@ "VertexColors", "UniformColor", "UniformSize", + "SizeSpace", "Thickness", "VertexPositions", "PointsSizesFeature", diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index ee7927a36..c4e153a31 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -182,6 +182,27 @@ def set_value(self, graphic, value: float | int): self._call_event_handlers(event) +# manages the coordinate space for scatter/line +class SizeSpace(GraphicFeature): + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + if "Line" in graphic.world_object.material.__class__.__name__: + graphic.world_object.material.thickness_space = value + else: + graphic.world_object.material.size_space = value + self._value = value + + event = FeatureEvent(type="size_space", info={"value": value}) + self._call_event_handlers(event) + + class VertexPositions(BufferManager): """ +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 3727087cc..565a4cd98 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -10,6 +10,7 @@ UniformColor, VertexCmap, PointsSizesFeature, + SizeSpace, ) @@ -54,6 +55,19 @@ def cmap(self, name: str): self._cmap[:] = name + @property + def size_space(self): + """ + The coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + + See https://docs.pygfx.org/stable/_autosummary/utils/utils/enums/pygfx.utils.enums.CoordSpace.html#pygfx.utils.enums.CoordSpace for available options. + """ + return self._size_space.value + + @size_space.setter + def size_space(self, value: str): + self._size_space.set_value(self, value) + def __init__( self, data: Any, @@ -63,6 +77,7 @@ def __init__( cmap: str | VertexCmap = None, cmap_transform: np.ndarray = None, isolated_buffer: bool = True, + size_space: str = "screen", *args, **kwargs, ): @@ -132,6 +147,7 @@ def __init__( self._colors, cmap_name=None, transform=None, alpha=alpha ) + self._size_space = SizeSpace(size_space) super().__init__(*args, **kwargs) def unshare_property(self, property: str): diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 1574587fe..8fe505ba9 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -6,11 +6,11 @@ from ._positions_base import PositionsGraphic from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector -from ._features import Thickness +from ._features import Thickness, SizeSpace class LineGraphic(PositionsGraphic): - _features = {"data", "colors", "cmap", "thickness"} + _features = {"data", "colors", "cmap", "thickness", "size_space"} def __init__( self, @@ -22,6 +22,7 @@ def __init__( cmap: str = None, cmap_transform: np.ndarray | Iterable = None, isolated_buffer: bool = True, + size_space: str = "screen", **kwargs, ): """ @@ -53,6 +54,9 @@ def __init__( cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap + size_space: str, default "screen" + coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + **kwargs passed to Graphic @@ -66,6 +70,7 @@ def __init__( cmap=cmap, cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, + size_space=size_space, **kwargs, ) @@ -83,10 +88,14 @@ def __init__( color_mode="uniform", color=self.colors, pick_write=True, + thickness_space=self.size_space, ) else: material = MaterialCls( - thickness=self.thickness, color_mode="vertex", pick_write=True + thickness=self.thickness, + color_mode="vertex", + pick_write=True, + thickness_space=self.size_space, ) geometry = pygfx.Geometry( positions=self._data.buffer, colors=self._colors.buffer diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 39d815c95..8dad7cd43 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -4,11 +4,11 @@ import pygfx from ._positions_base import PositionsGraphic -from ._features import PointsSizesFeature, UniformSize +from ._features import PointsSizesFeature, UniformSize, SizeSpace class ScatterGraphic(PositionsGraphic): - _features = {"data", "sizes", "colors", "cmap"} + _features = {"data", "sizes", "colors", "cmap", "size_space"} def __init__( self, @@ -21,6 +21,7 @@ def __init__( isolated_buffer: bool = True, sizes: float | np.ndarray | Iterable[float] = 1, uniform_size: bool = False, + size_space: str = "screen", **kwargs, ): """ @@ -60,6 +61,9 @@ def __init__( if True, uses a uniform buffer for the scatter point sizes, basically saves GPU VRAM when all scatter points are the same size + size_space: str, default "screen" + coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + kwargs passed to Graphic @@ -73,6 +77,7 @@ def __init__( cmap=cmap, cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, + size_space=size_space, **kwargs, ) @@ -80,6 +85,7 @@ def __init__( geo_kwargs = {"positions": self._data.buffer} material_kwargs = {"pick_write": True} + self._size_space = SizeSpace(size_space) if uniform_color: material_kwargs["color_mode"] = "uniform" @@ -97,6 +103,7 @@ def __init__( self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) geo_kwargs["sizes"] = self.sizes.buffer + material_kwargs["size_space"] = self.size_space world_object = pygfx.Points( pygfx.Geometry(**geo_kwargs), material=pygfx.PointsMaterial(**material_kwargs), diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 81403c06b..b76ece2ca 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -443,3 +443,45 @@ def test_thickness(thickness): else: assert isinstance(graphic.world_object.material, pygfx.LineMaterial) + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("size_space", ["screen", "world", "model"]) +def test_size_space(graphic_type, size_space): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["size_space"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if size_space is None: + size_space = "screen" # default space + + # size_space is really an alias for pygfx.utils.enums.CoordSpace + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + + # test getter + assert graphic.world_object.material.thickness_space == size_space + assert graphic.size_space == size_space + + # test setter + graphic.size_space = "world" + assert graphic.size_space == "world" + assert graphic.world_object.material.thickness_space == "world" + + elif graphic_type == "scatter": + + # test getter + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + assert graphic.world_object.material.size_space == size_space + assert graphic.size_space == size_space + + # test setter + graphic.size_space = "world" + assert graphic.size_space == "world" + assert graphic.world_object.material.size_space == "world"