on mouseover function can't access data

4,340 views
Skip to first unread message

Emily Yount

unread,
May 12, 2012, 12:52:54 PM5/12/12
to d3...@googlegroups.com
I'm working on a scatter plot with a tooltip that shows a few attributes of my data set. Unfortunately, my mouseover function can't seem to access the data...

Here's a snippet of my code... I keep getting the error in the console that d is undefined.

        onMouseMove = function(d) { 
            //tooltip position
            var browserWidth = $(window).width();
            tooltip.style('top', ((d3.event.pageY)-1300)+"px")
                   .style('left', d3.event.pageX-265 + "px");
           
            //tooltip content
            var companyName = d.company;
            var compChange = d.compChange;
            var earnChange = d.earnChange;
           
                if (compChange > 0 && earnChange < 0) {
                    tooltip.html("<p class='tipCompany'>"+companyName+"</p>"+"<p class='tipInfo'> Net earnings: "+earnChange+" %<br /> CEO total pay: +"+compChange+" %</p>");
                } else if (compChange > 0 && earnChange > 0) {
                    tooltip.html("<p class='tipCompany'>"+companyName+"</p>"+"<p class='tipInfo'> Net earnings: +"+earnChange+" %<br /> CEO total pay: +"+compChange+" %</p>");
                } else if (compChange < 0 && earnChange < 0) {
                    tooltip.html("<p class='tipCompany'>"+companyName+"</p>"+"<p class='tipInfo'> Net earnings: "+earnChange+" %<br /> CEO total pay: "+compChange+" %</p>");
                } else if (compChange < 0 && earnChange > 0) {
                    tooltip.html("<p class='tipCompany'>"+companyName+"</p>"+"<p class='tipInfo'> Net earnings: +"+earnChange+" %<br /> CEO total pay: "+compChange+" %</p>");
                }
           
            //set tooltip to visible
            tooltip.style('visibility', 'visible');
        }

//this is inside of a function that makes the scatter plot
                var circles = svg.selectAll("circle")
                        .data(dataSet, function(d) {return d.company})
                        .enter()
                        .append("circle")
                        .attr("cx", function(d) {
                            return xScale(d.earnChange);
                        })
                        .attr("cy", function(d) {
                            return yScale(d.compChange);
                        })
                        .attr("r", 5)
                        .attr("fill", function(d) {
                            return(d.dotColor);
                        })
                        .attr("class", "dot")
                        .on("mouseover", onMouseOver)
                        .on("mousemove", onMouseMove)
                        .on("mouseout", onMouseOut);

Scott Murray

unread,
May 12, 2012, 1:35:58 PM5/12/12
to d3...@googlegroups.com
Hi, Emily,

When you create the circles, you are calling the functions without arguments:

.on("mouseover", onMouseOver)
.on("mousemove", onMouseMove)
.on("mouseout", onMouseOut);

Yet in the function definition, of course it is expecting an argument — d!

onMouseMove = function(d) { // <--- here!

I expect the reason d.company is undefined is because d isn't being passed to onMouseOver, so d.anything would fail. (It's just that you ask for d.company first, so you get that error first.)

Try:
.on("mouseover", onMouseOver(d))
.on("mousemove", onMouseMove(d))
.on("mouseout", onMouseOut(d));

Let us know!

Scott

Emily Yount

unread,
May 12, 2012, 1:45:17 PM5/12/12
to d3...@googlegroups.com
Yeah, that's what I thought too, but then I get the following error:

d is not defined
.on("mouseover", onMouseOver(d))

Scott Murray

unread,
May 12, 2012, 2:03:00 PM5/12/12
to d3...@googlegroups.com
Oops! I mean, try this:

.on("mouseover", function(d) {
onMouseOver(d);
})

I think you have to specify an anonymous function as the listener, and d is relayed to that. So from within the anonymous function, you can take d and hand it off to something else, if you like (like onMouseOver).

Someone else on this list could offer a more elegant solution, I'm sure.

Also see reference:

https://github.com/mbostock/d3/wiki/Selections#wiki-on

Ger Hobbelt

unread,
May 12, 2012, 2:21:38 PM5/12/12
to d3...@googlegroups.com
No need for anonymous functions there as anonymous function references are not treated different from named function references - in JS they are the same after all.

Anyway, IIRC mousemove/blur/etc. are events which don't provide the data reference as parameter; you can verify this (and otherwise see what you get when the going gets tough) by adding this line at the start of your handler:

console.log("yada", this, arguments);

the important bits being the 'this' and 'arguments' so that you can see what is fed to the function using the inspector.

For mouseover/out the 'this' references the SVG node itself, which can be turned into a selection by saying:

  d3.select(this)

and then you might be able to access the data by saying things like 

  d3.select(this).datum()

-- but this last bit depends on where you are and how your SVG is constructed. You may have to check with the parentNode (or grandpa, ...) to access the relevant data element.

HTH



(OT: when you use console.log() in IE, the code will crash until you've opened the development environment [F12] - I had forgotten that nasty bit of knowledge to my detriment a couple of days ago.)

Met vriendelijke groeten / Best regards,

Ger Hobbelt

--------------------------------------------------
web:    http://www.hobbelt.com/
        http://www.hebbut.net/
mail:   g...@hobbelt.com
mobile: +31-6-11 120 978
--------------------------------------------------

Ger Hobbelt

unread,
May 12, 2012, 2:28:26 PM5/12/12
to d3...@googlegroups.com
An alternative approach may be using closures - 'tis a bit nasty to some tastes, but it should work, also for multiple items which each their own info:

d3.selectAll("xyz")
  .data(dd)
  .....
  .each(function do_the_closure_thing(d, i)
  {
    var el_s = d3.select(this)
       .on('mouseover', function() {
          // take what you need:
          do_my_thing1(d);
          do_my_thing2(el_s, d, i);
       });
  });



Met vriendelijke groeten / Best regards,

Ger Hobbelt

--------------------------------------------------
web:    http://www.hobbelt.com/
        http://www.hebbut.net/
mail:   g...@hobbelt.com
mobile: +31-6-11 120 978
--------------------------------------------------



Emily Yount

unread,
May 12, 2012, 2:42:30 PM5/12/12
to d3...@googlegroups.com
K, I tried the anonymous function before and that didn't seem to work. So far, though, based on y'alls suggestions I am getting the console to trace the data I'm looking for with the following...

    var circle = d3.select(this);
    console.log(circle.data());

produces the following...

[Object { id=17, company="Johnson Outdoors Inc.", industryID="6", more...}]

which I think is what I want.

However, circle.data().company doesn't access the company attribute.

Do I need to do something like .getAttribute('company'), similar to how you parse XML?


Thanks so much for all the input,
Emily

Mike Bostock

unread,
May 12, 2012, 4:30:29 PM5/12/12
to d3...@googlegroups.com
Emily's original code was correct. When you call selection.on(type,
listener), you are registering a *listener* callback function for
events of the specified *type* on the given *selection*. So, the
correct form is:

.on("mouseover", onMouseOver)

This does not immediately call onMouseOver; notice that there are no
parens following it. Instead, the function is being passed as an
object, so that selection.on can register for later callback when a
mouseover event is triggered. You can think of it just like plain
JavaScript:

element.onmouseover = onMouseOver;

The other thing to note is that when you register a listener with
selection.on, the listener will be called with two arguments: the data
(d) and the index (i). So the value of `d` within the onMouseOver
function in the original code Emily posted should be the data bound to
the selection. If the value of `d` is undefined, then something else
is likely happening—something unbinding the data from those elements,
say by selection.datum(null) or an inadvertent parent.select(child)
which overwrites the data on the first element. It's tough to say
without seeing the full code.

The listener (onMouseOver) is also called with a specific `this`
context: the element that received the mouseover event. Sometimes it's
useful to access this element within the listener function, say to
d3.select(this) and then style it.

If you change the code to this:

.on("mouseover", function(d) { onMouseOver(d); })

You haven't changed the value of `d`, but now you've lost the `this`
context and dropped the second argument `i`. So there's no reason to
do that. You could use a closure as Ger suggested to capture the data
(and make it continue to work even if the data is unbound from the
selected elements), but I think that's overkill and there's likely a
problem elsewhere that needs fixing.

One last thing. If you call selection.data() with no arguments, it
returns the array of data for the first group. (See nested selections
[1] for details.) If you just want the first datum, then call
selection.datum() instead. But, you shouldn't need to
d3.select(this).datum(), since the value of `d` passed to your
listener function is exactly the same thing. If it's not, then you
need to find where the data is being unbound elsewhere in your code.

Mike

[1] http://bost.ocks.org/mike/nest/

Emily Yount

unread,
May 12, 2012, 4:39:11 PM5/12/12
to d3...@googlegroups.com
Thanks, Mike.

The confusing thing is that if my console is tracing that data object with all the elements I'm looking to access, how could my data be unbound when it reaches the onmouseover function?

Essentially d3.select(this).data() returns exactly what I'm looking for... I just can't seem to access the attributes within that data set.

Mike Bostock

unread,
May 12, 2012, 4:47:27 PM5/12/12
to d3...@googlegroups.com
> how could my data be unbound when it reaches the onmouseover function?

A few ways: by selection.datum(null),
selection.data(arrayWithUndefinedValues), parent.select(child), etc.
It's impossible to tell without seeing the rest of the code. When it
doubt, try removing code until it works.

> Essentially d3.select(this).data() returns exactly what I'm looking for... I
> just can't seem to access the attributes within that data set.

That returns an array of data. If you want the first datum, you need
to `d3.select(this).data()[0]`, or better `d3.select(this).datum()`.
But you really shouldn't need to do this, since the first argument to
the listener is always the element's bound data.

Mike

Emily Yount

unread,
May 12, 2012, 5:15:26 PM5/12/12
to d3...@googlegroups.com
Okay, I'll definitely look into where my data could have become unbound.

However, d3.select(this).datum().company; (company being an attribute) seems to work for now.

Of course figuring out how the data became unbound would be a cleaner solution, so I'll work on that in the future :)

Thanks, Mike!

Sachin Dangol

unread,
May 27, 2013, 11:10:28 PM5/27/13
to d3...@googlegroups.com
Hello Emily,
     I think the simplest solution would be just define the function which returns the anonymous function required by "mouseevent" :

//call like this:
.on("mouseover", onMouseOver(d))

//define like this
function onMouseOver(d) {
return (function() {
// here d is accessible
// d.color whaterver
});
}


thanks,
Sachin
Reply all
Reply to author
Forward
0 new messages