Thursday 20 April 2023

Web Tutorial: The SVG Venn Diagram

Hey guys. Hope April's going well.

This month's web tutorial will be light. We are going to explore creating a Venn Diagram using SVG. If you somehow have no idea what a Venn Diagram is, it's a series of at least two shapes (usually circles) that represent different areas of interest and how much (if any) these areas overlap.

In this web tutorial, we will create a Venn Diagram with three circles.

Begin with some HTML. We will just have the svg tag in the body.
<!DOCTYPE html>
<html>
    <head>
        <title>Venn Diagram</title>

        <style>

        </style>
    </head>

    <body>
        <svg>
            SVG not supported
        </svg>
    </body>
</html>


The tag is styled with a width and height. Take note, because width and height are important here. Most of our other calculations are derived off these. There is a solid red outline to show what we are doing.
<style>
  svg
  {
    width: 800px;
    height: 550px;
    outline: 1px solid red;
  }

</style>

Editor's Note: The combination of SVGs and CSS just does not work very well in Safari.

OK, here it is.




Now we have circles!
<svg>
  <circle />

  SVG not supported
</svg>


Since each circle has a unique fill color, we will just put the fill color right here. I have opted for a translucent red. Not that the translucency will be important.
<svg>
  <circle fill="rgba(255, 0, 0, 0.5)"/>

  SVG not supported
</svg>


Now, all the circles will have a 150 pixel radius. Considering the width of the SVG, which has been set to 800 pixels, if the width has to at least accommodate the width of two circles, that would be four times the radius. (4 x 150) = 600 pixels is less than 800 pixels and allows for a bit of buffer at the sides, so this is great.
<style>
  svg
  {
    width: 800px;
    height: 550px;
    outline: 1px solid red;
  }

  circle
  {
    r: 150;
  }
</style>


We will also give the circles a grey 3 pixel outline.
circle
{
  stroke: rgb(150, 150, 150);
  stroke-width: 3;

  r: 150;
}


The circles will be in different positions, so back to the HTML markup, let's determine the center. If the width of a circle is 150 pixels and the center of the SVG is at (800 / 2) = 400 pixels,  We'll want the center of the leftmost circle at 300 pixels thereabouts.
<svg>
  <circle  cx="300" fill="rgba(255, 0, 0, 0.5)"/>

  SVG not supported
</svg>


And if we have the SVG's height at 550 pixels, and the SVG's height is supposed to accommodate the height of two circles, again (150 x 4) = 600 pixels is more than 550 pixels! Do we therefore have a problem? No, because the circles are going to overlap, so they'll take up much less height. If we set the center to be 200 pixels from the top, the topmost line of the circle will have (200 - 150) = 50 pixels from the top. If the bottom circle is also 50 pixels from the bottom, (50 + 50 + 600 - 550) = 150 pixels, which is about how much the top circles and the bottom circle have to intersect. This may be relevant later.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>

  SVG not supported
</svg>


Here comes the first circle!




The second and third circle should come in succession. Let's go for the second circle. It will lie vertically flush to the first, so the cy attribute is the same.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cy="200"/>
 
  SVG not supported
</svg>


But horizontally, it should occupy the other side of the red rectangle, so we want it at an equidistant position from the mid-point, as the first circle, but in the other direction. So if the cx attribute of the first circle is 300, which is 100 pixels less than the mid-point which is at 400, then the other circle should be (400 + 100) = 500 pixels!
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cx="500" cy="200"/>
 
  SVG not supported
</svg>


And this circle will be a translucent green.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cx="500" cy="200" fill="rgba(0, 255, 0, 0.5)"/>
 
  SVG not supported
</svg>


Pretty good so far. Note how the intersecting portion is a nice (actually rather sickly) green. That's because the translucent red and green circles have now combined colors.




For the third circle, let's make this one blue. The cx attribute is obviously 400, which is the mid-point of the SVG.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cx="500" cy="200" fill="rgba(0, 255, 0, 0.5)"/>
  <circle cx="400" fill="rgba(0, 0, 255, 0.5)"/>
 
  SVG not supported
</svg>


The cy attribute now... I know this is very unscientific, but I basically just adjusted it till it looked all right.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cx="500" cy="200" fill="rgba(0, 255, 0, 0.5)"/>
  <circle cx="400" cy="372" fill="rgba(0, 0, 255, 0.5)"/>
 
  SVG not supported
</svg>


So far so good.




Optional: The intersections

Now, the next part isn't entirely necessary. If you're happy with the color contrast and want to just get on with adding text, you can skip this because it's a bit of a pain in the behind and doesn't add that much value.

But if you want a different color intersection from what we currently have, let's begin by adding a path tag. It will have a deep brown fill and will be very translucent.
<svg>
  <circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
  <circle cx="500" cy="200" fill="rgba(0, 255, 0, 0.5)"/>
  <circle cx="400" cy="372" fill="rgba(0, 0, 255, 0.5)"/>
 
  <path fill="rgba(100, 100, 0, 0.2)"/>
 
  SVG not supported
</svg>


Now there will be three of such paths, all the exact same shape. So you don't want to repeat yourself. In the styles, set paths to have the same stroke and stroke-width properties of circles.
<style>
    svg
    {
        width: 800px;
        height: 550px;
        outline: 1px solid red;
    }

    circle
    {
        stroke: rgb(150, 150, 150);
        stroke-width: 3;
        r: 150;
    }

    path
    {
        stroke: rgb(150, 150, 150);
        stroke-width: 3;
    }

</style>


We will set the shape here. Use the d property, with the path() function.
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("");
}


We begin by moving the pointer to the middle of the SVG. The first attribute is 400, but the second, needs some trial and error. The "Z" is to specify that the path will be closed at the end.
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("M 400 312 Z");
}


We use "a" to say that we want a curve. But first, we want the end point to be vertically upwards.
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("M 400 312 a 0 0 0 0 0 0 0 -223 Z");
}


And then we want the curve to the right, and slightly downwards. I won't bore you with the math. Actually, there is no math. I totally winged this.
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("M 400 312 a 270 190 0 0 0 0 -223 Z");
}


This is what you should have. See that superimposed green patch? It's supposed to be a very translucent brown, but you won't see this because it's superimposed over the dirty green intersection.




And now you move the point back where the way you came...
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("M 400 312 a 270 190 0 0 0 0 -223 a 0 0 0 0 0 0 223 Z");
}


And curve it exactly the same way.
path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 3;
    d: path("M 400 312 a 270 190 0 0 0 0 -223 a 270 190 0 0 0 0 223 Z");
}


And now you've filled up the entire thing!




Now we need two more of these. One will be a translucent blue-green, and one will be a translucent deep purple.
<path fill="rgba(100, 100, 0, 0.2)"/>
<path fill="rgba(0, 100, 100, 0.2)"/>
<path fill="rgba(100, 0, 100, 0.2)"/>


That area has gotten so dark because there are now three overlapping paths on that same spot.




We will the g tag to encapsulate the first circle.
<path fill="rgba(100, 100, 0, 0.2)"/>

<g>
    <path fill="rgba(0, 100, 100, 0.2)"/>
</g>

<path fill="rgba(100, 0, 100, 0.2)"/>


Here, we translate the second path towards the right and a few pixels down.
<g transform="translate(97 4)">
    <path fill="rgba(0, 100, 100, 0.2)"/>
</g>


You can see the very translucent blue-green path now that it's been shifted out.




Here, I nest the g tag encapsulation, and do a rotation 120 degrees around the point (352, 228). For the first number, I take roughly half of 97 and subtract it from the midpoint, 400. So it's like (97 / 2 = 48) (not exactly, but close), then (400 - 48 = 352). I arrived at the second number through a bit of trial and error, and a lot of adjustment.
<g transform="translate(97 4)">
    <g transform="rotate(120 352 228)">
        <path fill="rgba(0, 100, 100, 0.2)"/>
    </g>
</g>


You see the idea!




Next, of course, we do the last path tag. The transformations are horizontally the exact opposite.
<path fill="rgba(100, 100, 0, 0.2)"/>

<g transform="translate(97 4)">
    <g transform="rotate(120 352 228)">
        <path fill="rgba(0, 100, 100, 0.2)"/>
    </g>
</g>

<g transform="translate(-97 4)">
    <g transform="rotate(-120 448 228)">

        <path fill="rgba(100, 0, 100, 0.2)"/>
    </g>
</g>


There you go, the contrast is more pronounced now! And if you feel like changing the colors, you have more control.



 

Back to text!

I met someone online recently by the handle of "makanprayaloha", and I want to use that handle in this Venn Diagram. The handle is made up of three parts. First, "makan", which is the Malay word for "eat". Second is "pray", which is hopefully self-explanatory. And last is "aloha" which apparently means "love" in Hawaiian (I dunno, "aloha" seems to be the go-to word for everything in Hawaii). So in totality, the entire thing means "eat, pray, love".

We will style text this way in the SVG. The text-anchor property is set to middle so that no matter where you place the location, the text just uses that as its center point. The rest of it is visual and not all that relevant.
<style>
    svg
    {
        width: 800px;
        height: 550px;
        outline: 1px solid red;
    }

    circle
    {
        stroke: rgb(150, 150, 150);
        stroke-width: 3;
        r: 150;
    }

    path
    {
        stroke: rgb(150, 150, 150);
        stroke-width: 0;
        d: path("M 400 312 a 270 190 0 0 0 0 -223 a 270 190 0 0 0 0 223 Z");
    }

    text
    {
        font: bold 15px sans-serif;
        fill: rgba(255, 255, 255, 0.8);
        text-anchor: middle;
    }

</style>


So we first put text in the circles. We should adjust the x and y attributes as desired.
<circle cx="300" cy="200" fill="rgba(255, 0, 0, 0.5)"/>
<circle cx="500" cy="200" fill="rgba(0, 255, 0, 0.5)"/>
<circle cx="400" cy="372" fill="rgba(0, 0, 255, 0.5)"/>

<path fill="rgba(100, 100, 0, 0.2)"/>

<g transform="translate(97 4)">
    <g transform="rotate(120 352 228)">
        <path fill="rgba(0, 100, 100, 0.2)"/>
    </g>
</g>

<g transform="translate(-97 4)">
    <g transform="rotate(-120 448 228)">
        <path fill="rgba(100, 0, 100, 0.2)"/>
    </g>
</g>

<text x="270" y="200">MAKAN</text>
<text x="530" y="200">PRAY</text>
<text x="400" y="402">ALOHA</text>


Simple right?




Here's what is not so simple - what if the text spans multiple lines? We are going to put text in the intersecting areas, and as you may have noticed, they're kind of small. We could make the text smaller, but that's an unsustainable solution. So we use tspan tags! For the text, we have "SAY GRACE", "LOVE FOOD" and LOVE GOD". The words are supposed to b on separate lines.
<text x="270" y="200">MAKAN</text>
<text x="530" y="200">PRAY</text>
<text x="400" y="402">ALOHA</text>

<text x="400" y="170">
    <tspan>SAY</tspan>
    <tspan>GRACE</tspan>
</text>

<text x="335" y="301">
    <tspan>LOVE</tspan>
    <tspan>FOOD</tspan>
</text>

<text x="465" y="301">
    <tspan>LOVE</tspan>
    <tspan>GOD</tspan>
</text>


Since we didn't specify anything else, even enclosed in tspan tags, the text still functions as though there were no tspan tags. Looks cramped, right?




The x attributes should mirror the x attributes of the parent.
<text x="400" y="170">
    <tspan x="400">SAY</tspan>
    <tspan x="400">GRACE</tspan>
</text>

<text x="335" y="301">
    <tspan x="335">LOVE</tspan>
    <tspan x="335">FOOD</tspan>
</text>

<text x="465" y="301">
    <tspan x="465">LOVE</tspan>
    <tspan x="465">GOD</tspan>
</text>


The dx attribute for all of these is 0, which means the tspan text does not move relative to the text tag. But in the second line of each text tag, the dy attribute is 15, which means the text moves 1 pixels downwards from the y attribute of the previous line.
<text x="400" y="170">
    <tspan x="400" dx="0" dy="0">SAY</tspan>
    <tspan x="400" dx="0" dy="15">GRACE</tspan>
</text>

<text x="335" y="301">
    <tspan x="335" dx="0" dy="0">LOVE</tspan>
    <tspan x="335" dx="0" dy="15">FOOD</tspan>
</text>

<text x="465" y="301">
    <tspan x="465" dx="0" dy="0">LOVE</tspan>
    <tspan x="465" dx="0" dy="15">GOD</tspan>
</text>


Falling into place!




Now for the last one, we have three lines. Again, the second and third lines have the dy attribute set to 15, because they're 15 pixels vertically from the previous line.
<text x="400" y="170">
    <tspan x="400" dx="0" dy="0">SAY</tspan>
    <tspan x="400" dx="0" dy="15">GRACE</tspan>
</text>

<text x="335" y="301">
    <tspan x="335" dx="0" dy="0">LOVE</tspan>
    <tspan x="335" dx="0" dy="15">FOOD</tspan>
</text>

<text x="465" y="301">
    <tspan x="465" dx="0" dy="0">LOVE</tspan>
    <tspan x="465" dx="0" dy="15">GOD</tspan>
</text>

<text x="400" y="243">
    <tspan x="400" dx="0" dy="0">MAKAN</tspan>
    <tspan x="400" dx="0" dy="15">PRAY</tspan>
    <tspan x="400" dx="0" dy="15">ALOHA</tspan>
</text>


And here's your text!




Some cleanup. Remove the red outline and set the stroke-width properties of circles and paths to 0.
svg
{
    width: 800px;
    height: 550px;
    outline: 0px solid red;
}

circle
{
    stroke: rgb(150, 150, 150);
    stroke-width: 0;
    r: 150;
}

path
{
    stroke: rgb(150, 150, 150);
    stroke-width: 0;
    d: path("M 400 312 a 270 190 0 0 0 0 -223 a 270 190 0 0 0 0 223 Z");
}


Finally...




Be ad-Venn-turous!
T___T

No comments:

Post a Comment