From 09e81578d9e4ab7bac83afcda4890df1195a7838 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2024 09:40:50 -0400 Subject: [PATCH 1/9] Update show based on simple web-server rather than altair_viewer --- altair/utils/_show.py | 75 +++++++++++++++++++++++++++++++++++++++ altair/vegalite/v5/api.py | 33 ++++++++--------- 2 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 altair/utils/_show.py diff --git a/altair/utils/_show.py b/altair/utils/_show.py new file mode 100644 index 000000000..1adf0aa08 --- /dev/null +++ b/altair/utils/_show.py @@ -0,0 +1,75 @@ +import webbrowser +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Union, Iterable, Optional + + +def open_html_in_browser( + html: Union[str, bytes], + using: Union[str, Iterable[str], None] = None, + port: Optional[int] = None, +): + """ + Display an html document in a web browser without creating a temp file. + + Instantiates a simple http server and uses the webbrowser module to + open the server's URL + + Based on + + Parameters + ---------- + html: str + HTML string to display + using: str or iterable of str + Name of the web browser to open (e.g. "chrome", "firefox", etc.). + If an iterable, choose the first browser available on the system. + If none, choose the system default browser. + port: int + Port to use. Defaults to a random port + """ + # Encode html to bytes + if isinstance(html, str): + html_bytes = html.encode("utf8") + else: + html_bytes = html + + browser = None + + if using is None: + browser = webbrowser.get(None) + else: + # normalize using to an iterable + if isinstance(using, str): + using = [using] + + for browser_key in using: + try: + browser = webbrowser.get(browser_key) + if browser is not None: + break + except webbrowser.Error: + pass + + if browser is None: + raise ValueError("Failed to locate a browser with name in " + str(using)) + + class OneShotRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + bufferSize = 1024 * 1024 + for i in range(0, len(html_bytes), bufferSize): + self.wfile.write(html_bytes[i : i + bufferSize]) + + def log_message(self, format, *args): + # Silence stderr logging + pass + + # Use specified port if provided, otherwise choose a random port (port value of 0) + server = HTTPServer( + ("127.0.0.1", port if port is not None else 0), OneShotRequestHandler + ) + browser.open("http://127.0.0.1:%s" % server.server_port) + server.handle_request() diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 4202fd9a8..43162553b 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -26,6 +26,7 @@ using_vegafusion as _using_vegafusion, compile_with_vegafusion as _compile_with_vegafusion, ) +from ...utils._show import open_html_in_browser from ...utils.core import DataFrameLike from ...utils.data import DataType @@ -2678,29 +2679,29 @@ def serve( ) def show( - self, embed_opt: Optional[dict] = None, open_browser: Optional[bool] = None + self, + embed_options: Optional[dict] = None, + using: Union[str, Iterable[str], None] = None, + port: Optional[int] = None, ) -> None: - """Show the chart in an external browser window. + """Show the chart in an external browser tab. - This requires a recent version of the altair_viewer package. + This requires the vl-convert-python package to be installed Parameters ---------- - embed_opt : dict (optional) + embed_options : dict (optional) The Vega embed options that control the display of the chart. - open_browser : bool (optional) - Specify whether a browser window should be opened. If not specified, - a browser window will be opened only if the server is not already - connected to a browser. + using: str or iterable of str + Name of the web browser to open (e.g. "chrome", "firefox", etc.). + If an iterable, choose the first browser available on the system. + If None, choose the system default browser. + port: int + Port to use. Defaults to a random port """ - try: - import altair_viewer - except ImportError as err: - raise ValueError( - "'show' method requires the altair_viewer package. " - "See http://github.com/altair-viz/altair_viewer" - ) from err - altair_viewer.show(self, embed_opt=embed_opt, open_browser=open_browser) + buffer = io.StringIO() + self.save(buffer, format="html", embed_options=embed_options, inline=True) + open_html_in_browser(buffer.getvalue(), using=using, port=port) @utils.use_signature(core.Resolve) def _set_resolve(self, **kwargs): From 3783dfca9c8fa3e714fdd12d7d231f82698c1d0d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2024 09:48:03 -0400 Subject: [PATCH 2/9] make internal --- altair/vegalite/v5/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 43162553b..c697fccc1 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -26,7 +26,7 @@ using_vegafusion as _using_vegafusion, compile_with_vegafusion as _compile_with_vegafusion, ) -from ...utils._show import open_html_in_browser +from ...utils._show import open_html_in_browser as _open_html_in_browser from ...utils.core import DataFrameLike from ...utils.data import DataType @@ -2701,7 +2701,7 @@ def show( """ buffer = io.StringIO() self.save(buffer, format="html", embed_options=embed_options, inline=True) - open_html_in_browser(buffer.getvalue(), using=using, port=port) + _open_html_in_browser(buffer.getvalue(), using=using, port=port) @utils.use_signature(core.Resolve) def _set_resolve(self, **kwargs): From b7ab561489ac0d0e22b57d9820a411c0a5c33945 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 24 Mar 2024 15:07:43 -0400 Subject: [PATCH 3/9] make internal --- altair/vegalite/v5/api.py | 33 ++++++++++----------------------- altair/vegalite/v5/display.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 647ee3fea..de242f3d9 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -2678,30 +2678,17 @@ def serve( http_server=http_server, ) - def show( - self, - embed_options: Optional[dict] = None, - using: Union[str, Iterable[str], None] = None, - port: Optional[int] = None, - ) -> None: - """Show the chart in an external browser tab. - - This requires the vl-convert-python package to be installed + def show(self) -> None: + """Display the chart using the active renderer""" + if renderers.active == "browser": + # Opens browser window as side-effect. + # We use a special case here so that IPython is not required + self._repr_mimebundle_() + else: + # Mime-bundle based renderer, requires running in an IPython session + from IPython.display import display - Parameters - ---------- - embed_options : dict (optional) - The Vega embed options that control the display of the chart. - using: str or iterable of str - Name of the web browser to open (e.g. "chrome", "firefox", etc.). - If an iterable, choose the first browser available on the system. - If None, choose the system default browser. - port: int - Port to use. Defaults to a random port - """ - buffer = io.StringIO() - self.save(buffer, format="html", embed_options=embed_options, inline=True) - _open_html_in_browser(buffer.getvalue(), using=using, port=port) + display(self) @utils.use_signature(core.Resolve) def _set_resolve(self, **kwargs): diff --git a/altair/vegalite/v5/display.py b/altair/vegalite/v5/display.py index e3733adcf..adfb16d23 100644 --- a/altair/vegalite/v5/display.py +++ b/altair/vegalite/v5/display.py @@ -106,6 +106,27 @@ def jupyter_renderer(spec: dict, **metadata): )._repr_mimebundle_() # type: ignore[attr-defined] +def browser_renderer( + spec: dict, inline=False, using=None, port=0, **metadata +) -> Dict[str, str]: + from altair.utils._show import open_html_in_browser + + if inline: + metadata["template"] = "inline" + mimebundle = spec_to_mimebundle( + spec, + format="html", + mode="vega-lite", + vega_version=VEGA_VERSION, + vegaembed_version=VEGAEMBED_VERSION, + vegalite_version=VEGALITE_VERSION, + **metadata, + ) + html = mimebundle["text/html"] + open_html_in_browser(html, using=using, port=port) + return {} + + html_renderer = HTMLRenderer( mode="vega-lite", template="universal", @@ -126,6 +147,7 @@ def jupyter_renderer(spec: dict, **metadata): renderers.register("png", png_renderer) renderers.register("svg", svg_renderer) renderers.register("jupyter", jupyter_renderer) +renderers.register("browser", browser_renderer) renderers.enable("default") From b5d0d7f9deb832ed0a1489c6110f0f0971355950 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 24 Mar 2024 15:23:34 -0400 Subject: [PATCH 4/9] lint / mypy --- altair/vegalite/v5/api.py | 1 - altair/vegalite/v5/display.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index de242f3d9..93bd295b3 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -26,7 +26,6 @@ using_vegafusion as _using_vegafusion, compile_with_vegafusion as _compile_with_vegafusion, ) -from ...utils._show import open_html_in_browser as _open_html_in_browser from ...utils.core import DataFrameLike from ...utils.data import DataType diff --git a/altair/vegalite/v5/display.py b/altair/vegalite/v5/display.py index adfb16d23..23c62781e 100644 --- a/altair/vegalite/v5/display.py +++ b/altair/vegalite/v5/display.py @@ -122,6 +122,10 @@ def browser_renderer( vegalite_version=VEGALITE_VERSION, **metadata, ) + + if isinstance(mimebundle, tuple): + mimebundle = mimebundle[0] + html = mimebundle["text/html"] open_html_in_browser(html, using=using, port=port) return {} From 7e3a4f5cb260cdc3203cc5ff32567caf92dd57b1 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2024 10:17:53 -0400 Subject: [PATCH 5/9] inline -> offline for consistency with "jupyter" renderer --- altair/vegalite/v5/display.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/altair/vegalite/v5/display.py b/altair/vegalite/v5/display.py index 23c62781e..b7901a7b7 100644 --- a/altair/vegalite/v5/display.py +++ b/altair/vegalite/v5/display.py @@ -107,11 +107,11 @@ def jupyter_renderer(spec: dict, **metadata): def browser_renderer( - spec: dict, inline=False, using=None, port=0, **metadata + spec: dict, offline=False, using=None, port=0, **metadata ) -> Dict[str, str]: from altair.utils._show import open_html_in_browser - if inline: + if offline: metadata["template"] = "inline" mimebundle = spec_to_mimebundle( spec, From 6f6b9f7a9891af70c4e28c8d3ac8636d13e8de78 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2024 11:42:27 -0400 Subject: [PATCH 6/9] Update docs --- doc/user_guide/display_frontends.rst | 107 ++++++++++++++++++--------- 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/doc/user_guide/display_frontends.rst b/doc/user_guide/display_frontends.rst index 132001a78..e44acd48e 100644 --- a/doc/user_guide/display_frontends.rst +++ b/doc/user_guide/display_frontends.rst @@ -55,6 +55,11 @@ The most used built-in renderers are: dependencies from the ``vl-convert-python`` package (rather than from an online CDN) so that an internet connection is not required. +``alt.renderers.enable("browser")`` + *(added in version 5.3):* Display charts in an external web browser. This renderer is + particularly useful when using Vega-Altair in a local non-Jupyter environment, such as + in `IPython`_ or `Spyder`_. See :ref:`display-browser` for more information. + In addition, Altair includes the following renderers: - ``"default"``, ``"colab"``, ``"kaggle"``, ``"zeppelin"``: identical to ``"html"`` @@ -179,57 +184,83 @@ If you are using a dashboarding package that is not listed here, please `open an Working in environments without a JavaScript frontend ----------------------------------------------------- The Vega-Lite specifications produced by Altair can be produced in any Python -environment, but to render these specifications currently requires a javascript +environment, but to render these specifications currently requires a JavaScript engine. For this reason, Altair works most seamlessly with the browser-based -environments mentioned above. +environments mentioned above. Even so, Altair can be used effectively in non-browser +based environments using the approaches described below. -If you would like to render plots from another Python interface that does not -have a built-in javascript engine, you'll need to somehow connect your charts -to a second tool that can execute javascript. +Static Image Renderers +~~~~~~~~~~~~~~~~~~~~~~ +The ``"png"`` and ``"svg"`` renderers rely on the JavaScript engine embedded in +the vl-convert optional dependency to generate static images from Vega-Lite chart +specifications. These static images are then displayed in IPython-based environments +using the Mime Renderer Extensions system. This approach may be used to display static +versions of Altair charts inline in the `IPython QtConsole`_ and `Spyder`_, as well as +in browser-based environments like JupyterLab. -There are a few options available for this: +The ``"svg"`` renderer is enabled like this:: -Altair Viewer -~~~~~~~~~~~~~ -.. note:: - - altair_viewer does not yet support Altair 5. + alt.renderers.enable("svg") -For non-notebook IDEs, a useful companion is the `Altair Viewer`_ package, -which provides an Altair renderer that works directly from any Python terminal. -Start by installing the package:: - $ pip install altair_viewer +The ``"png"`` renderer is enabled like this:: -When enabled, this will serve charts via a local HTTP server and automatically open -a browser window in which to view them, with subsequent charts displayed in the -same window. + alt.renderers.enable("png") -If you are using an IPython-compatible terminal ``altair_viewer`` can be enabled via -Altair's standard renderer framework:: - import altair as alt - alt.renderers.enable('altair_viewer') +The ``"png"`` renderer supports the following keyword argument configuration options: -If you prefer to manually trigger chart display, you can use the built-in :meth:`Chart.show` -method to manually trigger chart display:: +- The ``scale_factor`` argument may be used to increase the chart size by the specified + scale factor (Default 1.0). +- The ``ppi`` argument controls the pixels-per-inch resolution of the displayed image (Default 72). - import altair as alt +Example usage:: - # load a simple dataset as a pandas DataFrame - from vega_datasets import data - cars = data.cars() + alt.renderers.enable("png", scale_factor=2, ppi=144) - chart = alt.Chart(cars).mark_point().encode( - x='Horsepower', - y='Miles_per_Gallon', - color='Origin', - ).interactive() - chart.show() +.. _display-browser: + +Browser Renderer +~~~~~~~~~~~~~~~~ +To support displaying charts with interactive features in non-browser based environments, +the ``"browser"`` renderer automatically opens charts in browser tabs of a system web browser. + +The ``"browser"`` renderer is enabled like this:: + + alt.renderers.enable("browser") + + +The ``"browser"`` renderer supports the following keyword argument configuration options: + +- The ``using`` argument may be used to specify which system web browser to use. This + may be set to a string to indicate the single browser that must be used (e.g. ``"safari"``), + or it may be set to a list of browser names where the first available browser is used. See the + documentation for the `webbrowser module`_ for the list of supported browser names. If not + specified, the system default browser is used. +- The ``offline`` argument may be used to specify whether JavaScript dependencies should + be loaded from an online CDN or embedded alongside the chart specification. When ``offline`` + is ``False`` (The default), JavaScript dependencies are loaded from an online CDN, and so + an internet connection is required. When ``offline`` is ``True``, JavaScript dependencies + are embedded alongside chart specification and so no internet connection is required. Setting + ``offline`` to ``True`` requires the optional ``vl-convert-python`` dependency. +- The ``port`` argument may be used to configure the system port that the chart HTML is served + on. Defaults to a random open port. + +Limitations: + +- The ``"browser"`` renderer sets up a temporary web server that serves the chart exactly once, + then opens the designated browser pointing to this server's URL. This approach does not require + the creation of temporary HTML files on disk, and it's memory efficient as there are no long-lived + web server processes required. A limitation of this approach is that the chart will be lost if the + browser is refreshed, and it's not possible to copy the chart URL and paste it in another browser + tab. +- When used in IPython-based environments, the ``"browser"`` renderer will automatically open the + chart in the browser when the chart is the final value of the cell or command. This behavior is not + available in the standard ``python`` REPL. In this case, the ``chart.show()`` method may be used to + manually invoke the active renderer and open the chart in the browser. +- This renderer is not compatible with remote environments like Binder or Colab. -This command will block the Python interpreter until the browser window containing -the chart is closed. Manual ``save()`` and display ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -678,3 +709,7 @@ see :ref:`display-general`. .. _Vega: https://vega.github.io/vega/ .. _VSCode-Python: https://code.visualstudio.com/docs/python/python-tutorial .. _Zeppelin: https://zeppelin.apache.org/ +.. _IPython: https://ipython.org/ +.. _Spyder: https://www.spyder-ide.org/ +.. _IPython QtConsole: https://qtconsole.readthedocs.io/en/stable/ +.. _webbrowser module: https://docs.python.org/3/library/webbrowser.html#webbrowser.register From e217c725857c2567b6ceeb83aa868da37a267d35 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2024 11:49:09 -0400 Subject: [PATCH 7/9] changelog --- doc/releases/changes.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst index 32b9f25c7..c84f1c8a8 100644 --- a/doc/releases/changes.rst +++ b/doc/releases/changes.rst @@ -12,9 +12,11 @@ Enhancements ~~~~~~~~~~~~ - Add "jupyter" renderer which uses JupyterChart for rendering (#3283). See :ref:`renderers` for more information. - Add integration of VegaFusion and JupyterChart to support data transformations in the Python kernel for interactive charts (##3281). See :ref:`vegafusion-data-transformer` for more information. -- Add ``embed_options`` argument to JupyterChart to allow customization of Vega Embed options (##3304) +- Add ``embed_options`` argument to JupyterChart to allow customization of Vega Embed options (#3304) - Add offline support for JupyterChart and the new "jupyter" renderer. See :ref:`user-guide-jupyterchart-offline` for more information. +- Add ``"browser"`` renderer to support displaying Altair charts in an external web browser. + See :ref:`display-browser` for more information (#3379). - Docs: Add :ref:`section on dashboards ` which have support for Altair (#3299) - Support restrictive FIPS-compliant environment (#3291) - Support opening charts in the Vega editor with ``chart.open_editor()`` (#3358) @@ -33,6 +35,7 @@ Bug Fixes Backward-Incompatible Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Updated ``chart.show()`` method to invoke the active renderer rather than depend on ``altair_saver`` (Which was never updated for use with Altair 5) (#3379). - Changed hash function from ``md5`` to a truncated ``sha256`` non-cryptograhic hash (#3291) Version 5.2.0 (released Nov 28, 2023) From 478944342eff67dedc49fe699252d97d680e07bc Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2024 11:53:22 -0400 Subject: [PATCH 8/9] drop dangling phrase --- altair/utils/_show.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/altair/utils/_show.py b/altair/utils/_show.py index 1adf0aa08..0030570ac 100644 --- a/altair/utils/_show.py +++ b/altair/utils/_show.py @@ -14,8 +14,6 @@ def open_html_in_browser( Instantiates a simple http server and uses the webbrowser module to open the server's URL - Based on - Parameters ---------- html: str From 63f9b45ff7e7067433d775f0679e780739303166 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 29 Mar 2024 16:11:00 -0400 Subject: [PATCH 9/9] Doc refinements --- doc/user_guide/display_frontends.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/user_guide/display_frontends.rst b/doc/user_guide/display_frontends.rst index e44acd48e..813a43cc4 100644 --- a/doc/user_guide/display_frontends.rst +++ b/doc/user_guide/display_frontends.rst @@ -240,9 +240,9 @@ The ``"browser"`` renderer supports the following keyword argument configuration specified, the system default browser is used. - The ``offline`` argument may be used to specify whether JavaScript dependencies should be loaded from an online CDN or embedded alongside the chart specification. When ``offline`` - is ``False`` (The default), JavaScript dependencies are loaded from an online CDN, and so + is ``False`` (the default), JavaScript dependencies are loaded from an online CDN, and so an internet connection is required. When ``offline`` is ``True``, JavaScript dependencies - are embedded alongside chart specification and so no internet connection is required. Setting + are embedded alongside the chart specification and so no internet connection is required. Setting ``offline`` to ``True`` requires the optional ``vl-convert-python`` dependency. - The ``port`` argument may be used to configure the system port that the chart HTML is served on. Defaults to a random open port. @@ -250,7 +250,7 @@ The ``"browser"`` renderer supports the following keyword argument configuration Limitations: - The ``"browser"`` renderer sets up a temporary web server that serves the chart exactly once, - then opens the designated browser pointing to this server's URL. This approach does not require + then opens the designated browser pointing to the server's URL. This approach does not require the creation of temporary HTML files on disk, and it's memory efficient as there are no long-lived web server processes required. A limitation of this approach is that the chart will be lost if the browser is refreshed, and it's not possible to copy the chart URL and paste it in another browser @@ -259,7 +259,7 @@ Limitations: chart in the browser when the chart is the final value of the cell or command. This behavior is not available in the standard ``python`` REPL. In this case, the ``chart.show()`` method may be used to manually invoke the active renderer and open the chart in the browser. -- This renderer is not compatible with remote environments like Binder or Colab. +- This renderer is not compatible with remote environments like `Binder`_ or `Colab`_. Manual ``save()`` and display @@ -709,6 +709,7 @@ see :ref:`display-general`. .. _Vega: https://vega.github.io/vega/ .. _VSCode-Python: https://code.visualstudio.com/docs/python/python-tutorial .. _Zeppelin: https://zeppelin.apache.org/ +.. _Binder: https://mybinder.org/ .. _IPython: https://ipython.org/ .. _Spyder: https://www.spyder-ide.org/ .. _IPython QtConsole: https://qtconsole.readthedocs.io/en/stable/