skip to Main Content

I am creating a gauge using svg.

Is there a way to add an arrow at the end of the animated gauge to indicate the entered value?

Since the gauge is displayed with the path’s stroke-dasharray, it seems that a marker cannot be applied according to the svg specification.

enter image description here

class arcGauge {
    constructor(targetEl) {
        this.el = targetEl;
        this.minmax = [];
        this.arcCoordinate = [];
        this.valueArcDataValue = 0;
        this.gaugeArc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    }

    // draw gauge
    init(data) {
        // set data
        this.viewBox = [0, 0, 110, 100];
        this.minmax = data.minmax || [];
        this.arcCoordinate = data.arcCoordinate || [120, 60];
        this.threshold = data.threshold || [];
        this.valueArcData = data.valueArcData;
        this.gaugeRadius = 42;
        this.gaugeStrokeWidth = 6;

        this._makeGauge();

    }

    _makeGauge() {
        const radius = this.gaugeRadius;

        let arcCoord = [];
        // coordinate
        this.arcCoordinate.forEach((ang) => {
            const radian = this._degreesToRadians(ang);
            const x = this.viewBox[2] / 2 + Math.cos(radian) * radius;
            const y = this.viewBox[2] / 2 + Math.sin(radian) * radius;
            arcCoord.push([x.toFixed(2), y.toFixed(2)]);
        });

        //arc
        this.gaugeArc.setAttribute('id', 'gaugeArc');
        this.gaugeArc.setAttribute('d', `M ${arcCoord[0][0]} ${arcCoord[0][1]} A ${radius} ${radius} 0 1 1  ${arcCoord[1][0]} ${arcCoord[1][1]}`);
        this.gaugeArc.setAttribute('fill', 'none');
        this.gaugeArc.setAttribute('stroke', this.valueArcData.color);
        this.gaugeArc.setAttribute('stroke-width', this.gaugeStrokeWidth);
        this.gaugeArc.setAttribute('transform', 'scale(-1, 1) translate(-110, 0)');

        let percentage = 0;
    percentage = this.valueArcData.value;

        this.gaugeArc.style.strokeDasharray = this._getArcLength(radius, 300, percentage);

        this.el.appendChild(this.gaugeArc);
    }

    // degree
    _degreesToRadians(degrees) {
        const pi = Math.PI;
        return degrees * (pi / 180);
    }
    // arc length
    _getArcLength(radius, degrees, val) {
        const radian = this._degreesToRadians(degrees);
        const arcLength = 2 * Math.PI * radius * (degrees / 360);
        const pathLength = arcLength * (val / 100);
        const dasharray = `${pathLength.toFixed(2)} ${arcLength.toFixed(2)}`;
        return dasharray;
    }



    // set gauge value
    setValue(v) {
            const baseValue = this.minmax[1] - this.minmax[0];
            const percentage = (v / baseValue) * 100;
            this.valueArcData.value = v;

            this.gaugeArc.style.strokeDasharray = this._getArcLength(this.gaugeRadius, 300, percentage);
            //gauge animation
            this.gaugeArc.style.transition = 'stroke-dasharray 1s ease-in-out';
    }

}


const arcGaugeData = {
  minmax: [0, 100],
  thresholdColor: ['#22b050', '#f4c141', '#e73621'],
  valueArcData: { type: 'arrow', color: '#3e3eff', value: 80 },
};
const arcGaugeIns = new arcGauge(document.querySelector('#chart'));
arcGaugeIns.init(arcGaugeData);

function changeArc(ipt) {
  arcGaugeIns.setValue(ipt.value);
}
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
<div class="container">
  <svg id="chart" xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100"></svg>
  <input type="range"  min="0" max="100" step="1" value="80" onchange="changeArc(this)" />
</div>

If this is not possible, please advise if there is an alternative.

2

Answers


  1. The main idea is this:
    you calculate the end point of the gauge using getPointAtLength and you move the arrow at this point.

    Also you calculate the angle of the path at the end point so that you can rotate the arrow accordingly.

    In order to calculate the angle you will need 2 points:

    point1 i.e the end point of the gauge

    point2 i.e a slightly offseted point (val + 0.1).

    This will work unless the value of the slider is 100. In this case I use the arrow as a marker.

    let length = chart.getTotalLength();
    chart.style.strokeDasharray = length;
    chart.style.strokeDashoffset = length;
    
    function gauge(rangeValue) {
      let val = length - (length * rangeValue) / 100;
      if (rangeValue < 100 && rangeValue > 0) {
        let point1 = chart.getPointAtLength(val);
        let point2 = chart.getPointAtLength(val + 0.1);
        let angle = Math.atan2(point2.y - point1.y, point2.x - point1.x);
        arrow.setAttribute(
          "transform",
          "translate(" +
            [point1.x, point1.y] +
            ")" +
            "rotate(" +
            (angle * 180) / Math.PI +
            ")"
        );
      } else {
        chart.setAttribute("marker", "url(#m)");
      }
      chart.style.strokeDashoffset = val;
    }
    
    gauge(Number(range.value));
    
    range.addEventListener("input", () => {
      let rangeValue = Number(range.value);
      gauge(rangeValue);
    });
    svg{border:solid;}
    <svg xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100">
    
      <path id="chart" d="M 34.00 91.37 A 42 42 0 1 1  76.00 91.37" fill="none" stroke="#3e3eff" stroke-width="6" transform="scale(-1, 1) translate(-110, 0)"  marker="url(#m)"></path>
      <marker id="m">
        <path id="markerarrow" fill="red" d="M0,0L0,-10L-14,0L0,10" />
      </marker>
      <use href="#markerarrow" id="arrow"/>
    </svg>
    </br>
    <p><input type="range" id="range" min="0" max="100" step="1" value="80" /></p>

    Observation: This solution is not perfect. If you move the slider very fast you may get an offseted arrow.

    Login or Signup to reply.
  2. In this example I just use a simple polygon as an arrow. The arrow is rotated from the center point.

    var svg = document.getElementById('svg01');
    
    document.forms.form01.range.addEventListener('change', e => {
      let rotate = 240 * e.target.value / 100;
      svg.querySelector('circle').setAttribute('stroke-dashoffset', rotate - 360);
      svg.querySelector('.polygon').setAttribute('transform', `rotate(-${rotate})`);
    });
    body {
      display: flex;
    }
    
    svg {
      border: solid;
      height: 90vh;
    }
    <svg id="svg01" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
      <g transform="translate(50 50) rotate(30)">
        <circle r="40" fill="none" stroke="#3e3eff" stroke-width="6" pathLength="360" stroke-dasharray="240 360" stroke-dashoffset="-200" />
        <g class="polygon" transform="rotate(-160)">
          <polygon transform="translate(40 0)" points="0,-7 10,7 -10,7 0,-7" fill="red" />
        </g>
      </g>
    </svg>
    <form name="form01">
      <input type="range" name="range" min="0" max="100" step="1" value="66" />
    </form>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search