123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- <!DOCTYPE html>
- <html lang="zh-TW">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>關鍵字關聯網路圖</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
- <style>
- body {
- margin: 0;
- padding: 20px;
- font-family: 'Microsoft JhengHei', 'SimHei', sans-serif;
- background: #000;
- color: #fff;
- }
- .container {
- max-width: 1600px;
- margin: 0 auto;
- background: #111;
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 15px rgba(255, 255, 255, 0.1);
- }
- h1 {
- text-align: center;
- color: #fff;
- margin-bottom: 20px;
- }
- #network {
- border: 1px solid #444;
- border-radius: 8px;
- background: #000;
- }
- .node {
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .node:hover {
- stroke: #ff6b35;
- stroke-width: 3px;
- }
- .link {
- stroke: #fff;
- stroke-opacity: 0.6;
- stroke-width: 1px;
- }
- .node-label {
- font-family: 'Microsoft JhengHei', 'SimHei', sans-serif;
- font-size: 24px;
- text-anchor: middle;
- pointer-events: none;
- fill: #fff;
- font-weight: bold;
- cursor: default;
- }
- .node-label-bg {
- fill: rgba(0, 0, 0, 0.7);
- stroke: rgba(255, 255, 255, 0.3);
- stroke-width: 1px;
- rx: 3;
- ry: 3;
- }
- .controls {
- text-align: center;
- margin-bottom: 15px;
- }
- button {
- background: #4CAF50;
- color: white;
- border: none;
- padding: 8px 16px;
- margin: 0 5px;
- border-radius: 5px;
- cursor: pointer;
- font-family: 'Microsoft JhengHei', sans-serif;
- }
- button:hover {
- background: #45a049;
- }
- .tooltip {
- position: absolute;
- background: rgba(0, 0, 0, 0.9);
- color: white;
- padding: 8px 12px;
- border-radius: 5px;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.3s;
- font-size: 12px;
- z-index: 1000;
- max-width: 200px;
- word-wrap: break-word;
- }
- .legend {
- position: absolute;
- top: 80px;
- right: 30px;
- background: rgba(17, 17, 17, 0.9);
- padding: 15px;
- border-radius: 8px;
- border: 1px solid #444;
- }
- .legend-item {
- display: flex;
- align-items: center;
- margin-bottom: 5px;
- font-size: 12px;
- }
- .legend-color {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- margin-right: 8px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>自行車花鼓關鍵字關聯網路圖</h1>
- <div class="controls">
- <button onclick="restartSimulation()">重新排列</button>
- <button onclick="centerNetwork()">置中顯示</button>
- <button onclick="toggleLabels()">切換標籤顯示</button>
- <button onclick="adjustSpacing()">調整間距</button>
- </div>
- <div class="legend">
- <div class="legend-item">
- <div class="legend-color" style="background: #ff4757;"></div>
- <span>核心關鍵字 (5+連接)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #ff6348;"></div>
- <span>重要關鍵字 (4連接)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #ffa502;"></div>
- <span>一般關鍵字 (2-3連接)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #70a1ff;"></div>
- <span>末端關鍵字 (1連接)</span>
- </div>
- </div>
- <svg id="network" width="100%" height="1500" viewBox="-200 -200 2000 1900"></svg>
- </div>
- <div class="tooltip" id="tooltip"></div>
- <script>
- // 關鍵字關聯資料
- const edgeList = [
- ['微風松高頂級會所', '微風松高高級餐廳推薦'],
- ['微風松高頂級會所', '台北頂級會員制餐廳'],
- ['微風松高頂級會所', '預約制高級會所台北'],
- ['微風松高頂級會所', '台北包廂餐廳微風松高'],
- ['微風松高頂級會所', '微風松高隱密接待餐廳'],
- // sealed bearing hub
- ['微風松高高級餐廳推薦', '微風松高米其林餐廳'],
- ['微風松高高級餐廳推薦', '微風松高高級火鍋'],
- ['微風松高高級餐廳推薦', '微風松高中餐推薦'],
- ['微風松高高級餐廳推薦', '微風松高日料推薦'],
- ['微風松高高級餐廳推薦', '微風松高法式料理餐廳'],
- // sealed cartridge bearings 延伸
- ['台北頂級會員制餐廳', '頂級餐飲會所推薦'],
- ['台北頂級會員制餐廳', '會員專屬高級餐廳台北'],
- ['台北頂級會員制餐廳', '預約制會員餐廳松高'],
- ['台北頂級會員制餐廳', '會員制米其林餐廳推薦'],
- ['台北頂級會員制餐廳', '台北隱藏版會員會所'],
- // mountain bike sealed hub 延伸
- ['預約制高級會所台北', '台北預約困難餐廳'],
- ['預約制高級會所台北', '預約制高級火鍋推薦'],
- ['預約制高級會所台北', '預約制日料餐廳台北'],
- ['預約制高級會所台北', '私廚等級餐廳推薦'],
- ['預約制高級會所台北', '高階客戶接待會所'],
- // sealed rear hub 延伸
- ['台北包廂餐廳微風松高', '微風松高家庭聚餐包廂'],
- ['台北包廂餐廳微風松高', '微風松高高級包廂中餐'],
- ['台北包廂餐廳微風松高', '松高適合聚餐的高級餐廳'],
- ['台北包廂餐廳微風松高', '松高隱密會客包廂'],
- ['台北包廂餐廳微風松高', '包廂火鍋推薦松高'],
- // lightweight sealed hub 延伸
- ['微風松高隱密接待餐廳', '高階接待餐廳推薦'],
- ['微風松高隱密接待餐廳', '外賓接待餐廳台北'],
- ['微風松高隱密接待餐廳', '商務聚餐隱密空間'],
- ['微風松高隱密接待餐廳', '高級安靜餐廳台北'],
- ['微風松高隱密接待餐廳', '企業高層聚餐餐廳'],
- // sealed bearing maintenance 延伸
- ];
- // 處理資料
- function processData() {
- const nodes = new Map();
- const links = [];
- edgeList.forEach(([source, target]) => {
- if (!nodes.has(source)) {
- nodes.set(source, { id: source, degree: 0 });
- }
- if (!nodes.has(target)) {
- nodes.set(target, { id: target, degree: 0 });
- }
- nodes.get(source).degree++;
- nodes.get(target).degree++;
- links.push({ source, target });
- });
- return {
- nodes: Array.from(nodes.values()),
- links: links
- };
- }
- // 獲取節點顏色(依據重要性)
- function getNodeColor(degree) {
- if (degree >= 5) return '#ff4757'; // 紅色 - 非常重要
- if (degree >= 4) return '#ff6348'; // 橙紅色 - 很重要
- if (degree >= 2) return '#ffa502'; // 橙色 - 重要
- return '#70a1ff'; // 藍色 - 較少連接
- }
- // 處理標籤文字換行
- function wrapLabel(text, maxWidth = 20) {
- const words = text.split(' ');
- const lines = [];
- let currentLine = '';
- for (const word of words) {
- const testLine = currentLine ? currentLine + ' ' + word : word;
- if (testLine.length <= maxWidth) {
- currentLine = testLine;
- } else {
- if (currentLine) {
- lines.push(currentLine);
- currentLine = word;
- } else {
- // 如果單個詞太長,強制換行
- lines.push(word);
- }
- }
- }
- if (currentLine) {
- lines.push(currentLine);
- }
- return lines;
- }
- let simulation;
- let svg, link, node, label, labelGroups;
- let labelsVisible = true;
- let currentSpacing = 150;
- // 繪製網路圖
- function drawNetwork() {
- const data = processData();
- svg = d3.select("#network");
- const width = parseInt(svg.style("width"));
- const height = 1500;
- svg.selectAll("*").remove();
- // 創建力導向模擬
- simulation = d3.forceSimulation(data.nodes)
- .force("link", d3.forceLink(data.links).id(d => d.id).distance(200)) // 原本是 currentSpacing
- .force("charge", d3.forceManyBody().strength(-1000)) // 原本是 -800
- .force("center", d3.forceCenter(width / 2, height / 2))
- .force("collision", d3.forceCollide().radius(d => Math.max(15, Math.sqrt(d.degree) * 6) + 35)); // 提高避免重疊
- // 創建連線
- link = svg.append("g")
- .selectAll("line")
- .data(data.links)
- .enter().append("line")
- .attr("class", "link");
- // 創建節點
- node = svg.append("g")
- .selectAll("circle")
- .data(data.nodes)
- .enter().append("circle")
- .attr("class", "node")
- .attr("r", d => Math.max(8, Math.sqrt(d.degree) * 4))
- .attr("fill", d => getNodeColor(d.degree))
- .attr("stroke", "#fff")
- .attr("stroke-width", 2)
- .call(d3.drag()
- .on("start", dragstarted)
- .on("drag", dragged)
- .on("end", dragended));
- // 創建標籤組
- labelGroups = svg.append("g")
- .selectAll("g")
- .data(data.nodes)
- .enter().append("g")
- .attr("class", "label-group");
- // 為每個標籤組添加多行文字
- labelGroups.each(function(d) {
- const lines = wrapLabel(d.id);
- const group = d3.select(this);
- lines.forEach((line, i) => {
- group.append("text")
- .attr("class", "node-label")
- .attr("dy", 50 + i * 20) // 移到節點下方
- .text(line);
- });
- });
- label = labelGroups.selectAll("text");
- // 滑鼠事件
- const tooltip = d3.select("#tooltip");
- node
- .on("mouseover", function(event, d) {
- tooltip.transition()
- .duration(200)
- .style("opacity", 1);
- tooltip.html(`
- <strong>${d.id}</strong><br/>
- 連接數: ${d.degree}<br/>
- 類型: ${d.degree >= 5 ? '核心關鍵字' : d.degree >= 4 ? '重要關鍵字' : d.degree >= 2 ? '一般關鍵字' : '末端關鍵字'}
- `)
- .style("left", (event.pageX + 10) + "px")
- .style("top", (event.pageY - 28) + "px");
- })
- .on("mouseout", function(d) {
- tooltip.transition()
- .duration(500)
- .style("opacity", 0);
- });
- // 模擬更新
- simulation.on("tick", () => {
- link
- .attr("x1", d => d.source.x)
- .attr("y1", d => d.source.y)
- .attr("x2", d => d.target.x)
- .attr("y2", d => d.target.y);
- node
- .attr("cx", d => d.x)
- .attr("cy", d => d.y);
- labelGroups
- .attr("transform", d => `translate(${d.x}, ${d.y})`);
- });
- }
- // 拖拽功能
- function dragstarted(event, d) {
- if (!event.active) simulation.alphaTarget(0.3).restart();
- d.fx = d.x;
- d.fy = d.y;
- }
- function dragged(event, d) {
- d.fx = event.x;
- d.fy = event.y;
- }
- function dragended(event, d) {
- if (!event.active) simulation.alphaTarget(0);
- d.fx = null;
- d.fy = null;
- }
- // 重新開始模擬
- function restartSimulation() {
- if (simulation) {
- simulation.alpha(1).restart();
- }
- }
- // 置中網路
- function centerNetwork() {
- const width = parseInt(svg.style("width"));
- const height = 1500;
- if (simulation) {
- simulation.force("center", d3.forceCenter(width / 2, height / 2));
- simulation.alpha(0.3).restart();
- }
- }
- // 切換標籤顯示
- function toggleLabels() {
- labelsVisible = !labelsVisible;
- const opacity = labelsVisible ? 1 : 0;
- if (label) {
- label.transition()
- .duration(300)
- .style("opacity", opacity);
- }
- }
- // 調整節點間距
- function adjustSpacing() {
- currentSpacing = currentSpacing === 150 ? 250 : 150;
- if (simulation) {
- simulation.force("link").distance(currentSpacing);
- simulation.force("collision").radius(d => Math.max(15, Math.sqrt(d.degree) * 6) + (currentSpacing === 250 ? 35 : 25));
- simulation.alpha(0.5).restart();
- }
- }
- // 初始化
- drawNetwork();
- </script>
- </body>
- </html>
|