Download source/src/AtlasMap.tsx from CladeTeam/ouroboros: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/spaces/CladeTeam/ouroboros/resolve/main/source/src/AtlasMap.tsx
- Command line
-
hf download hf://spaces/CladeTeam/ouroboros/source/src/AtlasMap.tsx
-
curl -L -o AtlasMap.tsx https://huggingface.co/spaces/CladeTeam/ouroboros/resolve/main/source/src/AtlasMap.tsx
18.6 kB
| import {useEffect, useMemo, useRef, useState} from 'react'; | |
| import {useMotion} from './Motion'; | |
| export type AtlasPoints = { | |
| ids:string[]; xy:number[][]; reference:boolean[]; lengths:number[]; | |
| clusters:number[]; colors:string[]; rotation_degrees:number; display_bounds?:number[][]; | |
| }; | |
| export type AtlasLandmark = {uniref_id:string; title:string; thumbnail:string; number:number}; | |
| type View = {zoom:number; x:number; y:number}; | |
| const gridSize=100; | |
| const reducedMotion=()=>window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| function normalize(data:AtlasPoints) { | |
| const angle=data.rotation_degrees*Math.PI/180,c=Math.cos(angle),s=Math.sin(angle); | |
| const xy=data.xy.map(([x,y])=>[x*c-y*s,x*s+y*c]); | |
| let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity; | |
| for(const [x,y] of xy){minX=Math.min(minX,x);maxX=Math.max(maxX,x);minY=Math.min(minY,y);maxY=Math.max(maxY,y);} | |
| if(data.display_bounds){[[minX,minY],[maxX,maxY]]=data.display_bounds;} | |
| const cx=(minX+maxX)/2,cy=(minY+maxY)/2,span=Math.max(maxX-minX,maxY-minY)||1; | |
| const vertices=new Float32Array(data.ids.length*6),grid=new Map<string,number[]>(); | |
| xy.forEach(([x,y],i)=>{ | |
| const nx=(x-cx)/span*1.72,ny=(y-cy)/span*1.72,color=data.colors[data.clusters[i]]; | |
| vertices.set([nx,ny,data.reference[i]?1:0,...[1,3,5].map(j=>parseInt(color.slice(j,j+2),16)/255)],i*6); | |
| const key=`${Math.floor(nx*gridSize)},${Math.floor(ny*gridSize)}`; | |
| const cell=grid.get(key);if(cell)cell.push(i);else grid.set(key,[i]); | |
| }); | |
| return {vertices,grid}; | |
| } | |
| export function AtlasMap({data,selectedId,onSelect,featured=[],preview=false}:{data:AtlasPoints;selectedId?:string|null;onSelect:(id:string)=>void;featured?:AtlasLandmark[];preview?:boolean}) { | |
| const canvas=useRef<HTMLCanvasElement>(null),host=useRef<HTMLDivElement>(null); | |
| const norm=useMemo(()=>normalize(data),[data]); | |
| const idIndex=useMemo(()=>new Map(data.ids.map((id,i)=>[id,i])),[data]); | |
| const [size,setSize]=useState({width:800,height:540}),[view,setView]=useState<View>({zoom:1,x:0,y:0}); | |
| const [layer,setLayer]=useState<'communities'|'reference'|'descriptions'>(preview?'reference':'communities'); | |
| const {enabled,reduced}=useMotion(); | |
| const [inView,setInView]=useState(false); | |
| const annotation=useRef(0); | |
| const [hover,setHover]=useState<{index:number;x:number;y:number}|null>(null); | |
| const [error,setError]=useState(''),[ready,setReady]=useState(false); | |
| const renderer=useRef<{gl:WebGLRenderingContext;program:WebGLProgram;locations:Record<string,WebGLUniformLocation|null>}|null>(null); | |
| const drag=useRef<{id:number;x:number;y:number;start:View;distance:number}|null>(null); | |
| const pointers=useRef(new Map<number,{x:number;y:number}>()); | |
| const pinch=useRef<{distance:number;cx:number;cy:number;view:View}|null>(null); | |
| const animation=useRef(0),fade=useRef(1),interacted=useRef(false); | |
| const liveView=useRef(view),liveSize=useRef(size),liveLayer=useRef(layer); | |
| liveView.current=view;liveSize.current=size;liveLayer.current=layer; | |
| const selectRef=useRef(onSelect);selectRef.current=onSelect; | |
| const aspect=size.width/size.height; | |
| const resetView=()=>({zoom:1.22*Math.min(1,liveSize.current.width/liveSize.current.height),x:0,y:0}); | |
| function stopAnimation(){cancelAnimationFrame(animation.current);interacted.current=true;fade.current=1;} | |
| function draw(){ | |
| const r=renderer.current,c=canvas.current;if(!r||!c)return; | |
| const {gl,program,locations:l}=r,s=liveSize.current,v=liveView.current; | |
| const dpr=Math.min(window.devicePixelRatio||1,2),w=Math.round(s.width*dpr),h=Math.round(s.height*dpr); | |
| if(c.width!==w||c.height!==h){c.width=w;c.height=h;} | |
| gl.viewport(0,0,w,h);gl.clearColor(.988,.984,.969,1);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(program); | |
| gl.uniform2f(l.scale,v.zoom/(s.width/s.height),v.zoom);gl.uniform2f(l.pan,v.x,v.y); | |
| const pointSize=Math.min(6,(preview?2.35:2.1)+Math.log2(Math.max(1,v.zoom))*.7)*dpr; | |
| gl.uniform1f(l.pointSize,pointSize); | |
| gl.uniform1f(l.layer,liveLayer.current==='reference'?1:liveLayer.current==='descriptions'?2:0); | |
| gl.uniform1f(l.annotation,annotation.current);c.dataset.annotationProgress=annotation.current.toFixed(4);gl.uniform1f(l.alpha,fade.current); | |
| // A soft underlay uses the same measured points; no invented contour boundaries. | |
| gl.uniform1f(l.pass,0); | |
| if(liveLayer.current==='communities'){gl.uniform1f(l.pointSize,pointSize*2.7);gl.uniform1f(l.alpha,fade.current*.045);gl.drawArrays(gl.POINTS,0,data.ids.length);} | |
| gl.uniform1f(l.pointSize,pointSize);gl.uniform1f(l.alpha,fade.current); | |
| // Reference points draw last in the coverage view so they remain visible. | |
| gl.uniform1f(l.pass,0);gl.drawArrays(gl.POINTS,0,data.ids.length); | |
| if(liveLayer.current==='reference'){gl.uniform1f(l.pass,1);gl.drawArrays(gl.POINTS,0,data.ids.length);} | |
| } | |
| useEffect(()=>{ | |
| const ro=new ResizeObserver(entries=>{const box=entries[0].contentRect;setSize({width:Math.max(1,box.width),height:Math.max(1,box.height)});});ro.observe(host.current!); | |
| return()=>ro.disconnect(); | |
| },[]); | |
| useEffect(()=>{ | |
| const c=canvas.current!,gl=c.getContext('webgl',{alpha:false,antialias:false}); | |
| if(!gl){setError('This browser cannot display the interactive map. Search by ID or choose a featured protein below.');return;} | |
| let program:WebGLProgram|null=null,buffer:WebGLBuffer|null=null;const shaders:WebGLShader[]=[]; | |
| function shader(type:number,source:string){const s=gl!.createShader(type);if(!s)throw new Error();shaders.push(s);gl!.shaderSource(s,source);gl!.compileShader(s);if(!gl!.getShaderParameter(s,gl!.COMPILE_STATUS))throw new Error(gl!.getShaderInfoLog(s)||'Shader compilation failed');return s;} | |
| const lost=(e:Event)=>{e.preventDefault();cancelAnimationFrame(animation.current);setError('Map graphics were interrupted. Reload to restore the map; ID search and featured proteins remain available.');}; | |
| c.addEventListener('webglcontextlost',lost); | |
| try { | |
| program=gl.createProgram();buffer=gl.createBuffer();if(!program||!buffer)throw new Error(); | |
| gl.attachShader(program,shader(gl.VERTEX_SHADER,`attribute vec3 point;attribute vec3 color;uniform vec2 scale;uniform vec2 pan;uniform float pointSize;uniform mediump float layer;varying mediump float reference;varying mediump vec3 tint;varying mediump float positionX;void main(){positionX=point.x;gl_Position=vec4(point.xy*scale+pan,0.,1.);gl_PointSize=pointSize+(point.z>.5?min(layer,1.)*2.:0.);reference=point.z;tint=color;}`)); | |
| gl.attachShader(program,shader(gl.FRAGMENT_SHADER,`precision mediump float; | |
| varying mediump float reference;varying mediump vec3 tint;varying mediump float positionX; | |
| uniform mediump float layer;uniform float alpha;uniform float pass;uniform float annotation; | |
| void main(){ | |
| float d=length(gl_PointCoord-vec2(.5));if(d>.5)discard; | |
| bool referenceView=layer>.5&&layer<1.5; | |
| if(referenceView&&((pass<.5&&reference>.5)||(pass>.5&&reference<.5)))discard; | |
| vec3 color=tint;float opacity=.67; | |
| if(referenceView){color=reference>.5?vec3(.85,.45,.13):vec3(.65,.69,.70);opacity=reference>.5?.96:.23;} | |
| if(layer>1.5){ | |
| float reveal=smoothstep(0.,.12,annotation-(positionX*.45+.5)*.88); | |
| color=reference>.5?vec3(.85,.45,.13):mix(vec3(.65,.69,.70),vec3(.22,.49,.75),reveal); | |
| opacity=reference>.5?.96:mix(.18,.82,reveal); | |
| } | |
| gl_FragColor=vec4(color,alpha*opacity*(1.-smoothstep(.28,.5,d))); | |
| }`)); | |
| gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw new Error(gl.getProgramInfoLog(program)||'Map linking failed'); | |
| gl.useProgram(program);gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,norm.vertices,gl.STATIC_DRAW); | |
| for(const [name,offset] of [['point',0],['color',12]] as const){const a=gl.getAttribLocation(program,name);gl.enableVertexAttribArray(a);gl.vertexAttribPointer(a,3,gl.FLOAT,false,24,offset);} | |
| gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); | |
| renderer.current={gl,program,locations:Object.fromEntries(['scale','pan','pointSize','layer','alpha','pass','annotation'].map(n=>[n,gl.getUniformLocation(program!,n)]))}; | |
| setReady(true);fade.current=reducedMotion()?1:0; | |
| const start=performance.now();const reveal=(now:number)=>{fade.current=interacted.current?1:Math.min(1,(now-start)/600);draw();if(fade.current<1)animation.current=requestAnimationFrame(reveal);};animation.current=requestAnimationFrame(reveal); | |
| }catch(error){console.error('Atlas renderer:',error);gl.clearColor(.988,.984,.969,1);gl.clear(gl.COLOR_BUFFER_BIT);setError('The map could not be drawn. ID search and featured proteins are still available.');} | |
| return()=>{cancelAnimationFrame(animation.current);c.removeEventListener('webglcontextlost',lost);renderer.current=null;if(buffer)gl.deleteBuffer(buffer);if(program)gl.deleteProgram(program);shaders.forEach(s=>gl.deleteShader(s));}; | |
| },[norm]); | |
| useEffect(()=>{if(preview||(!interacted.current&&!selectedId))setView(resetView());},[size.width,size.height,preview]); | |
| useEffect(()=>{draw();},[view,size,layer,ready]); | |
| useEffect(()=>{ | |
| if(preview||!selectedId||!ready)return; | |
| const index=idIndex.get(selectedId);if(index===undefined)return; | |
| stopAnimation();const v=liveView.current,a=liveSize.current.width/liveSize.current.height; | |
| const zoom=Math.max(Math.min(2.1,a*2.1),v.zoom),target={zoom,x:-norm.vertices[index*6]*zoom/a,y:-norm.vertices[index*6+1]*zoom}; | |
| if(reducedMotion()){setView(target);return;} | |
| const start=performance.now();const move=(now:number)=>{const t=Math.min(1,(now-start)/480),e=1-Math.pow(1-t,3);setView({zoom:v.zoom+(target.zoom-v.zoom)*e,x:v.x+(target.x-v.x)*e,y:v.y+(target.y-v.y)*e});if(t<1)animation.current=requestAnimationFrame(move);};animation.current=requestAnimationFrame(move); | |
| return()=>cancelAnimationFrame(animation.current); | |
| },[selectedId,ready,idIndex,norm,preview]); | |
| useEffect(()=>{ | |
| const observer=new IntersectionObserver(([entry])=>setInView(entry.isIntersecting)); | |
| if(host.current)observer.observe(host.current); | |
| return()=>observer.disconnect(); | |
| },[]); | |
| useEffect(()=>{ | |
| if(!ready)return; | |
| if(layer!=='descriptions'){annotation.current=0;draw();return;} | |
| if(reduced){annotation.current=1;draw();return;} | |
| if(!enabled||!inView)return; | |
| let frame=0; | |
| const from=annotation.current,start=performance.now(); | |
| const tick=(now:number)=>{annotation.current=Math.min(1,from+(now-start)/1400);draw();if(annotation.current<1)frame=requestAnimationFrame(tick);}; | |
| frame=requestAnimationFrame(tick); | |
| return()=>cancelAnimationFrame(frame); | |
| },[layer,ready,enabled,reduced,inView]); | |
| function nearest(x:number,y:number){ | |
| const v=liveView.current,s=liveSize.current,a=s.width/s.height,nx=((x/s.width*2-1)-v.x)*a/v.zoom,ny=((1-y/s.height*2)-v.y)/v.zoom; | |
| const radius=18/s.height/v.zoom;let best=-1,distance=radius*radius; | |
| for(let gx=Math.floor((nx-radius)*gridSize);gx<=Math.floor((nx+radius)*gridSize);gx++)for(let gy=Math.floor((ny-radius)*gridSize);gy<=Math.floor((ny+radius)*gridSize);gy++)for(const i of norm.grid.get(`${gx},${gy}`)||[]){const dx=norm.vertices[i*6]-nx,dy=norm.vertices[i*6+1]-ny,d=dx*dx+dy*dy;if(d<distance){distance=d;best=i;}} | |
| return best; | |
| } | |
| function zoomBy(factor:number,cx=0,cy=0){stopAnimation();setView(v=>{const z=Math.max(.25,Math.min(24,v.zoom*factor)),ratio=z/v.zoom;return {zoom:z,x:cx-(cx-v.x)*ratio,y:cy-(cy-v.y)*ratio};});setHover(null);} | |
| useEffect(()=>{ | |
| const c=canvas.current!;const wheel=(e:WheelEvent)=>{if(!e.ctrlKey&&document.activeElement!==c)return;e.preventDefault();const rect=c.getBoundingClientRect();zoomBy(Math.exp(-e.deltaY*.002),(e.clientX-rect.left)/rect.width*2-1,1-(e.clientY-rect.top)/rect.height*2);};c.addEventListener('wheel',wheel,{passive:false});return()=>c.removeEventListener('wheel',wheel); | |
| },[]); | |
| const markers=useMemo(()=>{ | |
| const ids=[...new Set([...featured.map(c=>c.uniref_id),...(selectedId?[selectedId]:[])])]; | |
| const placed:{id:string;left:number;top:number;x:number;y:number;number:number|string;title:string}[]=[]; | |
| for(const id of ids){ | |
| const i=idIndex.get(id);if(i===undefined)continue; | |
| const x=(norm.vertices[i*6]*view.zoom/aspect+view.x+1)/2*size.width,y=(1-norm.vertices[i*6+1]*view.zoom-view.y)/2*size.height; | |
| if(x<0||x>size.width||y<0||y>size.height)continue; | |
| let left=x,top=y; | |
| for(let step=0;step<100;step++){ | |
| const radius=step===0?0:22+Math.floor(step/12)*16,angle=step*2.4; | |
| left=Math.max(24,Math.min(size.width-24,x+Math.cos(angle)*radius));top=Math.max(24,Math.min(size.height-24,y+Math.sin(angle)*radius)); | |
| if(placed.every(p=>Math.hypot(p.left-left,p.top-top)>42))break; | |
| } | |
| const f=featured.find(c=>c.uniref_id===id);placed.push({id,left,top,x,y,number:f?.number||'•',title:f?.title||id}); | |
| } | |
| return placed; | |
| },[featured,selectedId,idIndex,norm,view,aspect,size]); | |
| const visibleMarkers=layer==='reference'?markers.filter(m=>m.id===selectedId||data.reference[idIndex.get(m.id)!]):markers; | |
| return <div className={'atlas-map '+(preview?'atlas-map-preview':'')} data-layer={layer}> | |
| <div className="atlas-map-toolbar"><div className="atlas-layers" role="group" aria-label="Map layer"><button aria-pressed={layer==='reference'} onClick={()=>{stopAnimation();setLayer('reference');}}>Swiss-Prot overlap</button><button aria-pressed={layer==='descriptions'} onClick={()=>{stopAnimation();if(layer!=='descriptions'){annotation.current=enabled?0:1;setLayer('descriptions');}}}>Ouroboros descriptions</button><button aria-pressed={layer==='communities'} onClick={()=>{stopAnimation();setLayer('communities');}}>Sequence communities</button></div><span className="micro">{data.ids.length.toLocaleString()} DISPLAYED</span></div> | |
| <div className="atlas-map-stage" ref={host}> | |
| <canvas ref={canvas} tabIndex={0} aria-label="Protein map. Drag to pan; pinch or focus and scroll to zoom. Arrow keys pan, plus and minus zoom, zero resets. Numbered buttons open featured proteins." | |
| onPointerDown={e=>{stopAnimation();e.currentTarget.focus({preventScroll:true});e.currentTarget.setPointerCapture(e.pointerId);pointers.current.set(e.pointerId,{x:e.clientX,y:e.clientY});if(pointers.current.size===2){const [a,b]=[...pointers.current.values()],r=e.currentTarget.getBoundingClientRect();pinch.current={distance:Math.hypot(a.x-b.x,a.y-b.y),cx:((a.x+b.x)/2-r.left)/r.width*2-1,cy:1-((a.y+b.y)/2-r.top)/r.height*2,view:liveView.current};drag.current=null;}else drag.current={id:e.pointerId,x:e.clientX,y:e.clientY,start:liveView.current,distance:0};setHover(null);}} | |
| onPointerMove={e=>{if(pointers.current.has(e.pointerId))pointers.current.set(e.pointerId,{x:e.clientX,y:e.clientY});const p=pinch.current;if(p&&pointers.current.size===2){const [a,b]=[...pointers.current.values()],r=e.currentTarget.getBoundingClientRect(),z=Math.max(.25,Math.min(24,p.view.zoom*Math.hypot(a.x-b.x,a.y-b.y)/Math.max(1,p.distance))),ratio=z/p.view.zoom;setView({zoom:z,x:((a.x+b.x)/2-r.left)/r.width*2-1-(p.cx-p.view.x)*ratio,y:1-((a.y+b.y)/2-r.top)/r.height*2-(p.cy-p.view.y)*ratio});return;}const d=drag.current;if(d&&d.id===e.pointerId){const dx=e.clientX-d.x,dy=e.clientY-d.y;d.distance=Math.max(d.distance,Math.hypot(dx,dy));setView({...d.start,x:d.start.x+dx/size.width*2,y:d.start.y-dy/size.height*2});return;}if(e.pointerType!=='mouse')return;const rect=e.currentTarget.getBoundingClientRect(),x=e.clientX-rect.left,y=e.clientY-rect.top,i=nearest(x,y);setHover(i<0?null:{index:i,x,y});}} | |
| onPointerUp={e=>{const d=drag.current;if(d&&d.id===e.pointerId&&d.distance<5){const r=e.currentTarget.getBoundingClientRect(),i=nearest(e.clientX-r.left,e.clientY-r.top);if(i>=0)selectRef.current(data.ids[i]);}pointers.current.delete(e.pointerId);pinch.current=null;drag.current=null;}} | |
| onPointerCancel={e=>{pointers.current.delete(e.pointerId);pinch.current=null;drag.current=null;}} onPointerLeave={()=>setHover(null)} | |
| onKeyDown={e=>{if(['+','=','-','0','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)){e.preventDefault();stopAnimation();if(e.key==='+'||e.key==='=')zoomBy(1.3);else if(e.key==='-')zoomBy(1/1.3);else if(e.key==='0')setView(resetView());else setView(v=>({...v,x:v.x+(e.key==='ArrowRight'?-.15:e.key==='ArrowLeft'?.15:0),y:v.y+(e.key==='ArrowDown'?.15:e.key==='ArrowUp'?-.15:0)}));}}}/> | |
| {error?<div className="atlas-map-error" role="status">{error}</div>:<> | |
| <div className="atlas-map-label" aria-hidden="true"><span>THE PROTEIN LANDSCAPE</span><small>{layer==='communities'?'34 sequence communities':layer==='reference'?'A small curated overlap':'Functional descriptions, at scale'}</small></div> | |
| <svg className="atlas-leaders" width={size.width} height={size.height} aria-hidden="true">{visibleMarkers.map(m=><g key={m.id} className={m.id===selectedId?'active':''}><line x1={m.x} y1={m.y} x2={m.left} y2={m.top}/><circle cx={m.x} cy={m.y} r="2.5"/></g>)}</svg> | |
| {visibleMarkers.map(m=><button key={m.id} className={'atlas-marker '+(m.id===selectedId?'selected':'')} style={{left:m.left,top:m.top}} title={`${m.number} · ${m.title} · ${m.id}`} aria-label={`Showcase ${m.number}: ${m.title}, ${m.id}`} aria-pressed={m.id===selectedId} onClick={()=>{stopAnimation();onSelect(m.id);}}>{m.number}</button>)} | |
| <div className="atlas-zoom" role="group" aria-label="Map controls"><button onClick={()=>zoomBy(1.4)} aria-label="Zoom in">+</button><button onClick={()=>zoomBy(1/1.4)} aria-label="Zoom out">−</button><button onClick={()=>{stopAnimation();setView(resetView());}} aria-label="Reset map">↺</button></div> | |
| </>} | |
| {hover&&!error&&<div className="atlas-hover" style={{left:Math.max(8,Math.min(hover.x+14,size.width-235)),top:Math.max(8,Math.min(hover.y+14,size.height-85))}}><strong>{data.ids[hover.index]}</strong><span>{data.lengths[hover.index]} aa · Community {data.clusters[hover.index]+1}</span><span>{data.reference[hover.index]?'Exact Swiss-Prot match':'No exact Swiss-Prot match'}</span><small>Click to explore →</small></div>} | |
| </div> | |
| <div className="atlas-map-caption"><span>{layer==='communities'?<><span className="atlas-color-key" aria-hidden="true">{['#2678b2','#d9822b','#2a9d8f','#8b5cf6'].map(c=><i key={c} style={{background:c}}/>)}</span>34 Leiden communities</>:<><i className={"atlas-dot "+(layer==='descriptions'?'description':'gray')}/> {layer==='descriptions'?'Ouroboros description beyond reference':'No exact match'} <i className="atlas-dot coral"/> Exact Swiss-Prot match</>}</span><span>Drag to pan · pinch / focus + scroll to zoom</span></div> | |
| <p className="atlas-map-note">ESM-2 · PCA / UMAP · sampled from a 1M-sequence projection. Display proportions are not corpus estimates. Generated descriptions are functional hypotheses; communities show embedding similarity.</p> | |
| </div>; | |
| } | |