Drawing an SVG line between multiple DOM objects
HTML offers the <canvas>
, but with some limitations, it's pixel based but can use SVGs but generally meant as "viewport" as opposed to DOM spanning. Rather than go into the "whys," Canvas doesn't quite fit what I'm after to create. SVGs can be positioned via absolute positioning anywhere on the viewport (just like any DOM object). Unlike other image types, the content inline SVGs can easily be altered via the DOM as they're XML data. This means I can easily change the color, or size, even shape of objects.
Hopefully, this tutorial is understandable for novices, more seasoned devs may want to skip the bottom for the codepen example. I've written my tutorial using only ES5 syntax although my codepen has some ES6 syntax.
Our Goals
- Draw an SVG line between objects on the screen.
- On resize change the SVGs position in objects on the screen have changed.
- Allow to have lines between multiple objects, and do this dynamically.
SVGs can be quite complicated, and drawing them with javascript is quite an art. There are plenty of libraries designed simply for manipulating SVGs and animating them but drawing a line is pretty easy.
The above will create a black line 1px wide that starts at 0 pixels and span 100 pixels to the right and 100 pixels down, to create a diagonal line.
Pictured: simple SVG line using the above code
Offset
It's 2018, but jQuery still has its place, offset
reliably can get us the absolute positioning of elements on the screen to the document as its base even if they aren't absolute positioned. This isn't a complete win for our goal of drawing a point between two objects as this only gets us the top-left corner of our a <div>
. We need the center of that a <div>
.
So with a bit of simple math, we can figure out the center position of a <div>
by querying the width and height of the div and dividing by 2, then adding it to the offset position. This will measure diagonally to the center.
Now to draw an SVG, we need two sets of center coordinates. X1, Y1 and X2, Y2.
Now if we just apply this to #mySVG
, we can draw a line that goes between the center of these two hypothetical DOM objects. We want to place the SVG in our body tag and then give it some really basic styling in CSS so it can occupy any space on the viewport.
Resizing Event
Not bad right? What happens if we resize? Our hypothetical DOM objects on the screen might move, thus we'd need a window resize event. We better make this a function now, and clean up the legibility first.
Adding resizing is pretty easy now:
More objects!
Pretty nifty right? Now that we've covered the basics of drawing and redrawing the SVG, we can use jQuery's clone
to duplicate our line within our SVG and call our drawSVG multiple times.
This isn't very dynamic as we're assuming we always know that we want to draw a line between 3 things on our screen. We're getting close but this isn't dynamic. It's time to break out .each and convert what we have into an object to cut down on our mess. We're going to do a few changes. First let's simplify our SVG.
Refactoring
Now that we're going to copy and paste our SVG, we don't need any co-ordinates. In fact, we do not want them at all until. A line without the required x1
, x2
, y1
, and y2
won't be rendered to the screen. This works for us as we want to use this as a template for future lines but do not want our original to display.
Also, for legibility, we can turn the messy code above into something more readable and hopefully maintainable.
Let's also assume all our hypthetical myPoints use the class .myPoint
and not IDs. We can now call our function drawBetweenObjects.drawSVG($(".myPoint"), more paramets)
Each()
If you're not familiar with iterating over arrays, now would be a grand time to learn. There are far better people who can explain it than I can. The long and short is using jQuery, we can create a variable that contains an array of objects based on our query. This way we can apply our drawSVG function to each entry in our array. This is where for newer javascript developers some of the lines of code might look confusing.
iterateOverObject
has some funky stuff, such as $(this)
which you have probably seen before. jQuery's $(this)
and javascript's this
aren't quite the same, What's the difference between '$(this)' and 'this'? but in the array of objects, it will use the current entry. Next we will need to select the next item in our query, eq
creates a new a query to the specific entry on the array of objects. So if I ask for var myLi = $("li")
, and there are four <li>
s on the page, myLi.eq(2)
would only require the data for the second <li>
. Using our index, we ask next in the list, using index + 1
.
Now we can update our script resizing.
Removing old clones
If you run the code, you'll be able to redraw the points, but the problem is our old lines are still in the DOM. So the best place to remove them is before we re-iterate over the object. So before we create new lines, we delete the old ones. Time to add a simple jQuery remove to the iterateOverObject
.
Now we should have an SVG line that will draw between any DOM objects with the class of .myPoint
and redraw the lines on a window resize.
Below is the hyper spiffy version that has a config file and the ability to delay rerenders on resize. To make this work for pages larger than 100vh, the SVG height of the #svg
would need to set the height as $(document).height();
See the Pen SVG Line Between Divs (multi-point) by Greg Gant (@fuzzywalrus) on CodePen.
It'd be feasible to make this without jQuery, by replacing jQuery with Document.querySelectorAll()
and removing the offset with techniques such as offsetLeft
and offsetTop
.