跳转至

B1 · 密集环形关系图

网络 60 节点环形弦膜的 Interactive Big Chart 真实参考实现。示例结论为“六百次协作,汇成一环”,副标题已经把图例、单位或时间口径写在图旁:60 个代码仓库 · 连线代表共享贡献者 · 悬停仓库可聚焦其关系 · 点击重播

技术档案

项目 约定
数据形状 网络 60 节点环形弦膜
场合 大型关系图、代码协作网络
读者时间 交互探索
渲染引擎 ECharts
交互与动效 hover 聚焦邻接,点击重播
示例来源行 CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG

实现与使用边界

  • 页面直接复用 gallery 中 “Six hundred collaborations, one ring” 的卡片结构和同名渲染块,没有按截图重新绘制。
  • 标题、副标题、图形、来源行四件套保持原样;数据编码、比例关系、灰阶和动画节奏由原实现决定。
  • 使用时只替换数据与文案,并保持“网络 60 节点环形弦膜”这一数据契约;若数据本体改变,应回到总目录重新选型。
  • 颜色限于 Mono 灰阶,面积编码继续使用平方根换算;柱长、位置与角度编码不得断轴或视觉夸大。

模板来源:Lieflat Charts B1 Circular Graph Dense,真实骨架取自 templates/big-circular.html 的 “Six hundred collaborations, one ring” 卡片与对应渲染块。

b1-circular-graph-dense.html —— 自包含单页源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lieflat Charts — B1 密集环形关系图</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
<link rel="stylesheet" href="../../../../vendor/fonts/fonts.css">
<style>
  :root{--bg:#F0EFEB;--dark:#1C1C1A}
  *{margin:0;padding:0;box-sizing:border-box}
  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
    display:flex;justify-content:center}
  .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;
    width:min(1100px,100%)}
  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
  .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}
  .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}
  #ch{height:78vh;min-height:620px;cursor:pointer}
</style>
</head>
<body>
<div class="card">
  <h2>六百次协作,汇成一环</h2>
  <div class="sub">60 个代码仓库 · 连线代表共享贡献者 · 悬停仓库可聚焦其关系 · 点击重播</div>
  <div id="ch"></div>
  <div class="src">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG</div>
</div>
<script>
const INK='#1C1C1A',PAPER='#F0EFEB';
const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic

// ── 60 repos in 5 org clusters; cluster = shade family ──
const ORGS=[
  ['platform',16,'#F0EFEB'],   // paper — the core
  ['product',14,'#C9C7BD'],
  ['data',12,'#A5A39A'],
  ['infra',10,'#8F8E88'],
  ['labs',8,'#6A6963'],
];
const nodes=[];
ORGS.forEach(([org,count],oi)=>{
  for(let i=0;i<count;i++){
    // activity: a few giants per cluster, long tail of small repos
    const heavy=i<2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;
    nodes.push({
      name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),
      org:oi,value:Math.round(heavy),
    });
  }
});
// links: dense inside a cluster, sparser across; weight = shared contributors
const links=[];
const idx={};nodes.forEach((n,i)=>idx[n.name]=i);
nodes.forEach((a,i)=>{
  nodes.forEach((b,j)=>{
    if(j<=i)return;
    const same=a.org===b.org;
    const p=same?.16:.045;
    if(rnd(i+1,j+2)<p){
      links.push({source:i,target:j,
        w:1+Math.round(rnd(i+2,j+3)*6),same});
    }
  });
});

const g=echarts.init(document.getElementById('ch'));
const opt={
  animationDuration:1800,animationEasing:'quarticOut',
  tooltip:{backgroundColor:PAPER,borderWidth:0,
    textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],
    formatter:p=>p.dataType==='node'
      ?p.name+' — '+p.value+' contributors'
      :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},
  series:[{
    type:'graph',layout:'circular',
    circular:{rotateLabel:true},
    left:24,right:24,top:24,bottom:24,
    data:nodes.map((n,i)=>({
      name:n.name,value:n.value,
      symbolSize:3.5+Math.sqrt(n.value)*1.7,
      itemStyle:{color:ORGS[n.org][2],borderWidth:0},
      // only the giants get a label — 60 names would be soup
      label:{show:n.value>=26,position:'right',distance:4,
        color:'#B3B0A4',fontFamily:'Inter',fontSize:8.5,fontWeight:600},
    })),
    links:links.map(l=>({
      source:l.source,target:l.target,
      rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,
      lineStyle:{
        width:.4+l.w*.28,
        color:l.same?'#55534B':'#3A3934',
        opacity:l.same?.5:.32,
        curveness:.3,
      },
    })),
    emphasis:{
      focus:'adjacency',
      lineStyle:{color:PAPER,opacity:.9,width:1.6},
      label:{show:true,color:PAPER},
    },
    blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},
  }],
};
const play=()=>{g.clear();g.setOption(opt)};
play();
document.getElementById('ch').addEventListener('click',play);
addEventListener('resize',()=>g.resize());
</script>
</body>
</html>