diff --git a/pyscriptjs/examples/altair.html b/pyscriptjs/examples/altair.html new file mode 100644 index 0000000..16ecb40 --- /dev/null +++ b/pyscriptjs/examples/altair.html @@ -0,0 +1,60 @@ + + + Altair + + + + + + - altair + - pandas + - vega_datasets + + + +
+ +import altair as alt +from vega_datasets import data + +source = data.movies.url + +pts = alt.selection(type="single", encodings=['x']) + +rect = alt.Chart(data.movies.url).mark_rect().encode( + alt.X('IMDB_Rating:Q', bin=True), + alt.Y('Rotten_Tomatoes_Rating:Q', bin=True), + alt.Color('count()', + scale=alt.Scale(scheme='greenblue'), + legend=alt.Legend(title='Total Records') + ) +) + +circ = rect.mark_point().encode( + alt.ColorValue('grey'), + alt.Size('count()', + legend=alt.Legend(title='Records in Selection') + ) +).transform_filter( + pts +) + +bar = alt.Chart(source).mark_bar().encode( + x='Major_Genre:N', + y='count()', + color=alt.condition(pts, alt.ColorValue("steelblue"), alt.ColorValue("grey")) +).properties( + width=550, + height=200 +).add_selection(pts) + +alt.vconcat( + rect + circ, + bar +).resolve_legend( + color="independent", + size="independent" +) + + + diff --git a/pyscriptjs/examples/antigravity.html b/pyscriptjs/examples/antigravity.html new file mode 100644 index 0000000..7c2bfa4 --- /dev/null +++ b/pyscriptjs/examples/antigravity.html @@ -0,0 +1,72 @@ + + + Antigravity + + + + + + + + + + + Based on xkcd: antigravity https://xkcd.com/353/. + +
+
+
+
+
+ + +import random + +from js import document, setInterval +from pyodide import create_proxy +from pyodide.http import open_url + +class Antigravity(): + + def __init__(self, node): + self.node = node + self.xoffset, self.yoffset = 0, 0 + setInterval(create_proxy(self.move), 10) + + def move(self): + char = document.getElementById('python') + console.log(char) + char.setAttribute('transform', f'translate({self.xoffset}, {-self.yoffset})') + self.xoffset += random.normalvariate(0, 1)/20 + if self.yoffset < 50: + self.yoffset += 0.1 + else: + self.yoffset += random.normalvariate(0, 1)/20 + + def _repr_svg_(self): + return open_url('./antigravity.svg').read() + +node = document.getElementById('antigravity') +node.replaceChildren() + +Antigravity(node) + + + + diff --git a/pyscriptjs/examples/antigravity.svg b/pyscriptjs/examples/antigravity.svg new file mode 100644 index 0000000..092a9e7 --- /dev/null +++ b/pyscriptjs/examples/antigravity.svg @@ -0,0 +1,72 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/pyscriptjs/examples/folium.html b/pyscriptjs/examples/folium.html new file mode 100644 index 0000000..5b6f90c --- /dev/null +++ b/pyscriptjs/examples/folium.html @@ -0,0 +1,49 @@ + + + Folium + + + + + + - folium + - pandas + + + +
+ +import folium +import json +import pandas as pd + +from pyodide.http import open_url + +url = ( + "https://raw.githubusercontent.com/python-visualization/folium/master/examples/data" +) +state_geo = f"{url}/us-states.json" +state_unemployment = f"{url}/US_Unemployment_Oct2012.csv" +state_data = pd.read_csv(open_url(state_unemployment)) +geo_json = json.loads(open_url(state_geo).read()) + +m = folium.Map(location=[48, -102], zoom_start=3) + +folium.Choropleth( + geo_data=geo_json, + name="choropleth", + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", + fill_color="YlGn", + fill_opacity=0.7, + line_opacity=0.2, + legend_name="Unemployment Rate (%)", +).add_to(m) + +folium.LayerControl().add_to(m) + +m + + + diff --git a/pyscriptjs/examples/matplotlib.html b/pyscriptjs/examples/matplotlib.html new file mode 100644 index 0000000..34a1186 --- /dev/null +++ b/pyscriptjs/examples/matplotlib.html @@ -0,0 +1,50 @@ + + + Matplotlib + + + + + + - matplotlib + + + +
+ +import matplotlib.pyplot as plt +import matplotlib.tri as tri +import numpy as np + +# First create the x and y coordinates of the points. +n_angles = 36 +n_radii = 8 +min_radius = 0.25 +radii = np.linspace(min_radius, 0.95, n_radii) + +angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False) +angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1) +angles[:, 1::2] += np.pi / n_angles + +x = (radii * np.cos(angles)).flatten() +y = (radii * np.sin(angles)).flatten() +z = (np.cos(radii) * np.cos(3 * angles)).flatten() + +# Create the Triangulation; no triangles so Delaunay triangulation created. +triang = tri.Triangulation(x, y) + +# Mask off unwanted triangles. +triang.set_mask(np.hypot(x[triang.triangles].mean(axis=1), + y[triang.triangles].mean(axis=1)) + < min_radius) + +fig1, ax1 = plt.subplots() +ax1.set_aspect('equal') +tpc = ax1.tripcolor(triang, z, shading='flat') +fig1.colorbar(tpc) +ax1.set_title('tripcolor of Delaunay triangulation, flat shading') + +fig1 + + + diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 3d2f2f2..8459b07 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -12,6 +12,95 @@ import io, base64, sys loop = asyncio.get_event_loop() +MIME_METHODS = { + '__repr__': 'text/plain', + '_repr_html_': 'text/html', + '_repr_markdown_': 'text/markdown', + '_repr_svg_': 'image/svg+xml', + '_repr_png_': 'image/png', + '_repr_pdf_': 'application/pdf', + '_repr_jpeg_': 'image/jpeg', + '_repr_latex': 'text/latex', + '_repr_json_': 'application/json', + '_repr_javascript_': 'application/javascript', + 'savefig': 'image/png' +} + +def render_image(mime, value, meta): + data = f'data:{mime};charset=utf-8;base64,{value}' + attrs = ' '.join(['{k}="{v}"' for k, v in meta.items()]) + return f'' + +def identity(value, meta): + return value + + +MIME_RENDERERS = { + 'text/plain': identity, + 'text/html' : identity, + 'image/png' : lambda value, meta: render_image('image/png', value, meta), + 'image/jpeg': lambda value, meta: render_image('image/jpeg', value, meta), + 'image/svg+xml': identity, + 'application/json': identity, + 'application/javascript': lambda value, meta: f'' +} + + +def eval_formatter(obj, print_method): + """ + Evaluates a formatter method. + """ + if hasattr(obj, print_method): + if print_method == 'savefig': + buf = io.BytesIO() + obj.savefig(buf, format='png') + buf.seek(0) + return base64.b64encode(buf.read()).decode('utf-8') + return getattr(obj, print_method)() + if print_method == '_repr_mimebundle_': + return {}, {} + return None + + +def format_mime(obj): + """ + Formats object using _repr_x_ methods. + """ + if isinstance(obj, str): + return obj, 'text/plain' + + mimebundle = eval_formatter(obj, '_repr_mimebundle_') + if isinstance(mimebundle, tuple): + format_dict, md_dict = mimebundle + else: + format_dict = mimebundle + md_dict = {} + + output, not_available = None, [] + for method, mime_type in reversed(MIME_METHODS.items()): + if mime_type in format_dict: + output = format_dict[mime_type] + else: + output = eval_formatter(obj, method) + + if output is None: + continue + elif mime_type not in MIME_RENDERERS: + not_available.append(mime_type) + continue + break + if output is None: + if not_available: + console.warning(f'Rendered object requested unavailable MIME renderers: {not_available}') + output = repr(output) + mime_type = 'text/plain' + elif isinstance(output, tuple): + output, meta = output + else: + meta = {} + return MIME_RENDERERS[mime_type](output, meta), mime_type + + class PyScript: loop = loop @@ -28,17 +117,13 @@ class PyScript: element_id = child.id = f"{element_id}-{exec_id}"; element.appendChild(child); - if hasattr(value, "savefig"): - console.log(f"FIGURE: {value}") - buf = io.BytesIO() - value.savefig(buf, format='png') - buf.seek(0) - img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8') - document.getElementById(element_id).innerHTML = f'
' - elif hasattr(value, "startswith") and value.startswith("data:image"): - document.getElementById(element_id).innerHTML = f'
' + element = document.getElementById(element_id) + html, mime_type = format_mime(value) + if mime_type in ('application/javascript', 'text/html'): + scriptEl = document.createRange().createContextualFragment(html) + element.appendChild(scriptEl) else: - document.getElementById(element_id).innerHTML = value; + element.innerHTML = html @staticmethod def run_until_complete(f): @@ -104,10 +189,9 @@ class Element: # Inject it into the DOM self.element.after(clone); - + return Element(clone.id, clone) - def remove_class(self, classname): if isinstance(classname, list): for cl in classname: @@ -144,7 +228,7 @@ class PyItemTemplate(Element): def __init__(self, data, labels=None, state_key=None, parent=None): self.data = data - + self.register_parent(parent) if not labels: @@ -167,7 +251,7 @@ class PyItemTemplate(Element): console.log('creating section') new_child = create('section', self._id, "task bg-white my-1") console.log('creating values') - + console.log('creating innerHtml') new_child._element.innerHTML = f"""