Repository: vega/altair

Issue:
Default Mark Property
I came accross a problem with the display of altair plots in Jupyter notebooks.
Take the following code, where df is any dataset:
    
    from altair import Chart
    Chart(df).to_dict().get('mark')
    
This would return 'point' in altair 1.0.0. In altair 1.2.0 this returns None, which results in a javascript error of the jupyter-vega extension and no display of the plot.

If the latter behavior is anticipated (i.e. the mark property of vega-lite plots may be undefined) the jupyter-vega extension would need a fix.

Niels


Discussion:
Thanks! Yes, I'd noticed that changed, but hadn't appreciated the consequences. We'll make sure to fix this in the upcoming 2.0 release.
I marked this as 1.3 as it should be easy to fix.

Resolving patch:
diff --git a/altair/__init__.py b/altair/__init__.py
--- a/altair/__init__.py
+++ b/altair/__init__.py
@@ -40,6 +40,7 @@
     EqualFilter,
     RangeFilter,
     OneOfFilter,
+    MaxRowsExceeded
 )
 
 from .datasets import (
diff --git a/altair/api.py b/altair/api.py
--- a/altair/api.py
+++ b/altair/api.py
@@ -51,6 +51,13 @@
 from .schema import UnitEncoding
 from .schema import VerticalAlign
 
+
+class MaxRowsExceeded(Exception):
+    """Raised if the number of rows in the dataset is too large."""
+    pass
+
+DEFAULT_MAX_ROWS = 5000
+
 #*************************************************************************
 # Channel Aliases
 #*************************************************************************
@@ -338,10 +345,27 @@ def serve(self, ip='127.0.0.1', port=8888, n_retries=50, files=None,
 
     def _finalize_data(self):
         """
-        This function is called by _finalize() below. It checks whether the
-        data attribute contains expressions, and if so it extracts the
-        appropriate data object and generates the appropriate transforms.
+        This function is called by _finalize() below. 
+
+        It performs final checks on the data:
+
+        * If the data has too many rows (more than max_rows).
+        * Whether the data attribute contains expressions, and if so it extracts
+          the appropriate data object and generates the appropriate transforms.
         """
+        # Check to see if data has too many rows.
+        if isinstance(self.data, pd.DataFrame):
+            if len(self.data) > self.max_rows:
+                raise MaxRowsExceeded(
+                    "Your dataset has too many rows and could take a long "
+                    "time to send to the frontend or to render. To override the "
+                    "default maximum rows (%s), set the max_rows property of "
+                    "your Chart to an integer larger than the number of rows "
+                    "in your dataset. Alternatively you could perform aggregations "
+                    "or other data reductions before using it with Altair" % DEFAULT_MAX_ROWS
+                )
+
+        # Handle expressions.
         if isinstance(self.data, expr.DataFrame):
             columns = self.data._cols
             calculated_cols = self.data._calculated_cols
@@ -370,6 +394,11 @@ class Chart(schema.ExtendedUnitSpec, TopLevelMixin):
     transform = T.Instance(Transform, allow_none=True, default_value=None,
                            help=schema.ExtendedUnitSpec.transform.help)
     mark = schema.Mark(allow_none=True, default_value='point', help="""The mark type.""")
+    
+    max_rows = T.Int(
+        default_value=DEFAULT_MAX_ROWS,
+        help="Maximum number of rows in the dataset to accept."
+    )
 
     @property
     def data(self):
@@ -385,7 +414,7 @@ def data(self, new):
         else:
             raise TypeError('Expected DataFrame or altair.Data, got: {0}'.format(new))
 
-    skip = ['data', '_data']
+    skip = ['data', '_data', 'max_rows']
 
     def __init__(self, data=None, **kwargs):
         super(Chart, self).__init__(**kwargs)
@@ -513,6 +542,10 @@ class LayeredChart(schema.LayerSpec, TopLevelMixin):
                     help=schema.LayerSpec.layers.help)
     transform = T.Instance(Transform, allow_none=True, default_value=None,
                            help=schema.LayerSpec.transform.help)
+    max_rows = T.Int(
+        default_value=DEFAULT_MAX_ROWS,
+        help="Maximum number of rows in the dataset to accept."
+    )
 
     @property
     def data(self):
@@ -528,7 +561,7 @@ def data(self, new):
         else:
             raise TypeError('Expected DataFrame or altair.Data, got: {0}'.format(new))
 
-    skip = ['data', '_data']
+    skip = ['data', '_data', 'max_rows']
 
     def __init__(self, data=None, **kwargs):
         super(LayeredChart, self).__init__(**kwargs)
@@ -567,6 +600,10 @@ class FacetedChart(schema.FacetSpec, TopLevelMixin):
                    help=schema.FacetSpec.spec.help)
     transform = T.Instance(Transform, allow_none=True, default_value=None,
                            help=schema.FacetSpec.transform.help)
+    max_rows = T.Int(
+        default_value=DEFAULT_MAX_ROWS,
+        help="Maximum number of rows in the dataset to accept."
+    )
 
     @property
     def data(self):
@@ -582,7 +619,7 @@ def data(self, new):
         else:
             raise TypeError('Expected DataFrame or altair.Data, got: {0}'.format(new))
 
-    skip = ['data', '_data']
+    skip = ['data', '_data', 'max_rows']
 
     def __init__(self, data=None, **kwargs):
         super(FacetedChart, self).__init__(**kwargs)


Test patch:
diff --git a/altair/tests/test_api.py b/altair/tests/test_api.py
--- a/altair/tests/test_api.py
+++ b/altair/tests/test_api.py
@@ -556,3 +556,15 @@ def test_empty_traits():
     # regression test for changes in #265
     assert Transform().to_dict() == {}  # filter not present
     assert Formula('blah').to_dict() == {'field': 'blah'}  # expr not present
+
+
+def test_max_rows():
+    chart = make_chart()
+    assert isinstance(chart.to_dict(), dict)
+    chart.max_rows = 5
+    with pytest.raises(MaxRowsExceeded):
+        chart.to_dict()
+    chart.max_rows = 15
+    d = chart.to_dict()
+    assert isinstance(d, dict)
+    assert 'max_rows' not in d