-
-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Add tooltip to Altair agent portrayal (#2795) #2838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add tooltip to Altair agent portrayal (#2795) #2838
Conversation
Performance benchmarks:
|
Hi this is my first time contributing to Mesa , is this acceptable? , what changes do I need to make to my commit to get the PR merged? |
79a5b64
to
bc039e4
Compare
@coderabbitai review |
✅ Actions performedReview triggered.
|
WalkthroughAdds per-agent tooltip support to portrayals and Altair rendering, refactors Altair data prep to records with tooltip propagation and shape-domain extraction, updates example app to emit tooltips and change color encoding, adds Matplotlib validation forbidding tooltips, and updates tests to include tooltip data. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as Example App
participant SR as SpaceRenderer
participant AB as AltairBackend
participant Altair as Altair Chart
App->>SR: call draw_agents(..., portrayals with tooltip)
SR->>AB: pass agent portrayals (records-oriented)
AB->>AB: build records per agent (x,y,color,shape,tooltip,...)
AB->>Altair: create encodings (color (original_color:Q or fill), shape domain, tooltip key)
Altair-->>App: interactive chart (legend, shapes, tooltips)
note right of AB: Tooltip key chosen from first valid record
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
mesa/visualization/backends/altair_backend.py (2)
217-248
: The records-based approach improves clarity.The refactoring from numpy-centric to pandas records-based DataFrame construction makes the code more maintainable and easier to extend with additional per-agent fields like tooltips.
However, consider extracting the record-building logic into a helper method if this pattern is reused elsewhere or if the method grows further:
def _build_agent_record(self, i: int, arguments: dict) -> dict: """Build a single agent record dictionary from arguments arrays.""" record = { "x": arguments["loc"][i][0], "y": arguments["loc"][i][1], "size": arguments["size"][i], "shape": arguments["shape"][i], "opacity": arguments["opacity"][i], "strokeWidth": arguments["strokeWidth"][i] / 10, "original_color": arguments["color"][i], } tooltip = arguments["tooltip"][i] if tooltip: record.update(tooltip) # Determine fill and stroke colors if arguments["filled"][i]: record["viz_fill_color"] = arguments["color"][i] record["viz_stroke_color"] = ( arguments["stroke"][i] if isinstance(arguments["stroke"][i], str) else None ) else: record["viz_fill_color"] = None record["viz_stroke_color"] = arguments["color"][i] return record
227-227
: Document or parameterize the strokeWidth scaling factor.The hardcoded division by 10 to scale strokeWidth for the continuous domain lacks context and may not be appropriate for all use cases.
Consider either:
- Adding a comment explaining why this scaling is necessary for Altair's continuous domain
- Making it a parameter if users need different scaling
Example:
- "strokeWidth": arguments["strokeWidth"][i] / 10, # Scale for continuous domain + "strokeWidth": arguments["strokeWidth"][i] / 10, # Scale to [0, 0.1] range for Altair's continuous domain
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
mesa/examples/basic/boltzmann_wealth_model/app.py
(2 hunks)mesa/visualization/backends/altair_backend.py
(6 hunks)mesa/visualization/components/portrayal_components.py
(2 hunks)tests/test_backends.py
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-20T16:44:20.677Z
Learnt from: Sahil-Chhoker
PR: projectmesa/mesa#2803
File: mesa/visualization/backends/altair_backend.py:312-553
Timestamp: 2025-06-20T16:44:20.677Z
Learning: In mesa/visualization/backends/altair_backend.py, the author prefers to keep the draw_propertylayer method intact despite its complexity, as they believe breaking it into smaller functions would divert attention from the main logic flow. The method is well-documented and follows a clear sequence of operations for property layer rendering.
Applied to files:
mesa/visualization/backends/altair_backend.py
🔇 Additional comments (9)
tests/test_backends.py (1)
251-251
: LGTM!The addition of the tooltip field with None values appropriately tests the new tooltip support without requiring specific tooltip data.
mesa/visualization/components/portrayal_components.py (1)
58-58
: LGTM!The tooltip attribute addition is well-integrated, follows the existing pattern for optional attributes, and has an appropriate type hint.
mesa/examples/basic/boltzmann_wealth_model/app.py (2)
13-17
: Excellent example of the new tooltip feature!The agent portrayal demonstrates both the numeric color mapping and the tooltip dictionary effectively, providing a clear template for users.
48-54
: LGTM!The draw_agents call appropriately configures the visualization with colormap parameters and the new legend_title parameter for clear legend labeling.
mesa/visualization/backends/altair_backend.py (5)
1-5
: LGTM!The enhanced module docstring accurately reflects the expanded visualization capabilities.
83-83
: LGTM!The tooltip field is properly initialized and collected through both the dict-based and AgentPortrayalStyle-based portrayal paths.
Also applies to: 138-138, 194-194
276-296
: LGTM!The color handling logic correctly distinguishes between numeric (quantitative) and categorical (nominal) color encodings, with appropriate scale and legend configurations for each type.
300-300
: LGTM!Using
dropna().unique().tolist()
correctly extracts the set of shapes present in the data for the domain specification.
284-289
: LGTM!The gradient legend configuration with title, orientation, type, and length provides a clear, user-friendly visualization for numeric color scales.
Note: vmin and vmax are the lower and upper bounds for the colorbar and the data is | ||
normalized between these values for color/colormap rendering. If they are not | ||
normalized between these values for color/colorbar rendering. If they are not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the typo in the comment.
The comment incorrectly mentions "color/colorbar" but should say "color/colormap" since normalization applies to the color or colormap attributes, not the colorbar boolean flag.
Apply this diff to correct the comment:
- normalized between these values for color/colorbar rendering. If they are not
+ normalized between these values for color/colormap rendering. If they are not
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
normalized between these values for color/colorbar rendering. If they are not | |
normalized between these values for color/colormap rendering. If they are not |
🤖 Prompt for AI Agents
In mesa/visualization/components/portrayal_components.py around line 95, the
comment has a typo saying "color/colorbar" but normalization applies to the
color or colormap attributes; update the comment to read "color/colormap"
instead. Make the single-word replacement in the comment so it now mentions
"color/colormap rendering" (or equivalent) to accurately reflect that
normalization targets color/colormap, not the colorbar boolean.
Thanks a lot for this PR. It's great to see this, and this is most definitely an acceptable PR. I particularly appreciate the fact that you also included a small update to the tests. Some minor comments.
|
@quaquel thanks a lot for your valuable feedback , I will attempt to make the above changes asap and update the PR so that it can be merged soon, pleasure contributing to Mesa. |
bc039e4
to
de78550
Compare
Hi @quaquel , thank you again for the excellent feedback. I've updated the PR to address all your points and the suggestions from coderabbitai: |
def post_process(chart): | ||
"""Post-process the Altair chart to add a colorbar legend.""" | ||
chart = chart.encode( | ||
color=alt.Color( | ||
"color:N", | ||
scale=alt.Scale(scheme="viridis", domain=[0, 10]), | ||
legend=alt.Legend( | ||
title="Wealth", | ||
orient="right", | ||
type="gradient", | ||
gradientLength=200, | ||
), | ||
), | ||
) | ||
return chart | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did you remove this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The post_process function was causing the application to crash after the new tooltip feature was added.
The original post_process function was overwriting the chart's entire .encode() block. This removed the x, y, and tooltip channels that the backend had just set up, leading to the TypeError and KeyError i was debugging.
To fix this, I moved the legend-creation logic directly into the altair_backend.py draw_agents method. It's now a default part of the agent rendering process and can be configured with the legend_title keyword argument. This makes the feature more robust and integrated into the backend, rather than relying on a post-processing step in the example app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original post_process function was overwriting the chart's entire .encode() block.
It's now a default part of the agent rendering process and can be configured with the legend_title keyword argument. This makes the feature more robust and integrated into the backend, rather than relying on a post-processing step in the example app.
@DipayanDasgupta post_process
is supposed to overwrite the chart's encode block because the examples are the user end of the API and we want to have more control over there because we can't implement every altair feature into mesa, I hope that's clear.
I think this method doesn't work here because what happens if the user doesn't want a gradient at the side but just colored agents, now he can't remove the colorbar because the logic is in the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, thanks for the clarification. My initial approach removed user flexibility, which is not ideal. I've reverted the changes in app.py
and the backend. The post_process
function is now back in the example app, and the legend logic has been removed from the backend. The original crash was due to a data handling issue in the backend, which is now fixed, so post_process
should work correctly again.
# that's why changing the the domain of strokeWidth beforehand. | ||
stroke_width = [data / 10 for data in arguments["strokeWidth"]] | ||
|
||
# Agent data preparation | ||
df_data = { | ||
"x": arguments["loc"][:, 0], | ||
"y": arguments["loc"][:, 1], | ||
"size": arguments["size"], | ||
"shape": arguments["shape"], | ||
"opacity": arguments["opacity"], | ||
"strokeWidth": stroke_width, | ||
"original_color": arguments["color"], | ||
"is_filled": arguments["filled"], | ||
"original_stroke": arguments["stroke"], | ||
} | ||
df = pd.DataFrame(df_data) | ||
|
||
# To ensure distinct shapes according to agent portrayal | ||
unique_shape_names_in_data = df["shape"].unique().tolist() | ||
|
||
fill_colors = [] | ||
stroke_colors = [] | ||
for i in range(len(df)): | ||
filled = df["is_filled"][i] | ||
main_color = df["original_color"][i] | ||
stroke_spec = ( | ||
df["original_stroke"][i] | ||
if isinstance(df["original_stroke"][i], str) | ||
else None | ||
) | ||
if filled: | ||
fill_colors.append(main_color) | ||
stroke_colors.append(stroke_spec) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, it seems that more is changed then just adding a tooltip. Can you explain what changed here and why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous approach of creating separate DataFrames and joining them (df.join(tooltip_df)) was causing a ValueError: Dataframe contains invalid column name: 0. Pandas was creating integer-based column names, which Altair cannot handle.The new code fixes this by building a list of dictionaries (records), where each dictionary represents a single agent's complete data (position, style, and tooltip). Creating the DataFrame from this list (pd.DataFrame(records)) is a more robust method that ensures all column names are correctly handled as strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sahil-Chhoker Can you review these changes? You are best positioned to judge whether this is all done correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a needed change to make the dataframe flexible but I've to do some testing before I can say anything.
@DipayanDasgupta can you just double check if you are not forgetting anything to include in the dataframe because it can be harder to spot later on.
# Tooltip list for interactivity | ||
# FIXME: Add more fields to tooltip (preferably from agent_portrayal) | ||
tooltip_list = ["x", "y"] | ||
legend_title = kwargs.pop("legend_title", "Color") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this legend stuff seems also new.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the legend is now created within the draw_agents method by default, I added the legend_title keyword argument to allow users to customize the legend's title directly. This makes the backend more flexible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, the title was hardcoded in the example app's post_process function. Now, it's a configurable part of the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again we want more control on the user side of API so it's best the legend are not put by the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a few more questions.
Thanks for the clarification. This is a part of the code base I am not presently intimately familiar with. I have asked someone who is to also take a look to ensure everything is in order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work on the PR, mostly smaller concerns aside from the post_process
part.
renderer.draw_agents(agent_portrayal=agent_portrayal, cmap="viridis", vmin=0, vmax=10) | ||
|
||
# The post_process function is used to modify the Altair chart after it has been created. | ||
# It can be used to add legends, colorbars, or other visual elements. | ||
renderer.post_process = post_process |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep this!
1e19cfb
to
de78550
Compare
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.
de78550
to
05bff03
Compare
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
mesa/examples/basic/boltzmann_wealth_model/app.py
(3 hunks)mesa/visualization/backends/altair_backend.py
(5 hunks)mesa/visualization/backends/matplotlib_backend.py
(1 hunks)mesa/visualization/components/portrayal_components.py
(1 hunks)tests/test_backends.py
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- mesa/examples/basic/boltzmann_wealth_model/app.py
🧰 Additional context used
🪛 Ruff (0.13.3)
mesa/visualization/backends/matplotlib_backend.py
144-146: Avoid specifying long messages outside the exception class
(TRY003)
numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"] | ||
for col in numeric_cols: | ||
if col in df.columns: | ||
df[col] = pd.to_numeric(df[col], errors="coerce") | ||
|
||
# Get tooltip keys from the first valid record | ||
tooltip_list = ["x", "y"] | ||
if any(t is not None for t in arguments["tooltip"]): | ||
first_valid_tooltip = next( | ||
(t for t in arguments["tooltip"] if t is not None), None | ||
) | ||
if first_valid_tooltip is not None: | ||
tooltip_list.extend(first_valid_tooltip.keys()) | ||
|
||
# Extract additional parameters from kwargs | ||
# FIXME: Add more parameters to kwargs | ||
title = kwargs.pop("title", "") | ||
xlabel = kwargs.pop("xlabel", "") | ||
ylabel = kwargs.pop("ylabel", "") | ||
# FIXME: Add more parameters to kwargs | ||
|
||
# Tooltip list for interactivity | ||
# FIXME: Add more fields to tooltip (preferably from agent_portrayal) | ||
tooltip_list = ["x", "y"] | ||
|
||
# Handle custom colormapping | ||
cmap = kwargs.pop("cmap", "viridis") | ||
vmin = kwargs.pop("vmin", None) | ||
vmax = kwargs.pop("vmax", None) | ||
|
||
color_is_numeric = np.issubdtype(df["original_color"].dtype, np.number) | ||
if color_is_numeric: | ||
color_min = vmin if vmin is not None else df["original_color"].min() | ||
color_max = vmax if vmax is not None else df["original_color"].max() | ||
|
||
fill_encoding = alt.Fill( | ||
"original_color:Q", | ||
scale=alt.Scale(scheme=cmap, domain=[color_min, color_max]), | ||
) | ||
else: | ||
fill_encoding = alt.Fill( | ||
"viz_fill_color:N", | ||
scale=None, | ||
title="Color", | ||
) | ||
color_is_numeric = pd.api.types.is_numeric_dtype(df["original_color"]) | ||
fill_encoding = ( | ||
alt.Fill("original_color:Q") | ||
if color_is_numeric | ||
else alt.Fill("viz_fill_color:N", scale=None, title="Color") | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix color dtype detection for Altair fill.
pd.to_numeric
is coercing string colors (e.g., "tab:blue"
) to NaN
, leaving the column as float and making color_is_numeric
truthy. Altair then encodes original_color:Q
, so categorical colors disappear. Guard the numeric conversion and reuse that flag in the encoding.
- numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"]
+ color_values = arguments["color"]
+ color_is_numeric = all(
+ isinstance(value, (int, float, np.number)) or value is None
+ for value in color_values
+ )
+
+ numeric_cols = ["x", "y", "size", "opacity", "strokeWidth"]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
+ if color_is_numeric and "original_color" in df.columns:
+ df["original_color"] = pd.to_numeric(
+ df["original_color"], errors="coerce"
+ )
+ else:
+ color_is_numeric = False
@@
- color_is_numeric = pd.api.types.is_numeric_dtype(df["original_color"])
fill_encoding = (
alt.Fill("original_color:Q")
if color_is_numeric
else alt.Fill("viz_fill_color:N", scale=None, title="Color")
)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"] | |
for col in numeric_cols: | |
if col in df.columns: | |
df[col] = pd.to_numeric(df[col], errors="coerce") | |
# Get tooltip keys from the first valid record | |
tooltip_list = ["x", "y"] | |
if any(t is not None for t in arguments["tooltip"]): | |
first_valid_tooltip = next( | |
(t for t in arguments["tooltip"] if t is not None), None | |
) | |
if first_valid_tooltip is not None: | |
tooltip_list.extend(first_valid_tooltip.keys()) | |
# Extract additional parameters from kwargs | |
# FIXME: Add more parameters to kwargs | |
title = kwargs.pop("title", "") | |
xlabel = kwargs.pop("xlabel", "") | |
ylabel = kwargs.pop("ylabel", "") | |
# FIXME: Add more parameters to kwargs | |
# Tooltip list for interactivity | |
# FIXME: Add more fields to tooltip (preferably from agent_portrayal) | |
tooltip_list = ["x", "y"] | |
# Handle custom colormapping | |
cmap = kwargs.pop("cmap", "viridis") | |
vmin = kwargs.pop("vmin", None) | |
vmax = kwargs.pop("vmax", None) | |
color_is_numeric = np.issubdtype(df["original_color"].dtype, np.number) | |
if color_is_numeric: | |
color_min = vmin if vmin is not None else df["original_color"].min() | |
color_max = vmax if vmax is not None else df["original_color"].max() | |
fill_encoding = alt.Fill( | |
"original_color:Q", | |
scale=alt.Scale(scheme=cmap, domain=[color_min, color_max]), | |
) | |
else: | |
fill_encoding = alt.Fill( | |
"viz_fill_color:N", | |
scale=None, | |
title="Color", | |
) | |
color_is_numeric = pd.api.types.is_numeric_dtype(df["original_color"]) | |
fill_encoding = ( | |
alt.Fill("original_color:Q") | |
if color_is_numeric | |
else alt.Fill("viz_fill_color:N", scale=None, title="Color") | |
) | |
color_values = arguments["color"] | |
color_is_numeric = all( | |
isinstance(value, (int, float, np.number)) or value is None | |
for value in color_values | |
) | |
numeric_cols = ["x", "y", "size", "opacity", "strokeWidth"] | |
for col in numeric_cols: | |
if col in df.columns: | |
df[col] = pd.to_numeric(df[col], errors="coerce") | |
if color_is_numeric and "original_color" in df.columns: | |
df["original_color"] = pd.to_numeric( | |
df["original_color"], errors="coerce" | |
) | |
else: | |
color_is_numeric = False | |
# Get tooltip keys from the first valid record | |
tooltip_list = ["x", "y"] | |
if any(t is not None for t in arguments["tooltip"]): | |
first_valid_tooltip = next( | |
(t for t in arguments["tooltip"] if t is not None), None | |
) | |
if first_valid_tooltip is not None: | |
tooltip_list.extend(first_valid_tooltip.keys()) | |
# Extract additional parameters from kwargs | |
title = kwargs.pop("title", "") | |
xlabel = kwargs.pop("xlabel", "") | |
ylabel = kwargs.pop("ylabel", "") | |
# FIXME: Add more parameters to kwargs | |
fill_encoding = ( | |
alt.Fill("original_color:Q") | |
if color_is_numeric | |
else alt.Fill("viz_fill_color:N", scale=None, title="Color") | |
) |
🤖 Prompt for AI Agents
In mesa/visualization/backends/altair_backend.py around lines 263-288, the code
coerces "original_color" to numeric which turns string color names into NaN and
makes color_is_numeric incorrectly truthy; fix it by NOT coercing original_color
when converting numeric_cols (remove "original_color" from numeric_cols) or by
computing color_is_numeric from the original column before any coercion (e.g.,
check dtype or try pd.to_numeric on a copy and inspect non-null fraction), then
use that computed boolean to choose alt.Fill("original_color:Q") vs
alt.Fill("viz_fill_color:N", ...); ensure the numeric conversion step only
affects true numeric columns and that the color_is_numeric flag is reused for
the encoding decision.
Summary
This PR adds a
tooltip
attribute toAgentPortrayalStyle
, enabling agent-specific information to be displayed on hover in Altair-based visualizations.Motive
This feature resolves issue #2795, which requested an easy way to display agent information (like health, status, etc.) directly in the visualization. It is highly useful for debugging and for closely following the state of specific agents during a simulation.
Implementation
The implementation consists of two main changes:
AgentPortrayalStyle
inportrayal_components.py
was extended to include atooltip: dict | None = None
attribute.collect_agent_data
anddraw_agents
methods inaltair_backend.py
were updated to process this newtooltip
data. The backend now correctly creates a Pandas DataFrame with the tooltip columns and adds them to the Altair chart'stooltip
encoding channel.Usage Examples
Users can now pass a dictionary to the
tooltip
attribute within theiragent_portrayal
function.This provides a much richer user experience for inspecting agents.
Before:
Hovering over an agent only displayed basic coordinate information.

After:
Hovering over an agent now displays the custom information defined in the

tooltip
dictionary.Additional Notes
As discussed in the original issue, this implementation is specific to the Altair backend. A similar feature for the Matplotlib backend is not feasible in an elegant way at this time.