Charles Pettis
Charles Pettis

Reputation: 357

Return which segment of a line is clicked on in an SVG with Javascript

I have an svg with a bunch of different paths. An example of one looks as follows:

<path d="M2943648.169104896,472020.4539518825L2943637.730875478,472048.6771215173" />

I'm attempting to find a way to get which section of the line has been clicked. I could add an onclick function which returns d, but this will only give me the full string as listed above, and I need just the relevant portion. As in, if a user clicks the first part of the line it'll return the correct coordinates (2943648.169104896,472020.4539518825) or if they click on the second segment, it will show the other coordinates that are in the value for d. Some of these paths have many different line segments.

I found an answer on here with a deprecated method of doing this and I was wondering what the latest way to do this is. Thank you.

Upvotes: 5

Views: 682

Answers (1)

  • I took the path to Array segments code from another SO answer:
    Split a svg path d to array of objects

  • wrapped it in code that creates new d-paths, incrementally adding every segment

  • replace the mouseover with a click

  • remove the specific color for a path segment

  • because N paths are overlayed some extra anti-aliasing kicks in

<svg-path-extractor colors="orange,green,blue,gold,hotpink,red">
  <div id=label></div>
  <svg viewBox="0 0 24 24">
    <path d="M 0 0 C 15 2 3 18 7 21 A 1 1 0 0 0 14 6 L 0 10 C 0 10 20 30 20 20 q -2 -4 4.13 -1 Z"></path>
    <g id=segments></g>
  </svg>
</svg-path-extractor>
<script>
  customElements.define("svg-path-extractor", class extends HTMLElement {
    connectedCallback() {
      let addPath = (d, stroke) => {}
      setTimeout(() => {
        let colors = this.getAttribute("colors").split `,`;
        let segments = [...this.querySelectorAll("svg path")].map(p => {
          return this.pathToArray(p);
        }).flat().map(seg => Object.keys(seg).map(key => seg[key]).join ` `);
        segments.map((seg, idx, arr) => {
          let d = arr.slice(0, idx + 1).join(" ");
          let p = document.createElementNS("http://www.w3.org/2000/svg", "path");
          p.setAttribute("d", d);
          p.setAttribute("fill", "none");
          p.setAttribute("stroke", colors.shift());
          p.onmouseover = (evt) => {
          let label = `${idx}d=${d}`.replace(seg,`<b>${seg}</b>`);
            this.querySelector("#label").innerHTML = label;
          }
          this.querySelector("svg #segments").prepend(p);
        })
      });
    }

    pathToArray(path) {
      if (typeof path != "string") path = path.getAttribute("d");
      const PATH_COMMANDS = {
        M: ["x", "y"],
        m: ["dx", "dy"],
        H: ["x"],
        h: ["dx"],
        V: ["y"],
        v: ["dy"],
        L: ["x", "y"],
        l: ["dx", "dy"],
        Z: [],
        C: ["x1", "y1", "x2", "y2", "x", "y"],
        c: ["dx1", "dy1", "dx2", "dy2", "dx", "dy"],
        S: ["x2", "y2", "x", "y"],
        s: ["dx2", "dy2", "dx", "dy"],
        Q: ["x1", "y1", "x", "y"],
        q: ["dx1", "dy1", "dx", "dy"],
        T: ["x", "y"],
        t: ["dx", "dy"],
        A: ["rx", "ry", "rotation", "large-arc", "sweep", "x", "y"],
        a: ["rx", "ry", "rotation", "large-arc", "sweep", "dx", "dy"]
      };
      const items = path.replace(/[\n\r]/g, '').
      replace(/-/g, ' -').
      replace(/(\d*\.)(\d+)(?=\.)/g, '$1$2 ').
      trim().
      split(/\s*,|\s+/);
      const segments = [];
      let currentCommand = '';
      let currentElement = {};
      while (items.length > 0) {
        let it = items.shift();
        if (PATH_COMMANDS.hasOwnProperty(it))
          currentCommand = it;
        else
          items.unshift(it);
        currentElement = {
          type: currentCommand
        };
        PATH_COMMANDS[currentCommand].forEach((prop) => {
          it = items.shift(); // TODO sanity check
          currentElement[prop] = it;
        });
        if (currentCommand === 'M') {
          currentCommand = 'L';
        } else if (currentCommand === 'm') {
          currentCommand = 'l';
        }
        segments.push(currentElement);
      }
      return segments
    }
  });
</script>
<style>
  body {
    font: 12px Arial
  }
  b{
    font-size:1.2em;
    color:darkgreen;
  }
  svg {
    width: 180px;
    background: pink;
  }
  svg #segments path{
    cursor:pointer;
  }

</style>

Upvotes: 1

Related Questions