├── altair_data_server
├── tests
│ ├── __init__.py
│ ├── test_entrypoint.py
│ ├── test_altair_server.py
│ └── test_provider.py
├── __init__.py
├── _altair_server.py
├── _background_server.py
└── _provide.py
├── requirements.txt
├── requirements_dev.txt
├── MANIFEST.in
├── setup.cfg
├── mypy.ini
├── postBuild
├── .travis.yml
├── Makefile
├── .github
└── workflows
│ ├── build.yml
│ └── lint.yml
├── RELEASING.md
├── CHANGES.md
├── LICENSE
├── .gitignore
├── setup.py
├── README.md
└── AltairDataServer.ipynb
/altair_data_server/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | altair
2 | portpicker
3 | tornado
4 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | black
2 | flake8
3 | mypy
4 | pytest
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | include Makefile
3 | include LICENSE
4 | include CHANGES.md
5 | include requirements.txt
6 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | ignore = E203, E266, E501, W503
4 | max-complexity = 18
5 | select = B,C,E,F,W,T4,B9
6 |
7 | [metadata]
8 | description-file = README.md
9 | license_file = LICENSE
10 |
--------------------------------------------------------------------------------
/altair_data_server/__init__.py:
--------------------------------------------------------------------------------
1 | """Altair data server"""
2 | __version__ = "0.5.0.dev0"
3 | __all__ = [
4 | "AltairDataServer",
5 | "data_server",
6 | "data_server_proxied",
7 | "Provider",
8 | "Resource",
9 | ]
10 |
11 | from ._altair_server import AltairDataServer, data_server, data_server_proxied
12 | from ._provide import Provider, Resource
13 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | python_version = 3.8
3 |
4 | [mypy-altair.*]
5 | ignore_missing_imports = True
6 |
7 | [mypy-numpy.*]
8 | ignore_missing_imports = True
9 |
10 | [mypy-pandas.*]
11 | ignore_missing_imports = True
12 |
13 | [mypy-portpicker.*]
14 | ignore_missing_imports = True
15 |
16 | [mypy-pytest.*]
17 | ignore_missing_imports = True
18 |
19 | [mypy-tornado.*]
20 | ignore_missing_imports = True
--------------------------------------------------------------------------------
/postBuild:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This is the binder-specific post-build file and ensure that the notebook runs
3 | # nicely. See https://mybinder.readthedocs.io/en/latest/config_files.html for more details.
4 |
5 | set -euo pipefail
6 |
7 | # To reach the data server behind the binder firewall, we need to inject a proxy.
8 | pip install jupyter-server-proxy
9 |
10 | # Jupyterlab 1.0 or newer required for Altair 3.
11 | pip install -U jupyterlab
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | cache: pip
4 |
5 | matrix:
6 | include:
7 | - python: 3.6
8 | - python: 3.7
9 | - python: 3.8
10 | - name: "lint"
11 | python: 3.8
12 | install:
13 | script:
14 | - black --check .
15 | - flake8 altair_data_server
16 | - mypy altair_data_server
17 |
18 | env:
19 | global:
20 | - TEST_DIR=/tmp/_altair_data_server/
21 |
22 | before_install:
23 | - pip install pip --upgrade
24 | - pip install -r requirements_dev.txt
25 |
26 | install:
27 | - pip install -e .
28 |
29 | script:
30 | - mkdir -p $TEST_DIR
31 | - cd $TEST_DIR && python -m pytest --pyargs altair_data_server
32 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: install
2 |
3 | PACKAGE = altair_data_server
4 |
5 | install:
6 | pip install .
7 |
8 | test:
9 | python -m black .
10 | python -m flake8 $(PACKAGE)
11 | python -m mypy $(PACKAGE)
12 | rm -r build
13 | python setup.py build &&\
14 | cd build/lib &&\
15 | python -m pytest --pyargs --doctest-modules $(PACKAGE)
16 |
17 | test-coverage:
18 | python setup.py build &&\
19 | cd build/lib &&\
20 | python -m pytest --pyargs --doctest-modules --cov=$(PACKAGE) --cov-report term $(PACKAGE)
21 |
22 | test-coverage-html:
23 | python setup.py build &&\
24 | cd build/lib &&\
25 | python -m pytest --pyargs --doctest-modules --cov=$(PACKAGE) --cov-report html $(PACKAGE)
26 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: [ '3.6', '3.7', '3.8' ]
11 | name: Python ${{ matrix.python-version }}
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v1
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install -e .
22 | - name: Test with pytest
23 | run: |
24 | pip install pytest
25 | pytest --doctest-modules altair_data_server
26 |
--------------------------------------------------------------------------------
/altair_data_server/tests/test_entrypoint.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import altair as alt
4 | from altair_data_server import data_server, data_server_proxied
5 |
6 | import pytest
7 |
8 |
9 | def test_entrypoint_exists() -> None:
10 | assert "data_server" in alt.data_transformers.names()
11 | assert "data_server_proxied" in alt.data_transformers.names()
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "name,server_function",
16 | [("data_server", data_server), ("data_server_proxied", data_server_proxied)],
17 | )
18 | def test_entrypoint_identity(name: str, server_function: Callable) -> None:
19 | with alt.data_transformers.enable(name):
20 | transformer = alt.data_transformers.get()
21 | assert transformer is server_function
22 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | name: flake8-black-mypy
9 | steps:
10 | - uses: actions/checkout@v1
11 | - name: Set up Python 3.8
12 | uses: actions/setup-python@v1
13 | with:
14 | python-version: 3.8
15 | - name: Install Requirements
16 | run: |
17 | pip install -r requirements.txt
18 | - name: Lint with flake8
19 | run: |
20 | pip install flake8
21 | flake8 . --count --statistics
22 | - name: Check formatting with black
23 | run: |
24 | pip install black
25 | black --check .
26 | - name: Check types with mypy
27 | run: |
28 | pip install mypy
29 | mypy altair_data_server
30 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | 1. Update version to, e.g. 1.0.0 in ``altair_data_server/__init__.py``
2 |
3 | 2. Make sure ``CHANGES.md`` is up to date for the release
4 |
5 | 3. Commit change and push to master
6 |
7 | git add . -u
8 | git commit -m "MAINT: bump version to 1.0.0"
9 | git push origin master
10 |
11 | 4. Tag the release:
12 |
13 | git tag -a v1.0.0 -m "version 1.0.0 release"
14 | git push origin v1.0.0
15 |
16 | 5. Build source & wheel distributions
17 |
18 | rm -r dist build # clean old builds & distributions
19 | python setup.py sdist # create a source distribution
20 | python setup.py bdist_wheel # create a universal wheel
21 |
22 | 6. publish to PyPI (Requires correct PyPI owner permissions)
23 |
24 | twine upload dist/*
25 |
26 | 7. update version to, e.g. 1.1.0.dev0 in ``altair_data_server/__init__.py``
27 |
28 | 8. add a new changelog entry for the unreleased version
29 |
30 | 9. Commit change and push to master
31 |
32 | git add . -u
33 | git commit -m "MAINT: bump version to 1.1.0.dev0"
34 | git push origin master
35 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Altair Data Server Change Log
2 |
3 | ## Version 0.5.0 (unreleased)
4 |
5 | ## Version 0.4.1
6 |
7 | - Allow content to be served from root URL
8 | - Fix some testing & distribution configurations
9 |
10 | ## Version 0.4.0
11 |
12 | - Make ``Provider`` and ``Resource`` top-level imports (#21).
13 | - Use a daemonic thread by default, so that server will automatically shut down
14 | when the parent python process terminates (#24).
15 | - Facilitate subclassing of ``Provider`` class (#27).
16 | - Add ability to specify port when enabling altair data server (#28).
17 | - Many minor bug fixes and improvements to testing, type hints, and CI.
18 |
19 | ## Version 0.3.0
20 |
21 | - Add support for Python 3.8
22 | - Drop support for Python 3.5 and lower
23 | - Format code with [black](https://black.readthedocs.io/)
24 | - Add static type checking with [mypy](http://mypy-lang.org/)
25 |
26 | ## Version 0.2.1
27 |
28 | - Add altair v4 entrypoint
29 |
30 | ## Version 0.2.0
31 |
32 | - Add `data_server_proxied` entrypoint for use with [jupyter-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy) ([#5](https://github.com/altair-viz/altair_data_server/pull/5))
33 | - Update implementation to support Tornado 6.0 ([#6](https://github.com/altair-viz/altair_data_server/pull/6))
34 |
35 | ## Version 0.1.0
36 |
37 | Initial release: basic Altair data server implementation with the following
38 | entrypoints:
39 |
40 | - ``altair.vegalite.v2.data_transformer``
41 | - ``altair.vegalite.v3.data_transformer``
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, altair-viz
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # emacs
107 | *~
108 |
109 | # Jupyter
110 | Untitled*.ipynb
--------------------------------------------------------------------------------
/altair_data_server/tests/test_altair_server.py:
--------------------------------------------------------------------------------
1 | import portpicker
2 | import re
3 | from typing import Any, Callable
4 |
5 | import numpy as np
6 | import pandas as pd
7 | import pytest
8 | from altair_data_server import data_server, data_server_proxied
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def session_context(request: Any) -> None:
13 | # Reset the server at the end of the session.
14 | request.addfinalizer(data_server.reset)
15 | request.addfinalizer(data_server_proxied.reset)
16 |
17 |
18 | @pytest.fixture
19 | def data() -> pd.DataFrame:
20 | return pd.DataFrame({"x": np.arange(5), "y": list("ABCDE")})
21 |
22 |
23 | def _decode_normal_url(url: str, fmt: str) -> str:
24 | return url
25 |
26 |
27 | def _decode_proxied_url(url: str, fmt: str) -> str:
28 | match = re.match(r"^\.\./proxy/([0-9]+)/([a-f0-9-]*\.([a-z]+))$", url)
29 | assert match
30 | assert match.group(3) == fmt
31 |
32 | # proxy only works when running under jupyter, use direct access here
33 | return f"http://localhost:{match.group(1)}/{match.group(2)}"
34 |
35 |
36 | @pytest.mark.parametrize(
37 | "fmt,parse_function", [("json", pd.read_json), ("csv", pd.read_csv)]
38 | )
39 | @pytest.mark.parametrize(
40 | "server_function,url_decoder",
41 | [(data_server, _decode_normal_url), (data_server_proxied, _decode_proxied_url)],
42 | )
43 | def test_data_server(
44 | data: pd.DataFrame,
45 | session_context: Any,
46 | fmt: str,
47 | parse_function: Callable,
48 | server_function: Callable,
49 | url_decoder: Callable,
50 | ) -> None:
51 | spec = server_function(data, fmt=fmt)
52 | assert isinstance(spec, dict)
53 | assert list(spec.keys()) == ["url"]
54 |
55 | url = url_decoder(spec["url"], fmt)
56 | served_data = parse_function(url)
57 | assert data.equals(served_data)
58 |
59 |
60 | @pytest.mark.parametrize(
61 | "server_function,url_decoder",
62 | [(data_server, _decode_normal_url), (data_server_proxied, _decode_proxied_url)],
63 | )
64 | @pytest.mark.parametrize("fmt", ["json", "csv"])
65 | def test_data_server_port(
66 | data: pd.DataFrame,
67 | session_context: Any,
68 | fmt: str,
69 | server_function: Callable,
70 | url_decoder: Callable,
71 | ) -> None:
72 | port = portpicker.pick_unused_port()
73 | spec = server_function(data, port=port, fmt=fmt)
74 | url = url_decoder(spec["url"], fmt=fmt)
75 | assert str(port) in url
76 |
--------------------------------------------------------------------------------
/altair_data_server/_altair_server.py:
--------------------------------------------------------------------------------
1 | """Altair data server."""
2 |
3 | from typing import Dict, Optional, Tuple
4 | from urllib import parse
5 |
6 | from altair_data_server._provide import Provider, Resource
7 | from altair.utils.data import (
8 | _data_to_json_string,
9 | _data_to_csv_string,
10 | _compute_data_hash,
11 | )
12 | import pandas as pd
13 |
14 |
15 | class AltairDataServer:
16 | """Backend server for Altair datasets."""
17 |
18 | def __init__(self) -> None:
19 | self._provider: Optional[Provider] = None
20 | # We need to keep references to served resources, because the background
21 | # server uses weakrefs.
22 | self._resources: Dict[str, Resource] = {}
23 |
24 | def reset(self) -> None:
25 | if self._provider is not None:
26 | self._provider.stop()
27 | self._resources = {}
28 |
29 | @staticmethod
30 | def _serialize(data: pd.DataFrame, fmt: str) -> Tuple[str, str]:
31 | """Serialize data to the given format."""
32 | if fmt == "json":
33 | content = _data_to_json_string(data)
34 | elif fmt == "csv":
35 | content = _data_to_csv_string(data)
36 | else:
37 | raise ValueError(f"Unrecognized format: {fmt!r}")
38 | return content, _compute_data_hash(content)
39 |
40 | def __call__(
41 | self, data: pd.DataFrame, fmt: str = "json", port: Optional[int] = None
42 | ) -> Dict[str, str]:
43 | if self._provider is None:
44 | self._provider = Provider().start(port=port)
45 | if port is not None and port != self._provider.port:
46 | self._provider.stop().start(port=port)
47 | content, resource_id = self._serialize(data, fmt)
48 | if resource_id not in self._resources:
49 | self._resources[resource_id] = self._provider.create(
50 | content=content,
51 | extension=fmt,
52 | headers={"Access-Control-Allow-Origin": "*"},
53 | )
54 | return {"url": self._resources[resource_id].url}
55 |
56 |
57 | class AltairDataServerProxied(AltairDataServer):
58 | def __call__(
59 | self,
60 | data: pd.DataFrame,
61 | fmt: str = "json",
62 | port: Optional[int] = None,
63 | urlpath: str = "..",
64 | ) -> Dict[str, str]:
65 | result = super().__call__(data, fmt=fmt, port=port)
66 |
67 | url_parts = parse.urlparse(result["url"])
68 | urlpath = urlpath.rstrip("/")
69 | # vega defaults to /files, redirect it to /proxy//
70 | result["url"] = f"{urlpath}/proxy/{url_parts.port}{url_parts.path}"
71 |
72 | return result
73 |
74 |
75 | # Singleton instances
76 | data_server = AltairDataServer()
77 | data_server_proxied = AltairDataServerProxied()
78 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | import re
4 |
5 | try:
6 | from setuptools import setup
7 | except ImportError:
8 | from distutils.core import setup
9 |
10 | # ==============================================================================
11 | # Utilities
12 | # ==============================================================================
13 |
14 |
15 | def read(path, encoding="utf-8"):
16 | path = os.path.join(os.path.dirname(__file__), path)
17 | with io.open(path, encoding=encoding) as fp:
18 | return fp.read()
19 |
20 |
21 | def get_install_requirements(path):
22 | content = read(path)
23 | return [req for req in content.split("\n") if req != "" and not req.startswith("#")]
24 |
25 |
26 | def version(path):
27 | """Obtain the packge version from a python file e.g. pkg/__init__.py
28 |
29 | See .
30 | """
31 | version_file = read(path)
32 | version_match = re.search(
33 | r"""^__version__ = ['"]([^'"]*)['"]""", version_file, re.M
34 | )
35 | if version_match:
36 | return version_match.group(1)
37 | raise RuntimeError("Unable to find version string.")
38 |
39 |
40 | HERE = os.path.abspath(os.path.dirname(__file__))
41 |
42 |
43 | # From https://github.com/jupyterlab/jupyterlab/blob/master/setupbase.py, BSD licensed
44 | def find_packages(top=HERE):
45 | """
46 | Find all of the packages.
47 | """
48 | packages = []
49 | for d, dirs, _ in os.walk(top, followlinks=True):
50 | if os.path.exists(os.path.join(d, "__init__.py")):
51 | packages.append(os.path.relpath(d, top).replace(os.path.sep, "."))
52 | elif d != top:
53 | # Do not look for packages in subfolders if current is not a package
54 | dirs[:] = []
55 | return packages
56 |
57 |
58 | setup(
59 | name="altair_data_server",
60 | version=version("altair_data_server/__init__.py"),
61 | description="A background data server for Altair charts.",
62 | long_description=read("README.md"),
63 | long_description_content_type="text/markdown",
64 | author="Jake VanderPlas",
65 | author_email="jakevdp@google.com",
66 | url="http://github.com/altair-viz/altair_data_server/",
67 | download_url="http://github.com/altair-viz/altair_data_server/",
68 | license="BSD 3-clause",
69 | packages=find_packages(),
70 | include_package_data=True,
71 | install_requires=get_install_requirements("requirements.txt"),
72 | python_requires=">=3.6",
73 | entry_points={
74 | "altair.vegalite.v2.data_transformer": [
75 | "data_server=altair_data_server:data_server",
76 | "data_server_proxied=altair_data_server:data_server_proxied",
77 | ],
78 | "altair.vegalite.v3.data_transformer": [
79 | "data_server=altair_data_server:data_server",
80 | "data_server_proxied=altair_data_server:data_server_proxied",
81 | ],
82 | "altair.vegalite.v4.data_transformer": [
83 | "data_server=altair_data_server:data_server",
84 | "data_server_proxied=altair_data_server:data_server_proxied",
85 | ],
86 | },
87 | classifiers=[
88 | "Development Status :: 5 - Production/Stable",
89 | "Environment :: Console",
90 | "Intended Audience :: Science/Research",
91 | "License :: OSI Approved :: BSD License",
92 | "Natural Language :: English",
93 | "Programming Language :: Python :: 3.6",
94 | "Programming Language :: Python :: 3.7",
95 | "Programming Language :: Python :: 3.8",
96 | ],
97 | )
98 |
--------------------------------------------------------------------------------
/altair_data_server/tests/test_provider.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from typing import Iterator
3 |
4 | import pytest
5 | from tornado.httpclient import HTTPClient, HTTPClientError
6 | import tornado.web
7 |
8 | from altair_data_server import Provider, Resource
9 |
10 |
11 | class RootHandler(tornado.web.RequestHandler):
12 | content: bytes = b"root content"
13 |
14 | def get(self) -> None:
15 | self.write(self.content)
16 |
17 |
18 | class ProviderSubclass(Provider):
19 | """Test class for Provider subclassing"""
20 |
21 | def _handlers(self) -> list:
22 | handlers = super()._handlers()
23 | return [("/", RootHandler)] + handlers
24 |
25 |
26 | @pytest.fixture
27 | def http_client() -> HTTPClient:
28 | return HTTPClient()
29 |
30 |
31 | @pytest.fixture(scope="module")
32 | def provider() -> Iterator[Provider]:
33 | provider = Provider()
34 | yield provider
35 | provider.stop()
36 |
37 |
38 | @pytest.fixture(scope="module")
39 | def provider_subclass() -> Iterator[Provider]:
40 | provider = ProviderSubclass().start()
41 | yield provider
42 | provider.stop()
43 |
44 |
45 | def test_content_resource(provider: Provider, http_client: HTTPClient) -> None:
46 | content = "testing content resource"
47 | resource = provider.create(content=content, extension="txt")
48 | assert isinstance(resource, Resource)
49 | assert resource.url.endswith("txt")
50 | assert http_client.fetch(resource.url).body.decode() == content
51 |
52 |
53 | def test_content_default_url(provider: Provider) -> None:
54 | content = "testing default url"
55 | resource1 = provider.create(content=content, extension="txt")
56 | resource2 = provider.create(content=content, extension="txt")
57 | path = resource1.url.split("/")[-1]
58 | assert path.endswith(".txt")
59 | assert len(path) > 4
60 | assert resource1.url == resource2.url
61 |
62 |
63 | @pytest.mark.parametrize("route", ["/content", "hello_world.txt", ""])
64 | def test_content_route(provider: Provider, http_client: HTTPClient, route: str) -> None:
65 | content = f"testing route {route!r}"
66 | resource = provider.create(content=content, route=route)
67 | assert resource.url.split("/")[-1] == route.lstrip("/")
68 | assert http_client.fetch(resource.url).body == content.encode()
69 |
70 |
71 | def test_handler_resource(provider: Provider, http_client: HTTPClient) -> None:
72 | class Handler:
73 | def __init__(self) -> None:
74 | self.count = 0
75 |
76 | def __call__(self) -> str:
77 | self.count += 1
78 | return f"Testing handler resource {self.count}\n"
79 |
80 | resource = provider.create(handler=Handler(), extension="txt")
81 | assert isinstance(resource, Resource)
82 | for i in range(1, 3):
83 | assert (
84 | http_client.fetch(resource.url).body.decode()
85 | == f"Testing handler resource {i}\n"
86 | )
87 |
88 |
89 | def test_file_resource(provider: Provider, http_client: HTTPClient) -> None:
90 | content = b"file content"
91 | with tempfile.NamedTemporaryFile(suffix=".txt") as f:
92 | f.write(content)
93 | f.flush()
94 |
95 | resource = provider.create(filepath=f.name)
96 | assert isinstance(resource, Resource)
97 | assert http_client.fetch(resource.url).body == content
98 |
99 |
100 | def test_provider_subclass(
101 | provider_subclass: Provider, http_client: HTTPClient
102 | ) -> None:
103 | url = provider_subclass.url
104 | content = http_client.fetch(url).body
105 | assert content == RootHandler.content
106 |
107 |
108 | def test_expected_404(provider: Provider, http_client: HTTPClient) -> None:
109 | resource = provider.create(content="some new content")
110 | url = resource.url + ".html"
111 | with pytest.raises(HTTPClientError) as err:
112 | http_client.fetch(url)
113 | assert err.value.code == 404
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Altair data server
2 |
3 | **This repo is no longer maintained.**
4 | In Altair 5,
5 | the recommended way to work with large datasets is via Vegafusion.
6 | The documentation contains a section of [how to use the Vegafusion renderer in Altair](https://altair-viz.github.io/user_guide/large_datasets.html#vegafusion-data-transformer)
7 | and if you need additional performance you can [use the Vegafusion widget renderer](https://vegafusion.io/widget_renderer.html) instead.
8 |
9 | ---
10 |
11 | [](https://travis-ci.org/altair-viz/altair_data_server)
12 | [](https://github.com/altair-viz/altair_data_server/actions?query=workflow%3Abuild)
13 | [](https://github.com/altair-viz/altair_data_server/actions?query=workflow%3Alint)
14 | [](https://github.com/psf/black)
15 | [](https://mybinder.org/v2/gh/altair-viz/altair_data_server/master?urlpath=lab/tree/AltairDataServer.ipynb)
16 | [](https://colab.research.google.com/github/altair-viz/altair_data_server/blob/master/AltairDataServer.ipynb)
17 |
18 |
19 | This is a data transformer plugin for [Altair](http://altair-viz.github.io)
20 | that transparently serves data for Altair charts via a background WSGI server.
21 |
22 | Note that charts will only render as long as your Python session is active.
23 |
24 | The data server is a good option when you'll be **generating multiple charts as
25 | part of an exploration of data**.
26 |
27 | ## Usage
28 |
29 | First install the package and its dependencies:
30 |
31 | ```
32 | $ pip install altair_data_server
33 | ```
34 |
35 | Next import altair and enable the data server:
36 |
37 | ```python
38 | import altair as alt
39 | alt.data_transformers.enable('data_server')
40 | ```
41 | Now when you create an Altair chart, the data will be served in the background
42 | rather than embedded in the chart specification.
43 |
44 | Once you are finished with exploration and want to generate charts that
45 | will have their data fully embedded in the notebook, you can restore the
46 | default data transformer:
47 |
48 | ```python
49 | alt.data_transformers.enable('default')
50 | ```
51 |
52 | and carry on from there.
53 |
54 | ## Remote Systems
55 | Remotely-hosted notebooks (like JupyterHub or Binder) usually do not allow the end
56 | user to access arbitrary ports. To enable users to work on that setup, make sure
57 | [jupyter-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy) is
58 | installed on the jupyter server, and use the proxied data server transformer:
59 |
60 | ```python
61 | alt.data_transformers.enable('data_server_proxied')
62 | ```
63 |
64 | The `urlpath` parameter allows you to override the prefix of the proxy URL. By
65 | default, it's set to `..`, which is currently the only way to make it work for
66 | arbitrary users when running inside the classic notebook on Binder. If you
67 | intend your notebooks to be run on Binder but inside JupyterLab, change it to
68 | `.` instead, which will work provided JupyterLab is in the [default
69 | workspace](https://jupyterlab.readthedocs.io/en/stable/user/urls.html#managing-workspaces-ui).
70 |
71 | ```python
72 | # for notebooks intended for JupyterLab on Binder
73 | alt.data_transformers.enable('data_server_proxied', urlpath='.')
74 | ```
75 |
76 | On a custom JupyterHub instance, a much more robust option is to take advantage
77 | of JupyterHub's [`/user-redirect`](https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#user-redirect)
78 | feature (which is not available on Binder):
79 |
80 | ```python
81 | # this will work for any JupyterHub user, whether they're using the classic
82 | # notebook, JupyterLab in the default workspace, or JupyterLab in a named
83 | # workspace
84 | alt.data_transformers.enable('data_server_proxied', urlpath='/user-redirect')
85 | ```
86 |
87 | If your JupyterHub lives somewhere else than at your server's root, add the
88 | appropriate prefix to `urlpath`.
89 |
90 | ## Example
91 |
92 | [](https://mybinder.org/v2/gh/altair-viz/altair_data_server/master?urlpath=lab/tree/AltairDataServer.ipynb)
93 | [](https://colab.research.google.com/github/altair-viz/altair_data_server/blob/master/AltairDataServer.ipynb)
94 |
95 | You can see this in action, as well as read some of the motivation for this
96 | plugin, in the example notebook: [AltairDataServer.ipynb](AltairDataServer.ipynb).
97 | Click the Binder or Colab links above to try it out in your browser.
98 |
99 | ## Known Issues
100 |
101 | Because [jupyter-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy)
102 | requires at least Python 3.5, the methods described in
103 | [Remote Systems](#remote-systems) do not work do not work for older versions of Python.
104 |
--------------------------------------------------------------------------------
/altair_data_server/_background_server.py:
--------------------------------------------------------------------------------
1 | # Note: the code in this file is adapted from source at
2 | # https://github.com/googlecolab/colabtools/blob/master/google/colab/html/_background_server.py
3 | # The following is its original license:
4 |
5 | # Copyright 2018 Google LLC
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | """WSGI server utilities to run in thread. WSGI chosen for easier interop."""
19 |
20 | import threading
21 |
22 | import portpicker
23 | import tornado
24 | import tornado.web
25 | import tornado.ioloop
26 | import tornado.httpserver
27 | from typing import Callable, Optional, Tuple, TypeVar
28 |
29 |
30 | def _build_server(
31 | started: threading.Event,
32 | stopped: threading.Event,
33 | ioloop: tornado.ioloop.IOLoop,
34 | app: tornado.web.Application,
35 | port: int,
36 | timeout: int,
37 | ) -> Tuple[tornado.httpserver.HTTPServer, Callable[[], None]]:
38 | """Closure to build the server function to be passed to the thread.
39 |
40 | Args:
41 | started: Threading event to notify when started.
42 | ioloop: IOLoop
43 | port: Port number to serve on.
44 | timeout: Http timeout in seconds.
45 | app: tornado application to serve.
46 | Returns:
47 | A function that takes a port and WSGI app and notifies
48 | about its status via the threading events provided.
49 | """
50 |
51 |
52 | T = TypeVar("T", bound="_BackgroundServer")
53 |
54 |
55 | class _BackgroundServer:
56 | """Tornado server running in a background thread."""
57 |
58 | _app: tornado.web.Application
59 | _port: Optional[int]
60 | _server_thread: Optional[threading.Thread]
61 | _ioloop: Optional[tornado.ioloop.IOLoop]
62 | _server: Optional[tornado.httpserver.HTTPServer]
63 |
64 | def __init__(self: T, app: tornado.web.Application) -> None:
65 | """Initialize the BackgroundServer.
66 |
67 | Parameters
68 | ----------
69 | app: tornado.web.Application
70 | application to run in the background thread.
71 | """
72 | self._app = app
73 | self._port = None
74 | self._server_thread = None
75 | self._ioloop = None
76 | self._server = None
77 |
78 | @property
79 | def app(self: T) -> tornado.web.Application:
80 | """Returns the app instance."""
81 | return self._app
82 |
83 | @property
84 | def port(self: T) -> int:
85 | """Returns the current port or error if the server is not started.
86 |
87 | Returns
88 | -------
89 | port: int
90 | The port being used by the server.
91 |
92 | Raises
93 | ------
94 | RuntimeError: If server has not been started yet.
95 | """
96 | if self._server_thread is None or self._port is None:
97 | raise RuntimeError("Server not running.")
98 | return self._port
99 |
100 | def stop(self: T) -> T:
101 | """Stops the server thread.
102 |
103 | If server thread is already stopped, this is a no-op.
104 |
105 | Returns
106 | -------
107 | self :
108 | Returns self for chaining.
109 | """
110 | if self._server_thread is None:
111 | return self
112 | assert self._ioloop is not None
113 | assert self._server is not None
114 |
115 | def shutdown() -> None:
116 | if self._server is not None:
117 | self._server.stop()
118 | if self._ioloop is not None:
119 | self._ioloop.stop()
120 |
121 | try:
122 | self._ioloop.add_callback(shutdown)
123 | self._server_thread.join()
124 | self._ioloop.close(all_fds=True)
125 | finally:
126 | self._server_thread = None
127 | self._ioloop = None
128 | self._server = None
129 |
130 | return self
131 |
132 | def start(
133 | self: T, port: Optional[int] = None, timeout: int = 1, daemon: bool = True
134 | ) -> T:
135 | """Starts a server in a thread using the provided WSGI application.
136 |
137 | Will wait until the thread has started to return.
138 |
139 | Parameters
140 | ----------
141 | port: int
142 | Number of the port to use for the application, will find an open
143 | port if one is not provided.
144 | timeout: int
145 | HTTP timeout in seconds. Default = 1.
146 | daemon: bool
147 | If True (default) use a daemon thread that will automatically terminate when
148 | the main process terminates.
149 |
150 | Returns
151 | -------
152 | self :
153 | Returns self for chaining.
154 | """
155 | if self._server_thread is not None:
156 | return self
157 |
158 | self._port = port
159 |
160 | if self._port is None:
161 | self._port = portpicker.pick_unused_port()
162 |
163 | self._ioloop = tornado.ioloop.IOLoop()
164 | self._server = tornado.httpserver.HTTPServer(
165 | self._app, idle_connection_timeout=timeout, body_timeout=timeout
166 | )
167 |
168 | def start_server(
169 | ioloop: tornado.ioloop.IOLoop,
170 | httpd: tornado.httpserver.HTTPServer,
171 | port: int,
172 | ) -> None:
173 | ioloop.make_current()
174 | httpd.listen(port=port)
175 | ioloop.start()
176 |
177 | self._server_thread = threading.Thread(
178 | target=start_server,
179 | daemon=daemon,
180 | kwargs={"ioloop": self._ioloop, "httpd": self._server, "port": self._port},
181 | )
182 |
183 | started = threading.Event()
184 | self._ioloop.add_callback(started.set)
185 | self._server_thread.start()
186 | started.wait()
187 |
188 | return self
189 |
--------------------------------------------------------------------------------
/altair_data_server/_provide.py:
--------------------------------------------------------------------------------
1 | # Note: the code in this file is adapted from source at
2 | # https://github.com/googlecolab/colabtools/blob/master/google/colab/html/_background_server.py
3 | # The following is its original license:
4 |
5 | # Copyright 2018 Google LLC
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | """Helper to provide resources via the colab service worker."""
19 |
20 | import abc
21 | import collections
22 | import hashlib
23 | import mimetypes
24 | from typing import Callable, Dict, MutableMapping, Optional
25 | import uuid
26 | import weakref
27 |
28 | import tornado.web
29 | import tornado.wsgi
30 |
31 | from altair_data_server._background_server import _BackgroundServer
32 |
33 |
34 | class Resource(metaclass=abc.ABCMeta):
35 | """Abstract resource class to handle content to colab."""
36 |
37 | def __init__(
38 | self,
39 | provider: "Provider",
40 | headers: Dict[str, str],
41 | extension: Optional[str] = None,
42 | route: Optional[str] = None,
43 | ):
44 | if not isinstance(headers, collections.abc.Mapping):
45 | raise ValueError("headers must be a dict")
46 | if route is not None and extension is not None:
47 | raise ValueError("Should only provide one of route or extension.")
48 | self.headers = headers
49 | if route is None:
50 | route = str(uuid.uuid4())
51 | if extension:
52 | route += "." + extension
53 | self._guid = route.lstrip("/")
54 | self._provider = provider
55 |
56 | @abc.abstractmethod
57 | def get(self, handler: tornado.web.RequestHandler) -> None:
58 | """Gets the resource using the tornado handler passed in.
59 |
60 | Args:
61 | handler: Tornado handler to be used.
62 | """
63 | for key, value in self.headers.items():
64 | handler.set_header(key, value)
65 |
66 | @property
67 | def guid(self) -> str:
68 | """Unique id used to serve and reference the resource."""
69 | return self._guid
70 |
71 | @property
72 | def url(self) -> str:
73 | """Url to fetch the resource at."""
74 | return f"{self._provider.url}/{self._guid}"
75 |
76 |
77 | class _ContentResource(Resource):
78 | """Content Resource"""
79 |
80 | def __init__(
81 | self,
82 | content: str,
83 | provider: "Provider",
84 | headers: Dict[str, str],
85 | extension: Optional[str] = None,
86 | route: Optional[str] = None,
87 | ):
88 | self.content = content
89 | if route is None:
90 | route = hashlib.md5(self.content.encode()).hexdigest()
91 | if extension is not None:
92 | route += "." + extension
93 | extension = None
94 | super().__init__(
95 | provider=provider, headers=headers, extension=extension, route=route
96 | )
97 |
98 | def get(self, handler: tornado.web.RequestHandler) -> None:
99 | super().get(handler)
100 | handler.write(self.content)
101 |
102 |
103 | class _FileResource(Resource):
104 | """File Resource"""
105 |
106 | def __init__(
107 | self,
108 | filepath: str,
109 | provider: "Provider",
110 | headers: Dict[str, str],
111 | extension: Optional[str] = None,
112 | route: Optional[str] = None,
113 | ):
114 | self.filepath = filepath
115 | super().__init__(
116 | provider=provider, headers=headers, extension=extension, route=route
117 | )
118 |
119 | def get(self, handler: tornado.web.RequestHandler) -> None:
120 | super().get(handler)
121 | with open(self.filepath) as f:
122 | data = f.read()
123 | handler.write(data)
124 |
125 |
126 | class _HandlerResource(Resource):
127 | """Handler Resource"""
128 |
129 | def __init__(
130 | self,
131 | func: Callable[[], str],
132 | provider: "Provider",
133 | headers: Dict[str, str],
134 | extension: Optional[str] = None,
135 | route: Optional[str] = None,
136 | ):
137 | self.func = func
138 | super().__init__(
139 | provider=provider, headers=headers, extension=extension, route=route
140 | )
141 |
142 | def get(self, handler: tornado.web.RequestHandler) -> None:
143 | super().get(handler)
144 | content = self.func()
145 | handler.write(content)
146 |
147 |
148 | class ResourceHandler(tornado.web.RequestHandler):
149 | """Serves the `Resource` objects."""
150 |
151 | def initialize(self, resources: Dict[str, Resource]) -> None:
152 | self.resources = resources
153 |
154 | def get(self) -> None:
155 | path = self.request.path
156 | resource = self.resources.get(path.lstrip("/"))
157 | if not resource:
158 | raise tornado.web.HTTPError(404)
159 | content_type, _ = mimetypes.guess_type(path)
160 | if content_type:
161 | self.set_header("Content-Type", content_type)
162 | resource.get(self)
163 |
164 |
165 | class Provider(_BackgroundServer):
166 | """Background server which can provide a set of resources."""
167 |
168 | _resources: MutableMapping[str, Resource]
169 |
170 | def __init__(self) -> None:
171 | """Initialize the server with a ResourceHandler script."""
172 | self._resources = weakref.WeakValueDictionary()
173 | app = tornado.web.Application(self._handlers())
174 | super().__init__(app)
175 |
176 | def _handlers(self) -> list:
177 | return [(r".*", ResourceHandler, dict(resources=self._resources))]
178 |
179 | @property
180 | def url(self) -> str:
181 | return f"http://localhost:{self.port}"
182 |
183 | def create(
184 | self,
185 | content: str = "",
186 | filepath: str = "",
187 | handler: Optional[Callable[[], str]] = None,
188 | headers: Optional[Dict[str, str]] = None,
189 | extension: Optional[str] = None,
190 | route: Optional[str] = None,
191 | ) -> Resource:
192 | """Creates and provides a new resource to be served.
193 |
194 | Can only provide one of content, path, or handler.
195 |
196 | Args:
197 | content: The string or byte content to return.
198 | filepath: The filepath to a file whose contents should be returned.
199 | handler: A function which will be executed and returned on each request.
200 | resource: A custom resource instance.
201 | headers: A dict of header values to return.
202 | extension: Optional extension to add to the url.
203 | route: Optional route to serve on.
204 | Returns:
205 | The the `Resource` object which will be served and will provide its url.
206 | Raises:
207 | ValueError: If you don't provide one of content, filepath, or handler.
208 | """
209 | sources = sum(map(bool, (content, filepath, handler)))
210 | if sources != 1:
211 | raise ValueError(
212 | "Must provide exactly one of content, filepath, or handler"
213 | )
214 |
215 | headers = headers or {}
216 | resource: Resource
217 |
218 | if content:
219 | resource = _ContentResource(
220 | content,
221 | headers=headers,
222 | extension=extension,
223 | provider=self,
224 | route=route,
225 | )
226 | elif filepath:
227 | resource = _FileResource(
228 | filepath,
229 | headers=headers,
230 | extension=extension,
231 | provider=self,
232 | route=route,
233 | )
234 | elif handler:
235 | resource = _HandlerResource(
236 | handler,
237 | headers=headers,
238 | extension=extension,
239 | provider=self,
240 | route=route,
241 | )
242 | else:
243 | raise ValueError("Must provide one of content, filepath, or handler.")
244 |
245 | self._resources[resource.guid] = resource
246 | self.start()
247 | return resource
248 |
--------------------------------------------------------------------------------
/AltairDataServer.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "colab_type": "text",
7 | "id": "zLWJIthRefRt"
8 | },
9 | "source": [
10 | "# Altair Data Server\n",
11 | "\n",
12 | "This notebook shows an example of using the [Altair data server](https://github.com/altair-viz/altair_data_server), a lightweight plugin for [Altair](http://altair-viz.github.io) that lets you efficiently and transparently work with larger datasets.\n",
13 | "\n",
14 | "Altair data server can be installed with pip:"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": 1,
20 | "metadata": {
21 | "colab": {
22 | "base_uri": "https://localhost:8080/",
23 | "height": 394
24 | },
25 | "colab_type": "code",
26 | "id": "A1uXsLU5fJnq",
27 | "outputId": "a447c3f8-48f0-4be8-daf1-c4d66ea00208"
28 | },
29 | "outputs": [],
30 | "source": [
31 | "!pip install altair_data_server"
32 | ]
33 | },
34 | {
35 | "cell_type": "markdown",
36 | "metadata": {
37 | "colab_type": "text",
38 | "id": "JZQsUiLgfNwM"
39 | },
40 | "source": [
41 | "## Motivation\n",
42 | "\n",
43 | "Altair charts are built on [vega-lite](http://vega.github.io/vega-lite), a visualization grammar that encodes charts in JSON before rendering them in your browser with Javascript.\n",
44 | "\n",
45 | "For example, consider the following chart:"
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": 2,
51 | "metadata": {
52 | "colab": {
53 | "base_uri": "https://localhost:8080/",
54 | "height": 171
55 | },
56 | "colab_type": "code",
57 | "id": "z4hH7BTsjuDX",
58 | "outputId": "701f7198-378f-4983-fef3-4e1658b72853"
59 | },
60 | "outputs": [
61 | {
62 | "data": {
63 | "application/vnd.vegalite.v2+json": {
64 | "$schema": "https://vega.github.io/schema/vega-lite/v2.6.0.json",
65 | "config": {
66 | "view": {
67 | "height": 300,
68 | "width": 400
69 | }
70 | },
71 | "data": {
72 | "name": "data-ce24079b8164f6736574dbd5cd2f7d05"
73 | },
74 | "datasets": {
75 | "data-ce24079b8164f6736574dbd5cd2f7d05": [
76 | {
77 | "category": "A",
78 | "value": 2
79 | },
80 | {
81 | "category": "B",
82 | "value": 6
83 | },
84 | {
85 | "category": "C",
86 | "value": 4
87 | },
88 | {
89 | "category": "D",
90 | "value": 7
91 | },
92 | {
93 | "category": "E",
94 | "value": 6
95 | }
96 | ]
97 | },
98 | "encoding": {
99 | "x": {
100 | "field": "value",
101 | "type": "quantitative"
102 | },
103 | "y": {
104 | "field": "category",
105 | "type": "nominal"
106 | }
107 | },
108 | "mark": "bar"
109 | },
110 | "image/png": "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",
111 | "text/plain": [
112 | "\n",
113 | "\n",
114 | "If you see this message, it means the renderer has not been properly enabled\n",
115 | "for the frontend that you are using. For more information, see\n",
116 | "https://altair-viz.github.io/user_guide/troubleshooting.html\n"
117 | ]
118 | },
119 | "execution_count": 2,
120 | "metadata": {},
121 | "output_type": "execute_result"
122 | }
123 | ],
124 | "source": [
125 | "import pandas as pd\n",
126 | "import numpy as np\n",
127 | "import altair as alt\n",
128 | "\n",
129 | "data = pd.DataFrame({\n",
130 | " 'value': [2, 6, 4, 7, 6],\n",
131 | " 'category': list('ABCDE'),\n",
132 | "})\n",
133 | "\n",
134 | "chart = alt.Chart(data).mark_bar().encode(\n",
135 | " x='value',\n",
136 | " y='category'\n",
137 | ")\n",
138 | "\n",
139 | "chart"
140 | ]
141 | },
142 | {
143 | "cell_type": "markdown",
144 | "metadata": {
145 | "colab_type": "text",
146 | "id": "jJ5TXmMpkIuu"
147 | },
148 | "source": [
149 | "The chart itself, including the data, is encoded to a JSON specification that you can inspect:"
150 | ]
151 | },
152 | {
153 | "cell_type": "code",
154 | "execution_count": 3,
155 | "metadata": {
156 | "colab": {
157 | "base_uri": "https://localhost:8080/",
158 | "height": 816
159 | },
160 | "colab_type": "code",
161 | "id": "VcslPA_AkX8Y",
162 | "outputId": "fe9da48b-49fa-421f-8f5a-6966560b8afa"
163 | },
164 | "outputs": [
165 | {
166 | "name": "stdout",
167 | "output_type": "stream",
168 | "text": [
169 | "{\n",
170 | " \"$schema\": \"https://vega.github.io/schema/vega-lite/v2.6.0.json\",\n",
171 | " \"config\": {\n",
172 | " \"view\": {\n",
173 | " \"height\": 300,\n",
174 | " \"width\": 400\n",
175 | " }\n",
176 | " },\n",
177 | " \"data\": {\n",
178 | " \"name\": \"data-ce24079b8164f6736574dbd5cd2f7d05\"\n",
179 | " },\n",
180 | " \"datasets\": {\n",
181 | " \"data-ce24079b8164f6736574dbd5cd2f7d05\": [\n",
182 | " {\n",
183 | " \"category\": \"A\",\n",
184 | " \"value\": 2\n",
185 | " },\n",
186 | " {\n",
187 | " \"category\": \"B\",\n",
188 | " \"value\": 6\n",
189 | " },\n",
190 | " {\n",
191 | " \"category\": \"C\",\n",
192 | " \"value\": 4\n",
193 | " },\n",
194 | " {\n",
195 | " \"category\": \"D\",\n",
196 | " \"value\": 7\n",
197 | " },\n",
198 | " {\n",
199 | " \"category\": \"E\",\n",
200 | " \"value\": 6\n",
201 | " }\n",
202 | " ]\n",
203 | " },\n",
204 | " \"encoding\": {\n",
205 | " \"x\": {\n",
206 | " \"field\": \"value\",\n",
207 | " \"type\": \"quantitative\"\n",
208 | " },\n",
209 | " \"y\": {\n",
210 | " \"field\": \"category\",\n",
211 | " \"type\": \"nominal\"\n",
212 | " }\n",
213 | " },\n",
214 | " \"mark\": \"bar\"\n",
215 | "}\n"
216 | ]
217 | }
218 | ],
219 | "source": [
220 | "print(chart.to_json())"
221 | ]
222 | },
223 | {
224 | "cell_type": "markdown",
225 | "metadata": {
226 | "colab_type": "text",
227 | "id": "1Rd9e1jHkZBm"
228 | },
229 | "source": [
230 | "Notice that the data is encoded in the chart specification itself: this is very convenient because it results in a single, well-defined specification that contains **everything** required to recreate the chart.\n",
231 | "\n",
232 | "However, this leads to issues for larger datasets. For example:"
233 | ]
234 | },
235 | {
236 | "cell_type": "code",
237 | "execution_count": 4,
238 | "metadata": {
239 | "colab": {
240 | "base_uri": "https://localhost:8080/",
241 | "height": 34
242 | },
243 | "colab_type": "code",
244 | "id": "QmYK5_8xlDNW",
245 | "outputId": "b5f64ac9-8b81-4a08-fcfc-84cf0ad7c193"
246 | },
247 | "outputs": [
248 | {
249 | "name": "stdout",
250 | "output_type": "stream",
251 | "text": [
252 | "Size of chart spec: 500.7 KB\n"
253 | ]
254 | }
255 | ],
256 | "source": [
257 | "df = pd.DataFrame({\n",
258 | " 'timepoint': np.arange(5000),\n",
259 | " 'value': np.random.randn(5000),\n",
260 | " 'label': np.random.choice(list('ABCDE'), 5000)\n",
261 | "})\n",
262 | "\n",
263 | "chart = alt.Chart(df).mark_line().encode(\n",
264 | " x='timepoint',\n",
265 | " y='value',\n",
266 | " color='label'\n",
267 | ")\n",
268 | "\n",
269 | "\n",
270 | "def print_size_of(chart):\n",
271 | " spec = chart.to_json()\n",
272 | " print(f\"Size of chart spec: {len(spec) / 1024:.1f} KB\")\n",
273 | " \n",
274 | "print_size_of(chart)"
275 | ]
276 | },
277 | {
278 | "cell_type": "markdown",
279 | "metadata": {
280 | "colab_type": "text",
281 | "id": "anh5j_S0mZoo"
282 | },
283 | "source": [
284 | "If we had rendered this chart, it would have resulted in about half a megabyte of JSON text being embedded into the notebook. If your notebook contains many charts, this can quickly lead to large and unwieldy notebooks, and in the worst cases to crashing the browser.\n",
285 | "\n",
286 | "For this reason, Altair builds in a protection that prevents you from embedding extremely large data. Here's what happens when you use a dataset with a large number of rows:"
287 | ]
288 | },
289 | {
290 | "cell_type": "code",
291 | "execution_count": 5,
292 | "metadata": {
293 | "colab": {
294 | "base_uri": "https://localhost:8080/",
295 | "height": 2360
296 | },
297 | "colab_type": "code",
298 | "id": "dMa55WLmn2m2",
299 | "outputId": "35d3c60b-ad3d-4abb-c913-68071687ee66"
300 | },
301 | "outputs": [
302 | {
303 | "ename": "MaxRowsError",
304 | "evalue": "The number of rows in your dataset is greater than the maximum allowed (5000). For information on how to plot larger datasets in Altair, see the documentation",
305 | "output_type": "error",
306 | "traceback": [
307 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
308 | "\u001b[0;31mMaxRowsError\u001b[0m Traceback (most recent call last)",
309 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/altair/vegalite/v2/api.py\u001b[0m in \u001b[0;36mto_dict\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 374\u001b[0m \u001b[0mcopy\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 375\u001b[0m \u001b[0moriginal_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgetattr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcopy\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'data'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mUndefined\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 376\u001b[0;31m \u001b[0mcopy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_prepare_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moriginal_data\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 377\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 378\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0moriginal_data\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mUndefined\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
310 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/altair/vegalite/v2/api.py\u001b[0m in \u001b[0;36m_prepare_data\u001b[0;34m(data, context)\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 64\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpipe\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata_transformers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 65\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mdata_transformers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconsolidate_datasets\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;34m'values'\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 66\u001b[0m \u001b[0mname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_dataset_name\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
311 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/toolz/functoolz.py\u001b[0m in \u001b[0;36mpipe\u001b[0;34m(data, *funcs)\u001b[0m\n\u001b[1;32m 550\u001b[0m \"\"\"\n\u001b[1;32m 551\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfunc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfuncs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 552\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 553\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 554\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
312 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/toolz/functoolz.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 281\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 282\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 283\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_partial\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 284\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mTypeError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 285\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_should_curry\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
313 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/altair/vegalite/data.py\u001b[0m in \u001b[0;36mdefault_data_transformer\u001b[0;34m(data, max_rows)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0mcurry\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mdefault_data_transformer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmax_rows\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m5000\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 12\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mpipe\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlimit_rows\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmax_rows\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_rows\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mto_values\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 13\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
314 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/toolz/functoolz.py\u001b[0m in \u001b[0;36mpipe\u001b[0;34m(data, *funcs)\u001b[0m\n\u001b[1;32m 550\u001b[0m \"\"\"\n\u001b[1;32m 551\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfunc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfuncs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 552\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 553\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 554\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
315 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/toolz/functoolz.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 281\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 282\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 283\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_partial\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 284\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mTypeError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 285\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_should_curry\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
316 | "\u001b[0;32m~/anaconda/lib/python3.6/site-packages/altair/utils/data.py\u001b[0m in \u001b[0;36mlimit_rows\u001b[0;34m(data, max_rows)\u001b[0m\n\u001b[1;32m 70\u001b[0m \u001b[0;34m'than the maximum allowed ({0}). '\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 71\u001b[0m \u001b[0;34m'For information on how to plot larger datasets '\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 72\u001b[0;31m 'in Altair, see the documentation'.format(max_rows))\n\u001b[0m\u001b[1;32m 73\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 74\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
317 | "\u001b[0;31mMaxRowsError\u001b[0m: The number of rows in your dataset is greater than the maximum allowed (5000). For information on how to plot larger datasets in Altair, see the documentation"
318 | ]
319 | },
320 | {
321 | "data": {
322 | "text/plain": [
323 | "Chart({\n",
324 | " data: x y\n",
325 | " 0 0 -0.187207\n",
326 | " 1 1 0.236761\n",
327 | " 2 2 0.204160\n",
328 | " 3 3 1.510038\n",
329 | " 4 4 0.855619\n",
330 | " 5 5 -1.630006\n",
331 | " 6 6 -1.017096\n",
332 | " 7 7 -1.100416\n",
333 | " 8 8 -2.332064\n",
334 | " 9 9 0.979970\n",
335 | " 10 10 1.274392\n",
336 | " 11 11 -0.500785\n",
337 | " 12 12 -0.567306\n",
338 | " 13 13 -1.946447\n",
339 | " 14 14 -0.984827\n",
340 | " 15 15 -0.045963\n",
341 | " 16 16 2.503872\n",
342 | " 17 17 0.481599\n",
343 | " 18 18 0.872812\n",
344 | " 19 19 -0.280674\n",
345 | " 20 20 0.587096\n",
346 | " 21 21 0.345636\n",
347 | " 22 22 0.226660\n",
348 | " 23 23 -0.103801\n",
349 | " 24 24 -1.070477\n",
350 | " 25 25 -0.391096\n",
351 | " 26 26 1.324717\n",
352 | " 27 27 -2.045824\n",
353 | " 28 28 -0.858693\n",
354 | " 29 29 -0.024313\n",
355 | " ... ... ...\n",
356 | " 49970 49970 1.778256\n",
357 | " 49971 49971 0.334451\n",
358 | " 49972 49972 -0.788223\n",
359 | " 49973 49973 -1.012857\n",
360 | " 49974 49974 -0.019542\n",
361 | " 49975 49975 -1.143520\n",
362 | " 49976 49976 1.499920\n",
363 | " 49977 49977 -1.715173\n",
364 | " 49978 49978 -1.476321\n",
365 | " 49979 49979 1.200555\n",
366 | " 49980 49980 -0.738952\n",
367 | " 49981 49981 -0.228170\n",
368 | " 49982 49982 -0.456527\n",
369 | " 49983 49983 1.236492\n",
370 | " 49984 49984 0.503881\n",
371 | " 49985 49985 -0.342733\n",
372 | " 49986 49986 -0.164370\n",
373 | " 49987 49987 0.217805\n",
374 | " 49988 49988 1.980753\n",
375 | " 49989 49989 1.574927\n",
376 | " 49990 49990 1.191852\n",
377 | " 49991 49991 -0.462079\n",
378 | " 49992 49992 1.255304\n",
379 | " 49993 49993 0.671050\n",
380 | " 49994 49994 1.563930\n",
381 | " 49995 49995 1.986160\n",
382 | " 49996 49996 -0.630699\n",
383 | " 49997 49997 0.455879\n",
384 | " 49998 49998 -2.018491\n",
385 | " 49999 49999 1.606584\n",
386 | " \n",
387 | " [50000 rows x 2 columns],\n",
388 | " encoding: EncodingWithFacet({\n",
389 | " x: X({\n",
390 | " shorthand: 'x'\n",
391 | " }),\n",
392 | " y: Y({\n",
393 | " shorthand: 'y'\n",
394 | " })\n",
395 | " }),\n",
396 | " mark: 'line'\n",
397 | "})"
398 | ]
399 | },
400 | "metadata": {},
401 | "output_type": "display_data"
402 | }
403 | ],
404 | "source": [
405 | "df = pd.DataFrame({\n",
406 | " 'x': np.arange(50000),\n",
407 | " 'y': np.random.randn(50000),\n",
408 | "})\n",
409 | "\n",
410 | "big_chart = alt.Chart(df).mark_line().encode(\n",
411 | " x='x',\n",
412 | " y='y'\n",
413 | ")\n",
414 | "\n",
415 | "big_chart.display()"
416 | ]
417 | },
418 | {
419 | "cell_type": "markdown",
420 | "metadata": {
421 | "colab_type": "text",
422 | "id": "XSjoiii9w-N2"
423 | },
424 | "source": [
425 | "We can print the size of the chart by temporarily disabling the maximum rows check:"
426 | ]
427 | },
428 | {
429 | "cell_type": "code",
430 | "execution_count": 6,
431 | "metadata": {
432 | "colab": {
433 | "base_uri": "https://localhost:8080/",
434 | "height": 34
435 | },
436 | "colab_type": "code",
437 | "id": "dyGpfv-ewWmD",
438 | "outputId": "6fbca078-5529-4405-e72c-754522b8e811"
439 | },
440 | "outputs": [
441 | {
442 | "name": "stdout",
443 | "output_type": "stream",
444 | "text": [
445 | "Size of chart spec: 3389.5 KB\n"
446 | ]
447 | }
448 | ],
449 | "source": [
450 | "with alt.data_transformers.enable(max_rows=None):\n",
451 | " print_size_of(big_chart)"
452 | ]
453 | },
454 | {
455 | "cell_type": "markdown",
456 | "metadata": {
457 | "colab_type": "text",
458 | "id": "QfiXTsM6n8hp"
459 | },
460 | "source": [
461 | "Had Altair displayed this, it would have added 3MB of JSON text to the notebook, and if we created multiple charts, it would be another 3MB each time. This can quickly add-up in the context of interactive data exploration.\n",
462 | "\n",
463 | "The way to get around this is to put the data somewhere that is not in the notebook itself, but is visible to the renderer running in the notebook. Altair has some [existing approaches](https://altair-viz.github.io/user_guide/faq.html#maxrowserror-how-can-i-plot-large-datasets) that work by saving the data to disk, but this is not always desirable, and doesn't always work in cloud-based Jupyter frontends."
464 | ]
465 | },
466 | {
467 | "cell_type": "markdown",
468 | "metadata": {
469 | "colab_type": "text",
470 | "id": "lXB0KYBGoek9"
471 | },
472 | "source": [
473 | "## A Solution: Altair Data Server\n",
474 | "\n",
475 | "The [Altair data server](https://github.com/altair-viz/altair_data_server) plugin provides a nice solution to this. Rather than embedding the data in the notebook or saving the data to disk, when enabled it starts a background server, serves the data, and inserts the appropriate URL into the altair chart:"
476 | ]
477 | },
478 | {
479 | "cell_type": "code",
480 | "execution_count": 7,
481 | "metadata": {
482 | "colab": {
483 | "base_uri": "https://localhost:8080/",
484 | "height": 348
485 | },
486 | "colab_type": "code",
487 | "id": "1t8btXFXoiBu",
488 | "outputId": "f72a561f-f2a1-4371-a517-7a6546a2a915"
489 | },
490 | "outputs": [
491 | {
492 | "name": "stdout",
493 | "output_type": "stream",
494 | "text": [
495 | "Size of chart spec: 0.4 KB\n"
496 | ]
497 | },
498 | {
499 | "data": {
500 | "application/vnd.vegalite.v2+json": {
501 | "$schema": "https://vega.github.io/schema/vega-lite/v2.6.0.json",
502 | "config": {
503 | "view": {
504 | "height": 300,
505 | "width": 400
506 | }
507 | },
508 | "data": {
509 | "format": {
510 | "type": "json"
511 | },
512 | "url": "http://localhost:21953/ce767407-e3f3-4d15-a806-7b05a362ddb5.json"
513 | },
514 | "encoding": {
515 | "x": {
516 | "field": "x",
517 | "type": "quantitative"
518 | },
519 | "y": {
520 | "field": "y",
521 | "type": "quantitative"
522 | }
523 | },
524 | "mark": "line"
525 | },
526 | "image/png": "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",
527 | "text/plain": [
528 | "\n",
529 | "\n",
530 | "If you see this message, it means the renderer has not been properly enabled\n",
531 | "for the frontend that you are using. For more information, see\n",
532 | "https://altair-viz.github.io/user_guide/troubleshooting.html\n"
533 | ]
534 | },
535 | "metadata": {},
536 | "output_type": "display_data"
537 | }
538 | ],
539 | "source": [
540 | "alt.data_transformers.enable('data_server')\n",
541 | "print_size_of(big_chart)\n",
542 | "big_chart.display()"
543 | ]
544 | },
545 | {
546 | "cell_type": "markdown",
547 | "metadata": {},
548 | "source": [
549 | "*Note: If you are running on a cloud-based hosted notebook like MyBinder, you will have to modify the above slightly, and instead run*\n",
550 | "```python\n",
551 | "alt.data_transformers.enable('data_server_proxied')\n",
552 | "```"
553 | ]
554 | },
555 | {
556 | "cell_type": "markdown",
557 | "metadata": {
558 | "colab_type": "text",
559 | "id": "MnmkhYaHpzsn"
560 | },
561 | "source": [
562 | "The resulting spec is only 0.4KB, which is small enough that embedding it in the notebook doesn't cause problems. It's instructive to look at the spec directly:"
563 | ]
564 | },
565 | {
566 | "cell_type": "code",
567 | "execution_count": 8,
568 | "metadata": {
569 | "colab": {
570 | "base_uri": "https://localhost:8080/",
571 | "height": 408
572 | },
573 | "colab_type": "code",
574 | "id": "OQSCXfEzo_fk",
575 | "outputId": "4124afd2-8a44-4541-ce6a-894a2300d717"
576 | },
577 | "outputs": [
578 | {
579 | "name": "stdout",
580 | "output_type": "stream",
581 | "text": [
582 | "{\n",
583 | " \"$schema\": \"https://vega.github.io/schema/vega-lite/v2.6.0.json\",\n",
584 | " \"config\": {\n",
585 | " \"view\": {\n",
586 | " \"height\": 300,\n",
587 | " \"width\": 400\n",
588 | " }\n",
589 | " },\n",
590 | " \"data\": {\n",
591 | " \"url\": \"http://localhost:21953/ce767407-e3f3-4d15-a806-7b05a362ddb5.json\"\n",
592 | " },\n",
593 | " \"encoding\": {\n",
594 | " \"x\": {\n",
595 | " \"field\": \"x\",\n",
596 | " \"type\": \"quantitative\"\n",
597 | " },\n",
598 | " \"y\": {\n",
599 | " \"field\": \"y\",\n",
600 | " \"type\": \"quantitative\"\n",
601 | " }\n",
602 | " },\n",
603 | " \"mark\": \"line\"\n",
604 | "}\n"
605 | ]
606 | }
607 | ],
608 | "source": [
609 | "print(big_chart.to_json())"
610 | ]
611 | },
612 | {
613 | "cell_type": "markdown",
614 | "metadata": {
615 | "colab_type": "text",
616 | "id": "Cc2Z_xEmuw6L"
617 | },
618 | "source": [
619 | "What the data transformer has done is to replace the embedded data with a URL reference, and to make that data available at that URL. We can see this by accessing the URL directly:"
620 | ]
621 | },
622 | {
623 | "cell_type": "code",
624 | "execution_count": 9,
625 | "metadata": {
626 | "colab": {
627 | "base_uri": "https://localhost:8080/",
628 | "height": 34
629 | },
630 | "colab_type": "code",
631 | "id": "QgAYaF0Ju4Y-",
632 | "outputId": "b9b01893-fef8-4896-cda2-42475046660e"
633 | },
634 | "outputs": [
635 | {
636 | "name": "stdout",
637 | "output_type": "stream",
638 | "text": [
639 | "http://localhost:21953/ce767407-e3f3-4d15-a806-7b05a362ddb5.json\n"
640 | ]
641 | }
642 | ],
643 | "source": [
644 | "url = big_chart.to_dict()['data']['url']\n",
645 | "print(url)"
646 | ]
647 | },
648 | {
649 | "cell_type": "markdown",
650 | "metadata": {},
651 | "source": [
652 | "We can load the data from the backend using Pandas"
653 | ]
654 | },
655 | {
656 | "cell_type": "code",
657 | "execution_count": 10,
658 | "metadata": {
659 | "colab": {
660 | "base_uri": "https://localhost:8080/",
661 | "height": 204
662 | },
663 | "colab_type": "code",
664 | "id": "glrsH04Gpmwa",
665 | "outputId": "e85cdf09-99db-4129-e360-762f44d3646e"
666 | },
667 | "outputs": [
668 | {
669 | "data": {
670 | "text/html": [
671 | "\n",
672 | "\n",
685 | "
\n",
686 | " \n",
687 | " \n",
688 | " | \n",
689 | " x | \n",
690 | " y | \n",
691 | "
\n",
692 | " \n",
693 | " \n",
694 | " \n",
695 | " | 0 | \n",
696 | " 0 | \n",
697 | " -0.187207 | \n",
698 | "
\n",
699 | " \n",
700 | " | 1 | \n",
701 | " 1 | \n",
702 | " 0.236761 | \n",
703 | "
\n",
704 | " \n",
705 | " | 2 | \n",
706 | " 2 | \n",
707 | " 0.204160 | \n",
708 | "
\n",
709 | " \n",
710 | " | 3 | \n",
711 | " 3 | \n",
712 | " 1.510038 | \n",
713 | "
\n",
714 | " \n",
715 | " | 4 | \n",
716 | " 4 | \n",
717 | " 0.855619 | \n",
718 | "
\n",
719 | " \n",
720 | "
\n",
721 | "
"
722 | ],
723 | "text/plain": [
724 | " x y\n",
725 | "0 0 -0.187207\n",
726 | "1 1 0.236761\n",
727 | "2 2 0.204160\n",
728 | "3 3 1.510038\n",
729 | "4 4 0.855619"
730 | ]
731 | },
732 | "execution_count": 10,
733 | "metadata": {},
734 | "output_type": "execute_result"
735 | }
736 | ],
737 | "source": [
738 | "if not url.startswith('http://localhost'):\n",
739 | " # Using proxied URL; reconstruct the host url\n",
740 | " *proxy, port, filename = url.split('/')\n",
741 | " url = f\"http://localhost:{port}/{filename}\"\n",
742 | " \n",
743 | "served_data = pd.read_json(url)\n",
744 | "served_data.head()"
745 | ]
746 | },
747 | {
748 | "cell_type": "markdown",
749 | "metadata": {
750 | "colab_type": "text",
751 | "id": "c1AY-0hxpyQ3"
752 | },
753 | "source": [
754 | "## When to use the data server\n",
755 | "\n",
756 | "There is one distinct disadvantage of using the data server approach: your charts will only render as long as your Python session is active.\n",
757 | "\n",
758 | "So the data server is a good option when you'll be **working interactively, generating multiple charts as part of an exploration of data**.\n",
759 | "\n",
760 | "But once you are finished with exploration and want to generate charts that will be fully embedded in the notebook, you can restore the default data transformer:\n",
761 | "```python\n",
762 | "alt.data_transformers.enable('default')\n",
763 | "```\n",
764 | "and carry on from there."
765 | ]
766 | }
767 | ],
768 | "metadata": {
769 | "colab": {
770 | "collapsed_sections": [],
771 | "name": "Altair Data Server.ipynb",
772 | "provenance": [],
773 | "version": "0.3.2"
774 | },
775 | "kernelspec": {
776 | "display_name": "Python 3",
777 | "language": "python",
778 | "name": "python3"
779 | },
780 | "language_info": {
781 | "codemirror_mode": {
782 | "name": "ipython",
783 | "version": 3
784 | },
785 | "file_extension": ".py",
786 | "mimetype": "text/x-python",
787 | "name": "python",
788 | "nbconvert_exporter": "python",
789 | "pygments_lexer": "ipython3",
790 | "version": "3.6.7"
791 | }
792 | },
793 | "nbformat": 4,
794 | "nbformat_minor": 4
795 | }
796 |
--------------------------------------------------------------------------------