Repository: altair-viz/altair

Issue:
tooltip throws error for Categorical variable
The following code used to work in recent versions of `altair` including the current in-development branch

But by commit f8912bad75d4247ab7 this code throws an error.

The problem appears to be that specifying a variable for a tooltip without a type throws an error if the variable in a `pandas.Categorical`.

Specifically, this code:

````
import altair as alt
import pandas as pd

df = (
    pd.DataFrame({"x": [1, 2], "y": [1, 2], "note": ["a", "b"]})
    .assign(note=lambda x: pd.Categorical(x["note"], ordered=True))
)

alt.Chart(df).encode(x="x", y="y", tooltip=["x", "y", "note"]).mark_point()
```

Now throws an error.

The error is:

```
---------------------------------------------------------------------------
SchemaValidationError                     Traceback (most recent call last)
File /fh/fast/bloom_j/software/miniconda3/envs/dms-vep-pipeline/lib/python3.11/site-packages/altair/vegalite/v5/api.py:2194, in Chart.to_dict(self, *args, **kwargs)
   2192     copy.data = core.InlineData(values=[{}])
   2193     return super(Chart, copy).to_dict(*args, **kwargs)
-> 2194 return super().to_dict(*args, **kwargs)

File /fh/fast/bloom_j/software/miniconda3/envs/dms-vep-pipeline/lib/python3.11/site-packages/altair/vegalite/v5/api.py:559, in TopLevelMixin.to_dict(self, *args, **kwargs)
    556 context["top_level"] = False
    557 kwargs["context"] = context
--> 559 dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
    561 # TODO: following entries are added after validation. Should they be validated?
    562 if is_top_level:
    563     # since this is top-level we add $schema if it's missing

File /fh/fast/bloom_j/software/miniconda3/envs/dms-vep-pipeline/lib/python3.11/site-packages/altair/utils/schemapi.py:422, in SchemaBase.to_dict(self, validate, ignore, context)
    420         self.validate(result)
    421     except jsonschema.ValidationError as err:
--> 422         raise SchemaValidationError(self, err)
    423 return result

SchemaValidationError: Invalid specification

        altair.vegalite.v5.api.Chart->0, validating 'type'

        [{'field': 'x', 'type': 'quantitative'}, {'field': 'y', 'type': 'quantitative'}, {'field': 'note', 'type': 'ordinal', 'sort': ['a', 'b']}] is not of type 'object'
        [{'field': 'x', 'type': 'quantitative'}, {'field': 'y', 'type': 'quantitative'}, {'field': 'note', 'type': 'ordinal', 'sort': ['a', 'b']}] is not of type 'object'
        Additional properties are not allowed ('sort' was unexpected)
        [{'field': 'x', 'type': 'quantitative'}, {'field': 'y', 'type': 'quantitative'}, {'field': 'note', 'type': 'ordinal', 'sort': ['a', 'b']}] is not of type 'null'
```


Discussion:
mattijn: Looks good! Can you add a test?
joelostblom: @mattijn Tests added!
mattijn: Tests pass again.

Patch:
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index de631b62..0b4dd372 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -404,10 +404,10 @@ class SchemaBase(object):
             parsed_shorthand = context.pop("parsed_shorthand", {})
             # Prevent that pandas categorical data is automatically sorted
             # when a non-ordinal data type is specifed manually
-            if "sort" in parsed_shorthand and kwds["type"] not in [
-                "ordinal",
-                Undefined,
-            ]:
+            # or if the encoding channel does not support sorting
+            if "sort" in parsed_shorthand and (
+                "sort" not in kwds or kwds["type"] not in ["ordinal", Undefined]
+            ):
                 parsed_shorthand.pop("sort")
 
             kwds.update(
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index 9fa2dccb..7f14f7bb 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -402,10 +402,10 @@ class SchemaBase(object):
             parsed_shorthand = context.pop("parsed_shorthand", {})
             # Prevent that pandas categorical data is automatically sorted
             # when a non-ordinal data type is specifed manually
-            if "sort" in parsed_shorthand and kwds["type"] not in [
-                "ordinal",
-                Undefined,
-            ]:
+            # or if the encoding channel does not support sorting
+            if "sort" in parsed_shorthand and (
+                "sort" not in kwds or kwds["type"] not in ["ordinal", Undefined]
+            ):
                 parsed_shorthand.pop("sort")
 
             kwds.update(


Test patch:
diff --git a/tests/vegalite/v5/tests/test_api.py b/tests/vegalite/v5/tests/test_api.py
index b4b82a17..404f0a45 100644
--- a/tests/vegalite/v5/tests/test_api.py
+++ b/tests/vegalite/v5/tests/test_api.py
@@ -138,16 +138,30 @@ def test_chart_infer_types():
         assert dct["encoding"]["size"]["type"] == "ordinal"
         assert dct["encoding"]["size"]["field"] == "s"
         assert dct["encoding"]["size"]["sort"] == [2, 1]
+        assert dct["encoding"]["tooltip"]["type"] == "ordinal"
+        assert dct["encoding"]["tooltip"]["field"] == "s"
+        # "sort" should be removed for channels that don't support it
+        assert "sort" not in dct["encoding"]["tooltip"]
 
     # Pass field names by keyword
-    chart = alt.Chart(data).mark_point().encode(x="x", y="y", color="c", size="s")
+    chart = (
+        alt.Chart(data)
+        .mark_point()
+        .encode(x="x", y="y", color="c", size="s", tooltip="s")
+    )
     _check_encodings(chart)
 
     # pass Channel objects by keyword
     chart = (
         alt.Chart(data)
         .mark_point()
-        .encode(x=alt.X("x"), y=alt.Y("y"), color=alt.Color("c"), size=alt.Size("s"))
+        .encode(
+            x=alt.X("x"),
+            y=alt.Y("y"),
+            color=alt.Color("c"),
+            size=alt.Size("s"),
+            tooltip=alt.Tooltip("s"),
+        )
     )
     _check_encodings(chart)
 
@@ -155,7 +169,7 @@ def test_chart_infer_types():
     chart = (
         alt.Chart(data)
         .mark_point()
-        .encode(alt.X("x"), alt.Y("y"), alt.Color("c"), alt.Size("s"))
+        .encode(alt.X("x"), alt.Y("y"), alt.Color("c"), alt.Size("s"), alt.Tooltip("s"))
     )
     _check_encodings(chart)
 
@@ -167,6 +181,7 @@ def test_chart_infer_types():
             alt.X("x", type="nominal"),
             alt.Y("y", type="ordinal"),
             alt.Size("s", type="nominal"),
+            alt.Tooltip("s", type="nominal"),
         )
     )
     dct = chart.to_dict()
@@ -174,6 +189,8 @@ def test_chart_infer_types():
     assert dct["encoding"]["y"]["type"] == "ordinal"
     assert dct["encoding"]["size"]["type"] == "nominal"
     assert "sort" not in dct["encoding"]["size"]
+    assert dct["encoding"]["tooltip"]["type"] == "nominal"
+    assert "sort" not in dct["encoding"]["tooltip"]
 
 
 @pytest.mark.parametrize(