[object Object]

← back to Catalog Crawl Atlas

Atlas: accumulating multi-facet filter — drilling stacks color+style+motif+material, grid shows the intersection with removable chips

bc458ad1f52b6fb1fda4675021b21c8fa9396eda · 2026-07-30 11:58:43 -0700 · Steve Abrams

Files touched

Diff

commit bc458ad1f52b6fb1fda4675021b21c8fa9396eda
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 30 11:58:43 2026 -0700

    Atlas: accumulating multi-facet filter — drilling stacks color+style+motif+material, grid shows the intersection with removable chips
---
 public/index.html | 72 ++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 48 insertions(+), 24 deletions(-)

diff --git a/public/index.html b/public/index.html
index a89b768..49bc174 100644
--- a/public/index.html
+++ b/public/index.html
@@ -72,11 +72,18 @@ svg{width:100%;height:100%;display:block}
 /* product grid */
 .products{border-top:1px solid var(--edge);background:#0a0b0e}
 .phead{position:sticky;top:0;background:linear-gradient(180deg,#14151a,#0e0f13);border-bottom:1px solid var(--edge);
-  padding:12px 22px;display:flex;align-items:center;gap:14px;z-index:3}
-.phead .swatch{width:22px;height:22px;border-radius:6px;border:1px solid rgba(255,255,255,.2)}
-.phead h3{font-family:'Cormorant Garamond',serif;font-size:22px;font-weight:600}
-.phead .meta{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--muted)}
-.phead .badge{font-size:9px;letter-spacing:1px;text-transform:uppercase;padding:2px 9px;border-radius:20px;border:1px solid var(--edge);color:var(--muted)}
+  padding:12px 22px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;z-index:3}
+.phead .lead{font-family:'Cormorant Garamond',serif;font-size:18px;color:var(--muted);margin-right:2px}
+.fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 9px 5px 8px;border-radius:22px;border:1px solid var(--edge);background:#101116;font-size:13px}
+.fchip .sw{width:14px;height:14px;border-radius:50%;border:1px solid rgba(255,255,255,.25)}
+.fchip .ty{font-size:8px;letter-spacing:1px;text-transform:uppercase;color:var(--muted)}
+.fchip b{font-weight:500}
+.fchip .x{cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 2px}
+.fchip .x:hover{color:var(--rose)}
+.fchip.color{border-color:#39d35366}.fchip.style{border-color:#5aa9e666}.fchip.motif{border-color:#b48ce666}.fchip.material{border-color:#4ecdc466}
+.phead .result{margin-left:auto;font-family:'Share Tech Mono',monospace;font-size:12px;color:var(--amber)}
+.phead .clear{cursor:pointer;font-size:11px;color:var(--muted);border:1px solid var(--edge);border-radius:16px;padding:4px 11px}
+.phead .clear:hover{color:var(--cream);border-color:var(--muted)}
 .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;padding:16px 22px 30px}
 .card{background:#121318;border:1px solid var(--edge);border-radius:12px;overflow:hidden;text-decoration:none;color:var(--cream);transition:.14s;display:flex;flex-direction:column}
 .card:hover{border-color:var(--amber);transform:translateY(-2px);box-shadow:0 8px 22px rgba(0,0,0,.6)}
@@ -115,7 +122,7 @@ svg{width:100%;height:100%;display:block}
           <span><b style="background:#4ecdc4"></b>Material</span>
         </div>
         <svg id="graph" viewBox="0 0 1000 700" preserveAspectRatio="xMidYMid meet"></svg>
-        <div class="hint">node = 1 sample product photo · click any node → re-centers the map AND opens all its products below</div>
+        <div class="hint">drill down: click a color, then a style, then a motif — each click STACKS into one filter · grid below shows items matching ALL selected</div>
       </div>
       <div class="products" id="products">
         <div class="phead" id="phead"></div>
@@ -129,7 +136,8 @@ svg{width:100%;height:100%;display:block}
 const CATCOLOR={color:'#39d353',style:'#5aa9e6',motif:'#b48ce6',material:'#4ecdc4'};
 const TYPEIDX={color:3,style:4,motif:5,material:6};
 let G=null, PROD=null, CDN='https://cdn.shopify.com', current=null, trail=[];
-let gridState={key:null,rows:[],shown:0};
+let gridState={rows:[],shown:0};
+const filter={color:null,style:null,motif:null,material:null};  // accumulated selection (one per facet)
 
 Promise.all([fetch('graph.json',{cache:'no-store'}).then(r=>r.json()),
              fetch('products.json',{cache:'no-store'}).then(r=>r.json())])
@@ -172,15 +180,16 @@ function renderGroupBtns(){
   box.querySelectorAll('button').forEach(b=>b.onclick=()=>center(b.dataset.key,true));
 }
 function center(key,reset,scroll){
-  if(!G.nodes[key])return; current=key;
+  if(!G.nodes[key])return; const node=G.nodes[key]; current=key;
   if(reset)trail=[key]; else if(trail[trail.length-1]!==key)trail.push(key);
+  // clicking a node ADDS/REPLACES that facet in the accumulating selection (toggle off if same)
+  filter[node.type] = (filter[node.type]===node.label) ? null : node.label;
   document.querySelectorAll('.sw').forEach(s=>s.classList.toggle('active',s.dataset.key===key));
-  renderCrumbs(); draw(); openGrid(key);
-  if(scroll!==false){ // reveal the grid below whenever a node lands in center
-    const ph=document.getElementById('phead');
-    if(ph) ph.scrollIntoView({behavior:'smooth',block:'start'});
-  }
+  renderCrumbs(); draw(); applyFilter();
+  if(scroll!==false){ const ph=document.getElementById('phead'); if(ph) ph.scrollIntoView({behavior:'smooth',block:'start'}); }
 }
+function removeFacet(type){ filter[type]=null; draw(); applyFilter(); }
+function clearFilter(){ Object.keys(filter).forEach(k=>filter[k]=null); applyFilter(); }
 function renderCrumbs(){
   document.getElementById('crumbs').innerHTML=trail.map((k,i)=>`<a data-i="${i}">${esc(G.nodes[k].label)}</a>`+(i<trail.length-1?'<span class="sep">›</span>':'')).join('');
   document.querySelectorAll('#crumbs a').forEach(a=>a.onclick=()=>{const i=+a.dataset.i;trail=trail.slice(0,i+1);center(trail[i],false);});
@@ -220,17 +229,32 @@ function nodeSVG(node,x,y,r,ring,defs,fs,isCenter){
     ${labels}</g>`;
 }
 
-/* ---- product grid below ---- */
-function openGrid(key){
-  const node=G.nodes[key]; const idx=TYPEIDX[node.type];
-  const rows=PROD.rows.filter(r=>r[idx]===node.label);
-  gridState={key,rows,shown:0};
-  const sw = node.type==='color' ? `<span class="swatch" style="background:${node.hex}"></span>` : `<span class="swatch" style="background:${CATCOLOR[node.type]}"></span>`;
-  document.getElementById('phead').innerHTML=`${sw}<h3>${esc(node.label)}</h3>
-    <span class="badge">${node.type}</span>
-    <span class="meta">${rows.length.toLocaleString()} products with this ${node.type}</span>`;
-  document.getElementById('grid').innerHTML='';
-  if(!rows.length){document.getElementById('grid').innerHTML='<div class="pempty">no imaged products indexed for this node</div>';return;}
+/* ---- accumulating multi-facet filter + product grid ---- */
+function colorHex(v){ const n=G.nodes['color:'+v]; return n?n.hex:'#888'; }
+function applyFilter(){
+  const active=Object.entries(filter).filter(([,v])=>v);        // [[type,value],...]
+  // intersection: product must match EVERY selected facet
+  const rows = active.length
+    ? PROD.rows.filter(r=>active.every(([type,val])=>r[TYPEIDX[type]]===val))
+    : [];
+  gridState={rows,shown:0};
+  // header: filter chips + result count
+  const chips = active.map(([type,val])=>{
+    const sw = type==='color' ? `<span class="sw" style="background:${colorHex(val)}"></span>`
+                              : `<span class="sw" style="background:${CATCOLOR[type]}"></span>`;
+    return `<span class="fchip ${type}">${sw}<span class="ty">${type}</span><b>${esc(val)}</b><span class="x" data-rm="${type}">✕</span></span>`;
+  }).join('');
+  const ph=document.getElementById('phead');
+  ph.innerHTML = active.length
+    ? `<span class="lead">Selection</span>${chips}
+       <span class="result">${rows.length.toLocaleString()} item${rows.length===1?'':'s'} match all</span>
+       <span class="clear" id="clearF">clear all</span>`
+    : `<span class="lead">Click a node to start selecting — color · style · motif · material stack into one filter</span>`;
+  ph.querySelectorAll('.x').forEach(x=>x.onclick=()=>removeFacet(x.dataset.rm));
+  const cf=document.getElementById('clearF'); if(cf) cf.onclick=clearFilter;
+  const grid=document.getElementById('grid'); grid.innerHTML='';
+  if(!active.length){ grid.innerHTML='<div class="pempty">no facets selected yet — pick a color, then drill into a style / motif to narrow</div>'; return; }
+  if(!rows.length){ grid.innerHTML='<div class="pempty">no products match all selected facets — try removing one</div>'; return; }
   renderMore();
 }
 function renderMore(){

← 319762a Atlas: auto-reveal products grid below on center; shorter gr  ·  back to Catalog Crawl Atlas  ·  Atlas refine: filter-aware neighbors (spokes show +N within 69a677b →