Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/altair/README.md
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions examples/altair/chart_basics/code.py
Original file line number Diff line number Diff line change
@@ -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"""
<div class="altair-chart"></div>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
<script type="application/json" class="vega-spec">{spec}</script>
<script>
(function() {{
var scripts = document.getElementsByClassName('vega-spec');
var el = scripts[scripts.length - 1];
var spec = JSON.parse(el.textContent);
var target = el.previousElementSibling;
function embed() {{
if (window.vegaEmbed) {{
vegaEmbed(target, spec, {{actions: false}});
}} else {{
setTimeout(embed, 100);
}}
}}
embed();
}})();
</script>
"""
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 <code>:N</code> and <code>:Q</code> suffixes tell Altair the "
"data type of each column: <em>nominal</em> (categories) and "
"<em>quantitative</em> (numbers). Altair uses these to pick "
"sensible scales and legends automatically."
)
1 change: 1 addition & 0 deletions examples/altair/chart_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["altair", "pandas"]
37 changes: 37 additions & 0 deletions examples/altair/chart_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
66 changes: 66 additions & 0 deletions examples/altair/interactive_selection/code.py
Original file line number Diff line number Diff line change
@@ -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: <code>add_params</code> attaches a "
"selection to a chart, and <code>transform_filter</code> uses "
"that selection elsewhere. The same pattern works for dropdowns, "
"sliders, and click selections."
)
1 change: 1 addition & 0 deletions examples/altair/interactive_selection/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["altair", "pandas", "numpy"]
53 changes: 53 additions & 0 deletions examples/altair/interactive_selection/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), 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"""
<div class="altair-chart"></div>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
<script type="application/json" class="vega-spec">{spec}</script>
<script>
(function() {{
var scripts = document.getElementsByClassName('vega-spec');
var el = scripts[scripts.length - 1];
var spec = JSON.parse(el.textContent);
var target = el.previousElementSibling;
function embed() {{
if (window.vegaEmbed) {{
vegaEmbed(target, spec, {{actions: false}});
}} else {{
setTimeout(embed, 100);
}}
}}
embed();
}})();
</script>
"""
display(HTML(html), append=True)
66 changes: 66 additions & 0 deletions examples/altair/layered_and_faceted/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>facet</code> 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)
1 change: 1 addition & 0 deletions examples/altair/layered_and_faceted/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["altair", "pandas", "numpy"]
53 changes: 53 additions & 0 deletions examples/altair/layered_and_faceted/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), 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"""
<div class="altair-chart"></div>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
<script type="application/json" class="vega-spec">{spec}</script>
<script>
(function() {{
var scripts = document.getElementsByClassName('vega-spec');
var el = scripts[scripts.length - 1];
var spec = JSON.parse(el.textContent);
var target = el.previousElementSibling;
function embed() {{
if (window.vegaEmbed) {{
vegaEmbed(target, spec, {{actions: false}});
}} else {{
setTimeout(embed, 100);
}}
}}
embed();
}})();
</script>
"""
display(HTML(html), append=True)
5 changes: 5 additions & 0 deletions examples/altair/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"chart_basics",
"layered_and_faceted",
"interactive_selection"
]