Skip to content

Commit de78550

Browse files
pre-commit-ci[bot]DipayanDasgupta
authored andcommitted
feat: Add tooltip to Altair agent portrayal (#2795)
This feature adds a `tooltip` attribute to `AgentPortrayalStyle`, enabling agent-specific information to be displayed on hover in Altair-based visualizations. This commit addresses review feedback by: - Adding documentation to clarify the feature is Altair-only. - Raising a ValueError if tooltips are used with the Matplotlib backend. - Applying consistency, typo, and formatting fixes suggested by reviewers.
1 parent 496dc79 commit de78550

File tree

5 files changed

+38
-20
lines changed

5 files changed

+38
-20
lines changed

mesa/examples/basic/boltzmann_wealth_model/app.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import altair as alt
2-
31
from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
42
from mesa.mesa_logging import INFO, log_to_stderr
53
from mesa.visualization import (
@@ -67,4 +65,4 @@ def agent_portrayal(agent):
6765
model_params=model_params,
6866
name="Boltzmann Wealth Model",
6967
)
70-
page # noqa
68+
page # noqa

mesa/visualization/backends/altair_backend.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
"""Altair-based renderer for Mesa spaces.
2+
3+
This module provides an Altair-based renderer for visualizing Mesa model spaces,
4+
agents, and property layers with interactive charting capabilities.
5+
"""
6+
17
import warnings
28
from collections.abc import Callable
39
from dataclasses import fields
@@ -201,8 +207,6 @@ def collect_agent_data(
201207

202208
return final_data
203209

204-
205-
206210
def draw_agents(
207211
self, arguments, chart_width: int = 450, chart_height: int = 350, **kwargs
208212
):
@@ -219,7 +223,8 @@ def draw_agents(
219223
"size": arguments["size"][i],
220224
"shape": arguments["shape"][i],
221225
"opacity": arguments["opacity"][i],
222-
"strokeWidth": arguments["strokeWidth"][i] / 10, # Scale for continuous domain
226+
"strokeWidth": arguments["strokeWidth"][i]
227+
/ 10, # Scale for continuous domain
223228
"original_color": arguments["color"][i],
224229
}
225230
# Add tooltip data if available
@@ -230,7 +235,11 @@ def draw_agents(
230235
# Determine fill and stroke colors
231236
if arguments["filled"][i]:
232237
record["viz_fill_color"] = arguments["color"][i]
233-
record["viz_stroke_color"] = arguments["stroke"][i] if isinstance(arguments["stroke"][i], str) else None
238+
record["viz_stroke_color"] = (
239+
arguments["stroke"][i]
240+
if isinstance(arguments["stroke"][i], str)
241+
else None
242+
)
234243
else:
235244
record["viz_fill_color"] = None
236245
record["viz_stroke_color"] = arguments["color"][i]
@@ -240,19 +249,19 @@ def draw_agents(
240249
df = pd.DataFrame(records)
241250

242251
# Ensure all columns that should be numeric are, handling potential Nones
243-
numeric_cols = ['x', 'y', 'size', 'opacity', 'strokeWidth', 'original_color']
252+
numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"]
244253
for col in numeric_cols:
245254
if col in df.columns:
246-
df[col] = pd.to_numeric(df[col], errors='coerce')
247-
255+
df[col] = pd.to_numeric(df[col], errors="coerce")
248256

249257
# Get tooltip keys from the first valid record
250258
tooltip_list = ["x", "y"]
251-
# This is the corrected line:
252259
if any(t is not None for t in arguments["tooltip"]):
253-
first_valid_tooltip = next((t for t in arguments["tooltip"] if t), None)
254-
if first_valid_tooltip:
255-
tooltip_list.extend(first_valid_tooltip.keys())
260+
first_valid_tooltip = next(
261+
(t for t in arguments["tooltip"] if t is not None), None
262+
)
263+
if first_valid_tooltip is not None:
264+
tooltip_list.extend(first_valid_tooltip.keys())
256265

257266
# Extract additional parameters from kwargs
258267
title = kwargs.pop("title", "")
@@ -316,10 +325,16 @@ def draw_agents(
316325
),
317326
title="Shape",
318327
),
319-
opacity=alt.Opacity("opacity:Q", title="Opacity", scale=alt.Scale(domain=[0, 1], range=[0, 1])),
328+
opacity=alt.Opacity(
329+
"opacity:Q",
330+
title="Opacity",
331+
scale=alt.Scale(domain=[0, 1], range=[0, 1]),
332+
),
320333
fill=fill_encoding,
321334
stroke=alt.Stroke("viz_stroke_color:N", scale=None),
322-
strokeWidth=alt.StrokeWidth("strokeWidth:Q", scale=alt.Scale(domain=[0, 1])),
335+
strokeWidth=alt.StrokeWidth(
336+
"strokeWidth:Q", scale=alt.Scale(domain=[0, 1])
337+
),
323338
tooltip=tooltip_list,
324339
)
325340
.properties(title=title, width=chart_width, height=chart_height)
@@ -431,4 +446,4 @@ def draw_propertylayer(
431446
main_charts.append(current_chart)
432447

433448
base = alt.layer(*main_charts).resolve_scale(color="independent")
434-
return base
449+
return base

mesa/visualization/backends/matplotlib_backend.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
OrthogonalGrid = SingleGrid | MultiGrid | OrthogonalMooreGrid | OrthogonalVonNeumannGrid
2828
HexGrid = HexSingleGrid | HexMultiGrid | mesa.discrete_space.HexGrid
2929

30-
3130
CORRECTION_FACTOR_MARKER_ZOOM = 0.01
3231

3332

@@ -141,6 +140,10 @@ def collect_agent_data(self, space, agent_portrayal, default_size=None):
141140
)
142141
else:
143142
aps = portray_input
143+
if aps.tooltip is not None:
144+
raise ValueError(
145+
"The 'tooltip' attribute in AgentPortrayalStyle is only supported by the Altair backend."
146+
)
144147
# Set defaults if not provided
145148
if aps.x is None and aps.y is None:
146149
aps.x, aps.y = self._get_agent_pos(agent, space)

mesa/visualization/components/portrayal_components.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class AgentPortrayalStyle:
5656
edgecolors: str | tuple | None = None
5757
linewidths: float | int | None = 1.0
5858
tooltip: dict | None = None
59+
"""A dictionary of data to display on hover. Note: This feature is only available with the Altair backend."""
5960

6061
def update(self, *updates_fields: tuple[str, Any]):
6162
"""Updates attributes from variable (field_name, new_value) tuple arguments.
@@ -92,7 +93,7 @@ class PropertyLayerStyle:
9293
(vmin, vmax), transparency (alpha) and colorbar visibility.
9394
9495
Note: vmin and vmax are the lower and upper bounds for the colorbar and the data is
95-
normalized between these values for color/colorbar rendering. If they are not
96+
normalized between these values for colormap rendering. If they are not
9697
declared the values are automatically determined from the data range.
9798
9899
Note: You can specify either a 'colormap' (for varying data) or a single
@@ -118,4 +119,4 @@ def __post_init__(self):
118119
if self.color is not None and self.colormap is not None:
119120
raise ValueError("Specify either 'color' or 'colormap', not both.")
120121
if self.color is None and self.colormap is None:
121-
raise ValueError("Specify one of 'color' or 'colormap'")
122+
raise ValueError("Specify one of 'color' or 'colormap'")

tests/test_backends.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def test_altair_backend_draw_agents():
248248
"color": np.array(["red", "blue"]),
249249
"filled": np.array([True, True]),
250250
"stroke": np.array(["black", "black"]),
251+
"tooltip": np.array([None, None]),
251252
}
252253
ab.space_drawer.get_viz_limits = MagicMock(return_value=(0, 10, 0, 10))
253254
assert ab.draw_agents(arguments) is not None

0 commit comments

Comments
 (0)