runs filter: N + T chip rows; group all/none meta chips; explicit row layout
- Add N and T axes alongside dataset/algorithm; chips populated from runs in the list, axis group hidden when there's a single unique value. - Dataset+algorithm on row 1, N+T on row 2 via two explicit .runs-filter-row flex containers (cleaner than a sentinel break elem that double-counted the row-gap). - 'all' and 'none' meta-chips now wrap as a unit inside .chip-meta-wrap so one doesn't orphan to the next line. - Row is hidden entirely when every axis in it collapses to a single value (:has selector on .runs-filter-row).
This commit is contained in:
parent
4576088c73
commit
d70eff3704
@ -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();
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
{% for r in runs %}
|
||||
<li class="run {% if just_submitted is defined and r.id == just_submitted %}just-submitted{% endif %}{% if r.stale %} stale{% endif %}"
|
||||
data-embedder="{{ r.embedder_short or '' }}"
|
||||
data-generator="{{ r.generator_short or '' }}">
|
||||
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 %}
|
||||
<input type="checkbox" class="compare-cb" data-stem="{{ r.emb_file[:-5] }}" aria-label="select run for comparison" />
|
||||
{% else %}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>embedding notebook · compare</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=32" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=36" />
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>embedding notebook</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=32" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=36" />
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
@ -309,15 +309,27 @@
|
||||
</div>
|
||||
|
||||
<div class="runs-filter" id="runs-filter">
|
||||
<div class="runs-filter-group">
|
||||
<div class="runs-filter-row">
|
||||
<div class="runs-filter-group" data-axis="dataset">
|
||||
<span class="ctl-label">dataset</span>
|
||||
<div class="chips" id="runs-flt-dataset" aria-label="filter by dataset"></div>
|
||||
</div>
|
||||
<div class="runs-filter-group">
|
||||
<div class="runs-filter-group" data-axis="algorithm">
|
||||
<span class="ctl-label">algorithm</span>
|
||||
<div class="chips" id="runs-flt-algo" aria-label="filter by algorithm"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="runs-filter-row">
|
||||
<div class="runs-filter-group" data-axis="n">
|
||||
<span class="ctl-label">N</span>
|
||||
<div class="chips" id="runs-flt-n" aria-label="filter by N"></div>
|
||||
</div>
|
||||
<div class="runs-filter-group" data-axis="t">
|
||||
<span class="ctl-label">T</span>
|
||||
<div class="chips" id="runs-flt-t" aria-label="filter by T"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="runs-slot"
|
||||
@ -485,7 +497,7 @@
|
||||
<script type="module" src="/static/dataset-picker.js?v=11"></script>
|
||||
<script type="module" src="/static/metrics.js?v=11"></script>
|
||||
<script src="/static/compare-select.js?v=2"></script>
|
||||
<script src="/static/runs-filter.js?v=1"></script>
|
||||
<script src="/static/runs-filter.js?v=3"></script>
|
||||
<script type="module" src="/static/run-modal.js?v=2"></script>
|
||||
<script>
|
||||
// Anchor-links alone don't expand <details>; force it.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user