Pandas DataFrame Preview

VERSION a9b38ce
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 ![alt](url), 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![Temperature and humidity by city](data:image/png;base64,{png})")