Skip to content

Conversation

DipayanDasgupta
Copy link

@DipayanDasgupta DipayanDasgupta commented Oct 4, 2025

Summary

This PR adds a tooltip attribute to AgentPortrayalStyle, 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:

  1. AgentPortrayalStyle in portrayal_components.py was extended to include a tooltip: dict | None = None attribute.
  2. The collect_agent_data and draw_agents methods in altair_backend.py were updated to process this new tooltip data. The backend now correctly creates a Pandas DataFrame with the tooltip columns and adds them to the Altair chart's tooltip encoding channel.

Usage Examples

Users can now pass a dictionary to the tooltip attribute within their agent_portrayal function.

def agent_portrayal(agent):
    return AgentPortrayalStyle(
        color=agent.wealth,
        tooltip={"Agent ID": agent.unique_id, "Wealth": agent.wealth},
    )

This provides a much richer user experience for inspecting agents.

Before:

Hovering over an agent only displayed basic coordinate information.
Screenshot 2025-10-04 165951

After:

Hovering over an agent now displays the custom information defined in the tooltip dictionary.
Screenshot 2025-10-04 165212

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.





<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- New Features
  - Agent tooltips now display ID and Wealth in the wealth model example.
  - Added a Wealth legend to the visualization.
  - Enhanced color legend customization (titles, orientation, numeric gradients).
  - Improved shape handling for agent markers based on observed data.
- Refactor
  - Streamlined visualization data preparation for more consistent rendering and legends.
- Documentation
  - Expanded descriptions of visualization features and behavior.
- Tests
  - Added tests covering tooltip support in the Altair visualization backend.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Copy link

github-actions bot commented Oct 4, 2025

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.6% [-0.0%, +1.2%] 🔵 +0.0% [-0.2%, +0.2%]
BoltzmannWealth large 🔵 -2.3% [-2.9%, -1.6%] 🔵 -1.2% [-1.5%, -0.9%]
Schelling small 🔵 -0.6% [-0.9%, -0.2%] 🔵 +0.9% [+0.6%, +1.2%]
Schelling large 🔵 -0.9% [-1.2%, -0.5%] 🔵 +0.9% [+0.2%, +1.6%]
WolfSheep small 🔵 -0.4% [-0.7%, -0.0%] 🔵 -0.5% [-1.1%, +0.3%]
WolfSheep large 🔵 -0.9% [-1.1%, -0.7%] 🔵 -2.0% [-2.3%, -1.6%]
BoidFlockers small 🔵 -2.8% [-3.4%, -2.3%] 🔵 -1.9% [-2.2%, -1.6%]
BoidFlockers large 🔵 -2.1% [-2.8%, -1.3%] 🔵 -0.7% [-1.5%, +0.2%]

@DipayanDasgupta
Copy link
Author

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?

@DipayanDasgupta DipayanDasgupta force-pushed the feature/altair-agent-tooltip branch from ec00505 to 79a5b64 Compare October 4, 2025 12:04
@DipayanDasgupta DipayanDasgupta force-pushed the feature/altair-agent-tooltip branch from 79a5b64 to bc039e4 Compare October 4, 2025 12:16
@DipayanDasgupta
Copy link
Author

@coderabbitai review

Copy link

coderabbitai bot commented Oct 4, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Oct 4, 2025

Walkthrough

Adds tooltip support to agent portrayals and Altair rendering, updates the example app to show Wealth legend and agent tooltips, removes a post-processing hook, and adjusts data handling in draw_agents to records-based with legend customization and shape domain extraction. Tests updated to pass tooltip data.

Changes

Cohort / File(s) Summary of changes
Example app updates
mesa/examples/basic/boltzmann_wealth_model/app.py
Add agent tooltip (ID, Wealth), pass legend_title="Wealth" to SpaceRenderer.draw_agents, remove post_process hook and its assignment.
Altair backend draw_agents refactor
mesa/visualization/backends/altair_backend.py
Rework data prep to records with tooltip per agent; numeric coercion; color legend config (title/orientation/type, gradient for numeric); extract shape domain from data; update tooltips derivation; docstring updates.
Portrayal components extension
mesa/visualization/components/portrayal_components.py
Add public attribute tooltip: dict
Tests alignment
tests/test_backends.py
Provide tooltip field in draw_agents test inputs to align with new backend handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as Example App
  participant SR as SpaceRenderer.draw_agents
  participant AB as AltairBackend
  participant Altair as Altair Chart

  App->>SR: draw_agents(..., legend_title="Wealth", tooltips in portrayals)
  SR->>AB: prepare data + call draw_agents(records)
  AB->>AB: Build records with colors, shapes, tooltip
  AB->>Altair: Create chart, encodings (color/shape/tooltip), legend config
  Altair-->>App: Interactive chart with wealth legend and tooltips
  note over AB,Altair: Tooltip keys derived from first valid tooltip<br/>Shape domain inferred from observed data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement, visualisation

Poem

A whisker twitch, a wealthy hue,
I hop through charts with tooltips new.
Legends gleam, domains take shape,
Data carrots nicely drape.
With gentle clicks, the truths unfold—
A bunny’s plot, precise and bold. 🥕📊

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely summarizes the main change by indicating the addition of a tooltip feature for Altair agent portrayals without extraneous details or jargon.
Description Check ✅ Passed The description follows a coherent feature template by including a summary, motivation, implementation details, usage examples with code snippets and before/after screenshots, and additional notes, providing reviewers with complete context and instructions.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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:

  1. Adding a comment explaining why this scaling is necessary for Altair's continuous domain
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between e1f9780 and bc039e4.

📒 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.

Comment on lines +258 to +263
tooltip_list = ["x", "y"]
# This is the corrected line:
if any(t is not None for t in arguments["tooltip"]):
first_valid_tooltip = next((t for t in arguments["tooltip"] if t), None)
if first_valid_tooltip:
tooltip_list.extend(first_valid_tooltip.keys())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix inconsistency in tooltip truthiness checks.

Lines 260 and 261 use different checks (is not None vs truthiness), which could cause unexpected behavior if users pass empty dictionaries as tooltips.

Apply this diff to make the checks consistent:

-        if any(t is not None for t in arguments["tooltip"]):
-            first_valid_tooltip = next((t for t in arguments["tooltip"] if t), None)
+        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)

This ensures that {} (empty dict) is treated consistently across both checks.

🤖 Prompt for AI Agents
In mesa/visualization/backends/altair_backend.py around lines 258 to 263, the
tooltip checks are inconsistent: the any() uses "is not None" while the next()
filter uses truthiness, causing empty dicts to be treated differently; change
the next() generator to filter with "is not None" (and keep the subsequent
conditional to check for None) so empty dicts aren't skipped by truthiness and
first_valid_tooltip is compared against None before extending tooltip_list.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant