'Customize tooltip in Altair chart

I'm using Google Colab for learning Python. In this Google Colab notebook I've built for demostration, I was able to get some JSON data and visualize it with Altair.

This is the chart image I got so far - you can interact with the data in the linked Google Colab notebook or directly in the vega-editor ready with the sample data:

Chart results so far:

Below is the explanation of the data I have and what I want to show:

In Yu-Gi-Oh! TCG, cards came from a certain card set. In each card set are Monster, Spell and Trap cards - as well another type of cards like: Link Monsters, XYZ monsters, etc.

A normal JSON structure of a monster card (- for this purpose - has ATK and DEF) has the following JSON structure:

{
   "id":89631139,
   "name":"Blue-Eyes White Dragon",
   "type":"Normal Monster",
   "atk":3000,
   "def":2500,
   "level":8,
   "race":"Dragon",
   "attribute":"LIGHT",
   "archetype":"Blue-Eyes"
}

But, a non-monster card (i.e. a spell card) has no values neither in ATK and DEF attributes:

Example:

{
   "id":35261759,
   "name":"Pot of Desires",
   "type":"Spell Card",
   "race":"Normal",
   "archetype":"Greed"
}

I want to visualize in the chart each card that comes in the card set and - while the cursor is on a point (hover), it will show the details of the card like, (Name, ATK, DEF (if any) and the Type of card).

Example of desired tooltip:

Desired tooltip

After some trial and error, research and read the documentation, I'm facing the following issues and wondering if the results I'm looking are possible:

  • When you hover on a point that does not have ATK and DEF - (see image 1) = how to modify the settings of Altair chart for condition the build of the tooltip? - see image 1:

Image 1:

Image 1

In "image 1", the card called "Pot of Desires" is a Spell Card = it doesn't have ATK or DEF, I've configured the chart with the invalid property for shown Nulll / None / NaN values) and Altair sets 0 as default to those missing values.

The expected output in (for a non-monster card) would be:

Name: Pot of Desires

Type: Spell Card

The expected output in (for a monster card) would be:

Name: Blue-Eyes White Dragon

ATK: 3000 - DEF: 2500

Type: Normal Monster

The expected output in (for a link monster card = has ATK, but no DEF) would be:

Name: Proxy Dragon

ATK: 3000

Type: Link Monster

I want to condition how to build the tooltip in these scenarios, it is possible?



Solution 1:[1]

Thanks to @joelostblom's answer, my case scenario is not possible - yet - let's hope this scenario could be more common in order to raise more attention of the pull request/feature I've created - https://github.com/vega/vega-lite/issues/7811.

Meanwhile, the accepted answer is: is not possible, but, I'll checking for new answers and possible solutions.


That being said, I decide to check further and share my findings here:

  • Using Vega Expressions, I tried to customize the tooltip data and I got pretty good results, IMHO, but, not as close as I intended.

Here are the results I mentioned:

Result with transform.calculate

The Python code of the previous chart is as follows:

# V.2 of the chart/graph:
alt.Chart(df).mark_point(size=200, filled=True, invalid=None, tooltip={'content': 'data'}).encode(
          x={"field": "def", "type": "quantitative", "title": "DEF"}, 
          y={"field": "atk", "type": "quantitative", "title": "ATK"}, 
          color={"field": "type", "type": "nominal", "title": "Types of cards"}, 
          shape="type"
      ).properties(
          title={
              "text": ["Cardset: " + cardSetName], 
              "subtitle": ["Here is shown the " + str(len(df.index)) + " card" + ('s' if len(df.index) > 1 or str(len(df.index)) == 0 else '') + " contained in the cardset."]
              }
          ).transform_calculate(
          Name = "datum.name",
          Type = "datum.race + ' ' + ((substring(datum.type, datum.type.length-1, datum.type.length) == 's') ? substring(datum.type, 2, datum.type.length-1) : substring(datum.type, 2, datum.type.length))",
          ATK_DEF = "isValid(datum.atk) ? '' + datum.atk + (isValid(datum.def) ? '/' + datum.def : '') : 'N/A'"
          ).interactive()
  • The rest of the data is added thanks to tooltip={'content': 'data'} configuration. If there would be a way to declare the fields to use only in the tooltip, this scenario would be possible with Vega/Altair.

Here, I share, the code you can copy/paste in the Vega Editor for try it yourself.

N.B. This is the direct vega-editor link with the sample data.

VEGA-LITE Code:

{
  "config": {"view": {"continuousWidth": 400, "continuousHeight": 300}},
  "data": {"name": "data-5583486ec9c6448394a7b9390873045c"},
  "mark": {
    "type": "point",
    "filled": true,
    "invalid": null,
    "size": 200,
    "tooltip": {"content": "data"}
  },
  "encoding": {
    "color": {"type": "nominal", "field": "type", "title": "Types of cards"},
    "shape": {"type": "nominal", "field": "type"},
    "x": {"type": "quantitative", "field": "def", "title": "DEF"},
    "y": {"type": "quantitative", "field": "atk", "title": "ATK"}
  },
  "selection": {
    "selector149": {
      "type": "interval",
      "bind": "scales",
      "encodings": ["x", "y"]
    }
  },
  "title": {
    "text": ["Cardset: 2017 Mega-Tins"],
    "subtitle": ["Here is shown the 8 cards contained in the cardset."]
  },
  "transform": [
    {"calculate": "datum.name", "as": "Name"},
    {
      "calculate": "datum.race + ' ' + ((substring(datum.type, datum.type.length-1, datum.type.length) == 's') ? substring(datum.type, 2, datum.type.length-1) : substring(datum.type, 2, datum.type.length))",
      "as": "Type"
    },
    {
      "calculate": "isValid(datum.atk) ? '' + datum.atk + (isValid(datum.def) ? '/' + datum.def : '') : 'N/A'",
      "as": "ATK_DEF"
    }
  ],
  "$schema": "https://vega.github.io/schema/vega-lite/v4.8.1.json",
  "datasets": {
    "data-5583486ec9c6448394a7b9390873045c": [
      {
        "id": 89631139,
        "name": "Blue-Eyes White Dragon",
        "type": "2 Normal Monsters",
        "desc": "This legendary dragon is a powerful engine of destruction. Virtually invincible, very few have faced this awesome creature and lived to tell the tale.",
        "atk": 3000,
        "def": 2500,
        "level": 8,
        "race": "Dragon",
        "attribute": "LIGHT",
        "archetype": "Blue-Eyes"
      },
      {
        "id": 46986414,
        "name": "Dark Magician",
        "type": "2 Normal Monsters",
        "desc": "The ultimate wizard in terms of attack and defense.",
        "atk": 2500,
        "def": 2100,
        "level": 7,
        "race": "Spellcaster",
        "attribute": "DARK",
        "archetype": "Dark Magician"
      },
      {
        "id": 26920296,
        "name": "Dreamland",
        "type": "2 Spell Cards",
        "desc": "This card can activate these effects depending on the monster card types on the field.\n? Fusion: Once per turn, if a monster(s) is sent from your hand or field to the GY by a card effect (except during the Damage Step): You can draw 1 card.\n? Synchro: When a monster(s) is Normal or Special Summoned (except during the Damage Step): You can increase their Levels by 1.\n? Xyz: Once per turn, during your End Phase: Destroy the monster(s) on the field with the highest Level.\nYou can only activate 1 \"Dreamland\" per turn.",
        "atk": null,
        "def": null,
        "level": null,
        "race": "Field",
        "attribute": null,
        "archetype": null
      },
      {
        "id": 80532587,
        "name": "Elder Entity N'tss",
        "type": "1 Fusion Monster",
        "desc": "1 Synchro Monster + 1 Xyz Monster\nMust be Special Summoned (from your Extra Deck) by sending the above cards you control to the GY. (You do not use \"Polymerization\".) Once per turn: You can Special Summon 1 Level 4 monster from your hand. If this card is sent to the GY: You can target 1 card on the field; destroy it. You can only Special Summon \"Elder Entity N'tss(s)\" once per turn.",
        "atk": 2500,
        "def": 1200,
        "level": 4,
        "race": "Fairy",
        "attribute": "LIGHT",
        "archetype": null
      },
      {
        "id": 23085002,
        "name": "Number 68: Sanaphond the Sky Prison",
        "type": "2 XYZ Monsters",
        "desc": "2 Level 8 monsters\nGains 100 ATK and DEF for each monster in the GYs. Once per turn: You can detach 1 material from this card; until the end of your opponent's next turn, this card cannot be destroyed by card effects, also neither player can Special Summon monsters from the GYs.",
        "atk": 2100,
        "def": 2700,
        "level": 8,
        "race": "Rock",
        "attribute": "DARK",
        "archetype": null
      },
      {
        "id": 59479050,
        "name": "Number 71: Rebarian Shark",
        "type": "2 XYZ Monsters",
        "desc": "2 Level 3 monsters\nOnce per turn, if this card has material: You can target 1 \"Number\" Xyz Monster in your GY, except \"Number 71: Rebarian Shark\"; Special Summon it, and if you do, attach 1 material from this card to it. If this card is sent to the GY: You can choose 1 \"Rank-Up-Magic\" Spell from your Deck and place it on top of your Deck.",
        "atk": 0,
        "def": 2000,
        "level": 3,
        "race": "Dragon",
        "attribute": "WATER",
        "archetype": null
      },
      {
        "id": 35261759,
        "name": "Pot of Desires",
        "type": "2 Spell Cards",
        "desc": "Banish 10 cards from the top of your Deck, face-down; draw 2 cards. You can only activate 1 \"Pot of Desires\" per turn.",
        "atk": null,
        "def": null,
        "level": null,
        "race": "Normal",
        "attribute": null,
        "archetype": "Greed"
      },
      {
        "id": 22862454,
        "name": "Proxy Dragon",
        "type": "1 Link Monster",
        "desc": "2 monsters\r\nIf a card(s) you control would be destroyed by battle or card effect, you can destroy 1 of your monsters this card points to, instead.",
        "atk": 1400,
        "def": null,
        "level": null,
        "race": "Cyberse",
        "attribute": "LIGHT",
        "archetype": null
      }
    ]
  }
}

CONFIG:

{}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1