big-force.html 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <title>Mono — Force Graph, Dense</title>
  7. <script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
  8. <link rel="preconnect" href="https://fonts.googleapis.com">
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
  10. <style>
  11. :root{--bg:#F0EFEB;--dark:#1C1C1A}
  12. *{margin:0;padding:0;box-sizing:border-box}
  13. body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
  14. display:flex;justify-content:center}
  15. .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;
  16. width:min(1100px,100%)}
  17. h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
  18. .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}
  19. .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}
  20. #ch{height:78vh;min-height:620px;cursor:grab}
  21. </style>
  22. </head>
  23. <body>
  24. <div class="card">
  25. <h2>A galaxy of one hundred eighty services</h2>
  26. <div class="sub">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay</div>
  27. <div id="ch"></div>
  28. <div class="src">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH</div>
  29. </div>
  30. <script>
  31. const INK='#1C1C1A',PAPER='#F0EFEB';
  32. const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
  33. // ── 8 domain hubs, each with a swarm of services; hubs interconnect ──
  34. const DOMAINS=[
  35. ['auth',26,'#F0EFEB'],['billing',24,'#DCDAD2'],['content',28,'#C9C7BD'],
  36. ['search',20,'#B3B0A4'],['notify',22,'#9C9A91'],['media',20,'#8F8E88'],
  37. ['analytics',18,'#7C7A72'],['edge',14,'#6A6963'],
  38. ];
  39. const nodes=[],links=[];
  40. DOMAINS.forEach(([dom,count,shade],di)=>{
  41. // the hub
  42. nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});
  43. // satellites: long-tail sizes, a couple of mid-weights per domain
  44. for(let i=0;i<count;i++){
  45. const v=i<3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;
  46. const name=dom+'-'+String(i+1).padStart(2,'0');
  47. nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});
  48. // satellite -> its hub
  49. links.push({source:name,target:dom,
  50. lineStyle:{width:.5+v*.05,color:'#3F3E38',opacity:.5}});
  51. // occasional satellite-to-satellite shortcut inside the domain
  52. if(i>1&&rnd(di+3,i+11)<.22){
  53. links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),
  54. lineStyle:{width:.5,color:'#33322D',opacity:.4}});
  55. }
  56. }
  57. });
  58. // hub-to-hub backbone: every domain talks to 2-3 others
  59. const H=DOMAINS.length;
  60. for(let a=0;a<H;a++)for(let b=a+1;b<H;b++){
  61. if(rnd(a+7,b+9)<.42){
  62. links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],
  63. lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#55534B',opacity:.65,curveness:.08}});
  64. }
  65. }
  66. // a few cross-domain satellite shortcuts — the messy real-world wires
  67. for(let k=0;k<26;k++){
  68. const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];
  69. const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];
  70. if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){
  71. links.push({source:a.name,target:b.name,
  72. lineStyle:{width:.5,color:'#2E2D29',opacity:.38,curveness:.15}});
  73. }
  74. }
  75. const g=echarts.init(document.getElementById('ch'));
  76. const opt={
  77. animationDuration:300,
  78. tooltip:{backgroundColor:PAPER,borderWidth:0,
  79. textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],
  80. formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},
  81. series:[{
  82. type:'graph',layout:'force',
  83. force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},
  84. roam:true,draggable:true,
  85. left:6,right:6,top:6,bottom:6,
  86. data:nodes.map(n=>({
  87. name:n.name,value:n.value,
  88. symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,
  89. itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},
  90. label:{show:n.hub,position:'inside',
  91. color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},
  92. })),
  93. links,
  94. emphasis:{
  95. focus:'adjacency',
  96. lineStyle:{color:PAPER,opacity:.9,width:1.4},
  97. label:{show:true,color:PAPER,position:'right'},
  98. },
  99. blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},
  100. }],
  101. };
  102. const play=()=>{g.clear();g.setOption(opt)};
  103. play();
  104. // click on empty canvas replays; clicks on nodes are for dragging
  105. g.getZr().on('click',e=>{if(!e.target)play()});
  106. addEventListener('resize',()=>g.resize());
  107. </script>
  108. </body>
  109. </html>