Skip to content
Merged
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
45 changes: 45 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ build/
dist/
.pytest_cache/
tour.gif
docs/index.md
site/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -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
Loading