Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js passing in multiple functions on hover

I used a tutorial to get this function on mouseover:

function arcTween(outerRadius, delay) {
    return function () {
        d3.select(this).transition().delay(delay).attrTween("d", function (d) {
            var i = d3.interpolate(d.outerRadius, outerRadius);
            return function (t) { d.outerRadius = i(t); return arc(d); };
        });
    };
} 

And I add it to parts of a pie chart this way:

.on("mouseover", arcTween(outerRadius, 0, 0))

However, I also have text tags added to svg for each slice in the pie chart, and want those to fade away if you are hovering over a different slice. So I gave those tags IDs when I create them based on the index, then wrote these two methods:

function visibilityShow(dataSetSize) {
    for (var i = 0; i < dataSetSize; i++) {
        $("#" + i).show();
    }
}

function visibilityHide(index, dataSetSize) {
    for (var i = 0; i < dataSetSize; i++) {
        if (i === index) {
            $("#" + i).show();
        } else {
            $("#" + i).hide();
        }
    }
}

Now these work in a vacuum, but when I try to put them on a mouseover event, it won't work. arcTween stops working, and "i" is always 0. These were what I tried:

Adding another .on("mouseover", ...)

        .on("mouseover", arcTween(outerRadius, 0))
        .on("mouseover", visibility(0, dataSet.length));

and also tried passing in the index with:

        .on("mouseover", arcTween(outerRadius, 0))
        .on("mouseover", function (d, i) { return visibility(i, d.length) });

But that always passes in i = 0 in addition to seemingly overwriting the arcTween() call.

I also tried

.on("mouseover", function (d, i) {
     return function {
          arcTween(outerRadius, 0);
          visibility(i, d.length);
     }
})

Anyone have any advice? (I'm using v3 because all the tutorials online are outdated.)

Thanks!

EDIT: Code Snippet

// This data will be gathered from API calls eventually
dataDefault = [];
dataController = [{ "label": "Example 1", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
                  { "label": "Example 2", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
                  { "label": "Example 3", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
                  { "label": "Example 4", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
                  { "label": "Example 5", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] }];

var displaySize = 20;

// This is used to keep track of what data is showing
var mode = "Default";

// The amount of pixels the SVG will take up
var width = 600,
    height = 675;

// It's a donut, so it has an outer radius and an inner radius. 2r = width so r = width/2
var outerRadius = width / 2,
    innerRadius = outerRadius / 3;

// Default color function for deciding the colros of the donut slices
var color = d3.scale.category10();

// The pie function for deciding the size of the donut slices
var pie = d3.layout.pie()
    .value(function (d) { return d["value"]; });

// At first we use the default data to create the pie
var pieData = pie(dataDefault);

// Create an arc
var arc = d3.svg.arc()
    .innerRadius(innerRadius);

// Add an SVG tag to the document
var svg = d3.select("#graphs").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
  .attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");

// Append an link tag for each point of the data, then add an path tag inside each a tag
svg.selectAll("a")
    .data(pieData)
  .enter().append("a")
    .append("path")
    .each(function (d) { d.outerRadius = outerRadius - 20; })
    .attr("d", arc)
    .attr("fill", function (d, i) { return color(i); })
    .on("mouseover", arcTween(outerRadius, 0, 0))
    .on("mouseout", arcTween(outerRadius - 20, 150))
        .append("title")
        .text(function (d) { return d["value"] + " hits"; });

// Change the default data to the Apps data so it animates on load
changeToAPI("Controller", dataController);

// Function used to increase slice size on hover
function arcTween(outerRadius, delay) {
    return function () {
        d3.select(this).transition().delay(delay).attrTween("d", function (d) {
            var i = d3.interpolate(d.outerRadius, outerRadius);
            return function (t) { d.outerRadius = i(t); return arc(d); };
        });
    };
}

// Passes the color scale into the change function
function getColor(name) {
    // Get the remainder when / 3
    var bucket = hashify(name) % 4;

    // Setup the array of color functions
    var colors = [d3.scale.category10(), d3.scale.category20(), d3.scale.category20b(), d3.scale.category20c()];

    // Return the correct bucket
    return colors[bucket];
}

// Function used to swap the data being shown
function changeToAPI(name, dataSet) {
    // Don't update if the data is already showing

    // JavaScript doesn't short circuit?
    if (dataSet === null) {
        dataSet = [{ "label": "No data...", "value": 1 }];
        changeTo(name, dataSet);
    } else if (dataSet.length === 0) {
        dataSet = [{ "label": "No data...", "value": 1 }];
        changeTo(name, dataSet);
    } else {

        mode = name;

        // Get the new pie and color functions
        var newData = pie(dataSet);
        var newColor = getColor(name);

        // Remove the labels, titles, and tooltips
        svg.selectAll("text").remove();
        svg.selectAll("title").remove();
        // Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
        svg.selectAll("a").remove();

        // Add the new slices if there are any
        var newSlices = svg.selectAll("a")
                            .data(newData);

        newSlices.enter()
            .append("a")
                .append("path")
                .style("cursor", "pointer");

        // Update the attributes of those slices and animate the transition
        newSlices.select("path")
                .each(function (d) { d.outerRadius = outerRadius - 20; })
                .transition()
                .attr("d", arc)
                .attr("fill", function (d, i) { return newColor(i); })
                .attr("title", function (d) { return d["value"]; });

        newSlices.selectAll("path")
                    .on("click", function (d) {
                        checkForChild(d["data"]["label"], d["data"]);
                    })
                    .on("mouseover.arcExpand", arcTween(outerRadius, 0))
                    .on("mouseover.textHide", function (d, i) {
                        visibilityHide(i, dataSet.length);
                    })
                    .on("mouseout.arcRetract", arcTween(outerRadius - 20, 150))
                    .on("mouseout.textShow", function (d, i) {
                        visibilityShow(dataSet.length);
                    });

        // Remove excess slices
        newSlices.exit().remove();

        // Add a title
        var title = svg.append("text")
            .attr("x", 0)
            .attr("y", -(outerRadius + 10))
            .style("text-anchor", "middle")
            .text("Distrubution of " + name + " Usage");

        // Add labels
        var labels = svg.selectAll(null)
            .data(newData)
            .enter()
                .append("text")
                .attr("fill", "white")
                .attr("id", function (d, i) { return i })
                .attr("transform", function (d) {
                    d.innerRadius = 0;
                    d.outerRadius = outerRadius;
                    return "translate(" + arc.centroid(d) + ")";
                })
                .attr("text-anchor", "middle")
                .text(function (d, i) {
                    return dataSet[i]["label"];
                });

        // Add tooltips
        svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });

        svg.append("circle")
        .attr("cx", 0)
        .attr("cy", 0)
        .attr("r", innerRadius)
        .style("fill", "white")
        .style("cursor", "pointer")
        .on("click", function () {
            changeToAPI("Controller", dataController);
        });

        // Adds back button if not at controller level
        if (dataSet !== dataController) {
            svg.append("text")
                .attr("x", 0)
                .attr("y", 12)
                .style("text-anchor", "middle")
                .style("color", "#efefef")
                .style("font-size", "40px")
                .text("Back");
        }
    }
}

function changeTo(name, dataSet) {
    // Don't update if the data is already showing

    // JavaScript doesn't short circuit?
    if (dataSet === null) {
        dataSet = [{ "label": "No data...", "value": 1 }];
    } else if (dataSet.length === 0) {
        dataSet = [{ "label": "No data...", "value": 1 }];
    }

    mode = name;

    // Get the new pie and color functions
    var newData = pie(dataSet);
    var newColor = getColor(name);

    // Remove the labels, titles, and tooltips
    svg.selectAll("text").remove();
    svg.selectAll("title").remove();
    // Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
    //svg.selectAll("a").remove();

    // Add the new slices if there are any
    var newSlices = svg.selectAll("a")
                        .data(newData);

    newSlices.enter()
        .append("a")
            .append("path")
            .style("cursor", "pointer");

    // Update the attributes of those slices and animate the transition
    newSlices.select("path")
            .each(function (d) { d.outerRadius = outerRadius - 20; })
            .transition()
            .attr("fill", function (d, i) { return newColor(i); })
            .attr("d", arc)
            .attr("title", function (d) { return d["value"]; });

    newSlices.selectAll("path")
                .on("mouseover.arc", arcTween(outerRadius, 0))
                .on("mouseover.text", function (d, i) {
                    visibilityHide(i, dataSet.length);
                 })
                .on("mouseout.arc", arcTween(outerRadius - 20, 150))
                .on("mouseout.text", function (d, i) {
                    visibilityShow(dataSet.length);
                });

    // Remove excess slices
    newSlices.exit().remove();

    // Add a title
    svg.append("text")
        .attr("x", 0)
        .attr("y", -(outerRadius + 10))
        .style("text-anchor", "middle")
        .text(function (e) {
            var title = "Distrubution of " + name + " Usage";
            if (name === "Defualt") {
                title = "Loading..."
            }
            return title;
        });

    // Add labels
    svg.selectAll(null)
        .data(newData)
        .enter()
            .append("text")
            .attr("fill", "white")
            .attr("id", function (d, i) { return i })
            .attr("transform", function (d) {
                d.innerRadius = 0;
                d.outerRadius = outerRadius;
                return "translate(" + arc.centroid(d) + ")";
            })
            .attr("text-anchor", "middle")
            .text(function (d, i) {
                return dataSet[i]["label"];
            });

    // Add tooltips
    svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });
}

function checkForChild(name, dataSet) {
    if (dataSet.hasOwnProperty("child")) {
        if (dataSet["child"] !== null) {
            if (dataSet["child"].length !== 0) {
                changeToAPI(name, dataSet["child"]);
            }
        }
    }
}

// Hashcode generator for strings
function hashify(string) {
    var hash = 0;

    // Add the value of each char to the hash value
    for (var i = 0; i < string.length; i++) {
        hash += string.charCodeAt(i);
    }

    return hash;
}

function visibilityShow(dataSetSize) {
    for (var i = 0; i < dataSetSize; i++) {
        $("#" + i).show();
    }
}

function visibilityHide(index, dataSetSize) {
    for (var i = 0; i < dataSetSize; i++) {
        if (i === index) {
            $("#" + i).show();
        } else {
            $("#" + i).hide();
        }
    }
}
body {
    font-family: Arial;
    transition: all ease .5s;
    text-align: center;
    color: rgb(58,58,58);
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <script src="https://d3js.org/d3.v3.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <title>General Statistics</title>
</head>
<body>
    <div id="graphs">
    </div>
</body>
</html>
like image 688
Riley Shaw Avatar asked Jul 07 '17 19:07

Riley Shaw


1 Answers

If you have more than one event listener of the same type in the same selection, you have to namespace your event listeners (but this probably won't fix your issue, please also read the Post Scriptum further down).

The problem right now, as @AndrewReid explained in his comment, is that the next event listener removes the previous one. According to the API:

If an event listener was already registered for the same type on the selected element, the existing listener is removed before the new listener is added.

Let's see it in the following demo.

Since you didn't provide your working code, I create a simple one here, with two event listeners: the first one increases the circle, and the second one fades out the text:

.on("mouseover", increaseCircle)//this one will not work!
.on("mouseover", fadeText)//only this one will work...

You can see that only the last one works:

var svg = d3.select("svg");

var circle = svg.append("circle")
  .attr("r", 20)
  .attr("cx", 100)
  .attr("cy", 50)
  .attr("fill", "tan")
  .attr("stroke", "black")
  .on("mouseover", increaseCircle)
  .on("mouseover", fadeText)
  .on("mouseout", function() {
    circle.transition().duration(500).attr("r", 20);
    text.transition().duration(500).style("opacity", 1);
  })

var text = svg.append("text")
  .attr("y", 55)
  .attr("x", 150)
  .style("font-family", "helvetica")
  .text("Hover over the circle");

function increaseCircle() {
  circle.transition().duration(500).attr("r", 40)
}

function fadeText() {
  text.transition().duration(500).style("opacity", 0)
}
<script src="https://d3js.org/d3.v3.js"></script>
<svg></svg>

Solution:

There is a very simple solution, though. According to the same API:

To register multiple listeners for the same event type, the type may be followed by an optional namespace, such as "click.foo" and "click.bar"

Thus, in the above demo, we just need something like this:

.on("mouseover.circle", increaseCircle)
.on("mouseover.text", fadeText)

Here is the demo, both event listeners work:

var svg = d3.select("svg");

var circle = svg.append("circle")
  .attr("r", 20)
  .attr("cx", 100)
  .attr("cy", 50)
  .attr("fill", "tan")
  .attr("stroke", "black")
  .on("mouseover.circle", increaseCircle)
  .on("mouseover.text", fadeText)
  .on("mouseout", function() {
    circle.transition().duration(500).attr("r", 20);
    text.transition().duration(500).style("opacity", 1);
  })

var text = svg.append("text")
  .attr("y", 55)
  .attr("x", 150)
  .style("font-family", "helvetica")
  .text("Hover over the circle");

function increaseCircle() {
  circle.transition().duration(500).attr("r", 40)
}

function fadeText() {
  text.transition().duration(500).style("opacity", 0)
}
<script src="https://d3js.org/d3.v3.js"></script>
<svg></svg>

Of course, a simple alternative to all this is just:

selection.on("mouseover", function(){
    foo();
    bar();
    baz();
    etc...
});

PS: The above answer deals with the namespace issue. However, besides that issue, your code has a couple of problems, which we cannot test because you didn't provide a working demo.

First problem: when you do this...

.on("mouseover", arcTween(outerRadius, 0, 0))

... you are calling arcTween immediately, and passing its value to the listener. You probably want:

.on("mouseover", function(){ arcTween(outerRadius, 0, 0)})

Second, this is not correct:

.on("mouseover", function (d, i) {
    return function {
        arcTween(outerRadius, 0);
        visibility(i, d.length);
    }
})

It should be just:

.on("mouseover", function (d, i) {
    arcTween(outerRadius, 0);
    visibility(i, d.length);
})
like image 83
Gerardo Furtado Avatar answered Nov 04 '22 02:11

Gerardo Furtado