Search code examples
javascriptsvgecmascript-6stroke-dasharray

Calculate the x and y for end of path within a circle SVG


I have a progress bar that is being updated by changing the stroke-dashoffset property of a path. However, I need to calculate the x and y coordinates of the endpoint (the rounded side) of the navy path every time it updates. I am going to add labels at these points in the future. I've been banging my head about this one for a while. I have tried using things similar to const x = radius * Math.sin(Math.PI * 2 * angle / 360); and const y = radius * Math.cos(Math.PI * 2 * angle / 360); but I don't think I am using them properly. Could someone assist me in the endeavor? Side note I am only using Vanilla JS.

const setStrokeDashOffset = (e) => {
  const dashOffset = e.target.getAttribute('data-attr');
  document
    .getElementById('progress-meter')
    .setAttribute('stroke-dashoffset', dashOffset);
}


const btn = document.querySelectorAll('button');
btn.forEach(x => {
  x.addEventListener('click', (e) => {
    setStrokeDashOffset(e)
  });
});
.donut-progress__svg {
  transform: scaleX(-1);
}

.donut-progress__circle {
  fill: none;
  stroke: none;
}

.donut-progress__path-elapsed {
  stroke: #aaaaaa;
  stroke-width: 10;
}

.donut-progress__path-remaining {
  stroke: navy;
  stroke-linecap: round;
  stroke-width: 10;
  transform: rotate(90deg);
  transform-origin: center;
  transition: 0.3s linear all;
}

.donut-progress__path-start {
  stroke: orange;
  stroke-linecap: round;
  stroke-width: 10;
  transform: rotate(90deg);
  transform-origin: center;
}
<svg id="donut-progress__svg" class="donut-progress__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
			<g class="donut-progress__circle">
				<circle class="donut-progress__path-elapsed" cx="50" cy="50" r="45"></circle>
				<path id="progress-meter" stroke-dasharray="283 283" stroke-dashoffset="165.08333333333334" class="donut-progress__path-remaining" stroke="#4764ae" d="
					M 50, 50
					m -45, 0
					a 45,45 0 1,0 90,0
					a 45,45 0 1,0 -90,0
					"></path>
          <path opacity="1" id="progress-meter-start" class="donut-progress__path-start" stroke-dashoffset="282" stroke-dasharray="283" d="
					M 50, 50
					m -45, 0
					a 45,45 0 1,0 90,0
					a 45,45 0 1,0 -90,0
					"></path>
			</g>
		</svg>


<button data-attr="100" class="one">move to 100</button>
<button data-attr="150" class="one">move to 150</button>
<button data-attr="200" class="one">move to 200</button>


Solution

  • SVG provides a method on <path> elements that you will find very useful here:

    myPath.getPointAtLength(len)
    

    Example below.

    const setStrokeDashOffset = (e) => {
      const dashOffset = e.target.getAttribute('data-attr');
      document
        .getElementById('progress-meter')
        .setAttribute('stroke-dashoffset', dashOffset);
    }
    
    
    const showEndPoint = (e) => {
      const dashOffset = e.target.getAttribute('data-attr');
      // Get the X,Y position of a point at "dashOffset" along the path
      const pt = document
        .getElementById('progress-meter')
        .getPointAtLength(dashOffset);
      // Update our red dot to show the location
      // Note that we are switching X and Y here to compensate for the fact that you rotate the original path
      const endpoint = document.getElementById('endpoint')
      endpoint.setAttribute('cx', pt.y);
      endpoint.setAttribute('cy', pt.x);
    }
    
    
    const btn = document.querySelectorAll('button');
    btn.forEach(x => {
      x.addEventListener('click', (e) => {
        setStrokeDashOffset(e);
        
        showEndPoint(e);
      });
    });
    .donut-progress__svg {
      transform: scaleX(-1);
    }
    
    .donut-progress__circle {
      fill: none;
      stroke: none;
    }
    
    .donut-progress__path-elapsed {
      stroke: #aaaaaa;
      stroke-width: 10;
    }
    
    .donut-progress__path-remaining {
      stroke: navy;
      stroke-linecap: round;
      stroke-width: 10;
      transform: rotate(90deg);
      transform-origin: center;
      transition: 0.3s linear all;
    }
    
    .donut-progress__path-start {
      stroke: orange;
      stroke-linecap: round;
      stroke-width: 10;
      transform: rotate(90deg);
      transform-origin: center;
    }
    <svg id="donut-progress__svg" class="donut-progress__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    			<g class="donut-progress__circle">
    				<circle class="donut-progress__path-elapsed" cx="50" cy="50" r="45"></circle>
    				<path id="progress-meter" stroke-dasharray="283 283" stroke-dashoffset="165.08333333333334" class="donut-progress__path-remaining" stroke="#4764ae" d="
    					M 50, 50
    					m -45, 0
    					a 45,45 0 1,0 90,0
    					a 45,45 0 1,0 -90,0
    					"></path>
              <path opacity="1" id="progress-meter-start" class="donut-progress__path-start" stroke-dashoffset="282" stroke-dasharray="283" d="
    					M 50, 50
    					m -45, 0
    					a 45,45 0 1,0 90,0
    					a 45,45 0 1,0 -90,0
    					"></path>
    			</g>
          <circle id="endpoint" r="3" fill="red"/>
    		</svg>
    
    
    <button data-attr="100" class="one">move to 100</button>
    <button data-attr="150" class="one">move to 150</button>
    <button data-attr="200" class="one">move to 200</button>

    And here's a slightly modified version of your SVG that avoids the necessity to switch the X and Y coords - as mentioned in the code of the previous version.

    const setStrokeDashOffset = (e) => {
      const dashOffset = e.target.getAttribute('data-attr');
      document
        .getElementById('progress-meter')
        .setAttribute('stroke-dashoffset', dashOffset);
    }
    
    
    const showEndPoint = (e) => {
      const dashOffset = e.target.getAttribute('data-attr');
      // Get the X,Y position of a point at "dashOffset" along the path
      const path = document.getElementById('progress-meter');
      const pt = path.getPointAtLength(path.getTotalLength() - dashOffset);
      // Update our red dot to show the location
      const endpoint = document.getElementById('endpoint')
      endpoint.setAttribute('cx', pt.x);
      endpoint.setAttribute('cy', pt.y);
    }
    
    
    const btn = document.querySelectorAll('button');
    btn.forEach(x => {
      x.addEventListener('click', (e) => {
        setStrokeDashOffset(e);
        
        showEndPoint(e);
      });
    });
    .donut-progress__svg {
      transform: scaleX(-1);
    }
    
    .donut-progress__circle {
      fill: none;
      stroke: none;
      transform: rotate(90deg);
      transform-origin: center;
    }
    
    .donut-progress__path-elapsed {
      stroke: #aaaaaa;
      stroke-width: 10;
    }
    
    .donut-progress__path-remaining {
      stroke: navy;
      stroke-linecap: round;
      stroke-width: 10;
      transition: 0.3s linear all;
    }
    
    .donut-progress__path-start {
      stroke: orange;
      stroke-linecap: round;
      stroke-width: 10;
    }
    <svg id="donut-progress__svg" class="donut-progress__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    			<g class="donut-progress__circle">
    				<circle class="donut-progress__path-elapsed" cx="50" cy="50" r="45"></circle>
    				<path id="progress-meter" stroke-dasharray="283 283" stroke-dashoffset="165.08333333333334" class="donut-progress__path-remaining" stroke="#4764ae" d="
    					M 50, 50
    					m -45, 0
    					a 45,45 0 1,0 90,0
    					a 45,45 0 1,0 -90,0
    					"></path>
              <path opacity="1" id="progress-meter-start" class="donut-progress__path-start" stroke-dashoffset="282" stroke-dasharray="283" d="
    					M 50, 50
    					m -45, 0
    					a 45,45 0 1,0 90,0
    					a 45,45 0 1,0 -90,0
    					"></path>
    
              <circle id="endpoint" r="3" fill="red"/>
    			</g>
    		</svg>
    
    
    <button data-attr="100" class="one">move to 100</button>
    <button data-attr="150" class="one">move to 150</button>
    <button data-attr="200" class="one">move to 200</button>