Pandas DataFrame Preview
Renders a pandas DataFrame as an actual Markdown table in the canvas,
(plus a chart drawn from the same data) instead of the usual
preformatted-text output — via the output="markdown"
fence attribute (SPEC.md's "Runnable code fences"/"Cached output"): a
cached block's captured stdout is normally wrapped in a passive ```text
fence and shown verbatim; with output="markdown" it's spliced in as real
Markdown instead, so df.to_markdown() (needs the tabulate package —
pandas shells out to it rather than implementing Markdown rendering
itself) turns into a real, rendered table on the next run rather than a wall
of pipe characters.
Reuses examples/python-venv.canvas.md's own
pattern for the venv itself, the same meshfox:var from= idiom SPEC.md's
"Computed variables" describes: venv-setup below creates .venv/
(idempotent — a no-op once it already exists), installs this demo's two
dependencies into it, and reports its own interpreter path as the computed
PYTHON variable. A second, independent computed variable, TMP_DIR,
comes from tmp-dir-setup — a fresh scratch directory generate-csv writes
a random CSV into and demo reads it back from. demo below references
both (interpreter="$PYTHON -u", env="TMP_DIR") with no deps= of its
own for either — both source blocks are already its implicit dependencies
via those two from= declarations. What does need an explicit deps= is
the ordering between generate-csv and demo themselves: they only agree
on a directory ($TMP_DIR) and a filename, not a computed variable, so
nothing implicit forces generate-csv to run first.
Environment setup
Creates .venv/ the first time anything below needs it and installs this
demo's requirements into it — both idempotent, so re-running this block on
every use (no cache here, unlike demo below) costs at most a quick "already
satisfied" pip check, not a real reinstall. Uses meshfox's own built-in
interpreter="@python_venv" (see python-venv.canvas.md's "Environment
setup" for the pattern in isolation): the fence's body is a plain
requirements.txt — pandas for the DataFrame itself, tabulate for
DataFrame.to_markdown() (pandas shells out to it rather than implementing
Markdown rendering itself), matplotlib for the chart demo draws below
its table. Deliberately unpinned: an exact == pin only has prebuilt wheels
for the Python versions that existed when it was released, so on a newer (or
older) python3 pip would fall back to compiling from source and fail —
unpinned, pip picks the newest release that has a wheel for this Python.
The builtin reports the venv's own
python3 as the computed PYTHON variable via $MESHFOX_VARS_OUT
(SPEC.md's "Computed variables"), so anything that needs the venv ready
just references $PYTHON (see the root node's note above) rather than
also declaring an explicit deps= on this block.
pandas
tabulate
matplotlib
A second, unrelated setup step: a fresh scratch directory for generate-csv
and demo below to share, reported as the computed TMP_DIR variable the
same way PYTHON above is. No cache here either — a new directory every
run, same reasoning as generate-csv's own randomness.
set -euo pipefail
TMP_DIR="$(mktemp -d -t meshfox-demo-XXXXXX)"
echo "TMP_DIR=$TMP_DIR" >> "$MESHFOX_VARS_OUT"
echo "tmp dir ready: $TMP_DIR"
Generate data
Generates a small random CSV with plain bash — no Python needed yet, so this
runs independently of venv-setup above (it only needs tmp-dir-setup's
$TMP_DIR, via env=, which is this fence's one implicit dependency).
$RANDOM (bash's own built-in, 0-32767) fills in the numbers. The file
itself lives at a fixed name (data.csv) inside that run's own scratch
$TMP_DIR — unlike $TMP_DIR itself, this path isn't a computed variable:
demo below just knows the filename and reconstructs the same path from its
own $TMP_DIR, so the two blocks need an explicit deps= to order them
(see demo's own note). No cache here either: re-running it hands demo
a fresh random dataset each time, which is rather the point of a "random
CSV" demo.
set -euo pipefail
CSV="$TMP_DIR/data.csv"
cities=(Berlin Tokyo Lima Nairobi Oslo Wellington)
{
echo "city,temp_c,humidity_pct"
for city in "${cities[@]}"; do
printf '%s,%d,%d\n' "$city" "$(( RANDOM % 40 - 5 ))" "$(( RANDOM % 60 + 20 ))"
done
} > "$CSV"
echo "generated: $CSV"
Demo
Reads the CSV generated above with pandas.read_csv and prints it via
.to_markdown(). The output="markdown" attribute on the fence below is
what turns that printed pipe-table into an actually-rendered table in the
canvas on the next run, instead of a passive text block — the only
difference from an ordinary cached fence.
Right after the table it draws a small matplotlib bar chart of the same
data and prints it as a Markdown image whose URL is a data:image/png;base64,…
URI: stdout is Markdown, so an image is just , and inlining the
PNG means there's no separate file to place next to the canvas (a
temp-directory path wouldn't resolve in either the web UI or the TUI). The
Agg backend renders straight to a buffer — no display or GUI toolkit needed.
The script also logs a line to stderr (sys.stderr) — with output="markdown",
stderr is captured separately from stdout and shown as its own plain-text
block, before the rendered table, regardless of where in the script it was
actually printed (core::output::render_output_block_markdown): stdout is
the only half meant to be parsed as Markdown, so stray warnings/log lines
never end up mixed into the table.
This fence mixes both ways a meshfox block can depend on another: interpreter=
and env= each pull in a from=-computed variable ($PYTHON, $TMP_DIR),
making their source blocks (venv-setup, tmp-dir-setup) implicit
dependencies — no deps= needed for either, per SPEC.md's "Computed
variables". But generate-csv doesn't compute a variable this fence
consumes — it just leaves data.csv sitting in $TMP_DIR under a name both
sides happen to agree on — so ordering it before this fence needs an actual
explicit deps="generate-data/generate-csv".
import base64
import io
import os
import sys
import matplotlib
matplotlib.use("Agg") # render to a buffer; no display needed
import matplotlib.pyplot as plt
import pandas as pd
path = os.path.join(os.environ["TMP_DIR"], "data.csv")
df = pd.read_csv(path)
print(f"loaded {len(df)} rows from {path}", file=sys.stderr)
print(df.to_markdown(index=False))
# A chart of the same data, inlined as a data: URI so it's part of the output.
fig, (ax_temp, ax_hum) = plt.subplots(1, 2, figsize=(8, 3), sharey=False)
ax_temp.bar(df["city"], df["temp_c"], color="#e56a6a")
ax_temp.set_title("Temperature (°C)")
ax_hum.bar(df["city"], df["humidity_pct"], color="#5b8def")
ax_hum.set_title("Humidity (%)")
for ax in (ax_temp, ax_hum):
ax.tick_params(axis="x", labelrotation=45)
fig.tight_layout()
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=100)
png = base64.b64encode(buf.getvalue()).decode("ascii")
print(f"\n")