Skip to content

GSP Matplotlib API Reference

The GSP Matplotlib backend provides rendering using the Matplotlib library, enabling integration with the Python scientific visualization ecosystem.

Overview

gsp_matplotlib

GSP Matplotlib package initialization.

Renderer Module

The renderer module contains the main Matplotlib renderer implementation and specialized renderers for different visual types.

gsp_matplotlib.renderer

Matplotlib Renderer Package.

MatplotlibRenderer

Bases: gsp.types.renderer_base.RendererBase

Matplotlib-based renderer for GSP visuals.

This renderer implements the GSP rendering interface using Matplotlib as the backend. It creates and manages a Matplotlib figure with multiple axes for different viewports, and renders various visual types (pixels, points, paths, markers, segments, texts) into them.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class MatplotlibRenderer(RendererBase):
    """Matplotlib-based renderer for GSP visuals.

    This renderer implements the GSP rendering interface using Matplotlib as the backend.
    It creates and manages a Matplotlib figure with multiple axes for different viewports,
    and renders various visual types (pixels, points, paths, markers, segments, texts) into them.
    """

    def __init__(self, canvas: Canvas):
        """Initialize the Matplotlib renderer.

        Args:
            canvas: The canvas defining the rendering surface dimensions and DPI.
        """
        self.canvas = canvas
        # Store mapping of viewport UUIDs to axes
        self._axes: dict[str, matplotlib.axes.Axes] = {}
        # Store mapping of visual UUIDs to matplotlib artists
        self._artists: dict[str, matplotlib.artist.Artist] = {}
        # Store group count per visual UUID
        self._group_count: dict[str, int] = {}

        # Create a figure
        figure_width = canvas.get_width() / canvas.get_dpi()
        figure_height = canvas.get_height() / canvas.get_dpi()
        self._figure: matplotlib.figure.Figure = matplotlib.pyplot.figure(figsize=(figure_width, figure_height), dpi=canvas.get_dpi())
        assert self._figure.canvas.manager is not None, "matplotlib figure canvas manager is None"
        self._figure.canvas.manager.set_window_title("Matplotlib")

    def get_canvas(self) -> Canvas:
        """Get the canvas associated with this renderer.

        Returns:
            The canvas instance.
        """
        return self.canvas

    def close(self) -> None:
        """Close the renderer and release resources.

        Stops the Matplotlib event loop and closes the figure.
        """
        # warnings.warn(f"Closing NetworkRenderer does not release any resources.", UserWarning)
        # stop the event loop if any - thus .show(block=True) will return
        self._figure.canvas.stop_event_loop()
        # close the figure
        matplotlib.pyplot.close(self._figure)
        self._figure = None  # type: ignore

    def show(self) -> None:
        """Display the rendered figure in an interactive window.

        This method shows the Matplotlib figure. It does nothing when running
        in test mode (GSP_TEST environment variable set to "True").
        """
        # handle non-interactive mode for tests
        in_test = os.environ.get("GSP_TEST") == "True"
        if in_test:
            return

        matplotlib.pyplot.show()

    def render(
        self,
        viewports: Sequence[Viewport],
        visuals: Sequence[VisualBase],
        model_matrices: Sequence[TransBuf],
        cameras: Sequence[Camera],
        return_image: bool = True,
        image_format: str = "png",
    ) -> bytes:
        """Render the scene to an image.

        Args:
            viewports: Sequence of viewport regions to render into.
            visuals: Sequence of visual elements to render.
            model_matrices: Sequence of model transformation matrices for each visual.
            cameras: Sequence of cameras defining view and projection for each visual.
            return_image: Whether to return the rendered image as bytes.
            image_format: Format for the output image (e.g., "png", "jpg").

        Returns:
            The rendered image as bytes in the specified format, or empty bytes if return_image is False.

        Raises:
            AssertionError: If the sequences don't all have the same length.
        """
        # =============================================================================
        # Sanity checks
        # =============================================================================

        assert (
            len(viewports) == len(visuals) == len(model_matrices) == len(cameras)
        ), f"All length MUST be equal. Mismatched lengths: {len(viewports)} viewports, {len(visuals)} visuals, {len(model_matrices)} model matrices, {len(cameras)} cameras"

        # =============================================================================
        # Create all the axes if needed
        # =============================================================================
        for viewport in viewports:
            if viewport.get_uuid() in self._axes:
                continue
            axes_rect = (
                viewport.get_x() / self.canvas.get_width(),
                viewport.get_y() / self.canvas.get_height(),
                viewport.get_width() / self.canvas.get_width(),
                viewport.get_height() / self.canvas.get_height(),
            )
            axes: matplotlib.axes.Axes = matplotlib.pyplot.axes(axes_rect)
            # this should be -1 to 1 - from normalized device coordinates - https://en.wikipedia.org/wiki/Graphics_pipeline
            # - https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
            axes.set_xlim(-1, 1)
            axes.set_ylim(-1, 1)
            # hide the borders
            axes.axis("off")
            # store axes for this viewport
            self._axes[viewport.get_uuid()] = axes

        # =============================================================================
        # Render each visual
        # =============================================================================

        for viewport, visual, model_matrix, camera in zip(viewports, visuals, model_matrices, cameras):
            self._render_visual(viewport, visual, model_matrix, camera)

        # =============================================================================
        # Render the output image
        # =============================================================================
        image_png_data = b""

        # honor return_image option
        if return_image:
            # Render the image to a PNG buffer
            image_png_buffer = io.BytesIO()
            self._figure.savefig(image_png_buffer, format=image_format, dpi=self.canvas.get_dpi())

            image_png_buffer.seek(0)
            image_png_data = image_png_buffer.getvalue()
            image_png_buffer.close()

        return image_png_data

    def _render_visual(self, viewport: Viewport, visual: VisualBase, model_matrix: TransBuf, camera: Camera):
        """Render a single visual in a given viewport using the specified camera."""
        if isinstance(visual, Image):
            from gsp_matplotlib.renderer.matplotlib_renderer_image import RendererImage

            RendererImage.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Pixels):
            from gsp_matplotlib.renderer.matplotlib_renderer_pixels import RendererPixels

            RendererPixels.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Points):
            from gsp_matplotlib.renderer.matplotlib_renderer_points import RendererPoints

            RendererPoints.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Paths):
            from gsp_matplotlib.renderer.matplotlib_renderer_paths import RendererPaths

            RendererPaths.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Markers):
            from gsp_matplotlib.renderer.matplotlib_renderer_markers import RendererMarkers

            RendererMarkers.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Segments):
            from gsp_matplotlib.renderer.matplotlib_renderer_segments import RendererSegments

            RendererSegments.render(self, viewport, visual, model_matrix, camera)

        elif isinstance(visual, Texts):
            from gsp_matplotlib.renderer.matplotlib_renderer_texts import RendererTexts

            RendererTexts.render(self, viewport, visual, model_matrix, camera)
        else:
            raise NotImplementedError(f"Rendering for visual type {type(visual)} is not implemented.")

    # =============================================================================
    #
    # =============================================================================

    def get_mpl_axes_for_viewport(self, viewport: Viewport) -> matplotlib.axes.Axes:
        """Get the Matplotlib axes associated with a viewport.

        Args:
            viewport: The viewport to get axes for.

        Returns:
            The Matplotlib Axes object for the given viewport.
        """
        return self._axes[viewport.get_uuid()]

    def get_mpl_figure(self) -> matplotlib.figure.Figure:
        """Get the underlying Matplotlib figure.

        Returns:
            The Matplotlib Figure object used by this renderer.
        """
        return self._figure

__init__(canvas: Canvas)

Initialize the Matplotlib renderer.

Parameters:

Name Type Description Default
canvas gsp.core.canvas.Canvas

The canvas defining the rendering surface dimensions and DPI.

required
Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(self, canvas: Canvas):
    """Initialize the Matplotlib renderer.

    Args:
        canvas: The canvas defining the rendering surface dimensions and DPI.
    """
    self.canvas = canvas
    # Store mapping of viewport UUIDs to axes
    self._axes: dict[str, matplotlib.axes.Axes] = {}
    # Store mapping of visual UUIDs to matplotlib artists
    self._artists: dict[str, matplotlib.artist.Artist] = {}
    # Store group count per visual UUID
    self._group_count: dict[str, int] = {}

    # Create a figure
    figure_width = canvas.get_width() / canvas.get_dpi()
    figure_height = canvas.get_height() / canvas.get_dpi()
    self._figure: matplotlib.figure.Figure = matplotlib.pyplot.figure(figsize=(figure_width, figure_height), dpi=canvas.get_dpi())
    assert self._figure.canvas.manager is not None, "matplotlib figure canvas manager is None"
    self._figure.canvas.manager.set_window_title("Matplotlib")

get_canvas() -> Canvas

Get the canvas associated with this renderer.

Returns:

Type Description
gsp.core.canvas.Canvas

The canvas instance.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
65
66
67
68
69
70
71
def get_canvas(self) -> Canvas:
    """Get the canvas associated with this renderer.

    Returns:
        The canvas instance.
    """
    return self.canvas

close() -> None

Close the renderer and release resources.

Stops the Matplotlib event loop and closes the figure.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
73
74
75
76
77
78
79
80
81
82
83
def close(self) -> None:
    """Close the renderer and release resources.

    Stops the Matplotlib event loop and closes the figure.
    """
    # warnings.warn(f"Closing NetworkRenderer does not release any resources.", UserWarning)
    # stop the event loop if any - thus .show(block=True) will return
    self._figure.canvas.stop_event_loop()
    # close the figure
    matplotlib.pyplot.close(self._figure)
    self._figure = None  # type: ignore

show() -> None

Display the rendered figure in an interactive window.

This method shows the Matplotlib figure. It does nothing when running in test mode (GSP_TEST environment variable set to "True").

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
85
86
87
88
89
90
91
92
93
94
95
96
def show(self) -> None:
    """Display the rendered figure in an interactive window.

    This method shows the Matplotlib figure. It does nothing when running
    in test mode (GSP_TEST environment variable set to "True").
    """
    # handle non-interactive mode for tests
    in_test = os.environ.get("GSP_TEST") == "True"
    if in_test:
        return

    matplotlib.pyplot.show()

render(viewports: Sequence[Viewport], visuals: Sequence[VisualBase], model_matrices: Sequence[TransBuf], cameras: Sequence[Camera], return_image: bool = True, image_format: str = 'png') -> bytes

Render the scene to an image.

Parameters:

Name Type Description Default
viewports typing.Sequence[gsp.core.viewport.Viewport]

Sequence of viewport regions to render into.

required
visuals typing.Sequence[gsp.types.visual_base.VisualBase]

Sequence of visual elements to render.

required
model_matrices typing.Sequence[gsp.types.transbuf.TransBuf]

Sequence of model transformation matrices for each visual.

required
cameras typing.Sequence[gsp.core.camera.Camera]

Sequence of cameras defining view and projection for each visual.

required
return_image bool

Whether to return the rendered image as bytes.

True
image_format str

Format for the output image (e.g., "png", "jpg").

'png'

Returns:

Type Description
bytes

The rendered image as bytes in the specified format, or empty bytes if return_image is False.

Raises:

Type Description
AssertionError

If the sequences don't all have the same length.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def render(
    self,
    viewports: Sequence[Viewport],
    visuals: Sequence[VisualBase],
    model_matrices: Sequence[TransBuf],
    cameras: Sequence[Camera],
    return_image: bool = True,
    image_format: str = "png",
) -> bytes:
    """Render the scene to an image.

    Args:
        viewports: Sequence of viewport regions to render into.
        visuals: Sequence of visual elements to render.
        model_matrices: Sequence of model transformation matrices for each visual.
        cameras: Sequence of cameras defining view and projection for each visual.
        return_image: Whether to return the rendered image as bytes.
        image_format: Format for the output image (e.g., "png", "jpg").

    Returns:
        The rendered image as bytes in the specified format, or empty bytes if return_image is False.

    Raises:
        AssertionError: If the sequences don't all have the same length.
    """
    # =============================================================================
    # Sanity checks
    # =============================================================================

    assert (
        len(viewports) == len(visuals) == len(model_matrices) == len(cameras)
    ), f"All length MUST be equal. Mismatched lengths: {len(viewports)} viewports, {len(visuals)} visuals, {len(model_matrices)} model matrices, {len(cameras)} cameras"

    # =============================================================================
    # Create all the axes if needed
    # =============================================================================
    for viewport in viewports:
        if viewport.get_uuid() in self._axes:
            continue
        axes_rect = (
            viewport.get_x() / self.canvas.get_width(),
            viewport.get_y() / self.canvas.get_height(),
            viewport.get_width() / self.canvas.get_width(),
            viewport.get_height() / self.canvas.get_height(),
        )
        axes: matplotlib.axes.Axes = matplotlib.pyplot.axes(axes_rect)
        # this should be -1 to 1 - from normalized device coordinates - https://en.wikipedia.org/wiki/Graphics_pipeline
        # - https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
        axes.set_xlim(-1, 1)
        axes.set_ylim(-1, 1)
        # hide the borders
        axes.axis("off")
        # store axes for this viewport
        self._axes[viewport.get_uuid()] = axes

    # =============================================================================
    # Render each visual
    # =============================================================================

    for viewport, visual, model_matrix, camera in zip(viewports, visuals, model_matrices, cameras):
        self._render_visual(viewport, visual, model_matrix, camera)

    # =============================================================================
    # Render the output image
    # =============================================================================
    image_png_data = b""

    # honor return_image option
    if return_image:
        # Render the image to a PNG buffer
        image_png_buffer = io.BytesIO()
        self._figure.savefig(image_png_buffer, format=image_format, dpi=self.canvas.get_dpi())

        image_png_buffer.seek(0)
        image_png_data = image_png_buffer.getvalue()
        image_png_buffer.close()

    return image_png_data

get_mpl_axes_for_viewport(viewport: Viewport) -> matplotlib.axes.Axes

Get the Matplotlib axes associated with a viewport.

Parameters:

Name Type Description Default
viewport gsp.core.viewport.Viewport

The viewport to get axes for.

required

Returns:

Type Description
matplotlib.axes.Axes

The Matplotlib Axes object for the given viewport.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
215
216
217
218
219
220
221
222
223
224
def get_mpl_axes_for_viewport(self, viewport: Viewport) -> matplotlib.axes.Axes:
    """Get the Matplotlib axes associated with a viewport.

    Args:
        viewport: The viewport to get axes for.

    Returns:
        The Matplotlib Axes object for the given viewport.
    """
    return self._axes[viewport.get_uuid()]

get_mpl_figure() -> matplotlib.figure.Figure

Get the underlying Matplotlib figure.

Returns:

Type Description
matplotlib.figure.Figure

The Matplotlib Figure object used by this renderer.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
226
227
228
229
230
231
232
def get_mpl_figure(self) -> matplotlib.figure.Figure:
    """Get the underlying Matplotlib figure.

    Returns:
        The Matplotlib Figure object used by this renderer.
    """
    return self._figure

Matplotlib Renderer

gsp_matplotlib.renderer.matplotlib_renderer

Matplotlib renderer for GSP visuals.

MatplotlibRenderer

Bases: gsp.types.renderer_base.RendererBase

Matplotlib-based renderer for GSP visuals.

This renderer implements the GSP rendering interface using Matplotlib as the backend. It creates and manages a Matplotlib figure with multiple axes for different viewports, and renders various visual types (pixels, points, paths, markers, segments, texts) into them.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class MatplotlibRenderer(RendererBase):
    """Matplotlib-based renderer for GSP visuals.

    This renderer implements the GSP rendering interface using Matplotlib as the backend.
    It creates and manages a Matplotlib figure with multiple axes for different viewports,
    and renders various visual types (pixels, points, paths, markers, segments, texts) into them.
    """

    def __init__(self, canvas: Canvas):
        """Initialize the Matplotlib renderer.

        Args:
            canvas: The canvas defining the rendering surface dimensions and DPI.
        """
        self.canvas = canvas
        # Store mapping of viewport UUIDs to axes
        self._axes: dict[str, matplotlib.axes.Axes] = {}
        # Store mapping of visual UUIDs to matplotlib artists
        self._artists: dict[str, matplotlib.artist.Artist] = {}
        # Store group count per visual UUID
        self._group_count: dict[str, int] = {}

        # Create a figure
        figure_width = canvas.get_width() / canvas.get_dpi()
        figure_height = canvas.get_height() / canvas.get_dpi()
        self._figure: matplotlib.figure.Figure = matplotlib.pyplot.figure(figsize=(figure_width, figure_height), dpi=canvas.get_dpi())
        assert self._figure.canvas.manager is not None, "matplotlib figure canvas manager is None"
        self._figure.canvas.manager.set_window_title("Matplotlib")

    def get_canvas(self) -> Canvas:
        """Get the canvas associated with this renderer.

        Returns:
            The canvas instance.
        """
        return self.canvas

    def close(self) -> None:
        """Close the renderer and release resources.

        Stops the Matplotlib event loop and closes the figure.
        """
        # warnings.warn(f"Closing NetworkRenderer does not release any resources.", UserWarning)
        # stop the event loop if any - thus .show(block=True) will return
        self._figure.canvas.stop_event_loop()
        # close the figure
        matplotlib.pyplot.close(self._figure)
        self._figure = None  # type: ignore

    def show(self) -> None:
        """Display the rendered figure in an interactive window.

        This method shows the Matplotlib figure. It does nothing when running
        in test mode (GSP_TEST environment variable set to "True").
        """
        # handle non-interactive mode for tests
        in_test = os.environ.get("GSP_TEST") == "True"
        if in_test:
            return

        matplotlib.pyplot.show()

    def render(
        self,
        viewports: Sequence[Viewport],
        visuals: Sequence[VisualBase],
        model_matrices: Sequence[TransBuf],
        cameras: Sequence[Camera],
        return_image: bool = True,
        image_format: str = "png",
    ) -> bytes:
        """Render the scene to an image.

        Args:
            viewports: Sequence of viewport regions to render into.
            visuals: Sequence of visual elements to render.
            model_matrices: Sequence of model transformation matrices for each visual.
            cameras: Sequence of cameras defining view and projection for each visual.
            return_image: Whether to return the rendered image as bytes.
            image_format: Format for the output image (e.g., "png", "jpg").

        Returns:
            The rendered image as bytes in the specified format, or empty bytes if return_image is False.

        Raises:
            AssertionError: If the sequences don't all have the same length.
        """
        # =============================================================================
        # Sanity checks
        # =============================================================================

        assert (
            len(viewports) == len(visuals) == len(model_matrices) == len(cameras)
        ), f"All length MUST be equal. Mismatched lengths: {len(viewports)} viewports, {len(visuals)} visuals, {len(model_matrices)} model matrices, {len(cameras)} cameras"

        # =============================================================================
        # Create all the axes if needed
        # =============================================================================
        for viewport in viewports:
            if viewport.get_uuid() in self._axes:
                continue
            axes_rect = (
                viewport.get_x() / self.canvas.get_width(),
                viewport.get_y() / self.canvas.get_height(),
                viewport.get_width() / self.canvas.get_width(),
                viewport.get_height() / self.canvas.get_height(),
            )
            axes: matplotlib.axes.Axes = matplotlib.pyplot.axes(axes_rect)
            # this should be -1 to 1 - from normalized device coordinates - https://en.wikipedia.org/wiki/Graphics_pipeline
            # - https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
            axes.set_xlim(-1, 1)
            axes.set_ylim(-1, 1)
            # hide the borders
            axes.axis("off")
            # store axes for this viewport
            self._axes[viewport.get_uuid()] = axes

        # =============================================================================
        # Render each visual
        # =============================================================================

        for viewport, visual, model_matrix, camera in zip(viewports, visuals, model_matrices, cameras):
            self._render_visual(viewport, visual, model_matrix, camera)

        # =============================================================================
        # Render the output image
        # =============================================================================
        image_png_data = b""

        # honor return_image option
        if return_image:
            # Render the image to a PNG buffer
            image_png_buffer = io.BytesIO()
            self._figure.savefig(image_png_buffer, format=image_format, dpi=self.canvas.get_dpi())

            image_png_buffer.seek(0)
            image_png_data = image_png_buffer.getvalue()
            image_png_buffer.close()

        return image_png_data

    def _render_visual(self, viewport: Viewport, visual: VisualBase, model_matrix: TransBuf, camera: Camera):
        """Render a single visual in a given viewport using the specified camera."""
        if isinstance(visual, Image):
            from gsp_matplotlib.renderer.matplotlib_renderer_image import RendererImage

            RendererImage.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Pixels):
            from gsp_matplotlib.renderer.matplotlib_renderer_pixels import RendererPixels

            RendererPixels.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Points):
            from gsp_matplotlib.renderer.matplotlib_renderer_points import RendererPoints

            RendererPoints.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Paths):
            from gsp_matplotlib.renderer.matplotlib_renderer_paths import RendererPaths

            RendererPaths.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Markers):
            from gsp_matplotlib.renderer.matplotlib_renderer_markers import RendererMarkers

            RendererMarkers.render(self, viewport, visual, model_matrix, camera)
        elif isinstance(visual, Segments):
            from gsp_matplotlib.renderer.matplotlib_renderer_segments import RendererSegments

            RendererSegments.render(self, viewport, visual, model_matrix, camera)

        elif isinstance(visual, Texts):
            from gsp_matplotlib.renderer.matplotlib_renderer_texts import RendererTexts

            RendererTexts.render(self, viewport, visual, model_matrix, camera)
        else:
            raise NotImplementedError(f"Rendering for visual type {type(visual)} is not implemented.")

    # =============================================================================
    #
    # =============================================================================

    def get_mpl_axes_for_viewport(self, viewport: Viewport) -> matplotlib.axes.Axes:
        """Get the Matplotlib axes associated with a viewport.

        Args:
            viewport: The viewport to get axes for.

        Returns:
            The Matplotlib Axes object for the given viewport.
        """
        return self._axes[viewport.get_uuid()]

    def get_mpl_figure(self) -> matplotlib.figure.Figure:
        """Get the underlying Matplotlib figure.

        Returns:
            The Matplotlib Figure object used by this renderer.
        """
        return self._figure

__init__(canvas: Canvas)

Initialize the Matplotlib renderer.

Parameters:

Name Type Description Default
canvas gsp.core.canvas.Canvas

The canvas defining the rendering surface dimensions and DPI.

required
Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(self, canvas: Canvas):
    """Initialize the Matplotlib renderer.

    Args:
        canvas: The canvas defining the rendering surface dimensions and DPI.
    """
    self.canvas = canvas
    # Store mapping of viewport UUIDs to axes
    self._axes: dict[str, matplotlib.axes.Axes] = {}
    # Store mapping of visual UUIDs to matplotlib artists
    self._artists: dict[str, matplotlib.artist.Artist] = {}
    # Store group count per visual UUID
    self._group_count: dict[str, int] = {}

    # Create a figure
    figure_width = canvas.get_width() / canvas.get_dpi()
    figure_height = canvas.get_height() / canvas.get_dpi()
    self._figure: matplotlib.figure.Figure = matplotlib.pyplot.figure(figsize=(figure_width, figure_height), dpi=canvas.get_dpi())
    assert self._figure.canvas.manager is not None, "matplotlib figure canvas manager is None"
    self._figure.canvas.manager.set_window_title("Matplotlib")

close() -> None

Close the renderer and release resources.

Stops the Matplotlib event loop and closes the figure.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
73
74
75
76
77
78
79
80
81
82
83
def close(self) -> None:
    """Close the renderer and release resources.

    Stops the Matplotlib event loop and closes the figure.
    """
    # warnings.warn(f"Closing NetworkRenderer does not release any resources.", UserWarning)
    # stop the event loop if any - thus .show(block=True) will return
    self._figure.canvas.stop_event_loop()
    # close the figure
    matplotlib.pyplot.close(self._figure)
    self._figure = None  # type: ignore

get_canvas() -> Canvas

Get the canvas associated with this renderer.

Returns:

Type Description
gsp.core.canvas.Canvas

The canvas instance.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
65
66
67
68
69
70
71
def get_canvas(self) -> Canvas:
    """Get the canvas associated with this renderer.

    Returns:
        The canvas instance.
    """
    return self.canvas

get_mpl_axes_for_viewport(viewport: Viewport) -> matplotlib.axes.Axes

Get the Matplotlib axes associated with a viewport.

Parameters:

Name Type Description Default
viewport gsp.core.viewport.Viewport

The viewport to get axes for.

required

Returns:

Type Description
matplotlib.axes.Axes

The Matplotlib Axes object for the given viewport.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
215
216
217
218
219
220
221
222
223
224
def get_mpl_axes_for_viewport(self, viewport: Viewport) -> matplotlib.axes.Axes:
    """Get the Matplotlib axes associated with a viewport.

    Args:
        viewport: The viewport to get axes for.

    Returns:
        The Matplotlib Axes object for the given viewport.
    """
    return self._axes[viewport.get_uuid()]

get_mpl_figure() -> matplotlib.figure.Figure

Get the underlying Matplotlib figure.

Returns:

Type Description
matplotlib.figure.Figure

The Matplotlib Figure object used by this renderer.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
226
227
228
229
230
231
232
def get_mpl_figure(self) -> matplotlib.figure.Figure:
    """Get the underlying Matplotlib figure.

    Returns:
        The Matplotlib Figure object used by this renderer.
    """
    return self._figure

render(viewports: Sequence[Viewport], visuals: Sequence[VisualBase], model_matrices: Sequence[TransBuf], cameras: Sequence[Camera], return_image: bool = True, image_format: str = 'png') -> bytes

Render the scene to an image.

Parameters:

Name Type Description Default
viewports typing.Sequence[gsp.core.viewport.Viewport]

Sequence of viewport regions to render into.

required
visuals typing.Sequence[gsp.types.visual_base.VisualBase]

Sequence of visual elements to render.

required
model_matrices typing.Sequence[gsp.types.transbuf.TransBuf]

Sequence of model transformation matrices for each visual.

required
cameras typing.Sequence[gsp.core.camera.Camera]

Sequence of cameras defining view and projection for each visual.

required
return_image bool

Whether to return the rendered image as bytes.

True
image_format str

Format for the output image (e.g., "png", "jpg").

'png'

Returns:

Type Description
bytes

The rendered image as bytes in the specified format, or empty bytes if return_image is False.

Raises:

Type Description
AssertionError

If the sequences don't all have the same length.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def render(
    self,
    viewports: Sequence[Viewport],
    visuals: Sequence[VisualBase],
    model_matrices: Sequence[TransBuf],
    cameras: Sequence[Camera],
    return_image: bool = True,
    image_format: str = "png",
) -> bytes:
    """Render the scene to an image.

    Args:
        viewports: Sequence of viewport regions to render into.
        visuals: Sequence of visual elements to render.
        model_matrices: Sequence of model transformation matrices for each visual.
        cameras: Sequence of cameras defining view and projection for each visual.
        return_image: Whether to return the rendered image as bytes.
        image_format: Format for the output image (e.g., "png", "jpg").

    Returns:
        The rendered image as bytes in the specified format, or empty bytes if return_image is False.

    Raises:
        AssertionError: If the sequences don't all have the same length.
    """
    # =============================================================================
    # Sanity checks
    # =============================================================================

    assert (
        len(viewports) == len(visuals) == len(model_matrices) == len(cameras)
    ), f"All length MUST be equal. Mismatched lengths: {len(viewports)} viewports, {len(visuals)} visuals, {len(model_matrices)} model matrices, {len(cameras)} cameras"

    # =============================================================================
    # Create all the axes if needed
    # =============================================================================
    for viewport in viewports:
        if viewport.get_uuid() in self._axes:
            continue
        axes_rect = (
            viewport.get_x() / self.canvas.get_width(),
            viewport.get_y() / self.canvas.get_height(),
            viewport.get_width() / self.canvas.get_width(),
            viewport.get_height() / self.canvas.get_height(),
        )
        axes: matplotlib.axes.Axes = matplotlib.pyplot.axes(axes_rect)
        # this should be -1 to 1 - from normalized device coordinates - https://en.wikipedia.org/wiki/Graphics_pipeline
        # - https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
        axes.set_xlim(-1, 1)
        axes.set_ylim(-1, 1)
        # hide the borders
        axes.axis("off")
        # store axes for this viewport
        self._axes[viewport.get_uuid()] = axes

    # =============================================================================
    # Render each visual
    # =============================================================================

    for viewport, visual, model_matrix, camera in zip(viewports, visuals, model_matrices, cameras):
        self._render_visual(viewport, visual, model_matrix, camera)

    # =============================================================================
    # Render the output image
    # =============================================================================
    image_png_data = b""

    # honor return_image option
    if return_image:
        # Render the image to a PNG buffer
        image_png_buffer = io.BytesIO()
        self._figure.savefig(image_png_buffer, format=image_format, dpi=self.canvas.get_dpi())

        image_png_buffer.seek(0)
        image_png_data = image_png_buffer.getvalue()
        image_png_buffer.close()

    return image_png_data

show() -> None

Display the rendered figure in an interactive window.

This method shows the Matplotlib figure. It does nothing when running in test mode (GSP_TEST environment variable set to "True").

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer.py
85
86
87
88
89
90
91
92
93
94
95
96
def show(self) -> None:
    """Display the rendered figure in an interactive window.

    This method shows the Matplotlib figure. It does nothing when running
    in test mode (GSP_TEST environment variable set to "True").
    """
    # handle non-interactive mode for tests
    in_test = os.environ.get("GSP_TEST") == "True"
    if in_test:
        return

    matplotlib.pyplot.show()

Markers Renderer

gsp_matplotlib.renderer.matplotlib_renderer_markers

Matplotlib renderer for Markers objects.

RendererMarkers

Renderer for Markers objects using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_markers.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class RendererMarkers:
    """Renderer for Markers objects using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        markers: Markers,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render the given Markers object onto the specified viewport using Matplotlib.

        Args:
            renderer (MatplotlibRenderer): The renderer instance.
            viewport (Viewport): The viewport to render onto.
            markers (Markers): The Markers object containing marker data.
            model_matrix (TransBuf): The model transformation matrix.
            camera (Camera): The camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(markers.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        sizes_buffer = TransBufUtils.to_buffer(markers.get_sizes())
        face_colors_buffer = TransBufUtils.to_buffer(markers.get_face_colors())
        edge_colors_buffer = TransBufUtils.to_buffer(markers.get_edge_colors())
        edge_widths_buffer = TransBufUtils.to_buffer(markers.get_edge_widths())

        # Convert buffers to numpy arrays
        sizes_numpy = Bufferx.to_numpy(sizes_buffer).reshape(-1)
        face_colors_numpy = Bufferx.to_numpy(face_colors_buffer) / 255.0  # normalize to [0, 1] range
        edge_colors_numpy = Bufferx.to_numpy(edge_colors_buffer) / 255.0  # normalize to [0, 1] range
        edge_widths_numpy = Bufferx.to_numpy(edge_widths_buffer).flatten()

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Markers.sanity_check_attributes_buffer(
            markers.get_marker_shape(),
            vertices_buffer,
            sizes_buffer,
            face_colors_buffer,
            edge_colors_buffer,
            edge_widths_buffer,
        )

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid = f"{viewport.get_uuid()}_{markers.get_uuid()}"

        if artist_uuid not in renderer._artists:
            axes = renderer.get_mpl_axes_for_viewport(viewport)
            mpl_marker_shape = ConverterUtils.marker_shape_gsp_to_mpl(markers.get_marker_shape())
            mpl_path_collection = axes.scatter([], [], marker=mpl_marker_shape)
            mpl_path_collection.set_visible(False)
            # hide until properly positioned and sized
            renderer._artists[artist_uuid] = mpl_path_collection
            axes.add_artist(mpl_path_collection)

        # =============================================================================
        # Get existing artists
        # =============================================================================

        mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[artist_uuid])
        mpl_path_collection.set_visible(True)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_path_collection.set_offsets(offsets=vertices_2d)
        mpl_path_collection.set_sizes(typing.cast(list, sizes_numpy))
        mpl_path_collection.set_facecolor(typing.cast(list, face_colors_numpy))
        mpl_path_collection.set_edgecolor(typing.cast(list, edge_colors_numpy))
        mpl_path_collection.set_linewidth(typing.cast(list, edge_widths_numpy))

        # Return the list of artists created/updated
        return [mpl_path_collection]

render(renderer: MatplotlibRenderer, viewport: Viewport, markers: Markers, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render the given Markers object onto the specified viewport using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.matplotlib_renderer.MatplotlibRenderer

The renderer instance.

required
viewport gsp.core.viewport.Viewport

The viewport to render onto.

required
markers gsp.visuals.Markers

The Markers object containing marker data.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix.

required
camera gsp.core.camera.Camera

The camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_markers.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    markers: Markers,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render the given Markers object onto the specified viewport using Matplotlib.

    Args:
        renderer (MatplotlibRenderer): The renderer instance.
        viewport (Viewport): The viewport to render onto.
        markers (Markers): The Markers object containing marker data.
        model_matrix (TransBuf): The model transformation matrix.
        camera (Camera): The camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(markers.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    sizes_buffer = TransBufUtils.to_buffer(markers.get_sizes())
    face_colors_buffer = TransBufUtils.to_buffer(markers.get_face_colors())
    edge_colors_buffer = TransBufUtils.to_buffer(markers.get_edge_colors())
    edge_widths_buffer = TransBufUtils.to_buffer(markers.get_edge_widths())

    # Convert buffers to numpy arrays
    sizes_numpy = Bufferx.to_numpy(sizes_buffer).reshape(-1)
    face_colors_numpy = Bufferx.to_numpy(face_colors_buffer) / 255.0  # normalize to [0, 1] range
    edge_colors_numpy = Bufferx.to_numpy(edge_colors_buffer) / 255.0  # normalize to [0, 1] range
    edge_widths_numpy = Bufferx.to_numpy(edge_widths_buffer).flatten()

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Markers.sanity_check_attributes_buffer(
        markers.get_marker_shape(),
        vertices_buffer,
        sizes_buffer,
        face_colors_buffer,
        edge_colors_buffer,
        edge_widths_buffer,
    )

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid = f"{viewport.get_uuid()}_{markers.get_uuid()}"

    if artist_uuid not in renderer._artists:
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        mpl_marker_shape = ConverterUtils.marker_shape_gsp_to_mpl(markers.get_marker_shape())
        mpl_path_collection = axes.scatter([], [], marker=mpl_marker_shape)
        mpl_path_collection.set_visible(False)
        # hide until properly positioned and sized
        renderer._artists[artist_uuid] = mpl_path_collection
        axes.add_artist(mpl_path_collection)

    # =============================================================================
    # Get existing artists
    # =============================================================================

    mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[artist_uuid])
    mpl_path_collection.set_visible(True)

    # =============================================================================
    # Update artists
    # =============================================================================

    mpl_path_collection.set_offsets(offsets=vertices_2d)
    mpl_path_collection.set_sizes(typing.cast(list, sizes_numpy))
    mpl_path_collection.set_facecolor(typing.cast(list, face_colors_numpy))
    mpl_path_collection.set_edgecolor(typing.cast(list, edge_colors_numpy))
    mpl_path_collection.set_linewidth(typing.cast(list, edge_widths_numpy))

    # Return the list of artists created/updated
    return [mpl_path_collection]

Paths Renderer

gsp_matplotlib.renderer.matplotlib_renderer_paths

Matplotlib renderer for Paths objects.

RendererPaths

Renderer for Paths objects using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_paths.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class RendererPaths:
    """Renderer for Paths objects using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        paths: Paths,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render the given Paths object onto the specified viewport using Matplotlib.

        Args:
            renderer (MatplotlibRenderer): The renderer instance.
            viewport (Viewport): The viewport to render onto.
            paths (Paths): The Paths object containing path data.
            model_matrix (TransBuf): The model transformation matrix.
            camera (Camera): The camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(paths.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        path_sizes_buffer = TransBufUtils.to_buffer(paths.get_path_sizes())
        colors_buffer = TransBufUtils.to_buffer(paths.get_colors())
        line_widths_buffer = TransBufUtils.to_buffer(paths.get_line_widths())

        # Convert buffers to numpy arrays
        path_sizes_numpy = Bufferx.to_numpy(path_sizes_buffer)
        colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
        line_widths_numpy = Bufferx.to_numpy(line_widths_buffer)
        line_widths_numpy = line_widths_numpy.reshape(-1)

        # =============================================================================
        #
        # =============================================================================
        # mpl_paths is of shape (M, 2, 2) where M is total number of line segments across all paths
        mpl_paths = np.zeros((0, 2, 2), dtype=np.float32)
        # mpl_colors is of shape (M, 4)
        mpl_colors = np.zeros((0, 4), dtype=np.float32)
        # mpl_line_widths is of shape (M,)
        mpl_line_widths = np.zeros((0,), dtype=np.float32)

        for path_index, path_size in enumerate(path_sizes_numpy):
            path_start = int(np.sum(path_sizes_numpy[:path_index]))
            path_size_int = int(path_size)
            path_vertices_2d = vertices_2d[path_start : path_start + path_size_int]

            # Create segments for this path
            path_mpl_paths = np.concatenate([path_vertices_2d[:-1].reshape(-1, 1, 2), path_vertices_2d[1:].reshape(-1, 1, 2)], axis=1)
            mpl_paths = np.vstack([mpl_paths, path_mpl_paths])

            mpl_colors = np.vstack([mpl_colors, colors_numpy[path_start : path_start + path_size_int - 1]])
            mpl_line_widths = np.hstack([mpl_line_widths, line_widths_numpy[path_start : path_start + path_size_int - 1]])

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Paths.sanity_check_attributes_buffer(
            vertices_buffer,
            path_sizes_buffer,
            colors_buffer,
            line_widths_buffer,
            paths.get_cap_style(),
            paths.get_join_style(),
        )

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid = f"{viewport.get_uuid()}_{paths.get_uuid()}"

        if artist_uuid not in renderer._artists:
            mpl_line_collection = matplotlib.collections.LineCollection([])
            mpl_line_collection.set_visible(False)
            # hide until properly positioned and sized
            renderer._artists[artist_uuid] = mpl_line_collection
            axes = renderer.get_mpl_axes_for_viewport(viewport)
            axes.add_artist(mpl_line_collection)

        # =============================================================================
        # Get existing artists
        # =============================================================================

        mpl_line_collection = typing.cast(matplotlib.collections.LineCollection, renderer._artists[artist_uuid])
        mpl_line_collection.set_visible(True)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_line_collection.set_paths(typing.cast(list, mpl_paths))
        mpl_line_collection.set_color(typing.cast(list, mpl_colors))
        mpl_line_collection.set_linewidth(typing.cast(list, mpl_line_widths))
        mpl_line_collection.set_capstyle(ConverterUtils.cap_style_gsp_to_mpl(paths.get_cap_style()))
        mpl_line_collection.set_joinstyle(ConverterUtils.join_style_gsp_to_mpl(paths.get_join_style()))

        # Return the list of artists created/updated
        changed_artists: list[matplotlib.artist.Artist] = []
        changed_artists.append(mpl_line_collection)
        return changed_artists

render(renderer: MatplotlibRenderer, viewport: Viewport, paths: Paths, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render the given Paths object onto the specified viewport using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.matplotlib_renderer.MatplotlibRenderer

The renderer instance.

required
viewport gsp.core.viewport.Viewport

The viewport to render onto.

required
paths gsp.visuals.paths.Paths

The Paths object containing path data.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix.

required
camera gsp.core.camera.Camera

The camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_paths.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    paths: Paths,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render the given Paths object onto the specified viewport using Matplotlib.

    Args:
        renderer (MatplotlibRenderer): The renderer instance.
        viewport (Viewport): The viewport to render onto.
        paths (Paths): The Paths object containing path data.
        model_matrix (TransBuf): The model transformation matrix.
        camera (Camera): The camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: A list of Matplotlib artist objects created or updated
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(paths.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    path_sizes_buffer = TransBufUtils.to_buffer(paths.get_path_sizes())
    colors_buffer = TransBufUtils.to_buffer(paths.get_colors())
    line_widths_buffer = TransBufUtils.to_buffer(paths.get_line_widths())

    # Convert buffers to numpy arrays
    path_sizes_numpy = Bufferx.to_numpy(path_sizes_buffer)
    colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
    line_widths_numpy = Bufferx.to_numpy(line_widths_buffer)
    line_widths_numpy = line_widths_numpy.reshape(-1)

    # =============================================================================
    #
    # =============================================================================
    # mpl_paths is of shape (M, 2, 2) where M is total number of line segments across all paths
    mpl_paths = np.zeros((0, 2, 2), dtype=np.float32)
    # mpl_colors is of shape (M, 4)
    mpl_colors = np.zeros((0, 4), dtype=np.float32)
    # mpl_line_widths is of shape (M,)
    mpl_line_widths = np.zeros((0,), dtype=np.float32)

    for path_index, path_size in enumerate(path_sizes_numpy):
        path_start = int(np.sum(path_sizes_numpy[:path_index]))
        path_size_int = int(path_size)
        path_vertices_2d = vertices_2d[path_start : path_start + path_size_int]

        # Create segments for this path
        path_mpl_paths = np.concatenate([path_vertices_2d[:-1].reshape(-1, 1, 2), path_vertices_2d[1:].reshape(-1, 1, 2)], axis=1)
        mpl_paths = np.vstack([mpl_paths, path_mpl_paths])

        mpl_colors = np.vstack([mpl_colors, colors_numpy[path_start : path_start + path_size_int - 1]])
        mpl_line_widths = np.hstack([mpl_line_widths, line_widths_numpy[path_start : path_start + path_size_int - 1]])

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Paths.sanity_check_attributes_buffer(
        vertices_buffer,
        path_sizes_buffer,
        colors_buffer,
        line_widths_buffer,
        paths.get_cap_style(),
        paths.get_join_style(),
    )

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid = f"{viewport.get_uuid()}_{paths.get_uuid()}"

    if artist_uuid not in renderer._artists:
        mpl_line_collection = matplotlib.collections.LineCollection([])
        mpl_line_collection.set_visible(False)
        # hide until properly positioned and sized
        renderer._artists[artist_uuid] = mpl_line_collection
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        axes.add_artist(mpl_line_collection)

    # =============================================================================
    # Get existing artists
    # =============================================================================

    mpl_line_collection = typing.cast(matplotlib.collections.LineCollection, renderer._artists[artist_uuid])
    mpl_line_collection.set_visible(True)

    # =============================================================================
    # Update artists
    # =============================================================================

    mpl_line_collection.set_paths(typing.cast(list, mpl_paths))
    mpl_line_collection.set_color(typing.cast(list, mpl_colors))
    mpl_line_collection.set_linewidth(typing.cast(list, mpl_line_widths))
    mpl_line_collection.set_capstyle(ConverterUtils.cap_style_gsp_to_mpl(paths.get_cap_style()))
    mpl_line_collection.set_joinstyle(ConverterUtils.join_style_gsp_to_mpl(paths.get_join_style()))

    # Return the list of artists created/updated
    changed_artists: list[matplotlib.artist.Artist] = []
    changed_artists.append(mpl_line_collection)
    return changed_artists

Pixels Renderer

gsp_matplotlib.renderer.matplotlib_renderer_pixels

Renderer for Pixels using Matplotlib.

RendererPixels

Renderer for Pixels using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_pixels.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
class RendererPixels:
    """Renderer for Pixels using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        pixels: Pixels,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render Pixels visual using Matplotlib.

        Args:
            renderer: The MatplotlibRenderer instance.
            viewport: The Viewport in which to render.
            pixels: The Pixels visual to render.
            model_matrix: The model transformation matrix as a TransBuf.
            camera: The Camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(pixels.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        colors_buffer = TransBufUtils.to_buffer(pixels.get_colors())

        # Convert buffers to numpy arrays
        colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Pixels.sanity_check_attributes_buffer(vertices_buffer, colors_buffer, pixels.get_groups())

        # =============================================================================
        #   Compute indices_per_group for groups depending on the type of groups
        # =============================================================================

        indices_per_group = GroupUtils.compute_indices_per_group(vertices_numpy.__len__(), pixels.get_groups())
        group_count = GroupUtils.get_group_count(vertices_numpy.__len__(), pixels.get_groups())

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid_prefix = f"{viewport.get_uuid()}_{pixels.get_uuid()}"

        # update stored group count
        old_group_count = None
        if artist_uuid_prefix in renderer._group_count:
            old_group_count = renderer._group_count[artist_uuid_prefix]
        renderer._group_count[artist_uuid_prefix] = group_count

        # If the group count has changed, destroy old artists
        if old_group_count is not None and old_group_count != group_count:
            RendererPixels.destroy_artists(renderer, viewport, pixels, old_group_count)

        # Create artists if they do not exist
        artist_uuid_sample = f"{artist_uuid_prefix}_group_0"
        if artist_uuid_sample not in renderer._artists:
            RendererPixels.create_artists(renderer, viewport, pixels, group_count)

        # =============================================================================
        # Update matplotlib for each group
        # =============================================================================

        changed_artists: list[matplotlib.artist.Artist] = []
        for group_index in range(group_count):
            group_uuid = f"{artist_uuid_prefix}_group_{group_index}"

            # =============================================================================
            # Get existing artists
            # =============================================================================

            mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[group_uuid])
            mpl_path_collection.set_visible(True)
            changed_artists.append(mpl_path_collection)

            # =============================================================================
            # Update artists
            # =============================================================================

            mpl_path_collection.set_offsets(offsets=vertices_2d[indices_per_group[group_index]])
            mpl_path_collection.set_facecolor(typing.cast(list, colors_numpy[group_index]))

        # Return the list of artists created/updated
        return changed_artists

    # =============================================================================
    #
    # =============================================================================

    @staticmethod
    def create_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None:
        """Create the artists associated with the given visual and group count.

        Args:
            renderer: The Matplotlib renderer.
            viewport: The viewport for which to create the artists.
            visual: The visual for which to create the artists.
            group_count: The number of groups in the visual.
        """
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        artist_uuid_prefix = f"{viewport.get_uuid()}_{visual.get_uuid()}"
        # compute 1 pixel size in points squared for matplotlib sizing
        assert axes.figure.get_dpi() is not None, "Canvas DPI must be set for proper pixel sizing"
        size_pt = UnitUtils.pixel_to_point(1.0, axes.figure.get_dpi())
        size_squared_pt = size_pt * size_pt

        for group_index in range(group_count):
            mpl_path_collection = axes.scatter([], [], s=size_squared_pt, marker="o")
            mpl_path_collection.set_antialiased(True)
            mpl_path_collection.set_linewidth(0)
            mpl_path_collection.set_visible(False)
            # hide until properly positioned and sized
            group_uuid = f"{artist_uuid_prefix}_group_{group_index}"
            renderer._artists[group_uuid] = mpl_path_collection
            axes.add_artist(mpl_path_collection)

    @staticmethod
    def destroy_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None:
        """Destroy the artists associated with the given visual and group count.

        Trigger a bug in matplotlib where artists are not properly removed from the axes.
        """
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        artist_uuid_prefix = f"{viewport.get_uuid()}_{visual.get_uuid()}"
        for group_index in range(group_count):
            group_uuid = f"{artist_uuid_prefix}_group_{group_index}"
            mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[group_uuid])
            del renderer._artists[group_uuid]
            mpl_path_collection.remove()

            # axes.collections.remove(mpl_path_collection)
            # axes.collections.remove(axes.collections.index(mpl_path_collection))

            ax = axes
            artist = mpl_path_collection

            print("Artist:", artist)
            print("In ax.artists?", artist in ax.artists)
            print("In ax.patches?", artist in ax.patches)
            print("In ax.lines?", artist in ax.lines)
            print("In ax.collections?", artist in ax.collections)
            print("In ax.texts?", artist in ax.texts)
            print("Figure art?", artist in getattr(ax.figure, "artists", []))

create_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None staticmethod

Create the artists associated with the given visual and group count.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.MatplotlibRenderer

The Matplotlib renderer.

required
viewport gsp.core.viewport.Viewport

The viewport for which to create the artists.

required
visual gsp.types.visual_base.VisualBase

The visual for which to create the artists.

required
group_count int

The number of groups in the visual.

required
Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_pixels.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@staticmethod
def create_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None:
    """Create the artists associated with the given visual and group count.

    Args:
        renderer: The Matplotlib renderer.
        viewport: The viewport for which to create the artists.
        visual: The visual for which to create the artists.
        group_count: The number of groups in the visual.
    """
    axes = renderer.get_mpl_axes_for_viewport(viewport)
    artist_uuid_prefix = f"{viewport.get_uuid()}_{visual.get_uuid()}"
    # compute 1 pixel size in points squared for matplotlib sizing
    assert axes.figure.get_dpi() is not None, "Canvas DPI must be set for proper pixel sizing"
    size_pt = UnitUtils.pixel_to_point(1.0, axes.figure.get_dpi())
    size_squared_pt = size_pt * size_pt

    for group_index in range(group_count):
        mpl_path_collection = axes.scatter([], [], s=size_squared_pt, marker="o")
        mpl_path_collection.set_antialiased(True)
        mpl_path_collection.set_linewidth(0)
        mpl_path_collection.set_visible(False)
        # hide until properly positioned and sized
        group_uuid = f"{artist_uuid_prefix}_group_{group_index}"
        renderer._artists[group_uuid] = mpl_path_collection
        axes.add_artist(mpl_path_collection)

destroy_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None staticmethod

Destroy the artists associated with the given visual and group count.

Trigger a bug in matplotlib where artists are not properly removed from the axes.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_pixels.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
@staticmethod
def destroy_artists(renderer: MatplotlibRenderer, viewport: Viewport, visual: VisualBase, group_count: int) -> None:
    """Destroy the artists associated with the given visual and group count.

    Trigger a bug in matplotlib where artists are not properly removed from the axes.
    """
    axes = renderer.get_mpl_axes_for_viewport(viewport)
    artist_uuid_prefix = f"{viewport.get_uuid()}_{visual.get_uuid()}"
    for group_index in range(group_count):
        group_uuid = f"{artist_uuid_prefix}_group_{group_index}"
        mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[group_uuid])
        del renderer._artists[group_uuid]
        mpl_path_collection.remove()

        # axes.collections.remove(mpl_path_collection)
        # axes.collections.remove(axes.collections.index(mpl_path_collection))

        ax = axes
        artist = mpl_path_collection

        print("Artist:", artist)
        print("In ax.artists?", artist in ax.artists)
        print("In ax.patches?", artist in ax.patches)
        print("In ax.lines?", artist in ax.lines)
        print("In ax.collections?", artist in ax.collections)
        print("In ax.texts?", artist in ax.texts)
        print("Figure art?", artist in getattr(ax.figure, "artists", []))

render(renderer: MatplotlibRenderer, viewport: Viewport, pixels: Pixels, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render Pixels visual using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.MatplotlibRenderer

The MatplotlibRenderer instance.

required
viewport gsp.core.viewport.Viewport

The Viewport in which to render.

required
pixels gsp.visuals.pixels.Pixels

The Pixels visual to render.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix as a TransBuf.

required
camera gsp.core.camera.Camera

The Camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_pixels.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    pixels: Pixels,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render Pixels visual using Matplotlib.

    Args:
        renderer: The MatplotlibRenderer instance.
        viewport: The Viewport in which to render.
        pixels: The Pixels visual to render.
        model_matrix: The model transformation matrix as a TransBuf.
        camera: The Camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(pixels.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    colors_buffer = TransBufUtils.to_buffer(pixels.get_colors())

    # Convert buffers to numpy arrays
    colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Pixels.sanity_check_attributes_buffer(vertices_buffer, colors_buffer, pixels.get_groups())

    # =============================================================================
    #   Compute indices_per_group for groups depending on the type of groups
    # =============================================================================

    indices_per_group = GroupUtils.compute_indices_per_group(vertices_numpy.__len__(), pixels.get_groups())
    group_count = GroupUtils.get_group_count(vertices_numpy.__len__(), pixels.get_groups())

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid_prefix = f"{viewport.get_uuid()}_{pixels.get_uuid()}"

    # update stored group count
    old_group_count = None
    if artist_uuid_prefix in renderer._group_count:
        old_group_count = renderer._group_count[artist_uuid_prefix]
    renderer._group_count[artist_uuid_prefix] = group_count

    # If the group count has changed, destroy old artists
    if old_group_count is not None and old_group_count != group_count:
        RendererPixels.destroy_artists(renderer, viewport, pixels, old_group_count)

    # Create artists if they do not exist
    artist_uuid_sample = f"{artist_uuid_prefix}_group_0"
    if artist_uuid_sample not in renderer._artists:
        RendererPixels.create_artists(renderer, viewport, pixels, group_count)

    # =============================================================================
    # Update matplotlib for each group
    # =============================================================================

    changed_artists: list[matplotlib.artist.Artist] = []
    for group_index in range(group_count):
        group_uuid = f"{artist_uuid_prefix}_group_{group_index}"

        # =============================================================================
        # Get existing artists
        # =============================================================================

        mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[group_uuid])
        mpl_path_collection.set_visible(True)
        changed_artists.append(mpl_path_collection)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_path_collection.set_offsets(offsets=vertices_2d[indices_per_group[group_index]])
        mpl_path_collection.set_facecolor(typing.cast(list, colors_numpy[group_index]))

    # Return the list of artists created/updated
    return changed_artists

Points Renderer

gsp_matplotlib.renderer.matplotlib_renderer_points

Renderer for Points using Matplotlib.

RendererPoints

Renderer for Points using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_points.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class RendererPoints:
    """Renderer for Points using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        points: Points,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render Points visual using Matplotlib.

        Args:
            renderer: The MatplotlibRenderer instance.
            viewport: The Viewport in which to render.
            points: The Points visual to render.
            model_matrix: The model transformation matrix as a TransBuf.
            camera: The Camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(points.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        sizes_buffer = TransBufUtils.to_buffer(points.get_sizes())
        face_colors_buffer = TransBufUtils.to_buffer(points.get_face_colors())
        edge_colors_buffer = TransBufUtils.to_buffer(points.get_edge_colors())
        edge_widths_buffer = TransBufUtils.to_buffer(points.get_edge_widths())

        # Convert buffers to numpy arrays
        sizes_numpy = Bufferx.to_numpy(sizes_buffer).flatten()
        face_colors_numpy = Bufferx.to_numpy(face_colors_buffer) / 255.0  # normalize to [0, 1] range
        edge_colors_numpy = Bufferx.to_numpy(edge_colors_buffer) / 255.0  # normalize to [0, 1] range
        edge_widths_numpy = Bufferx.to_numpy(edge_widths_buffer).flatten()

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Points.sanity_check_attributes_buffer(
            vertices_buffer,
            sizes_buffer,
            face_colors_buffer,
            edge_colors_buffer,
            edge_widths_buffer,
        )

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid = f"{viewport.get_uuid()}_{points.get_uuid()}"
        if artist_uuid not in renderer._artists:
            axes = renderer.get_mpl_axes_for_viewport(viewport)
            mpl_path_collection = axes.scatter([], [])
            mpl_path_collection.set_visible(False)
            # hide until properly positioned and sized
            renderer._artists[artist_uuid] = mpl_path_collection
            axes.add_artist(mpl_path_collection)

        # =============================================================================
        # Get existing artists
        # =============================================================================

        mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[artist_uuid])
        mpl_path_collection.set_visible(True)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_path_collection.set_offsets(offsets=vertices_2d)
        mpl_path_collection.set_sizes(typing.cast(list, sizes_numpy))
        mpl_path_collection.set_facecolor(typing.cast(list, face_colors_numpy))
        mpl_path_collection.set_edgecolor(typing.cast(list, edge_colors_numpy))
        mpl_path_collection.set_linewidth(typing.cast(list, edge_widths_numpy))

        # Return the list of artists created/updated
        changed_artists: list[matplotlib.artist.Artist] = []
        changed_artists.append(mpl_path_collection)
        return changed_artists

render(renderer: MatplotlibRenderer, viewport: Viewport, points: Points, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render Points visual using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.matplotlib_renderer.MatplotlibRenderer

The MatplotlibRenderer instance.

required
viewport gsp.core.viewport.Viewport

The Viewport in which to render.

required
points gsp.visuals.points.Points

The Points visual to render.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix as a TransBuf.

required
camera gsp.core.camera.Camera

The Camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_points.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    points: Points,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render Points visual using Matplotlib.

    Args:
        renderer: The MatplotlibRenderer instance.
        viewport: The Viewport in which to render.
        points: The Points visual to render.
        model_matrix: The model transformation matrix as a TransBuf.
        camera: The Camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(points.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    sizes_buffer = TransBufUtils.to_buffer(points.get_sizes())
    face_colors_buffer = TransBufUtils.to_buffer(points.get_face_colors())
    edge_colors_buffer = TransBufUtils.to_buffer(points.get_edge_colors())
    edge_widths_buffer = TransBufUtils.to_buffer(points.get_edge_widths())

    # Convert buffers to numpy arrays
    sizes_numpy = Bufferx.to_numpy(sizes_buffer).flatten()
    face_colors_numpy = Bufferx.to_numpy(face_colors_buffer) / 255.0  # normalize to [0, 1] range
    edge_colors_numpy = Bufferx.to_numpy(edge_colors_buffer) / 255.0  # normalize to [0, 1] range
    edge_widths_numpy = Bufferx.to_numpy(edge_widths_buffer).flatten()

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Points.sanity_check_attributes_buffer(
        vertices_buffer,
        sizes_buffer,
        face_colors_buffer,
        edge_colors_buffer,
        edge_widths_buffer,
    )

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid = f"{viewport.get_uuid()}_{points.get_uuid()}"
    if artist_uuid not in renderer._artists:
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        mpl_path_collection = axes.scatter([], [])
        mpl_path_collection.set_visible(False)
        # hide until properly positioned and sized
        renderer._artists[artist_uuid] = mpl_path_collection
        axes.add_artist(mpl_path_collection)

    # =============================================================================
    # Get existing artists
    # =============================================================================

    mpl_path_collection = typing.cast(matplotlib.collections.PathCollection, renderer._artists[artist_uuid])
    mpl_path_collection.set_visible(True)

    # =============================================================================
    # Update artists
    # =============================================================================

    mpl_path_collection.set_offsets(offsets=vertices_2d)
    mpl_path_collection.set_sizes(typing.cast(list, sizes_numpy))
    mpl_path_collection.set_facecolor(typing.cast(list, face_colors_numpy))
    mpl_path_collection.set_edgecolor(typing.cast(list, edge_colors_numpy))
    mpl_path_collection.set_linewidth(typing.cast(list, edge_widths_numpy))

    # Return the list of artists created/updated
    changed_artists: list[matplotlib.artist.Artist] = []
    changed_artists.append(mpl_path_collection)
    return changed_artists

Segments Renderer

gsp_matplotlib.renderer.matplotlib_renderer_segments

Renderer for Segments using Matplotlib.

RendererSegments

Renderer for Segments using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_segments.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class RendererSegments:
    """Renderer for Segments using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        segments: Segments,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render Segments visual using Matplotlib.

        Args:
            renderer: The MatplotlibRenderer instance.
            viewport: The Viewport in which to render.
            segments: The Segments visual to render.
            model_matrix: The model transformation matrix as a TransBuf.
            camera: The Camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(segments.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        colors_buffer = TransBufUtils.to_buffer(segments.get_colors())
        line_widths_buffer = TransBufUtils.to_buffer(segments.get_line_widths())

        # Convert buffers to numpy arrays
        positions_numpy = vertices_2d.reshape(-1, 2, 2)
        colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
        line_widths_numpy = Bufferx.to_numpy(line_widths_buffer)
        line_widths_numpy = line_widths_numpy.reshape(-1)

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Segments.sanity_check_attributes_buffer(
            vertices_buffer,
            line_widths_buffer,
            segments.get_cap_style(),
            colors_buffer,
        )

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid = f"{viewport.get_uuid()}_{segments.get_uuid()}"
        if artist_uuid not in renderer._artists:
            mpl_line_collection = matplotlib.collections.LineCollection([])
            mpl_line_collection.set_visible(False)
            # hide until properly positioned and sized
            renderer._artists[artist_uuid] = mpl_line_collection
            axes = renderer.get_mpl_axes_for_viewport(viewport)
            axes.add_artist(mpl_line_collection)

        # =============================================================================
        # Get existing artists
        # =============================================================================

        mpl_line_collection = typing.cast(matplotlib.collections.LineCollection, renderer._artists[artist_uuid])
        mpl_line_collection.set_visible(True)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_line_collection.set_paths(typing.cast(list, positions_numpy))
        mpl_line_collection.set_color(typing.cast(list, colors_numpy))
        mpl_line_collection.set_linewidth(typing.cast(list, line_widths_numpy))
        mpl_line_collection.set_capstyle(ConverterUtils.cap_style_gsp_to_mpl(segments.get_cap_style()))

        # Return the list of artists created/updated
        changed_artists: list[matplotlib.artist.Artist] = []
        changed_artists.append(mpl_line_collection)
        return changed_artists

render(renderer: MatplotlibRenderer, viewport: Viewport, segments: Segments, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render Segments visual using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.matplotlib_renderer.MatplotlibRenderer

The MatplotlibRenderer instance.

required
viewport gsp.core.viewport.Viewport

The Viewport in which to render.

required
segments gsp.visuals.segments.Segments

The Segments visual to render.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix as a TransBuf.

required
camera gsp.core.camera.Camera

The Camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_segments.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    segments: Segments,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render Segments visual using Matplotlib.

    Args:
        renderer: The MatplotlibRenderer instance.
        viewport: The Viewport in which to render.
        segments: The Segments visual to render.
        model_matrix: The model transformation matrix as a TransBuf.
        camera: The Camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(segments.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    colors_buffer = TransBufUtils.to_buffer(segments.get_colors())
    line_widths_buffer = TransBufUtils.to_buffer(segments.get_line_widths())

    # Convert buffers to numpy arrays
    positions_numpy = vertices_2d.reshape(-1, 2, 2)
    colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
    line_widths_numpy = Bufferx.to_numpy(line_widths_buffer)
    line_widths_numpy = line_widths_numpy.reshape(-1)

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Segments.sanity_check_attributes_buffer(
        vertices_buffer,
        line_widths_buffer,
        segments.get_cap_style(),
        colors_buffer,
    )

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid = f"{viewport.get_uuid()}_{segments.get_uuid()}"
    if artist_uuid not in renderer._artists:
        mpl_line_collection = matplotlib.collections.LineCollection([])
        mpl_line_collection.set_visible(False)
        # hide until properly positioned and sized
        renderer._artists[artist_uuid] = mpl_line_collection
        axes = renderer.get_mpl_axes_for_viewport(viewport)
        axes.add_artist(mpl_line_collection)

    # =============================================================================
    # Get existing artists
    # =============================================================================

    mpl_line_collection = typing.cast(matplotlib.collections.LineCollection, renderer._artists[artist_uuid])
    mpl_line_collection.set_visible(True)

    # =============================================================================
    # Update artists
    # =============================================================================

    mpl_line_collection.set_paths(typing.cast(list, positions_numpy))
    mpl_line_collection.set_color(typing.cast(list, colors_numpy))
    mpl_line_collection.set_linewidth(typing.cast(list, line_widths_numpy))
    mpl_line_collection.set_capstyle(ConverterUtils.cap_style_gsp_to_mpl(segments.get_cap_style()))

    # Return the list of artists created/updated
    changed_artists: list[matplotlib.artist.Artist] = []
    changed_artists.append(mpl_line_collection)
    return changed_artists

Texts Renderer

gsp_matplotlib.renderer.matplotlib_renderer_texts

Renderer for Texts using Matplotlib.

RendererTexts

Renderer for Texts using Matplotlib.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_texts.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class RendererTexts:
    """Renderer for Texts using Matplotlib."""

    @staticmethod
    def render(
        renderer: MatplotlibRenderer,
        viewport: Viewport,
        texts: Texts,
        model_matrix: TransBuf,
        camera: Camera,
    ) -> list[matplotlib.artist.Artist]:
        """Render Texts visual using Matplotlib.

        Args:
            renderer: The MatplotlibRenderer instance.
            viewport: The Viewport in which to render.
            texts: The Texts visual to render.
            model_matrix: The model transformation matrix as a TransBuf.
            camera: The Camera providing view and projection matrices.

        Returns:
            list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
        """
        # =============================================================================
        # Transform vertices with MVP matrix
        # =============================================================================

        vertices_buffer = TransBufUtils.to_buffer(texts.get_positions())
        model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
        view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
        projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

        # convert all necessary buffers to numpy arrays
        vertices_numpy = Bufferx.to_numpy(vertices_buffer)
        model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
        view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
        projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

        # Apply Model-View-Projection transformation to the vertices
        vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

        # Convert 3D vertices to 2D - shape (N, 2)
        vertices_2d = vertices_3d_transformed[:, :2]

        # =============================================================================
        # Convert all attributes to numpy arrays
        # =============================================================================

        # Convert all attributes to buffer
        colors_buffer = TransBufUtils.to_buffer(texts.get_colors())
        font_sizes_buffer = TransBufUtils.to_buffer(texts.get_font_sizes())
        anchors_buffer = TransBufUtils.to_buffer(texts.get_anchors())
        angles_buffer = TransBufUtils.to_buffer(texts.get_angles())

        # Convert buffers to numpy arrays
        font_sizes_numpy = Bufferx.to_numpy(font_sizes_buffer).flatten()
        colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
        anchors_numpy = Bufferx.to_numpy(anchors_buffer)
        angles_numpy = Bufferx.to_numpy(angles_buffer).flatten()

        # =============================================================================
        # Sanity checks attributes buffers
        # =============================================================================

        Texts.sanity_check_attributes_buffer(
            vertices_buffer,
            texts.get_strings(),
            colors_buffer,
            font_sizes_buffer,
            anchors_buffer,
            angles_buffer,
            texts.get_font_name(),
        )

        # =============================================================================
        # Create the artists if needed
        # =============================================================================

        artist_uuid_base = f"{viewport.get_uuid()}_{texts.get_uuid()}"
        for text_index in range(len(texts.get_strings())):
            artist_uuid = f"{artist_uuid_base}_{text_index}"
            if artist_uuid in renderer._artists:
                continue
            mpl_text = matplotlib.text.Text()
            mpl_text.set_visible(False)
            # hide until properly positioned and sized
            renderer._artists[artist_uuid] = mpl_text
            mpl_axes = renderer.get_mpl_axes_for_viewport(viewport)
            mpl_axes.add_artist(mpl_text)

        # Remove extra artists if the number of text strings has decreased
        existing_artist_uuids = [f"{artist_uuid_base}_{i}" for i in range(len(texts.get_strings()))]
        artists_to_remove = [uuid for uuid in renderer._artists.keys() if uuid.startswith(artist_uuid_base) and uuid not in existing_artist_uuids]
        for artist_uuid in artists_to_remove:
            mpl_text = typing.cast(matplotlib.text.Text, renderer._artists[artist_uuid])
            mpl_text.remove()  # remove from axes
            del renderer._artists[artist_uuid]  # remove from renderer's artist dict

        # =============================================================================
        # Get existing artists
        # =============================================================================

        changed_artists: list[matplotlib.artist.Artist] = []
        for text_index in range(len(texts.get_strings())):
            artist_uuid = f"{artist_uuid_base}_{text_index}"
            mpl_text = typing.cast(matplotlib.text.Text, renderer._artists[artist_uuid])
            mpl_text.set_visible(True)

            # =============================================================================
            # Update artists
            # =============================================================================

            mpl_text.set_x(vertices_2d[text_index, 0])
            mpl_text.set_y(vertices_2d[text_index, 1])
            mpl_text.set_text(texts.get_strings()[text_index])
            mpl_text.set_rotation(angles_numpy[text_index] / np.pi * 180.0)  # convert rad to deg
            # print(f"angles_numpy[{text_index}]: {angles_numpy[text_index]}")

            ha_label = "center" if anchors_numpy[text_index, 0] == 0.0 else "right" if anchors_numpy[text_index, 0] == 1.0 else "left"
            mpl_text.set_horizontalalignment(ha_label)
            va_label = "center" if anchors_numpy[text_index, 1] == 0.0 else "top" if anchors_numpy[text_index, 1] == 1.0 else "bottom"
            mpl_text.set_verticalalignment(va_label)

            mpl_text.set_fontfamily(texts.get_font_name())
            mpl_text.set_fontsize(font_sizes_numpy[text_index])
            mpl_text.set_color(typing.cast(tuple, colors_numpy[text_index]))

            # Return the list of artists created/updated
            changed_artists.append(mpl_text)

        return changed_artists

render(renderer: MatplotlibRenderer, viewport: Viewport, texts: Texts, model_matrix: TransBuf, camera: Camera) -> list[matplotlib.artist.Artist] staticmethod

Render Texts visual using Matplotlib.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.matplotlib_renderer.MatplotlibRenderer

The MatplotlibRenderer instance.

required
viewport gsp.core.viewport.Viewport

The Viewport in which to render.

required
texts gsp.visuals.texts.Texts

The Texts visual to render.

required
model_matrix gsp.types.transbuf.TransBuf

The model transformation matrix as a TransBuf.

required
camera gsp.core.camera.Camera

The Camera providing view and projection matrices.

required

Returns:

Type Description
list[matplotlib.artist.Artist]

list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.

Source code in src/gsp_matplotlib/renderer/matplotlib_renderer_texts.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@staticmethod
def render(
    renderer: MatplotlibRenderer,
    viewport: Viewport,
    texts: Texts,
    model_matrix: TransBuf,
    camera: Camera,
) -> list[matplotlib.artist.Artist]:
    """Render Texts visual using Matplotlib.

    Args:
        renderer: The MatplotlibRenderer instance.
        viewport: The Viewport in which to render.
        texts: The Texts visual to render.
        model_matrix: The model transformation matrix as a TransBuf.
        camera: The Camera providing view and projection matrices.

    Returns:
        list[matplotlib.artist.Artist]: List of Matplotlib artists created/updated.
    """
    # =============================================================================
    # Transform vertices with MVP matrix
    # =============================================================================

    vertices_buffer = TransBufUtils.to_buffer(texts.get_positions())
    model_matrix_buffer = TransBufUtils.to_buffer(model_matrix)
    view_matrix_buffer = TransBufUtils.to_buffer(camera.get_view_matrix())
    projection_matrix_buffer = TransBufUtils.to_buffer(camera.get_projection_matrix())

    # convert all necessary buffers to numpy arrays
    vertices_numpy = Bufferx.to_numpy(vertices_buffer)
    model_matrix_numpy = Bufferx.to_numpy(model_matrix_buffer).squeeze()
    view_matrix_numpy = Bufferx.to_numpy(view_matrix_buffer).squeeze()
    projection_matrix_numpy = Bufferx.to_numpy(projection_matrix_buffer).squeeze()

    # Apply Model-View-Projection transformation to the vertices
    vertices_3d_transformed = MathUtils.apply_mvp_to_vertices(vertices_numpy, model_matrix_numpy, view_matrix_numpy, projection_matrix_numpy)

    # Convert 3D vertices to 2D - shape (N, 2)
    vertices_2d = vertices_3d_transformed[:, :2]

    # =============================================================================
    # Convert all attributes to numpy arrays
    # =============================================================================

    # Convert all attributes to buffer
    colors_buffer = TransBufUtils.to_buffer(texts.get_colors())
    font_sizes_buffer = TransBufUtils.to_buffer(texts.get_font_sizes())
    anchors_buffer = TransBufUtils.to_buffer(texts.get_anchors())
    angles_buffer = TransBufUtils.to_buffer(texts.get_angles())

    # Convert buffers to numpy arrays
    font_sizes_numpy = Bufferx.to_numpy(font_sizes_buffer).flatten()
    colors_numpy = Bufferx.to_numpy(colors_buffer) / 255.0  # normalize to [0, 1] range
    anchors_numpy = Bufferx.to_numpy(anchors_buffer)
    angles_numpy = Bufferx.to_numpy(angles_buffer).flatten()

    # =============================================================================
    # Sanity checks attributes buffers
    # =============================================================================

    Texts.sanity_check_attributes_buffer(
        vertices_buffer,
        texts.get_strings(),
        colors_buffer,
        font_sizes_buffer,
        anchors_buffer,
        angles_buffer,
        texts.get_font_name(),
    )

    # =============================================================================
    # Create the artists if needed
    # =============================================================================

    artist_uuid_base = f"{viewport.get_uuid()}_{texts.get_uuid()}"
    for text_index in range(len(texts.get_strings())):
        artist_uuid = f"{artist_uuid_base}_{text_index}"
        if artist_uuid in renderer._artists:
            continue
        mpl_text = matplotlib.text.Text()
        mpl_text.set_visible(False)
        # hide until properly positioned and sized
        renderer._artists[artist_uuid] = mpl_text
        mpl_axes = renderer.get_mpl_axes_for_viewport(viewport)
        mpl_axes.add_artist(mpl_text)

    # Remove extra artists if the number of text strings has decreased
    existing_artist_uuids = [f"{artist_uuid_base}_{i}" for i in range(len(texts.get_strings()))]
    artists_to_remove = [uuid for uuid in renderer._artists.keys() if uuid.startswith(artist_uuid_base) and uuid not in existing_artist_uuids]
    for artist_uuid in artists_to_remove:
        mpl_text = typing.cast(matplotlib.text.Text, renderer._artists[artist_uuid])
        mpl_text.remove()  # remove from axes
        del renderer._artists[artist_uuid]  # remove from renderer's artist dict

    # =============================================================================
    # Get existing artists
    # =============================================================================

    changed_artists: list[matplotlib.artist.Artist] = []
    for text_index in range(len(texts.get_strings())):
        artist_uuid = f"{artist_uuid_base}_{text_index}"
        mpl_text = typing.cast(matplotlib.text.Text, renderer._artists[artist_uuid])
        mpl_text.set_visible(True)

        # =============================================================================
        # Update artists
        # =============================================================================

        mpl_text.set_x(vertices_2d[text_index, 0])
        mpl_text.set_y(vertices_2d[text_index, 1])
        mpl_text.set_text(texts.get_strings()[text_index])
        mpl_text.set_rotation(angles_numpy[text_index] / np.pi * 180.0)  # convert rad to deg
        # print(f"angles_numpy[{text_index}]: {angles_numpy[text_index]}")

        ha_label = "center" if anchors_numpy[text_index, 0] == 0.0 else "right" if anchors_numpy[text_index, 0] == 1.0 else "left"
        mpl_text.set_horizontalalignment(ha_label)
        va_label = "center" if anchors_numpy[text_index, 1] == 0.0 else "top" if anchors_numpy[text_index, 1] == 1.0 else "bottom"
        mpl_text.set_verticalalignment(va_label)

        mpl_text.set_fontfamily(texts.get_font_name())
        mpl_text.set_fontsize(font_sizes_numpy[text_index])
        mpl_text.set_color(typing.cast(tuple, colors_numpy[text_index]))

        # Return the list of artists created/updated
        changed_artists.append(mpl_text)

    return changed_artists

Extra Module

The extra module provides additional utilities and extensions for Matplotlib rendering.

gsp_matplotlib.extra

GSP Matplotlib extra utilities package initialization.

Bufferx

Utility class for Buffer extended functionality using numpy.

Source code in src/gsp_matplotlib/extra/bufferx.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class Bufferx:
    """Utility class for Buffer extended functionality using numpy."""

    # =============================================================================
    # Matrix functions
    # =============================================================================

    @staticmethod
    def mat4_identity() -> Buffer:
        """Create a Buffer containing a 4x4 identity matrix."""
        mat4_numpy = np.asarray([np.identity(4, dtype=np.float32)])
        buffer = Bufferx.from_numpy(mat4_numpy, BufferType.mat4)
        return buffer

    # =============================================================================
    # .to_numpy/.from_numpy
    # =============================================================================
    @staticmethod
    def to_numpy(buffer: Buffer) -> np.ndarray:
        """Convert a Buffer to a numpy array."""
        if buffer.get_type() == BufferType.float32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 1))
        elif buffer.get_type() == BufferType.int8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.int8).reshape((count, 1))
        elif buffer.get_type() == BufferType.int32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.int32).reshape((count, 1))
        elif buffer.get_type() == BufferType.uint8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 1))
        elif buffer.get_type() == BufferType.uint32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint32).reshape((count, 1))
        elif buffer.get_type() == BufferType.vec2:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 2))
        elif buffer.get_type() == BufferType.vec3:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 3))
        elif buffer.get_type() == BufferType.vec4:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4))
        elif buffer.get_type() == BufferType.mat4:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4, 4))
        elif buffer.get_type() == BufferType.rgba8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 4))
        else:
            raise NotImplementedError(f"unable to convert buffer {buffer} to numpy array")

    @staticmethod
    def from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer:
        """Create a Buffer from a numpy array."""
        if bufferType == BufferType.float32:
            # sanity check
            assert array_numpy.dtype == np.float32, f"Numpy array must be of dtype float32, got {array_numpy.dtype}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.uint32:
            # sanity check
            assert array_numpy.dtype == np.uint32, f"Numpy array must be of dtype uint32, got {array_numpy.dtype}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.vec2:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 2, f"Numpy array must be of shape (2,), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.vec3:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 3, f"Numpy array must be of shape (3,), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.mat4:
            # sanity check
            assert (
                array_numpy.shape.__len__() == 3 and array_numpy.shape[1] == 4 and array_numpy.shape[2] == 4
            ), f"Numpy array must be of shape (4, 4), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, 1)
            return buffer
        elif bufferType == BufferType.rgba8:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 4, f"Numpy array must be of shape (4,), got {array_numpy.shape} "

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.uint8).tobytes()), 0, count)
            return buffer
        else:
            raise NotImplementedError(f"unable to create a {bufferType} buffer from numpy array of shape {array_numpy.shape} and dtype {array_numpy.dtype}")

mat4_identity() -> Buffer staticmethod

Create a Buffer containing a 4x4 identity matrix.

Source code in src/gsp_matplotlib/extra/bufferx.py
14
15
16
17
18
19
@staticmethod
def mat4_identity() -> Buffer:
    """Create a Buffer containing a 4x4 identity matrix."""
    mat4_numpy = np.asarray([np.identity(4, dtype=np.float32)])
    buffer = Bufferx.from_numpy(mat4_numpy, BufferType.mat4)
    return buffer

to_numpy(buffer: Buffer) -> np.ndarray staticmethod

Convert a Buffer to a numpy array.

Source code in src/gsp_matplotlib/extra/bufferx.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@staticmethod
def to_numpy(buffer: Buffer) -> np.ndarray:
    """Convert a Buffer to a numpy array."""
    if buffer.get_type() == BufferType.float32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 1))
    elif buffer.get_type() == BufferType.int8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.int8).reshape((count, 1))
    elif buffer.get_type() == BufferType.int32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.int32).reshape((count, 1))
    elif buffer.get_type() == BufferType.uint8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 1))
    elif buffer.get_type() == BufferType.uint32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint32).reshape((count, 1))
    elif buffer.get_type() == BufferType.vec2:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 2))
    elif buffer.get_type() == BufferType.vec3:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 3))
    elif buffer.get_type() == BufferType.vec4:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4))
    elif buffer.get_type() == BufferType.mat4:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4, 4))
    elif buffer.get_type() == BufferType.rgba8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 4))
    else:
        raise NotImplementedError(f"unable to convert buffer {buffer} to numpy array")

from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer staticmethod

Create a Buffer from a numpy array.

Source code in src/gsp_matplotlib/extra/bufferx.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@staticmethod
def from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer:
    """Create a Buffer from a numpy array."""
    if bufferType == BufferType.float32:
        # sanity check
        assert array_numpy.dtype == np.float32, f"Numpy array must be of dtype float32, got {array_numpy.dtype}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.uint32:
        # sanity check
        assert array_numpy.dtype == np.uint32, f"Numpy array must be of dtype uint32, got {array_numpy.dtype}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.vec2:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 2, f"Numpy array must be of shape (2,), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.vec3:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 3, f"Numpy array must be of shape (3,), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.mat4:
        # sanity check
        assert (
            array_numpy.shape.__len__() == 3 and array_numpy.shape[1] == 4 and array_numpy.shape[2] == 4
        ), f"Numpy array must be of shape (4, 4), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, 1)
        return buffer
    elif bufferType == BufferType.rgba8:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 4, f"Numpy array must be of shape (4,), got {array_numpy.shape} "

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.uint8).tobytes()), 0, count)
        return buffer
    else:
        raise NotImplementedError(f"unable to create a {bufferType} buffer from numpy array of shape {array_numpy.shape} and dtype {array_numpy.dtype}")

Bufferx

gsp_matplotlib.extra.bufferx

Utility class for Buffer extended functionality using numpy.

Bufferx

Utility class for Buffer extended functionality using numpy.

Source code in src/gsp_matplotlib/extra/bufferx.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class Bufferx:
    """Utility class for Buffer extended functionality using numpy."""

    # =============================================================================
    # Matrix functions
    # =============================================================================

    @staticmethod
    def mat4_identity() -> Buffer:
        """Create a Buffer containing a 4x4 identity matrix."""
        mat4_numpy = np.asarray([np.identity(4, dtype=np.float32)])
        buffer = Bufferx.from_numpy(mat4_numpy, BufferType.mat4)
        return buffer

    # =============================================================================
    # .to_numpy/.from_numpy
    # =============================================================================
    @staticmethod
    def to_numpy(buffer: Buffer) -> np.ndarray:
        """Convert a Buffer to a numpy array."""
        if buffer.get_type() == BufferType.float32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 1))
        elif buffer.get_type() == BufferType.int8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.int8).reshape((count, 1))
        elif buffer.get_type() == BufferType.int32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.int32).reshape((count, 1))
        elif buffer.get_type() == BufferType.uint8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 1))
        elif buffer.get_type() == BufferType.uint32:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint32).reshape((count, 1))
        elif buffer.get_type() == BufferType.vec2:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 2))
        elif buffer.get_type() == BufferType.vec3:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 3))
        elif buffer.get_type() == BufferType.vec4:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4))
        elif buffer.get_type() == BufferType.mat4:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4, 4))
        elif buffer.get_type() == BufferType.rgba8:
            count = buffer.get_count()
            return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 4))
        else:
            raise NotImplementedError(f"unable to convert buffer {buffer} to numpy array")

    @staticmethod
    def from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer:
        """Create a Buffer from a numpy array."""
        if bufferType == BufferType.float32:
            # sanity check
            assert array_numpy.dtype == np.float32, f"Numpy array must be of dtype float32, got {array_numpy.dtype}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.uint32:
            # sanity check
            assert array_numpy.dtype == np.uint32, f"Numpy array must be of dtype uint32, got {array_numpy.dtype}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.vec2:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 2, f"Numpy array must be of shape (2,), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.vec3:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 3, f"Numpy array must be of shape (3,), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
            return buffer
        elif bufferType == BufferType.mat4:
            # sanity check
            assert (
                array_numpy.shape.__len__() == 3 and array_numpy.shape[1] == 4 and array_numpy.shape[2] == 4
            ), f"Numpy array must be of shape (4, 4), got {array_numpy.shape}"

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, 1)
            return buffer
        elif bufferType == BufferType.rgba8:
            # sanity check
            assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 4, f"Numpy array must be of shape (4,), got {array_numpy.shape} "

            count = array_numpy.shape[0]
            buffer = Buffer(count, bufferType)
            buffer.set_data(bytearray(array_numpy.astype(np.uint8).tobytes()), 0, count)
            return buffer
        else:
            raise NotImplementedError(f"unable to create a {bufferType} buffer from numpy array of shape {array_numpy.shape} and dtype {array_numpy.dtype}")

from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer staticmethod

Create a Buffer from a numpy array.

Source code in src/gsp_matplotlib/extra/bufferx.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@staticmethod
def from_numpy(array_numpy: np.ndarray, bufferType: BufferType) -> Buffer:
    """Create a Buffer from a numpy array."""
    if bufferType == BufferType.float32:
        # sanity check
        assert array_numpy.dtype == np.float32, f"Numpy array must be of dtype float32, got {array_numpy.dtype}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.uint32:
        # sanity check
        assert array_numpy.dtype == np.uint32, f"Numpy array must be of dtype uint32, got {array_numpy.dtype}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.vec2:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 2, f"Numpy array must be of shape (2,), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.vec3:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 3, f"Numpy array must be of shape (3,), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, count)
        return buffer
    elif bufferType == BufferType.mat4:
        # sanity check
        assert (
            array_numpy.shape.__len__() == 3 and array_numpy.shape[1] == 4 and array_numpy.shape[2] == 4
        ), f"Numpy array must be of shape (4, 4), got {array_numpy.shape}"

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.float32).tobytes()), 0, 1)
        return buffer
    elif bufferType == BufferType.rgba8:
        # sanity check
        assert array_numpy.shape.__len__() == 2 and array_numpy.shape[1] == 4, f"Numpy array must be of shape (4,), got {array_numpy.shape} "

        count = array_numpy.shape[0]
        buffer = Buffer(count, bufferType)
        buffer.set_data(bytearray(array_numpy.astype(np.uint8).tobytes()), 0, count)
        return buffer
    else:
        raise NotImplementedError(f"unable to create a {bufferType} buffer from numpy array of shape {array_numpy.shape} and dtype {array_numpy.dtype}")

mat4_identity() -> Buffer staticmethod

Create a Buffer containing a 4x4 identity matrix.

Source code in src/gsp_matplotlib/extra/bufferx.py
14
15
16
17
18
19
@staticmethod
def mat4_identity() -> Buffer:
    """Create a Buffer containing a 4x4 identity matrix."""
    mat4_numpy = np.asarray([np.identity(4, dtype=np.float32)])
    buffer = Bufferx.from_numpy(mat4_numpy, BufferType.mat4)
    return buffer

to_numpy(buffer: Buffer) -> np.ndarray staticmethod

Convert a Buffer to a numpy array.

Source code in src/gsp_matplotlib/extra/bufferx.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@staticmethod
def to_numpy(buffer: Buffer) -> np.ndarray:
    """Convert a Buffer to a numpy array."""
    if buffer.get_type() == BufferType.float32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 1))
    elif buffer.get_type() == BufferType.int8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.int8).reshape((count, 1))
    elif buffer.get_type() == BufferType.int32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.int32).reshape((count, 1))
    elif buffer.get_type() == BufferType.uint8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 1))
    elif buffer.get_type() == BufferType.uint32:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint32).reshape((count, 1))
    elif buffer.get_type() == BufferType.vec2:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 2))
    elif buffer.get_type() == BufferType.vec3:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 3))
    elif buffer.get_type() == BufferType.vec4:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4))
    elif buffer.get_type() == BufferType.mat4:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.float32).reshape((count, 4, 4))
    elif buffer.get_type() == BufferType.rgba8:
        count = buffer.get_count()
        return np.frombuffer(buffer.to_bytearray(), dtype=np.uint8).reshape((count, 4))
    else:
        raise NotImplementedError(f"unable to convert buffer {buffer} to numpy array")

Utils Module

The utils module provides converter utilities for the Matplotlib backend.

gsp_matplotlib.utils

Utility module for GSP Matplotlib conversions.

ConverterUtils

Utility class for converting GSP types to Matplotlib types.

Source code in src/gsp_matplotlib/utils/converter_utils.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class ConverterUtils:
    """Utility class for converting GSP types to Matplotlib types."""

    @staticmethod
    def cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal["butt", "round", "projecting"]:
        """Convert CapStyle enum to Matplotlib string.

        Args:
            gsp_cap_style (CapStyle): The GSP cap style.

        Returns:
            str: The corresponding Matplotlib cap style.
        """
        if gsp_cap_style == CapStyle.BUTT:
            return "butt"
        elif gsp_cap_style == CapStyle.ROUND:
            return "round"
        elif gsp_cap_style == CapStyle.PROJECTING:
            return "projecting"
        else:
            raise ValueError(f"Unsupported CapStyle: {gsp_cap_style}")

    @staticmethod
    def join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal["miter", "round", "bevel"]:
        """Convert JoinStyle enum to Matplotlib string.

        Args:
            gsp_join_style (JoinStyle): The GSP join style.

        Returns:
            str: The corresponding Matplotlib join style.
        """
        if gsp_join_style == JoinStyle.MITER:
            return "miter"
        elif gsp_join_style == JoinStyle.ROUND:
            return "round"
        elif gsp_join_style == JoinStyle.BEVEL:
            return "bevel"
        else:
            raise ValueError(f"Unsupported JoinStyle: {gsp_join_style}")

    @staticmethod
    def marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str:
        """Convert GSP marker shape to Matplotlib marker shape.

        Args:
            gsp_marker_shape (MarkerShape): The GSP marker shape.

        Returns:
            str: The corresponding Matplotlib marker shape.
        """
        if gsp_marker_shape == MarkerShape.disc:
            mpl_marker_shape = "o"
        elif gsp_marker_shape == MarkerShape.square:
            mpl_marker_shape = "s"
        elif gsp_marker_shape == MarkerShape.club:
            mpl_marker_shape = r"$\clubsuit$"
        else:
            raise ValueError(f"Unsupported marker shape: {gsp_marker_shape}")

        return mpl_marker_shape

cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal['butt', 'round', 'projecting'] staticmethod

Convert CapStyle enum to Matplotlib string.

Parameters:

Name Type Description Default
gsp_cap_style gsp.types.CapStyle

The GSP cap style.

required

Returns:

Name Type Description
str typing.Literal['butt', 'round', 'projecting']

The corresponding Matplotlib cap style.

Source code in src/gsp_matplotlib/utils/converter_utils.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@staticmethod
def cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal["butt", "round", "projecting"]:
    """Convert CapStyle enum to Matplotlib string.

    Args:
        gsp_cap_style (CapStyle): The GSP cap style.

    Returns:
        str: The corresponding Matplotlib cap style.
    """
    if gsp_cap_style == CapStyle.BUTT:
        return "butt"
    elif gsp_cap_style == CapStyle.ROUND:
        return "round"
    elif gsp_cap_style == CapStyle.PROJECTING:
        return "projecting"
    else:
        raise ValueError(f"Unsupported CapStyle: {gsp_cap_style}")

join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal['miter', 'round', 'bevel'] staticmethod

Convert JoinStyle enum to Matplotlib string.

Parameters:

Name Type Description Default
gsp_join_style gsp.types.JoinStyle

The GSP join style.

required

Returns:

Name Type Description
str typing.Literal['miter', 'round', 'bevel']

The corresponding Matplotlib join style.

Source code in src/gsp_matplotlib/utils/converter_utils.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@staticmethod
def join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal["miter", "round", "bevel"]:
    """Convert JoinStyle enum to Matplotlib string.

    Args:
        gsp_join_style (JoinStyle): The GSP join style.

    Returns:
        str: The corresponding Matplotlib join style.
    """
    if gsp_join_style == JoinStyle.MITER:
        return "miter"
    elif gsp_join_style == JoinStyle.ROUND:
        return "round"
    elif gsp_join_style == JoinStyle.BEVEL:
        return "bevel"
    else:
        raise ValueError(f"Unsupported JoinStyle: {gsp_join_style}")

marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str staticmethod

Convert GSP marker shape to Matplotlib marker shape.

Parameters:

Name Type Description Default
gsp_marker_shape gsp.types.MarkerShape

The GSP marker shape.

required

Returns:

Name Type Description
str str

The corresponding Matplotlib marker shape.

Source code in src/gsp_matplotlib/utils/converter_utils.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@staticmethod
def marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str:
    """Convert GSP marker shape to Matplotlib marker shape.

    Args:
        gsp_marker_shape (MarkerShape): The GSP marker shape.

    Returns:
        str: The corresponding Matplotlib marker shape.
    """
    if gsp_marker_shape == MarkerShape.disc:
        mpl_marker_shape = "o"
    elif gsp_marker_shape == MarkerShape.square:
        mpl_marker_shape = "s"
    elif gsp_marker_shape == MarkerShape.club:
        mpl_marker_shape = r"$\clubsuit$"
    else:
        raise ValueError(f"Unsupported marker shape: {gsp_marker_shape}")

    return mpl_marker_shape

Converter Utils

gsp_matplotlib.utils.converter_utils

Utility class for converting GSP types to Matplotlib types.

ConverterUtils

Utility class for converting GSP types to Matplotlib types.

Source code in src/gsp_matplotlib/utils/converter_utils.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class ConverterUtils:
    """Utility class for converting GSP types to Matplotlib types."""

    @staticmethod
    def cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal["butt", "round", "projecting"]:
        """Convert CapStyle enum to Matplotlib string.

        Args:
            gsp_cap_style (CapStyle): The GSP cap style.

        Returns:
            str: The corresponding Matplotlib cap style.
        """
        if gsp_cap_style == CapStyle.BUTT:
            return "butt"
        elif gsp_cap_style == CapStyle.ROUND:
            return "round"
        elif gsp_cap_style == CapStyle.PROJECTING:
            return "projecting"
        else:
            raise ValueError(f"Unsupported CapStyle: {gsp_cap_style}")

    @staticmethod
    def join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal["miter", "round", "bevel"]:
        """Convert JoinStyle enum to Matplotlib string.

        Args:
            gsp_join_style (JoinStyle): The GSP join style.

        Returns:
            str: The corresponding Matplotlib join style.
        """
        if gsp_join_style == JoinStyle.MITER:
            return "miter"
        elif gsp_join_style == JoinStyle.ROUND:
            return "round"
        elif gsp_join_style == JoinStyle.BEVEL:
            return "bevel"
        else:
            raise ValueError(f"Unsupported JoinStyle: {gsp_join_style}")

    @staticmethod
    def marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str:
        """Convert GSP marker shape to Matplotlib marker shape.

        Args:
            gsp_marker_shape (MarkerShape): The GSP marker shape.

        Returns:
            str: The corresponding Matplotlib marker shape.
        """
        if gsp_marker_shape == MarkerShape.disc:
            mpl_marker_shape = "o"
        elif gsp_marker_shape == MarkerShape.square:
            mpl_marker_shape = "s"
        elif gsp_marker_shape == MarkerShape.club:
            mpl_marker_shape = r"$\clubsuit$"
        else:
            raise ValueError(f"Unsupported marker shape: {gsp_marker_shape}")

        return mpl_marker_shape

cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal['butt', 'round', 'projecting'] staticmethod

Convert CapStyle enum to Matplotlib string.

Parameters:

Name Type Description Default
gsp_cap_style gsp.types.CapStyle

The GSP cap style.

required

Returns:

Name Type Description
str typing.Literal['butt', 'round', 'projecting']

The corresponding Matplotlib cap style.

Source code in src/gsp_matplotlib/utils/converter_utils.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@staticmethod
def cap_style_gsp_to_mpl(gsp_cap_style: CapStyle) -> Literal["butt", "round", "projecting"]:
    """Convert CapStyle enum to Matplotlib string.

    Args:
        gsp_cap_style (CapStyle): The GSP cap style.

    Returns:
        str: The corresponding Matplotlib cap style.
    """
    if gsp_cap_style == CapStyle.BUTT:
        return "butt"
    elif gsp_cap_style == CapStyle.ROUND:
        return "round"
    elif gsp_cap_style == CapStyle.PROJECTING:
        return "projecting"
    else:
        raise ValueError(f"Unsupported CapStyle: {gsp_cap_style}")

join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal['miter', 'round', 'bevel'] staticmethod

Convert JoinStyle enum to Matplotlib string.

Parameters:

Name Type Description Default
gsp_join_style gsp.types.JoinStyle

The GSP join style.

required

Returns:

Name Type Description
str typing.Literal['miter', 'round', 'bevel']

The corresponding Matplotlib join style.

Source code in src/gsp_matplotlib/utils/converter_utils.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@staticmethod
def join_style_gsp_to_mpl(gsp_join_style: JoinStyle) -> Literal["miter", "round", "bevel"]:
    """Convert JoinStyle enum to Matplotlib string.

    Args:
        gsp_join_style (JoinStyle): The GSP join style.

    Returns:
        str: The corresponding Matplotlib join style.
    """
    if gsp_join_style == JoinStyle.MITER:
        return "miter"
    elif gsp_join_style == JoinStyle.ROUND:
        return "round"
    elif gsp_join_style == JoinStyle.BEVEL:
        return "bevel"
    else:
        raise ValueError(f"Unsupported JoinStyle: {gsp_join_style}")

marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str staticmethod

Convert GSP marker shape to Matplotlib marker shape.

Parameters:

Name Type Description Default
gsp_marker_shape gsp.types.MarkerShape

The GSP marker shape.

required

Returns:

Name Type Description
str str

The corresponding Matplotlib marker shape.

Source code in src/gsp_matplotlib/utils/converter_utils.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@staticmethod
def marker_shape_gsp_to_mpl(gsp_marker_shape: MarkerShape) -> str:
    """Convert GSP marker shape to Matplotlib marker shape.

    Args:
        gsp_marker_shape (MarkerShape): The GSP marker shape.

    Returns:
        str: The corresponding Matplotlib marker shape.
    """
    if gsp_marker_shape == MarkerShape.disc:
        mpl_marker_shape = "o"
    elif gsp_marker_shape == MarkerShape.square:
        mpl_marker_shape = "s"
    elif gsp_marker_shape == MarkerShape.club:
        mpl_marker_shape = r"$\clubsuit$"
    else:
        raise ValueError(f"Unsupported marker shape: {gsp_marker_shape}")

    return mpl_marker_shape

Viewport Events Matplotlib

gsp_matplotlib.viewport_events.viewport_events_matplotlib

MatplotlibRenderer event handler for viewport events.

ViewportEventsMatplotlib

Bases: gsp.types.viewport_events_base.ViewportEventsBase

MatplotlibRenderer event handler for viewport.

Source code in src/gsp_matplotlib/viewport_events/viewport_events_matplotlib.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
class ViewportEventsMatplotlib(ViewportEventsBase):
    """MatplotlibRenderer event handler for viewport."""

    __slots__ = [
        "_renderer",
        "_viewport",
        "_has_key_focus",
        "_mpl_key_press_cid",
        "_mpl_key_release_cid",
        "_mpl_button_press_cid",
        "_mpl_button_release_cid",
        "_mpl_mouse_move_cid",
        "_mpl_scroll_event_cid",
        "_mpl_resize_event_cid",
    ]

    def __init__(self, renderer: MatplotlibRenderer, viewport: Viewport) -> None:
        """Initialize the Matplotlib viewport event handler.

        Args:
            renderer: MatplotlibRenderer associated with this event handler.
            viewport: Viewport associated with this event handler.
        """
        self._renderer = renderer
        """MatplotlibRenderer associated with this event handler"""
        self._viewport = viewport
        """viewport associated with this event handler"""
        self._has_key_focus = False
        """True if this viewport has the keyboard focus"""

        # Intanciate events
        self.key_press_event = Event[KeyboardEventCallback]()
        self.key_release_event = Event[KeyboardEventCallback]()
        self.button_press_event = Event[MouseEventCallback]()
        self.button_release_event = Event[MouseEventCallback]()
        self.mouse_move_event = Event[MouseEventCallback]()
        self.mouse_scroll_event = Event[MouseEventCallback]()
        self.canvas_resize_event = Event[CanvasResizeEventCallback]()

        # event connections
        mpl_canvas: matplotlib.backend_bases.FigureCanvasBase = self._renderer.get_mpl_figure().canvas
        self._mpl_key_press_cid = mpl_canvas.mpl_connect("key_press_event", typing.cast(Any, self._on_key_press))
        self._mpl_key_release_cid = mpl_canvas.mpl_connect("key_release_event", typing.cast(Any, self._on_key_release))
        self._mpl_button_press_cid = mpl_canvas.mpl_connect("button_press_event", typing.cast(Any, self._on_button_press))
        self._mpl_button_release_cid = mpl_canvas.mpl_connect("button_release_event", typing.cast(Any, self._on_button_release))
        self._mpl_mouse_move_cid = mpl_canvas.mpl_connect("motion_notify_event", typing.cast(Any, self._on_mouse_move))
        self._mpl_scroll_event_cid = mpl_canvas.mpl_connect("scroll_event", typing.cast(Any, self._on_mouse_scroll))
        self._mpl_resize_event_cid = mpl_canvas.mpl_connect("resize_event", typing.cast(Any, self._on_canvas_resize))

    def close(self):
        """Close the event handler and release resources."""
        mpl_canvas: matplotlib.backend_bases.FigureCanvasBase = self._renderer.get_mpl_figure().canvas
        if self._mpl_key_press_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_key_press_cid)
            self._mpl_key_press_cid = None
        if self._mpl_key_release_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_key_release_cid)
            self._mpl_key_release_cid = None
        if self._mpl_button_press_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_button_press_cid)
            self._mpl_button_press_cid = None
        if self._mpl_button_release_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_button_release_cid)
            self._mpl_button_release_cid = None
        if self._mpl_mouse_move_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_mouse_move_cid)
            self._mpl_mouse_move_cid = None
        if self._mpl_scroll_event_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_scroll_event_cid)
            self._mpl_scroll_event_cid = None
        if self._mpl_resize_event_cid is not None:
            mpl_canvas.mpl_disconnect(self._mpl_resize_event_cid)
            self._mpl_resize_event_cid = None

    # =============================================================================
    # Matplotlib event handler
    # =============================================================================

    def _on_key_press(self, mpl_key_event: matplotlib.backend_bases.KeyEvent) -> None:
        # discard events outside the viewport
        if self._has_key_focus is False:
            return
        # convert and dispatch event
        keyboard_event = self._mpl_key_event_to_gsp(mpl_key_event, EventType.KEY_PRESS)
        self.key_press_event.dispatch(keyboard_event)

    def _on_key_release(self, mpl_key_event: matplotlib.backend_bases.KeyEvent) -> None:
        # discard events outside the viewport
        if self._has_key_focus is False:
            return
        # convert and dispatch event
        keyboard_event = self._mpl_key_event_to_gsp(mpl_key_event, EventType.KEY_RELEASE)
        self.key_release_event.dispatch(keyboard_event)

    def _on_button_press(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent) -> None:
        # print("matplotlib button press event:", mpl_mouse_event)
        # Set key focus if the event is inside the viewport, otherwise remove key focus
        if self._viewport_contains_mpl_mouse_event(mpl_mouse_event):
            self._has_key_focus = True
        else:
            self._has_key_focus = False

        # discard events outside the viewport
        if self._viewport_contains_mpl_mouse_event(mpl_mouse_event) is False:
            return

        # convert and dispatch event
        mouse_event = self._mpl_mouse_event_to_gsp(mpl_mouse_event, EventType.BUTTON_PRESS)
        self.button_press_event.dispatch(mouse_event)

    def _on_button_release(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent) -> None:
        # print("matplotlib button release event:", mpl_mouse_event)

        # discard events outside the viewport
        if self._viewport_contains_mpl_mouse_event(mpl_mouse_event) is False:
            return
        # convert and dispatch event
        mouse_event = self._mpl_mouse_event_to_gsp(mpl_mouse_event, EventType.BUTTON_RELEASE)
        self.button_release_event.dispatch(mouse_event)

    def _on_mouse_move(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent) -> None:
        # discard events outside the viewport
        if self._viewport_contains_mpl_mouse_event(mpl_mouse_event) is False:
            return
        # convert and dispatch event
        mouse_event = self._mpl_mouse_event_to_gsp(mpl_mouse_event, EventType.MOUSE_MOVE)
        self.mouse_move_event.dispatch(mouse_event)

    def _on_mouse_scroll(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent) -> None:
        # discard events outside the viewport
        if self._viewport_contains_mpl_mouse_event(mpl_mouse_event) is False:
            return
        # convert and dispatch event
        mouse_event = self._mpl_mouse_event_to_gsp(mpl_mouse_event, EventType.MOUSE_SCROLL)
        self.mouse_scroll_event.dispatch(mouse_event)

    def _on_canvas_resize(self, mpl_resize_event: matplotlib.backend_bases.ResizeEvent) -> None:
        # dispatch canvas resize event
        canvas_resize_event = CanvasResizeEvent(
            viewport_uuid=self._viewport.get_uuid(),
            event_type=EventType.CANVAS_RESIZE,
            canvas_width_px=mpl_resize_event.width,
            canvas_height_px=mpl_resize_event.height,
        )
        self.canvas_resize_event.dispatch(canvas_resize_event)

    # =============================================================================
    #
    # =============================================================================

    def _viewport_contains_mpl_mouse_event(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent) -> bool:
        """Check if the matplotlib mouse event is inside this viewport.

        Args:
            mpl_mouse_event: Matplotlib mouse event.

        Returns:
            True if the mouse event is inside this viewport, False otherwise.
        """
        mouse_x = mpl_mouse_event.x / UnitUtils.device_pixel_ratio()
        mouse_y = mpl_mouse_event.y / UnitUtils.device_pixel_ratio()
        if mouse_x < self._viewport.get_x():
            return False
        if mouse_x >= self._viewport.get_x() + self._viewport.get_width():
            return False
        if mouse_y < self._viewport.get_y():
            return False
        if mouse_y >= self._viewport.get_y() + self._viewport.get_height():
            return False
        return True

    # =============================================================================
    # Conversion matplotlib event to gsp events
    # =============================================================================
    def _mpl_mouse_event_to_gsp(self, mpl_mouse_event: matplotlib.backend_bases.MouseEvent, event_type: EventType) -> MouseEvent:
        # Sanity check
        assert self._viewport_contains_mpl_mouse_event(mpl_mouse_event), "Mouse event is outside the viewport"

        mouse_x = mpl_mouse_event.x / UnitUtils.device_pixel_ratio()
        mouse_y = mpl_mouse_event.y / UnitUtils.device_pixel_ratio()
        event_x: float = ((mouse_x - self._viewport.get_x()) / self._viewport.get_width() - 0.5) * 2.0
        event_y: float = ((mouse_y - self._viewport.get_y()) / self._viewport.get_height() - 0.5) * 2.0

        # print(f"Converted mouse_x: {mouse_x}, mouse_y: {mouse_y} to event_x: {event_x}, event_y: {event_y}")

        mouse_event = MouseEvent(
            viewport_uuid=self._viewport.get_uuid(),
            event_type=event_type,
            x_ndc=event_x,
            y_ndc=event_y,
            left_button=mpl_mouse_event.button == 1,
            middle_button=mpl_mouse_event.button == 2,
            right_button=mpl_mouse_event.button == 3,
            scroll_steps=mpl_mouse_event.step if hasattr(mpl_mouse_event, "step") else 0.0,
        )
        return mouse_event

    def _mpl_key_event_to_gsp(self, mpl_key_event: matplotlib.backend_bases.KeyEvent, event_type: EventType) -> KeyEvent:
        assert mpl_key_event.key is not None
        keyboard_event = KeyEvent(
            viewport_uuid=self._viewport.get_uuid(),
            event_type=event_type,
            key_name=mpl_key_event.key,
        )
        return keyboard_event

__init__(renderer: MatplotlibRenderer, viewport: Viewport) -> None

Initialize the Matplotlib viewport event handler.

Parameters:

Name Type Description Default
renderer gsp_matplotlib.renderer.MatplotlibRenderer

MatplotlibRenderer associated with this event handler.

required
viewport gsp.core.Viewport

Viewport associated with this event handler.

required
Source code in src/gsp_matplotlib/viewport_events/viewport_events_matplotlib.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(self, renderer: MatplotlibRenderer, viewport: Viewport) -> None:
    """Initialize the Matplotlib viewport event handler.

    Args:
        renderer: MatplotlibRenderer associated with this event handler.
        viewport: Viewport associated with this event handler.
    """
    self._renderer = renderer
    """MatplotlibRenderer associated with this event handler"""
    self._viewport = viewport
    """viewport associated with this event handler"""
    self._has_key_focus = False
    """True if this viewport has the keyboard focus"""

    # Intanciate events
    self.key_press_event = Event[KeyboardEventCallback]()
    self.key_release_event = Event[KeyboardEventCallback]()
    self.button_press_event = Event[MouseEventCallback]()
    self.button_release_event = Event[MouseEventCallback]()
    self.mouse_move_event = Event[MouseEventCallback]()
    self.mouse_scroll_event = Event[MouseEventCallback]()
    self.canvas_resize_event = Event[CanvasResizeEventCallback]()

    # event connections
    mpl_canvas: matplotlib.backend_bases.FigureCanvasBase = self._renderer.get_mpl_figure().canvas
    self._mpl_key_press_cid = mpl_canvas.mpl_connect("key_press_event", typing.cast(Any, self._on_key_press))
    self._mpl_key_release_cid = mpl_canvas.mpl_connect("key_release_event", typing.cast(Any, self._on_key_release))
    self._mpl_button_press_cid = mpl_canvas.mpl_connect("button_press_event", typing.cast(Any, self._on_button_press))
    self._mpl_button_release_cid = mpl_canvas.mpl_connect("button_release_event", typing.cast(Any, self._on_button_release))
    self._mpl_mouse_move_cid = mpl_canvas.mpl_connect("motion_notify_event", typing.cast(Any, self._on_mouse_move))
    self._mpl_scroll_event_cid = mpl_canvas.mpl_connect("scroll_event", typing.cast(Any, self._on_mouse_scroll))
    self._mpl_resize_event_cid = mpl_canvas.mpl_connect("resize_event", typing.cast(Any, self._on_canvas_resize))

close()

Close the event handler and release resources.

Source code in src/gsp_matplotlib/viewport_events/viewport_events_matplotlib.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def close(self):
    """Close the event handler and release resources."""
    mpl_canvas: matplotlib.backend_bases.FigureCanvasBase = self._renderer.get_mpl_figure().canvas
    if self._mpl_key_press_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_key_press_cid)
        self._mpl_key_press_cid = None
    if self._mpl_key_release_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_key_release_cid)
        self._mpl_key_release_cid = None
    if self._mpl_button_press_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_button_press_cid)
        self._mpl_button_press_cid = None
    if self._mpl_button_release_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_button_release_cid)
        self._mpl_button_release_cid = None
    if self._mpl_mouse_move_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_mouse_move_cid)
        self._mpl_mouse_move_cid = None
    if self._mpl_scroll_event_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_scroll_event_cid)
        self._mpl_scroll_event_cid = None
    if self._mpl_resize_event_cid is not None:
        mpl_canvas.mpl_disconnect(self._mpl_resize_event_cid)
        self._mpl_resize_event_cid = None

Animator Matplotlib

gsp_matplotlib.animator.animator_matplotlib

Matplotlib-based animator for GSP scenes.

Provides animation capabilities using a MatplotlibRenderer backend with support for real-time animation display and video export.

AnimatorMatplotlib

Bases: gsp.types.animator_base.AnimatorBase

Animator for GSP scenes using a Matplotlib renderer.

Manages animation loops with callback functions that update visuals each frame. Supports real-time display and video export in various formats. Uses Matplotlib's FuncAnimation for efficient rendering updates.

Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
class AnimatorMatplotlib(AnimatorBase):
    """Animator for GSP scenes using a Matplotlib renderer.

    Manages animation loops with callback functions that update visuals each frame.
    Supports real-time display and video export in various formats. Uses Matplotlib's
    FuncAnimation for efficient rendering updates.
    """

    def __init__(
        self,
        matplotlib_renderer: MatplotlibRenderer,
        fps: int = 50,
        video_duration: float = 10.0,
        video_path: str | None = None,
        video_writer: str | None = None,
    ):
        """Initialize the Matplotlib animator.

        Args:
            matplotlib_renderer: The Matplotlib renderer to use for rendering frames.
            fps: Target frames per second for the animation.
            video_duration: Total duration of the animation in seconds.
            video_path: Path where the video should be saved. If None, no video is saved.
            video_writer: Video writer to use ("ffmpeg" or "pillow"). If None, auto-detected from extension.

        Raises:
            ValueError: If the video format is not supported.
        """
        self._callbacks: list[AnimatorFunc] = []
        self._matplotlib_renderer = matplotlib_renderer
        self._fps = fps
        self._video_duration = video_duration
        self._video_path = video_path
        self._video_writer: str | None = None
        self._time_last_update: float | None = None

        self._funcAnimation: matplotlib.animation.FuncAnimation | None = None

        self._canvas: Canvas | None = None
        self._viewports: Sequence[Viewport] | None = None
        self._visuals: Sequence[VisualBase] | None = None
        self._model_matrices: Sequence[TransBuf] | None = None
        self._cameras: Sequence[Camera] | None = None

        self.on_video_saved = Event[VideoSavedCalledback]()
        """Event triggered when the video has been successfully saved to disk."""

        # guess the video writer from the file extension if not provided
        if self._video_path is not None:
            if video_writer is not None:
                self._video_writer = video_writer
            else:
                video_ext = os.path.splitext(self._video_path)[1].lower()
                if video_ext in [".mp4", ".m4v", ".mov"]:
                    self._video_writer = "ffmpeg"
                elif video_ext in [".gif", ".apng", ".webp"]:
                    self._video_writer = "pillow"
                else:
                    raise ValueError(f"Unsupported video format: {video_ext}")

    # =============================================================================
    # .add_callback/.remove_callback/.decorator
    # =============================================================================

    def add_callback(self, func: AnimatorFunc) -> None:
        """Add a callback to the animation loop.

        Args:
            func: The animator function to call on each frame.
        """
        self._callbacks.append(func)

    def remove_callback(self, func: AnimatorFunc) -> None:
        """Remove a callback from the animation loop.

        Args:
            func: The animator function to remove.
        """
        self._callbacks.remove(func)

    def event_listener(self, func: AnimatorFunc) -> AnimatorFunc:
        """Decorator to add a callback to the animation loop.

        Args:
            func: The animator function to register as a callback.

        Returns:
            The wrapped animator function.

        Usage:
            ```python
                @animation_loop.event_listener
                def my_callback(delta_time: float) -> Sequence[VisualBase]:
                    ...

                # later, if needed
                animation_loop.remove_callback(my_callback)
            ```
        """
        self.add_callback(func)

        def wrapper(delta_time: float) -> Sequence[VisualBase]:
            # print("Before the function runs")
            result = func(delta_time)
            # print("After the function runs")
            return result

        return wrapper

    # =============================================================================
    # .start()
    # =============================================================================
    def start(self, viewports: Sequence[Viewport], visuals: Sequence[VisualBase], model_matrices: Sequence[TransBuf], cameras: Sequence[Camera]) -> None:
        """Start the animation loop.

        Begins rendering frames using registered callbacks to update visuals.
        In test mode (GSP_TEST=True), saves a single preview image instead of animating.

        Args:
            viewports: Sequence of viewport regions to render into.
            visuals: Sequence of visual elements to render and animate.
            model_matrices: Sequence of model transformation matrices.
            cameras: Sequence of cameras defining view and projection.
        """
        self._canvas = self._matplotlib_renderer.get_canvas()
        self._viewports = viewports
        self._visuals = visuals
        self._model_matrices = model_matrices
        self._cameras = cameras
        self._time_last_update = time.time()

        # =============================================================================
        # Render the image once
        # =============================================================================

        self._matplotlib_renderer.render(viewports, visuals, model_matrices, cameras)

        # =============================================================================
        # Handle GSP_TEST=True
        # =============================================================================

        # detect if we are in not interactive mode - used during testing
        in_test = "GSP_TEST" in os.environ and os.environ["GSP_TEST"] == "True"

        # if we are not in interactive mode, save a preview image and return
        if in_test == True:
            # notify all animator callbacks
            changed_visuals: list[VisualBase] = []
            for animator_callback in self._callbacks:
                _changed_visuals = animator_callback(1.0 / self._fps)
                changed_visuals.extend(_changed_visuals)

            # render the scene to get the new image
            image_png_data = self._matplotlib_renderer.render(viewports, visuals, model_matrices, cameras, return_image=True, image_format="png")
            # get the main script name
            main_script_name = os.path.basename(__main__.__file__) if hasattr(__main__, "__file__") else "interactive"
            main_script_basename = os.path.splitext(main_script_name)[0]
            # buid the output image path
            image_path = os.path.join(__dirname__, "../../../examples/output", f"{main_script_basename}_animator_matplotlib.png")
            image_path = os.path.abspath(image_path)
            # save image_png_data in a image file
            with open(image_path, "wb") as image_file:
                image_file.write(image_png_data)
            # log the event
            print(f"Saved animation preview image to: {image_path}")
            return

        # NOTE: here we are in interactive mode!!

        # =============================================================================
        # Initialize the animation
        # =============================================================================

        figure = self._matplotlib_renderer.get_mpl_figure()
        self._funcAnimation = matplotlib.animation.FuncAnimation(
            figure, self._mpl_animate, frames=int(self._video_duration * self._fps), interval=1000.0 / self._fps
        )

        # save the animation if a path is provided
        if self._video_path is not None:
            self._funcAnimation.save(self._video_path, writer=self._video_writer, fps=self._fps)
            # Dispatch the video saved event
            self.on_video_saved.dispatch()

        # =============================================================================
        # Show the animation
        # =============================================================================

        self._matplotlib_renderer.show()

    # =============================================================================
    # .stop()
    # =============================================================================
    def stop(self):
        """Stop the animation loop.

        Stops the Matplotlib animation timer and clears internal state.
        """
        self._canvas = None
        self._viewports = None
        self._time_last_update = None

        # stop the animation function timer
        if self._funcAnimation is not None:
            self._funcAnimation.event_source.stop()
            self._funcAnimation = None

    # =============================================================================
    # ._mpl_animate()
    # =============================================================================

    def _mpl_animate(self, frame_index: int) -> list[matplotlib.artist.Artist]:
        """Internal callback for Matplotlib animation.

        Called by Matplotlib's FuncAnimation on each frame to update the display.
        Notifies all registered callbacks and re-renders changed visuals.

        Args:
            frame_index: The current frame number in the animation sequence.

        Returns:
            List of Matplotlib artists that were updated during this frame.
        """
        # sanity checks
        assert self._canvas is not None, "Canvas MUST be set during the animation"
        assert self._viewports is not None, "Viewports MUST be set during the animation"
        assert self._visuals is not None, "Visuals MUST be set during the animation"
        assert self._model_matrices is not None, "Model matrices MUST be set during the animation"
        assert self._cameras is not None, "Cameras MUST be set during the animation"

        # compute delta time
        present = time.time()
        delta_time = (present - self._time_last_update) if self._time_last_update is not None else (1 / self._fps)
        self._time_last_update = present

        # notify all animator callbacks
        changed_visuals: list[VisualBase] = []
        for callback in self._callbacks:
            _changed_visuals = callback(delta_time)
            changed_visuals.extend(_changed_visuals)

        # Render the scene to update the visuals
        self._matplotlib_renderer.render(self._viewports, self._visuals, self._model_matrices, self._cameras)

        # convert all changed visuals to mpl artists
        changed_mpl_artists: list[matplotlib.artist.Artist] = []
        for visual in changed_visuals:
            mpl_artists = self._get_mpl_artists(self._viewports, self._visuals, visual)
            changed_mpl_artists.extend(mpl_artists)

        # return the changed mpl artists
        return changed_mpl_artists

    # =============================================================================
    # ._get_mpl_artists()
    # =============================================================================

    def _get_mpl_artists(self, viewports: Sequence[Viewport], visuals: Sequence[VisualBase], visual_base: VisualBase) -> list[matplotlib.artist.Artist]:
        """Get the Matplotlib artists corresponding to a given visual.

        This is needed for Matplotlib's FuncAnimation to update only the relevant artists
        that have changed, improving rendering performance.

        Args:
            viewports: Sequence of all viewports in the scene.
            visuals: Sequence of all visuals in the scene.
            visual_base: The specific visual to get artists for.

        Returns:
            List of Matplotlib Artist objects corresponding to the visual.

        Raises:
            NotImplementedError: If the visual type is not supported.
        """
        mpl_artists: list[matplotlib.artist.Artist] = []

        # Find the index of the visual in the visuals list
        visual_index = visuals.index(visual_base)
        visual = visuals[visual_index]
        viewport = viewports[visual_index]

        if isinstance(visual, Pixels):
            pixels: Pixels = visual
            positions_buffer = TransBufUtils.to_buffer(pixels.get_positions())
            group_count = GroupUtils.get_group_count(positions_buffer.get_count(), pixels.get_groups())
            artist_uuid_prefix = f"{viewport.get_uuid()}_{visual.get_uuid()}"
            for group_index in range(group_count):
                group_uuid = f"{artist_uuid_prefix}_group_{group_index}"
                mpl_artists.append(self._matplotlib_renderer._artists[group_uuid])
        elif isinstance(visual, Points):
            points: Points = visual
            artist_uuid = f"{viewport.get_uuid()}_{points.get_uuid()}"
            mpl_artist = self._matplotlib_renderer._artists[artist_uuid]
            mpl_artists.append(mpl_artist)
        elif isinstance(visual, Texts):
            texts: Texts = visual
            for text_index in range(len(texts.get_strings())):
                artist_uuid = f"{viewport.get_uuid()}_{texts.get_uuid()}_{text_index}"
                mpl_artist = self._matplotlib_renderer._artists[artist_uuid]
                mpl_artists.append(mpl_artist)
        else:
            raise NotImplementedError(f"Getting mpl artists for visual type {type(visual)} is not implemented.")

        return mpl_artists

on_video_saved = Event[VideoSavedCalledback]() instance-attribute

Event triggered when the video has been successfully saved to disk.

__init__(matplotlib_renderer: MatplotlibRenderer, fps: int = 50, video_duration: float = 10.0, video_path: str | None = None, video_writer: str | None = None)

Initialize the Matplotlib animator.

Parameters:

Name Type Description Default
matplotlib_renderer gsp_matplotlib.renderer.MatplotlibRenderer

The Matplotlib renderer to use for rendering frames.

required
fps int

Target frames per second for the animation.

50
video_duration float

Total duration of the animation in seconds.

10.0
video_path str | None

Path where the video should be saved. If None, no video is saved.

None
video_writer str | None

Video writer to use ("ffmpeg" or "pillow"). If None, auto-detected from extension.

None

Raises:

Type Description
ValueError

If the video format is not supported.

Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def __init__(
    self,
    matplotlib_renderer: MatplotlibRenderer,
    fps: int = 50,
    video_duration: float = 10.0,
    video_path: str | None = None,
    video_writer: str | None = None,
):
    """Initialize the Matplotlib animator.

    Args:
        matplotlib_renderer: The Matplotlib renderer to use for rendering frames.
        fps: Target frames per second for the animation.
        video_duration: Total duration of the animation in seconds.
        video_path: Path where the video should be saved. If None, no video is saved.
        video_writer: Video writer to use ("ffmpeg" or "pillow"). If None, auto-detected from extension.

    Raises:
        ValueError: If the video format is not supported.
    """
    self._callbacks: list[AnimatorFunc] = []
    self._matplotlib_renderer = matplotlib_renderer
    self._fps = fps
    self._video_duration = video_duration
    self._video_path = video_path
    self._video_writer: str | None = None
    self._time_last_update: float | None = None

    self._funcAnimation: matplotlib.animation.FuncAnimation | None = None

    self._canvas: Canvas | None = None
    self._viewports: Sequence[Viewport] | None = None
    self._visuals: Sequence[VisualBase] | None = None
    self._model_matrices: Sequence[TransBuf] | None = None
    self._cameras: Sequence[Camera] | None = None

    self.on_video_saved = Event[VideoSavedCalledback]()
    """Event triggered when the video has been successfully saved to disk."""

    # guess the video writer from the file extension if not provided
    if self._video_path is not None:
        if video_writer is not None:
            self._video_writer = video_writer
        else:
            video_ext = os.path.splitext(self._video_path)[1].lower()
            if video_ext in [".mp4", ".m4v", ".mov"]:
                self._video_writer = "ffmpeg"
            elif video_ext in [".gif", ".apng", ".webp"]:
                self._video_writer = "pillow"
            else:
                raise ValueError(f"Unsupported video format: {video_ext}")

add_callback(func: AnimatorFunc) -> None

Add a callback to the animation loop.

Parameters:

Name Type Description Default
func gsp.types.animator_types.AnimatorFunc

The animator function to call on each frame.

required
Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
101
102
103
104
105
106
107
def add_callback(self, func: AnimatorFunc) -> None:
    """Add a callback to the animation loop.

    Args:
        func: The animator function to call on each frame.
    """
    self._callbacks.append(func)

event_listener(func: AnimatorFunc) -> AnimatorFunc

Decorator to add a callback to the animation loop.

Parameters:

Name Type Description Default
func gsp.types.animator_types.AnimatorFunc

The animator function to register as a callback.

required

Returns:

Type Description
gsp.types.animator_types.AnimatorFunc

The wrapped animator function.

Usage
    @animation_loop.event_listener
    def my_callback(delta_time: float) -> Sequence[VisualBase]:
        ...

    # later, if needed
    animation_loop.remove_callback(my_callback)
Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def event_listener(self, func: AnimatorFunc) -> AnimatorFunc:
    """Decorator to add a callback to the animation loop.

    Args:
        func: The animator function to register as a callback.

    Returns:
        The wrapped animator function.

    Usage:
        ```python
            @animation_loop.event_listener
            def my_callback(delta_time: float) -> Sequence[VisualBase]:
                ...

            # later, if needed
            animation_loop.remove_callback(my_callback)
        ```
    """
    self.add_callback(func)

    def wrapper(delta_time: float) -> Sequence[VisualBase]:
        # print("Before the function runs")
        result = func(delta_time)
        # print("After the function runs")
        return result

    return wrapper

remove_callback(func: AnimatorFunc) -> None

Remove a callback from the animation loop.

Parameters:

Name Type Description Default
func gsp.types.animator_types.AnimatorFunc

The animator function to remove.

required
Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
109
110
111
112
113
114
115
def remove_callback(self, func: AnimatorFunc) -> None:
    """Remove a callback from the animation loop.

    Args:
        func: The animator function to remove.
    """
    self._callbacks.remove(func)

start(viewports: Sequence[Viewport], visuals: Sequence[VisualBase], model_matrices: Sequence[TransBuf], cameras: Sequence[Camera]) -> None

Start the animation loop.

Begins rendering frames using registered callbacks to update visuals. In test mode (GSP_TEST=True), saves a single preview image instead of animating.

Parameters:

Name Type Description Default
viewports typing.Sequence[gsp.core.viewport.Viewport]

Sequence of viewport regions to render into.

required
visuals typing.Sequence[gsp.types.visual_base.VisualBase]

Sequence of visual elements to render and animate.

required
model_matrices typing.Sequence[gsp.types.transbuf.TransBuf]

Sequence of model transformation matrices.

required
cameras typing.Sequence[gsp.core.camera.Camera]

Sequence of cameras defining view and projection.

required
Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def start(self, viewports: Sequence[Viewport], visuals: Sequence[VisualBase], model_matrices: Sequence[TransBuf], cameras: Sequence[Camera]) -> None:
    """Start the animation loop.

    Begins rendering frames using registered callbacks to update visuals.
    In test mode (GSP_TEST=True), saves a single preview image instead of animating.

    Args:
        viewports: Sequence of viewport regions to render into.
        visuals: Sequence of visual elements to render and animate.
        model_matrices: Sequence of model transformation matrices.
        cameras: Sequence of cameras defining view and projection.
    """
    self._canvas = self._matplotlib_renderer.get_canvas()
    self._viewports = viewports
    self._visuals = visuals
    self._model_matrices = model_matrices
    self._cameras = cameras
    self._time_last_update = time.time()

    # =============================================================================
    # Render the image once
    # =============================================================================

    self._matplotlib_renderer.render(viewports, visuals, model_matrices, cameras)

    # =============================================================================
    # Handle GSP_TEST=True
    # =============================================================================

    # detect if we are in not interactive mode - used during testing
    in_test = "GSP_TEST" in os.environ and os.environ["GSP_TEST"] == "True"

    # if we are not in interactive mode, save a preview image and return
    if in_test == True:
        # notify all animator callbacks
        changed_visuals: list[VisualBase] = []
        for animator_callback in self._callbacks:
            _changed_visuals = animator_callback(1.0 / self._fps)
            changed_visuals.extend(_changed_visuals)

        # render the scene to get the new image
        image_png_data = self._matplotlib_renderer.render(viewports, visuals, model_matrices, cameras, return_image=True, image_format="png")
        # get the main script name
        main_script_name = os.path.basename(__main__.__file__) if hasattr(__main__, "__file__") else "interactive"
        main_script_basename = os.path.splitext(main_script_name)[0]
        # buid the output image path
        image_path = os.path.join(__dirname__, "../../../examples/output", f"{main_script_basename}_animator_matplotlib.png")
        image_path = os.path.abspath(image_path)
        # save image_png_data in a image file
        with open(image_path, "wb") as image_file:
            image_file.write(image_png_data)
        # log the event
        print(f"Saved animation preview image to: {image_path}")
        return

    # NOTE: here we are in interactive mode!!

    # =============================================================================
    # Initialize the animation
    # =============================================================================

    figure = self._matplotlib_renderer.get_mpl_figure()
    self._funcAnimation = matplotlib.animation.FuncAnimation(
        figure, self._mpl_animate, frames=int(self._video_duration * self._fps), interval=1000.0 / self._fps
    )

    # save the animation if a path is provided
    if self._video_path is not None:
        self._funcAnimation.save(self._video_path, writer=self._video_writer, fps=self._fps)
        # Dispatch the video saved event
        self.on_video_saved.dispatch()

    # =============================================================================
    # Show the animation
    # =============================================================================

    self._matplotlib_renderer.show()

stop()

Stop the animation loop.

Stops the Matplotlib animation timer and clears internal state.

Source code in src/gsp_matplotlib/animator/animator_matplotlib.py
230
231
232
233
234
235
236
237
238
239
240
241
242
def stop(self):
    """Stop the animation loop.

    Stops the Matplotlib animation timer and clears internal state.
    """
    self._canvas = None
    self._viewports = None
    self._time_last_update = None

    # stop the animation function timer
    if self._funcAnimation is not None:
        self._funcAnimation.event_source.stop()
        self._funcAnimation = None