From ca59516f265946de35854bbeac6a5e042657d5e6 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Tue, 21 Apr 2026 21:07:33 -0600 Subject: [PATCH] multiselect chips for dataset/algo filters --- app/web/static/metrics.js | 88 +++++++++++++++++++++++++--------- app/web/static/style.css | 80 ++++++++++++++++++++----------- app/web/templates/index.html | 1 - app/web/templates/metrics.html | 12 ++--- 4 files changed, 122 insertions(+), 59 deletions(-) diff --git a/app/web/static/metrics.js b/app/web/static/metrics.js index ae5dd77..4a79e65 100644 --- a/app/web/static/metrics.js +++ b/app/web/static/metrics.js @@ -20,8 +20,13 @@ const PALETTE = [ const state = { raw: [], - dataset: "all", - algorithm: "all", + // Multi-select: membership = "this value is currently enabled". Empty + // set = nothing shows (matches "none" toggle). Both default to all + // values on load so the initial view matches what a user expects. + datasets: new Set(), + algorithms: new Set(), + datasetsAll: [], + algorithmsAll: [], stat: "mean", // Runs the user has clicked in the legend to isolate. Empty = show all. selected: new Set(), @@ -55,28 +60,67 @@ function populateFilters() { datasets.add(shortGen(r.meta?.generator_path)); algos.add(shortEmb(r.meta?.embedder)); } - const dsSel = document.getElementById("flt-dataset"); - const algoSel = document.getElementById("flt-algo"); - for (const d of [...datasets].filter(Boolean).sort()) { - const o = document.createElement("option"); - o.value = d; o.textContent = d; - dsSel.appendChild(o); - } - for (const a of [...algos].filter(Boolean).sort()) { - const o = document.createElement("option"); - o.value = a; o.textContent = a; - algoSel.appendChild(o); - } + state.datasetsAll = [...datasets].filter(Boolean).sort(); + state.algorithmsAll = [...algos].filter(Boolean).sort(); + // Default to everything enabled. + state.datasets = new Set(state.datasetsAll); + state.algorithms = new Set(state.algorithmsAll); + renderChips(); document.getElementById("total-count").textContent = state.raw.length; } +function renderChips() { + paintChips("flt-dataset", state.datasetsAll, state.datasets); + paintChips("flt-algo", state.algorithmsAll, state.algorithms); +} + +function paintChips(containerId, values, selectedSet) { + const el = document.getElementById(containerId); + el.innerHTML = ""; + + for (const v of values) { + const b = document.createElement("button"); + b.type = "button"; + b.className = "chip" + (selectedSet.has(v) ? " is-on" : ""); + b.setAttribute("aria-pressed", selectedSet.has(v) ? "true" : "false"); + b.dataset.value = v; + b.dataset.role = "value"; + b.textContent = v; + el.appendChild(b); + } + + 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; + el.appendChild(b); + } +} + function wireEvents() { - document.getElementById("flt-dataset").addEventListener("change", (e) => { - state.dataset = e.target.value; render(); - }); - document.getElementById("flt-algo").addEventListener("change", (e) => { - state.algorithm = e.target.value; render(); - }); + const bindChipRow = (containerId, selectedSet, allList) => { + document.getElementById(containerId).addEventListener("click", (e) => { + const btn = e.target.closest(".chip"); + if (!btn) return; + const role = btn.dataset.role; + if (role === "value") { + const v = btn.dataset.value; + if (selectedSet.has(v)) selectedSet.delete(v); + else selectedSet.add(v); + } else if (role === "all") { + for (const v of allList) selectedSet.add(v); + } else if (role === "none") { + selectedSet.clear(); + } + renderChips(); + render(); + }); + }; + bindChipRow("flt-dataset", state.datasets, state.datasetsAll); + bindChipRow("flt-algo", state.algorithms, state.algorithmsAll); + document.querySelectorAll('input[name="stat"]').forEach((el) => { el.addEventListener("change", (e) => { if (e.target.checked) { state.stat = e.target.value; render(); } @@ -86,8 +130,8 @@ function wireEvents() { function matchesFilters(r) { const m = r.meta || {}; - if (state.dataset !== "all" && shortGen(m.generator_path) !== state.dataset) return false; - if (state.algorithm !== "all" && shortEmb(m.embedder) !== state.algorithm) return false; + if (!state.datasets.has(shortGen(m.generator_path))) return false; + if (!state.algorithms.has(shortEmb(m.embedder))) return false; return true; } diff --git a/app/web/static/style.css b/app/web/static/style.css index 1357461..5f48823 100644 --- a/app/web/static/style.css +++ b/app/web/static/style.css @@ -896,15 +896,15 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c } .filter-bar { - display: grid; - grid-template-columns: auto auto 1fr auto; - align-items: end; - gap: 1.4rem 1.8rem; + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 1.1rem 2rem; padding: 0.9rem 0 1rem; border-bottom: 1px solid var(--rule); margin-bottom: 1.4rem; } -.filter-group { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; } +.filter-group { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; } .filter-group .ctl-label { font-family: var(--mono); font-size: 0.68rem; @@ -912,22 +912,49 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c text-transform: uppercase; color: var(--mute); } -.filter-group select { - font-family: var(--mono); - font-size: 0.85rem; - color: var(--ink); - background: transparent; - border: 0; - border-bottom: 1px solid var(--rule-2); - padding: 3px 18px 4px 2px; - outline: none; - min-width: 12ch; +.filter-group.stat-group { min-width: 18rem; } + +.filter-group .chips { + display: flex; + flex-wrap: wrap; + gap: 0.3rem 0.35rem; + align-items: center; } -.filter-group select:focus { - border-bottom-color: var(--accent); +.chip { + font-family: var(--mono); + font-size: 0.75rem; + color: var(--mute); + background: transparent; + border: 1px solid var(--rule-2); + border-radius: 2px; + padding: 0.22rem 0.55rem; + cursor: pointer; + transition: background 120ms ease, border-color 120ms ease, color 120ms ease; + user-select: none; + letter-spacing: 0.02em; + font-variant-numeric: tabular-nums; +} +.chip:hover { color: var(--ink); } +.chip.is-on { + color: var(--accent); + border-color: var(--accent); background: var(--accent-tint); } -.filter-group.stat-group { min-width: 18rem; } +.chip:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; } +.chip-meta { + color: var(--faint); + font-family: var(--serif); + font-style: italic; + font-size: 0.76rem; + letter-spacing: 0; + border-style: dashed; + margin-left: 0.25rem; +} +.chip-meta:hover { + color: var(--accent); + border-color: var(--accent); + background: transparent; +} .filter-group .segmented.count-4 { display: grid; grid-template-columns: repeat(4, 1fr); @@ -965,8 +992,10 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c font-family: var(--mono); font-size: 0.8rem; color: var(--ink); - text-align: right; font-variant-numeric: tabular-nums; + margin-left: auto; + align-self: center; + padding-top: 0.3rem; } .filter-count .muted { color: var(--mute); } @@ -1122,14 +1151,9 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c @media (max-width: 780px) { .metrics-page { padding: 1rem 1.1rem 1.4rem; } - .filter-bar { - grid-template-columns: 1fr 1fr; - gap: 0.9rem 1.1rem; - } - .filter-group.stat-group { grid-column: 1 / -1; min-width: 0; } - .filter-count { grid-column: 1 / -1; text-align: left; } - .legend .legend-row { - grid-template-columns: 10px 1fr; - } + .filter-bar { gap: 0.9rem 1.2rem; } + .filter-group.stat-group { width: 100%; min-width: 0; } + .filter-count { margin-left: 0; text-align: left; padding-top: 0; } + .legend .legend-row { grid-template-columns: 10px 1fr; } .legend .fn { display: none; } } diff --git a/app/web/templates/index.html b/app/web/templates/index.html index b9ab784..78f435d 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -183,7 +183,6 @@ diff --git a/app/web/templates/metrics.html b/app/web/templates/metrics.html index 91ce8b4..fa4042a 100644 --- a/app/web/templates/metrics.html +++ b/app/web/templates/metrics.html @@ -4,7 +4,7 @@ metrics — embedding notebook - + @@ -24,16 +24,12 @@
dataset - +
algorithm - +
@@ -89,7 +85,7 @@ web · metrics · port 8001 - +