/ snippet.html
snippet.html
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.0"> 6 <title>Ethereum Transaction Lifecycle Visualizer</title> 7 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> 8 <style> 9 body, html { 10 margin: 0; 11 padding: 0; 12 width: 100%; 13 height: 100%; 14 overflow: hidden; 15 font-family: Arial, sans-serif; 16 } 17 #visualization-container { 18 width: 100%; 19 height: 100%; 20 overflow: auto; 21 border: 1px solid #ccc; 22 resize: both; 23 } 24 #visualization { 25 width: 100%; 26 height: 100%; 27 } 28 #controls { 29 position: fixed; 30 bottom: 20px; 31 right: 20px; 32 width: 150px; 33 height: 150px; 34 } 35 .control-button { 36 position: absolute; 37 width: 60px; 38 height: 30px; 39 background-color: rgba(255, 255, 255, 0.7); 40 border: 1px solid #999; 41 border-radius: 5px; 42 } 43 #upBtn { top: 0; left: 45px; } 44 #leftBtn { top: 60px; left: 0; } 45 #rightBtn { top: 60px; right: 0; } 46 #downBtn { bottom: 0; left: 45px; } 47 #metadata { 48 position: fixed; 49 background-color: rgba(255, 255, 255, 0.9); 50 padding: 10px; 51 border-radius: 5px; 52 max-width: 300px; 53 max-height: 80%; 54 overflow-y: auto; 55 } 56 #metadata { 57 bottom: 20px; 58 left: 20px; 59 } 60 #metadata input, #metadata textarea, #addNodeForm input, #addNodeForm select { 61 width: 100%; 62 margin-bottom: 10px; 63 } 64 #metadata button, #addNodeForm button { 65 margin-right: 10px; 66 } 67 #editToggle { 68 position: fixed; 69 top: 20px; 70 right: 20px; 71 } 72 </style> 73 </head> 74 <body onload="onLoadPopulateNodesFromURl()"> 75 <div id="visualization-container"> 76 <div id="visualization"></div> 77 </div> 78 <div id="controls"> 79 <button id="upBtn" class="control-button">Up</button> 80 <button id="leftBtn" class="control-button">Previous</button> 81 <button id="rightBtn" class="control-button">Next</button> 82 <button id="downBtn" class="control-button">Down</button> 83 </div> 84 <div id="metadata"></div> 85 86 <script> 87 let nodeIds = ["User"]; 88 let connectionMatrix = { 89 "User": {} 90 }; 91 let nodeMetadata = { 92 "User": { 93 "content": "Initiates and signs the transaction", 94 "url": "https://ethereum.org/en/developers/docs/transactions/" 95 } 96 }; 97 98 const directionToGridDelta = { 99 "next": [1, 0], 100 "previous": [-1, 0], 101 "up": [0, -1], 102 "down": [0, 1] 103 }; 104 105 let isEditMode = false; 106 107 function assignGridPositions(startNode) { 108 const gridPositions = {}; 109 const queue = [[startNode, 0, 0]]; 110 const visited = new Set(); 111 112 let minX = 0, maxX = 0, minY = 0, maxY = 0; 113 114 while (queue.length > 0) { 115 const [node, x, y] = queue.shift(); 116 if (visited.has(node)) continue; 117 visited.add(node); 118 gridPositions[node] = [x, y]; 119 120 // Track the min and max values to calculate the grid's extent 121 minX = Math.min(minX, x); 122 maxX = Math.max(maxX, x); 123 minY = Math.min(minY, y); 124 maxY = Math.max(maxY, y); 125 126 for (let neighbor in connectionMatrix[node]) { 127 const connection = connectionMatrix[node][neighbor]; 128 if (connection) { 129 const [dx, dy] = directionToGridDelta[connection[0]]; 130 queue.push([neighbor, x + dx, y + dy]); 131 } 132 } 133 } 134 135 // Return the grid positions along with min and max extents 136 return { gridPositions, minX, maxX, minY, maxY }; 137 } 138 139 function createVisualization() { 140 const width = window.innerWidth; 141 const height = window.innerHeight; 142 143 d3.select("#visualization").selectAll("*").remove(); 144 145 const { gridPositions, minX, maxX, minY, maxY } = assignGridPositions(nodeIds[0]); 146 147 // Calculate grid size based on the width and number of horizontal steps 148 const horizontalSteps = maxX - minX; 149 const verticalSteps = maxY - minY + 1; 150 const gridSizeX = width / (horizontalSteps + 1); // Adjust for margin 151 const gridSizeY = height / (verticalSteps + 1); // Adjust for margin 152 153 // Use the smaller grid size to maintain aspect ratio 154 const gridSize = Math.min(gridSizeX, gridSizeY); 155 156 // Calculate node and text sizes based on grid size 157 const nodeRadius = gridSize * 0.3; // Node radius as a fraction of grid size 158 const transactionRadius = nodeRadius * 0.3; // Adjust transaction size 159 const fontSize = nodeRadius * 0.3; // Font size as a fraction of node radius 160 161 // Center the grid within the SVG 162 const offsetX = (width - (horizontalSteps * gridSize)) / 2; 163 const offsetY = (height - (verticalSteps * gridSize)) / 2; 164 165 let nodes = nodeIds.map(id => ({ 166 id: id, 167 x: offsetX + (gridPositions[id][0] - minX) * gridSize, 168 y: offsetY + (gridPositions[id][1] - minY) * gridSize 169 })); 170 171 const links = []; 172 for (let source in connectionMatrix) { 173 for (let target in connectionMatrix[source]) { 174 if (connectionMatrix[source][target]) { 175 links.push({ 176 source: nodes.find(n => n.id === source), 177 target: nodes.find(n => n.id === target), 178 type: connectionMatrix[source][target][0] 179 }); 180 } 181 } 182 } 183 184 const svg = d3.select("#visualization") 185 .append("svg") 186 .attr("width", width) 187 .attr("height", height); 188 189 const link = svg.append("g") 190 .selectAll("line") 191 .data(links) 192 .join("line") 193 .attr("stroke", "#999") 194 .attr("stroke-opacity", 0.6) 195 .attr("x1", d => d.source.x) 196 .attr("y1", d => d.source.y) 197 .attr("x2", d => d.target.x) 198 .attr("y2", d => d.target.y); 199 200 const node = svg.append("g") 201 .selectAll("circle") 202 .data(nodes) 203 .join("circle") 204 .attr("r", nodeRadius) 205 .attr("fill", "lightblue") 206 .attr("cx", d => d.x) 207 .attr("cy", d => d.y); 208 209 const label = svg.append("g") 210 .selectAll("text") 211 .data(nodes) 212 .join("text") 213 .text(d => d.id) 214 .attr("font-size", fontSize) 215 .attr("text-anchor", "middle") 216 .attr("dominant-baseline", "central") 217 .attr("x", d => d.x) 218 .attr("y", d => d.y); 219 220 const transaction = svg.append("circle") 221 .attr("r", transactionRadius) 222 .attr("fill", "red"); 223 224 return { nodes, transaction }; 225 } 226 227 let { nodes, transaction } = createVisualization(); 228 let currentNode = nodes[0]; 229 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 230 231 const directionMapping = { 232 "up": "up", 233 "down": "down", 234 "next": "right", 235 "previous": "left" 236 }; 237 238 function updateMetadataDisplay(nodeId) { 239 const metadata = nodeMetadata[nodeId]; 240 let html = `<h3><a href="${metadata.url}" target="_blank">${nodeId}</a></h3>`; 241 if (isEditMode) { 242 html += ` 243 <form id="metadataForm"> 244 <input type="text" id="nodeNameInput" value="${nodeId}"> 245 <textarea id="content" name="content" placeholder="Content">${metadata.content}</textarea> 246 <input type="url" id="url" name="url" value="${metadata.url}" placeholder="URL"> 247 <button type="button" onclick="saveMetadata('${nodeId}')">Save</button> 248 <button type="button" onclick="deleteNode('${nodeId}')">Delete</button> 249 <button type="button" onclick="uploadNodeData()">Upload</button> 250 <button type="button" onclick="downloadNodeData()">Download</button> 251 </form>`; 252 } else { 253 html += `<p>${metadata.content}</p>`; 254 } 255 document.getElementById("metadata").innerHTML = html; 256 } 257 258 function moveTransaction(direction) { 259 for (let targetId in connectionMatrix[currentNode.id]) { 260 const connection = connectionMatrix[currentNode.id][targetId]; 261 if (connection && connection[0] === direction) { 262 const targetNode = nodes.find(n => n.id === targetId); 263 if (targetNode) { 264 currentNode = targetNode; 265 transaction.transition() 266 .duration(500) 267 .attr("cx", currentNode.x) 268 .attr("cy", currentNode.y); 269 updateButtons(); 270 updateMetadataDisplay(currentNode.id); 271 break; 272 } 273 } 274 } 275 } 276 277 function updateButtons() { 278 for (let direction in directionMapping) { 279 const buttonId = `#${directionMapping[direction]}Btn`; 280 let isEnabled = false; 281 for (let targetId in connectionMatrix[currentNode.id]) { 282 const connection = connectionMatrix[currentNode.id][targetId]; 283 if (connection && connection[0] === direction) { 284 isEnabled = true; 285 break; 286 } 287 } 288 d3.select(buttonId).property("disabled", !isEnabled); 289 } 290 } 291 292 d3.select("#upBtn").on("click", () => moveTransaction("up")); 293 d3.select("#downBtn").on("click", () => moveTransaction("down")); 294 d3.select("#leftBtn").on("click", () => moveTransaction("previous")); 295 d3.select("#rightBtn").on("click", () => moveTransaction("next")); 296 297 updateButtons(); 298 updateMetadataDisplay(currentNode.id); 299 300 window.addEventListener('resize', () => { 301 ({ nodes, transaction } = createVisualization()); 302 currentNode = nodes.find(n => n.id === currentNode.id); 303 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 304 updateButtons(); 305 updateMetadataDisplay(currentNode.id); 306 }); 307 308 // Function to get URL parameters 309 function getUrlParameter(name) { 310 name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 311 const regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 312 const results = regex.exec(location.search); 313 return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 314 } 315 316 // Function to populate nodes from a file name in the URL parameter 317 function onLoadPopulateNodesFromURl() { 318 const fileName = getUrlParameter('dataFile'); 319 if (!fileName) { 320 console.error('No dataFile parameter found in the URL.'); 321 return; 322 } 323 324 // Construct the relative URL for the JSON file 325 const dataUrl = `./${fileName}`; 326 327 fetch(dataUrl) 328 .then(response => { 329 if (!response.ok) { 330 throw new Error('Network response was not ok'); 331 } 332 return response.json(); 333 }) 334 .then(data => { 335 nodeIds = data.nodeIds; 336 connectionMatrix = data.connectionMatrix; 337 nodeMetadata = data.nodeMetadata; 338 ({ nodes, transaction } = createVisualization()); 339 currentNode = nodes[0]; 340 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 341 updateButtons(); 342 updateMetadataDisplay(currentNode.id); 343 }) 344 .catch((error) => { 345 console.error('Error fetching node data:', error); 346 }); 347 } 348 349 </script> 350 </body> 351 </html>