Roadmap · Features · P2 · M

Run comparison overlay

A build-it-yourself guide. Today the live chart shows exactly one run's train/val curve at a time. This lets you pick several past runs and draw their curves on the same axes — the whole point of a visual tool is seeing hyperparameter choices side by side, not one at a time.

Done when: you can select two or more runs from the runs list and see their validation curves overlaid on one chart, each in a distinct color with a legend naming the run — reusing the data the API already serves.
0 / 0 steps

The good news

This is almost entirely a frontend change — the backend already gives you everything. GET /api/runs lists every persisted run, and GET /api/run/<id> returns that run's full metrics array (the {epoch, train, val} rows from metrics.jsonl). Comparison is just: fetch several runs' metrics and draw them together.

  1. No new persistence — runs are already on disk under runs/<id>/.
  2. The one refactor: drawChart in static/graph.js is hardwired to the two-series CHART_SERIES (val/train) of a single run. Generalize it to draw a list of named series.

Steps

  1. Generalize drawChart to take multiple named series
    modulearn/static/graph.js — drawChart (~line 784)

    Extract the axis-fitting + line-drawing into a core that accepts an array of {label, color, points:[{epoch, value}]}. Keep the current live view as one caller (val + train of the active run); add a second caller for comparison. The min/max auto-fit already loops over series — widen it to span every series passed in.

    // core: draw any number of series on shared auto-fit axes
    function drawSeries(seriesList) {
      const cv = document.getElementById("chart");
      const all = seriesList.flatMap(s => s.points);
      if (all.length < 2) { cv.style.display = "none"; return; }
      // ...existing ymin/ymax/X/Y fitting, but over `all` instead of CHART_SERIES...
      seriesList.forEach(s => { /* existing line + latest-dot draw, per series */ });
      // legend: one swatch per series, label = s.label
    }
    Keep the live path intact. Wrap the existing behavior: drawChart(metrics) becomes a thin adapter that builds the val/train series and calls drawSeries. pollRun keeps calling drawChart unchanged, so you don't disturb live training.

  2. Add a compare picker to the runs UI
    modulearn/static/graph.js — near refreshRuns / loadRun

    The runs <select> is single-pick. Add a lightweight multi-select (checkboxes, or multiple on the select) plus a "Compare" action. Reuse the run list you already fetch in refreshRuns.

    async function compareRuns(runIds) {
      const palette = ["#d8a24a","#5aa9e6","#3fb950","#c98bdb","#c8663c"];
      const series = await Promise.all(runIds.map(async (id, i) => {
        const j = await (await fetch("/api/run/" + id)).json();
        const points = (j.metrics || [])
          .filter(m => typeof m.epoch === "number" && typeof m.val === "number")
          .map(m => ({ epoch: m.epoch, value: m.val }));
        return { label: id, color: palette[i % palette.length], points };
      }));
      stopPolling();            // comparison is a static view, not a live one
      drawSeries(series);
    }
    Compare on val by default. Overlaying both train and val for many runs gets noisy fast. Start with the validation curve (the number people actually compare); a train/val toggle is a nice follow-up, not a requirement.

  3. Label the axes and legend for many runs
    modulearn/static/graph.js — legend block in the new core

    The single-run legend prints val/train. For comparison the legend should name the runs. Truncate long run ids (they're <dataset>_<hex8>) so the legend fits, and keep the loss · epoch axis hint.

  4. Verify the overlay

    Generate a couple of runs, then compare them.

    modulearn demo                       # in the editor: Train twice with different lr
    # then select both runs and hit Compare
    # expect: two validation curves on one chart, distinct colors, legend by run id
    Sanity-check with the API directly. The overlay is only as right as the data: curl -s localhost:8000/api/run/<id> | python -m json.tool should show the same metrics rows you see plotted. If a curve is missing, it's a filter/keys issue in the adapter, not the chart.

  5. Commit
    git add modulearn/static/graph.js
    git commit -m "Add run comparison overlay to the live chart"
    git push
    gh run list --branch main --limit 1   # expect: success
  6. Mark it shipped on the roadmap

    Check the {area:"features", … title:"Run comparison overlay"} card's box in docs/roadmap.html (shipped state is stored per-browser).

Worth knowing

This unlocks the next two features. A generalized multi-series chart is exactly what hyperparameter sweep needs to show a fan of runs at once — build the overlay first and the sweep's payoff is basically free. It also pairs with stop / re-run: kill a bad run, tweak, re-launch, and compare the two curves.