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.
|
// Filter the recent-runs list by chip-groups. State lives outside
|
||||||
// State lives outside #runs-slot so it survives the 3s htmx poll. After
|
// #runs-slot so it survives the 3s htmx poll. After each swap we repopulate
|
||||||
// each swap we repopulate chip options from whatever runs came back, then
|
// chips from whatever came back, then re-apply selection to hide rows.
|
||||||
// re-apply the current selection to hide non-matching rows.
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const slot = document.getElementById('runs-slot');
|
const slot = document.getElementById('runs-slot');
|
||||||
const dsEl = document.getElementById('runs-flt-dataset');
|
if (!slot) return;
|
||||||
const algEl = document.getElementById('runs-flt-algo');
|
|
||||||
if (!slot || !dsEl || !algEl) return;
|
|
||||||
|
|
||||||
// null = "all selected" (no filtering on this axis). Populated Sets
|
const AXES = [
|
||||||
// override that. Sticky across htmx swaps.
|
{ prop: 'generator', chipsId: 'runs-flt-dataset', numeric: false },
|
||||||
let datasets = null;
|
{ prop: 'embedder', chipsId: 'runs-flt-algo', numeric: false },
|
||||||
let algorithms = null;
|
{ prop: 'n', chipsId: 'runs-flt-n', numeric: true },
|
||||||
|
{ prop: 't', chipsId: 'runs-flt-t', numeric: true },
|
||||||
|
];
|
||||||
|
|
||||||
function scanValues() {
|
for (const ax of AXES) {
|
||||||
const ds = new Set();
|
ax.el = document.getElementById(ax.chipsId);
|
||||||
const alg = new Set();
|
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) => {
|
slot.querySelectorAll('li.run').forEach((li) => {
|
||||||
const d = li.dataset.generator; if (d) ds.add(d);
|
for (const ax of AXES) {
|
||||||
const a = li.dataset.embedder; if (a) alg.add(a);
|
const v = li.dataset[ax.prop];
|
||||||
|
if (v) out.get(ax.prop).add(v);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return {
|
return out;
|
||||||
datasets: [...ds].sort(),
|
}
|
||||||
algorithms: [...alg].sort(),
|
|
||||||
};
|
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) {
|
function paint(container, values, selected) {
|
||||||
@ -40,68 +50,65 @@
|
|||||||
b.textContent = v;
|
b.textContent = v;
|
||||||
container.appendChild(b);
|
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']]) {
|
for (const [role, label] of [['all', 'all'], ['none', 'none']]) {
|
||||||
const b = document.createElement('button');
|
const b = document.createElement('button');
|
||||||
b.type = 'button';
|
b.type = 'button';
|
||||||
b.className = 'chip chip-meta';
|
b.className = 'chip chip-meta';
|
||||||
b.dataset.role = role;
|
b.dataset.role = role;
|
||||||
b.textContent = label;
|
b.textContent = label;
|
||||||
container.appendChild(b);
|
metaWrap.appendChild(b);
|
||||||
}
|
}
|
||||||
|
container.appendChild(metaWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
function repaint() {
|
function repaint() {
|
||||||
const { datasets: allDs, algorithms: allAlg } = scanValues();
|
const scanned = scanAll();
|
||||||
paint(dsEl, allDs, datasets);
|
for (const ax of AXES) {
|
||||||
paint(algEl, allAlg, algorithms);
|
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() {
|
function apply() {
|
||||||
slot.querySelectorAll('li.run').forEach((li) => {
|
slot.querySelectorAll('li.run').forEach((li) => {
|
||||||
const ds = li.dataset.generator || '';
|
let pass = true;
|
||||||
const al = li.dataset.embedder || '';
|
for (const ax of AXES) {
|
||||||
const passDs = datasets == null || datasets.has(ds);
|
if (ax.selected == null) continue;
|
||||||
const passAl = algorithms == null || algorithms.has(al);
|
const v = li.dataset[ax.prop] || '';
|
||||||
li.classList.toggle('filtered-out', !(passDs && passAl));
|
if (!ax.selected.has(v)) { pass = false; break; }
|
||||||
|
}
|
||||||
|
li.classList.toggle('filtered-out', !pass);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bind(container, getSet, setSet, allGetter) {
|
for (const ax of AXES) {
|
||||||
container.addEventListener('click', (e) => {
|
if (!ax.el) continue;
|
||||||
|
ax.el.addEventListener('click', (e) => {
|
||||||
const btn = e.target.closest('.chip');
|
const btn = e.target.closest('.chip');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const role = btn.dataset.role;
|
const role = btn.dataset.role;
|
||||||
const all = allGetter();
|
const all = sortValues(scanAll().get(ax.prop), ax.numeric);
|
||||||
let cur = getSet();
|
let cur = ax.selected == null ? new Set(all) : ax.selected;
|
||||||
if (cur == null) cur = new Set(all);
|
|
||||||
if (role === 'value') {
|
if (role === 'value') {
|
||||||
const v = btn.dataset.value;
|
const v = btn.dataset.value;
|
||||||
if (cur.has(v)) cur.delete(v);
|
if (cur.has(v)) cur.delete(v); else cur.add(v);
|
||||||
else cur.add(v);
|
|
||||||
} else if (role === 'all') {
|
} else if (role === 'all') {
|
||||||
cur = new Set(all);
|
cur = new Set(all);
|
||||||
} else if (role === 'none') {
|
} else if (role === 'none') {
|
||||||
cur = new Set();
|
cur = new Set();
|
||||||
}
|
}
|
||||||
setSet(cur);
|
ax.selected = cur;
|
||||||
repaint();
|
repaint();
|
||||||
apply();
|
apply();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bind(
|
|
||||||
dsEl,
|
|
||||||
() => datasets,
|
|
||||||
(s) => { datasets = s; },
|
|
||||||
() => scanValues().datasets,
|
|
||||||
);
|
|
||||||
bind(
|
|
||||||
algEl,
|
|
||||||
() => algorithms,
|
|
||||||
(s) => { algorithms = s; },
|
|
||||||
() => scanValues().algorithms,
|
|
||||||
);
|
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', (e) => {
|
document.body.addEventListener('htmx:afterSwap', (e) => {
|
||||||
if (e.target && e.target.id === 'runs-slot') {
|
if (e.target && e.target.id === 'runs-slot') {
|
||||||
repaint();
|
repaint();
|
||||||
|
|||||||
@ -516,12 +516,22 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c
|
|||||||
|
|
||||||
.runs-filter {
|
.runs-filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.6rem 1.4rem;
|
gap: 0.4rem;
|
||||||
padding: 0.25rem 0 0.7rem;
|
padding: 0.25rem 0 0.7rem;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
border-bottom: 1px dashed var(--rule);
|
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 {
|
.runs-filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -542,6 +552,11 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c
|
|||||||
gap: 0.25rem 0.3rem;
|
gap: 0.25rem 0.3rem;
|
||||||
align-items: center;
|
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.filtered-out { display: none; }
|
||||||
.runs li.run.just-submitted {
|
.runs li.run.just-submitted {
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
{% for r in runs %}
|
{% 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 %}"
|
<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-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 %}
|
{% 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" />
|
<input type="checkbox" class="compare-cb" data-stem="{{ r.emb_file[:-5] }}" aria-label="select run for comparison" />
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>embedding notebook · compare</title>
|
<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">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>embedding notebook</title>
|
<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 src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
@ -309,15 +309,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="runs-filter" id="runs-filter">
|
<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>
|
<span class="ctl-label">dataset</span>
|
||||||
<div class="chips" id="runs-flt-dataset" aria-label="filter by dataset"></div>
|
<div class="chips" id="runs-flt-dataset" aria-label="filter by dataset"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="runs-filter-group">
|
<div class="runs-filter-group" data-axis="algorithm">
|
||||||
<span class="ctl-label">algorithm</span>
|
<span class="ctl-label">algorithm</span>
|
||||||
<div class="chips" id="runs-flt-algo" aria-label="filter by algorithm"></div>
|
<div class="chips" id="runs-flt-algo" aria-label="filter by algorithm"></div>
|
||||||
</div>
|
</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
|
<div
|
||||||
id="runs-slot"
|
id="runs-slot"
|
||||||
@ -485,7 +497,7 @@
|
|||||||
<script type="module" src="/static/dataset-picker.js?v=11"></script>
|
<script type="module" src="/static/dataset-picker.js?v=11"></script>
|
||||||
<script type="module" src="/static/metrics.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/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 type="module" src="/static/run-modal.js?v=2"></script>
|
||||||
<script>
|
<script>
|
||||||
// Anchor-links alone don't expand <details>; force it.
|
// Anchor-links alone don't expand <details>; force it.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user