Tikhon
Tikhon

Reputation: 1024

Dynamically Adding a tooltip to static SVG Using D3

We have a static SVG image that we are trying to dynamically add tooltips to say, a hover event on a object within the SVG image using d3.js in the context of an AngularJS application.

The SVG image is a floorplan and is fairly complex, however we've started very small in the POC process. Here is a small representative snippet of one section:

<g id="f3s362c12">
  <g>
    <rect x="75.2" y="92.4" pointer-events="visible" fill="none" 
    width="64.7" height="57.8" />
    <polyline fill="none" stroke="#CDDDED" stroke-width="0.5" 
     stroke-miterlimit="10" points="118.4,149.9 140.3,149.9 140.3,92.4
     75.2,92.4 75.2,128.7" />
  </g>
  <g>
    <text transform="matrix(1 0 0 1 87.8719 144.8836)" fill="#010101" 
     font-family="arial, sans-serif" font-size="13.4182">362.12</text>
  </g>
</g>

D3.js is new to us, however from our research, it seems to be capable of doing what we need it to do since it seems to be designed to work with SVG and represent data in the SVG, however all of the examples we have found are of dynamically creating SVG (mainly charts) but not manipulating existing SVG images.

In a nutshell what we need to do is:

  1. Find g tags that have id's starting with "f3" as in g id="f3s362c12" above
  2. For each rect associated with the g tag, add a tooltip hover event ( possibly a label?)
  3. When a user hovers over say g id="f3s362c12" select the rect and grab the corresponding data record for f3s362c12, that I have loaded from a .csv file, IE:

({"floor":"3","location":"f3s362c12","name":"David Byrne","occ":"Singer","img":"img/davidAvatar.jpg\r"})

  1. Add this info to the tooltip/label so when you hover over g id=f3s362c12, you see a tooltip with David Byrne, whose occupation is singer with his avatar image.

I've created a Plunk that:

  1. Loads the SVG in the HTML
  2. Loads the .csv file.

The problem we're having is with the d3.js. For example, in our Plunk, in script.js, we do something like this to find our g tags:

 var svg = d3.select("#svgFP");
 var allG = svg.selectAll("g").each(function (d,i) {}

However, it's at this point that we hit the wall, since we are trying to find a rect on allG using "this" keyword.

  if (this.id.indexOf("f3") > -1)
    {
        //1. Add label/div/hover
        //2. Find corresponding record from array object.
        //3. Inject respective name, occupation and image into label/div along with mouseover/mouseout event.
    }

We've been using Firebug to try to find properties to use, but it's been quite frustrating and fruitless to say the least, so we thought that there might be one or two d3/angular guru's in SO that might be able to show us the way.

Thanks in advance.

Upvotes: 4

Views: 1550

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

The first problem you have in your plunker is your CSV: The first row a CSV has to be the header, which defines what each column is. Thus, I modified your CSV to this:

floor,location,name,surname,role,image
3,f3s362c12,David ,Byrne,Singer,img/davidAvatar.jpg
3,f3s362c11,Tina,Weymouth,Bassist,img/tinaAvatar.jpg
3,f3s362c2,Jerry,Harrison,Keyboards,img/jerryAvatar.jpg
3,f3s362c1,Chris,Frantz,Drums,img/chrisAvatar.jpg

Now comes your actual question.

The best solution for a D3 code would be binding the data to the DOM elements (in this case, your SVG). But, since you already have a static SVG, not made with D3 and not having any bound data, here is a solution.

First, select all relevant group elements:

var groups = d3.selectAll("g[id^='f3']");

And add the mouseover code to that selection.

In the mouseover code comes the most important part of this solution: we get the ID of the element which the cursor is over...

var groupId = this.id

... and then, based on this ID, we filter the previously loaded CSV:

var thisData = data.filter(function(d) {
    return d.location === groupId
});

This is what the above code does: we loaded the CSV in a variable named data. That variable is an array of objects, each object having a location property (look at the CSV header I created). Then, we compare the location of each object (d.location) with the ID of the group (groupId).

Now, you have a variable named thisData, which you can use to populate your tooltip.

Here is a demo with a MCVE version of your code:

var tooltip = d3.select("body")
            .append("div")
            .style("position", "absolute")
            .style("background-color", "gainsboro")
            .style("border", "1px solid black")
            .style("padding", "20px")
            .style("pointer-events", "none")
            .style("font-size", "12px");

        var data = d3.csvParse(d3.select("#csv").text());

        var groups = d3.selectAll("g[id^='f3']");

        groups.on("mousemove", function() {
            var groupId = this.id
            var thisData = data.filter(function(d) {
                return d.location === groupId
            });
            tooltip.html("Name: " + thisData[0].name + " " + thisData[0].surname + "<br>Role: " + thisData[0].role)
                .style("top", (d3.event.pageY - 2) + "px").style("left", (d3.event.pageX + 2) + "px")
                .style("visibility", "visible");
        }).on("mouseout", function() {
            tooltip.style("visibility", "hidden");
        });
pre {
	display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" height="100%">
    <g id="background">
        <rect x="1.5" y="0.3" fill="#5A8CC9" width="298.7" height="300.4" />
    </g>
    <g id="f3s362c12">
        <g>
            <rect x="75.2" y="92.4" pointer-events="visible" fill="none" width="64.7" height="57.8" />
            <polyline fill="none" stroke="#CDDDED" stroke-width="0.5" stroke-miterlimit="10" points="118.4,149.9 140.3,149.9 140.3,92.4
                75.2,92.4 75.2,128.7        " />
        </g>
        <g>
            <text transform="matrix(1 0 0 1 87.8719 144.8836)" fill="#010101" font-family="arial, sans-serif" font-size="13.4182">362.12</text>
        </g>
    </g>
    <g id="f3s362c11">
        <g>
            <rect x="75.2" y="149.9" pointer-events="visible" fill="none" width="64.7" height="57.8" />
            <polyline fill="none" stroke="#CDDDED" stroke-width="0.5" stroke-miterlimit="10" points="118.4,207.8 140.3,207.8 140.3,149.9
                75.2,149.9 75.2,186.2       " />
        </g>
        <g>
            <text transform="matrix(1 0 0 1 87.8719 201.6532)" fill="#010101" font-family="arial, sans-serif" font-size="13.4182">362.11</text>
        </g>
    </g>
    <g id="f3s362c2">
        <g>
            <rect x="140.3" y="149.9" pointer-events="visible" fill="none" width="68.8" height="57.8" />
            <polyline fill="none" stroke="#CDDDED" stroke-width="0.5" stroke-miterlimit="10" points="208.7,183.5 208.7,149.9 140.3,149.9
                140.3,207.8 185.8,207.8         " />
        </g>
        <g>
            <text transform="matrix(1 0 0 1 163.782 201.6532)" fill="#010101" font-family="arial, sans-serif" font-size="13.4182">362.2</text>
        </g>
    </g>
    <g id="f3s362c1">
        <g>
            <rect x="140.3" y="92.4" pointer-events="visible" fill="none" width="68.8" height="57.8" />
            <polyline fill="none" stroke="#CDDDED" stroke-width="0.5" stroke-miterlimit="10" points="208.7,126 208.7,92.4 140.3,92.4
                140.3,149.9 185.8,149.9         " />
        </g>
        <g>
            <text transform="matrix(1 0 0 1 163.782 144.8836)" fill="#010101" font-family="arial, sans-serif" font-size="13.4182">362.1</text>
        </g>
    </g>
</svg>
<pre id="csv">floor,location,name,surname,role,image
3,f3s362c12,David ,Byrne,Singer,img/davidAvatar.jpg
3,f3s362c11,Tina,Weymouth,Bassist,img/tinaAvatar.jpg
3,f3s362c2,Jerry,Harrison,Keyboards,img/jerryAvatar.jpg
3,f3s362c1,Chris,Frantz,Drums,img/chrisAvatar.jpg</pre>

Summarising, these are the steps:

  1. Create a selection to get your mouse events;
  2. Load and parse your CSV;
  3. When the user hover over a <g>, filter your data array according to that <g>'s ID;
  4. Use the filtered array to create your tooltip's html.

PS: in this snippet I'm using a <pre> element to store your CSV. I'm only doing this because, unlike a Plunker, I cannot load a CSV using the S.O. snippet.

Upvotes: 3

Related Questions