Creating a triangle connecting a div and a point on an image

I'm trying to sort out the best way to create the lower opacity triangles you see that connect the boxes with the names of the deltas on the left with their locations on the right. The solution only needs to work on larger viewports (once the elements stack the triangles aren't necessary). My default idea is finding a way to get three points with JS (top and bottom right of the box and then a rough estimation of the point on the map), then create a shape from those three points (somehow). I feel like there might be a way to do it with pseudo elements, but I'm not sure that's actually possible. What's the right approach here? If it matters, it's setup using grid, and it goes 1 (white boxes), 2 (map), 3 (colored boxes) so that it will stack in the right order on narrower viewports.
No description
13 Replies
Chris Bolson
Chris Bolson3w ago
I would probably use JavaScript generated SVGs to join the boxes with their locations on the map. Are the points actual icons/images placed on the map or are they part of the original image? If they are elements placed on the map you can use JS to get their position and use that as one of the "points" of the SVG path. The other 2 poiints (as this is a triangle) would be the top-right and bottom-right of the corresponding label element.
DigitalCharlie
I could do it either way — I was thinking about having them be part of the image just to guarantee they're anchored correctly no matter what, but if they're actual elements it would let people click on them which might be good. I haven't done much with SVGs (yet). Do you have a resource you might recommend I look at to start?
ἔρως
ἔρως3w ago
mdn is a good resource for svg and inkscape is a somewhat complicated but usable vector editor i think photopea can do vector, but im not sure yes, it works with vectors too you can try it (and yes, it's a website)
Chris Bolson
Chris Bolson3w ago
Creating an SVG in JS is much like creating any other html element except that, instead of using createElement you use createElementNS You then define the properties a normal. An SVG is made up of a container and then it's "contents". This might be a path (lines) a shape, (polygon, circle etc.) or other elements. In your case, as you want a triangle, you would need to define a polygon. The good thing is that a single SVG can (and normally does) contain many elements so in fact you actually only need a single SVG. This means that you can add all of your triangles within a single SVG element. I will try to give an outline of how you could achieve this:
// create SVG element
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
svg.style.position = 'absolute';
svg.style.inset = '0';
// create SVG element
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
svg.style.position = 'absolute';
svg.style.inset = '0';
In this case you will need the SVG to be able to extend across the whole viewport so it has width and height of 100% You then need to calculate the positions of your elements, both the starting element and it's corresponding end point. Specifically you need to get the top-right and bottom-right points of your starting element and the left-center point of your image on the map. You can use getBoundingClientRect to get the element data which will give you an object containing data amongst which are the top,right,bottom & left points. You need these to define your 3 points of your polygon, something like this:
// select start and end elements
const startElement = document.querySelector("#start-element");
const endElement = document.querySelector("#end-element");

// Get the bounding rectangles start and end elements
const startElementRect = startElement.getBoundingClientRect();
const endElementRect = endElement.getBoundingClientRect();

// define first point - right-top of first element
const point1 = {
x: startElementRect.right,
y: startElementRect.top
};
// define second point - right-bottom of first element
const point2 = {
x: startElementRect.right,
y: startElementRect.bottom
};
// define last point - left-center of first element
const point3 = {
x: endElementRect.left,
y: (endElementRect.top + endElementRect.bottom) / 2
};
// select start and end elements
const startElement = document.querySelector("#start-element");
const endElement = document.querySelector("#end-element");

// Get the bounding rectangles start and end elements
const startElementRect = startElement.getBoundingClientRect();
const endElementRect = endElement.getBoundingClientRect();

// define first point - right-top of first element
const point1 = {
x: startElementRect.right,
y: startElementRect.top
};
// define second point - right-bottom of first element
const point2 = {
x: startElementRect.right,
y: startElementRect.bottom
};
// define last point - left-center of first element
const point3 = {
x: endElementRect.left,
y: (endElementRect.top + endElementRect.bottom) / 2
};
As you can (hopefully) see, you should have 3 points within the viewport. Now you need to generate the polygon to add to the SVG that we have already created:
// Create a polygon (triangle)
const polygon = document.createElementNS(svgNS, "polygon");
polygon.setAttribute("points", `${point1.x},${point1.y} ${point2.x},${point2.y} ${point3.x},${point3.y}`);
polygon.setAttribute("fill", "lightblue");
// Create a polygon (triangle)
const polygon = document.createElementNS(svgNS, "polygon");
polygon.setAttribute("points", `${point1.x},${point1.y} ${point2.x},${point2.y} ${point3.x},${point3.y}`);
polygon.setAttribute("fill", "lightblue");
You now need to add this polygon to the SVG
// add ppolygon to SVG
svg.appendChild(polygon);
// add ppolygon to SVG
svg.appendChild(polygon);
In the HTML add an empty container where you will add the SVG:
<div id="svg-container"></div>
<div id="svg-container"></div>
Then finally, back in the JS, append the SVG to this empty container
// add SVG to DOM
document.querySelector("#svg-container").appendChild(svg);
// add SVG to DOM
document.querySelector("#svg-container").appendChild(svg);
sorry for all the code 😟 Note - as you have multiple elements that require triangles your will need to create a loop of some sort to generate each separate polygon. My demo code would only look for 2 specific elements.
DigitalCharlie
Thank you! I had started working a bunch of this out, but was stuck at setting the points because I couldn't get it to actually set properly is there a good reason to fully generate it with JS vs putting the elements in the HTML and then updating them?
Chris Bolson
Chris Bolson3w ago
you are right, you could do that. However bear in mind that, possibly, your might have more elements or that they might be dynamic. It could also cause issues on different screen sizes. This way the JS takes care of the positioning automatically and allows you to have more or less elements without having to touch the code. I would also add a viewport resize function that recalculates the values if the user changes the size. But yes, you could hard code them. You are right.
DigitalCharlie
Got it. I had just been thinking about writing one function that updates and hard coding them in, then running the function on page load and on resize.
Chris Bolson
Chris Bolson3w ago
yes, that would work too
DigitalCharlie
Thank you so much! It's working now (I haven't done all 3 yet, but the first is working)
Chris Bolson
Chris Bolson3w ago
👏
DigitalCharlie
last question — is using startElementRect just a little easier than using offset math? I'm guessing both work, just trying to make sure I'm also learning/understanding why different choices are better/worse/different
Chris Bolson
Chris Bolson3w ago
"startElementRect" is just the variable name that I gave to the getBoundingClientRect() for the start element. getBoundingClientRect() returns the size and position relative to the viewport. https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect offsetTop and offsetBottom (I presume you are referring to those), return the position relative to the nearest (positioned) parent. So, to use these, as you say you will then need to do some math to calculate the corresponding corners and even then probably won't be the position in the viewport. https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop I am sure that somebody with a better understanding of the inner workings of JS can explain it better.
DigitalCharlie
Ah, makes sense. I was positioning the SVG exactly under the grid I setup, trying to make the offset from the parent elements the same as what it needs to be for the svg
Want results from more Discord servers?
Add your server