Skip to content

Commit

Permalink
Merge pull request #1062 from jakevdp/internals
Browse files Browse the repository at this point in the history
DOC: add documentation of internal details + some doc cleanup
  • Loading branch information
jakevdp authored Jul 31, 2018
2 parents e33c0e2 + f0bf1d9 commit 98a4ee7
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 5 deletions.
19 changes: 19 additions & 0 deletions altair/vegalite/v2/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,25 @@ def __init__(self, data=Undefined, encoding=Undefined, mark=Undefined,

@classmethod
def from_dict(cls, dct, validate=True):
"""Construct class from a dictionary representation
Parameters
----------
dct : dictionary
The dict from which to construct the class
validate : boolean
If True (default), then validate the input against the schema.
Returns
-------
obj : Chart object
The wrapped schema
Raises
------
jsonschema.ValidationError :
if validate=True and dct does not conform to the schema
"""
# First try from_dict for the Chart type
try:
return super(Chart, cls).from_dict(dct, validate=validate)
Expand Down
1 change: 0 additions & 1 deletion doc/getting_started/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ development version directly from GitHub using:
.. _vega_datasets: https://github.com/altair-viz/vega_datasets

.. _Vega-Lite: http://vega.github.io/vega-lite
.. _Vega: https://vega.github.io/vega/
.. _conda: http://conda.pydata.org
.. _Altair source repository: http://github.com/altair-viz/altair
.. _JupyterLab: http://jupyterlab.readthedocs.io/en/stable/
Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ beautiful and effective visualizations with a minimal amount of code.
user_guide/faq
user_guide/troubleshooting
user_guide/renderers
user_guide/internals
user_guide/API

.. toctree::
Expand Down
6 changes: 6 additions & 0 deletions doc/user_guide/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
API Reference
=============

This is the class and function reference of Altair, and the following content
is generated automatically from the code documentation strings.
Please refer to the `full user guide <http://altair-viz.github.io>`_ for
further details, as this low-level documentation may not be enough to give
full guidelines on their use.

Top-Level Objects
-----------------

Expand Down
3 changes: 1 addition & 2 deletions doc/user_guide/display_frontends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@ The command will block the Python interpreter, and will have to be canceled with
``Ctrl-C`` to execute any further code.

.. _entrypoints: https://github.com/takluyver/entrypoints
.. _ipyvega: https://github.com/vega/ipyvega/tree/master
.. _ipyvega: https://github.com/vega/ipyvega/tree/vega
.. _ipyvega: https://github.com/vega/ipyvega/
.. _JupyterLab: http://jupyterlab.readthedocs.io/en/stable/
.. _nteract: https://nteract.io
.. _Colab: https://colab.research.google.com
Expand Down
4 changes: 2 additions & 2 deletions doc/user_guide/importing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ create Vega plots using a very low-level Python interface that mirrors the
schema itself.


.. Vega-Lite: http://vega.github.io/vega-lite/
.. Vega: http://vega.github.io/vega/
.. _Vega-Lite: http://vega.github.io/vega-lite/
.. _Vega: http://vega.github.io/vega/
228 changes: 228 additions & 0 deletions doc/user_guide/internals.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
.. currentmodule:: altair

.. _user-guide-internals:

Altair Internals: Understanding the Library
===========================================
This section will provide some details about how the Altair API relates to the
Vega-Lite visualization specification, and how you can use that knowledge to
use the package more effectively.

First of all, it is important to realize that when stripped down to its core,
Altair itself cannot render visualizations. Altair is an API that does one
very well-defined thing:

- **Altair provides a Python API for generating validated Vega-Lite specifications**

That's it. In order to take those specifications and turn them into actual
visualizations requires a frontend that is correctly set up, but strictly
speaking that rendering is generally not controlled by the Altair package.

Altair chart to Vega-Lite Spec
------------------------------
Since Altair is fundamentally about constructing chart specifications, the central
functionality of any chart object are the :meth:`~Chart.to_dict` and
:meth:`~Chart.to_json` methods, which output the chart specification as a Python
dict or JSON string, respectively. For example, here is a simple scatter chart,
from which we can output the JSON representation:

.. altair-plot::
:output: stdout

import altair as alt
from vega_datasets import data

chart = alt.Chart(data.cars.url).mark_point().encode(
x='Horsepower:Q',
y='Miles_per_Gallon:Q',
color='Origin:N',
).configure_view(
height=300,
width=400,
)

print(chart.to_json(indent=2))

Before returning the dict or JSON output, Altair validates it against the
`Vega-Lite schema <https://github.com/vega/schema>`_ using the
`jsonschema <https://python-jsonschema.readthedocs.io/en/latest/>`_ package.
The Vega-Lite schema defines valid attributes and values that can appear
within the specification of a Vega-Lite chart.

With the JSON schema in hand, it can then be passed to a library such as
`Vega-Embed <https://github.com/vega/vega-embed>`_ that knows how to read the
specification and render the chart that it describes, and the result is the
following visualization:

.. altair-plot::
:hide-code:

chart

Whenever you use Altair within JupyterLab, Jupyter notebook, or other frontends,
it is frontend extensions that extract the JSON output from the Altair chart
object and pass that specification along to the appropriate rendering code.

Altair's Low-Level Object Structure
-----------------------------------
The standard API methods used in Altair (e.g. :meth:`~Chart.mark_point`,
:meth:`~Chart.encode`, ``configure_*()``, ``transform_*()``, etc.)
are higher-level convenience functions that wrap the low-level API.
That low-level API is essentially a Python object hierarchy that mirrors
that of the JSON schema definition.

For example, we can choose to avoid the convenience methods and rather construct
the above chart using these low-level object types directly:

.. altair-plot::

alt.Chart(
data=alt.UrlData(
url='https://vega.github.io/vega-datasets/data/cars.json'
),
mark='point',
encoding=alt.EncodingWithFacet(
x=alt.PositionFieldDef(
field='Horsepower',
type='quantitative'
),
y=alt.PositionFieldDef(
field='Miles_per_Gallon',
type='quantitative'
),
color=alt.MarkPropFieldDefWithCondition(
field='Origin',
type='nominal'
)
),
config=alt.Config(
view=alt.ViewConfig(
height=300,
width=400
)
)
)

This low-level approach is much more verbose than the typical idiomatic approach
to creating Altair charts, but it makes much more clear the mapping between
Altair's python object structure and Vega-Lite's schema definition structure.

One of the nice features of Altair is that this low-level object hierarchy is not
constructed by hand, but rather *programmatically generated* from the Vega-Lite
schema, using the ``generate_schema_wrapper.py`` script that you can find in
`Altair's repository <https://github.com/altair-viz/altair/blob/master/tools/generate_schema_wrapper.py>`_.
This auto-generation of code propagates descriptions from the vega-lite schema
into the Python class docstrings, from which the
`API Reference <http://altair-viz.github.io/user_guide/API.html>`_
within Altair's documentation are in turn automatically generated.
This means that as the Vega-Lite schema evolves, Altair can very quickly be brought
up-to-date, and only the higher-level chart methods need to be updated by hand.

Converting Vega-Lite to Altair
------------------------------
With this knowledge in mind, and with a bit of practice, it is fairly
straightforward to construct an Altair chart from a Vega-Lite spec.
For example, consider the
`Simple Bar Chart <https://vega.github.io/vega-lite/examples/bar.html>`_ example
from the Vega-Lite documentation, which has the following JSON specification::

{
"$schema": "https://vega.github.io/schema/vega-lite/v2.json",
"description": "A simple bar chart with embedded data.",
"data": {
"values": [
{"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
{"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
{"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
]
},
"mark": "bar",
"encoding": {
"x": {"field": "a", "type": "ordinal"},
"y": {"field": "b", "type": "quantitative"}
}
}

At the lowest level, we can use the :meth:`~Chart.from_json` class method to
construct an Altair chart object from this string of Vega-Lite JSON:

.. altair-plot::

import altair as alt

alt.Chart.from_json("""
{
"$schema": "https://vega.github.io/schema/vega-lite/v2.json",
"description": "A simple bar chart with embedded data.",
"data": {
"values": [
{"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
{"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
{"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
]
},
"mark": "bar",
"encoding": {
"x": {"field": "a", "type": "ordinal"},
"y": {"field": "b", "type": "quantitative"}
}
}
""")

Likewise, if you have the Python dictionary equivalent of the JSON string,
you can use the :meth:`~Chart.from_dict` method to construct the chart object:

.. altair-plot::

import altair as alt

alt.Chart.from_dict({
"$schema": "https://vega.github.io/schema/vega-lite/v2.json",
"description": "A simple bar chart with embedded data.",
"data": {
"values": [
{"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
{"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
{"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
]
},
"mark": "bar",
"encoding": {
"x": {"field": "a", "type": "ordinal"},
"y": {"field": "b", "type": "quantitative"}
}
})

With a bit more effort and some judicious copying and pasting, we can
manually convert this into more idiomatic Altair code for the same chart,
including constructing a Pandas dataframe from the data values:

.. altair-plot::

import altair as alt
import pandas as pd

data = pd.DataFrame.from_records([
{"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
{"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
{"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
])

alt.Chart(data).mark_bar().encode(
x='a:O',
y='b:Q'
)

The key is to realize that ``"encoding"`` properties are usually set using the
:meth:`~Chart.encode` method, encoding types are usually computed from
short-hand type codes, ``"transform"`` and ``"config"`` properties come from
the ``transform_*()`` and ``configure_*()`` methods, and so on.

This approach is the process by which Altair contributors constructed many
of the initial examples in the
`Altair Example Gallery <https://altair-viz.github.io/gallery/index.html>`_,
drawing inspiration from the
`Vega-Lite Example Gallery <https://vega.github.io/vega-lite/examples/>`_.
Becoming familiar with the mapping between Altair and Vega-Lite at this level
is useful in making use of the Vega-Lite documentation in places where Altair's
documentation is weak or incomplete.

0 comments on commit 98a4ee7

Please sign in to comment.