Source code for modelly.viz.solara.space_view

"""Space rendering component for SolaraViz, driven by a SpaceRenderer."""

from __future__ import annotations

import itertools
from typing import TYPE_CHECKING, Any

import altair as alt
import pandas as pd
import solara

from modelly.viz.render.renderer import SpaceRenderer
from modelly.viz.utils import update_counter

if TYPE_CHECKING:
    from modelly.core.model import Model

_EMPTY_CHART_WIDTH = 450
_EMPTY_CHART_HEIGHT = 350
_VIEW_STROKE_WIDTH = 1.5


[docs] def create_space_component(renderer: SpaceRenderer): """Create a space visualization component for the given renderer.""" def SpaceVisualizationComponent(model: Model): """Component that renders the model's space using the provided renderer.""" return SpaceRendererComponent(model, renderer) return SpaceVisualizationComponent
def _clear_matplotlib_canvas(renderer: SpaceRenderer) -> None: """Remove the previously drawn artists and colorbars from the matplotlib canvas.""" # Clear the previous plotted data and agents all_artists = [ renderer.canvas.lines[:], renderer.canvas.collections[:], renderer.canvas.patches[:], renderer.canvas.images[:], renderer.canvas.artists[:], ] # Remove duplicate colorbars from the canvas for cbar in renderer.backend_renderer._active_colorbars: cbar.remove() renderer.backend_renderer._active_colorbars.clear() # Chain them together into a single iterable for artist in itertools.chain.from_iterable(all_artists): artist.remove() def _render_matplotlib_space(renderer: SpaceRenderer, viz_dependencies) -> None: """Redraw the space on the matplotlib canvas and display the figure.""" _clear_matplotlib_canvas(renderer) if renderer.space_mesh: renderer.draw_structure() if renderer.agent_mesh: renderer.draw_agents() if renderer.property_layer_mesh: renderer.draw_property_layer() if renderer.post_process and not renderer._post_process_applied: renderer.post_process(renderer.canvas) renderer._post_process_applied = True solara.FigureMatplotlib( renderer.canvas.get_figure(), format="png", bbox_inches="tight", dependencies=viz_dependencies, ) def _layer_altair_charts(charts): """Combine the space charts into one chart, or an empty chart when there are none.""" if not charts: # If no charts are available, return an empty chart return ( alt.Chart(pd.DataFrame()) .mark_point() .properties(width=_EMPTY_CHART_WIDTH, height=_EMPTY_CHART_HEIGHT) ) if len(charts) == 1: return charts[0] return alt.layer(*charts).resolve_axis(x="independent", y="independent") def _render_altair_space(renderer: SpaceRenderer) -> None: """Draw the space charts with Altair and display the layered chart.""" structure = renderer.draw_structure() if renderer.space_mesh else None agents = renderer.draw_agents() if renderer.agent_mesh else None props = renderer.draw_property_layer() if renderer.property_layer_mesh else None spatial_charts_list = [chart for chart in [structure, props, agents] if chart] final_chart = _layer_altair_charts(spatial_charts_list) if renderer.post_process: final_chart = renderer.post_process(final_chart) final_chart = final_chart.configure_view( stroke="black", strokeWidth=_VIEW_STROKE_WIDTH ) solara.FigureAltair(final_chart, on_click=None, on_hover=None) @solara.component def SpaceRendererComponent( model: Model, renderer: SpaceRenderer, # FIXME: Manage dependencies properly dependencies: list[Any] | None = None, ): """Render the space of a model using a SpaceRenderer. Args: model (Model): The model whose space is to be rendered. renderer: A SpaceRenderer instance to render the model's space. dependencies (list[any], optional): List of dependencies for the component. """ update_counter.get() # update renderer's space according to the model's space/grid renderer.space = getattr(model, "grid", getattr(model, "space", None)) viz_dependencies = [update_counter.value] if dependencies: viz_dependencies.extend(dependencies) if renderer.backend == "matplotlib": _render_matplotlib_space(renderer, viz_dependencies) else: _render_altair_space(renderer) return None