/* Global variables */
let gridSize = 3; // starting with 3 nodes
let selectedNode = null;
// Save node-specific properties so that if you edit the grid later the changes persist.
let nodeProperties = {}; // e.g., { 0: {color: “#ff0000″, size: 20}, … }
// When the page loads, build the initial grid and attach event listeners.
document.addEventListener(‘DOMContentLoaded’, function() {
generateGrid(gridSize);
document.getElementById(‘addRowCol’).addEventListener(‘click’, addNode);
document.getElementById(‘submitGrid’).addEventListener(‘click’, submitGrid);
document.getElementById(‘exportCSV’).addEventListener(‘click’, exportCSV);
document.getElementById(‘updateNode’).addEventListener(‘click’, function(){
if (selectedNode) {
let newColor = document.getElementById(‘nodeColor’).value;
let newSize = +document.getElementById(‘nodeSize’).value;
nodeProperties[selectedNode.id] = { color: newColor, size: newSize };
updateVisualization();
document.getElementById(‘nodeControls’).style.display = ‘none’;
}
});
// Render the initial (empty) visualization.
updateVisualization();
});
// Generates the grid table with a header row and header column.
function generateGrid(n) {
gridSize = n;
let table = document.getElementById(‘grid’);
table.innerHTML = ”;
// Create header row (first row): first cell is empty
let headerRow = document.createElement(‘tr’);
let corner = document.createElement(‘th’);
headerRow.appendChild(corner);
for (let j = 0; j < n; j++) {
let th = document.createElement('th');
let input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Name';
input.className = 'node-name';
input.setAttribute('data-index', j);
input.addEventListener('input', syncHeaders);
th.appendChild(input);
headerRow.appendChild(th);
}
table.appendChild(headerRow);
// Create a row for each node.
for (let i = 0; i < n; i++) {
let row = document.createElement('tr');
// First cell in each row: node name input.
let th = document.createElement('th');
let input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Name';
input.className = 'node-name';
input.setAttribute('data-index', i);
input.addEventListener('input', syncHeaders);
th.appendChild(input);
row.appendChild(th);
// Then cells for relationship data.
for (let j = 0; j {
if (input !== this) {
input.value = this.value;
}
});
updateVisualization();
}
// Adds a new node by adding a new header (row & column) to the grid.
function addNode() {
gridSize++;
let table = document.getElementById(‘grid’);
// Append a new header cell (for the new column) in the first row.
let headerRow = table.rows[0];
let newTh = document.createElement(‘th’);
let newInput = document.createElement(‘input’);
newInput.type = ‘text’;
newInput.placeholder = ‘Name’;
newInput.className = ‘node-name’;
newInput.setAttribute(‘data-index’, gridSize – 1);
newInput.addEventListener(‘input’, syncHeaders);
newTh.appendChild(newInput);
headerRow.appendChild(newTh);
// Create a new row for the new node.
let newRow = document.createElement(‘tr’);
// The new row header cell.
let newRowHeader = document.createElement(‘th’);
let newRowInput = document.createElement(‘input’);
newRowInput.type = ‘text’;
newRowInput.placeholder = ‘Name’;
newRowInput.className = ‘node-name’;
newRowInput.setAttribute(‘data-index’, gridSize – 1);
newRowInput.addEventListener(‘input’, syncHeaders);
newRowHeader.appendChild(newRowInput);
newRow.appendChild(newRowHeader);
// Create data cells for the new row.
for (let j = 0; j < gridSize; j++) {
let td = document.createElement('td');
let cellInput = document.createElement('input');
cellInput.type = 'text';
cellInput.className = 'cell-input';
cellInput.setAttribute('data-row', gridSize – 1);
cellInput.setAttribute('data-col', j);
if ((gridSize – 1) === j) {
cellInput.value = "0";
cellInput.disabled = true;
}
cellInput.addEventListener('input', updateVisualizationFromGrid);
td.appendChild(cellInput);
newRow.appendChild(td);
}
table.appendChild(newRow);
// For every existing row (except the header row), add a new cell for the new column.
for (let i = 1; i
{
if (input.value.trim() === “”) {
input.value = “0”;
}
});
updateVisualization();
}
// This function is called when a grid cell is edited.
function updateVisualizationFromGrid() {
updateVisualization();
}
// Reads the current grid and header values, builds arrays of nodes and links,
// and then calls drawNetwork.
function updateVisualization() {
// Build the nodes using the header row (top row) inputs.
let nodes = [];
let headerInputs = document.querySelectorAll(‘tr:first-child .node-name’);
headerInputs.forEach((input, i) => {
let name = input.value.trim() || “Node ” + (i + 1);
let defaultColor = “#1f77b4”;
let defaultSize = 15;
if (nodeProperties[i]) {
defaultColor = nodeProperties[i].color;
defaultSize = nodeProperties[i].size;
}
nodes.push({ id: i, name: name, color: defaultColor, size: defaultSize });
});
// Build the links by scanning every grid cell. (Only cells with a “1” indicate a relationship.)
let links = [];
let cellInputs = document.querySelectorAll(‘.cell-input’);
cellInputs.forEach(input => {
let value = input.value.trim();
let row = parseInt(input.getAttribute(‘data-row’));
let col = parseInt(input.getAttribute(‘data-col’));
// Skip the diagonal cells (self-relationships)
if (row !== col && value === “1”) {
links.push({ source: row, target: col });
}
});
drawNetwork(nodes, links);
}
// Uses D3 to render a force-directed network in the SVG.
function drawNetwork(nodes, links) {
const svg = d3.select(“#networkSvg”);
const width = +svg.attr(“width”);
const height = +svg.attr(“height”);
// Remove any existing drawing.
svg.selectAll(“*”).remove();
// Set up the force simulation.
const simulation = d3.forceSimulation(nodes)
.force(“link”, d3.forceLink(links).id(d => d.id).distance(100))
.force(“charge”, d3.forceManyBody().strength(-300))
.force(“center”, d3.forceCenter(width / 2, height / 2));
// Draw links (edges).
const link = svg.append(“g”)
.attr(“class”, “links”)
.selectAll(“line”)
.data(links)
.enter().append(“line”)
.attr(“stroke”, “#999”)
.attr(“stroke-width”, 2);
// Draw nodes.
const node = svg.append(“g”)
.attr(“class”, “nodes”)
.selectAll(“circle”)
.data(nodes)
.enter().append(“circle”)
.attr(“r”, d => d.size)
.attr(“fill”, d => d.color)
.call(d3.drag()
.on(“start”, dragstarted)
.on(“drag”, dragged)
.on(“end”, dragended))
.on(“click”, nodeClicked);
// Add text labels.
const label = svg.append(“g”)
.attr(“class”, “labels”)
.selectAll(“text”)
.data(nodes)
.enter().append(“text”)
.attr(“dy”, -10)
.attr(“text-anchor”, “middle”)
.text(d => d.name)
.attr(“font-size”, “12px”)
.attr(“fill”, “#333”);
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);
label.attr(“x”, d => d.x)
.attr(“y”, d => d.y);
});
// Drag event functions.
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;
}
// When a node is clicked, show the editing controls.
function nodeClicked(event, d) {
selectedNode = d;
let controls = document.getElementById(‘nodeControls’);
controls.style.display = ‘block’;
document.getElementById(‘nodeColor’).value = d.color;
document.getElementById(‘nodeSize’).value = d.size;
}
}
// Export the current grid as a CSV file.
function exportCSV() {
let csv = “”;
let table = document.getElementById(“grid”);
// Loop through each table row.
for (let i = 0; i < table.rows.length; i++) {
let row = table.rows[i];
let rowData = [];
// For each cell in the row, grab the value of the contained input (if any).
for (let j = 0; j < row.cells.length; j++) {
let cell = row.cells[j];
let input = cell.querySelector("input");
// Wrap values in quotes to handle commas.
if (input) {
rowData.push('"' + input.value.trim() + '"');
} else {
rowData.push('""');
}
}
csv += rowData.join(",") + "\n";
}
// Create a Blob from the CSV data and trigger a download.
let blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
let url = URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = "ego_network.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}