Repository: projectmesa/mesa

Issue:
rename altair.py and matplotlib.py
The naming of `mesa.visualization.components.altair` and `mesa.visualization.components.matplotlib` can cause namespace clashes. They thus need to be renamed to e.g., `altair_components` and `matplotlib_components`.

Discussion:
github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]      | Run time [95% CI]       |
|-----------------|--------|-------------------------|-------------------------|
| BoltzmannWealth | small  | 🔵 -0.9% [-1.7%, -0.0%] | 🔵 -0.1% [-0.2%, +0.0%] |
| BoltzmannWealth | large  | 🔵 -0.3% [-0.7%, +0.1%] | 🔵 -0.7% [-1.5%, -0.0%] |
| Schelling       | small  | 🔵 -0.4% [-0.8%, +0.0%] | 🔵 -1.1% [-1.3%, -0.9%] |
| Schelling       | large  | 🔵 +0.7% [-0.4%, +1.7%] | 🔵 -0.5% [-2.3%, +1.7%] |
| WolfSheep       | small  | 🔵 +0.1% [-0.3%, +0.9%] | 🔵 +0.8% [+0.6%, +1.0%] |
| WolfSheep       | large  | 🔵 -0.6% [-1.3%, +0.2%] | 🔵 -1.5% [-2.5%, -0.4%] |
| BoidFlockers    | small  | 🔵 +0.4% [-0.3%, +1.1%] | 🔵 -0.9% [-1.6%, -0.4%] |
| BoidFlockers    | large  | 🔵 +0.4% [-0.5%, +1.0%] | 🔵 +0.1% [-0.5%, +0.7%] |

Corvince: On the first question I would go for 2), generic API with specific backends. Also I have been thinking about possibly removing Altair completely for 3.0 and then add the backend keyword and the Altair drawing functions in 3.1.

This would give us more time to have a proper Altair implementation for all spaces and probably also adding interactive elements to the Altair plots, e.g. clicking on agents to get additional information/ change the model. This isn't hard to do in Altair, but I would like to have some time to have this truly stable. 
quaquel: >  On the first question I would go for 2), generic API with specific backends. Also I have been thinking about possibly removing Altair completely for 3.0 and then add the backend keyword and the Altair drawing functions in 3.1.

We were broadly in agreement to declare `visualization.components` experimental. That would give us the same possibility for implementing the Altair side properly for 3.1. One concern I have at the moment is that I am not convinced it is possible to abstract away the backend with the current status of the API. Declaring the entire components namespace experimental would give us more leeway if needed. 


Corvince: > We were broadly in agreement to declare visualization.components experimental

Was there already an agreement on that? Then that is fine as well. I proposed that somewhere, but the alternative was to have a good component API, which you single handedly provided in the last days :D 
So personally I would be happy with declaring it stable already. But declaring it experimental would also work
quaquel: > Was there already an agreement on that? Then that is fine as well. 

I think @EwoutH agreed with your suggestion. I want to keep it experimental, with the understanding that we aim to implement Altair with a compatible API. We will only break the API it if we can't find a clean solution for altair within the confines of the API established for matplotlib. 

I'll do more reorganization of the code, such as moving `make_space_component` and `make_plot_components` up, while adding backend specific versions to `altair_compoments` and `matplotlib_components. This will lay the groundwork for 3.1.
quaquel: Ok, I have reorganized things further.
1. In `matplotlib_components` and `altair_components` there are now `make_(backend)_(type)_component` functions. Note that Altair only has space at the moment
2. I added generic `make_space_component` and `make_plot_component` that take a backend keyword argument (next to all the other kwargs). This gives a common backend-independent API.
3. I updated all examples to use the generic make_space_component and make_plot_component, although they all use the matplotlib backend. As part of the Altair work, we should move a few of them to Altair. 
4. I updated tests and docs were needed to cover everything. 

Note that some of the stuff is implemented with an eye towards the altair rework. If @EwoutH agrees, it would make sense to add the experimental warnings as well (I can do it here or in a follow up PR).

I think this PR is ready for review. 

EwoutH: I can dive into this Monday, if you would like to move forward before then I trust both your judgement.
EwoutH: What's the minimal input you currently need to unblock this PR?
EwoutH: A few simple points:
1. I find it a bit inconsistent that `matplotlib` sometimes gets shortened to `mpl` and sometimes doesn't.
2. `components.matplotlib_components` feels redundant. What if we only allow importing directly from `components.matplotlib`, and not add them to a lower level `__all__`?
3. I don't see that much added value in `make_space_component` with an keyword argument now. I don't see us supporting more than a few backends, so I would suggest explicit `make_space_matplotlib` and `make_space_altair` functions
quaquel: > 1. I find it a bit inconsistent that matplotlib sometimes gets shortened to mpl and sometimes doesn't.

I found that the names became rather long if I did not do it, but I am fine with changing it. 

> 2. components.matplotlib_components feels redundant. What if we only allow importing directly from components.matplotlib, and not add them to a lower level __all__?

Not sure I follow. At a minimum, the name `matplotlib` should not be used because it can give rise to namespace clashes when also importing `matplotlib` itself, the same applies to altair. It is what motivated this PR in the first place (#2434).

> 3. I don't see that much added value in make_space_component with an keyword argument now. I don't see us supporting more than a few backends, so I would suggest explicit make_space_matplotlib and make_space_altair functions. We can document and type hint those better, and check in each of that type of space is supported.

I followed @Corvince's suggestion based on my question at the start of this PR. I am fine with dropping it and only keeping the backend-specific ones. The benefit, however, of doing it this way is that it enforces us to have a common API for both backends. This is something that is quite desirable from a user point of view.

I haven't bothered with comprehensive type hinting yet. I started doing it using `@overload`, but I need to do a bit more reading on how that interacts with different values for keyword arguments. 
EwoutH: Sounds it was well thought out, if you and Vincent align, I'm good
quaquel: Ok, let's see what @Corvince makes of this PR.
Corvince: I agree with this PR very much. On question 2) I would agree to promote `mpl_space_drawing` to mesa.visualization. Its neither component nor solara viz specific. 

On matplotlib vs mpl: I agree that matplotlib names become a bit unwieldy due to their length. But I also think mpl is something to stumple over. Still I think mpl is the bit better solution, since I don't expect it being used that much directly. API-wise I think a simple user-facing solution is to just use `from mesa.visualization import draw_space, make_space_component`. Its not really necessary to import the invidiual `make_mpl_space_component`, so its not very user-facing. 

Following this logic it might also make sense to define draw_space outside of mpl_space_drawing, since from a user-perspecive its more important to describe *what* we want to do (draw space) than to describe *how* we want to do it (using matplotlib)
Corvince: Unrelated, and I wanted to create a separate PR but its hard to get the timing right with respect to the reorganizing of the files:

`draw_space` currently takes the `post_process` argument. Since this works on the `ax` but we already return the `ax` I don't think thats necessary. I think it should be just an option for `make_space_component`. If you agree maybe you can just change it to this PR? (and then apply post process in make_mpl_space_component on the return value of draw_space)
quaquel: >  agree with this PR very much. On question 2) I would agree to promote mpl_space_drawing to mesa.visualization. Its neither component nor solara viz specific.

Ok, I'll make that move.

> On matplotlib vs mpl: I agree that matplotlib names become a bit unwieldy due to their length. But I also think mpl is something to stumple over. Still I think mpl is the bit better solution, since I don't expect it being used that much directly. API-wise I think a simple user-facing solution is to just use from mesa.visualization import draw_space, make_space_component. Its not really necessary to import the invidiual make_mpl_space_component, so its not very user-facing.

I agree, imports should be a the `mesa.visualization` level for the public part of the API. Its only for more sophisticated control that a user might want to dive deeper but these are more advanced users. 

> Following this logic it might also make sense to define draw_space outside of mpl_space_drawing, since from a user-perspecive its more important to describe what we want to do (draw space) than to describe how we want to do it (using matplotlib)

Not sure I follow your reasoning here. 

Would that not create redundancy with the `make_space_component` because then we would end up with `make_space_component` and `draw_space` both of which would take a `backend` kwarg. Conceptually, I agree that they are different, and `draw_space` might be used more widely (e.g., in VideoViz #2453). 

Also, at the moment we don't have an Altair equivalent for the space drawing functions.

Last, if we make this change, should that not imply that all draw_x functins in `mpl_spqace_drawing` would need to have `mpl` or something equivalent in their name? if we get to implementing an Altair equivalent to `mpl_space_drawing,` we would risk getting namespace clashes if we have the same function names in both. 
quaquel: Ok, I have moved `mpl_space_drawing.py` from `mesa.visualization.components` into `mesa.visualization`. I have updated the API docs to reflect this move. I also slightly changed what is importable from `mesa.visualization`. The current version is backward compatible with main, while including the new generic `make_space_component`, `make_plot_component`, and `draw_space`.

I think that only leaves open a choice on `draw_space`
quaquel: > draw_space currently takes the post_process argument. Since this works on the ax but we already return the ax I don't think thats necessary. I think it should be just an option for make_space_component. If you agree maybe you can just change it to this PR? (and then apply post process in make_mpl_space_component on the return value of draw_space)

good point, I'll make that change here
wang-boyu: Thanks for the work here. It is pretty similar to the refactoring part in https://github.com/projectmesa/mesa/pull/2453/, with the main differences in API styles (OO vs. functional).

Some comments from my side -

On the matplotlib plotting functions:

1. We both extracted matplotlib plotting functions into a separate file, here `mpl_space_drawing.py`. I would  suggest to extract measure plottting function there too, apart from `draw_space`, which might be useful for other Viz that needs to plot measures using matplotlib. In addition, if `mpl_space_drawing.py` contains these measure plotting function, then a more generic name such as `mpl_drawing.py` might be better, since it's not just about space drawing.
2. Also regarding the matplotlib plotting functions, I would suggest to limit the amount of public APIs, such as `draw_orthogonal_grid`, `draw_hex_grid`, which are prone to future updates and hence breaking changes.

On the SolaraViz components:

1. Similarly to https://github.com/projectmesa/mesa/pull/2453/, all SolaraViz stuff are kept in `solara_viz.py` and the `components` folder. Main different is where we put `make_space_component`. Here it is in `components/__init__.py`, which makes sense, since it's about creating a Solara component.
   I put it in `solara_viz.py` because there's also a `make_space_component` for VideoViz. As a result, we can have
   ```python
   from mesa.visualization.video_viz import VideoViz, make_space_component, make_measure_component
   from mesa.visualization.solara_viz import SolaraViz, make_space_component, make_measure_component
   ```

I think we overall agree on most of the stuff. Personally I don't have strong preferences on either OO-style or functional style. In summary, we could perhaps view the SolaraViz feature as some sort of "frontend" while matplotlib and altair are its "backend". So we could have in the future (apologies for the poor drawing)

```mermaid
graph LR
    A(SolaraViz) --- B(matplotlib)
    A(SolaraViz) --- C(altair)
    A(SolaraViz) --- D(other_backend)
    E(VideoViz) --- B(matplotlib)
    F(StreamlitViz) --- B(matplotlib)
```

The "frontend"s share common API in their creations:

```python
page = SolaraViz(
    model,
    [make_space_component(agent_portrayal=agent_portrayal),
     make_measure_component("happy")],
)

viz = VideoViz(
    model,
    [make_space_component(agent_portrayal=agent_portrayal),
     make_measure_component("happy")],
)
```

but differ in how there are used:

```python
page   # simply use the page to render Solara webpage

viz.record(steps=50, filepath="schelling.mp4")
```
quaquel: Thanks for the feedback @wang-boyu. Some quick reactions

> I would suggest to extract measure plottting function there too.

I checked the code but at the moment there is no detailed measure plotting helper functions in matplotlib_components.py. If they are added in the future, I agree, that whatever is useful beyond solara should be in a seperate matplotlib/altair plotting file.

> Also regarding the matplotlib plotting functions, I would suggest to limit the amount of public APIs, 

I agree. That's why I removed them from the `__init__` in `mesa.visualization`. So they are available, but you have to dig into the api docs to find them. We could add a leading underscore to them if desired, but at the moment I find that a bit of overkill. Also, it seems that @Corvince, @EwoutH, and me all broadly agree that mesa.visualization is work in progress and experimental (but with minimum API breaking changes unless absolutely necessary. 

> I think we overall agree on most of the stuff. Personally I don't have strong preferences on either OO-style or functional style. In summary, we could perhaps view the SolaraViz feature as some sort of "frontend" while matplotlib and altair are its "backend"

I want to hash out an OO version but not for MESA 3.0. I also completely agree on the altair/matplotlib/plotly(?) backend idea. 
wang-boyu: I suppose we're waiting for @Corvince's comments now? Also curious about what @EwoutH thinks regarding our changes.
quaquel: > I suppose we're waiting for @Corvince's comments now? Also curious about what @EwoutH thinks regarding our changes.

Yes, I would like to get @Corvince's perspective on the `draw_space` function and then this is ready to be merged. Given that @wang-boyu, @Corvince and I agree on this refactoring, I think @EwoutH will happily accept our view, but I'll chat with him today about it. 

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b2c81407..23783860 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ ci:
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: v0.6.3
+  rev: v0.7.2
   hooks:
     # Run the linter.
     - id: ruff
@@ -14,12 +14,12 @@ repos:
     - id: ruff-format
       types_or: [ python, pyi, jupyter ]
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v3.17.0
+    rev: v3.19.0
     hooks:
     -   id: pyupgrade
         args: [--py310-plus]
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.6.0  # Use the ref you want to point at
+    rev: v5.0.0  # Use the ref you want to point at
     hooks:
     -   id: trailing-whitespace
     -   id: check-toml
diff --git a/docs/apis/visualization.md b/docs/apis/visualization.md
index 7815ec56..7c39dae5 100644
--- a/docs/apis/visualization.md
+++ b/docs/apis/visualization.md
@@ -21,19 +21,26 @@ For a detailed tutorial, please refer to our [Visualization Tutorial](../tutoria
 ```
 
 
-## Matplotlib-based components
+## Matplotlib-based visualizations
 
 ```{eval-rst}
-.. automodule:: mesa.visualization.components.matplotlib
+.. automodule:: mesa.visualization.components.matplotlib_components
    :members:
    :undoc-members:
    :show-inheritance:
 ```
 
-## Altair-based components
+```{eval-rst}
+.. automodule:: mesa.visualization.mpl_space_drawing
+   :members:
+   :undoc-members:
+   :show-inheritance:
+```
+
+## Altair-based visualizations
 
 ```{eval-rst}
-.. automodule:: mesa.visualization.components.altair
+.. automodule:: mesa.visualization.components.altair_components
    :members:
    :undoc-members:
    :show-inheritance:
diff --git a/mesa/examples/advanced/pd_grid/app.py b/mesa/examples/advanced/pd_grid/app.py
index fafedde6..6664e2c1 100644
--- a/mesa/examples/advanced/pd_grid/app.py
+++ b/mesa/examples/advanced/pd_grid/app.py
@@ -3,7 +3,11 @@ Solara-based visualization for the Spatial Prisoner's Dilemma Model.
 """
 
 from mesa.examples.advanced.pd_grid.model import PdGrid
-from mesa.visualization import SolaraViz, make_plot_component, make_space_component
+from mesa.visualization import (
+    SolaraViz,
+    make_plot_component,
+    make_space_component,
+)
 from mesa.visualization.UserParam import Slider
 
 
diff --git a/mesa/examples/basic/boid_flockers/app.py b/mesa/examples/basic/boid_flockers/app.py
index bcecb0a3..e374185c 100644
--- a/mesa/examples/basic/boid_flockers/app.py
+++ b/mesa/examples/basic/boid_flockers/app.py
@@ -51,7 +51,7 @@ model = BoidFlockers()
 
 page = SolaraViz(
     model,
-    [make_space_component(agent_portrayal=boid_draw)],
+    [make_space_component(agent_portrayal=boid_draw, backend="matplotlib")],
     model_params=model_params,
     name="Boid Flocking Model",
 )
diff --git a/mesa/examples/basic/boltzmann_wealth_model/app.py b/mesa/examples/basic/boltzmann_wealth_model/app.py
index ff329ab9..7f92cbbb 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/app.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/app.py
@@ -1,5 +1,9 @@
 from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealthModel
-from mesa.visualization import SolaraViz, make_plot_component, make_space_component
+from mesa.visualization import (
+    SolaraViz,
+    make_plot_component,
+    make_space_component,
+)
 
 
 def agent_portrayal(agent):
diff --git a/mesa/visualization/__init__.py b/mesa/visualization/__init__.py
index 4bac9870..66597b0e 100644
--- a/mesa/visualization/__init__.py
+++ b/mesa/visualization/__init__.py
@@ -1,7 +1,11 @@
 """Solara based visualization for Mesa models."""
 
-from .components.altair import make_space_altair
-from .components.matplotlib import make_plot_component, make_space_component
+from mesa.visualization.mpl_space_drawing import (
+    draw_space,
+)
+
+from .components import make_plot_component, make_space_component
+from .components.altair_components import make_space_altair
 from .solara_viz import JupyterViz, SolaraViz
 from .UserParam import Slider
 
@@ -10,6 +14,7 @@ __all__ = [
     "SolaraViz",
     "Slider",
     "make_space_altair",
-    "make_space_component",
+    "draw_space",
     "make_plot_component",
+    "make_space_component",
 ]
diff --git a/mesa/visualization/components/__init__.py b/mesa/visualization/components/__init__.py
new file mode 100644
index 00000000..4b70fc2b
--- /dev/null
+++ b/mesa/visualization/components/__init__.py
@@ -0,0 +1,83 @@
+"""custom solara components."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from .altair_components import SpaceAltair, make_altair_space
+from .matplotlib_components import (
+    SpaceMatplotlib,
+    make_mpl_plot_component,
+    make_mpl_space_component,
+)
+
+
+def make_space_component(
+    agent_portrayal: Callable | None = None,
+    propertylayer_portrayal: dict | None = None,
+    post_process: Callable | None = None,
+    backend: str = "matplotlib",
+    **space_drawing_kwargs,
+) -> SpaceMatplotlib | SpaceAltair:
+    """Create a Matplotlib-based space visualization component.
+
+    Args:
+        agent_portrayal: Function to portray agents.
+        propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
+        post_process : a callable that will be called with the Axes instance. Allows for fine-tuning plots (e.g., control ticks)
+        backend: the backend to use {"matplotlib", "altair"}
+        space_drawing_kwargs : additional keyword arguments to be passed on to the underlying backend specific space drawer function. See
+                               the functions for drawing the various spaces for the appropriate backend further details.
+
+
+    Returns:
+        function: A function that creates a space component
+    """
+    if backend == "matplotlib":
+        return make_mpl_space_component(
+            agent_portrayal,
+            propertylayer_portrayal,
+            post_process,
+            **space_drawing_kwargs,
+        )
+    elif backend == "altair":
+        return make_altair_space(
+            agent_portrayal,
+            propertylayer_portrayal,
+            post_process,
+            **space_drawing_kwargs,
+        )
+    else:
+        raise ValueError(
+            f"unknown backend {backend}, must be one of matplotlib, altair"
+        )
+
+
+def make_plot_component(
+    measure: str | dict[str, str] | list[str] | tuple[str],
+    post_process: Callable | None = None,
+    backend: str = "matplotlib",
+    **plot_drawing_kwargs,
+):
+    """Create a plotting function for a specified measure using the specified backend.
+
+    Args:
+        measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
+        post_process: a user-specified callable to do post-processing called with the Axes instance.
+        backend: the backend to use {"matplotlib", "altair"}
+        plot_drawing_kwargs: additional keyword arguments to pass onto the backend specific function for making a plotting component
+
+    Notes:
+        altair plotting backend is not yet implemented and planned for mesa 3.1.
+
+    Returns:
+        function: A function that creates a plot component
+    """
+    if backend == "matplotlib":
+        return make_mpl_plot_component(measure, post_process, **plot_drawing_kwargs)
+    elif backend == "altair":
+        raise NotImplementedError("altair line plots are not yet implemented")
+    else:
+        raise ValueError(
+            f"unknown backend {backend}, must be one of matplotlib, altair"
+        )
diff --git a/mesa/visualization/components/altair.py b/mesa/visualization/components/altair_components.py
similarity index 81%
rename from mesa/visualization/components/altair.py
rename to mesa/visualization/components/altair_components.py
index 5aeee847..b610e46f 100644
--- a/mesa/visualization/components/altair.py
+++ b/mesa/visualization/components/altair_components.py
@@ -1,6 +1,7 @@
 """Altair based solara components for visualization mesa spaces."""
 
 import contextlib
+import warnings
 
 import solara
 
@@ -12,7 +13,33 @@ from mesa.space import ContinuousSpace, _Grid
 from mesa.visualization.utils import update_counter
 
 
-def make_space_altair(agent_portrayal=None):  # noqa: D103
+def make_space_altair(*args, **kwargs):  # noqa: D103
+    warnings.warn(
+        "make_space_altair has been renamed to make_altair_space",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return make_altair_space(*args, **kwargs)
+
+
+def make_altair_space(
+    agent_portrayal, propertylayer_portrayal, post_process, **space_drawing_kwargs
+):
+    """Create an Altair-based space visualization component.
+
+    Args:
+        agent_portrayal: Function to portray agents.
+        propertylayer_portrayal: not yet implemented
+        post_process :not yet implemented
+        space_drawing_kwargs : not yet implemented
+
+    ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
+    "size", "marker", and "zorder". Other field are ignored and will result in a user warning.
+
+
+    Returns:
+        function: A function that creates a SpaceMatplotlib component
+    """
     if agent_portrayal is None:
 
         def agent_portrayal(a):
@@ -25,7 +52,12 @@ def make_space_altair(agent_portrayal=None):  # noqa: D103
 
 
 @solara.component
-def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):  # noqa: D103
+def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
+    """Create an Altair-based space visualization component.
+
+    Returns:
+        a solara FigureAltair instance
+    """
     update_counter.get()
     space = getattr(model, "grid", None)
     if space is None:
diff --git a/mesa/visualization/components/matplotlib_components.py b/mesa/visualization/components/matplotlib_components.py
new file mode 100644
index 00000000..6c7bb1ae
--- /dev/null
+++ b/mesa/visualization/components/matplotlib_components.py
@@ -0,0 +1,176 @@
+"""Matplotlib based solara components for visualization MESA spaces and plots."""
+
+from __future__ import annotations
+
+import warnings
+from collections.abc import Callable
+
+import matplotlib.pyplot as plt
+import solara
+from matplotlib.figure import Figure
+
+from mesa.visualization.mpl_space_drawing import draw_space
+from mesa.visualization.utils import update_counter
+
+
+def make_space_matplotlib(*args, **kwargs):  # noqa: D103
+    warnings.warn(
+        "make_space_matplotlib has been renamed to make_mpl_space_component",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return make_mpl_space_component(*args, **kwargs)
+
+
+def make_mpl_space_component(
+    agent_portrayal: Callable | None = None,
+    propertylayer_portrayal: dict | None = None,
+    post_process: Callable | None = None,
+    **space_drawing_kwargs,
+) -> SpaceMatplotlib:
+    """Create a Matplotlib-based space visualization component.
+
+    Args:
+        agent_portrayal: Function to portray agents.
+        propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
+        post_process : a callable that will be called with the Axes instance. Allows for fine tuning plots (e.g., control ticks)
+        space_drawing_kwargs : additional keyword arguments to be passed on to the underlying space drawer function. See
+                               the functions for drawing the various spaces for further details.
+
+    ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
+    "size", "marker", and "zorder". Other field are ignored and will result in a user warning.
+
+
+    Returns:
+        function: A function that creates a SpaceMatplotlib component
+    """
+    if agent_portrayal is None:
+
+        def agent_portrayal(a):
+            return {}
+
+    def MakeSpaceMatplotlib(model):
+        return SpaceMatplotlib(
+            model,
+            agent_portrayal,
+            propertylayer_portrayal,
+            post_process=post_process,
+            **space_drawing_kwargs,
+        )
+
+    return MakeSpaceMatplotlib
+
+
+@solara.component
+def SpaceMatplotlib(
+    model,
+    agent_portrayal,
+    propertylayer_portrayal,
+    dependencies: list[any] | None = None,
+    post_process: Callable | None = None,
+    **space_drawing_kwargs,
+):
+    """Create a Matplotlib-based space visualization component."""
+    update_counter.get()
+
+    space = getattr(model, "grid", None)
+    if space is None:
+        space = getattr(model, "space", None)
+
+    fig = Figure()
+    ax = fig.add_subplot()
+
+    draw_space(
+        space,
+        agent_portrayal,
+        propertylayer_portrayal=propertylayer_portrayal,
+        ax=ax,
+        **space_drawing_kwargs,
+    )
+
+    if post_process is not None:
+        post_process(ax)
+
+    solara.FigureMatplotlib(
+        fig, format="png", bbox_inches="tight", dependencies=dependencies
+    )
+
+
+def make_plot_measure(*args, **kwargs):  # noqa: D103
+    warnings.warn(
+        "make_plot_measure has been renamed to make_plot_component",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return make_mpl_plot_component(*args, **kwargs)
+
+
+def make_mpl_plot_component(
+    measure: str | dict[str, str] | list[str] | tuple[str],
+    post_process: Callable | None = None,
+    save_format="png",
+):
+    """Create a plotting function for a specified measure.
+
+    Args:
+        measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
+        post_process: a user-specified callable to do post-processing called with the Axes instance.
+        save_format: save format of figure in solara backend
+
+    Returns:
+        function: A function that creates a PlotMatplotlib component.
+    """
+
+    def MakePlotMatplotlib(model):
+        return PlotMatplotlib(
+            model, measure, post_process=post_process, save_format=save_format
+        )
+
+    return MakePlotMatplotlib
+
+
+@solara.component
+def PlotMatplotlib(
+    model,
+    measure,
+    dependencies: list[any] | None = None,
+    post_process: Callable | None = None,
+    save_format="png",
+):
+    """Create a Matplotlib-based plot for a measure or measures.
+
+    Args:
+        model (mesa.Model): The model instance.
+        measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
+        dependencies (list[any] | None): Optional dependencies for the plot.
+        post_process: a user-specified callable to do post-processing called with the Axes instance.
+        save_format: format used for saving the figure.
+
+    Returns:
+        solara.FigureMatplotlib: A component for rendering the plot.
+    """
+    update_counter.get()
+    fig = Figure()
+    ax = fig.subplots()
+    df = model.datacollector.get_model_vars_dataframe()
+    if isinstance(measure, str):
+        ax.plot(df.loc[:, measure])
+        ax.set_ylabel(measure)
+    elif isinstance(measure, dict):
+        for m, color in measure.items():
+            ax.plot(df.loc[:, m], label=m, color=color)
+        ax.legend(loc="best")
+    elif isinstance(measure, list | tuple):
+        for m in measure:
+            ax.plot(df.loc[:, m], label=m)
+        ax.legend(loc="best")
+
+    if post_process is not None:
+        post_process(ax)
+
+    ax.set_xlabel("Step")
+    # Set integer x axis
+    ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
+    solara.FigureMatplotlib(
+        fig, format=save_format, bbox_inches="tight", dependencies=dependencies
+    )
diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/mpl_space_drawing.py
similarity index 77%
rename from mesa/visualization/components/matplotlib.py
rename to mesa/visualization/mpl_space_drawing.py
index 2bda984f..6353d810 100644
--- a/mesa/visualization/components/matplotlib.py
+++ b/mesa/visualization/mpl_space_drawing.py
@@ -1,4 +1,10 @@
-"""Matplotlib based solara components for visualization MESA spaces and plots."""
+"""Helper functions for drawing mesa spaces with matplotlib.
+
+These functions are used by the provided matplotlib components, but can also be used to quickly visualize
+a space with matplotlib for example when creating a mp4 of a movie run or when needing a figure
+for a paper.
+
+"""
 
 import itertools
 import math
@@ -6,15 +12,13 @@ import warnings
 from collections.abc import Callable
 from typing import Any
 
-import matplotlib.pyplot as plt
 import networkx as nx
 import numpy as np
-import solara
+from matplotlib import pyplot as plt
 from matplotlib.axes import Axes
 from matplotlib.cm import ScalarMappable
 from matplotlib.collections import PatchCollection
 from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
-from matplotlib.figure import Figure
 from matplotlib.patches import RegularPolygon
 
 import mesa
@@ -32,95 +36,12 @@ from mesa.space import (
     PropertyLayer,
     SingleGrid,
 )
-from mesa.visualization.utils import update_counter
 
-# For typing
 OrthogonalGrid = SingleGrid | MultiGrid | OrthogonalMooreGrid | OrthogonalVonNeumannGrid
 HexGrid = HexSingleGrid | HexMultiGrid | mesa.experimental.cell_space.HexGrid
 Network = NetworkGrid | mesa.experimental.cell_space.Network
 
 
-def make_space_matplotlib(*args, **kwargs):  # noqa: D103
-    warnings.warn(
-        "make_space_matplotlib has been renamed to make_space_component",
-        DeprecationWarning,
-        stacklevel=2,
-    )
-    return make_space_component(*args, **kwargs)
-
-
-def make_space_component(
-    agent_portrayal: Callable | None = None,
-    propertylayer_portrayal: dict | None = None,
-    post_process: Callable | None = None,
-    **space_drawing_kwargs,
-):
-    """Create a Matplotlib-based space visualization component.
-
-    Args:
-        agent_portrayal: Function to portray agents.
-        propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
-        post_process : a callable that will be called with the Axes instance. Allows for fine tuning plots (e.g., control ticks)
-        space_drawing_kwargs : additional keyword arguments to be passed on to the underlying space drawer function. See
-                               the functions for drawing the various spaces for further details.
-
-    ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
-    "size", "marker", and "zorder". Other field are ignored and will result in a user warning.
-
-
-    Returns:
-        function: A function that creates a SpaceMatplotlib component
-    """
-    if agent_portrayal is None:
-
-        def agent_portrayal(a):
-            return {}
-
-    def MakeSpaceMatplotlib(model):
-        return SpaceMatplotlib(
-            model,
-            agent_portrayal,
-            propertylayer_portrayal,
-            post_process=post_process,
-            **space_drawing_kwargs,
-        )
-
-    return MakeSpaceMatplotlib
-
-
-@solara.component
-def SpaceMatplotlib(
-    model,
-    agent_portrayal,
-    propertylayer_portrayal,
-    dependencies: list[any] | None = None,
-    post_process: Callable | None = None,
-    **space_drawing_kwargs,
-):
-    """Create a Matplotlib-based space visualization component."""
-    update_counter.get()
-
-    space = getattr(model, "grid", None)
-    if space is None:
-        space = getattr(model, "space", None)
-
-    fig = Figure()
-    ax = fig.add_subplot()
-
-    draw_space(
-        space,
-        agent_portrayal,
-        propertylayer_portrayal=propertylayer_portrayal,
-        ax=ax,
-        post_process=post_process,
-        **space_drawing_kwargs,
-    )
-
-    solara.FigureMatplotlib(
-        fig, format="png", bbox_inches="tight", dependencies=dependencies
-    )
-
-
 def collect_agent_data(
     space: OrthogonalGrid | HexGrid | Network | ContinuousSpace | VoronoiGrid,
     agent_portrayal: Callable,
@@ -173,7 +94,6 @@ def draw_space(
     agent_portrayal: Callable,
     propertylayer_portrayal: dict | None = None,
     ax: Axes | None = None,
-    post_process: Callable | None = None,
     **space_drawing_kwargs,
 ):
     """Draw a Matplotlib-based visualization of the space.
@@ -184,8 +104,6 @@ def draw_space(
         propertylayer_portrayal: a dict specifying how to show propertylayer(s)
         ax: the axes upon which to draw the plot
         post_process: a callable called with the Axes instance
-        postprocess: a user-specified callable to do post-processing called with the Axes instance. This callable
-        can be used for any further fine-tuning of the plot (e.g., changing ticks, etc.)
         space_drawing_kwargs: any additional keyword arguments to be passed on to the underlying function for drawing the space.
 
     Returns:
@@ -214,9 +132,6 @@ def draw_space(
     if propertylayer_portrayal:
         draw_property_layers(space, propertylayer_portrayal, ax=ax)
 
-    if post_process is not None:
-        post_process(ax=ax)
-
     return ax
 
 
@@ -641,83 +556,3 @@ def _scatter(ax: Axes, arguments, **kwargs):
                 **{k: v[logical] for k, v in arguments.items()},
                 **kwargs,
             )
-
-
-def make_plot_measure(*args, **kwargs):  # noqa: D103
-    warnings.warn(
-        "make_plot_measure has been renamed to make_plot_component",
-        DeprecationWarning,
-        stacklevel=2,
-    )
-    return make_plot_component(*args, **kwargs)
-
-
-def make_plot_component(
-    measure: str | dict[str, str] | list[str] | tuple[str],
-    post_process: Callable | None = None,
-    save_format="png",
-):
-    """Create a plotting function for a specified measure.
-
-    Args:
-        measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
-        post_process: a user-specified callable to do post-processing called with the Axes instance.
-        save_format: save format of figure in solara backend
-
-    Returns:
-        function: A function that creates a PlotMatplotlib component.
-    """
-
-    def MakePlotMatplotlib(model):
-        return PlotMatplotlib(
-            model, measure, post_process=post_process, save_format=save_format
-        )
-
-    return MakePlotMatplotlib
-
-
-@solara.component
-def PlotMatplotlib(
-    model,
-    measure,
-    dependencies: list[any] | None = None,
-    post_process: Callable | None = None,
-    save_format="png",
-):
-    """Create a Matplotlib-based plot for a measure or measures.
-
-    Args:
-        model (mesa.Model): The model instance.
-        measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
-        dependencies (list[any] | None): Optional dependencies for the plot.
-        post_process: a user-specified callable to do post-processing called with the Axes instance.
-        save_format: format used for saving the figure.
-
-    Returns:
-        solara.FigureMatplotlib: A component for rendering the plot.
-    """
-    update_counter.get()
-    fig = Figure()
-    ax = fig.subplots()
-    df = model.datacollector.get_model_vars_dataframe()
-    if isinstance(measure, str):
-        ax.plot(df.loc[:, measure])
-        ax.set_ylabel(measure)
-    elif isinstance(measure, dict):
-        for m, color in measure.items():
-            ax.plot(df.loc[:, m], label=m, color=color)
-        ax.legend(loc="best")
-    elif isinstance(measure, list | tuple):
-        for m in measure:
-            ax.plot(df.loc[:, m], label=m)
-        ax.legend(loc="best")
-
-    if post_process is not None:
-        post_process(ax)
-
-    ax.set_xlabel("Step")
-    # Set integer x axis
-    ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
-    solara.FigureMatplotlib(
-        fig, format=save_format, bbox_inches="tight", dependencies=dependencies
-    )
diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index 4bca98d8..d5d83f43 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Literal
 import reacton.core
 import solara
 
-import mesa.visualization.components.altair as components_altair
+import mesa.visualization.components.altair_components as components_altair
 from mesa.visualization.UserParam import Slider
 from mesa.visualization.utils import force_update, update_counter
 


Test patch:
diff --git a/tests/test_components_matplotlib.py b/tests/test_components_matplotlib.py
index c85dd1ce..9c454e77 100644
--- a/tests/test_components_matplotlib.py
+++ b/tests/test_components_matplotlib.py
@@ -17,7 +17,7 @@ from mesa.space import (
     PropertyLayer,
     SingleGrid,
 )
-from mesa.visualization.components.matplotlib import (
+from mesa.visualization.mpl_space_drawing import (
     draw_continuous_space,
     draw_hex_grid,
     draw_network,
diff --git a/tests/test_solara_viz.py b/tests/test_solara_viz.py
index af6badd0..92766966 100644
--- a/tests/test_solara_viz.py
+++ b/tests/test_solara_viz.py
@@ -6,9 +6,9 @@ import ipyvuetify as vw
 import solara
 
 import mesa
-import mesa.visualization.components.altair
-import mesa.visualization.components.matplotlib
-from mesa.visualization.components.matplotlib import make_space_component
+import mesa.visualization.components.altair_components
+import mesa.visualization.components.matplotlib_components
+from mesa.visualization.components.matplotlib_components import make_mpl_space_component
 from mesa.visualization.solara_viz import Slider, SolaraViz, UserInputs
 
 
@@ -88,10 +88,12 @@ class TestMakeUserInput(unittest.TestCase):  # noqa: D101
 
 def test_call_space_drawer(mocker):  # noqa: D103
     mock_space_matplotlib = mocker.spy(
-        mesa.visualization.components.matplotlib, "SpaceMatplotlib"
+        mesa.visualization.components.matplotlib_components, "SpaceMatplotlib"
     )
 
-    mock_space_altair = mocker.spy(mesa.visualization.components.altair, "SpaceAltair")
+    mock_space_altair = mocker.spy(
+        mesa.visualization.components.altair_components, "SpaceAltair"
+    )
 
     model = mesa.Model()
     mocker.patch.object(mesa.Model, "__init__", return_value=None)
@@ -103,7 +105,9 @@ def test_call_space_drawer(mocker):  # noqa: D103
     propertylayer_portrayal = None
     # initialize with space drawer unspecified (use default)
     # component must be rendered for code to run
-    solara.render(SolaraViz(model, components=[make_space_component(agent_portrayal)]))
+    solara.render(
+        SolaraViz(model, components=[make_mpl_space_component(agent_portrayal)])
+    )
     # should call default method with class instance and agent portrayal
     mock_space_matplotlib.assert_called_with(
         model, agent_portrayal, propertylayer_portrayal, post_process=None
@@ -114,7 +118,7 @@ def test_call_space_drawer(mocker):  # noqa: D103
     solara.render(SolaraViz(model))
     # should call default method with class instance and agent portrayal
     assert mock_space_matplotlib.call_count == 0
-    assert mock_space_altair.call_count > 0
+    assert mock_space_altair.call_count == 0
 
     # specify a custom space method
     class AltSpace:
@@ -132,7 +136,7 @@ def test_call_space_drawer(mocker):  # noqa: D103
         centroids_coordinates=[(0, 1), (0, 0), (1, 0)],
     )
     solara.render(
-        SolaraViz(voronoi_model, components=[make_space_component(agent_portrayal)])
+        SolaraViz(voronoi_model, components=[make_mpl_space_component(agent_portrayal)])
     )