diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 779d6ee74..a9ec5263c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.5', '3.6', '3.7', '3.8' ] + python-version: [ '3.6', '3.7', '3.8' ] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v1 @@ -24,7 +24,7 @@ jobs: run: | python -m pip install --upgrade pip pip install .[dev] - pip install selenium + pip install altair_saver - name: Test with pytest run: | pytest --doctest-modules altair diff --git a/altair/sphinxext/altairgallery.py b/altair/sphinxext/altairgallery.py index d747f7061..22431898c 100644 --- a/altair/sphinxext/altairgallery.py +++ b/altair/sphinxext/altairgallery.py @@ -132,7 +132,7 @@ def save_example_pngs(examples, image_dir, make_thumbnails=True): chart.save(image_file) hashes[filename] = example_hash except ImportError: - warnings.warn("Could not import selenium: using generic image") + warnings.warn("Unable to save image: using generic image") create_generic_image(image_file) with open(hash_file, 'w') as f: diff --git a/altair/utils/headless.py b/altair/utils/headless.py deleted file mode 100644 index d5f5672f4..000000000 --- a/altair/utils/headless.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Utilities that use selenium + chrome headless to save figures -""" - -import contextlib -import os -import tempfile - - -@contextlib.contextmanager -def temporary_filename(**kwargs): - """Create and clean-up a temporary file - - Arguments are the same as those passed to tempfile.mkstemp - - We could use tempfile.NamedTemporaryFile here, but that causes issues on - windows (see https://bugs.python.org/issue14243). - """ - filedescriptor, filename = tempfile.mkstemp(**kwargs) - os.close(filedescriptor) - - try: - yield filename - finally: - if os.path.exists(filename): - os.remove(filename) - - -HTML_TEMPLATE = """ - - - - Embedding Vega-Lite - - - - - -
- - -""" - -EXTRACT_CODE = { -'png': """ - var spec = arguments[0]; - var mode = arguments[1]; - var scaleFactor = arguments[2]; - var done = arguments[3]; - - if(mode === 'vega-lite'){ - // compile vega-lite to vega - vegaLite = (typeof vegaLite === "undefined") ? vl : vegaLite; - const compiled = vegaLite.compile(spec); - spec = compiled.spec; - } - - new vega.View(vega.parse(spec), { - loader: vega.loader(), - logLevel: vega.Warn, - renderer: 'none', - }) - .initialize() - .toCanvas(scaleFactor) - .then(function(canvas){return canvas.toDataURL('image/png');}) - .then(done) - .catch(function(err) { console.error(err); }); - """, -'svg': """ - var spec = arguments[0]; - var mode = arguments[1]; - var scaleFactor = arguments[2]; - var done = arguments[3]; - - if(mode === 'vega-lite'){ - // compile vega-lite to vega - vegaLite = (typeof vegaLite === "undefined") ? vl : vegaLite; - const compiled = vegaLite.compile(spec); - spec = compiled.spec; - } - - new vega.View(vega.parse(spec), { - loader: vega.loader(), - logLevel: vega.Warn, - renderer: 'none', - }) - .initialize() - .toSVG(scaleFactor) - .then(done) - .catch(function(err) { console.error(err); }); - """, -'vega': """ - var spec = arguments[0]; - var mode = arguments[1]; - var done = arguments[3]; - - if(mode === 'vega-lite'){ - // compile vega-lite to vega - vegaLite = (typeof vegaLite === "undefined") ? vl : vegaLite; - const compiled = vegaLite.compile(spec); - spec = compiled.spec; - } - - done(spec); - """} - - -def compile_spec(spec, format, mode, - vega_version, vegaembed_version, vegalite_version, - scale_factor=1, driver_timeout=20, webdriver='chrome'): - # TODO: detect & use local Jupyter caches of JS packages? - - # selenium is an optional dependency, so import it here - try: - import selenium.webdriver - except ImportError: - raise ImportError("selenium package is required " - "for saving chart as {}".format(format)) - - if format not in ['png', 'svg', 'vega']: - raise NotImplementedError("format must be 'svg', 'png' or 'vega'") - - if mode not in ['vega', 'vega-lite']: - raise ValueError("mode must be either 'vega' or 'vega-lite'") - - if vega_version is None: - raise ValueError("must specify vega_version") - - if vegaembed_version is None: - raise ValueError("must specify vegaembed_version") - - if mode == 'vega-lite' and vegalite_version is None: - raise ValueError("must specify vega-lite version") - - if webdriver == 'chrome': - webdriver_class = selenium.webdriver.Chrome - webdriver_options_class = selenium.webdriver.chrome.options.Options - elif webdriver == 'firefox': - webdriver_class = selenium.webdriver.Firefox - webdriver_options_class = selenium.webdriver.firefox.options.Options - else: - raise ValueError("webdriver must be 'chrome' or 'firefox'") - - html = HTML_TEMPLATE.format(vega_version=vega_version, - vegalite_version=vegalite_version, - vegaembed_version=vegaembed_version) - - webdriver_options = webdriver_options_class() - webdriver_options.add_argument("--headless") - - if issubclass(webdriver_class, selenium.webdriver.Chrome): - # for linux/osx root user, need to add --no-sandbox option. - # since geteuid doesn't exist on windows, we don't check it - if hasattr(os, 'geteuid') and (os.geteuid() == 0): - webdriver_options.add_argument('--no-sandbox') - - driver = webdriver_class(options=webdriver_options) - - try: - driver.set_page_load_timeout(driver_timeout) - - with temporary_filename(suffix='.html') as htmlfile: - with open(htmlfile, 'w') as f: - f.write(html) - driver.get("file://" + htmlfile) - online = driver.execute_script("return navigator.onLine") - if not online: - raise ValueError("Internet connection required for saving " - "chart as {}".format(format)) - return driver.execute_async_script(EXTRACT_CODE[format], - spec, mode, scale_factor) - finally: - driver.close() diff --git a/altair/utils/mimebundle.py b/altair/utils/mimebundle.py index d5f8fc07b..6a9f3af2f 100644 --- a/altair/utils/mimebundle.py +++ b/altair/utils/mimebundle.py @@ -1,6 +1,3 @@ -import base64 - -from .headless import compile_spec from .html import spec_to_html @@ -12,13 +9,13 @@ def spec_to_mimebundle(spec, format, mode=None, """Convert a vega/vega-lite specification to a mimebundle The mimebundle type is controlled by the ``format`` argument, which can be - one of the following ['png', 'svg', 'vega', 'vega-lite', 'html', 'json'] + one of the following ['html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite'] Parameters ---------- spec : dict a dictionary representing a vega-lite plot spec - format : string {'png', 'svg', 'vega', 'vega-lite', 'html', 'json'} + format : string {'html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite'} the file format to be saved. mode : string {'vega', 'vega-lite'} The rendering mode. @@ -38,9 +35,8 @@ def spec_to_mimebundle(spec, format, mode=None, Note ---- - The png, svg, and vega outputs require the pillow and selenium Python modules - to be installed. Additionally they requires either chromedriver - (if webdriver=='chrome') or geckodriver (if webdriver=='firefox') + The png, svg, pdf, and vega outputs require the altair_saver package + to be installed. """ if mode not in ['vega', 'vega-lite']: raise ValueError("mode must be either 'vega' or 'vega-lite'") @@ -49,34 +45,29 @@ def spec_to_mimebundle(spec, format, mode=None, if vega_version is None: raise ValueError("Must specify vega_version") return {'application/vnd.vega.v{}+json'.format(vega_version[0]): spec} - elif format in ['png', 'svg', 'vega']: - render = compile_spec(spec, format=format, mode=mode, - vega_version=vega_version, - vegaembed_version=vegaembed_version, - vegalite_version=vegalite_version, **kwargs) - if format == 'png': - render = base64.b64decode(render.split(',', 1)[1].encode()) - return {'image/png': render} - elif format == 'svg': - return {'image/svg+xml': render} - elif format == 'vega': - assert mode == 'vega-lite' # TODO: handle vega->vega conversion more gracefully - return {'application/vnd.vega.v{}+json'.format(vega_version[0]): render} - elif format == 'html': + if format in ['png', 'svg', 'pdf', 'vega']: + try: + import altair_saver + except ImportError: + raise ValueError( + "Saving charts in {fmt!r} format requires the altair_saver package: " + "see http://github.com/altair-viz/altair_saver/".format(fmt=format) + ) + return altair_saver.render(spec, format, mode=mode, **kwargs) + if format == 'html': html = spec_to_html(spec, mode=mode, vega_version=vega_version, vegaembed_version=vegaembed_version, vegalite_version=vegalite_version, **kwargs) return {'text/html': html} - elif format == 'vega-lite': + if format == 'vega-lite': assert mode == 'vega-lite' # sanity check: should never be False if mode == 'vega': raise ValueError("Cannot convert a vega spec to vegalite") if vegalite_version is None: raise ValueError("Must specify vegalite_version") return {'application/vnd.vegalite.v{}+json'.format(vegalite_version[0]): spec} - elif format == 'json': + if format == 'json': return {'application/json': spec} - else: - raise ValueError("format must be one of " - "['png', 'svg', 'vega', 'vega-lite', 'html', 'json']") + raise ValueError("format must be one of " + "['html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite']") diff --git a/altair/utils/tests/test_mimebundle.py b/altair/utils/tests/test_mimebundle.py index 8d7279936..452f333be 100644 --- a/altair/utils/tests/test_mimebundle.py +++ b/altair/utils/tests/test_mimebundle.py @@ -1,195 +1,192 @@ import pytest -import json - -try: - import selenium -except ImportError: - selenium = None +import altair as alt from ..mimebundle import spec_to_mimebundle -# example from https://vega.github.io/editor/#/examples/vega-lite/bar -VEGALITE_SPEC = json.loads( - """ - { - "$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"} - } - } - """ -) +def require_altair_saver(func): + try: + import altair_saver # noqa: F401 + except ImportError: + return pytest.mark.skip("altair_saver not importable; cannot run saver tests")(func) + else: + return func -VEGA_SPEC = json.loads( - """ - { - "$schema": "https://vega.github.io/schema/vega/v3.0.json", - "description": "A simple bar chart with embedded data.", - "autosize": "pad", - "padding": 5, - "height": 200, - "style": "cell", - "data": [ - { - "name": "source_0", - "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} - ] + +@pytest.fixture +def vegalite_spec(): + return { + "$schema": "https://vega.github.io/schema/vega-lite/v4.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}, + ] }, - { - "name": "data_0", - "source": "source_0", - "transform": [ - {"type": "formula", "expr": "toNumber(datum[\\"b\\"])", "as": "b"}, - { - "type": "filter", - "expr": "datum[\\"b\\"] !== null && !isNaN(datum[\\"b\\"])" - } - ] - } - ], - "signals": [ - {"name": "x_step", "value": 21}, - { - "name": "width", - "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step" - } - ], - "marks": [ - { - "name": "marks", - "type": "rect", - "style": ["bar"], - "from": {"data": "data_0"}, - "encode": { - "update": { - "fill": {"value": "#4c78a8"}, - "x": {"scale": "x", "field": "a"}, - "width": {"scale": "x", "band": true}, - "y": {"scale": "y", "field": "b"}, - "y2": {"scale": "y", "value": 0} - } - } - } - ], - "scales": [ - { - "name": "x", - "type": "band", - "domain": {"data": "data_0", "field": "a", "sort": true}, - "range": {"step": {"signal": "x_step"}}, - "paddingInner": 0.1, - "paddingOuter": 0.05 + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "ordinal"}, + "y": {"field": "b", "type": "quantitative"}, }, - { - "name": "y", - "type": "linear", - "domain": {"data": "data_0", "field": "b"}, - "range": [{"signal": "height"}, 0], - "nice": true, - "zero": true - } - ], - "axes": [ - { - "scale": "x", - "orient": "bottom", - "title": "a", - "labelOverlap": true, - "encode": { - "labels": { - "update": { - "angle": {"value": 270}, - "align": {"value": "right"}, - "baseline": {"value": "middle"} - } + } + + +@pytest.fixture +def vega_spec(): + return { + "$schema": "https://vega.github.io/schema/vega/v5.json", + "axes": [ + { + "domain": False, + "grid": True, + "gridScale": "x", + "labels": False, + "maxExtent": 0, + "minExtent": 0, + "orient": "left", + "scale": "y", + "tickCount": {"signal": "ceil(height/40)"}, + "ticks": False, + "zindex": 0, + }, + { + "grid": False, + "labelAlign": "right", + "labelAngle": 270, + "labelBaseline": "middle", + "labelOverlap": True, + "orient": "bottom", + "scale": "x", + "title": "a", + "zindex": 0, + }, + { + "grid": False, + "labelOverlap": True, + "orient": "left", + "scale": "y", + "tickCount": {"signal": "ceil(height/40)"}, + "title": "b", + "zindex": 0, + }, + ], + "background": "white", + "data": [ + { + "name": "source_0", + "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}, + ], + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "expr": 'isValid(datum["b"]) && isFinite(+datum["b"])', + "type": "filter", + } + ], + }, + ], + "description": "A simple bar chart with embedded data.", + "height": 200, + "marks": [ + { + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "width": {"band": True, "scale": "x"}, + "x": {"field": "a", "scale": "x"}, + "y": {"field": "b", "scale": "y"}, + "y2": {"scale": "y", "value": 0}, + } + }, + "from": {"data": "data_0"}, + "name": "marks", + "style": ["bar"], + "type": "rect", } - }, - "zindex": 1 - }, - { - "scale": "y", - "orient": "left", - "title": "b", - "labelOverlap": true, - "tickCount": {"signal": "ceil(height/40)"}, - "zindex": 1 - }, - { - "scale": "y", - "orient": "left", - "grid": true, - "tickCount": {"signal": "ceil(height/40)"}, - "gridScale": "x", - "domain": false, - "labels": false, - "maxExtent": 0, - "minExtent": 0, - "ticks": false, - "zindex": 0 - } - ], - "config": {"axisY": {"minExtent": 30}} + ], + "padding": 5, + "scales": [ + { + "domain": {"data": "data_0", "field": "a", "sort": True}, + "name": "x", + "paddingInner": 0.1, + "paddingOuter": 0.05, + "range": {"step": {"signal": "x_step"}}, + "type": "band", + }, + { + "domain": {"data": "data_0", "field": "b"}, + "name": "y", + "nice": True, + "range": [{"signal": "height"}, 0], + "type": "linear", + "zero": True, + }, + ], + "signals": [ + {"name": "x_step", "value": 20}, + { + "name": "width", + "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step", + }, + ], + "style": "cell", } - """ -) -VEGAEMBED_VERSION = '3.14.0' -VEGALITE_VERSION = '2.3.1' -VEGA_VERSION = '3.3.1' -@pytest.mark.skipif('not selenium') -def test_spec_to_vega_mimebundle(): - try: - bundle = spec_to_mimebundle( - spec=VEGALITE_SPEC, - format='vega', - mode='vega-lite', - vega_version=VEGA_VERSION, - vegalite_version=VEGALITE_VERSION, - vegaembed_version=VEGAEMBED_VERSION - ) - except ValueError as err: - if str(err).startswith('Internet connection'): - pytest.skip("web connection required for png/svg export") - else: - raise - assert bundle == {'application/vnd.vega.v3+json': VEGA_SPEC} +@require_altair_saver +def test_vegalite_to_vega_mimebundle(vegalite_spec, vega_spec): + bundle = spec_to_mimebundle( + spec=vegalite_spec, + format="vega", + mode="vega-lite", + vega_version=alt.VEGA_VERSION, + vegalite_version=alt.VEGALITE_VERSION, + vegaembed_version=alt.VEGAEMBED_VERSION, + ) + assert bundle == {"application/vnd.vega.v5+json": vega_spec} -def test_spec_to_vegalite_mimebundle(): +def test_spec_to_vegalite_mimebundle(vegalite_spec): bundle = spec_to_mimebundle( - spec=VEGALITE_SPEC, - mode='vega-lite', - format='vega-lite', - vegalite_version=VEGALITE_VERSION + spec=vegalite_spec, + mode="vega-lite", + format="vega-lite", + vegalite_version=alt.VEGALITE_VERSION, ) - assert bundle == {'application/vnd.vegalite.v2+json': VEGALITE_SPEC} + assert bundle == {"application/vnd.vegalite.v4+json": vegalite_spec} -def test_spec_to_json_mimebundle(): +def test_spec_to_vega_mimebundle(vega_spec): bundle = spec_to_mimebundle( - spec=VEGALITE_SPEC, - mode='vega-lite', - format='json', + spec=vega_spec, + mode="vega", + format="vega", + vega_version=alt.VEGA_VERSION ) - assert bundle == {'application/json': VEGALITE_SPEC} + assert bundle == {"application/vnd.vega.v5+json": vega_spec} + + +def test_spec_to_json_mimebundle(): + bundle = spec_to_mimebundle(spec=vegalite_spec, mode="vega-lite", format="json",) + assert bundle == {"application/json": vegalite_spec} diff --git a/altair/vegalite/v3/tests/test_api.py b/altair/vegalite/v3/tests/test_api.py index 40b72a0c0..55d26036c 100644 --- a/altair/vegalite/v3/tests/test_api.py +++ b/altair/vegalite/v3/tests/test_api.py @@ -14,9 +14,9 @@ from altair.utils import AltairDeprecationWarning try: - import selenium + import altair_saver # noqa: F401 except ImportError: - selenium = None + altair_saver = None def getargs(*args, **kwargs): @@ -230,44 +230,39 @@ def test_selection_expression(): @pytest.mark.parametrize('format', ['html', 'json', 'png', 'svg']) -@pytest.mark.skipif('not selenium') def test_save(format, basic_chart): - if format in ['html', 'json', 'svg']: - out = io.StringIO() - mode = 'r' - else: + if format == 'png': out = io.BytesIO() mode = 'rb' + else: + out = io.StringIO() + mode = 'r' + + if format in ['svg', 'png'] and not altair_saver: + with pytest.raises(ValueError) as err: + basic_chart.save(out, format=format) + assert "github.com/altair-viz/altair_saver" in str(err.value) + return + + basic_chart.save(out, format=format) + out.seek(0) + content = out.read() + + if format == 'json': + assert '$schema' in json.loads(content) + if format == 'html': + assert content.startswith('') fid, filename = tempfile.mkstemp(suffix='.' + format) os.close(fid) - + try: - try: - basic_chart.save(out, format=format) - basic_chart.save(filename) - except ValueError as err: - if str(err).startswith('Internet connection'): - pytest.skip("web connection required for png/svg export") - else: - raise - - out.seek(0) + basic_chart.save(filename) with open(filename, mode) as f: - assert f.read() == out.read() + assert f.read() == content finally: os.remove(filename) - out.seek(0) - - if format == 'json': - spec = json.load(out) - assert '$schema' in spec - - elif format == 'html': - content = out.read() - assert content.startswith('') - def test_facet(): # wrapped facet diff --git a/altair/vegalite/v4/tests/test_api.py b/altair/vegalite/v4/tests/test_api.py index c1ab4cc8e..5560ad5d8 100644 --- a/altair/vegalite/v4/tests/test_api.py +++ b/altair/vegalite/v4/tests/test_api.py @@ -13,9 +13,9 @@ import altair.vegalite.v4 as alt try: - import selenium + import altair_saver # noqa: F401 except ImportError: - selenium = None + altair_saver = None def getargs(*args, **kwargs): @@ -229,44 +229,39 @@ def test_selection_expression(): @pytest.mark.parametrize('format', ['html', 'json', 'png', 'svg']) -@pytest.mark.skipif('not selenium') def test_save(format, basic_chart): - if format in ['html', 'json', 'svg']: - out = io.StringIO() - mode = 'r' - else: + if format == 'png': out = io.BytesIO() mode = 'rb' + else: + out = io.StringIO() + mode = 'r' + + if format in ['svg', 'png'] and not altair_saver: + with pytest.raises(ValueError) as err: + basic_chart.save(out, format=format) + assert "github.com/altair-viz/altair_saver" in str(err.value) + return + + basic_chart.save(out, format=format) + out.seek(0) + content = out.read() + + if format == 'json': + assert '$schema' in json.loads(content) + if format == 'html': + assert content.startswith('') fid, filename = tempfile.mkstemp(suffix='.' + format) os.close(fid) try: - try: - basic_chart.save(out, format=format) - basic_chart.save(filename) - except ValueError as err: - if str(err).startswith('Internet connection'): - pytest.skip("web connection required for png/svg export") - else: - raise - - out.seek(0) + basic_chart.save(filename) with open(filename, mode) as f: - assert f.read() == out.read() + assert f.read() == content finally: os.remove(filename) - out.seek(0) - - if format == 'json': - spec = json.load(out) - assert '$schema' in spec - - elif format == 'html': - content = out.read() - assert content.startswith('') - def test_facet(): # wrapped facet diff --git a/doc/user_guide/saving_charts.rst b/doc/user_guide/saving_charts.rst index 5e63f3965..3bfcb5e8f 100644 --- a/doc/user_guide/saving_charts.rst +++ b/doc/user_guide/saving_charts.rst @@ -70,8 +70,8 @@ This JSON can then be inserted into any web page using the vegaEmbed_ library. HTML format ~~~~~~~~~~~ -If you wish for Altair to take care of the embedding for you, you can save a -file using +If you wish for Altair to take care of the HTML embedding for you, you can +save a chart directly to an HTML file using .. code-block:: python @@ -131,7 +131,7 @@ javascript-enabled web browser: You can view the result here: `chart.html `_. -By default ``canvas`` is used for rendering the visualization in vegaEmbed. To +By default, ``canvas`` is used for rendering the visualization in vegaEmbed. To change to ``svg`` rendering, use the ``embed_options`` as such: .. code-block:: python @@ -141,54 +141,35 @@ change to ``svg`` rendering, use the ``embed_options`` as such: .. note:: - This is not the same as ``alt.renderers.enable('svg')``, what renders a - static ``svg`` image. + This is not the same as ``alt.renderers.enable('svg')``, what renders the + chart as a static ``svg`` image within a Jupyter notebook. .. _saving-png: -PNG and SVG format -~~~~~~~~~~~~~~~~~~ -To save an Altair chart object as a PNG or SVG image, you can use +PNG, SVG, and PDF format +~~~~~~~~~~~~~~~~~~~~~~~~ +To save an Altair chart object as a PNG, SVG, or PDF image, you can use .. code-block:: python chart.save('chart.png') chart.save('chart.svg') + chart.save('chart.pdf') -However, saving these images requires some additional dependencies to run the +However, saving these images requires some additional extensions to run the javascript code necessary to interpret the Vega-Lite specification and output it in the form of an image. -Altair is set up to do this conversion using selenium and headless Chrome or -Firefox, which requires the following: +Altair can do this via the altair_saver_ package, which can be installed with:: -- the Selenium_ python package. This can be installed using:: + $ conda install altair_saver - $ conda install selenium +or:: - or:: + $ pip install altair_saver - $ pip install selenium - -- a recent version of `Google Chrome`_ or `Mozilla Firefox`_. Please see the - Chrome or Firefox installation page for installation details for your own - operating system. - -- `Chrome Driver`_ or `Gecko Driver`_, which allows Chrome or Firefox - respectively to be run in a *headless* state (i.e. to execute Javascript - code without opening an actual browser window). - If you use homebrew on OSX, this can be installed with:: - - $ brew cask install chromedriver - $ brew install geckodriver - - See the ``chromedriver`` or ``geckodriver`` documentation for details on - installation. - -Once those dependencies are installed, you should be able to save charts as -``png`` or ``svg``. Altair defaults to using chromedriver. If you'd like to use geckodriver:: - - chart.save('chart.png', webdriver='firefox') +See the altair_saver_ documentation for information about additional installation +requirements. Figure Size/Resolution ^^^^^^^^^^^^^^^^^^^^^^ @@ -202,9 +183,5 @@ This can be done with the ``scale_factor`` argument, which defaults to 1.0:: chart.save('chart.png', scale_factor=2.0) -.. _Selenium: http://selenium-python.readthedocs.io/ -.. _Google Chrome: https://www.google.com/chrome/ -.. _Mozilla Firefox: https://www.mozilla.org/firefox/ -.. _Chrome Driver: https://sites.google.com/a/chromium.org/chromedriver/ -.. _Gecko Driver: https://github.com/mozilla/geckodriver/releases +.. _altair_saver http://github.com/altair-viz/altair_saver/ .. _vegaEmbed: https://github.com/vega/vega-embed