/ 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>