#HTML


<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>UpSet Plot — Interactive Viewer</title> <link rel="stylesheet" href="upset.css"> </head> <body> <header> <h1>Up<span>Set</span> Viewer</h1> <span class="badge">Interactive</span> <span class="badge" style="margin-left:auto; border-color:#ff4081; color:#ff4081;">v2.0</span> </header> <div class="layout"> <aside class="sidebar"> <div class="sidebar-section"> <h3>Sets</h3> <div id="set-list"></div> </div> <div class="sidebar-section"> <h3>Sort by</h3> <div class="sort-group"> <button class="sort-btn active" data-sort="size">↓ Intersection size</button> <button class="sort-btn" data-sort="degree">◈ Degree</button> <button class="sort-btn" data-sort="name">Aa Name</button> </div> </div> <div class="sidebar-section"> <h3>Min intersection size</h3> <div class="filter-row"> <label>Threshold <span id="min-size-val">1</span></label> <input type="range" id="min-size" min="1" max="10" value="1" step="1"> </div> </div> <div class="sidebar-section"> <h3>Degree filter</h3> <div class="degree-group" id="deg-group"></div> </div> <!-- ★ New: Boxplot mode --> <div class="sidebar-section"> <h3>Box plot mode</h3> <div class="sort-group"> <button class="box-mode-btn active" data-mode="intersection" title="One box per intersection (avg value across sets)"> ▣ Per intersection </button> <button class="box-mode-btn" data-mode="per-set" title="Select a column first, then see each set's values separately"> ◧ Per set (select column) </button> </div> <div style="font-size:0.65rem; color:var(--muted); margin-top:8px; line-height:1.5;"> 「Per set」は列をクリックして交差を選択後に有効 </div> </div> <div class="sidebar-section"> <h3>Summary</h3> <div class="stat-grid"> <div class="stat-cell"> <div class="sv" id="stat-intersections">—</div> <div class="sk">Intersections</div> </div> <div class="stat-cell"> <div class="sv" id="stat-elements">—</div> <div class="sk">Elements</div> </div> <div class="stat-cell"> <div class="sv" id="stat-sets">—</div> <div class="sk">Active Sets</div> </div> <div class="stat-cell"> <div class="sv" id="stat-maxdeg">—</div> <div class="sk">Max Degree</div> </div> </div> </div> <div class="sidebar-section"> <button class="reset-btn" id="reset-btn">⟳ Reset all filters</button> </div> </aside> <main class="main"> <div id="upset-wrap"></div> <div class="boxplot-section"> <div class="section-title"> Box Plot — <span>Value distribution per intersection / set</span> </div> <canvas id="boxplot-canvas"></canvas> </div> </main> </div> <div id="tooltip"></div> <script src="upset.js"></script> </body> </html>



#JS

/* ============================================= upset.js — UpSet Plot + BoxPlot Engine Per-set values: element[set] = value ============================================= */ class UpSetApp { constructor(data) { this.rawSets = data.sets; this.rawElems = data.elements; // [{id, label, sets:{SetName: value, ...}}] this.activesets = new Set(data.sets.map(s => s.name)); this.sortMode = 'size'; this.minSize = 1; this.activeDeg = new Set(); this.highlighted = null; this.selected = null; // Boxplot mode: 'intersection' = one box per intersection (avg value) // 'per-set' = one box per set within the selected intersection this.boxMode = 'intersection'; this._buildUI(); this._render(); } /* ─── Data helpers ─── */ // Return all set-names this element belongs to (filtered to active) _elemSets(el) { return Object.keys(el.sets).filter(s => this.activesets.has(s)); } // Value of element in a specific set _elemVal(el, setName) { return el.sets[setName]; } // Average value of element across the sets it participates in (for intersection-level stats) _elemAvgVal(el, setNames) { const vals = setNames.map(s => el.sets[s]).filter(v => v !== undefined); if (!vals.length) return null; return vals.reduce((a, b) => a + b, 0) / vals.length; } /* ─── Compute intersections ─── */ _computeIntersections() { const map = new Map(); for (const el of this.rawElems) { const inSets = this._elemSets(el); if (inSets.length === 0) continue; const key = [...inSets].sort().join('|||'); if (!map.has(key)) { map.set(key, { key, sets: [...inSets].sort(), elements: [], degree: inSets.length, }); } map.get(key).elements.push(el); } let intersections = [...map.values()].filter( ix => ix.elements.length >= this.minSize ); if (this.activeDeg.size > 0) { intersections = intersections.filter(ix => this.activeDeg.has(ix.degree)); } if (this.sortMode === 'size') { intersections.sort((a, b) => b.elements.length - a.elements.length); } else if (this.sortMode === 'degree') { intersections.sort((a, b) => a.degree - b.degree || b.elements.length - a.elements.length); } else { intersections.sort((a, b) => a.key.localeCompare(b.key)); } return intersections; } /* ─── Stats ─── */ _stats(values) { const v = values.filter(x => x !== null && x !== undefined && isFinite(x)); if (!v.length) return null; const s = [...v].sort((a, b) => a - b); const n = s.length; const q1 = s[Math.floor(n * 0.25)]; const median = n % 2 === 0 ? (s[n/2-1] + s[n/2]) / 2 : s[Math.floor(n/2)]; const q3 = s[Math.floor(n * 0.75)]; const iqr = q3 - q1; const whiskerLo = Math.min(...s.filter(x => x >= q1 - 1.5 * iqr)); const whiskerHi = Math.max(...s.filter(x => x <= q3 + 1.5 * iqr)); const outliers = s.filter(x => x < whiskerLo || x > whiskerHi); const mean = s.reduce((a, b) => a + b, 0) / n; return { min: s[0], q1, median, q3, max: s[n-1], whiskerLo, whiskerHi, outliers, mean, n }; } /* ─── Sidebar UI ─── */ _buildUI() { /* Set toggles */ const setList = document.getElementById('set-list'); setList.innerHTML = ''; for (const s of this.rawSets) { const label = document.createElement('label'); label.className = 'set-toggle'; label.innerHTML = ` <input type="checkbox" checked> <span class="swatch" style="background:${s.color}"></span> <span class="set-name">${s.name}</span> <span class="set-count">${this._setCount(s.name)}</span> `; label.querySelector('input').addEventListener('change', e => { if (e.target.checked) { this.activesets.add(s.name); label.classList.remove('off'); } else { this.activesets.delete(s.name); label.classList.add('off'); } this._render(); }); setList.appendChild(label); } /* Sort */ document.querySelectorAll('.sort-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.sortMode = btn.dataset.sort; this._render(); }); }); /* Min size slider */ const slider = document.getElementById('min-size'); const sliderVal = document.getElementById('min-size-val'); slider.addEventListener('input', () => { this.minSize = +slider.value; sliderVal.textContent = slider.value; this._render(); }); /* Degree filter */ const degGroup = document.getElementById('deg-group'); degGroup.innerHTML = ''; const allBtn = document.createElement('button'); allBtn.className = 'deg-btn active'; allBtn.textContent = 'All'; allBtn.addEventListener('click', () => { this.activeDeg.clear(); document.querySelectorAll('.deg-btn').forEach(b => b.classList.remove('active')); allBtn.classList.add('active'); this._render(); }); degGroup.appendChild(allBtn); for (let d = 1; d <= this.rawSets.length; d++) { const btn = document.createElement('button'); btn.className = 'deg-btn'; btn.textContent = d; btn.addEventListener('click', () => { allBtn.classList.remove('active'); if (this.activeDeg.has(d)) { this.activeDeg.delete(d); btn.classList.remove('active'); } else { this.activeDeg.add(d); btn.classList.add('active'); } if (this.activeDeg.size === 0) allBtn.classList.add('active'); this._render(); }); degGroup.appendChild(btn); } /* Boxplot mode toggle */ document.querySelectorAll('.box-mode-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.box-mode-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.boxMode = btn.dataset.mode; this._render(); }); }); /* Reset */ document.getElementById('reset-btn').addEventListener('click', () => { this.activesets = new Set(this.rawSets.map(s => s.name)); this.sortMode = 'size'; this.minSize = 1; this.activeDeg.clear(); this.highlighted = null; this.selected = null; this.boxMode = 'intersection'; document.querySelectorAll('.set-toggle').forEach(l => { l.classList.remove('off'); l.querySelector('input').checked = true; }); document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); document.querySelector('.sort-btn[data-sort="size"]').classList.add('active'); slider.value = 1; sliderVal.textContent = '1'; document.querySelectorAll('.deg-btn').forEach(b => b.classList.remove('active')); allBtn.classList.add('active'); document.querySelectorAll('.box-mode-btn').forEach(b => b.classList.remove('active')); document.querySelector('.box-mode-btn[data-mode="intersection"]').classList.add('active'); this._render(); }); /* Tooltip */ this.tooltip = document.getElementById('tooltip'); document.addEventListener('mousemove', e => { this.tooltip.style.left = (e.clientX + 14) + 'px'; this.tooltip.style.top = (e.clientY - 10) + 'px'; }); } _setCount(name) { return this.rawElems.filter(e => name in e.sets).length; } _setColor(name) { return this.rawSets.find(s => s.name === name)?.color || '#888'; } /* ─── Main render ─── */ _render() { const intersections = this._computeIntersections(); const activeSets = this.rawSets.filter(s => this.activesets.has(s.name)); this._renderUpSet(intersections, activeSets); this._renderBoxplot(intersections, activeSets); this._updateStats(intersections); } /* ─── UpSet matrix ─── */ _renderUpSet(intersections, activeSets) { const wrap = document.getElementById('upset-wrap'); wrap.innerHTML = ''; if (!intersections.length || !activeSets.length) { wrap.innerHTML = '<div class="empty-state">No intersections match current filters.</div>'; return; } const maxCount = Math.max(...intersections.map(ix => ix.elements.length)); const BAR_H = 180; const maxSetSize = Math.max(...activeSets.map(s => this.rawElems.filter(e => s.name in e.sets).length )); /* Top bar chart */ const barArea = document.createElement('div'); barArea.className = 'bar-area'; const yAxis = document.createElement('div'); yAxis.className = 'bar-yaxis'; yAxis.style.height = BAR_H + 'px'; for (let i = 4; i >= 0; i--) { const t = document.createElement('div'); t.className = 'tick'; t.textContent = Math.round(maxCount * i / 4); yAxis.appendChild(t); } barArea.appendChild(yAxis); const barsRow = document.createElement('div'); barsRow.className = 'bars-row'; intersections.forEach(ix => { const col = document.createElement('div'); col.className = 'bar-col' + (this.highlighted === ix.key ? ' highlighted' : ''); const h = (ix.elements.length / maxCount) * BAR_H; const inner = document.createElement('div'); inner.className = 'bar-inner'; inner.style.height = h + 'px'; const val = document.createElement('div'); val.className = 'bar-val'; val.textContent = ix.elements.length; col.appendChild(val); col.appendChild(inner); this._addColEvents(col, ix, intersections, activeSets); barsRow.appendChild(col); }); barArea.appendChild(barsRow); wrap.appendChild(barArea); const div = document.createElement('div'); div.className = 'chart-divider'; wrap.appendChild(div); /* Matrix */ const matrixArea = document.createElement('div'); matrixArea.className = 'matrix-area'; /* Set labels */ const labelCol = document.createElement('div'); labelCol.className = 'set-labels'; for (const s of activeSets) { const row = document.createElement('div'); row.className = 'set-label-row'; row.innerHTML = `<span class="swatch-sm" style="background:${s.color}"></span><span>${s.name}</span>`; labelCol.appendChild(row); } matrixArea.appendChild(labelCol); /* Dot columns */ const dotCols = document.createElement('div'); dotCols.className = 'dot-cols'; intersections.forEach(ix => { const col = document.createElement('div'); col.className = 'dot-col' + (this.highlighted === ix.key ? ' highlighted' : '') + (this.selected === ix.key ? ' selected' : ''); const onIndices = activeSets.map((s, idx) => ix.sets.includes(s.name) ? idx : -1).filter(x => x >= 0); activeSets.forEach(s => { const dot = document.createElement('div'); dot.className = 'dot ' + (ix.sets.includes(s.name) ? 'on' : 'off'); col.appendChild(dot); }); if (onIndices.length > 1) { const CELL = 52; const top = onIndices[0] * CELL + (CELL - 18) / 2 + 9; const bot = onIndices[onIndices.length - 1] * CELL + (CELL - 18) / 2 + 9; const conn = document.createElement('div'); conn.className = 'connector'; conn.style.top = top + 'px'; conn.style.height = (bot - top) + 'px'; col.appendChild(conn); } this._addColEvents(col, ix, intersections, activeSets); dotCols.appendChild(col); }); matrixArea.appendChild(dotCols); /* Set size bars */ const sizeArea = document.createElement('div'); sizeArea.className = 'size-area'; for (const s of activeSets) { const cnt = this.rawElems.filter(e => s.name in e.sets).length; const w = Math.max(4, (cnt / maxSetSize) * 100); const row = document.createElement('div'); row.className = 'size-row'; row.innerHTML = ` <div class="size-bar" style="width:${w}px; background:${s.color}"></div> <span class="size-val">${cnt}</span> `; sizeArea.appendChild(row); } matrixArea.appendChild(sizeArea); wrap.appendChild(matrixArea); } _addColEvents(col, ix, intersections, activeSets) { col.addEventListener('mouseenter', () => { this.highlighted = ix.key; this._showTooltip(ix); this._renderUpSet(intersections, activeSets); }); col.addEventListener('mouseleave', () => { this.highlighted = null; this._hideTooltip(); this._renderUpSet(intersections, activeSets); }); col.addEventListener('click', () => { this.selected = this.selected === ix.key ? null : ix.key; this._renderUpSet(intersections, activeSets); this._renderBoxplot(intersections, activeSets); }); } /* ─── Boxplot ─── */ _renderBoxplot(intersections, activeSets) { const canvas = document.getElementById('boxplot-canvas'); const ctx = canvas.getContext('2d'); const DPR = window.devicePixelRatio || 1; const W = Math.max(400, canvas.parentElement.clientWidth || 800); const H = 340; canvas.width = W * DPR; canvas.height = H * DPR; canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; ctx.scale(DPR, DPR); ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0f0f12'; ctx.fillRect(0, 0, W, H); const PAD_L = 54, PAD_R = 20, PAD_T = 36, PAD_B = 72; const plotW = W - PAD_L - PAD_R; const plotH = H - PAD_T - PAD_B; // ── Decide what to draw ── // Mode A (intersection): one box per intersection, value = avg across sets // Mode B (per-set): selected intersection → one box per set let boxes = []; // [{label, color, values}] if (this.boxMode === 'per-set' && this.selected) { // Per-set breakdown for the selected intersection const ix = intersections.find(i => i.key === this.selected); if (ix) { for (const s of ix.sets) { const vals = ix.elements.map(el => el.sets[s]).filter(v => v !== undefined); boxes.push({ label: s, color: this._setColor(s), values: vals }); } } } else { // One box per intersection (avg value across participating sets) const show = this.selected ? intersections.filter(ix => ix.key === this.selected) : intersections.slice(0, Math.min(intersections.length, 20)); for (const ix of show) { // avg value across all sets this element appears in within this intersection const vals = ix.elements.map(el => this._elemAvgVal(el, ix.sets)).filter(v => v !== null); const label = ix.sets.length <= 2 ? ix.sets.join(' ∩ ') : ix.sets.slice(0, 2).join(' ∩ ') + '…'; let color = '#00e5ff'; if (ix.sets.length === 1) color = this._setColor(ix.sets[0]); if (this.selected === ix.key) color = '#ff4081'; boxes.push({ label, color, values: vals }); } } if (!boxes.length) return; const allStats = boxes.map(b => ({ ...b, st: this._stats(b.values) })).filter(x => x.st); if (!allStats.length) return; const allVals = allStats.flatMap(x => x.values); const globalMin = Math.min(...allVals); const globalMax = Math.max(...allVals); const range = globalMax - globalMin || 1; const toY = v => PAD_T + plotH - ((v - globalMin) / range) * plotH; // Grid ctx.strokeStyle = '#2a2a38'; ctx.lineWidth = 1; ctx.setLineDash([3, 4]); ctx.textAlign = 'right'; ctx.font = '10px JetBrains Mono, monospace'; ctx.fillStyle = '#6b6b85'; for (let i = 0; i <= 5; i++) { const v = globalMin + (range * i / 5); const y = toY(v); ctx.beginPath(); ctx.moveTo(PAD_L, y); ctx.lineTo(W - PAD_R, y); ctx.stroke(); ctx.fillText(v.toFixed(1), PAD_L - 6, y + 4); } ctx.setLineDash([]); // Draw boxes const slotW = plotW / allStats.length; const BOX_W = Math.min(30, slotW * 0.45); allStats.forEach(({ label, color, st }, i) => { const cx = PAD_L + slotW * i + slotW / 2; const alpha = '88'; // Whisker line ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx, toY(st.whiskerLo)); ctx.lineTo(cx, toY(st.whiskerHi)); ctx.stroke(); // Whisker caps [[st.whiskerLo], [st.whiskerHi]].forEach(([v]) => { ctx.beginPath(); ctx.moveTo(cx - BOX_W * 0.35, toY(v)); ctx.lineTo(cx + BOX_W * 0.35, toY(v)); ctx.stroke(); }); // IQR box const boxTop = toY(st.q3); const boxH = toY(st.q1) - boxTop; ctx.fillStyle = color + alpha; ctx.fillRect(cx - BOX_W / 2, boxTop, BOX_W, boxH); ctx.strokeStyle = color; ctx.strokeRect(cx - BOX_W / 2, boxTop, BOX_W, boxH); // Median ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx - BOX_W / 2, toY(st.median)); ctx.lineTo(cx + BOX_W / 2, toY(st.median)); ctx.stroke(); // Mean dot ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(cx, toY(st.mean), 3, 0, Math.PI * 2); ctx.fill(); // Outliers ctx.fillStyle = color; st.outliers.forEach(v => { ctx.beginPath(); ctx.arc(cx, toY(v), 3, 0, Math.PI * 2); ctx.fill(); }); // X label (rotated) ctx.save(); ctx.translate(cx, H - PAD_B + 10); ctx.rotate(-Math.PI / 4); ctx.fillStyle = '#c0c0d0'; ctx.font = '9.5px JetBrains Mono, monospace'; ctx.textAlign = 'right'; ctx.fillText(label, 0, 0); ctx.restore(); // n label above box ctx.fillStyle = '#6b6b85'; ctx.font = '8px JetBrains Mono, monospace'; ctx.textAlign = 'center'; ctx.fillText(`n=${st.n}`, cx, boxTop - 4); }); // Y-axis label ctx.save(); ctx.translate(12, H / 2); ctx.rotate(-Math.PI / 2); ctx.fillStyle = '#6b6b85'; ctx.font = '10px JetBrains Mono, monospace'; ctx.textAlign = 'center'; const yLabel = (this.boxMode === 'per-set' && this.selected) ? 'Value (per set)' : 'Value (avg across sets)'; ctx.fillText(yLabel, 0, 0); ctx.restore(); // Legend ctx.fillStyle = '#6b6b85'; ctx.font = '9px JetBrains Mono, monospace'; ctx.textAlign = 'left'; const note = this.selected ? (this.boxMode === 'per-set' ? 'Per-set breakdown of selected intersection' : 'Avg value for selected intersection') : '● mean — median | Click column to isolate | Toggle mode in sidebar'; ctx.fillText(note, PAD_L, H - 8); } /* ─── Tooltip ─── */ _showTooltip(ix) { // Show per-set values for each element const label = ix.sets.join(' ∩ '); let html = `<div class="tt-title">${label}</div>`; html += `<div class="tt-row"><span class="tt-key">Count</span><span class="tt-val">${ix.elements.length}</span></div>`; html += `<div class="tt-row"><span class="tt-key">Degree</span><span class="tt-val">${ix.degree}</span></div>`; // Per-set stats for (const s of ix.sets) { const vals = ix.elements.map(el => el.sets[s]).filter(v => v !== undefined); const st = this._stats(vals); if (st) { html += `<div class="tt-sep">${s}</div>`; html += `<div class="tt-row"><span class="tt-key">Median</span><span class="tt-val" style="color:${this._setColor(s)}">${st.median.toFixed(2)}</span></div>`; html += `<div class="tt-row"><span class="tt-key">Mean</span><span class="tt-val">${st.mean.toFixed(2)}</span></div>`; } } this.tooltip.innerHTML = html; this.tooltip.classList.add('show'); } _hideTooltip() { this.tooltip.classList.remove('show'); } /* ─── Stats panel ─── */ _updateStats(intersections) { const totalElems = new Set(); intersections.forEach(ix => ix.elements.forEach(e => totalElems.add(e.id))); document.getElementById('stat-intersections').textContent = intersections.length; document.getElementById('stat-elements').textContent = totalElems.size; document.getElementById('stat-sets').textContent = this.activesets.size; const maxDeg = intersections.length ? Math.max(...intersections.map(ix => ix.degree)) : 0; document.getElementById('stat-maxdeg').textContent = maxDeg; } } /* ─── Boot ─── */ async function boot() { try { const resp = await fetch('data.json'); if (!resp.ok) throw new Error('data.json not found'); const data = await resp.json(); window._upsetApp = new UpSetApp(data); } catch (e) { document.getElementById('upset-wrap').innerHTML = `<div class="empty-state">⚠ Could not load data.json<br><small>${e.message}</small></div>`; console.error(e); } } document.addEventListener('DOMContentLoaded', boot); window.addEventListener('resize', () => { if (window._upsetApp) window._upsetApp._render(); });



#css

/* ============================================= UpSet Plot — Industrial Data-Lab Aesthetic ============================================= */ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;700&family=Syne:wght@400;600;800&display=swap'); :root { --bg: #0f0f12; --surface: #16161c; --surface2: #1e1e27; --border: #2a2a38; --accent: #00e5ff; --accent2: #ff4081; --text: #e8e8f0; --muted: #6b6b85; --dot-off: #2a2a38; --radius: 6px; --bar-h: 180px; --cell: 52px; --label-w: 110px; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'JetBrains Mono', monospace; background: var(--bg); color: var(--text); min-height: 100vh; overflow-x: auto; } /* ─── Header ─── */ header { display: flex; align-items: center; gap: 18px; padding: 20px 32px; border-bottom: 1px solid var(--border); background: var(--surface); position: sticky; top: 0; z-index: 100; } header h1 { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 1.35rem; letter-spacing: -0.02em; color: var(--text); } header h1 span { color: var(--accent); } .badge { font-size: 0.62rem; letter-spacing: 0.12em; padding: 3px 8px; border: 1px solid var(--accent); color: var(--accent); border-radius: 2px; text-transform: uppercase; } /* ─── Layout ─── */ .layout { display: flex; gap: 0; min-height: calc(100vh - 61px); } /* ─── Sidebar ─── */ .sidebar { width: 260px; min-width: 220px; background: var(--surface); border-right: 1px solid var(--border); padding: 20px 16px; display: flex; flex-direction: column; gap: 24px; flex-shrink: 0; } .sidebar-section h3 { font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border); } /* Set toggles */ .set-toggle { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--radius); cursor: pointer; transition: background 0.15s; user-select: none; } .set-toggle:hover { background: var(--surface2); } .set-toggle input { display: none; } .set-toggle .swatch { width: 12px; height: 12px; border-radius: 2px; flex-shrink: 0; transition: opacity 0.2s; } .set-toggle.off .swatch { opacity: 0.25; } .set-toggle .set-name { font-size: 0.82rem; flex: 1; transition: color 0.2s; } .set-toggle.off .set-name { color: var(--muted); } .set-count { font-size: 0.72rem; color: var(--muted); } /* Sort / generic button group */ .sort-group { display: flex; flex-direction: column; gap: 6px; } .sort-btn, .box-mode-btn { background: none; border: 1px solid var(--border); color: var(--muted); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; padding: 6px 10px; border-radius: var(--radius); cursor: pointer; text-align: left; transition: all 0.15s; } .sort-btn:hover, .box-mode-btn:hover { border-color: var(--accent); color: var(--accent); } .sort-btn.active, .box-mode-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(0,229,255,0.06); } /* Min size filter */ .filter-row { display: flex; flex-direction: column; gap: 8px; } .filter-row label { font-size: 0.72rem; color: var(--muted); display: flex; justify-content: space-between; } .filter-row label span { color: var(--accent); } input[type="range"] { -webkit-appearance: none; width: 100%; height: 3px; background: var(--border); border-radius: 2px; outline: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: 2px solid var(--bg); } /* Degree filter */ .degree-group { display: flex; flex-wrap: wrap; gap: 5px; } .deg-btn { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; padding: 4px 9px; border-radius: var(--radius); border: 1px solid var(--border); background: none; color: var(--muted); cursor: pointer; transition: all 0.15s; } .deg-btn:hover { border-color: var(--accent2); color: var(--accent2); } .deg-btn.active { border-color: var(--accent2); color: var(--accent2); background: rgba(255,64,129,0.08); } /* Stats panel */ .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .stat-cell { background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 8px; text-align: center; } .stat-cell .sv { font-size: 1.1rem; font-weight: 600; color: var(--accent); } .stat-cell .sk { font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; } /* Reset btn */ .reset-btn { background: none; border: 1px solid var(--border); color: var(--muted); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; padding: 8px; border-radius: var(--radius); cursor: pointer; text-align: center; transition: all 0.15s; width: 100%; } .reset-btn:hover { border-color: var(--accent2); color: var(--accent2); } /* ─── Main ─── */ .main { flex: 1; padding: 28px 32px; display: flex; flex-direction: column; gap: 0; overflow-x: auto; } /* ─── UpSet chart ─── */ #upset-wrap { display: inline-flex; flex-direction: column; min-width: 600px; } /* Bar chart */ .bar-area { display: flex; align-items: flex-end; gap: 0; } .bar-yaxis { width: var(--label-w); flex-shrink: 0; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; padding-right: 8px; } .bar-yaxis .tick { font-size: 0.62rem; color: var(--muted); line-height: 1; } .bars-row { display: flex; align-items: flex-end; gap: 6px; } .bar-col { width: var(--cell); display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: pointer; position: relative; } .bar-col .bar-val { font-size: 0.65rem; color: var(--muted); position: absolute; top: -16px; white-space: nowrap; } .bar-col .bar-inner { width: 32px; background: var(--accent); border-radius: 3px 3px 0 0; transition: background 0.15s; min-height: 4px; } .bar-col.highlighted .bar-inner, .bar-col:hover .bar-inner { background: var(--accent2); } .bar-col.selected .bar-inner { background: var(--accent2) !important; } /* Divider */ .chart-divider { height: 1px; background: var(--border); width: 100%; } /* Matrix */ .matrix-area { display: flex; } .set-labels { width: var(--label-w); flex-shrink: 0; display: flex; flex-direction: column; } .set-label-row { height: var(--cell); display: flex; align-items: center; gap: 8px; padding-right: 8px; justify-content: flex-end; } .set-label-row .swatch-sm { width: 8px; height: 8px; border-radius: 1px; flex-shrink: 0; } .set-label-row span { font-size: 0.75rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 75px; text-align: right; } .dot-cols { display: flex; gap: 6px; } .dot-col { width: var(--cell); display: flex; flex-direction: column; align-items: center; cursor: pointer; position: relative; } .dot-col::before { content: ''; position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%); width: 1px; background: var(--border); z-index: 0; } .dot-col.highlighted::before, .dot-col.selected::before { background: rgba(255,64,129,0.3); } .dot { width: 18px; height: 18px; border-radius: 50%; margin: calc((var(--cell) - 18px)/2) 0; flex-shrink: 0; transition: background 0.15s, transform 0.15s; position: relative; z-index: 1; } .dot.off { background: var(--dot-off); } .dot.on { background: var(--accent); } .dot-col.highlighted .dot.on, .dot-col.selected .dot.on { background: var(--accent2); } .dot-col:hover .dot.on { transform: scale(1.25); } .dot-col .connector { position: absolute; left: 50%; transform: translateX(-50%); width: 3px; background: var(--accent); z-index: 0; border-radius: 2px; transition: background 0.15s; } .dot-col.highlighted .connector, .dot-col.selected .connector { background: var(--accent2); } /* Set size bars */ .size-area { display: flex; flex-direction: column; } .size-row { height: var(--cell); display: flex; align-items: center; padding-left: 12px; gap: 6px; } .size-bar { height: 10px; border-radius: 2px; background: var(--accent); min-width: 3px; } .size-val { font-size: 0.65rem; color: var(--muted); white-space: nowrap; } /* ─── Boxplot section ─── */ .boxplot-section { margin-top: 40px; padding-top: 28px; border-top: 1px solid var(--border); } .section-title { font-family: 'Syne', sans-serif; font-weight: 600; font-size: 0.85rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 20px; } .section-title span { color: var(--accent); } #boxplot-canvas { display: block; width: 100%; max-width: 900px; } /* ─── Tooltip ─── */ #tooltip { position: fixed; background: var(--surface2); border: 1px solid var(--accent); border-radius: var(--radius); padding: 10px 14px; font-size: 0.72rem; line-height: 1.7; pointer-events: none; opacity: 0; transition: opacity 0.15s; z-index: 999; max-width: 240px; color: var(--text); } #tooltip.show { opacity: 1; } #tooltip .tt-title { font-weight: 600; color: var(--accent); margin-bottom: 4px; font-size: 0.78rem; } #tooltip .tt-row { display: flex; justify-content: space-between; gap: 12px; } #tooltip .tt-key { color: var(--muted); } #tooltip .tt-val { color: var(--text); font-weight: 600; } #tooltip .tt-sep { font-size: 0.65rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-top: 6px; padding-top: 4px; border-top: 1px solid var(--border); } /* ─── Empty ─── */ .empty-state { padding: 60px 20px; text-align: center; color: var(--muted); font-size: 0.8rem; } /* ─── Scrollbar ─── */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: var(--bg); } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--muted); }



# json

{ "sets": [ { "name": "Set A", "color": "#e74c3c" }, { "name": "Set B", "color": "#3498db" }, { "name": "Set C", "color": "#2ecc71" }, { "name": "Set D", "color": "#f39c12" }, { "name": "Set E", "color": "#9b59b6" } ], "elements": [ { "id": 1, "label": "Elem 1", "sets": { "Set A": 12.3 } }, { "id": 2, "label": "Elem 2", "sets": { "Set A": 15.1 } }, { "id": 3, "label": "Elem 3", "sets": { "Set A": 9.8 } }, { "id": 4, "label": "Elem 4", "sets": { "Set A": 18.4 } }, { "id": 5, "label": "Elem 5", "sets": { "Set A": 11.0 } }, { "id": 6, "label": "Elem 6", "sets": { "Set B": 22.5 } }, { "id": 7, "label": "Elem 7", "sets": { "Set B": 19.3 } }, { "id": 8, "label": "Elem 8", "sets": { "Set B": 25.0 } }, { "id": 9, "label": "Elem 9", "sets": { "Set B": 17.8 } }, { "id": 10, "label": "Elem 10", "sets": { "Set C": 8.2 } }, { "id": 11, "label": "Elem 11", "sets": { "Set C": 6.5 } }, { "id": 12, "label": "Elem 12", "sets": { "Set C": 10.1 } }, { "id": 13, "label": "Elem 13", "sets": { "Set D": 30.2 } }, { "id": 14, "label": "Elem 14", "sets": { "Set D": 28.7 } }, { "id": 15, "label": "Elem 15", "sets": { "Set D": 33.5 } }, { "id": 16, "label": "Elem 16", "sets": { "Set E": 5.5 } }, { "id": 17, "label": "Elem 17", "sets": { "Set E": 7.2 } }, { "id": 18, "label": "Elem 18", "sets": { "Set A": 14.0, "Set B": 8.5 } }, { "id": 19, "label": "Elem 19", "sets": { "Set A": 16.3, "Set B": 11.2 } }, { "id": 20, "label": "Elem 20", "sets": { "Set A": 13.5, "Set B": 9.0 } }, { "id": 21, "label": "Elem 21", "sets": { "Set A": 10.5, "Set C": 4.2 } }, { "id": 22, "label": "Elem 22", "sets": { "Set A": 12.8, "Set C": 5.9 } }, { "id": 23, "label": "Elem 23", "sets": { "Set A": 25.4, "Set D": 31.0 } }, { "id": 24, "label": "Elem 24", "sets": { "Set A": 27.1, "Set D": 29.4 } }, { "id": 25, "label": "Elem 25", "sets": { "Set B": 18.9, "Set C": 7.3 } }, { "id": 26, "label": "Elem 26", "sets": { "Set B": 20.3, "Set C": 8.8 } }, { "id": 27, "label": "Elem 27", "sets": { "Set B": 26.7, "Set D": 32.1 } }, { "id": 28, "label": "Elem 28", "sets": { "Set C": 7.8, "Set E": 3.1 } }, { "id": 29, "label": "Elem 29", "sets": { "Set D": 29.5, "Set E": 6.4 } }, { "id": 30, "label": "Elem 30", "sets": { "Set A": 13.2, "Set B": 7.8, "Set C": 3.5 } }, { "id": 31, "label": "Elem 31", "sets": { "Set A": 15.7, "Set B": 9.3, "Set C": 4.1 } }, { "id": 32, "label": "Elem 32", "sets": { "Set A": 22.1, "Set B": 14.5, "Set D": 28.9 } }, { "id": 33, "label": "Elem 33", "sets": { "Set A": 20.8, "Set C": 6.2, "Set D": 27.3 } }, { "id": 34, "label": "Elem 34", "sets": { "Set B": 16.4, "Set C": 5.7, "Set E": 2.9 } }, { "id": 35, "label": "Elem 35", "sets": { "Set A": 19.5, "Set B": 12.1, "Set C": 4.8, "Set D": 26.0 } }, { "id": 36, "label": "Elem 36", "sets": { "Set A": 21.3, "Set B": 13.7, "Set C": 5.5, "Set D": 24.8 } }, { "id": 37, "label": "Elem 37", "sets": { "Set A": 18.0, "Set B": 11.5, "Set C": 4.3, "Set D": 25.5, "Set E": 3.8 } } ] }