From 79402e2787f6509d8280a5d8d56f77af80cd2664 Mon Sep 17 00:00:00 2001 From: David Quarel Date: Thu, 17 Sep 2026 12:52:52 +0000 Subject: [PATCH] Website: mkdocs from the .md files, built to gh-pages on every push to main Three pages, default mkdocs theme: the README as the front page (copied at build time, image and repo links rewritten), docs/api.md (one table per group of the API) and docs/examples.md (the Colab notebook, the demo GIF, how to record your own). .github/workflows/site.yml builds with --strict on pull requests and force-pushes the built site to the orphan gh-pages branch on pushes to main, the same pattern as the demo notebook. The README links to the site. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01G9zs684RFxHvW6cNWqTPSF --- .github/workflows/site.yml | 45 ++++++++++++++++++++++++++++++ .gitignore | 2 ++ README.md | 2 ++ docs/api.md | 56 ++++++++++++++++++++++++++++++++++++++ docs/examples.md | 25 +++++++++++++++++ mkdocs.yml | 11 ++++++++ 6 files changed, 141 insertions(+) create mode 100644 .github/workflows/site.yml create mode 100644 docs/api.md create mode 100644 docs/examples.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml new file mode 100644 index 0000000..157a8ca --- /dev/null +++ b/.github/workflows/site.yml @@ -0,0 +1,45 @@ +name: site + +# Builds the documentation site (mkdocs, from README.md and docs/*.md) and, on pushes to main, +# force-pushes it to the orphan `gh-pages` branch that GitHub Pages serves. On pull requests it +# only builds, with --strict so broken links fail the check. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install mkdocs + - name: Build (README becomes the front page) + run: | + set -e + # images live next to the pages; links into the repo point at GitHub + sed -e 's|](docs/|](|g' -e 's|](examples/|](https://github.com/ARENA-education/liveplot/blob/main/examples/|g' README.md > docs/index.md + mkdocs build --strict + - name: Publish to gh-pages + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + run: | + set -e + cd site + touch .nojekyll + git init -q -b gh-pages + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -q -m "Build site from main ${GITHUB_SHA::7}" + git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push -q --force origin gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c9e821d..d2bef1d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build/ dist/ .pytest_cache/ tour.gif +docs/index.md +site/ diff --git a/README.md b/README.md index da92886..aab834e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Live training curves in Jupyter, Colab and the VS Code / Cursor interactive wind [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ARENA-education/liveplot/blob/demo/examples/demo.ipynb) [![tests](https://github.com/ARENA-education/liveplot/actions/workflows/tests.yml/badge.svg)](https://github.com/ARENA-education/liveplot/actions/workflows/tests.yml) +Documentation: **[arena-education.github.io/liveplot](https://arena-education.github.io/liveplot/)** (this README, the API, and the examples; built from the `.md` files on every push). + Try it in Colab with the badge above: that notebook is [`examples/demo.py`](examples/demo.py), a cell-by-cell tour of the features, which CI converts with jupytext and publishes to the `demo` branch on every push to `main`. ![training loss every step, eval loss and accuracy every 50 steps, on one panel with two y-axes](docs/demo.gif) diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..8671cb5 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,56 @@ +# API + +One class. Everything else is a method on it, on a panel, or on an axis, and the names are tqdm's, wandb's or matplotlib's wherever one exists. + +## `LivePlot(*args, **options)` + +```python +LivePlot([iterable,] *panels, total=None, initial=0, unit="step", unit_scale=1, + refresh_seconds=1.0, smooth=None, max_cols=3, rows=None, cols=None, + cell_size=(5, 3.5), dpi=100, progress=True, desc=None, record=False) +``` + +| argument | meaning | +|---|---| +| `iterable` | anything you would wrap with tqdm: a range, a DataLoader, an existing tqdm bar. Iterating the plot yields its items, shows a tqdm bar under the plot, and finishes the plot when the loop ends. Leave it out for nested loops and use `plot(inner)` instead. | +| `*panels` | layout strings, one per panel: `"loss"`, `"return | entropy"`, `"lossD lossG | acc"`. Names separated by spaces share the left y-axis; names after `|` go on a right-hand axis. With no strings, every metric shares one panel. Metrics no string mentions get a panel of their own. | +| `total`, `initial`, `unit`, `unit_scale` | tqdm's arguments, with tqdm's meaning. They define the x-axis: `x = initial + n * unit_scale`, where `n` counts items consumed. `total` is in items and fixes the x range; the single-loop form takes it from `len(iterable)`. | +| `refresh_seconds` | minimum time between redraws. `0` redraws on every arrival, as fast as rendering allows, and costs nothing while idle. | +| `smooth` | default smoothing weight for every panel, wandb's time-weighted EMA in `[0, 1)`. | +| `max_cols`, `rows`, `cols` | grid shape. `max_cols=None` gives a near-square grid. | +| `progress`, `desc` | switch the tqdm bars off, or give the single-loop form's bar a description. | +| `record` | `True` keeps every rendered frame in `plot.frames`; a path such as `"run.gif"` also writes an animated GIF at `finish()`. | + +## Logging + +| call | meaning | +|---|---| +| `plot.log(loss=0.3, acc=0.9)` | record metrics at the current x. Values are anything `float()` accepts, one-element tensors included. | +| `plot.log(step, loss=0.3)` | an explicit x for this call only, like wandb's `step=`. | +| `plot.log(step, {"loss": 0.3})` | the dict form. | +| `for batch in plot(loader, desc="epoch 3")` | wrap an inner loop: a tqdm bar per loop, the x count continues across loops. | +| `plot.finish()` | draw the final frame and shut the renderer down. Called for you by the iterator form and by `with`. | + +## Configuring, with matplotlib's names + +| handle | how to get it | setters | +|---|---|---| +| an axis | `plot["acc"]` is the y-axis holding that metric, left or right; `plot.panels[i].left` / `.right` | `set_ylabel`, `set_ylim(lo, hi)` or `set_ylim((lo, hi))`, `set_yscale("log")`, `axhline(y, label=..., **kwargs)`, `set(**kwargs)` | +| a panel | `plot.panels[i]`, or `plot["acc"].panel` | `set_title`, `set_xlabel`, `set_xlim`, `set_smooth(weight)`, `axvline(x=None, label=..., **kwargs)` on this panel, `set(**kwargs)`, plus the left axis's setters | +| the plot | `plot` | the same setters applied to every panel (and to panels created later); `axhline(y, label, metric=None)` on every left axis or on one metric's axis; `axvline(x=None, label=...)` on every panel | + +A setter called during the run re-lays the figure out on the next frame. Extra kwargs on `axhline` / `axvline` go to the matplotlib artist (`color`, `linestyle`, `linewidth`, `alpha`, ...). + +## Afterwards + +| attribute / call | meaning | +|---|---| +| `plot.data` | `{metric: (xs, values)}`, the full history. | +| `plot.latest` | the last value of each metric. | +| `plot.figure()` | a matplotlib `Figure` of the current state, to title, tweak or `savefig`. | +| `plot.save_gif(path, speedup=1.0)` | the recorded frames as an animated GIF at the pace of the run (needs `record=True`). | +| `plot.frames` | the recorded `(time, png_bytes)` frames. | + +## Interrupts + +Interrupting a cell is safe: the render process ignores the interrupt, the plot freezes at its last frame with `plot.data` intact, and re-running the cell starts a fresh plot. A plot dropped without `finish()` shuts its process down when garbage collected, and the process exits by itself if the kernel dies. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..deb9c12 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,25 @@ +# Examples + +## The tour, in Colab + +[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ARENA-education/liveplot/blob/demo/examples/demo.ipynb) + +[`examples/demo.py`](https://github.com/ARENA-education/liveplot/blob/main/examples/demo.py) walks through the features cell by cell: the one-liner, layout strings and metrics logged at different rates, nested epoch/batch loops, matplotlib-style configuration, your own tqdm bar, interrupts, recording, and reading the data back. CI converts it to a notebook on the `demo` branch on every push to `main`, which is what the badge opens. It also runs as a plain script, and cell by cell in the VS Code / Cursor interactive window. + +## What it looks like + +A tiny numpy classifier logging its training loss every step and its eval loss and accuracy every 50 steps, laid out as `"loss | eval_loss eval_acc"`. Recorded by the library itself with `record="demo.gif"`: + +![demo](demo.gif) + +## Recording your own + +```python +plot = LivePlot(range(steps), "loss | acc", record="run.gif") +for step in plot: + plot.log(loss=train_step()) + if step % 50 == 0: + plot.log(acc=evaluate()) +``` + +`record="run.gif"` writes an animated GIF at `finish()` that replays at the real pace of the run; `record=True` just keeps the frames for `plot.save_gif(path, speedup=2.0)` later. Recording works outside a notebook too, so a script can produce the GIF for a README. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..ff00d07 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: liveplot +site_description: Live training curves in Jupyter, Colab and VS Code, at (almost) no cost to the training loop +site_url: https://arena-education.github.io/liveplot/ +repo_url: https://github.com/ARENA-education/liveplot +docs_dir: docs +nav: + - Home: index.md + - API: api.md + - Examples: examples.md +theme: + name: mkdocs