Search code examples
htmlsvghyperlink

Shifting focus to a group element in an svg


Shifting focus to a group element in an svg

I have a tall and wide svg, generated by graphviz.

If I open the svg with a browser I can search for text and focus will move to show the sought text.

I wish to emulate this behaviour by putting a link to a svg fragment in an html file. So if I have file.svg containing <g id="sought"><text>goes here</text></g> the link would look like <a href="file.svg#sought">.

This doesn't have the desired effect.

Experimentation reveals that a link to a svg file containing <rect id="sought" /> will make the browser move focus to the rect concerned.

I appreciate I may be asking for the impossible here, but if there's an answer that doesn't involve changing the svg I'd like to hear about it.

Minimal example follows:

pic.svg

<?xml version="1.0" 
      encoding="UTF-8" 
      standalone="no"?>
<svg 
    xmlns:svg="http://www.w3.org/2000/svg" 
    xmlns="http://www.w3.org/2000/svg"
    version="1.1" 
    width="6000" height="6000" y="0" x="0">
    <rect id="black" fill="black" height="100" width="100" y="50" x="5500" />
    <text y="50" x="5500">black</text>
    <rect id="red" fill="red" height="100" width="100" y="5500" x="5500" />
    <text y="5500" x="5500">red</text>
    <g id="green">
        <rect fill="green" height="100" width="100" y="5500" x="50" />
    <text y="5500" x="50">green</text>
    </g>
</svg>

svglink.html

<!DOCTYPE html>
<html >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body
{
    width: 20em;
    height: 10em;
    text-align: left;
}
</style>
</head>
<body >
<span>links to pic.svg: </span>
<ul>
<li><a href="pic.svg#red">red rect</a></li>
<li><a href="pic.svg#green">green group</a></li>
</ul>
</body>
</html>

Please note:

  • following the red rect link reveals the red rectangle in pic.svg.

  • following the green group link doesn't reveal the green rectangle contained in the group

  • ctrl-f red|green|black when browsing pic.svg shifts focus to to the text, as desired.

This may well be by design, but I haven't found any documentation for this.

Modifying the html, perhaps scripting a button to reveal the group would be an option?


Solution

  • Apparently some browser can only scroll/jump to SVG rendered elements with geometry properties. While you can retrieve position or bounding box data for a <g> (e.g via getBBox() element it doesn't have intrinsic dimensions.

    scrollIntoView()

    A workaround might be to override the default scroll in view behaviour and replace it via JavaScript scrollIntoView() method.
    target.scrollIntoView() suffers from the same limitations. However, we can tweak the target selection:

    If a targeted element is not of the type path, line, rect, polyline, polygon, circle, ellipse, use or text – so for instance a <g> – we query for the next child element in this class to define it as the scroll target.

    //reset default anchor scroll
    let hash = window.location.hash;
    let targetId = hash ? hash.substring(1) : '';
    scrollToSVGEl(targetId);
    
    
    function scrollToSVGEl(targetId) {
      let target = targetId ? document.getElementById(targetId) : '';
      let renderedEls = ['path', 'line', 'rect', 'polyline', 'polygon', 'circle', 'ellipse', 'text', 'use'];
      if (!target) return false;
    
      let type = target.nodeName
    
      // select rendered element if target is group
      if (!renderedEls.includes(type)) {
        target = target.querySelector(`${renderedEls.join(', ')}`)
      }
      target.scrollIntoView({
        behavior: "smooth"
      });
    }
    
    
    // override default link behaviour
    let links = document.querySelectorAll('.aSvg')
    links.forEach(lnk => {
      lnk.addEventListener('click', e => {
        e.preventDefault();
        targetId = e.currentTarget.href.split('#').slice(-1)
        window.location.hash = targetId;
        scrollToSVGEl(targetId)
      })
    })
    html,
    body {
      scroll-behavior: smooth;
    }
    
    html,
    body,
     :target,
     :target * {
      scroll-padding: 100px;
    }
    
    svg {
      outline: 1px solid #ccc;
    }
    
    nav {
      background: rgba(0, 0, 0, 0.3);
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      a {
        margin-right: 1em;
      }
    }
    
    text {
      font-size: 24px;
    }
    <nav>
    
      <p><a class="aSvg" href="#black">black rect</a>
        <a class="aSvg" href="#red">red rect </a>
        <a class="aSvg" href="#green">green </a>
        <a class="aSvg" href="#text">text group</a>
        <a class="aSvg" href="#text2">text group 2</a>
      </p>
    
    
    </nav>
    
    
    <svg width="4000" viewBox="0 0 4000 5000">
            <rect id="black" x="30px" y="20px" width="250px" height="70px" fill="black" />
            <rect id="red" x="250px" y="400px" width="260px" height="100px" fill="red" />
            <g id="green">
                <rect x="500px" y="1200px" width="50px" height="50px" fill="green" />
                <g id="text">
                    <text x="3500" y="3200">Text</text>
    
                    <g id="text2">
                        <text x="500" y="3600">Text 2</text>
                    </g>
    
                </g>
            </g>
        </svg>

    This approach requires your SVG to be embedded in a HTML document. However, you could probably also add the script to your SVG (probably not very feasible).

    scrollTo()

    An alternative would be to retrieve the target's screen coordinates via getBoundingClientRect() and scroll to this position via scrollTo()

    //reset default anchor scroll
    let hash = window.location.hash;
    let targetId = hash ? hash.substring(1) : '';
    scrollToSVGEl(targetId);
    
    
    function scrollToSVGEl(targetId) {
      let target = targetId ? document.getElementById(targetId) : '';
      if (!target) return false;
      let {
        top,
        left
      } = target.getBoundingClientRect()
      let x = left + window.scrollX;
      let y = top + window.scrollY;
      window.scrollTo({
        top: y,
        left: x,
        behavior: 'smooth'
      });
    }
    
    
    // override default link behaviour
    let links = document.querySelectorAll('.aSvg')
    links.forEach(lnk => {
      lnk.addEventListener('click', e => {
        e.preventDefault();
        targetId = e.currentTarget.href.split('#').slice(-1)
        window.location.hash = targetId;
        scrollToSVGEl(targetId)
      })
    })
    html,
    body {
      scroll-behavior: smooth;
    }
    
    html,
    body,
     :target,
     :target * {
      scroll-padding: 100px;
    }
    
    svg {
      outline: 1px solid #ccc;
    }
    
    nav {
      background: rgba(0, 0, 0, 0.3);
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      a {
        margin-right: 1em;
      }
    }
    
    text {
      font-size: 24px;
    }
    <nav>
      <p><a class="aSvg" href="#black">black rect</a>
        <a class="aSvg" href="#red">red rect </a>
        <a class="aSvg" href="#green">green </a>
        <a class="aSvg" href="#text">text group</a>
        <a class="aSvg" href="#text2">text group 2</a>
      </p>
    </nav>
    
    
    <svg width="4000" viewBox="0 0 4000 5000">
            <rect id="black" x="30px" y="20px" width="250px" height="70px" fill="black" />
            <rect id="red" x="250px" y="400px" width="260px" height="100px" fill="red" />
            <g id="green">
                <rect x="500px" y="1200px" width="50px" height="50px" fill="green" />
                <g id="text">
                    <text x="3500" y="3200">Text</text>
                </g>
    
                <g id="text2">
                    <text x="500" y="3600">Text 2</text>
                </g>
            </g>
    </svg>