Repository: vega/altair
Pull request: Expand mark spec when using to_dict
Suggestion to close https://github.com/altair-viz/altair/issues/2508. To test that the correct chart was created, (e.g. in a student assignment), it would be quite useful it if was possible to test that the functionality of the chart was the same, even if a slightly different syntax was used to create it. 

Currently, if I want to test that the correct mark was used in a spec where a mark option might or might not be present, I need to write tests something like this:

```py
chart = alt.Chart().mark_bar(color='blue')

chart_dict = chart.to_dict()
if isinstance(chart_dict['mark'], dict):
    # If the mark takes a param, such as `color='blue'` in this example
    assert chart_dict['mark']['type'] == 'bar'
else:
    # If the spec is just `chart = alt.Chart().mark_bar()`
    assert chart_dict['mark'] == 'bar'
```

With this PR, I can always write tests like this, which is more convenient:

```py
assert chart.to_dict()['mark']['type'] == 'bar'
```

This build upon #2813 in spirit, so that `to_dict()` will always return the "longer version" of a spec, expanding both shorthands to proper VegaLite encodings and with this PR also expanding single mark strings to a dictionary. A notable difference is that the shorthands are altair specific so we have to expand them, whether this expansion of the mark to a dictionary is not technically needed since vega-lite accepts a string too. This means that `to_dict()` can be used to test the functionality of the chart, whereas `chart.mark` etc can be used to check if the desired altair syntax was used  (e.g. if you want to check that a shorthand was used instead of the long form encoding specification)

Problem statement:
Add a method to always return the long/most verbose version of a chart spec
To test that the correct chart was created, (e.g. in a student assignment), it would be quite useful it if was possible to convert a chart to the most verbose version of the vega spec. For example, using `mark_point()` will set `chart.mark` to `'point'`, whereas using `mark_point(opacity=0.5)` will instead set `chart.mark` to a dictionary and `chart.mark.type` to `"point"`. For writing tests for autograding, it would be helpful is there was a method that always returned the long dictionary version (and maybe there is and I missed it).

Hints from issue discussion:
Can you clarify what you mean by "the most verbose"? For example, if you have something like
```python
chart.encode(x='x')
```
do you want the result to look something like this?
```
spec = {'encoding': {'x': {'field': 'x', 'type': 'quantitative', 'axis': {}, 'scale': {'bins': {}, ...}, ...}}}
```
I was mainly thinking about being able to always return a dictionary with the existing properties/keys so that `chart.mark` would return `MarkDef({type: 'point'})` rather than just `'point'`. The advantage would be that this would allow me to always check the `type` key in the dict regardless of whether students just used an empty `mark_point()` or specified some additional properties such as opacity etc.

I didn't think of that all the empty keys could also be included as you brought up, and I agree that this would actually be the "most verbose" version. I think for my purposes, either of these implementation would be a great help. The more verbose one with all the empty keys would avoid key errors in the tests, but the way our autograding works a key error would result in the same type of test failure as if the key value does not match the expected value, so either one is fine for my purposes here.
Thanks - I think the closest thing to getting this kind of verbose specification would be to do a `to_dict`/`from_dict` round-trip. This would wrap each entry in the associated schema class; e.g.
```python
import altair as alt
chart = alt.Chart('data.txt').mark_point().encode(x='x:Q')
print(chart.mark)
# 'point'

chart2 = alt.Chart.from_dict(chart.to_dict())
print(chart2.mark)
# Mark('point')
```
This doesn't expand `Mark('point')` to `Mark({'type': 'point'})`, but I'm not sure there is any way to do this automatically with the information available to Altair: nothing exists in the vega-lite schema to indicate that these two are treated equivalently by the renderer.

Commit message: Fix failing test and update docs

Patch to review:
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index 4471c65ca..227ef1091 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -374,6 +374,8 @@ def to_dict(self, validate=True, ignore=None, context=None):
             kwds = {
                 k: v for k, v in kwds.items() if k not in list(ignore) + ["shorthand"]
             }
+            if "mark" in kwds and isinstance(kwds["mark"], str):
+                kwds["mark"] = {"type": kwds["mark"]}
             result = _todict(
                 kwds,
                 validate=sub_validate,
diff --git a/doc/user_guide/internals.rst b/doc/user_guide/internals.rst
index f63479ad0..d7187db44 100644
--- a/doc/user_guide/internals.rst
+++ b/doc/user_guide/internals.rst
@@ -136,7 +136,7 @@ from the Vega-Lite documentation, which has the following JSON specification::
           {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
         ]
       },
-      "mark": "bar",
+      "mark": {"type": "bar"},
       "encoding": {
         "x": {"field": "a", "type": "ordinal"},
         "y": {"field": "b", "type": "quantitative"}
@@ -161,7 +161,7 @@ construct an Altair chart object from this string of Vega-Lite JSON:
           {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
         ]
       },
-      "mark": "bar",
+      "mark": {"type": "bar"},
       "encoding": {
         "x": {"field": "a", "type": "ordinal"},
         "y": {"field": "b", "type": "quantitative"}
@@ -186,7 +186,7 @@ you can use the :meth:`~Chart.from_dict` method to construct the chart object:
           {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
         ]
       },
-      "mark": "bar",
+      "mark": {"type": "bar"},
       "encoding": {
         "x": {"field": "a", "type": "ordinal"},
         "y": {"field": "b", "type": "quantitative"}
diff --git a/doc/user_guide/saving_charts.rst b/doc/user_guide/saving_charts.rst
index ec0d5939b..7f45dc9ec 100644
--- a/doc/user_guide/saving_charts.rst
+++ b/doc/user_guide/saving_charts.rst
@@ -61,7 +61,7 @@ The contents of the resulting file will look something like this:
           "type": "quantitative"
         }
       },
-      "mark": "point"
+      "mark": {"type": "point"},
     }
 
 This JSON can then be inserted into any web page using the vegaEmbed_ library.
@@ -121,7 +121,7 @@ javascript-enabled web browser:
               "type": "quantitative"
             }
           },
-          "mark": "point"
+          "mark": {"type": "point"},
         };
         var opt = {"renderer": "canvas", "actions": false};
         vegaEmbed("#vis", spec, opt);
diff --git a/tests/tests/test_magics.py b/tests/tests/test_magics.py
index 91e28ec50..0ff47d920 100644
--- a/tests/tests/test_magics.py
+++ b/tests/tests/test_magics.py
@@ -120,7 +120,7 @@
         "x": {"field": "category", "type": "ordinal"},
         "y": {"field": "amount", "type": "quantitative"},
     },
-    "mark": "bar",
+    "mark": {"type": "bar"},
 }
 
 
diff --git a/tests/utils/tests/test_html.py b/tests/utils/tests/test_html.py
index 5f44704b5..ccd41745c 100644
--- a/tests/utils/tests/test_html.py
+++ b/tests/utils/tests/test_html.py
@@ -7,7 +7,7 @@
 def spec():
     return {
         "data": {"url": "data.json"},
-        "mark": "point",
+        "mark": {"type": "point"},
         "encoding": {
             "x": {"field": "x", "type": "quantitative"},
             "y": {"field": "y", "type": "quantitative"},
diff --git a/tests/utils/tests/test_mimebundle.py b/tests/utils/tests/test_mimebundle.py
index db6b579fe..bb21d9127 100644
--- a/tests/utils/tests/test_mimebundle.py
+++ b/tests/utils/tests/test_mimebundle.py
@@ -32,7 +32,7 @@ def vegalite_spec():
                 {"a": "I", "b": 52},
             ]
         },
-        "mark": "bar",
+        "mark": {"type": "bar"},
         "encoding": {
             "x": {"field": "a", "type": "ordinal"},
             "y": {"field": "b", "type": "quantitative"},
diff --git a/tests/utils/tests/test_schemapi.py b/tests/utils/tests/test_schemapi.py
index 4c82839b0..7def729ba 100644
--- a/tests/utils/tests/test_schemapi.py
+++ b/tests/utils/tests/test_schemapi.py
@@ -426,3 +426,11 @@ def validate_encoding(encoding):
     assert "shorthand" not in dct["encoding"]["y"]
     assert dct["encoding"]["y"]["field"] == "b"
     assert dct["encoding"]["y"]["type"] == "quantitative"
+
+
+def test_to_dict_expand_mark_spec():
+    # Test that `to_dict` correctly expands marks to a dictionary
+    # without impacting the original spec which remains a string
+    chart = alt.Chart().mark_bar()
+    assert chart.to_dict()["mark"] == {"type": "bar"}
+    assert chart.mark == "bar"
diff --git a/tests/vegalite/tests/test_common.py b/tests/vegalite/tests/test_common.py
index 549177a83..f3a4a1a2a 100644
--- a/tests/vegalite/tests/test_common.py
+++ b/tests/vegalite/tests/test_common.py
@@ -11,7 +11,7 @@
 def basic_spec():
     return {
         "data": {"url": "data.csv"},
-        "mark": "line",
+        "mark": {"type": "line"},
         "encoding": {
             "color": {"type": "nominal", "field": "color"},
             "x": {"type": "quantitative", "field": "xval"},
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index 5520592c7..b02ac697f 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -372,6 +372,8 @@ def to_dict(self, validate=True, ignore=None, context=None):
             kwds = {
                 k: v for k, v in kwds.items() if k not in list(ignore) + ["shorthand"]
             }
+            if "mark" in kwds and isinstance(kwds["mark"], str):
+                kwds["mark"] = {"type": kwds["mark"]}
             result = _todict(
                 kwds,
                 validate=sub_validate,
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
index f5e7469e7..1dcd0220b 100644
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -424,3 +424,11 @@ def validate_encoding(encoding):
     assert "shorthand" not in dct["encoding"]["y"]
     assert dct["encoding"]["y"]["field"] == "b"
     assert dct["encoding"]["y"]["type"] == "quantitative"
+
+
+def test_to_dict_expand_mark_spec():
+    # Test that `to_dict` correctly expands marks to a dictionary
+    # without impacting the original spec which remains a string
+    chart = alt.Chart().mark_bar()
+    assert chart.to_dict()["mark"] == {"type": "bar"}
+    assert chart.mark == "bar"


Code review comments:
diff_hunk: @@ -61,7 +61,7 @@ The contents of the resulting file will look something like this:
           "type": "quantitative"
         }
       },
-      "mark": "point"
+      "mark": {"type": "point"},
path: doc/user_guide/saving_charts.rst
text: @user1:
```suggestion
      "mark": {"type": "point"}
```

diff_hunk: @@ -121,7 +121,7 @@ javascript-enabled web browser:
               "type": "quantitative"
             }
           },
-          "mark": "point"
+          "mark": {"type": "point"},
path: doc/user_guide/saving_charts.rst
text: @user1:
```suggestion
          "mark": {"type": "point"}
```