-
Notifications
You must be signed in to change notification settings - Fork 795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Changing to MIME based rendering #216
Conversation
Is this close to being ready to merge? I'm excited to see Altair working with JupyterLab! |
Any news on this? I've mostly switched to jupyterlab at this point but I'm sorely missing Altair. |
You must be the earliest of early-adopters 😄 |
altair/api.py
Outdated
from vega import VegaLite | ||
display(VegaLite(self.to_dict())) | ||
spec = self.to_dict() | ||
data = {'application/vnd.vegalite+json': spec} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest adding fallback mimetypes:
data = {
'application/vnd.vegalite+json': spec,
'application/json': spec,
'text/plain': '<altair.VegaLite object>'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add the text/plain one, but because the spec includes the full data, I don't think we should ship it twice...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok:
data = {
'application/vnd.vegalite.v1+json': spec,
'text/plain': '<altair.VegaLite object>'
}
Before we merge this PR, other things to add:
|
☑️ Bring over utils.py from ipyvega: altair-viz/jupyter_vega#8 |
Ok, last thing to do is move From my understanding, the need for
Another option is: data = df.to_json(orient='records') This should sanitize the DataFrame as well and it doesn't require that we maintain this sanitization code in the future. Thoughts? |
We should also move over the main Vega and VegaLite classes. In our
discussion with Kyle, we felt that users would never use those classes in
practice without Altair. Also, Kyle wants to be abel touse them without
requiring jupyterlab_vega.
We have found that df.to_json alone doesn't do sufficient sanitization for
what Vega/VegaLite is expecting. I have recently found other cases that our
current sanitization code doesn't handle . If anything I think we will have
to expand what it does...
…On Fri, Mar 10, 2017 at 5:29 PM, Grant Nestor ***@***.***> wrote:
Ok, last thing to do is move utils.py from jupyterlab_vega:
https://github.com/altair-viz/jupyterlab_vega/blob/master/
jupyterlab_vega/utils.py
From my understanding, the need for utils.py is to sanitize DataFrames
before displaying. It does this by:
- Copying the DaraFrame
- Raise ValueError if it has a hierarchical index
- Convert categoricals to strings
- Convert np.int dtypes to Python int objects
- Convert floats to objects and replace NaNs by None
- Convert DateTime dtypes into appropriate string representations
Another option is:
data = df.to_json(orient='records')
This should sanitize the DataFrame as well and it doesn't require that we
maintain this sanitization code in the future.
Thoughts?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#216 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AABr0AnRkhwGmWzrelEGduxONmbwY_npks5rkerMgaJpZM4KKaTc>
.
--
Brian E. Granger
Associate Professor of Physics and Data Science
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
[email protected] and [email protected]
|
Ok, regarding the classes, there isn't much in there that's relevant to Altair. Here is the Vega (Base) class: class Vega():
"""A display class for displaying Vega visualizations in the Jupyter Notebook and IPython kernel.
Vega expects a spec (a JSON-able dict) and data (dict) argument
not already-serialized JSON strings.
Scalar types (None, number, string) are not allowed, only dict containers.
"""
# wrap data in a property, which warns about passing already-serialized JSON
_spec = None
_data = None
_read_flags = 'r'
def __init__(self, spec=None, data=None, url=None, filename=None, metadata=None):
"""Create a Vega display object given raw data.
Parameters
----------
spec : dict
Vega spec. Not an already-serialized JSON string.
data : dict
A dict of Vega datasets where the key is the dataset name and the
value is the data values. Not an already-serialized JSON string.
Scalar types (None, number, string) are not allowed, only dict
or list containers.
url : unicode
A URL to download the data from.
filename : unicode
Path to a local file to load the data from.
metadata: dict
Specify extra metadata to attach to the json display object.
"""
if spec is not None and isinstance(spec, str):
if spec.startswith('http') and url is None:
url = spec
filename = None
spec = None
elif _safe_exists(spec) and filename is None:
url = None
filename = spec
spec = None
self.spec = spec
self.data = data
self.metadata = metadata
self.url = url
self.filename = filename
self.reload()
self._check_data()
def reload(self):
"""Reload the raw spec from file or URL."""
if self.filename is not None:
with open(self.filename, self._read_flags) as f:
self.spec = json.loads(f.read())
elif self.url is not None:
try:
# Deferred import
from urllib.request import urlopen
response = urlopen(self.url)
self.spec = response.read()
# extract encoding from header, if there is one:
encoding = None
for sub in response.headers['content-type'].split(';'):
sub = sub.strip()
if sub.startswith('charset'):
encoding = sub.split('=')[-1].strip()
break
# decode spec, if an encoding was specified
if encoding:
self.spec = self.spec.decode(encoding, 'replace')
except:
self.spec = None
def _check_data(self):
if self.spec is not None and not isinstance(self.spec, dict):
raise TypeError("%s expects a JSONable dict, not %r" % (self.__class__.__name__, self.spec))
if self.data is not None and not isinstance(self.data, dict):
raise TypeError("%s expects a dict, not %r" % (self.__class__.__name__, self.data))
@property
def spec(self):
return self._spec
@property
def data(self):
return self._data
@spec.setter
def spec(self, spec):
if isinstance(spec, str):
# warnings.warn("%s expects a JSONable dict, not %r" % (self.__class__.__name__, spec))
spec = json.loads(spec)
self._spec = spec
@data.setter
def data(self, data):
if isinstance(data, str):
# warnings.warn("%s expects a dict, not %r" % (self.__class__.__name__, data))
data = json.loads(data)
self._data = data
def _ipython_display_(self):
bundle = {
'application/vnd.vega.v2+json': prepare_vega_spec(self.spec, self.data),
'text/plain': '<jupyterlab_vega.Vega object>'
}
display(bundle, raw=True) Altair already handles URLs and data validation, pandas can load data from files, so it seems to me that the only relevant part is If we take a look at def prepare_vegalite_spec(spec, data=None):
"""Prepare a Vega-Lite spec for sending to the frontend.
This allows data to be passed in either as part of the spec
or separately. If separately, the data is assumed to be a
pandas DataFrame or object that can be converted to to a DataFrame.
Note that if data is not None, this modifies spec in-place
"""
if isinstance(data, pd.DataFrame):
# We have to do the isinstance test first because we can't
# compare a DataFrame to None.
data = sanitize_dataframe(data)
spec['data'] = {'values': data.to_dict(orient='records')}
elif data is None:
# Data is either passed in spec or error
if 'data' not in spec:
raise ValueError('No data provided')
else:
# As a last resort try to pass the data to a DataFrame and use it
data = pd.DataFrame(data)
data = sanitize_dataframe(data)
spec['data'] = {'values': data.to_dict(orient='records')}
return spec Given that Altair only stores data in a DataFrame and Now, class ToDict(Visitor):
"""Crawl object structure to output dictionary"""
def generic_visit(self, obj, *args, **kwargs):
return obj
def visit_list(self, obj, *args, **kwargs):
return [self.visit(o) for o in obj]
def visit_BaseObject(self, obj, *args, **kwargs):
D = {}
for k in obj.traits():
if k in obj and k not in obj.skip:
v = getattr(obj, k)
if v is not None:
D[k] = self.visit(v)
return D
def _visit_with_data(self, obj, data=True):
D = self.visit_BaseObject(obj)
if data:
if isinstance(obj.data, schema.Data):
D['data'] = self.visit(obj.data)
elif isinstance(obj.data, pd.DataFrame):
values = sanitize_dataframe(obj.data).to_dict(orient='records')
D['data'] = self.visit(schema.Data(values=values))
else:
D.pop('data', None)
return D
visit_Chart = _visit_with_data
visit_LayeredChart = _visit_with_data
visit_FacetedChart = _visit_with_data Notice In conclusion, I don't think we need to migrate anything from jupyterlab_vega because def _ipython_display_(self):
from IPython.display import display
spec = self.to_dict()
data = {
'application/vnd.vegalite.v1+json': spec,
'text/plain': '<altair.VegaLite object>'
}
display(data, raw=True) |
TL;DR: Altair's |
@jakevdp Any idea why this task is failing? https://travis-ci.org/altair-viz/altair/jobs/223802066 |
Looks like it's trying to install IPython 6.0 on Python 2.7. I think the fix is to update the travis.yml so that it runs |
Hopefully will be fixed by #319 |
OK – if you pull in those commits from master, things should work now. |
It worked! Thanks @jakevdp! This is ready for review. Feel free to check out and test with JupyterLab. |
Looks good! The only issues I see reading through the changes are that I'll try to give it a test run in JupyterLab today. |
Good catch! Updated. |
Oops, I should have thought of that... travis uses the contents of Brings me to altair-viz/jupyter_vega#32, which we need to think about before moving forward here. |
I have picked this work up again in preparation for the JupyterLab beta, which is planned for this Friday. We have decided that JupyterLab will ship with vega/vegalite rendering built in. Because of that the Based on that, I have done the following:
I am finishings this up, but things are already working well: |
doc/index.rst
Outdated
|
||
# load built-in dataset as a pandas DataFrame | ||
cars = load_dataset('cars') | ||
|
||
# Needed for rendering in JupyterLab, nteract, omit in the classic Jupyter Notebook | ||
enable_mime_rendering() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is an irreversible operation, we might comment it out for people who do a mass copy-paste and run the code before reading it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
I've not had a chance to try it yet, but the code looks good on a read-through. I'm happy to have this merged. |
Going ahead with the merge! Once this jupyterlab/jupyterlab#2578 is merged everything should be working fine with master of both. |
As used by altair: vega/altair#216
As used by altair: vega/altair#216
Initial prototype that works with JupyterLab.
Closes #294 and #172.