Source code for modelly.viz.altair.backend

# noqa: D100
from collections.abc import Callable

import altair as alt
import numpy as np

import modelly
from modelly.space import (
    OrthogonalMooreGrid,
    OrthogonalVonNeumannGrid,
)
from modelly.viz.altair.agent_data import (
    style_columns,
    style_defaults,
    style_from_dict,
    to_arrays,
    warn_dict_portrayal,
)
from modelly.viz.altair.backend_charts import (
    add_tooltip_columns,
    agent_encodings,
    agent_frame,
    fill_encoding,
    layer_matches_space,
    property_layer_chart,
)
from modelly.viz.render.backend import AbstractRenderer

OrthogonalGrid = OrthogonalMooreGrid | OrthogonalVonNeumannGrid
HexGrid = modelly.space.HexGrid
Network = modelly.space.Network


[docs] class AltairBackend(AbstractRenderer): """Altair-based renderer for Modelly spaces. This module provides an Altair-based renderer for visualizing Modelly model spaces, agents, and property layers with interactive charting capabilities. """
[docs] def initialize_canvas(self) -> None: """Initialize the Altair canvas.""" self._canvas = None
[docs] def draw_structure(self, **kwargs) -> alt.Chart: """Draw the space structure using Altair. Args: **kwargs: Additional arguments passed to the space drawer. Checkout respective `SpaceDrawer` class on details on how to pass ``**kwargs``. Returns: alt.Chart: The Altair chart representing the space structure. """ return self.space_drawer.draw_altair(**kwargs)
[docs] def collect_agent_data( self, space, agent_portrayal: Callable, default_size: float | None = None ): """Collect plotting data for all agents in the space for Altair. Args: space: The Modelly space containing agents. agent_portrayal: Callable that returns AgentPortrayalStyle for each agent. default_size: Default marker size if not specified in portrayal. Returns: dict: Dictionary containing agent plotting data arrays. """ style_fields = style_defaults() fallback_size = ( default_size if default_size is not None else style_fields.get("size") ) styles = self._portrayal_styles(space, agent_portrayal, style_fields) return to_arrays(style_columns(styles, style_fields, fallback_size))
def _portrayal_styles( self, space, agent_portrayal: Callable, style_fields: dict ) -> list: """Return each agent's AgentPortrayalStyle, positioned in the space.""" # Warn only once per call instead of once per agent dict_portrayal_warned = False styles = [] for agent in space.agents: portray_input = agent_portrayal(agent) if isinstance(portray_input, dict): if not dict_portrayal_warned: warn_dict_portrayal() dict_portrayal_warned = True position = self._get_agent_pos(agent, space) aps = style_from_dict(portray_input, position, style_fields) else: aps = portray_input if aps.x is None and aps.y is None: aps.x, aps.y = self._get_agent_pos(agent, space) styles.append(aps) return styles
[docs] def draw_agents( self, arguments, chart_width: int = 450, chart_height: int = 350, **kwargs ): """Draw agents using Altair backend. Args: arguments: Dictionary containing agent data arrays. chart_width: Width of the chart. chart_height: Height of the chart. **kwargs: ``title``, ``xlabel``, ``ylabel``, ``cmap``, ``vmin`` and ``vmax`` customise the chart; any other keyword is an Altair point-mark property (for example ``strokeDash``) passed to ``mark_point``. Checkout respective `SpaceDrawer` class on details on how to pass ``**kwargs``. Returns: alt.Chart: The Altair chart representing the agents, or None if no agents. """ if arguments["loc"].size == 0: return None df = agent_frame(arguments) title = kwargs.pop("title", "") xlabel = kwargs.pop("xlabel", "") ylabel = kwargs.pop("ylabel", "") # Tooltip list for interactivity tooltip_list = add_tooltip_columns(df, arguments["tooltip"]) # Handle custom colormapping cmap = kwargs.pop("cmap", "viridis") vmin = kwargs.pop("vmin", None) vmax = kwargs.pop("vmax", None) fill = fill_encoding(df, cmap, vmin, vmax) limits = self.space_drawer.get_viz_limits() encodings = agent_encodings(df, (xlabel, ylabel), limits, fill) return ( alt.Chart(df) .mark_point(**kwargs) .encode(**encodings, tooltip=tooltip_list) .properties(title=title, width=chart_width, height=chart_height) )
[docs] def draw_property_layer( self, space, property_layers: dict[str, np.ndarray], property_layer_portrayal: Callable, chart_width: int = 450, chart_height: int = 350, ): """Draw property layers using Altair backend. Args: space: The Modelly space object containing the property layers. property_layers: A dictionary mapping property_layer names to numpy arrays. property_layer_portrayal: A function that returns PropertyLayerStyle that contains the visualization parameters. chart_width: The width of the chart. chart_height: The height of the chart. Returns: alt.Chart: A tuple containing the base chart and the color bar chart. """ main_charts = [] for layer_name, layer in property_layers.items(): if layer_name == "empty": continue portrayal = property_layer_portrayal(layer_name) if portrayal is None: continue data = layer.astype(float) if layer.dtype == bool else layer if not layer_matches_space(space, layer_name, data): continue current_chart = property_layer_chart( layer_name, data, portrayal, (chart_width, chart_height) ) if current_chart is not None: main_charts.append(current_chart) base = alt.layer(*main_charts).resolve_scale(color="independent") return base