From 40ff6766798c9f4829322088f6a164cf3efb10ec Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:00 +0100 Subject: [PATCH 1/2] Add PyScript examples for altair Generated by apply_llm_response.py from prompts/altair/response.toml. Examples included: - chart_basics: Chart basics - layered_and_faceted: Layered and faceted charts - interactive_selection: Interactive linked views Generated-By: apply_llm_response.py --- examples/altair/README.md | 18 +++++ examples/altair/chart_basics/code.py | 48 +++++++++++++ examples/altair/chart_basics/config.toml | 1 + examples/altair/chart_basics/setup.py | 70 +++++++++++++++++++ examples/altair/interactive_selection/code.py | 66 +++++++++++++++++ .../altair/interactive_selection/config.toml | 1 + .../altair/interactive_selection/setup.py | 53 ++++++++++++++ examples/altair/layered_and_faceted/code.py | 66 +++++++++++++++++ .../altair/layered_and_faceted/config.toml | 1 + examples/altair/layered_and_faceted/setup.py | 53 ++++++++++++++ examples/altair/order.json | 5 ++ 11 files changed, 382 insertions(+) create mode 100644 examples/altair/README.md create mode 100644 examples/altair/chart_basics/code.py create mode 100644 examples/altair/chart_basics/config.toml create mode 100644 examples/altair/chart_basics/setup.py create mode 100644 examples/altair/interactive_selection/code.py create mode 100644 examples/altair/interactive_selection/config.toml create mode 100644 examples/altair/interactive_selection/setup.py create mode 100644 examples/altair/layered_and_faceted/code.py create mode 100644 examples/altair/layered_and_faceted/config.toml create mode 100644 examples/altair/layered_and_faceted/setup.py create mode 100644 examples/altair/order.json diff --git a/examples/altair/README.md b/examples/altair/README.md new file mode 100644 index 0000000..1e7ff9c --- /dev/null +++ b/examples/altair/README.md @@ -0,0 +1,18 @@ +# altair Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/altair/chart_basics/code.py b/examples/altair/chart_basics/code.py new file mode 100644 index 0000000..cbecb2b --- /dev/null +++ b/examples/altair/chart_basics/code.py @@ -0,0 +1,48 @@ +""" +A first look at Vega-Altair: declarative statistical visualization. + +Altair charts are built by combining three things: + 1. A data source (typically a pandas DataFrame). + 2. A mark (the visual primitive: point, bar, line, ...). + 3. Encodings that map data columns to visual channels (x, y, color, ...). + +See https://altair-viz.github.io for the full documentation. +""" +from IPython.core.display import display, HTML + +heading("A bakery's daily sales") +note( + "Seven days of pastry sales at a small bakery. We'll plot the " + "daily totals as a bar chart, mapping the day to the x-axis and " + "the number of pastries sold to the y-axis." +) + +bakery = pd.DataFrame({ + "day": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + "pastries_sold": [42, 38, 55, 47, 73, 96, 81], + "is_weekend": [False, False, False, False, False, True, True], +}) + +display(bakery, append=True) + +# The classic Altair pattern: Chart(data) -> mark -> encode. +chart = ( + alt.Chart(bakery) + .mark_bar() + .encode( + x=alt.X("day:N", sort=bakery["day"].tolist(), title="Day of week"), + y=alt.Y("pastries_sold:Q", title="Pastries sold"), + color=alt.Color("is_weekend:N", title="Weekend?"), + tooltip=["day", "pastries_sold"], + ) + .properties(title="Pastries sold per day", width=420, height=260) +) + +show_chart(chart) + +note( + "The :N and :Q suffixes tell Altair the " + "data type of each column: nominal (categories) and " + "quantitative (numbers). Altair uses these to pick " + "sensible scales and legends automatically." +) diff --git a/examples/altair/chart_basics/config.toml b/examples/altair/chart_basics/config.toml new file mode 100644 index 0000000..7ae43df --- /dev/null +++ b/examples/altair/chart_basics/config.toml @@ -0,0 +1 @@ +packages = ["altair", "pandas"] diff --git a/examples/altair/chart_basics/setup.py b/examples/altair/chart_basics/setup.py new file mode 100644 index 0000000..ebaec6b --- /dev/null +++ b/examples/altair/chart_basics/setup.py @@ -0,0 +1,70 @@ +"""Shim IPython's display API onto PyScript and import altair.""" +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import altair as alt +import pandas as pd + + +def show_chart(chart): + """Render an Altair chart as inline HTML via Vega-Embed.""" + spec = chart.to_json() + html = f""" +
+ + + + + + """ + display(HTML(html), append=True) diff --git a/examples/altair/interactive_selection/code.py b/examples/altair/interactive_selection/code.py new file mode 100644 index 0000000..531280d --- /dev/null +++ b/examples/altair/interactive_selection/code.py @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------- +# Interactivity: drag a brush on one chart to filter another. +# --------------------------------------------------------------------- + +heading("Brushing and linking: athletes' training data") +note( + "Two charts that share a selection: drag a rectangle in the " + "scatter plot to highlight athletes; the bar chart updates to " + "show only the selected group's distribution by sport." +) + +n = 200 +sports = rng.choice(["Running", "Cycling", "Swimming", "Rowing"], size=n) +# Heart rate and weekly training hours, with sport-specific tendencies. +hours_per_week = rng.uniform(2, 18, size=n).round(1) +resting_hr = ( + 72 - 1.4 * hours_per_week + rng.normal(0, 4, size=n) +).round(1).clip(40, 90) + +athletes = pd.DataFrame({ + "athlete_id": np.arange(n), + "sport": sports, + "hours_per_week": hours_per_week, + "resting_hr": resting_hr, +}) + +# An interval selection that we can attach to a chart and reference +# from other charts to filter or recolor. +brush = alt.selection_interval() + +scatter = ( + alt.Chart(athletes) + .mark_circle(size=70) + .encode( + x=alt.X("hours_per_week:Q", title="Training hours per week"), + y=alt.Y("resting_hr:Q", title="Resting heart rate (bpm)"), + # `alt.when(...).then(...).otherwise(...)` recolors based on selection. + color=alt.when(brush).then("sport:N").otherwise(alt.value("lightgray")), + tooltip=["sport", "hours_per_week", "resting_hr"], + ) + .add_params(brush) + .properties(width=420, height=300, title="Drag to select athletes") +) + +bars = ( + alt.Chart(athletes) + .mark_bar() + .encode( + x=alt.X("count():Q", title="Selected athletes"), + y=alt.Y("sport:N", title=None), + color="sport:N", + ) + .transform_filter(brush) # The bar chart only sees brushed rows. + .properties(width=420, height=120, title="Counts by sport") +) + +# `&` stacks charts vertically; `|` would place them side by side. +linked = scatter & bars +show_chart(linked) + +note( + "Two ideas worth keeping: add_params attaches a " + "selection to a chart, and transform_filter uses " + "that selection elsewhere. The same pattern works for dropdowns, " + "sliders, and click selections." +) diff --git a/examples/altair/interactive_selection/config.toml b/examples/altair/interactive_selection/config.toml new file mode 100644 index 0000000..ff0e3e7 --- /dev/null +++ b/examples/altair/interactive_selection/config.toml @@ -0,0 +1 @@ +packages = ["altair", "pandas", "numpy"] diff --git a/examples/altair/interactive_selection/setup.py b/examples/altair/interactive_selection/setup.py new file mode 100644 index 0000000..632bb89 --- /dev/null +++ b/examples/altair/interactive_selection/setup.py @@ -0,0 +1,53 @@ +"""Imports and helpers for the third altair example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import altair as alt +import pandas as pd +import numpy as np + +rng = np.random.default_rng(0) + + +def show_chart(chart): + """Render an Altair chart as inline HTML via Vega-Embed.""" + spec = chart.to_json() + html = f""" +
+ + + + + + """ + display(HTML(html), append=True) diff --git a/examples/altair/layered_and_faceted/code.py b/examples/altair/layered_and_faceted/code.py new file mode 100644 index 0000000..a6a1a22 --- /dev/null +++ b/examples/altair/layered_and_faceted/code.py @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------- +# Layering marks together, and splitting one chart into many with facets. +# --------------------------------------------------------------------- + +heading("Layering: city temperatures with a rolling mean") +note( + "Sixty days of synthetic daily high temperatures for two cities. " + "We layer a faint line of raw values under a thicker rolling mean " + "to show both detail and trend in a single chart." +) + +n_days = 60 +dates = pd.date_range("2026-04-01", periods=n_days, freq="D") +cities = ["Lisbon", "Reykjavik"] +records = [] +for city, base in zip(cities, [22, 8]): + trend = base + 4 * np.sin(np.arange(n_days) * 2 * np.pi / 30) + noise = rng.normal(0, 2.0, size=n_days) + records.append(pd.DataFrame({ + "date": dates, + "city": city, + "temperature_c": (trend + noise).round(2), + })) +weather = pd.concat(records, ignore_index=True) +weather["rolling_mean"] = ( + weather.groupby("city")["temperature_c"] + .transform(lambda s: s.rolling(7, min_periods=1).mean()) +) + +base = alt.Chart(weather).encode( + x=alt.X("date:T", title="Date"), + color=alt.Color("city:N", title="City"), +) + +# Two marks sharing the same data and x/color encodings, layered with `+`. +raw_line = base.mark_line(opacity=0.3).encode(y="temperature_c:Q") +smooth_line = base.mark_line(size=3).encode( + y=alt.Y("rolling_mean:Q", title="Temperature (°C)") +) + +layered = (raw_line + smooth_line).properties( + title="Daily highs with 7-day rolling mean", + width=520, + height=260, +) +show_chart(layered) + +heading("Faceting: one small chart per city") +note( + "The facet operator splits the data by a column and " + "draws a separate sub-chart for each value, sharing scales and " + "axes. Great for comparing groups side by side." +) + +faceted = ( + alt.Chart(weather) + .mark_area(opacity=0.6) + .encode( + x=alt.X("date:T", title=None), + y=alt.Y("temperature_c:Q", title="°C"), + color="city:N", + ) + .properties(width=240, height=160) + .facet(column=alt.Column("city:N", title=None)) +) +show_chart(faceted) diff --git a/examples/altair/layered_and_faceted/config.toml b/examples/altair/layered_and_faceted/config.toml new file mode 100644 index 0000000..ff0e3e7 --- /dev/null +++ b/examples/altair/layered_and_faceted/config.toml @@ -0,0 +1 @@ +packages = ["altair", "pandas", "numpy"] diff --git a/examples/altair/layered_and_faceted/setup.py b/examples/altair/layered_and_faceted/setup.py new file mode 100644 index 0000000..01e2542 --- /dev/null +++ b/examples/altair/layered_and_faceted/setup.py @@ -0,0 +1,53 @@ +"""Imports and helpers for the second altair example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import altair as alt +import pandas as pd +import numpy as np + +rng = np.random.default_rng(7) + + +def show_chart(chart): + """Render an Altair chart as inline HTML via Vega-Embed.""" + spec = chart.to_json() + html = f""" +
+ + + + + + """ + display(HTML(html), append=True) diff --git a/examples/altair/order.json b/examples/altair/order.json new file mode 100644 index 0000000..ac87916 --- /dev/null +++ b/examples/altair/order.json @@ -0,0 +1,5 @@ +[ + "chart_basics", + "layered_and_faceted", + "interactive_selection" +] From 36de3cd6d7700c7c86d4b7d229a3d5e96fe4bd14 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 16:30:01 +0100 Subject: [PATCH 2/2] Move setups to first example. --- examples/altair/chart_basics/code.py | 33 +++++++++++++++++++++++++++ examples/altair/chart_basics/setup.py | 33 --------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/altair/chart_basics/code.py b/examples/altair/chart_basics/code.py index cbecb2b..f4026ee 100644 --- a/examples/altair/chart_basics/code.py +++ b/examples/altair/chart_basics/code.py @@ -8,8 +8,41 @@ See https://altair-viz.github.io for the full documentation. """ +import altair as alt +import pandas as pd from IPython.core.display import display, HTML + + +def show_chart(chart): + """Render an Altair chart as inline HTML via Vega-Embed.""" + spec = chart.to_json() + html = f""" +
+ + + + + + """ + display(HTML(html), append=True) + + heading("A bakery's daily sales") note( "Seven days of pastry sales at a small bakery. We'll plot the " diff --git a/examples/altair/chart_basics/setup.py b/examples/altair/chart_basics/setup.py index ebaec6b..d5b729b 100644 --- a/examples/altair/chart_basics/setup.py +++ b/examples/altair/chart_basics/setup.py @@ -35,36 +35,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import altair as alt -import pandas as pd - - -def show_chart(chart): - """Render an Altair chart as inline HTML via Vega-Embed.""" - spec = chart.to_json() - html = f""" -
- - - - - - """ - display(HTML(html), append=True)