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 @@
+
+
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"""