Repository: vega/altair

Issue:
Using numpy scalars in charts leads to JSON serialization errors
simple example:
```python
import numpy as np
import altair as alt

size = np.arange(10).max()  # seems innocuous enough...

alt.Chart('data.csv').mark_point(size=size).to_json()
```
```pytb
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-593a48d22bb1> in <module>()
      2 import altair as alt
      3 
----> 4 alt.Chart('data.csv').mark_point(size=np.arange(10).max()).to_json()

/usr/local/lib/python3.6/dist-packages/altair/utils/schemapi.py in to_json(self, validate, ignore, context, indent, sort_keys, **kwargs)
    338         """
    339         dct = self.to_dict(validate=validate, ignore=ignore, context=context)
--> 340         return json.dumps(dct, indent=indent, sort_keys=sort_keys, **kwargs)
    341 
    342     @classmethod

/usr/lib/python3.6/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    236         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237         separators=separators, default=default, sort_keys=sort_keys,
--> 238         **kw).encode(obj)
    239 
    240 

/usr/lib/python3.6/json/encoder.py in encode(self, o)
    199         chunks = self.iterencode(o, _one_shot=True)
    200         if not isinstance(chunks, (list, tuple)):
--> 201             chunks = list(chunks)
    202         return ''.join(chunks)
    203 

/usr/lib/python3.6/json/encoder.py in _iterencode(o, _current_indent_level)
    428             yield from _iterencode_list(o, _current_indent_level)
    429         elif isinstance(o, dict):
--> 430             yield from _iterencode_dict(o, _current_indent_level)
    431         else:
    432             if markers is not None:

/usr/lib/python3.6/json/encoder.py in _iterencode_dict(dct, _current_indent_level)
    402                 else:
    403                     chunks = _iterencode(value, _current_indent_level)
--> 404                 yield from chunks
    405         if newline_indent is not None:
    406             _current_indent_level -= 1

/usr/lib/python3.6/json/encoder.py in _iterencode_dict(dct, _current_indent_level)
    402                 else:
    403                     chunks = _iterencode(value, _current_indent_level)
--> 404                 yield from chunks
    405         if newline_indent is not None:
    406             _current_indent_level -= 1

/usr/lib/python3.6/json/encoder.py in _iterencode(o, _current_indent_level)
    435                     raise ValueError("Circular reference detected")
    436                 markers[markerid] = o
--> 437             o = _default(o)
    438             yield from _iterencode(o, _current_indent_level)
    439             if markers is not None:

/usr/lib/python3.6/json/encoder.py in default(self, o)
    178         """
    179         raise TypeError("Object of type '%s' is not JSON serializable" %
--> 180                         o.__class__.__name__)
    181 
    182     def encode(self, o):

TypeError: Object of type 'int64' is not JSON serializable
```


Discussion:
While fixing this, it would be nice to serialize numpy datetimes correctly within chart specs, so they could e.g. be used directly in ``scale.domain`` arguments

Resolving patch:
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -8,6 +8,8 @@
 import json
 
 import jsonschema
+import numpy as np
+import pandas as pd
 
 
 # If DEBUG_MODE is True, then schema objects are converted to dict and
@@ -54,13 +56,17 @@ def _todict(obj, validate, context):
     """Convert an object to a dict representation."""
     if isinstance(obj, SchemaBase):
         return obj.to_dict(validate=validate, context=context)
-    elif isinstance(obj, (list, tuple)):
+    elif isinstance(obj, (list, tuple, np.ndarray)):
         return [_todict(v, validate, context) for v in obj]
     elif isinstance(obj, dict):
         return {k: _todict(v, validate, context) for k, v in obj.items()
                 if v is not Undefined}
     elif hasattr(obj, 'to_dict'):
         return obj.to_dict()
+    elif isinstance(obj, np.number):
+        return float(obj)
+    elif isinstance(obj, (pd.Timestamp, np.datetime64)):
+        return pd.Timestamp(obj).isoformat()
     else:
         return obj
 
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -4,6 +4,8 @@
 import json
 
 import jsonschema
+import numpy as np
+import pandas as pd
 
 
 # If DEBUG_MODE is True, then schema objects are converted to dict and
@@ -50,13 +52,17 @@ def _todict(obj, validate, context):
     """Convert an object to a dict representation."""
     if isinstance(obj, SchemaBase):
         return obj.to_dict(validate=validate, context=context)
-    elif isinstance(obj, (list, tuple)):
+    elif isinstance(obj, (list, tuple, np.ndarray)):
         return [_todict(v, validate, context) for v in obj]
     elif isinstance(obj, dict):
         return {k: _todict(v, validate, context) for k, v in obj.items()
                 if v is not Undefined}
     elif hasattr(obj, 'to_dict'):
         return obj.to_dict()
+    elif isinstance(obj, np.number):
+        return float(obj)
+    elif isinstance(obj, (pd.Timestamp, np.datetime64)):
+        return pd.Timestamp(obj).isoformat()
     else:
         return obj
 


Test patch:
diff --git a/altair/utils/tests/test_schemapi.py b/altair/utils/tests/test_schemapi.py
--- a/altair/utils/tests/test_schemapi.py
+++ b/altair/utils/tests/test_schemapi.py
@@ -4,10 +4,13 @@
 # tools/generate_schema_wrapper.py. Do not modify directly.
 import copy
 import io
+import json
 import jsonschema
 import pickle
 import pytest
 
+import numpy as np
+
 from ..schemapi import (UndefinedType, SchemaBase, Undefined, _FromDict,
                         SchemaValidationError)
 
@@ -324,3 +327,17 @@ def test_schema_validation_error():
     assert "validating {!r}".format(the_err.validator) in message
     assert the_err.message in message
 
+
+def test_serialize_numpy_types():
+    m = MySchema(
+        a={'date': np.datetime64('2019-01-01')},
+        a2={'int64': np.int64(1), 'float64': np.float64(2)},
+        b2=np.arange(4),
+    )
+    out = m.to_json()
+    dct = json.loads(out)
+    assert dct == {
+        'a': {'date': '2019-01-01T00:00:00'},
+        'a2': {'int64': 1, 'float64': 2},
+        'b2': [0, 1, 2, 3],
+    }
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -1,9 +1,12 @@
 import copy
 import io
+import json
 import jsonschema
 import pickle
 import pytest
 
+import numpy as np
+
 from ..schemapi import (UndefinedType, SchemaBase, Undefined, _FromDict,
                         SchemaValidationError)
 
@@ -320,3 +323,17 @@ def test_schema_validation_error():
     assert "validating {!r}".format(the_err.validator) in message
     assert the_err.message in message
 
+
+def test_serialize_numpy_types():
+    m = MySchema(
+        a={'date': np.datetime64('2019-01-01')},
+        a2={'int64': np.int64(1), 'float64': np.float64(2)},
+        b2=np.arange(4),
+    )
+    out = m.to_json()
+    dct = json.loads(out)
+    assert dct == {
+        'a': {'date': '2019-01-01T00:00:00'},
+        'a2': {'int64': 1, 'float64': 2},
+        'b2': [0, 1, 2, 3],
+    }