news.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <!DOCTYPE html>
  2. <html lang="zh-TW">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>關鍵字關聯網路圖</title>
  7. <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
  8. <style>
  9. body {
  10. margin: 0;
  11. padding: 20px;
  12. font-family: 'Microsoft JhengHei', 'SimHei', sans-serif;
  13. background: #000;
  14. color: #fff;
  15. }
  16. .container {
  17. max-width: 1600px;
  18. margin: 0 auto;
  19. background: #111;
  20. border-radius: 10px;
  21. padding: 20px;
  22. box-shadow: 0 4px 15px rgba(255, 255, 255, 0.1);
  23. }
  24. h1 {
  25. text-align: center;
  26. color: #fff;
  27. margin-bottom: 20px;
  28. }
  29. #network {
  30. border: 1px solid #444;
  31. border-radius: 8px;
  32. background: #000;
  33. }
  34. .node {
  35. cursor: pointer;
  36. transition: all 0.3s ease;
  37. }
  38. .node:hover {
  39. stroke: #ff6b35;
  40. stroke-width: 3px;
  41. }
  42. .link {
  43. stroke: #fff;
  44. stroke-opacity: 0.6;
  45. stroke-width: 1px;
  46. }
  47. .node-label {
  48. font-family: 'Microsoft JhengHei', 'SimHei', sans-serif;
  49. font-size: 24px;
  50. text-anchor: middle;
  51. pointer-events: none;
  52. fill: #fff;
  53. font-weight: bold;
  54. cursor: default;
  55. }
  56. .node-label-bg {
  57. fill: rgba(0, 0, 0, 0.7);
  58. stroke: rgba(255, 255, 255, 0.3);
  59. stroke-width: 1px;
  60. rx: 3;
  61. ry: 3;
  62. }
  63. .controls {
  64. text-align: center;
  65. margin-bottom: 15px;
  66. }
  67. button {
  68. background: #4CAF50;
  69. color: white;
  70. border: none;
  71. padding: 8px 16px;
  72. margin: 0 5px;
  73. border-radius: 5px;
  74. cursor: pointer;
  75. font-family: 'Microsoft JhengHei', sans-serif;
  76. }
  77. button:hover {
  78. background: #45a049;
  79. }
  80. .tooltip {
  81. position: absolute;
  82. background: rgba(0, 0, 0, 0.9);
  83. color: white;
  84. padding: 8px 12px;
  85. border-radius: 5px;
  86. pointer-events: none;
  87. opacity: 0;
  88. transition: opacity 0.3s;
  89. font-size: 12px;
  90. z-index: 1000;
  91. max-width: 200px;
  92. word-wrap: break-word;
  93. }
  94. .legend {
  95. position: absolute;
  96. top: 80px;
  97. right: 30px;
  98. background: rgba(17, 17, 17, 0.9);
  99. padding: 15px;
  100. border-radius: 8px;
  101. border: 1px solid #444;
  102. }
  103. .legend-item {
  104. display: flex;
  105. align-items: center;
  106. margin-bottom: 5px;
  107. font-size: 12px;
  108. }
  109. .legend-color {
  110. width: 12px;
  111. height: 12px;
  112. border-radius: 50%;
  113. margin-right: 8px;
  114. }
  115. </style>
  116. </head>
  117. <body>
  118. <div class="container">
  119. <h1>自行車花鼓關鍵字關聯網路圖</h1>
  120. <div class="controls">
  121. <button onclick="restartSimulation()">重新排列</button>
  122. <button onclick="centerNetwork()">置中顯示</button>
  123. <button onclick="toggleLabels()">切換標籤顯示</button>
  124. <button onclick="adjustSpacing()">調整間距</button>
  125. </div>
  126. <div class="legend">
  127. <div class="legend-item">
  128. <div class="legend-color" style="background: #ff4757;"></div>
  129. <span>核心關鍵字 (5+連接)</span>
  130. </div>
  131. <div class="legend-item">
  132. <div class="legend-color" style="background: #ff6348;"></div>
  133. <span>重要關鍵字 (4連接)</span>
  134. </div>
  135. <div class="legend-item">
  136. <div class="legend-color" style="background: #ffa502;"></div>
  137. <span>一般關鍵字 (2-3連接)</span>
  138. </div>
  139. <div class="legend-item">
  140. <div class="legend-color" style="background: #70a1ff;"></div>
  141. <span>末端關鍵字 (1連接)</span>
  142. </div>
  143. </div>
  144. <svg id="network" width="100%" height="1500" viewBox="-200 -200 2000 1900"></svg>
  145. </div>
  146. <div class="tooltip" id="tooltip"></div>
  147. <script>
  148. // 關鍵字關聯資料
  149. const edgeList = [
  150. ['微風松高頂級會所', '微風松高高級餐廳推薦'],
  151. ['微風松高頂級會所', '台北頂級會員制餐廳'],
  152. ['微風松高頂級會所', '預約制高級會所台北'],
  153. ['微風松高頂級會所', '台北包廂餐廳微風松高'],
  154. ['微風松高頂級會所', '微風松高隱密接待餐廳'],
  155. // sealed bearing hub
  156. ['微風松高高級餐廳推薦', '微風松高米其林餐廳'],
  157. ['微風松高高級餐廳推薦', '微風松高高級火鍋'],
  158. ['微風松高高級餐廳推薦', '微風松高中餐推薦'],
  159. ['微風松高高級餐廳推薦', '微風松高日料推薦'],
  160. ['微風松高高級餐廳推薦', '微風松高法式料理餐廳'],
  161. // sealed cartridge bearings 延伸
  162. ['台北頂級會員制餐廳', '頂級餐飲會所推薦'],
  163. ['台北頂級會員制餐廳', '會員專屬高級餐廳台北'],
  164. ['台北頂級會員制餐廳', '預約制會員餐廳松高'],
  165. ['台北頂級會員制餐廳', '會員制米其林餐廳推薦'],
  166. ['台北頂級會員制餐廳', '台北隱藏版會員會所'],
  167. // mountain bike sealed hub 延伸
  168. ['預約制高級會所台北', '台北預約困難餐廳'],
  169. ['預約制高級會所台北', '預約制高級火鍋推薦'],
  170. ['預約制高級會所台北', '預約制日料餐廳台北'],
  171. ['預約制高級會所台北', '私廚等級餐廳推薦'],
  172. ['預約制高級會所台北', '高階客戶接待會所'],
  173. // sealed rear hub 延伸
  174. ['台北包廂餐廳微風松高', '微風松高家庭聚餐包廂'],
  175. ['台北包廂餐廳微風松高', '微風松高高級包廂中餐'],
  176. ['台北包廂餐廳微風松高', '松高適合聚餐的高級餐廳'],
  177. ['台北包廂餐廳微風松高', '松高隱密會客包廂'],
  178. ['台北包廂餐廳微風松高', '包廂火鍋推薦松高'],
  179. // lightweight sealed hub 延伸
  180. ['微風松高隱密接待餐廳', '高階接待餐廳推薦'],
  181. ['微風松高隱密接待餐廳', '外賓接待餐廳台北'],
  182. ['微風松高隱密接待餐廳', '商務聚餐隱密空間'],
  183. ['微風松高隱密接待餐廳', '高級安靜餐廳台北'],
  184. ['微風松高隱密接待餐廳', '企業高層聚餐餐廳'],
  185. // sealed bearing maintenance 延伸
  186. ];
  187. // 處理資料
  188. function processData() {
  189. const nodes = new Map();
  190. const links = [];
  191. edgeList.forEach(([source, target]) => {
  192. if (!nodes.has(source)) {
  193. nodes.set(source, { id: source, degree: 0 });
  194. }
  195. if (!nodes.has(target)) {
  196. nodes.set(target, { id: target, degree: 0 });
  197. }
  198. nodes.get(source).degree++;
  199. nodes.get(target).degree++;
  200. links.push({ source, target });
  201. });
  202. return {
  203. nodes: Array.from(nodes.values()),
  204. links: links
  205. };
  206. }
  207. // 獲取節點顏色(依據重要性)
  208. function getNodeColor(degree) {
  209. if (degree >= 5) return '#ff4757'; // 紅色 - 非常重要
  210. if (degree >= 4) return '#ff6348'; // 橙紅色 - 很重要
  211. if (degree >= 2) return '#ffa502'; // 橙色 - 重要
  212. return '#70a1ff'; // 藍色 - 較少連接
  213. }
  214. // 處理標籤文字換行
  215. function wrapLabel(text, maxWidth = 20) {
  216. const words = text.split(' ');
  217. const lines = [];
  218. let currentLine = '';
  219. for (const word of words) {
  220. const testLine = currentLine ? currentLine + ' ' + word : word;
  221. if (testLine.length <= maxWidth) {
  222. currentLine = testLine;
  223. } else {
  224. if (currentLine) {
  225. lines.push(currentLine);
  226. currentLine = word;
  227. } else {
  228. // 如果單個詞太長,強制換行
  229. lines.push(word);
  230. }
  231. }
  232. }
  233. if (currentLine) {
  234. lines.push(currentLine);
  235. }
  236. return lines;
  237. }
  238. let simulation;
  239. let svg, link, node, label, labelGroups;
  240. let labelsVisible = true;
  241. let currentSpacing = 150;
  242. // 繪製網路圖
  243. function drawNetwork() {
  244. const data = processData();
  245. svg = d3.select("#network");
  246. const width = parseInt(svg.style("width"));
  247. const height = 1500;
  248. svg.selectAll("*").remove();
  249. // 創建力導向模擬
  250. simulation = d3.forceSimulation(data.nodes)
  251. .force("link", d3.forceLink(data.links).id(d => d.id).distance(200)) // 原本是 currentSpacing
  252. .force("charge", d3.forceManyBody().strength(-1000)) // 原本是 -800
  253. .force("center", d3.forceCenter(width / 2, height / 2))
  254. .force("collision", d3.forceCollide().radius(d => Math.max(15, Math.sqrt(d.degree) * 6) + 35)); // 提高避免重疊
  255. // 創建連線
  256. link = svg.append("g")
  257. .selectAll("line")
  258. .data(data.links)
  259. .enter().append("line")
  260. .attr("class", "link");
  261. // 創建節點
  262. node = svg.append("g")
  263. .selectAll("circle")
  264. .data(data.nodes)
  265. .enter().append("circle")
  266. .attr("class", "node")
  267. .attr("r", d => Math.max(8, Math.sqrt(d.degree) * 4))
  268. .attr("fill", d => getNodeColor(d.degree))
  269. .attr("stroke", "#fff")
  270. .attr("stroke-width", 2)
  271. .call(d3.drag()
  272. .on("start", dragstarted)
  273. .on("drag", dragged)
  274. .on("end", dragended));
  275. // 創建標籤組
  276. labelGroups = svg.append("g")
  277. .selectAll("g")
  278. .data(data.nodes)
  279. .enter().append("g")
  280. .attr("class", "label-group");
  281. // 為每個標籤組添加多行文字
  282. labelGroups.each(function(d) {
  283. const lines = wrapLabel(d.id);
  284. const group = d3.select(this);
  285. lines.forEach((line, i) => {
  286. group.append("text")
  287. .attr("class", "node-label")
  288. .attr("dy", 50 + i * 20) // 移到節點下方
  289. .text(line);
  290. });
  291. });
  292. label = labelGroups.selectAll("text");
  293. // 滑鼠事件
  294. const tooltip = d3.select("#tooltip");
  295. node
  296. .on("mouseover", function(event, d) {
  297. tooltip.transition()
  298. .duration(200)
  299. .style("opacity", 1);
  300. tooltip.html(`
  301. <strong>${d.id}</strong><br/>
  302. 連接數: ${d.degree}<br/>
  303. 類型: ${d.degree >= 5 ? '核心關鍵字' : d.degree >= 4 ? '重要關鍵字' : d.degree >= 2 ? '一般關鍵字' : '末端關鍵字'}
  304. `)
  305. .style("left", (event.pageX + 10) + "px")
  306. .style("top", (event.pageY - 28) + "px");
  307. })
  308. .on("mouseout", function(d) {
  309. tooltip.transition()
  310. .duration(500)
  311. .style("opacity", 0);
  312. });
  313. // 模擬更新
  314. simulation.on("tick", () => {
  315. link
  316. .attr("x1", d => d.source.x)
  317. .attr("y1", d => d.source.y)
  318. .attr("x2", d => d.target.x)
  319. .attr("y2", d => d.target.y);
  320. node
  321. .attr("cx", d => d.x)
  322. .attr("cy", d => d.y);
  323. labelGroups
  324. .attr("transform", d => `translate(${d.x}, ${d.y})`);
  325. });
  326. }
  327. // 拖拽功能
  328. function dragstarted(event, d) {
  329. if (!event.active) simulation.alphaTarget(0.3).restart();
  330. d.fx = d.x;
  331. d.fy = d.y;
  332. }
  333. function dragged(event, d) {
  334. d.fx = event.x;
  335. d.fy = event.y;
  336. }
  337. function dragended(event, d) {
  338. if (!event.active) simulation.alphaTarget(0);
  339. d.fx = null;
  340. d.fy = null;
  341. }
  342. // 重新開始模擬
  343. function restartSimulation() {
  344. if (simulation) {
  345. simulation.alpha(1).restart();
  346. }
  347. }
  348. // 置中網路
  349. function centerNetwork() {
  350. const width = parseInt(svg.style("width"));
  351. const height = 1500;
  352. if (simulation) {
  353. simulation.force("center", d3.forceCenter(width / 2, height / 2));
  354. simulation.alpha(0.3).restart();
  355. }
  356. }
  357. // 切換標籤顯示
  358. function toggleLabels() {
  359. labelsVisible = !labelsVisible;
  360. const opacity = labelsVisible ? 1 : 0;
  361. if (label) {
  362. label.transition()
  363. .duration(300)
  364. .style("opacity", opacity);
  365. }
  366. }
  367. // 調整節點間距
  368. function adjustSpacing() {
  369. currentSpacing = currentSpacing === 150 ? 250 : 150;
  370. if (simulation) {
  371. simulation.force("link").distance(currentSpacing);
  372. simulation.force("collision").radius(d => Math.max(15, Math.sqrt(d.degree) * 6) + (currentSpacing === 250 ? 35 : 25));
  373. simulation.alpha(0.5).restart();
  374. }
  375. }
  376. // 初始化
  377. drawNetwork();
  378. </script>
  379. </body>
  380. </html>