30 lines
3.2 KiB
JavaScript
30 lines
3.2 KiB
JavaScript
|
|
// Living species occupy stable depth-first lanes; extinct ancestors are reparented by World.sample().
|
||
|
|
export function lineageOrder(species){
|
||
|
|
const byId=new Map(species.map(s=>[s.id,s])),ordered=[],seen=new Set();
|
||
|
|
const visit=(s,depth)=>{if(seen.has(s.id))return;seen.add(s.id);ordered.push({s,depth});species.filter(c=>c.parent===s.id).sort((a,b)=>a.origin-b.origin||a.id-b.id).forEach(c=>visit(c,depth+1))};
|
||
|
|
species.filter(s=>!byId.has(s.parent)).sort((a,b)=>a.id-b.id).forEach(s=>visit(s,0));
|
||
|
|
for(const s of species)visit(s,0);
|
||
|
|
return ordered;
|
||
|
|
}
|
||
|
|
export function lineageColor(s,layer='plants'){
|
||
|
|
if(layer==='trophic')return s.trophicRole==='primary-consumer'?'#a7d46d':s.trophicRole==='secondary-consumer'?'#e9c976':s.trophicRole==='tertiary-consumer'?'#d88979':'#aa8bc7';
|
||
|
|
if(layer==='habitat')return s.g.waterAffinity<1/3?'#d5bc88':s.g.waterAffinity<2/3?'#86c0a1':'#78b8da';
|
||
|
|
return s.color;
|
||
|
|
}
|
||
|
|
export function drawLineage(ctx,{width:w,height:h,species,hist,scale=1,layer='plants'}){
|
||
|
|
const ordered=lineageOrder(species),byId=new Map(species.map(s=>[s.id,s]));
|
||
|
|
ctx.setTransform(scale,0,0,scale,0,0);ctx.clearRect(0,0,w,h);
|
||
|
|
const compact=w<500,left=compact?66:115,right=w-(compact?37:57),first=hist[0]?.time??0,last=hist.at(-1)?.time??first,span=Math.max(1,last-first),x=time=>left+Math.max(0,Math.min(1,(time-first)/span))*(right-left);
|
||
|
|
ctx.fillStyle='#899c94';ctx.font='12px system-ui';ctx.textAlign='right';ctx.fillText('種',left-12,20);ctx.textAlign='left';ctx.fillText('現在',right+8,20);
|
||
|
|
for(const [i,{s}] of ordered.entries()){
|
||
|
|
const y=36+i*31;ctx.strokeStyle='#2c3c3c';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(right,y);ctx.stroke();
|
||
|
|
ctx.fillStyle=lineageColor(s,layer);ctx.textAlign='right';ctx.font='12px system-ui';ctx.fillText(compact?s.name.replace('種 ','#'):s.name,left-8,y+4);ctx.textAlign='left';ctx.fillStyle='#dce7df';ctx.fillText(s.count.toLocaleString(),right+8,y+4);
|
||
|
|
}
|
||
|
|
const lanes=new Map(ordered.map((v,i)=>[v.s.id,36+i*31]));
|
||
|
|
for(const {s} of ordered){if(!lanes.has(s.parent)||s.origin>last)continue;const cx=x(Math.max(first,s.origin)),py=lanes.get(s.parent),cy=lanes.get(s.id),birthCount=hist.find(v=>v.time>=s.origin)?.counts[s.id]||s.count,width=Math.max(1.5,Math.min(16,1+Math.sqrt(birthCount)*.78));ctx.strokeStyle=lineageColor(s,layer);ctx.globalAlpha=s.origin<first?.45:.75;ctx.lineWidth=width;ctx.lineCap='round';ctx.setLineDash(s.origin<first?[3,4]:[]);ctx.beginPath();ctx.moveTo(cx,py);ctx.lineTo(cx,cy);ctx.stroke();ctx.setLineDash([]);ctx.globalAlpha=1}
|
||
|
|
for(const {s} of ordered){const y=lanes.get(s.id);for(let i=1;i<hist.length;i++){
|
||
|
|
const a=hist[i-1].counts[s.id]||0,b=hist[i].counts[s.id]||0;if(!a&&!b)continue;
|
||
|
|
const count=Math.max(a,b),width=Math.max(1.5,Math.min(28,1.5+Math.sqrt(count)*1.08));ctx.strokeStyle=lineageColor(s,layer);ctx.globalAlpha=.92;ctx.lineWidth=width;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(x(a?hist[i-1].time:Math.max(hist[i-1].time,s.origin)),y);ctx.lineTo(x(hist[i].time),y);ctx.stroke();
|
||
|
|
}if(hist.length===1&&s.count){ctx.fillStyle=lineageColor(s,layer);ctx.beginPath();ctx.arc(x(first),y,Math.max(2,Math.min(9,Math.sqrt(s.count)*.42)),0,Math.PI*2);ctx.fill()}}
|
||
|
|
ctx.globalAlpha=1;return {ordered,first,last,byId};
|
||
|
|
}
|