coatless-quarto / pyodide

Community developed Quarto extension to enable interactive Python code cells in HTML documents using Pyodide
http://quarto.thecoatlessprofessor.com/pyodide/
69 stars 7 forks source link

Support Altair charts #7

Open joelostblom opened 8 months ago

joelostblom commented 8 months ago

Is your feature request related to a problem? Please describe.

Currently, altair charts renders as text:

image

The output from altair is vega-lite specs that can be rendered as interactive HTML. I tried some of the different renderers here https://altair-viz.github.io/user_guide/display_frontends.html, such as PNG and SVG, but none of them display.

Describe the solution you'd like Chart objects to render by default as they do in a notebook environment.

Describe alternatives you've considered Not sure what an alternative solution would be here.

Additional context

Code:

import pyodide_js
await pyodide_js.loadPackage('micropip')
import micropip
await micropip.install('altair')

import altair as alt
import pandas as pd

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

chart = alt.Chart(source).mark_bar().encode(
    x='a',
    y='b'
)

chart

Edit: Modified the MWE to include await before micropip.install().

coatless commented 8 months ago

It looks like the pyodide package for this is now being built thanks to your work in:

https://github.com/pyodide/pyodide/pull/4580

From there, we'll probably add a detect statement to run the necessary setup Python code to include the graphs similar to matplotlib.

psychemedia commented 7 months ago

Related to this is a more general issue relating to the rendering of HTML from IPython.__repr__ calls in other packages.

Eg with ipyleaflet we get a similar issue:

import micropip
await micropip.install(["ipyleaflet", "sqlite3"])
from IPython.display import display
from ipyleaflet import Map, Marker, basemaps, basemap_to_tiles
m = Map(
  basemap=basemap_to_tiles(
    basemaps.NASAGIBS.ModisTerraTrueColorCR, "2017-04-08"
  ),
  center=(52.204793, 360.121558),
  zoom=4
)
m.add_layer(Marker(location=(52.204793, 360.121558)))
display(m)

returns text.

Map(center=[52.204793, 360.121558], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text', 'zoom_out_title']), AttributionControl(options=['position', 'prefix'], position='bottomright')), crs={'name': 'EPSG3857', 'custom': False}, default_style=MapStyle(), dragging_style=MapStyle(cursor='move'), layers=(TileLayer(attribution='Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ([ESDIS](https://earthdata.nasa.gov/)) with funding provided by NASA/HQ.', base=True, max_zoom=9, min_zoom=1, name='NASAGIBS.ModisTerraTrueColorCR', options=['attribution', 'bounds', 'detect_retina', 'max_native_zoom', 'max_zoom', 'min_native_zoom', 'min_zoom', 'no_wrap', 'tile_size', 'tms', 'zoom_offset'], url='https://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/2017-04-08/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg'), Marker(location=[52.204793, 360.121558], options=['alt', 'draggable', 'keyboard', 'rise_offset', 'rise_on_hover', 'rotation_angle', 'rotation_origin', 'title', 'z_index_offset'])), options=['bounce_at_zoom_limits', 'box_zoom', 'center', 'close_popup_on_click', 'double_click_zoom', 'dragging', 'fullscreen', 'inertia', 'inertia_deceleration', 'inertia_max_speed', 'interpolation', 'keyboard', 'keyboard_pan_offset', 'keyboard_zoom_offset', 'max_zoom', 'min_zoom', 'prefer_canvas', 'scroll_wheel_zoom', 'tap', 'tap_tolerance', 'touch_zoom', 'world_copy_jump', 'zoom', 'zoom_animation_threshold', 'zoom_delta', 'zoom_snap'], style=MapStyle(), zoom=4.0)

In an IPython/Jupyter notebook, display() is available without having to explicitly import, so it may be useful to:

  1. support some sort of display() function;
  2. automatically import it.
joelostblom commented 3 months ago

Great point @psychemedia ! It would be very helpful if it was possible to support the same outputs as jupyter environments (such as_repr_html_()) since this would also give nicer output for pandas dataframes etc.

I noticed that it seems like these are already support in some cases, for example for pandas data frames where either _repr_html_() or to_html() gives the following output (which I believe would look like in jupyter if the relevant CSS styles were added):

image

Likewise I can use display_html to display arbitrary HTML code:

image

So if an object has a _repr_html_() method already, it might just be a matter of hooking into that instead of the default __repr__() (maybe a similar solution also exists for tibbles and similar in R?). However, if I try to add an onClick JS event, then nothing happens when I try to click the button (text should be displayed below as in this demo):

image

@coatless Is the reason for altair charts and similar output not displaying this inabiility to execution JS code (which in the case of altair would prevent loading Vega-Lite from the CDNs)? That could be why the png/svg renderers are not working either since they still require to connect to the VegaLite CDNs to create the charts in the first place. Do you have an idea where to get started with working on this? I'm working on a viz textbook for ggplot and altair and would love to use this extension for the interactive coding exercises in both languages.

joelostblom commented 3 months ago

I should also add that altair has a way of forcing the output to be HTML (instead of automatically converting to textual output in unsupported environments) via to_html(), but it doesn't actually render the chart (which I'm guessing is due to the reason mentioned above):

image

The content of the output div:

image