Skip to content
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

Add tooltip option to Altair chart #2082

Merged
merged 5 commits into from
Mar 15, 2024
Merged

Conversation

FoFFolo
Copy link
Contributor

@FoFFolo FoFFolo commented Mar 14, 2024

I added in the altair.py experimental script the option to see a tooltip on each agent in the canvas when hovering on them. The logic is to see every option described by the agent_portrayal function, except for the x, y, color and size properties.
When defining the type of each tooltip, I didn't manage to find an optimal algorithm to check if a string can be considered a date object, in order to change its type from "nominal" to "temporal", without using an external library called dateutil, which contains the function parser.parse() that returns a boolean if the string is a right date format.

Here is an example:

import mesa
import solara
from matplotlib.figure import Figure
from mesa.experimental import JupyterViz
from money_model.agent import MoneyAgent
from money_model.model import MoneyModel


def agent_portrayal(MoneyAgent):
    size = 10
    color = "tab:red"
    if MoneyAgent.wealth > 0:
        size = 50
        color = "tab:blue"
    if MoneyAgent.unique_id == 0:
        size = 60
        color = "yellow"
    return {"size": size, "color": color, "Unique_id": MoneyAgent.unique_id}


def make_histogram(model):
    # ...

model_params = {
    # ...
}

page = JupyterViz(
    model_class=MoneyModel,
    model_params=model_params,
    measures=[make_histogram],
    name="Money Model",
    agent_portrayal=agent_portrayal,
    space_drawer="altair"
)
# This is required to render the visualization in the Jupyter notebook
page

In the agent_portrayal function, I set that every agent will have a tooltip to see their unique_id.
If we run this script using solara:

image

If I click on "View Source" in the altair chart, the content is:

{
  "config": {"view": {"continuousWidth": 300, "continuousHeight": 300}},
  "data": {
    "values": [
      {"size": 50, "color": "tab:blue", "Unique_id": 3, "x": 0, "y": 1},
      {"size": 50, "color": "tab:blue", "Unique_id": 48, "x": 0, "y": 3},
      {"size": 50, "color": "tab:blue", "Unique_id": 26, "x": 0, "y": 5},
      {"size": 50, "color": "tab:blue", "Unique_id": 12, "x": 1, "y": 0},
     // ...
      {"size": 50, "color": "tab:blue", "Unique_id": 27, "x": 9, "y": 8}
    ]
  },
  "mark": {"type": "point", "filled": true},
  "encoding": {
    "color": {"field": "color", "type": "nominal"},
    "size": {"field": "size", "type": "quantitative"},
    "tooltip": [{"field": "Unique_id", "type": "quantitative"}],
    "x": {"axis": null, "field": "x", "type": "ordinal"},
    "y": {"axis": null, "field": "y", "type": "ordinal"}
  },
  "height": 280,
  "width": 280,
  "$schema": "https://vega.github.io/schema/vega-lite/v5.16.3.json"
}

What's new is the tooltip array element and the unique_id value on each agent.

Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
Schelling small 🟢 -4.3% [-4.6%, -4.1%] 🔵 -0.5% [-0.7%, -0.3%]
Schelling large 🟢 -5.5% [-5.9%, -5.1%] 🔵 -1.1% [-1.7%, -0.5%]
WolfSheep small 🟢 -6.9% [-7.1%, -6.7%] 🔵 -1.1% [-1.2%, -1.0%]
WolfSheep large 🟢 -6.8% [-7.1%, -6.6%] 🔵 -0.5% [-2.5%, +1.4%]
BoidFlockers small 🟢 -4.4% [-4.9%, -3.9%] 🔵 -1.2% [-1.9%, -0.6%]
BoidFlockers large 🔵 -2.5% [-3.0%, -2.0%] 🔵 -0.5% [-1.1%, +0.0%]

@EwoutH EwoutH added the enhancement Release notes label label Mar 14, 2024
@EwoutH EwoutH requested a review from Corvince March 14, 2024 21:37
def detect_type(key):
key_type = type(all_agent_data[0][key])
tooltip_type = ""
if key_type == int:
Copy link
Contributor

Choose a reason for hiding this comment

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

For checking type, it is recommended with isinstance, see https://stackoverflow.com/questions/152580/whats-the-canonical-way-to-check-for-type-in-python:

first_by_key = all_agent_data[0][key]
if isinstance(first_by_key, int):
    ...
elif isinstance(first_by_key, ...):
    ...

@@ -34,12 +35,39 @@ def portray(g):
all_agent_data.append(agent_data)
return all_agent_data

def detect_type(key):
Copy link
Contributor

Choose a reason for hiding this comment

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

The naming is clearer with detect_tooltip_type.

encoding_dict = {
# no x-axis label
"x": alt.X("x", axis=None, type="ordinal"),
# no y-axis label
"y": alt.Y("y", axis=None, type="ordinal"),
"tooltip": [
alt.Tooltip(key, type=tooltip_types[key])
for key in all_agent_data[0].keys()
Copy link
Contributor

Choose a reason for hiding this comment

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

The [key for key in all_agent_data[0].keys() if key not in invalid_tooltips] here is a repetition from L57-59. They can precomputed once so that there is no repetition.

@rht
Copy link
Contributor

rht commented Mar 14, 2024

There are Ruff errors that can't be autofixed in the pre-commit.ci.

@FoFFolo
Copy link
Contributor Author

FoFFolo commented Mar 15, 2024

I started by changing type checks using isinstance, but then I discovered that there is a function inside the altair library that
does the job.
I replaced my implementations with an integrated function on Altair, called
infer_vegalite_type() which detect automatically the type for the tooltip,
which uses the infer_dtype() pandas function.
The altair function can be found in the altair library folder, in the path "altair/utils/core.py".

def infer_vegalite_type(
    data: object,
) -> Union[InferredVegaLiteType, Tuple[InferredVegaLiteType, list]]:
    """
    From an array-like input, infer the correct vega typecode
    ('ordinal', 'nominal', 'quantitative', or 'temporal')

    Parameters
    ----------
    data: object
    """
    typ = infer_dtype(data, skipna=False)

    if typ in [
        "floating",
        "mixed-integer-float",
        "integer",
        "mixed-integer",
        "complex",
    ]:
        return "quantitative"
    elif typ == "categorical" and hasattr(data, "cat") and data.cat.ordered:
        return ("ordinal", data.cat.categories.tolist())
    elif typ in ["string", "bytes", "categorical", "boolean", "mixed", "unicode"]:
        return "nominal"
    elif typ in [
        "datetime",
        "datetime64",
        "timedelta",
        "timedelta64",
        "date",
        "time",
        "period",
    ]:
        return "temporal"
    else:
        warnings.warn(
            "I don't know how to infer vegalite type from '{}'.  "
            "Defaulting to nominal.".format(typ),
            stacklevel=1,
        )
        return "nominal"

The altair proper function doesn't implement date object detection on strings, so the user has to specify the date object in the agent_portrayal function. E.g:

from datetime import datetime

def agent_portrayal(MoneyAgent):
    # ....
    return {"Date": datetime.now()}

alt.Tooltip(
key, type=alt.utils.infer_vegalite_type([all_agent_data[0][key]])
)
for key in all_agent_data[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

One last simplification, you can do

[
    alt.Tooltip(key, type=alst.utils.infer_vegalite_type([value])
    for key, value in all_agent_data[0].items()
    if key not in invalid_tooltips
]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay done. Yeah I agree with you.

One last simplification, you can do

@rht
Copy link
Contributor

rht commented Mar 15, 2024

I started by changing type checks using isinstance, but then I discovered that there is a function inside the altair library that
does the job.

I see, that simplifies the implementation a lot.

@rht rht merged commit 1634b8f into projectmesa:main Mar 15, 2024
12 checks passed
@rht
Copy link
Contributor

rht commented Mar 15, 2024

Merged, thank you @FoFFolo.

@FoFFolo FoFFolo deleted the altair_tooltip branch March 16, 2024 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants