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 show ID and Wealth in the wealth model example; Altair backend displays per-agent hover data.
- Refactor
  - Visualization data pipeline rewritten to use record-based agent data for more consistent color/shape encodings and legends.
- Bug Fixes
  - Matplotlib backend now rejects tooltips (Altair-only) to prevent unsupported usage.
- Documentation
  - Expanded docs describing visualization behavior and tooltip support.
- Tests
  - Added tests covering tooltip support in the Altair 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 2 times, most recently 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 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

Cohort / File(s) Summary of changes
Example app updates
mesa/examples/basic/boltzmann_wealth_model/app.py
Add per-agent tooltip (Agent ID, Wealth) to portrayals; adjust color encoding usage in post-processing; simplify SpaceRenderer.draw_agents invocation by removing explicit cmap/vmin/vmax args.
Altair backend draw_agents refactor
mesa/visualization/backends/altair_backend.py
Build agent data as records (list of dicts) including optional tooltip; coerce numeric columns post-DataFrame creation; derive tooltip key dynamically; use original_color:Q when numeric, otherwise use fill color; extract unique shape domain; update docstrings.
Portrayal components extension
mesa/visualization/components/portrayal_components.py
Add public attribute `tooltip: dict
Matplotlib backend validation
mesa/visualization/backends/matplotlib_backend.py
Remove CORRECTION_FACTOR_MARKER_ZOOM constant; add validation to reject tooltip in AgentPortrayalStyle for Matplotlib (raises ValueError).
Tests alignment
tests/test_backends.py
Include tooltip field in Altair draw_agents test inputs to match new data structure handling.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Added AgentPortrayalStyle and PropertyLayerStyle #2786 — Introduced AgentPortrayalStyle/PropertyLayerStyle; directly related as this PR extends AgentPortrayalStyle with tooltip and adapts backends.
  • projectmesa/mesa#XXXX — (if applicable) prior Altair rendering changes that touch color/encoding decisions — consider reviewing alongside this change.

Suggested labels

enhancement, visualisation

Suggested reviewers

  • quaquel
  • tpike3

Poem

A twitch of whiskers, tooltips bright,
I hop through charts by day and night.
Shapes align and legends sing,
Data carrots on a string.
Click and see each agent’s tale—
A rabbit’s triumph, small and hale. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description does not follow the repository’s official PR template prompt and lacks the required boilerplate that directs the author to select a bug fix or feature template via the Preview tab, so it fails to conform to the prescribed structure. Please update the PR description to use the repository’s feature/enhancement template by selecting it from the Preview tab, ensuring the standard template introduction and all required sections are included.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarizes the primary change by indicating the addition of tooltip support for agent portrayals in the Altair backend and follows conventional “feat:” prefix usage in a concise, single sentence.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% 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.

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.

@quaquel
Copy link
Member

quaquel commented Oct 6, 2025

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?

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.

  1. You changed the wording in some of the docs from colorbar to colormap. I guess the latter is more correct, but technically, this is a separate issue from the tooltip, which is at the core of this PR. Ideally, we like PRs to be atomic, meaning they address a single issue.
  2. Coderrabbit also has a suggestion on empty dicts that is worth looking at. You are best positioned to judge if the AI suggestion is actually correct or not.
  3. It would be good to make very clear in the documentation of AgentPortrayalStyle that the tooltip only works with the Altair backend. I probably would even raise a ValueError if tooltip is provided with the matplotlib backend because errors should not be passed over silently.

@DipayanDasgupta
Copy link
Author

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

@DipayanDasgupta DipayanDasgupta force-pushed the feature/altair-agent-tooltip branch from bc039e4 to de78550 Compare October 7, 2025 06:09
@DipayanDasgupta
Copy link
Author

DipayanDasgupta commented Oct 7, 2025

Hi @quaquel , thank you again for the excellent feedback. I've updated the PR to address all your points and the suggestions from coderabbitai:
The typo in the PropertyLayerStyle docstring is now corrected.
I've implemented the coderabbitai suggestion for a more consistent truthiness check in the Altair backend.
The AgentPortrayalStyle docstring for the tooltip attribute now clearly states that it is only for the Altair backend.
The Matplotlib backend will now raise a ValueError if a user attempts to use the tooltip attribute, preventing silent failures.
All the CI checks should now be passing. Please let me know if there's anything else needed in order to merge this PR.

@DipayanDasgupta
Copy link
Author

Hi maintainers @quaquel @tpike3 , just wondering, what's the status on the PR, is it mergeable or do I need to make any further changes?

@EwoutH EwoutH requested a review from Sahil-Chhoker October 10, 2025 08:23
Comment on lines 40 to 56
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


Copy link
Member

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

Copy link
Author

@DipayanDasgupta DipayanDasgupta Oct 10, 2025

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.

Copy link
Collaborator

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.

Copy link
Author

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.

Comment on lines -221 to -253
# 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)
Copy link
Member

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?

Copy link
Author

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.

Copy link
Member

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.

Copy link
Collaborator

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")
Copy link
Member

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.

Copy link
Author

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.

Copy link
Author

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.

Copy link
Collaborator

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.

Copy link
Member

@quaquel quaquel left a 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.

@quaquel
Copy link
Member

quaquel commented Oct 10, 2025

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.

Copy link
Collaborator

@Sahil-Chhoker Sahil-Chhoker left a 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.

Comment on lines 66 to 70
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Keep this!

@DipayanDasgupta DipayanDasgupta force-pushed the feature/altair-agent-tooltip branch from 1e19cfb to de78550 Compare October 10, 2025 16:26
DipayanDasgupta and others added 2 commits October 10, 2025 16:44
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.
@DipayanDasgupta DipayanDasgupta force-pushed the feature/altair-agent-tooltip branch from de78550 to 05bff03 Compare October 10, 2025 16:45
@DipayanDasgupta
Copy link
Author

@coderabbitai review

Copy link

coderabbitai bot commented Oct 12, 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 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: 1

📜 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 bc039e4 and 05bff03.

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

Comment on lines +263 to +288
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")
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

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

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.

3 participants