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..f4026ee --- /dev/null +++ b/examples/altair/chart_basics/code.py @@ -0,0 +1,81 @@ +""" +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. +""" +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 " + "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..d5b729b
--- /dev/null
+++ b/examples/altair/chart_basics/setup.py
@@ -0,0 +1,37 @@
+"""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) 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) + + +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( + "Thefacet 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) + + +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" +]