166

I have a set of data that I am plotting in a scatter. When I mouseover one of the circles I would like it to popup with data (like x, y values, maybe more). Here is what I tried using:

vis.selectAll("circle")
   .data(datafiltered).enter().append("svg:circle")
   .attr("cx", function(d) { return x(d.x);})
   .attr("cy", function(d) {return y(d.y)})
   .attr("fill", "red").attr("r", 15)
   .on("mouseover", function() {
        d3.select(this).enter().append("text")
            .text(function(d) {return d.x;})
            .attr("x", function(d) {return x(d.x);})
            .attr("y", function (d) {return y(d.y);}); });

I suspect I need to be more informative about what data to enter?

2
  • 1
    I've also tried: vis.selectAll("circle").each(function (d) { vis.append("svg:text").attr("x", d.x).attr("y", d.y) .text(function (d) { return d.x; }); }); to no avail alas.
    – ScottieB
    May 29, 2012 at 19:22
  • 2
    bl.ocks.org/Caged/6476579
    – ehsan
    Feb 21, 2014 at 7:40

6 Answers 6

190

I assume that what you want is a tooltip. The easiest way to do this is to append an svg:title element to each circle, as the browser will take care of showing the tooltip and you don't need the mousehandler. The code would be something like

vis.selectAll("circle")
   .data(datafiltered).enter().append("svg:circle")
   ...
   .append("svg:title")
   .text(function(d) { return d.x; });

If you want fancier tooltips, you could use tipsy for example. See here for an example.

7
  • 3
    I like tipsy. My only issue now is that it points to the upper left corner of the circle, rather than the edge as in that demo. I'm not finding any obvious reason why. jsfiddle.net/scottieb/JwaaV (tipsy at very bottom)
    – ScottieB
    May 29, 2012 at 21:08
  • That jsfiddle doesn't seem to have any tooltips? May 29, 2012 at 21:13
  • You could try adding the tooltip to an svg:g that you overlay with the actual circle, but give zero width and height. Currently it's taking the bounding box and putting the tooltip at the edge. Playing around with tipsy's options might help as well. May 30, 2012 at 7:50
  • 1
    It does not seem to work anymore. Also I found an example using svg:title which fails: bl.ocks.org/ilyabo/1339996
    – nos
    May 11, 2016 at 5:09
  • 1
    @nos Works for me. May 11, 2016 at 17:15
148

A really good way to make a tooltip is described here: Simple D3 tooltip example

You have to append a div

var tooltip = d3.select("body")
    .append("div")
    .style("position", "absolute")
    .style("z-index", "10")
    .style("visibility", "hidden")
    .text("a simple tooltip");

Then you can just toggle it using

.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){return tooltip.style("top",
    (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px");})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});

d3.event.pageX / d3.event.pageY is the current mouse coordinate.

If you want to change the text you can use tooltip.text("my tooltip text");

Working Example

<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div class="example_div"></div>
</body>
<script type="text/javascript">
  var tooltip = d3.select("body")
    .append("div")
    .style("position", "absolute")
    .style("z-index", "10")
    .style("visibility", "hidden")
    .text("a simple tooltip");

  var sampleSVG = d3.select(".example_div")
    .append("svg:svg")
    .attr("class", "sample")
    .attr("width", 300)
    .attr("height", 300);

  d3.select(".example_div svg")
    .append("svg:circle")
    .attr("stroke", "black")
    .attr("fill", "aliceblue")
    .attr("r", 50)
    .attr("cx", 52)
    .attr("cy", 52)
    .on("mouseover", function(){return tooltip.style("visibility", "visible");})
    .on("mousemove", function(){return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
    .on("mouseout", function(){return tooltip.style("visibility", "hidden");});
</script>

3
  • 4
    Can you bind data to this tooltip? Dec 16, 2013 at 18:09
  • 2
    Afaik you can bind data to every DOM-element.
    – Pwdr
    Dec 17, 2013 at 0:29
  • to bind the data to this, just add d inside the parenthesis like so : function(d){ ... and change the text to what ever you want. For example, say you have a name it would be : tooltip.text(d.name):
    – thatOneGuy
    Apr 6, 2016 at 12:48
40

There is an awesome library for doing that that I recently discovered. It's simple to use and the result is quite neat: d3-tip.

You can see an example here:

enter image description here

Basically, all you have to do is to download(index.js), include the script:

<script src="index.js"></script>

and then follow the instructions from here (same link as example)

But for your code, it would be something like:

define the method:

var tip = d3.tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(function(d) {
    return "<strong>Frequency:</strong> <span style='color:red'>" + d.frequency + "</span>";
  })

create your svg (as you already do)

var svg = ...

call the method:

svg.call(tip);

add tip to your object:

vis.selectAll("circle")
   .data(datafiltered).enter().append("svg:circle")
...
   .on('mouseover', tip.show)
   .on('mouseout', tip.hide)

Don't forget to add the CSS:

<style>
.d3-tip {
  line-height: 1;
  font-weight: bold;
  padding: 12px;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  border-radius: 2px;
}

/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
  box-sizing: border-box;
  display: inline;
  font-size: 10px;
  width: 100%;
  line-height: 1;
  color: rgba(0, 0, 0, 0.8);
  content: "\25BC";
  position: absolute;
  text-align: center;
}

/* Style northward tooltips differently */
.d3-tip.n:after {
  margin: -1px 0 0 0;
  top: 100%;
  left: 0;
}
</style>
2
  • 2
    The latest d3-tip supports d3v4 just fine. It's not obvious if you google around, but it is working great for me with d3v4.
    – moodboom
    Apr 6, 2017 at 3:04
  • unfortunately, the .attr() function no longer working in D3 v5. Sep 25, 2020 at 6:41
8

This concise example demonstrates common way how to create custom tooltip in d3.

var w = 500;
var h = 150;

var dataset = [5, 10, 15, 20, 25];

// firstly we create div element that we can use as
// tooltip container, it have absolute position and
// visibility: hidden by default

var tooltip = d3.select("body")
  .append("div")
  .attr('class', 'tooltip');

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

// here we add some circles on the page

var circles = svg.selectAll("circle")
  .data(dataset)
  .enter()
  .append("circle");

circles.attr("cx", function(d, i) {
    return (i * 50) + 25;
  })
  .attr("cy", h / 2)
  .attr("r", function(d) {
    return d;
  })
  
  // we define "mouseover" handler, here we change tooltip
  // visibility to "visible" and add appropriate test
  
  .on("mouseover", function(d) {
    return tooltip.style("visibility", "visible").text('radius = ' + d);
  })
  
  // we move tooltip during of "mousemove"
  
  .on("mousemove", function() {
    return tooltip.style("top", (event.pageY - 30) + "px")
      .style("left", event.pageX + "px");
  })
  
  // we hide our tooltip on "mouseout"
  
  .on("mouseout", function() {
    return tooltip.style("visibility", "hidden");
  });
.tooltip {
    position: absolute;
    z-index: 10;
    visibility: hidden;
    background-color: lightblue;
    text-align: center;
    padding: 4px;
    border-radius: 4px;
    font-weight: bold;
    color: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>

1
  • If anyone needs the tool-tip to move relative to the position of the object. Like in case of a tree graph. You might want to use return tooltip.style("top", (d.x + 40) + "px") .style("left", (d.y + 80) + "px"); in the 'mousemove' attribute. The d.x will help move the tool-tip relative to the object, not the entire page Oct 29, 2019 at 15:26
6

You can pass in the data to be used in the mouseover like this- the mouseover event uses a function with your previously entered data as an argument (and the index as a second argument) so you don't need to use enter() a second time.

vis.selectAll("circle")
.data(datafiltered).enter().append("svg:circle")
.attr("cx", function(d) { return x(d.x);})
.attr("cy", function(d) {return y(d.y)})
.attr("fill", "red").attr("r", 15)
.on("mouseover", function(d,i) {
    d3.select(this).append("text")
        .text( d.x)
        .attr("x", x(d.x))
        .attr("y", y(d.y)); 
});
3
  • Thanks. I really needed the d3.select(this) to modify the shape and didn't know how to get the instance in an enter/update.
    – Turbo
    Feb 6, 2019 at 3:53
  • You are using some functions x() and y() that are not defined in your code. I think that can be removed.
    – Robert
    Feb 12, 2019 at 16:22
  • they were given in the OP
    – danimal
    Mar 5, 2019 at 17:03
0

You can think about what you want before you do it yourself, I'll provide 4 examples here.

Essentially demos 1, 2, 4 are pretty much in the same spirit, demo3 is using the title approach.


demo 1, 2, 4: add text (or foreignobject) tags to each item

  • demo1: pure javascript writing.

  • demo2: same as demo1, use d3.js instead

  • demo4: the example applied to the Histogram, and show why I use so much text instead of just using one.

    Note:

demo3: this is very convenient, if the requirements are not high, this is probably the best way. (It's the same as the Lars Kotthoff answered.)

Examples

<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
  text.tooltip {
    display: none;
  }

  circle:hover + text.tooltip {
    display: initial;
  }

  circle:hover + foreignobject {
    display: initial;
    color: #ffff00;
    background-color: #015db7;
  }

  /* ↓ used for demo4Histogram only */
  rect:hover + foreignobject {
    display: initial;
  }

  rect:hover {
    fill: red;
  }
</style>
<body></body>
<script>
  const w = 500
  const h = 150
  const dataset = [5, 10, 15, 20, 25]

  function demo1PureJS() {
    const svgFrag = document.createRange().createContextualFragment(`
<header>PureJS</header>
<svg width="400" height="150"><g></g></svg><br>
`)
    const gElem = svgFrag.querySelector(`g`)
    for (const idx in dataset) {
      const r = dataset[idx]
      const [cx, cy] = [idx * 50 + 25, h / 2];

      gElem.insertAdjacentHTML("beforeend", `
<circle cx="${cx}" cy="${cy}" r="${r}" data-tooltip="(${cx}, ${cy})"></circle>
<text class="tooltip" x="${cx}" y="${cy}" fill="red">${r}</text>
`)
      document.body.append(svgFrag)

    }
  }

  function demo2D3js() {
    const svg = d3.select("body")
      .append("svg")
      .attr("width", w)
      .attr("height", h)

    svg.node().insertAdjacentHTML("beforebegin", "<header>demo2D3js</header>")

    svg.selectAll("circle")
      .data(dataset)
      .enter()
      .append("circle")
      .attr("cx", (d, i) => i * 50 + 25)
      .attr("cy", h / 2)
      .attr("r", d => d)
      .text((d, idx, arr) => {
        const circle = arr[idx]
        const x = circle.getAttribute("cx")
        const y = circle.getAttribute("cy")

        const testCase = "foreignobject"
        if (testCase === "foreignobject") { // 👈 focus here
          circle.insertAdjacentHTML("afterend", `
     <foreignobject x="${x}" y="${y}" width="${d.toString().length * 12}" height="26" display="none">
        <div>${d}</div>
     </foreignobject>
    `)

        } else {
          circle.insertAdjacentHTML("afterend", `<text class="tooltip" x="${x}" y="${y}" fill="yellow">${d}</text>`)
        }
        return ""
      })
  }

  function demo3SVGTitle() {
    /*
    https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
    <rect x="11" y="1" width="8" height="8">
      <title>I'm a square</title>
    </rect>
     */

    const svg = d3.select("body")
      .append("svg")
      .attr("width", w)
      .attr("height", h)

    svg.node().insertAdjacentHTML("beforebegin", "<header>SVGTitle</header>")

    svg.selectAll("circle")
      .data(dataset)
      .enter()
      .append("circle")
      .attr("cx", (d, i) => i * 50 + 25)
      .attr("cy", h / 2)
      .attr("r", d => d)
      .append("svg:title") // 👈 focus here
      .text(d => d)
  }

  async function demo4Histogram() {
    const margin = {top: 50, right: 50, bottom: 50, left: 50},
      width = 900 - margin.left - margin.right,
      height = 900 - margin.top - margin.bottom

    const svg = d3.select("body")
      .append("svg")

    svg.node().insertAdjacentHTML("beforebegin", "<header>Histogram</header>")

    const mainG = svg.attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", `translate(${margin.left}, ${margin.top})`)


    const dataSet = []
    await d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/1_OneNum.csv", (row) => {
      dataSet.push(row)
    })

    // X: price
    const scaleX = d3.scaleLinear()
      .domain([0, 2000])
      .range([0, width])

    mainG.append("g")
      .attr("transform", `translate(0,${height})`)
      .call(d3.axisBottom(scaleX)
      )

    const histogram = d3.histogram()
      .value(d => d.price)
      .domain(scaleX.domain())
      .thresholds(scaleX.ticks(50))

    const bins = histogram(dataSet)

    // Y: Count
    const scaleY = d3.scaleLinear()
      .domain([0, d3.max(bins, d => d.length)])
      .range([height, 0])

    mainG.append("g")
      .call(d3.axisLeft(scaleY))

    mainG.selectAll("rect")
      .data(bins)
      .enter()
      .append("rect")
      .attr("transform", d => `translate(${scaleX(d.x0)},${scaleY(d.length)})`)
      .attr("x", 1)
      .attr("width", d => d3.max([0, scaleX(d.x1) - scaleX(d.x0) - 1]))
      .attr("height", d => height - scaleY(d.length))
      .attr("fill", "#298e75")
      .attr("fill-opacity", 0.4)
      .text((d, idx, arr) => { // 👈 focus here
        const rect = arr[idx]
        const [x, y, width] = [rect.getAttribute("x"), rect.getAttribute("y") ?? 0, rect.getAttribute("width")];
        if (width > 0) {
          const msg = `${d.x0}~${d.x1}: ${d.length}`
          rect.insertAdjacentHTML("afterend", `
     <foreignobject x="${x}" y="${y}" width="${msg.length * 13}" height=26 display="none" class="tooltip"
     transform="translate(${scaleX(d.x0)},${scaleY(d.length)})">
        <div>${msg}</div>
     </foreignobject>
    `)
        }
        return ""
      })

    /**
     You can certainly consider creating just one element and moving it around to achieve the display effect. [see https://stackoverflow.com/a/47002479/9935654]
     On my side, I made a corresponding element individually, which seems to generate a lot of duplicate items, but it can be done as follows:
     If you are interested in a specific marker, you can click on it, and it will display the message forever(cancel again to hidden)
     * */
    document.querySelectorAll(`foreignObject.tooltip`).forEach(div => { // 👈 focus here
      div.addEventListener("click", () => {
        div.setAttribute("display", div.getAttribute("display") === "none" ? "" : "none")
      })
    })
  }

  demo1PureJS()
  demo2D3js()
  demo3SVGTitle()
  demo4Histogram()

</script>

demo4: Because each element has a label, it is possible to display multiple labels at the same time, something that cannot be done with just one element.

demo4Histogram


d3.js version: v7

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.