From 09e81578d9e4ab7bac83afcda4890df1195a7838 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2024 09:40:50 -0400 Subject: [PATCH] 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):