Is there a way to generate a SVG path connecting various DIV elements programmatically?

How can I achieve a dynamic SVG path between my word DIVs? This is my current progress and this is my desired outcome.

Could you please explain why my code isn't working as expected? I attempted to calculate the XY positions of my words to create an SVG path based on their start and end points.

HTML :

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="styles.css">
  <title>SVG Path Animation</title>
</head>

<body>
  <div id="container">
    <div class="word" id="w1" style="margin-left: 10%; margin-top: 1%;">First</div>
    <div class="word" id="w2" style="margin-left: 60%; margin-top: 2%;">Second</div>
    <div class="word" id="w3" style="margin-left: 20%; margin-top: 3%;">Third</div>
    <div class="word" id="w4" style="margin-left: 70%; margin-top: 4%;">Fourth</div>
    <div class="word" id="w5" style="margin-left: 30%; margin-top: 5%;">Fifth</div>
  </div>
  <script src="script.js"></script>
</body>

</html>


JavaScript :

document.addEventListener("DOMContentLoaded", function() {
    const wa = document.getElementById("w1");
    const wb = document.getElementById("w2");
    const wc = document.getElementById("w3");
    const wd = document.getElementById("w4");
    const we = document.getElementById("w5");
    const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");

    const x1 = wa.offsetLeft + wa.offsetWidth;
    const y1 = wa.offsetTop + wa.offsetHeight / 2;

    const x2 = wb.offsetLeft;
    const y2 = wb.offsetTop + wb.offsetHeight / 2;

    const x3 = wc.offsetLeft + wc.offsetWidth;
    const y3 = wc.offsetTop + wc.offsetHeight / 2;

    const x4 = wd.offsetLeft;
    const y4 = wd.offsetTop + wd.offsetHeight / 2;

    const x5 = we.offsetLeft + we.offsetWidth;
    const y5 = we.offsetTop + we.offsetHeight / 2;

    // svgPath.setAttribute("d", M${x1},${y1} L${x2},${y2});
    svgPath.setAttribute("d", `M ${x1},${y1} L ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x5},${y5}`);
    svgPath.setAttribute("stroke", "black");
    svgPath.setAttribute("stroke-width", "2");
    svgPath.setAttribute("fill", "none");

    document.body.appendChild(svgPath);

});


CSS :

.word {
    position: absolute;
    font-size: 16px;
  }

  #container {
    width: 1500px;
    height: 900px;
    position: relative;
  }

Answer №1

Robert Longson pointed out that in order to render the connecting line paths, you'll need a parent svg element.

To achieve this, you can add an absolutely positioned <svg> to your container div like shown below:

document.addEventListener("DOMContentLoaded", function() {
  const ns = "http://www.w3.org/2000/svg";
  const container = document.getElementById("container");
  const wa = document.getElementById("w1");
  const wb = document.getElementById("w2");
  const wc = document.getElementById("w3");
  const wd = document.getElementById("w4");
  const we = document.getElementById("w5");


  const x1 = wa.offsetLeft + wa.offsetWidth;
  const y1 = wa.offsetTop + wa.offsetHeight / 2;

  const x2 = wb.offsetLeft;
  const y2 = wb.offsetTop + wb.offsetHeight / 2;

  const x3 = wc.offsetLeft + wc.offsetWidth;
  const y3 = wc.offsetTop + wc.offsetHeight / 2;

  const x4 = wd.offsetLeft;
  const y4 = wd.offsetTop + wd.offsetHeight / 2;

  const x5 = we.offsetLeft + we.offsetWidth;
  const y5 = we.offsetTop + we.offsetHeight / 2;

  // create parent svg
  const svg = document.createElementNS(ns, "svg");
  // apply same dimensions as container element to svg 
  svg.setAttribute("viewBox", "0 0 1500 900");
  svg.setAttribute(
    "style",
    "position:absolute; width:100%; height:100%; left:0; top:0;"
  );
  const svgPath = document.createElementNS(ns, "path");
  // svgPath.setAttribute("d", M${x1},${y1} L${x2},${y2});
  svgPath.setAttribute(
    "d",
    `M ${x1},${y1} L ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x5},${y5}`
  );
  svgPath.setAttribute("stroke", "black");
  svgPath.setAttribute("stroke-width", "2");
  svgPath.setAttribute("fill", "none");


  // append to container div
  svg.append(svgPath);
  container.insertBefore(svg, container.children[0]);

});
svg {
  width: 100%;
  border: 1px solid #ccc;
}

.word {
  position: absolute;
  font-size: 16px;
}

#container {
  width: 1500px;
  height: 900px;
  //max-width:100%;
  aspect-ratio: 15/9;
  position: relative;
}
<div id="container">
  <div class="word" id="w1" style="margin-left: 10%; margin-top: 1%;">Premier</div>
  <div class="word" id="w2" style="margin-left: 60%; margin-top: 2%;">Deuxième</div>
  <div class="word" id="w3" style="margin-left: 20%; margin-top: 3%;">Troisième</div>
  <div class="word" id="w4" style="margin-left: 70%; margin-top: 4%;">Quatrième</div>
  <div class="word" id="w5" style="margin-left: 30%; margin-top: 5%;">Cinquième</div>
</div>

An Alternative Approach: Rendering Everything to SVG

If you only need to display simple text elements, you could utilize svg <text>.

let words = [
  { text: "Premier", x: "10%", y: "1%" },
  { text: "Deuxième", x: "60%", y: "2%" },
  { text: "Troisième", x: "20%", y: "3%" },
  { text: "Quatrième", x: "70%", y: "4%" },
  { text: "Cinquième", x: "30%", y: "5%" }
];

let svg = document.getElementById("svg");
drawText(svg, words, (fontSize = 16));
function drawText(svg, words) {
  const ns = "http://www.w3.org/2000/svg";
  //const svg = document.createElementNS(ns, "svg");
  words.forEach((word) => {
    let textEl = document.createElementNS(ns, "text");
    let { text, x, y } = word;

    textEl.setAttribute("font-size", fontSize);
    textEl.setAttribute("x", x);
    textEl.setAttribute("y", y);

    // baseline adjust
    textEl.setAttribute("dy", fontSize);
    textEl.textContent = text;
    svg.append(textEl);
  });
}

drawLines(svg);
function drawLines(svg, margin = 5) {
  const ns = "http://www.w3.org/2000/svg";
  let textEls = svg.querySelectorAll("text");
  for (let i = 1; i < textEls.length; i += 1) {
    let textEl0 = textEls[i - 1];
    let textEl = textEls[i];
    let bb0 = textEl0.getBBox();
    let bb = textEl.getBBox();

    let x1, x2, y1, y2;
    //left to right
    if (bb0.x < bb.x) {
      x1 = bb0.x + bb0.width + margin;
      y1 = bb0.y + bb0.height / 2;
      x2 = bb.x - margin;
      y2 = bb.y + bb.height / 2;
    }
    // right to left
    else {
      x1 = bb.x + bb.width + margin;
      y1 = bb.y + bb.height / 2;
      x2 = bb0.x - margin;
      y2 = bb0.y + bb0.height / 2;
    }
    let line = document.createElementNS(ns, "line");
    line.setAttribute("stroke-width", "1");
    line.setAttribute("stroke", "#000");
    line.setAttribute("x1", x1);
    line.setAttribute("y1", y1);
    line.setAttribute("x2", x2);
    line.setAttribute("y2", y2);
    svg.append(line);
  }
}
svg{
  width: 100%;
  border: 1px solid #ccc;
}
  <svg id="svg" viewBox="0 0 1500 900">
  </svg>

The main advantage of this method is that your diagram can be scaled proportionally.

There are some drawbacks:
<text> elements do not behave like HTML block elements, so if you require borders for example, you would need to calculate and include <rect> elements based on the bounding box of the <text> element.
For more complex text box contents, you might consider using <foreignObject>. However, please note that foreignObjects may not display correctly in all graphic or viewing applications aside from web browsers.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Finding elements using XPath in Selenium

When using Firebug to inspect an input field I need to fill in, I found this code: <label for="form:composite:tabView:ssn">Fødselsnummer</label> In my selenium script, I tried the following: WebElement element = driver.findElement(By.xpath ...

Flexbox keeps elements on the same line without breaking

I'm currently delving into flexbox and aiming to create a layout similar to the one shown below: https://i.sstatic.net/epnkU.png Here is the code I have come up with: .container { display: flex; gap: 26px; } .flex50 { flex: 50%; ...

CSS Color-Range Conditional Styling

I am interested in creating a simple chart using divs and spans, with the goal of implementing conditional formatting on each column based on its value to determine height. I have been struggling with figuring out how to make it work similar to Excel' ...

What is preventing the listener from activating?

I came across some HTML code that looks like this: <form id="robokassa" action="//test.robokassa.ru/Index.aspx" method="post"> <input type="text" id="OutSum" name="OutSum" value="" placeholder="Сумма пополнения"> ...

What is the best way to choose content that has been clicked on?

How do I display only clicked content? Currently, I have a system where clicking a button shows the data in the nav tag. The code is written in views.py. class TopView(TemplateView): model = Data template_name = 'index.html' def get ...

Guide to positioning a bootstrap navbar toggle button to the left or right of the logo

My React app includes a bootstrap navbar with the following code - <nav className="navbar navbar-default navbar-expand-sm navbar-light bg-light"> <div className="container-fluid"> <div className="navba ...

Retrieve the visitor's IP address following the submission of an AJAX form

Currently, I am facing an issue with my HTML form and JavaScript method integration. Whenever a visitor submits the form, a JavaScript method is triggered which sends an AJAX request to a PHP file on my server. The problem arises when trying to retrieve ...

Exploring Ways to Utilize Multiple Access Levels with PHP and MySQL

Currently, I am developing a PHP & MySQL management system for a college level institution. In this system, each employee and student will have their own account and I aim to display personalized pages for each user. In my scenario, there are 3 levels o ...

Proper positioning of popover ensures it does not exceed the boundaries of its parent

In my ngprime table, the header row contains a column field with a popover set to display at the top. However, it is covering the actual field instead of appearing above it. This issue arises because the popover cannot display outside of its parent div, ca ...

What is the best way to display a div in Chrome without allowing any user interactions?

I currently have a <div> placed on top of my webpage that follows the mouse cursor. Occasionally, users are able to move the mouse quickly enough to enter the tracking <div>. Additionally, this <div> sometimes prevents users from clicking ...

Tips for updating the background color when clicking in Vue

I've been attempting to change the background color of an element upon clicking it using Vue, but so far I haven't had any success. Here's what I have come up with, including a method that has two functions for the onclick event in Vue. &l ...

Functionality of local data storage

I'm currently exploring the capabilities of the localStorage feature. Let's say I have a large JSON object stored using localStorage. I need to share this data with the testing team for review. However, if I try to display the stored data on an H ...

Development of multiple interactive 3D models for the website

I have created a stunning 3D model of a city featuring 4 unique buildings, and now I want to take it to the next level by making them interactive. My vision is for users to hover over a building and see text appear on top of it, with the option to click an ...

Eliminating the background color upon clicking

Here is the link structure I have set up: <ul> <li><a href="home.html"><span>home</span></a></li> </ul> However, when I click on the link, a shadow appears. I would like the link to appea ...

Resize a responsive grid of images

Within my slider, I have three slides with 3 images each. The layout consists of one large image on the left and two smaller ones on the right. To ensure responsiveness, I've used max-width: 100% on the images so they can scale down proportionally as ...

How about I visit the campgrounds/edit page for a change?

In this get route, the previous link it was redirected from is stored in the req.session object under returnTo. Once redirected, it goes to the /login and we are able to view the object in the console. router.get('/login', (req, res) => { ...

Issue: Module './' could not be located

Encountered this error while trying to launch my application using NPM start. Strangely, it worked perfectly on another computer. I unzipped the file, ran npm install, and attempted to start the app, only to be hit with the following error: Any assistance ...

Issue with file path reaching into the public directory

Here is the image of the folder path:- https://i.sstatic.net/GESWX.png The CSS file seems to not be working in the exercises folder. It is only importing the header-ejs file without any styles, but it works fine for chapters and other files. How can I re ...

Create a new instance of the TypeScript singleton for each unit test

I have a TypeScript singleton class structured like this: export default class MySingleton { private constructor({ prop1, prop2, ... }: MySingletonConfig) { this.prop1 = prop1 ?? 'defaultProp1'; this.prop2 = prop2; ...

The use of ReactDom.render is no longer permissible in Next.js

I just set up a fresh Next JS application using Next 12. Encountering this issue consistently on every page load in the browser: Alert: The use of ReactDOM.render is no longer supported in React 18. Please switch to createRoot instead. Until you make th ...