javascripthtmlcsssvgmeter

SVG gauge meter with dynamic strokes


I am trying to build an SVG something similar to: enter image description here

The strokes are completely dynamic, as they come from an API. I want to place the strokes at the points received (as array of percentage values). Need not be in order and the distance between 2 strokes need not be equal

I am trying with something like below but not able to come up with a logic for the placement of strokes. I tried to follow the answer here: https://stackoverflow.com/a/66076805/6456247 but the distance between strokes here are equal. In my scenario, they are not consistent.

Fiddle Link: https://jsfiddle.net/puq8v594/2/

let divisionsElement = document.getElementById('divisions');
let length = parseInt(divisionsElement.getAttribute("r")) * Math.PI;
let divisionsArray = [20,30,80,90]; //stroke at 20%, 30%,80%,90% position
let METER_DIVISIONS_GAP = 0.2;

divisionsArray.map(ele => {
    setupPath(ele);
})


function setupPath(val) {
    divisionsElement.setAttribute("stroke-dasharray", val + ' ' +METER_DIVISIONS_GAP);
}
circle {
  fill: none;
}
.circle-back {
  stroke: #F8F6F0;
  stroke-width:9px;
}

.circle-fill {
  stroke: grey;
  stroke-width: 10px;
}

.circle-progress {
  stroke: blue;
  stroke-width:9px;
}
<svg viewBox="0 0 126 126" preserveAspectRatio="xMinYMin meet">

  <clipPath id="cut-off">
    <rect x="0" y="0" width="100%" height="50" />
  </clipPath>

  <clipPath id="progress-percent">
    <path x="0" y="0" width="12%" height="50" />
  </clipPath>
  
  <g>
    <circle r="35%" cx="40%" cy="40%" class="circle-fill" clip-path="url(#cut-off)" />

    <circle id="divisions" r="35%" cx="40%" cy="40%" class="circle-back" clip-path="url(#cut-off)" 
    stroke-dasharray="20 0.5" stroke-dashoffset="25" />

    <circle r="35%" cx="40%" cy="40%" class="circle-progress" clip-path="url(#progress-percent)" />
  </g>
  
</svg>

-----------EDIT------------

For somebody who wants to implement this with support to IE, I was able to implement it by making some changes to the code by Michael.

var data = [10,20,10,10,25,25];
var dataSum = 0;
for (let i = 0; i < data.length - 1; i++) { dataSum += data[i]; }



var drawArray = "";
var indexOfFill = 2;
var arcPath = document.getElementById("meter-back");
var filledPath = document.getElementById("meter-fill");

var totalLength = arcPath.getTotalLength();
    function drawPath() {

  // Draw the background strokes
  let test='';
   for (i = 0; i < data.length; i++) {
     let barLen = ((data[i]) * totalLength) / 100;
     test = test + ((barLen) - (0.2 * (dataSum / 100)) + " " + 0.2 * (dataSum / 100) + " ")
  }
  arcPath.setAttribute("stroke-dasharray", test);
  animate(filledPath, totalLength, 0,50);
  }drawPath();
  
  
function animate(el,length,end,percentage) {
    el.style.visibility = 'visible';
    if (end <= percentage) {
          el.style.strokeOpacity = 5;
          let gap = 100 - end;
          let offset = 100;
          el.style.strokeDasharray = ((end / 100) * length) + ' ' + ((gap / 100) * length);
          el.style.strokeDashoffset = (offset / 100) * length;

          animate(el, length, ++end, percentage);
        }

}  
<svg width="100%" height="600px" viewBox="0 0 200 200" preserveAspectRatio="xMinYMin meet">
  <defs>
    <filter id="outline" y="-50%" height="200%">
      <feMorphology operator="erode" radius="0.2" />
      <feComposite operator="out" in="SourceGraphic" result="outline" />
      <feFlood flood-color="grey" />
      <feComposite operator="in" in2="outline" />
      <feComposite operator="atop" in2="SourceGraphic" />
    </filter>
  </defs>

  <g filter="url(#outline)">
    <path fill="none" stroke-dasharray="" stroke-width="20" stroke="grey" d="M30 100 A 40 40 0 0 1 170 100" />
    <path id="meter-back" fill="none" stroke-dasharray="" stroke-width="20" stroke="white" d="M30 100 A 40 40 0 0 1 170 100" />
    <path id="meter-fill" fill="none" stroke-dasharray="" stroke="rgba(128,128,128,0.5)" stroke-width="20" d="M30 100 A 40 40 0 0 1 170 100" style="visibility: hidden;" ></path>
  </g>
  

</svg>


Solution

  • It might be easier to do this as just an arc path with a pathLength set to 100 (or almost 100).

    let data =[5 , 10 , 25  ,25 , 6 , 9  , 20];
    let i=0;
    let drawArray = "";
    let arcPath = document.getElementById("meter");
    
    function setupPath() {
        for (i=0; i < data.length; i++) {
          drawArray = drawArray + (data[i] - 0.5) + " 0.5 ";
        }
      arcPath.setAttribute("stroke-dasharray", drawArray);
    }; setupPath();
    <svg width="800px" height="600px" viewBox="0 0 200 200" preserveAspectRatio="xMinYMin meet">
     
        <path id="meter" fill="none" stroke-dasharray="" stroke-width="20" stroke="blue" pathLength="99.5" d="M30 100 A 40 40 0 0 1 170 100" />
      
    </svg>

    Update:

    I thought from the question that you were expecting an array of percentage values that add up to 100 as input. But if you don't know the number of values or their sum, then you can normalize the data as follows. Also, you can add an outline using a filter - which saves you from having to do a lot of math to draw arc segments explicitly.

    I also realized you wanted a partial fill - so you can control where the gauge is filled to, by changing the variable - indexOfFill.

    var data = [5, 10 ,25 ,25 ,6 ,9 ,20 ,16 ,33 ,45 ,67];
    var dataSum = data.reduce((partial_sum, a) => partial_sum + a, 0);
    var indexOfFill = 3;
    
    var i=0;
    var drawArray = "";
    var arcPath = document.getElementById("meter-back");
    var filledPath = document.getElementById("meter-fill");
    
    
    function setupPath() {
      
      // Draw the background
        for (i=0; i < data.length; i++) {
          drawArray = drawArray + (data[i] - (0.5 * (dataSum/100)) + " " + 0.5*(dataSum/100) +" ")};
        arcPath.setAttribute("stroke-dasharray", drawArray);
        arcPath.setAttribute("pathLength", dataSum - (0.5 * (dataSum/100)));
      
      //Draw the fill
      drawArray="";
        for (i=0; i < indexOfFill; i++) {
          if ( (i + 1) === indexOfFill ) {
            drawArray = drawArray + (data[i] - (0.5 * (dataSum/100)) + " " + dataSum)
          } else {
            drawArray = drawArray + (data[i] - (0.5 * (dataSum/100)) + " " + 0.5*(dataSum/100) +" ")}};
        filledPath.setAttribute("stroke-dasharray", drawArray);
        filledPath.setAttribute("pathLength", dataSum - (0.5 * (dataSum/100)));  
      
    }; setupPath();
    <svg width="800px" height="600px" viewBox="0 0 200 200" preserveAspectRatio="xMinYMin meet">
     <defs>
      <filter id="outline" y="-50%" height="200%">
        <feMorphology operator="erode" radius="0.2"/>
        <feComposite operator="out" in="SourceGraphic" result="outline"/>
        <feFlood flood-color="black"/>
        <feComposite operator="in" in2="outline"/>
        <feComposite operator="atop" in2="SourceGraphic"/>
       </filter>
      </defs>
      
      <g filter="url(#outline)">
        <path id="meter-back" fill="none" stroke-dasharray="" stroke-width="20" stroke="white" pathLength="99.5" d="M30 100 A 40 40 0 0 1 170 100" />
        
        <path id="meter-fill" fill="none" stroke-dasharray="" stroke-width="20" stroke="rgb(90,90,220)" pathLength="99.5" d="M30 100 A 40 40 0 0 1 170 100" />
      </g>
    </svg>