diff --git a/app/web/static/runs-filter.js b/app/web/static/runs-filter.js index 8e99583..71a8eba 100644 --- a/app/web/static/runs-filter.js +++ b/app/web/static/runs-filter.js @@ -1,30 +1,40 @@ -// Filter the recent-runs list by dataset + algorithm chips. -// State lives outside #runs-slot so it survives the 3s htmx poll. After -// each swap we repopulate chip options from whatever runs came back, then -// re-apply the current selection to hide non-matching rows. +// Filter the recent-runs list by chip-groups. State lives outside +// #runs-slot so it survives the 3s htmx poll. After each swap we repopulate +// chips from whatever came back, then re-apply selection to hide rows. (function () { const slot = document.getElementById('runs-slot'); - const dsEl = document.getElementById('runs-flt-dataset'); - const algEl = document.getElementById('runs-flt-algo'); - if (!slot || !dsEl || !algEl) return; + if (!slot) return; - // null = "all selected" (no filtering on this axis). Populated Sets - // override that. Sticky across htmx swaps. - let datasets = null; - let algorithms = null; + const AXES = [ + { prop: 'generator', chipsId: 'runs-flt-dataset', numeric: false }, + { prop: 'embedder', chipsId: 'runs-flt-algo', numeric: false }, + { prop: 'n', chipsId: 'runs-flt-n', numeric: true }, + { prop: 't', chipsId: 'runs-flt-t', numeric: true }, + ]; - function scanValues() { - const ds = new Set(); - const alg = new Set(); + for (const ax of AXES) { + ax.el = document.getElementById(ax.chipsId); + ax.group = ax.el ? ax.el.closest('.runs-filter-group') : null; + ax.selected = null; // null = all + } + + function scanAll() { + const out = new Map(AXES.map(a => [a.prop, new Set()])); slot.querySelectorAll('li.run').forEach((li) => { - const d = li.dataset.generator; if (d) ds.add(d); - const a = li.dataset.embedder; if (a) alg.add(a); + for (const ax of AXES) { + const v = li.dataset[ax.prop]; + if (v) out.get(ax.prop).add(v); + } }); - return { - datasets: [...ds].sort(), - algorithms: [...alg].sort(), - }; + return out; + } + + function sortValues(vals, numeric) { + const arr = [...vals]; + if (numeric) arr.sort((a, b) => Number(a) - Number(b)); + else arr.sort(); + return arr; } function paint(container, values, selected) { @@ -40,68 +50,65 @@ b.textContent = v; container.appendChild(b); } + // Keep all/none atomic so they wrap together rather than orphaning. + const metaWrap = document.createElement('span'); + metaWrap.className = 'chip-meta-wrap'; for (const [role, label] of [['all', 'all'], ['none', 'none']]) { const b = document.createElement('button'); b.type = 'button'; b.className = 'chip chip-meta'; b.dataset.role = role; b.textContent = label; - container.appendChild(b); + metaWrap.appendChild(b); } + container.appendChild(metaWrap); } function repaint() { - const { datasets: allDs, algorithms: allAlg } = scanValues(); - paint(dsEl, allDs, datasets); - paint(algEl, allAlg, algorithms); + const scanned = scanAll(); + for (const ax of AXES) { + if (!ax.el) continue; + const values = sortValues(scanned.get(ax.prop), ax.numeric); + // Hide the whole group when there's nothing to filter by. + if (ax.group) ax.group.style.display = values.length <= 1 ? 'none' : ''; + paint(ax.el, values, ax.selected); + } } function apply() { slot.querySelectorAll('li.run').forEach((li) => { - const ds = li.dataset.generator || ''; - const al = li.dataset.embedder || ''; - const passDs = datasets == null || datasets.has(ds); - const passAl = algorithms == null || algorithms.has(al); - li.classList.toggle('filtered-out', !(passDs && passAl)); + let pass = true; + for (const ax of AXES) { + if (ax.selected == null) continue; + const v = li.dataset[ax.prop] || ''; + if (!ax.selected.has(v)) { pass = false; break; } + } + li.classList.toggle('filtered-out', !pass); }); } - function bind(container, getSet, setSet, allGetter) { - container.addEventListener('click', (e) => { + for (const ax of AXES) { + if (!ax.el) continue; + ax.el.addEventListener('click', (e) => { const btn = e.target.closest('.chip'); if (!btn) return; const role = btn.dataset.role; - const all = allGetter(); - let cur = getSet(); - if (cur == null) cur = new Set(all); + const all = sortValues(scanAll().get(ax.prop), ax.numeric); + let cur = ax.selected == null ? new Set(all) : ax.selected; if (role === 'value') { const v = btn.dataset.value; - if (cur.has(v)) cur.delete(v); - else cur.add(v); + if (cur.has(v)) cur.delete(v); else cur.add(v); } else if (role === 'all') { cur = new Set(all); } else if (role === 'none') { cur = new Set(); } - setSet(cur); + ax.selected = cur; repaint(); apply(); }); } - bind( - dsEl, - () => datasets, - (s) => { datasets = s; }, - () => scanValues().datasets, - ); - bind( - algEl, - () => algorithms, - (s) => { algorithms = s; }, - () => scanValues().algorithms, - ); - document.body.addEventListener('htmx:afterSwap', (e) => { if (e.target && e.target.id === 'runs-slot') { repaint(); diff --git a/app/web/static/style.css b/app/web/static/style.css index b070979..abae981 100644 --- a/app/web/static/style.css +++ b/app/web/static/style.css @@ -516,12 +516,22 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c .runs-filter { display: flex; - flex-wrap: wrap; - gap: 0.6rem 1.4rem; + flex-direction: column; + gap: 0.4rem; padding: 0.25rem 0 0.7rem; margin-bottom: 0.3rem; border-bottom: 1px dashed var(--rule); } +.runs-filter-row { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 1.4rem; +} +/* Hide a row entirely when every child group is display:none (all axes + in it have a single value). :has is supported in all modern evergreens. */ +.runs-filter-row:not(:has(.runs-filter-group:not([style*="display: none"]))) { + display: none; +} .runs-filter-group { display: flex; align-items: center; @@ -542,6 +552,11 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c gap: 0.25rem 0.3rem; align-items: center; } +.runs-filter-group .chips .chip-meta-wrap { + display: inline-flex; + gap: 0.25rem 0.3rem; + flex-shrink: 0; +} .runs li.run.filtered-out { display: none; } .runs li.run.just-submitted { diff --git a/app/web/templates/_runs.html b/app/web/templates/_runs.html index ebffc90..4359ca6 100644 --- a/app/web/templates/_runs.html +++ b/app/web/templates/_runs.html @@ -9,7 +9,9 @@ {% for r in runs %}
  • + data-generator="{{ r.generator_short or '' }}" + data-n="{{ r.params.get('num_points', '') if r.params else '' }}" + data-t="{{ r.params.get('num_timesteps', r.params.get('num_snapshots', '')) if r.params else '' }}"> {% if r.emb_exists and not r.stale %} {% else %} diff --git a/app/web/templates/compare.html b/app/web/templates/compare.html index 8746396..e200d3f 100644 --- a/app/web/templates/compare.html +++ b/app/web/templates/compare.html @@ -4,7 +4,7 @@ embedding notebook · compare - + - +