Introduction of Space Renderer
Summary
Introduces SpaceRenderer into the Mesa visualization system, aiming to simplify and decouple the logic of drawing agents, grids, and property layers.
Motive
Part of my GSOC commitment.
Implementation
This PR introduces 2 new files, named space_renderer.py and space_drawers.py.
-
space_renderer.py:- Contains the
SpaceRendererclass, the main backend for Solara to render the space, agents, and property layers. - Key methods include:
-
draw_structure(): Draws the grid structure of the space. -
draw_propertylayer(): Draws property layers onto the space. -
draw_agents(): Draws agents onto the space. -
render(): A comprehensive method to draw all components at once.
-
- Manages different rendering backends (Matplotlib, Altair).
- Handles data collection for agents for both matplotlib and altair and maps agent coordinates.
- Contains the
-
space_drawers.py:- Provides specialized drawer classes for different Mesa space types:
-
OrthogonalSpaceDrawer: For standard grid spaces. -
HexSpaceDrawer: For hexagonal grid spaces. -
NetworkSpaceDrawer: For network-based spaces. -
ContinuousSpaceDrawer: For continuous spaces. -
VoronoiSpaceDrawer: For Voronoi diagram spaces.
-
- These classes are used internally by
SpaceRendererto handle the specific drawing logic for each space structure as they contain the specific grid/space drawing logic.
- Provides specialized drawer classes for different Mesa space types:
Usage Examples
# for matplolib
renderer = SpaceRenderer(model, ax=ax, backend='matplotlib')
renderer.draw_structure()
renderer.draw_agents(agent_portrayal)
renderer.draw_propertylayer(propertylayer_portrayal)
# if want agents on different ax:
fig, ax = plt.subplot()
renderer.draw_agnets(agent_portrayal, ax=ax)
# for altair, no need to pass ax, if passed will be ignored with a warning
renderer.SpaceRenderer(model, backend="altair")
# if want to apply different modifications to the matplotlib axes
renderer.canvas.set_title("This is a Title")
renderer.canvas.set_xlabel("X-axis")
renderer.canvas.set_ylabel("Y-axis")
renderer.canvas.set_aspect("equal")
renderer.canvas.figure.set_size_inches(10, 10)
page = SolaraViz(
model,
renderer,
components=[...],
...
)
page
Additional Context
Checkout the discussion in #2772. Currently only contains the full coverage for matplotlib spaces, only Orthogonal spaces are available for altair (with property layers).
Summary by CodeRabbit
-
New Features
- Introduced advanced visualization capabilities for agent-based model spaces, supporting orthogonal grids, hex grids, continuous spaces, Voronoi grids, and network grids.
- Added a unified renderer that enables layered rendering of space structures, agents, and property layers using both matplotlib and Altair backends.
- Enhanced the interactive visualization experience in the Solara interface with new components and rendering options.
Performance benchmarks:
| Model | Size | Init time [95% CI] | Run time [95% CI] |
|---|---|---|---|
| BoltzmannWealth | small | π΅ +0.9% [-0.2%, +2.2%] | π΅ +0.3% [+0.0%, +0.5%] |
| BoltzmannWealth | large | π΅ +0.1% [-0.7%, +0.8%] | π΅ +2.0% [+0.5%, +3.5%] |
| Schelling | small | π΅ +0.4% [+0.2%, +0.6%] | π΅ -0.5% [-0.7%, -0.3%] |
| Schelling | large | π΅ +0.3% [-1.5%, +3.2%] | π΅ -4.1% [-6.7%, -1.2%] |
| WolfSheep | small | π΅ +0.3% [-0.1%, +0.6%] | π΅ -0.7% [-0.8%, -0.5%] |
| WolfSheep | large | π΅ +1.0% [+0.6%, +1.3%] | π΅ -0.7% [-1.0%, -0.3%] |
| BoidFlockers | small | π΅ +1.6% [+0.6%, +2.6%] | π΅ +0.3% [+0.1%, +0.5%] |
| BoidFlockers | large | π΅ +1.6% [+1.0%, +2.1%] | π΅ -0.6% [-1.0%, -0.2%] |
@coderabbitai full review
Walkthrough
A new SpaceRenderer class and supporting drawer modules are introduced for advanced visualization of Mesa model spaces, supporting multiple space types and rendering backends. The visualization API is updated to require and expose SpaceRenderer, with new Solara components and controllers integrated to utilize this renderer for flexible, layered agent-based model visualizations.
Changes
| File(s) | Change Summary |
|---|---|
| mesa/visualization/init.py | Imported SpaceRenderer and added it to the __all__ list, making it a public API component. |
| mesa/visualization/solara_viz.py | Updated Solara visualization to require a renderer argument, integrate space visualization via SpaceRenderer, and add new components. |
| mesa/visualization/space_drawers.py | Introduced new module with drawer classes for orthogonal, hex, network, continuous, and Voronoi spaces, supporting matplotlib/Altair. |
| mesa/visualization/space_renderer.py | Added new SpaceRenderer class for rendering model spaces, agents, and property layers with support for multiple backends and formats. |
| mesa/visualization/backends/init.py | Added new module exporting AltairBackend and MatplotlibBackend as visualization backends. |
| mesa/visualization/backends/abstract_renderer.py | Added abstract base class AbstractRenderer defining interface for visualization backends with methods for drawing structure, agents, and property layers. |
| mesa/visualization/backends/altair_backend.py | Added AltairBackend class implementing visualization backend using Altair for interactive spatial visualizations. |
| mesa/visualization/backends/matplotlib_backend.py | Added MatplotlibBackend class implementing visualization backend using Matplotlib for static spatial visualizations. |
Sequence Diagram(s)
sequenceDiagram
participant User
participant SolaraViz
participant SpaceRenderer
participant Model
User->>SolaraViz: Initialize with Model and SpaceRenderer
SolaraViz->>Model: Access model's space/grid
SolaraViz->>SpaceRenderer: Set space to model's space
User->>SolaraViz: Interact (step, reset, etc.)
SolaraViz->>SpaceRenderer: render(agent_portrayal, propertylayer_portrayal)
SpaceRenderer->>Model: Collect agent/property data
SpaceRenderer->>SpaceRenderer: Draw structure, agents, property layers
SpaceRenderer-->>SolaraViz: Return visualization component
SolaraViz-->>User: Display updated visualization
Poem
A rabbit with a painter's flair,
Now draws your grids with utmost care.
Hex, network, or orthogonal lines,
Spaces renderedβhow it shines!
With Solara's help, the models gleam,
Agents dance in a vivid dream.
πβ¨
β¨ Finishing Touches
- [ ] π Generate Docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
πͺ§ Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
-
I pushed a fix in commit <commit_id>, please review it. -
Explain this complex logic. -
Open a follow-up GitHub issue for this discussion.
-
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. Examples:-
@coderabbitai explain this code block. -
@coderabbitai modularize this function.
-
- PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:-
@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase. -
@coderabbitai read src/utils.ts and explain its main purpose. -
@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format. -
@coderabbitai help me debug CodeRabbit configuration file.
-
Support
Need help? Create a ticket on our support page for assistance with any issues or questions.
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.
CodeRabbit Commands (Invoked using PR comments)
-
@coderabbitai pauseto pause the reviews on a PR. -
@coderabbitai resumeto resume the paused reviews. -
@coderabbitai reviewto trigger an incremental review. This is useful when automatic reviews are disabled for the repository. -
@coderabbitai full reviewto do a full review from scratch and review all the files again. -
@coderabbitai summaryto regenerate the summary of the PR. -
@coderabbitai generate docstringsto generate docstrings for this PR. -
@coderabbitai generate sequence diagramto generate a sequence diagram of the changes in this PR. -
@coderabbitai resolveresolve all the CodeRabbit review comments. -
@coderabbitai configurationto show the current CodeRabbit configuration for the repository. -
@coderabbitai helpto get help.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
Documentation and Community
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.
@Sahil-Chhoker some thoughts for you (let me know what you think)--
Goal: Make drawing more extensible so it is easier to integrate other visualization libraries (e.g. plotly).
- How: Possibly build a base_render or somethingclass so it is easier to integrate other visualization libraries
Goal: Speed up - How: With the above could lazy load the import modules so only the library being used is called - How: Other ways to optimize loading of agents but i think this would go beyond the scope of this effort and may be a next year GSOC thing.
Goal: Easier user build How hard would it be to get this down to one line for quick and dirty visualizations, and then users can expand it for more customization
renderer = SpaceRenderer(model, ax=ax, backend='matplotlib')
renderer.draw_structure()
renderer.draw_agents(agent_portrayal)
renderer.draw_propertylayer(propertylayer_portrayal)
Can users still pass in custom drawing like with @Holzhauer and #2799
My biggest concern is we need to break it and make it more extensible.
I will say your code is excellent. The amount of work, and your code and comments are incredibly clean.
Thoughts @EwoutH , @quaquel , @jackiekazil
Goal: Make drawing more extensible so it is easier to integrate other visualization libraries (e.g. plotly). - How: Possibly build a
base_renderor somethingclass so it is easier to integrate other visualization libraries
One really quick idea that comes to mind after hearing this is having the main SpaceRenderer that holds the reference to two MatplotlibBackend and AltairBackend (and potentially PlotlyBackend in the future). The main drawing components are divided into the library specific backends and common code stays, well, common in the SpaceRenderer.
Goal: Speed up - How: With the above could lazy load the import modules so only the library being used is called - How: Other ways to optimize loading of agents but i think this would go beyond the scope of this effort and may be a next year GSOC thing.
Will have to think it through.
Goal: Easier user build How hard would it be to get this down to one line for quick and dirty visualizations, and then users can expand it for more customization
Just do:
renderer.render(agent_portrayal, propertylayer_portrayal)
# renderer.render() only draws structure
# renderer.render(agent_portrayal) draws agents on structure
# renderer.render(propertylayer_portrayal) draws property layers on structure
Can users still pass in custom drawing like with @Holzhauer and #2799
Yes the components parameter in the solara_viz retains and is the best possible way in my opinion to make and draw custom components rather than having some sloppy cover for it.
My biggest concern is we need to break it and make it more extensible.
This is still experimental, so its okay.
I will say your code is excellent. The amount of work, and your code and comments are incredibly clean.
Thanks!
A more concrete form of my idea, @tpike3 let me know your thoughts.
class AbstractRendererBackend(ABC):
"""Abstract base class for a visualization backend."""
def __init__(self, space, space_drawer, map_coords_func):
self.space = space
self.space_drawer = space_drawer
self.map_coords_func = map_coords_func
self.space_mesh = None
self.agent_mesh = None
self.propertylayer_mesh = None
self.canvas = None
@abstractmethod
def initialize_canvas(self, **kwargs):
"""Set up the drawing canvas (e.g., Matplotlib Axes, Altair Chart)."""
@abstractmethod
def draw_structure(self, **kwargs):
"""Draw the space structure (grid lines, etc.)."""
@abstractmethod
def draw_agents(self, agent_portrayal, **kwargs):
"""Collect agent data and draw them."""
@abstractmethod
def draw_propertylayer(self, propertylayer_portrayal, **kwargs):
"""Draw property layers."""
@abstractmethod
def render(self, agent_portrayal, propertylayer_portrayal, **kwargs):
"""Render the entire visualization."""
def clear_meshes(self):
"""Clear all stored mesh objects."""
self.space_mesh = None
self.agent_mesh = None
self.propertylayer_mesh = None
class MatplotlibBackend(AbstractRendererBackend):
"""A renderer that uses Matplotlib."""
def initialize_canvas(self, ax=None, **kwargs):
...
def draw_structure(self, **kwargs):
...
def _collect_agent_data(self, agent_portrayal, default_size):
...
def _draw_agents_on_ax(self, ax, arguments, **kwargs):
...
def draw_agents(self, agent_portrayal, **kwargs):
...
def draw_propertylayer(self, propertylayer_portrayal, **kwargs):
...
def render(self, agent_portrayal, propertylayer_portrayal, **kwargs):
...
...
class AltairBackend(AbstractRendererBackend):
"""A renderer that uses Altair."""
def initialize_canvas(self, **kwargs):
...
def draw_structure(self, **kwargs):
...
def _collect_agent_data(self, agent_portrayal, default_size):
...
def draw_agents(self, agent_portrayal, **kwargs):
...
def draw_propertylayer(self, propertylayer_portrayal, **kwargs):
...
def render(self, agent_portrayal, propertylayer_portrayal, **kwargs):
...
class SpaceRenderer:
def __init__(self, model, backend: str = "matplotlib", **kwargs):
self.space = getattr(model, "grid", getattr(model, "space", None))
self.space_drawer = self._get_space_drawer()
self.backend = self._create_backend(backend, **kwargs)
def _create_backend(self, backend_name: str, **kwargs):
"""Factory method to create the appropriate backend."""
if backend_name == "matplotlib":
backend_cls = MatplotlibBackend
elif backend_name == "altair":
backend_cls = AltairBackend
else:
raise ValueError(f"Unsupported backend: '{backend_name}'")
backend = backend_cls(self.space, self.space_drawer, self._map_coordinates)
backend.initialize_canvas(**kwargs)
return backend
def _get_space_drawer(self):
...
def _map_coordinates(self, arguments):
...
def draw_structure(self, **kwargs):
return self.backend.draw_structure(**kwargs)
def draw_agents(self, agent_portrayal, **kwargs):
return self.backend.draw_agents(agent_portrayal, **kwargs)
def draw_propertylayer(self, propertylayer_portrayal, **kwargs):
return self.backend.draw_propertylayer(propertylayer_portrayal, **kwargs)
def render(self, agent_portrayal=None, propertylayer_portrayal=None, **kwargs):
return self.backend.render(agent_portrayal, propertylayer_portrayal, **kwargs)
@property
def canvas(self):
return self.backend.canvas
def clear_meshes(self):
self.backend.clear_meshes()
@Corvince hope you are doing well! I think you might find this (very) interesting.
A more concrete form of my idea, @tpike3 let me know your thoughts.
class AbstractRendererBackend(ABC): """Abstract base class for a visualization backend.""" def __init__(self, space, space_drawer, map_coords_func): self.space = space self.space_drawer = space_drawer self.map_coords_func = map_coords_func self.space_mesh = None self.agent_mesh = None self.propertylayer_mesh = None self.canvas = None @abstractmethod def initialize_canvas(self, **kwargs): """Set up the drawing canvas (e.g., Matplotlib Axes, Altair Chart).""" @abstractmethod def draw_structure(self, **kwargs): """Draw the space structure (grid lines, etc.).""" @abstractmethod def draw_agents(self, agent_portrayal, **kwargs): """Collect agent data and draw them.""" @abstractmethod def draw_propertylayer(self, propertylayer_portrayal, **kwargs): """Draw property layers.""" @abstractmethod def render(self, agent_portrayal, propertylayer_portrayal, **kwargs): """Render the entire visualization.""" def clear_meshes(self): """Clear all stored mesh objects.""" self.space_mesh = None self.agent_mesh = None self.propertylayer_mesh = None class MatplotlibBackend(AbstractRendererBackend): """A renderer that uses Matplotlib.""" def initialize_canvas(self, ax=None, **kwargs): ... def draw_structure(self, **kwargs): ... def _collect_agent_data(self, agent_portrayal, default_size): ... def _draw_agents_on_ax(self, ax, arguments, **kwargs): ... def draw_agents(self, agent_portrayal, **kwargs): ... def draw_propertylayer(self, propertylayer_portrayal, **kwargs): ... def render(self, agent_portrayal, propertylayer_portrayal, **kwargs): ... ... class AltairBackend(AbstractRendererBackend): """A renderer that uses Altair.""" def initialize_canvas(self, **kwargs): ... def draw_structure(self, **kwargs): ... def _collect_agent_data(self, agent_portrayal, default_size): ... def draw_agents(self, agent_portrayal, **kwargs): ... def draw_propertylayer(self, propertylayer_portrayal, **kwargs): ... def render(self, agent_portrayal, propertylayer_portrayal, **kwargs): ... class SpaceRenderer: def __init__(self, model, backend: str = "matplotlib", **kwargs): self.space = getattr(model, "grid", getattr(model, "space", None)) self.space_drawer = self._get_space_drawer() self.backend = self._create_backend(backend, **kwargs) def _create_backend(self, backend_name: str, **kwargs): """Factory method to create the appropriate backend.""" if backend_name == "matplotlib": backend_cls = MatplotlibBackend elif backend_name == "altair": backend_cls = AltairBackend else: raise ValueError(f"Unsupported backend: '{backend_name}'") backend = backend_cls(self.space, self.space_drawer, self._map_coordinates) backend.initialize_canvas(**kwargs) return backend def _get_space_drawer(self): ... def _map_coordinates(self, arguments): ... def draw_structure(self, **kwargs): return self.backend.draw_structure(**kwargs) def draw_agents(self, agent_portrayal, **kwargs): return self.backend.draw_agents(agent_portrayal, **kwargs) def draw_propertylayer(self, propertylayer_portrayal, **kwargs): return self.backend.draw_propertylayer(propertylayer_portrayal, **kwargs) def render(self, agent_portrayal=None, propertylayer_portrayal=None, **kwargs): return self.backend.render(agent_portrayal, propertylayer_portrayal, **kwargs) @property def canvas(self): return self.backend.canvas def clear_meshes(self): self.backend.clear_meshes()
I like it!
You should be able to do a backend registry for SpaceRender so we can avoid a long string of if-else statements. See a gpt generated approach below (so be skeptical). However, I think this approach with each different backend in their own file will make it more intuitively obvious and easier for others or us to extend later on.
One nitpick is the naming conventions maybe just AbstractRender and then MatplotlibRender, AltairRender, but I am never happy with naming conventions, so dont waste cycles on this.
_BACKENDS: dict[str, type[AbstractRendererBackend]] = {}
def register_backend(name: str):
def decorator(cls):
_BACKENDS[name] = cls
return cls
return decorator
@register_backend("matplotlib")
class MatplotlibBackend(AbstractRendererBackend):
...
backend_cls = _BACKENDS.get(backend_name)
if backend_cls is None:
raise ValueError(...)
Got it, will start working on it.
@tpike3, I've added both the backends separately and I think they work fine. Let me know your thoughts. Also about the backend registry, my personal view that at most we can have 3 backends (current ones + plotly) because these are all the libraries solara supports, so I don't feel the need to create such complicated system to select a backend system, but this is just my personal opinion, let me know if you feel otherwise.
@Sahil-Chhok I am on holiday right now, so give me a couple more days to review. However, I do like the new set up
That is a fairpoint about Solara and only having three backends, so no need for a registry.
Is there anyway to support backward compatibility with current visualization or is that an impossible ask?
I am on holiday right now, so give me a couple more days to review.
Sure, just let me know your thoughts whenever you have time.
Is there anyway to support backward compatibility with current visualization or is that an impossible ask?
I've just added new functionality, so the previous one works as is, we can give a deprecation warning there and ask for a shift.
@tpike3, how should I go about the tests?
If I make the SpaceRenderer optional in SolaraViz this will enable us to have backwards compatibility and all the current tests will pass and new tests for SpaceRenderer can be written in a separate file but they will be the exact copy of currents tests, just with SpaceRenderer in them.
Or I can just update the current tests to include the SpaceRenderer but that will prevent backwards compatibility.
(btw I am talking about the solaraviz tests)
I suggest keeping it backward compatible, so we don't need to go to Mesa 4 to use this new stuff. Having duplicate tests is a small price to pay for this, in my view. When moving to Mesa 4, we can just delete the old tests.
I agree with @quaquel , backwards compatibility is critical and some redundancies in tests are a small price to pay
@coderabbitai full review
β Actions performed
Full review triggered.
@Sahil-Chhoker I think you are way ahead of schedule! I think the only thing left for this is PR is the backward compatibility and some of the coderabbitai suggestions.
@EwoutH, @quaquel @jackiekazil -- any other suggestions or thoughts?
Performance benchmarks:
| Model | Size | Init time [95% CI] | Run time [95% CI] |
|---|---|---|---|
| BoltzmannWealth | small | π΅ -0.4% [-1.2%, +0.5%] | π΅ +0.5% [+0.3%, +0.6%] |
| BoltzmannWealth | large | π΅ -1.3% [-2.1%, -0.5%] | π’ -6.5% [-9.6%, -3.0%] |
| Schelling | small | π΅ +0.5% [+0.3%, +0.7%] | π΅ +1.0% [+0.8%, +1.1%] |
| Schelling | large | π΅ -2.0% [-2.8%, -0.9%] | π’ -7.7% [-11.9%, -3.4%] |
| WolfSheep | small | π΅ -0.2% [-0.5%, +0.1%] | π΅ -0.1% [-0.3%, +0.1%] |
| WolfSheep | large | π΅ -0.6% [-1.7%, +0.4%] | π΅ -2.2% [-3.6%, -1.1%] |
| BoidFlockers | small | π΅ -2.1% [-2.6%, -1.5%] | π΅ -0.1% [-0.3%, +0.0%] |
| BoidFlockers | large | π΅ -1.9% [-2.6%, -1.3%] | π΅ +0.4% [+0.1%, +0.7%] |
Thanks for your continued work on this, glad to see it's ready for review!
I will try to review it somewhere this weekend.
Great work on the SpaceRenderer system! The backend abstraction and separation of concerns look really solid. I also really like the API, it's quite elegant overall.
I wanted to discuss one architectural question: should we consider backend-specific subclasses instead of the current string-based backend selection?
Current approach:
renderer = SpaceRenderer(model, backend="matplotlib")
renderer.draw_structure(figsize=(10, 8)) # Works with matplotlib, ignored by altair
Potential subclass approach:
renderer = MatplotlibSpaceRenderer(model)
renderer.draw_structure(figsize=(10, 8)) # Type-safe, IDE knows this parameter
renderer.save_figure("output.png") # Backend-specific methods possible
The main advantage would be type safety - IDEs could provide proper autocomplete and catch invalid parameters at development time rather than runtime. Each backend could also expose specific methods without cluttering the base API.
We could maintain the current simple API with a factory function:
# Simple usage (returns appropriate subclass)
renderer = SpaceRenderer(model, backend="matplotlib")
# Advanced usage (direct class access)
from mesa.visualization.backends import MatplotlibSpaceRenderer
renderer = MatplotlibSpaceRenderer(model)
The common functionality would stay in the base SpaceRenderer class, so code duplication would be minimal. The current architecture already has the backend abstraction that would make this transition straightforward.
The trade-off is slightly more complexity for advanced users who want full type safety, though the factory function would keep the simple cases unchanged.
Thanks for the praise!
Yeah, the current design is intentional, itβs meant to get most users up and running with visualizations quickly, without needing to worry about backend specifics.
The whole idea behind SpaceRenderer is to keep things backend-agnostic. If we start adding subclass-specific methods like matplotlib_renderer.set_grid_lines() or altair_renderer.set_interactive_tooltip(), it kind of defeats the purpose β switching backends would suddenly become messy instead of seamless.
That said, I totally get the appeal of type safety and IDE hints. To give advanced users that flexibility, Iβve already exposed the canvas (ax for Matplotlib, chart for Altair) through SpaceRenderer. So if someone really needs to tweak things at a deeper level, they still can. Those changes already show up in the frontend for Matplotlib, and Iβm working on making it work just as smoothly for Altair too.
Don't different backends fundamentally have different capabilities? And wouldn't it make sense to proactively inform users of those different capabilities? Because now, if you switch backends, and have used some functionality that is supported by one but not by another, you will get a strange, unknown and hard error, instead of an informative, explicit error.
To quote PEP 20:
Explicit is better than implicit.
Errors should never pass silently.
In the runtime error thrown by SolaraViz, it can be clearly seen where the code is wrong and what is not supported.
I have been wondering about this issue ever since I did the cleanup over half a year ago. I see good arguments for both approaches. What I like about @Sahil-Chhoker's approach is that you offer a backend agnostic common interface for default operations, while giving access to the underlying backend-specific details for further fine-tuning. For someone not familiar with the specifics of either back-end, this is convenient.
However, given the fundamentally different API philosophy behind matplotlib and altair, I have been worried about how much of a common interface can be created.
@tpike3, the docstrings really were a disaster. I've tried to improve them, tell me if there is still anything concerning.
@Sahil-Chhoker Running your code in the examples\basic\boltzmann_wealth_model I had some issues, but it may be the way I used it. (Thanks for the recommendation, I should have done this much earlier.)
1 - It wasn't backwards compatible, I could not run the existing code with your changes out of the box (could be because we are still midstride) 2 - Running with the new code, based on your usage example (which I may have misunderstood ) I was getting 2 plots.
Below is my examples\basic\boltzmann_wealth_model\app.py. I could also push to your branch if that is easier.
from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
from mesa.mesa_logging import INFO, log_to_stderr
from mesa.visualization import (
SolaraViz,
SpaceRenderer,
)
log_to_stderr(INFO)
def agent_portrayal(agent):
color = agent.wealth # we are using a colormap to translate wealth to color
return {"color": color}
model_params = {
"seed": {
"type": "InputText",
"value": 42,
"label": "Random Seed",
},
"n": {
"type": "SliderInt",
"value": 50,
"label": "Number of agents:",
"min": 10,
"max": 100,
"step": 1,
},
"width": 10,
"height": 10,
}
# Create initial model instance
model = BoltzmannWealth(50, 10, 10)
renderer = SpaceRenderer(model, backend='matplotlib')
#renderer.draw_structure()
#renderer.draw_agents(agent_portrayal)
#renderer.draw_propertylayer(propertylayer_portrayal)
# Create the SolaraViz page. This will automatically create a server and display the
# visualization elements in a web browser.
# Display it using the following command in the example directory:
# solara run app.py
# It will automatically update and display any changes made to this file
page = SolaraViz(
model,
renderer,
model_params=model_params,
name="Boltzmann Wealth Model",
)
page # noqa
@tpike3, thanks for checking. Missing out these little things is what I was afraid of.
- I made space renderer optional but never added the null check so that's covered now.
- It's default solara viz behavior (existed before this PR) that if there is no components list passed, it automatically draws a altair space, it will not happen if you pass a component's list even if it's empty. Should I remove this functionality?
@tpike3, thanks for checking. Missing out these little things is what I was afraid of.
- I made space renderer optional but never added the null check so that's covered now.
- It's default solara viz behavior (existed before this PR) that if there is no components list passed, it automatically draws a altair space, it will not happen if you pass a component's list even if it's empty. Should I remove this functionality?
@Sahil-Chhoker Thanks --- for #2 we need to keep it backwards compatible. There may be a couple ways to do this, but off the top of my head, can you use renderer to override the lack of components?
@Sahil-Chhoker running through some more variants -
- The Solara resizing feature of the plots is better, but still clunky, could you take a look at the again
I pretty sure you have said this, but you need to update the Altair plot. Both backends should produce the same look and feel
Right now
| Plot | Matplotlib | Altair |
|---|---|---|
| position | in center of x,y grid | on intersection of x,y |
| labels | no x y labels | has x,y labels |
| legend | no legend | has legend |
Also for the boltzmann wealth model altair is just showing circles and has a legend for opacity but the circles are not filling in.
Still some very good work