Generating An Infographic With SVG

I was asked to build "click to copy" image summarizing search results, allowing users to include the sumary graphic in support tickets. The graphic needed to be a raster image, but I chose to build the image with SVG and then copy the SVG to a Canvas element to generate a PNG for the users to copy.

The final graphic should look like this. I changed the style and content, but kept the basic idea. This graphic shows the search results of a hypothetical chess database.

Creating the SVG step by step

The image breaks down into a few components:

I started with a rectangle using the golden ratio, then as I created the background dot grid and saw how it fit in the rectangle, I tweaked the rectangle dimensions slightly to make all of the edge include semicircle dots.

First, the background field:

    <svg id="my-image" viewBox="0 0 595 374" xmlns="http://www.w3.org/2000/svg" width="595"
      <rect x="0" y="0" width="595" height="374" fill="#1C4203" />
    </svg>
  

Then, I need to add the dots. I do this by defining an SVG pattern and setting it as the fill for the an additional rectangle layered on top of the background field.

  <defs>
    <pattern id="patternBg" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#163900" />
    </pattern>
  </defs>

  ...

  <rect x="0" y="0" width="595" height="374" fill="url(#patternBg)" />

  

Applying Masks To Patterns

I considered different approaches to creating the light green circle of dots, pretty quickly it made sense to me to consider how I would create this graphic in a vector graphic editor like Affinity Designer. In that case, I would create a layer of dots and then apply a mask. Of course, if you can do it in an SVG editor, then you can also do it in hand-coded SVG.

So first, I create yet another rectangle full of dots, exactly the the same as the dark green dots, except this time I use a different fill color.

    <pattern id="patternFg" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#90B579" />
    </pattern>
    ...
    <rect x="0" y="0" width="595" height="374" fill="url(#patternFg)" />
  

Now I have the light green dots on top of the dark green dots. I can define a mask in the shape of a circle and then apply that to the "dot circle" rectangle, to hide any dots outside of the mask.

    <mask id="dot-circle" x="0" y="0" width="595" height="374">
    <circle cx="355" cy="187" r="100" fill="#fff" />
    </mask>
    ...
    <rect x="0" y="0" width="595" height="374" fill="url(#patternFg)" mask="url(#dot-circle)" />
  

Finally, I create one last layer of dots, this time in the "highlight" color, and I use a rectangular mask to select just the dots I want to highlight.

For the dynamically generated images, the rectangle selection makes it easy to calculate the correct mask for each search summary graphic. I know the radius and spacing of the dots, so given the number of dots to highlight, I can create multiple rectangle masks exposing the dots I want. To start with, though, I am just getting the mask coordinates by eyeballing the image, I'll worry about doing the calculations when I get this SVG generated by JavaScript code.

The new dot layer:

    <pattern id="patternHighlight" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#EDEDED" />
    </pattern>
    ...
    <rect x="0" y="0" width="595" height="374" fill="url(#patternHighlight)" />
  

And with the mask applied:

    <!-- TODO: will need to calculate the mask depending on how many dots to highlight -->
    <mask id="highlight-dots" x="0" y="0" width="595" height="374">
      <rect x="365" y="145" width="20" height="40" fill="#fff"></rect>
      <rect x="385" y="145" width="20" height="62" fill="#fff"></rect>
    </mask>
    ...
    <rect x="0" y="0" width="595" height="374" fill="url(#patternHighlight)" mask="url(#highlight-dots)" />
  

Adding the text

The finishing touch is to add the text elements. To display the numbers correctly, there will be some calculation in the JavaScript code, spacing text elements based on the number of digits in the search results value. For now, I just adjusted the dx values manually, giving 13px per character.

Here's the full SVG code, including the text elements:


<svg id="my-image" viewBox="0 0 595 374" xmlns="http://www.w3.org/2000/svg" width="595"
  <style>
   .title {
     font-family: "Roboto";
     font-size: 22px;
     fill: #90B579;
   }

   .total {
     font-family: "Roboto";
     font-size: 22px;
     fill: #90B579;
   }

   .label {
     font-family: "Roboto";
     font-size: 11px;
     font-weight: 500;
     fill: #90B579;
   }

   .flagged-value {
     font-family: "Roboto";
     font-size: 50px;
     fill: #EDEDED;
  
   }

   .flagged-label {
     font-family: "Roboto";
     font-size: 11px;
     font-weight: 500;
     fill: #EDEDED;
   }
  </style>
  
  <defs>
    <pattern id="patternBg" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#163900" /> 
    </pattern>
    
    <pattern id="patternFg" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#90B579" /> 
    </pattern>

    <pattern id="patternHighlight" x="-11" y="-11" width="22" height="22" patternUnits="userSpaceOnUse">
      <circle cx="11" cy="11" r="7" stroke="none" fill="#EDEDED" /> 
    </pattern>

    <mask id="dot-circle" x="0" y="0" width="595" height="374">  
      <!-- <path d="M0,0  l 75,100  150,75  -25,-125  Z"  /> -->
      <circle cx="355" cy="187" r="100" fill="#fff" />
    </mask>

    <!-- TODO: will need to calculate the mask depending on how many dots to highlight -->
    <mask id="highlight-dots" x="0" y="0" width="595" height="374">  
      <rect x="365" y="145" width="20" height="40" fill="#fff"></rect>
      <rect x="385" y="145" width="20" height="62" fill="#fff"></rect>
    </mask>
  </defs>

  <!-- started with 600 x 370 (golden ratio) -->
  
  <rect x="0" y="0" width="595" height="374" fill="#1C4203" />
  <rect x="0" y="0" width="595" height="374" fill="url(#patternBg)" />
  <rect x="0" y="0" width="595" height="374" fill="url(#patternFg)" mask="url(#dot-circle)" />
  <rect x="0" y="0" width="595" height="374" fill="url(#patternHighlight)" mask="url(#highlight-dots)" />
  
  <text x="10" y="30" class="title">Magnus AND "Evans Gambit" NOT Kasparov</text>
  <text x="10" y="30" dy="16" class="label">SEARCH QUERY</text>
  <text x="10" y="90" width="100" class="title">492</text>
  
  <!-- TODO: Will need to calculate the dx value based on number of digits in result total -->
  <text x="10" y="90" dx="39" class="total">/925.2B</text>
  <text x="10" y="90" dy="16" class="label">POSITIONS</text>

  <text x="10" y="150" width="100" class="title">783</text>
  <text x="10" y="150" dx="39" class="total">/323.4M</text>
  <text x="10" y="150" dy="16" class="label">GAMES</text>

  <text x="10" y="210" width="100" class="title">83</text>
  <text x="10" y="210" dx="26" class="total">/429</text>
  <text x="10" y="210" dy="16" class="label">EVENTS</text>

  <text x="10" y="270" width="100" class="title">322</text>
  <text x="10" y="270" dx="39" class="total">/10.2K</text>
  <text x="10" y="270" dy="16" class="label">PLAYERS</text>

  <text x="10" y="350" class="flagged-value">5</text>
  <text x="10" y="350" dx="35" class="flagged-label">GAMES SELECTED</text>
</svg>

Magnus AND "Evans Gambit" NOT Kasparov SEARCH QUERY 492 /925.2B POSITIONS 783 /323.4M GAMES 83 /429 EVENTS 322 /10.2K PLAYERS 5 GAMES SELECTED

Next