maps-gallery.html 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <!doctype html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <title>Lieflat Charts · Maps · 2 张参考实现</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;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
  12. *{margin:0;padding:0;box-sizing:border-box}
  13. body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
  14. .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
  15. .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
  16. .card.wide{grid-column:1/-1}
  17. h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
  18. .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
  19. .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
  20. .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
  21. padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}
  22. svg{width:100%;max-height:330px;display:block;margin:0 auto}
  23. svg text{font-family:'Inter',sans-serif}
  24. .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
  25. @keyframes pop{from{transform:scale(0)}to{transform:none}}
  26. .fade{animation:fade .9s ease both}
  27. @keyframes fade{from{opacity:0}}
  28. .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
  29. @keyframes draw{to{stroke-dashoffset:0}}
  30. @media (prefers-reduced-motion:reduce){
  31. .pop,.fade{animation:none}
  32. .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
  33. }
  34. .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
  35. .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
  36. .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
  37. </style>
  38. </head>
  39. <body>
  40. <div class="pagehead">
  41. <h1>Maps · 2 张参考实现</h1>
  42. <p>仅在用户明确要求地图或地域分布时使用。M1 为美国州级,M2 为世界国家级;地图数据与 ECharts 通过网络加载。</p>
  43. </div>
  44. <div class="grid2">
  45. <!-- M1 · US Choropleth -->
  46. <div class="card">
  47. <span class="badge">MAPS · US CHOROPLETH</span>
  48. <h2>Sign-ups across the states</h2>
  49. <div class="sub">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data</div>
  50. <div class="ch" id="mapus" style="height:340px"></div>
  51. <div class="src">CHOROPLETH · MONO-MAP · GROWTH GEO</div>
  52. </div>
  53. <!-- M2 · World Choropleth -->
  54. <div class="card wide">
  55. <span class="badge">MAPS · WORLD CHOROPLETH</span>
  56. <h2>Where the users are</h2>
  57. <div class="sub">real outlines · shade = monthly active accounts · gray = markets we haven't opened</div>
  58. <div class="ch" id="mapworld" style="height:400px"></div>
  59. <div class="src">CHOROPLETH · MONO-MAP · GLOBAL OPS</div>
  60. </div>
  61. </div>
  62. <script>
  63. const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
  64. const obsReveal=(id,fn)=>{
  65. const n=document.getElementById(id);
  66. const go=()=>fn(n);
  67. const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
  68. io.observe(n);
  69. n.style.cursor='pointer';
  70. n.addEventListener('click',go);
  71. };
  72. // ════ M1 · US choropleth ════
  73. // 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。
  74. // 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角
  75. // (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。
  76. // Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、
  77. // 纸色描边当州界发丝、无数据州留 #E4E3DC(沉默可见)。
  78. // 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。
  79. // ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线
  80. // (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。
  81. const MAP_LEGEND={
  82. itemWidth:11,itemHeight:11,itemSymbol:'rect',
  83. textStyle:{fontFamily:'Inter',fontSize:9,color:'#8F8E88'},
  84. inRange:{color:['#D8D7D1','#B0AFA9','#8F8E88','#4A4944','#1C1C1A']},
  85. };
  86. (()=>{
  87. const D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,
  88. 'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,
  89. 'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,
  90. 'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,
  91. 'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,
  92. 'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,
  93. 'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};
  94. fetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')
  95. .then(r=>r.json()).then(geo=>{
  96. echarts.registerMap('mono-usa',geo,{
  97. Alaska:{left:-131,top:25,width:15},
  98. Hawaii:{left:-110,top:28,width:5},
  99. 'Puerto Rico':{left:-76,top:26,width:2},
  100. });
  101. const opt={
  102. animationDuration:900,animationEasing:'quarticOut',
  103. tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
  104. textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
  105. formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},
  106. visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',
  107. pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
  108. {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
  109. ...MAP_LEGEND},
  110. series:[{type:'map',map:'mono-usa',top:10,bottom:48,
  111. itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:1},
  112. emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
  113. textBorderColor:PAPER,textBorderWidth:3},
  114. itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},
  115. select:{disabled:true},
  116. data:Object.entries(D).map(([n,v])=>({name:n,value:v,
  117. label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,
  118. fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),
  119. }],
  120. };
  121. obsReveal('mapus',elDom=>{
  122. const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
  123. g.clear();g.setOption(opt);
  124. });
  125. });
  126. })();
  127. // ════ M2 · World choropleth ════
  128. // 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,
  129. // 关键市场(≥39k)直接把数值标在版图上。
  130. (()=>{
  131. const D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,
  132. 'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,
  133. 'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,
  134. 'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,
  135. 'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,
  136. 'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,
  137. 'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,
  138. 'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};
  139. const BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],
  140. 'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};
  141. fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json')
  142. .then(r=>r.json()).then(geo=>{
  143. echarts.registerMap('mono-world',geo);
  144. const opt={
  145. animationDuration:900,animationEasing:'quarticOut',
  146. tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
  147. textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
  148. formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},
  149. visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',
  150. pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
  151. {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
  152. ...MAP_LEGEND},
  153. series:[{type:'map',map:'mono-world',top:4,bottom:36,
  154. boundingCoords:[[-170,76],[190,-58]], // crop antarctica
  155. itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:.7},
  156. emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
  157. textBorderColor:PAPER,textBorderWidth:3},
  158. itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.1}},
  159. select:{disabled:true},
  160. data:Object.entries(D).map(([n,v])=>({name:n,value:v,
  161. label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,
  162. fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),
  163. }],
  164. };
  165. obsReveal('mapworld',elDom=>{
  166. const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
  167. g.clear();g.setOption(opt);
  168. });
  169. });
  170. })();
  171. addEventListener('resize',()=>{
  172. ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
  173. });
  174. </script>
  175. </body>
  176. </html>