From c585977ce5dc81a6385244e7922ed78e6794ce3f Mon Sep 17 00:00:00 2001 From: Elizabeth Lawrence Date: Fri, 23 May 2025 16:54:10 -0400 Subject: [PATCH] fix zoom, drag functionality, improve structure of functions * rehaul of how functions are set up for better modularity * fixed bug where after clicking link zoom+drag functionality stopped working * added slightly different hover colours after node/links are selected --- script.js | 665 ++++++++++++++++++++++------------------------------- styles.css | 9 +- 2 files changed, 279 insertions(+), 395 deletions(-) diff --git a/script.js b/script.js index 2e6a97e..505b711 100644 --- a/script.js +++ b/script.js @@ -75,7 +75,6 @@ const questions = [ { text: "What new technology (equipment) can be used in your study?", tags: ["Planning"] }, { text: "What Essential Ocean Variables will you be collecting?", tags: ["Planning"] }, { text: "How can we strengthen the connection with other ocean observation components?", tags: ["Planning"] }, - { text: "Have you communicated with the relevant stakeholders' (needs and expectations)?", tags: ["Planning"] }, // Data Collection @@ -149,8 +148,8 @@ const questions = [ { text: "What key metrics should I be tracking to assess my effectiveness in evaluation?", tags: ["Evaluation"] }, { text: "Is your Data Management Plan working?", tags: ["Evaluation"] }, { text: "Does my review result in a diverse perspective, or reinforcing existing assumptions?", tags: ["Evaluation"] }, - - { text: "Have you thought about a data management plan?", tags: ["Planning", "Data Management"] }, + + { text: "Have you thought about a data management plan?", tags: ["Planning", "Data Management"] }, { text: "Do you have a strategy for outreach?", tags: ["Communication & Outreach"] }, { text: "Have you evaluated your data collection methods?", tags: ["Evaluation", "Data Collection"] }, { text: "Have you considered the societal impact of your work?", tags: ["Application in Society"] }, @@ -162,420 +161,286 @@ const questions = [ { text: "Did your data collection methods work as expected?", tags: ["Data Collection", "Evaluation"] }, ]; -/////////////// Set up the space for the spiderweb ////////////////////// -// const svgWidth = 800; // Set the width of the SVG box -// const svgHeight = 600; // Set the height of the SVG box -// const radius = 200; // Circle radius -// const centerX = svgWidth / 2; -// const centerY = svgHeight / 2; -const container = document.getElementById('spiderweb'); -const svg = d3.select('#spiderweb').append('svg'); - -// Function to update SVG dimensions -function updateSvgDimensions() { - const svgWidth = container.offsetWidth; - const svgHeight = container.offsetHeight || (svgWidth * 0.8); // Fallback height - console.log(`Container width: ${svgWidth}, height: ${svgHeight}`); - - svg.attr('width', svgWidth).attr('height', svgHeight); -} - -// Initialize the dimensions -updateSvgDimensions(); - -// Use ResizeObserver to listen for container size changes -const resizeObserver = new ResizeObserver(() => { - updateSvgDimensions(); -}); - -// Observe the container -resizeObserver.observe(container); - -let svgWidth = container.offsetWidth; -let svgHeight = container.offsetHeight || (svgWidth * 0.8); -// let svgWidth = window.innerWidth * 0.5; // % of viewport width -// let svgHeight = window.innerHeight * 0.5; // % of viewport height -let radius = Math.min(svgWidth, svgHeight) * 0.35; // Adjust radius based on dimensions -let centerX = svgWidth / 2; -let centerY = svgHeight / 2; +const tooltip = d3.select("#tooltip"); +let multiSelectMode = false; +// Initialize SVG +const container = document.getElementById("spiderweb"); +const svg = d3.select(container).append("svg"); +const svgGroup = svg.append("g"); -// Assign positions for nodes -function updateNodePositions() { - centerX = svgWidth / 2; - centerY = svgHeight / 2; - radius = Math.min(svgWidth, svgHeight) * 0.35; +const dimensions = { width: 0, height: 0, radius: 0, centerX: 0, centerY: 0 }; - data.nodes.forEach((node, i) => { - const angle = (i / data.nodes.length) * 2 * Math.PI - Math.PI / 2; - node.x = centerX + radius * Math.cos(angle); - node.y = centerY + radius * Math.sin(angle); - }); +function updateDimensions() { + dimensions.width = container.offsetWidth; + dimensions.height = container.offsetHeight || dimensions.width * 0.8; + dimensions.radius = Math.min(dimensions.width, dimensions.height) * 0.35; + dimensions.centerX = dimensions.width / 2; + dimensions.centerY = dimensions.height / 2; - // Update node and link positions - node - .attr("cx", d => d.x) - .attr("cy", d => d.y); - - link - .attr("x1", d => data.nodes.find(node => node.id === d.source).x) - .attr("y1", d => data.nodes.find(node => node.id === d.source).y) - .attr("x2", d => data.nodes.find(node => node.id === d.target).x) - .attr("y2", d => data.nodes.find(node => node.id === d.target).y); - - svgGroup.selectAll(".node-label") - .attr("x", d => d.x + (22 * Math.cos(Math.atan2(d.y - centerY, d.x - centerX)))) - .attr("y", d => d.y + (22 * Math.sin(Math.atan2(d.y - centerY, d.x - centerX)))) - .attr("text-anchor", d => { - const angle = Math.atan2(d.y - centerY, d.x - centerX); - return Math.cos(angle) > 0 ? "start" : "end"; // Correct side for horizontal alignment - }) - .attr("alignment-baseline", d => { - const angle = Math.atan2(d.y - centerY, d.x - centerX); - return Math.sin(angle) > 0 ? "hanging" : "alphabetic"; // Correct vertical alignment - }) - .text(d => d.name|| d.id);; + svg.attr("width", dimensions.width).attr("height", dimensions.height); } -// data.nodes.forEach((node, i) => { -// const angle = (i / data.nodes.length) * 2 * Math.PI - Math.PI / 2; // Start at top -// node.x = centerX + radius * Math.cos(angle); -// node.y = centerY + radius * Math.sin(angle); -// }); - -// Create the SVG canvas -// const svg = d3.select("#spiderweb").append("svg") -// .attr("width", svgWidth) -// .attr("height", svgHeight) -// .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`) -// .attr("preserveAspectRatio", "xMidYMid meet"); - -// Tooltip -const tooltip = d3.select("#tooltip"); - -// Selection mode toggle -let multiSelectMode = false; -document.getElementById("toggle-selection-mode").addEventListener("change", (event) => { - multiSelectMode = event.target.checked; - document.getElementById("toggle-label").textContent = multiSelectMode ? "Multi-Select" : "Single Select"; -}); - - -/////////////// Set up functions for zoom etc ////////////////////// -// Add zoom and pan behavior const zoom = d3.zoom() - .scaleExtent([0.5, 5]) // Define the zoom scale range - .on("zoom", (event) => { - svgGroup.attr("transform", event.transform); // Apply zoom and pan - }); - +.scaleExtent([0.5, 5]) +.on("zoom", (event) => { + svgGroup.attr("transform", event.transform); +}); svg.call(zoom); -// Function to reset zoom to default position -function updateZoomForSelectedItems() { - // Get selected nodes and links - const selectedNodes = d3.selectAll(".node.selected").data(); - const selectedLinks = d3.selectAll(".link.selected").data(); - - if (selectedNodes.length === 0 && selectedLinks.length === 0) return; - - // Calculate the bounding box of all selected nodes and links - const allSelectedElements = [...selectedNodes, ...selectedLinks.flatMap(link => [link.source, link.target])]; - const xValues = allSelectedElements.map(e => e.x); - const yValues = allSelectedElements.map(e => e.y); - - // Compute the center of the selected elements - const centerX = d3.mean(xValues); - const centerY = d3.mean(yValues); - - // Compute the scale based on the bounding box of the selected elements - const xExtent = d3.extent(xValues); - const yExtent = d3.extent(yValues); - const width = xExtent[1] - xExtent[0]; - const height = yExtent[1] - yExtent[0]; - const padding = 50; - - const scale = Math.min( - (svgWidth - 2 * padding) / width, - (svgHeight - 2 * padding) / height - ); - - // Apply the zoom transformation to the center of the selected elements - const transform = d3.zoomIdentity - .translate(svgWidth / 2, svgHeight / 2) - .scale(scale) - .translate(-centerX, -centerY); - - svg.transition() - .duration(750) - .call(zoom.transform, transform); +function findNodeById(id) { + return data.nodes.find((node) => node.id === id); } - -function resetZoom() { - const transform = d3.zoomIdentity - .translate(svgWidth / 2, svgHeight / 2) // Center the zoom to the center of the SVG - .scale(1) // Set the zoom scale to 1 (default zoom level) - .translate(-centerX, -centerY); // Adjust for the center of the graph (optional) - - svg.transition() - .duration(750) - .call(zoom.transform, transform); // Apply the zoom transform with the reset state +function initializeNodes() { + svgGroup + .selectAll(".node") + .data(data.nodes) + .enter() + .append("circle") + .attr("class", "node") + .attr("r", 15) + .on("mouseover", showTooltip) + .on("mouseout", hideTooltip) + .on("click", toggleNodeSelection); } -let svgMoved = false; // Flag to ensure the SVG moves only once -// Function to handle the SVG transformation -function adjustSvg() { - const svgContainer = d3.select("#spiderweb").node(); // Get the SVG container - const svgBoundingRect = svgContainer.getBoundingClientRect(); // Get dimensions and position - - // Set the info panel width as 50% of the screen width - const infoPanelWidth = window.innerWidth * 0.5; - - // Calculate translation to shift the SVG left of the info panel - //const translateX = -(infoPanelWidth / 2.5); // Shift left by half the info panel width - //const translateY = 0; // No vertical translation +function initializeLinks() { + svgGroup + .selectAll(".link") + .data(data.links) + .enter() + .append("line") + .attr("class", "link") + .on("mouseover", showTooltip) + .on("mouseout", hideTooltip) + .on("click", toggleLinkSelection); +} - // Define a dynamic scale factor - //const scale = 0.70; // Shrinks to % of its size +function initializeLabels() { + svgGroup + .selectAll(".node-label") + .data(data.nodes) + .enter() + .append("text") + .attr("class", "node-label") + .text((d) => d.name || d.id) + .on("mouseover", showTooltip) + .on("mouseout", hideTooltip); +} - // Ensure it doesn't move off-screen - const maxTranslateX = -(svgBoundingRect.width * (1 - scale)) / 2; - const finalTranslateX = Math.max(translateX, maxTranslateX); // Ensure it stays within bounds +function positionElements() { + data.nodes.forEach((node, i) => { + const angle = (i / data.nodes.length) * 2 * Math.PI - Math.PI / 2; + node.x = dimensions.centerX + dimensions.radius * Math.cos(angle); + node.y = dimensions.centerY + dimensions.radius * Math.sin(angle); + }); - // Apply the transformation - d3.select("svg") - .transition() - .duration(300) // Smooth transition for resizing - .attr("transform", `translate(${finalTranslateX}, ${translateY}) scale(${scale})`); + svgGroup.selectAll(".node").attr("cx", (d) => d.x).attr("cy", (d) => d.y); + svgGroup + .selectAll(".link") + .attr("x1", (d) => findNodeById(d.source).x) + .attr("y1", (d) => findNodeById(d.source).y) + .attr("x2", (d) => findNodeById(d.target).x) + .attr("y2", (d) => findNodeById(d.target).y); + svgGroup + .selectAll(".node-label") + .attr("x", (d) => d.x + 22 * Math.cos(Math.atan2(d.y - dimensions.centerY, d.x - dimensions.centerX))) + .attr("y", (d) => d.y + 22 * Math.sin(Math.atan2(d.y - dimensions.centerY, d.x - dimensions.centerX))) + .attr("text-anchor", (d) => { + const angle = Math.atan2(d.y - dimensions.centerY, d.x - dimensions.centerX); + return Math.cos(angle) > 0 ? "start" : "end"; + }) + .attr("alignment-baseline", (d) => { + const angle = Math.atan2(d.y - dimensions.centerY, d.x - dimensions.centerX); + return Math.sin(angle) > 0 ? "hanging" : "alphabetic"; + }); + } -////////////////////////// Set up the nodes and links of the spiderweb ////////////////////// -// Create a group to hold nodes and links -const svgGroup = svg.append("g"); - -// Add links -const link = svgGroup.selectAll(".link") - .data(data.links) - .enter() - .append("line") - .attr("class", "link") - .attr("x1", d => { - const sourceNode = data.nodes.find(node => node.id === d.source); - return sourceNode.x; - }) - .attr("y1", d => { - const sourceNode = data.nodes.find(node => node.id === d.source); - return sourceNode.y; - }) - .attr("x2", d => { - const targetNode = data.nodes.find(node => node.id === d.target); - return targetNode.x; - }) - .attr("y2", d => { - const targetNode = data.nodes.find(node => node.id === d.target); - return targetNode.y; - }) - .on("click", (event, d) => { - // Add selected class to the clicked link - const sourceNode = data.nodes.find(node => node.id === d.source); - const targetNode = data.nodes.find(node => node.id === d.target); - - const isSelected = d3.select(event.currentTarget).classed("selected"); - - if (multiSelectMode) { - // Toggle the selected class on the clicked link - const link = d3.select(event.currentTarget); - const isLinkSelected = link.classed("selected"); - link.classed("selected", !isLinkSelected); - - // Re-evaluate node selection based on remaining selected links - d3.selectAll(".node").classed("selected", node => { - // Check if the node is part of any currently selected links - const isConnectedToSelectedLink = d3 - .selectAll(".link.selected") - .data() - .some(link => link.source === node.id || link.target === node.id); - - return isConnectedToSelectedLink; - updateZoomForSelectedItems(); +function toggleNodeSelection(event, node) { + const nodeElement = d3.select(this); + const isSelected = nodeElement.classed("selected"); + const nodeId = node.id; + + if (multiSelectMode) { + if (isSelected) { + // Deselect the node itself + nodeElement.classed("selected", false); + + // Deselect all links connected to this node + d3.selectAll(".link").each(function(link) { + if (link.source === nodeId || link.target === nodeId) { + d3.select(this).classed("selected", false); + } }); + hideInfoPanel();resetZoom(); + // Do NOT try to deselect other nodes here! } else { - if (isSelected) { - // If the link is already selected, deselect it and reset zoom - d3.select(event.currentTarget).classed("selected", false); - resetZoom(); - } else { - // Zoom into the link if it's not selected - const transform = d3.zoomIdentity - .translate(svgWidth / 2, svgHeight / 2) - .scale(2) - .translate(-d.x, -d.y); - - svg.transition() - .duration(750) - .call(zoom.transform, transform); - // Single-select mode: clear all and highlight the current link and nodes - d3.selectAll(".node").classed("selected", false); - d3.selectAll(".link").classed("selected", false); - - d3.selectAll(".node").classed("selected", node => { - return node.id === sourceNode.id || node.id === targetNode.id; - }); - d3.select(event.currentTarget).classed("selected", true); + // Select the node itself + nodeElement.classed("selected", true); + } + } else { + if (isSelected) { + // Deselect if the node is already selected + d3.selectAll(".link, .node").classed("selected", false); + hideInfoPanel();resetZoom(); + } else { + // Clear all selections and select the clicked node + d3.selectAll(".link, .node").classed("selected", false); + nodeElement.classed("selected", true); } } - // Update the info panel with the current selection - updateInfoPanel(); + + if (!isSelected) zoomToTarget([node]); + updateInfoPanel(); +} + +function toggleLinkSelection(event, link) { + const linkElement = d3.select(this); + const isSelected = linkElement.classed("selected"); + + const sourceNodeId = link.source; + const targetNodeId = link.target; + const sourceNode = findNodeById(sourceNodeId); + const targetNode = findNodeById(targetNodeId); + + if (multiSelectMode) { + if (isSelected) { + // Deselect the link itself + linkElement.classed("selected", false); + + // Check if the source and target nodes are connected to any other selected links + const selectedLinks = d3.selectAll(".link.selected").data(); + + // Determine if sourceNode should remain selected + const isSourceConnected = selectedLinks.some( + (otherLink) => + otherLink.source === sourceNodeId || otherLink.target === sourceNodeId + ); + + // Determine if targetNode should remain selected + const isTargetConnected = selectedLinks.some( + (otherLink) => + otherLink.source === targetNodeId || otherLink.target === targetNodeId + ); + + // Deselect sourceNode if not connected to other selected links + if (!isSourceConnected) { + d3.selectAll(".node").each(function (node) { + if (node.id === sourceNodeId) { + d3.select(this).classed("selected", false); + } + }); + } + + // Deselect targetNode if not connected to other selected links + if (!isTargetConnected) { + d3.selectAll(".node").each(function (node) { + if (node.id === targetNodeId) { + d3.select(this).classed("selected", false); + } + }); + } + hideInfoPanel();resetZoom(); + } else { + // Select the link and connected nodes + linkElement.classed("selected", true); - if (!svgMoved) { - adjustSvg(); // Perform initial adjustment - svgMoved = true; // Set the flag so this block doesn't execute again + d3.selectAll(".node").each(function (node) { + if (node.id === sourceNodeId || node.id === targetNodeId) { + d3.select(this).classed("selected", true); + } + }); } - }) - .on("mouseover", (event, d) => { - tooltip.style("left", `${event.pageX + 20}px`) - .style("top", `${event.pageY + 20}px`) - .style("display", "inline-block") - .html(`Connection:
${d.source} ↔ ${d.target}`) ; - }) - .on("mouseout", () => { - tooltip.style("display", "none"); - }); - -// Add nodes -const node = svgGroup.selectAll(".node") - .data(data.nodes) - .enter() - .append("circle") - .attr("class", "node") - .attr("r", 15) - .attr("cx", d => d.x) - .attr("cy", d => d.y) - .attr("fill", "#1f78b4") - .on("mouseover", (event, d) => { - tooltip.style("left", `${event.pageX + 10}px`) - .style("top", `${event.pageY + 10}px`) - .style("display", "inline-block") - .html(`${d.id}
${d.description}`); - }) - .on("mouseout", () => { - tooltip.style("display", "none"); - }) - .on("click", (event, d) => { - const node = d3.select(event.currentTarget); - const isSelected = node.classed("selected"); - - if (multiSelectMode) { - // Toggle selected class on the clicked node - node.classed("selected", !isSelected); - updateZoomForSelectedItems(); + } else { + // Single-select mode logic + if (isSelected) { + d3.selectAll(".link, .node").classed("selected", false); + hideInfoPanel();resetZoom(); } else { - if (isSelected) { - // If the node is already selected, deselect it and reset zoom - node.classed("selected", false); - resetZoom(); - } else { - // Zoom into the node if it's not selected - const transform = d3.zoomIdentity - .translate(svgWidth / 2, svgHeight / 2) - .scale(2) - .translate(-d.x, -d.y); - - svg.transition() - .duration(750) - .call(zoom.transform, transform); - // Deselect all nodes and links, then select the clicked node - d3.selectAll(".node").classed("selected", false); - d3.selectAll(".link").classed("selected", false); - node.classed("selected", true); + d3.selectAll(".link, .node").classed("selected", false); + linkElement.classed("selected", true); + + d3.selectAll(".node").each(function (node) { + if (node.id === sourceNodeId || node.id === targetNodeId) { + d3.select(this).classed("selected", true); + } + }); } } - // Show info panel with questions related to the selected nodes and links - updateInfoPanel(); - if (!svgMoved) { - adjustSvg(); // Perform initial adjustment - svgMoved = true; // Set the flag so this block doesn't execute again - } - }); + + if (!isSelected)zoomToTarget([sourceNode, targetNode]); + updateInfoPanel(); +} +function zoomToTarget(targetNodes = []) { + const svg = d3.select("svg"); // Adjust selector based on your setup + const g = svg.select("g"); // Assuming your graph elements are within a element + + if (targetNodes.length === 0) { + // Reset zoom to default view + const resetTransform = d3.zoomIdentity.translate(0, 0).scale(1); + svg.transition() + .duration(750) + .call(zoom.transform, resetTransform); // Update d3.zoom state + return; + } -// Reset zoom and deselect all nodes and links -document.getElementById("deselect-all").addEventListener("click", () => { - d3.selectAll(".node").classed("selected", false); - d3.selectAll(".link").classed("selected", false); - hideInfoPanel(); - resetZoom(); - svgMoved = false; -}); + const bounds = { x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity }; - // Handle showing the info panel -function showInfoPanel(content) { - d3.select("#info-panel") - .style("display", "block"); - // Add content to the info panel - d3.select("#info-content").html(content); - // Add the close button behavior - //document.getElementById("close-info-panel").addEventListener("click", hideInfoPanel); -} + targetNodes.forEach(node => { + bounds.x1 = Math.min(bounds.x1, node.x); + bounds.y1 = Math.min(bounds.y1, node.y); + bounds.x2 = Math.max(bounds.x2, node.x); + bounds.y2 = Math.max(bounds.y2, node.y); + }); -// Handle hiding the info panel -function hideInfoPanel() { - // Hide the info panel - d3.select("#info-panel") - .style("display", "none"); + const width = bounds.x2 - bounds.x1; + const height = bounds.y2 - bounds.y1; + const centerX = (bounds.x1 + bounds.x2) / 2; + const centerY = (bounds.y1 + bounds.y2) / 2; - resetZoom(); + const svgWidth = parseInt(svg.style("width")); + const svgHeight = parseInt(svg.style("height")); - // Reset the spiderweb scale - // svg.transition().duration(300) - // .attr("transform", "scale(1) translate(0, 0)"); + const scale = Math.min(svgWidth / width, svgHeight / height, 2); // Cap scale at 2 for max zoom + const translate = [ + svgWidth / 2 - scale * centerX - 100, + svgHeight / 2 - scale * centerY, + ]; - // Remove selected class from all nodes and links - d3.selectAll(".node").classed("selected", false); - d3.selectAll(".link").classed("selected", false); + const transform = d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale); - // Reset svgMoved flag - svgMoved = false; + svg.transition() + .duration(750) + .call(zoom.transform, transform); // Update d3.zoom state } -// Add event listener to close button -document.getElementById("close-info-panel").addEventListener("click", hideInfoPanel); - -// Add labels -svgGroup.selectAll(".node-label") - .data(data.nodes) - .enter() - .append("text") - .attr("class", "node-label") - .attr("x", d => d.x + (22 * Math.cos(Math.atan2(d.y - centerY, d.x - centerX)))) // Offset text outward - .attr("y", d => d.y + (22 * Math.sin(Math.atan2(d.y - centerY, d.x - centerX)))) // Offset text outward - .attr("text-anchor", d => { - const angle = Math.atan2(d.y - centerY, d.x - centerX); - return Math.cos(angle) > 0 ? "start" : "end"; // Correct side for horizontal alignment - }) - .attr("alignment-baseline", d => { - const angle = Math.atan2(d.y - centerY, d.x - centerX); - return Math.sin(angle) > 0 ? "hanging" : "alphabetic"; // Correct vertical alignment - }) - .text(d => d.name || d.id) - .on("mouseover", (event, d) => { - tooltip.style("left", `${event.pageX + 10}px`) - .style("top", `${event.pageY + 10}px`) - .style("display", "inline-block") - .html(`${d.id}
${d.description}`); - }) - .on("mouseout", () => { - tooltip.style("display", "none"); - }); +function resetZoom() { + const resetTransform = d3.zoomIdentity.translate(0, 0).scale(1); + svg.transition() + .duration(750) + .call(zoom.transform, resetTransform); // Update d3.zoom state +} +function showTooltip(event, d) { + const content = d.description || `${d.source} ↔ ${d.target}`; + tooltip.style("left", `${event.pageX + 10}px`) + .style("top", `${event.pageY + 10}px`) + .style("display", "inline-block") + .html(`${d.id || "Link"}
${content}`); +} +function hideTooltip() { + tooltip.style("display", "none"); +} - // Update info panel with questions related to selected nodes and links function updateInfoPanel() { + console.log("updateInfoPanel called"); const selectedNodes = d3.selectAll(".node.selected").data(); const selectedLinks = d3.selectAll(".link.selected").data(); - + const selectedNodeIds = selectedNodes.map(node => node.id); const selectedLinkNodeIds = selectedLinks.flatMap(link => [link.source, link.target]); @@ -598,27 +463,43 @@ function updateInfoPanel() { hideInfoPanel(); } } +function showInfoPanel(content) { + console.log("showInfoPanel called with content:", content); + d3.select("#info-content").html(content); + document.getElementById("info-panel").style.display = "block"; +} +function hideInfoPanel() { + console.log("hideInfoPanel called"); + document.getElementById("info-panel").style.display = "none"; + resetZoom(); +} -// Update dimensions on window resize -window.addEventListener("resize", () => { - svgWidth = window.innerWidth * 0.9; - svgHeight = window.innerHeight * 0.9; - - svg.attr("width", svgWidth) - .attr("height", svgHeight) - .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`); - - updateSvgDimensions(); - updateNodePositions(); +function resetSelection() { + d3.selectAll(".node, .link").classed("selected", false); + updateInfoPanel(); + zoomToTarget(); +} - if (svgMoved) { - adjustSvg(); // Recalculate transformation on resize - } +document.getElementById("toggle-selection-mode").addEventListener("change", (e) => { + multiSelectMode = e.target.checked; + document.getElementById("toggle-label").textContent = multiSelectMode ? "Multi-Select" : "Single Select"; }); +document.getElementById("close-info-panel").addEventListener("click", hideInfoPanel); +document.getElementById("deselect-all").addEventListener("click", resetSelection); -// Initial positioning -updateNodePositions(); +function initialize() { + updateDimensions(); + initializeLinks(); + initializeNodes(); + initializeLabels(); + positionElements(); + + const resizeObserver = new ResizeObserver(() => { + updateDimensions(); + positionElements(); + }); + resizeObserver.observe(container); +} -console.log(data.nodes); -console.log(data.links); +initialize(); \ No newline at end of file diff --git a/styles.css b/styles.css index 1a597c6..016e20d 100644 --- a/styles.css +++ b/styles.css @@ -110,8 +110,8 @@ svg { stroke-width: 1.75px; } .node:hover { - fill: #ff9900; /* Change to a different color when selected */ - stroke: #a16305; + fill: #ffb700; /* Change to a different color when selected */ + stroke: #bd9406; stroke-width: 1.75px; } /* Text labels for nodes */ @@ -143,7 +143,10 @@ svg { stroke: #ff9900; stroke-width: 6px; } - +.link.selected:hover { + stroke: #fccd86fb; + stroke-width: 6px; +} /* Default link label style */ .link-label { font-family: 'Montserrat', sans-serif;