Add tooltip to D3 linear graph

2019-04-16 10:47发布

问题:

I'm getting crazy on how to add tooltip to my linear d3 chart, so that the user can see the specific value of the performance along the years. This is the fiddle: http://jsfiddle.net/d9Lya/ and this the code

    // define dimensions of graph
    var m = [20, 80, 80, 80]; // margins
    var w = 650 - m[1] - m[3]; // width
    var h = 500 - m[0] - m[2]; // height


    var data = [30, 28, 33] ;
    var years = [2010, 2011, 2012] ;

    var data2 = [100, 200, 200] ;
    var years2 = [2009, 2010, 2011] ;
    var alldata = data.concat(data2);
    var allyears = years.concat(years2);

    var data3 = [200, 220, 300] ;
    var years3 = [2011, 2012, 2013] ;
    var alldata = data.concat(data2, data3);
    var allyears = years.concat(years2, years3);


    //unique vals functin
    var unique = function(origArr) {
        var newArr = [],
            origLen = origArr.length,
            found,
            x, y;
        for ( x = 0; x < origLen; x++ ) {
            found = undefined;
            for ( y = 0; y < newArr.length; y++ ) {
                if ( origArr[x] === newArr[y] ) {
                  found = true;
                  break;
                }
            }
            if ( !found) newArr.push( origArr[x] );   
        }
       return newArr;
    };


    allyears = unique(allyears);


    var x = d3.scale.linear().domain([d3.min(allyears), d3.max(allyears)]).range([0,w]);


    var y = d3.scale.linear().domain([0, (d3.max(alldata))*1.3]).range([h, 0]);


    var line = d3.svg.line()

        .x(function(d,i) { 

            return x(years[i]); 
        })

        .y(function(d) { 

            return y(d); 
        })


    var line2 = d3.svg.line()

        .x(function(d,i) { 
            return x(years2[i]); 
        })

        .y(function(d) { 

            return y(d); 
        })          

    var line3 = d3.svg.line()
        .x(function(d,i) { 
            return x(years3[i]); 
        })

        .y(function(d) { 

            return y(d); 
        })          

    // Add an SVG element with the desired dimensions and margin.
    var graph = d3.select("#graph").append("svg:svg")
        .attr("width", w + m[1] + m[3])
        .attr("height", h + m[0] + m[2])
        .append("svg:g")
        .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    // create xAxis
    var xAxis = d3.svg.axis().scale(x).ticks(allyears.length).tickSize(-h).tickSubdivide(true);
    // Add the x-axis.
    graph.append("svg:g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + h + ")")
          .call(xAxis);


    // create left yAxis
    var yAxisLeft = d3.svg.axis().scale(y).ticks(8).orient("left");
    // Add the y-axis to the left
    graph.append("svg:g")
          .attr("class", "y axis")
          .attr("transform", "translate(-25,0)")
          .call(yAxisLeft);


    graph.append("svg:text")
        .attr("class", "title1")
        .attr("x", (w/2))
        .attr("y", 0)
        .text("Performance")
        .style({ "stroke": "Black", "fill": "Black", "stroke-width": "1px"})
        .attr("text-anchor", "middle")  
        .style("font-size", "16px") ;   



    graph.append("svg:path").attr("d", line(data)).style("stroke", "steelblue");
    graph.append("svg:text")
        .attr("class", "title1")
        .attr("x", 0)
        .attr("y", 30)
        .text("TEAM A")
        .style({ "stroke": "steelblue", "fill": "steelblue", "stroke-width": "0px"});       
    graph.append("svg:path")
    .attr("d", line2(data2)).style("stroke", "green")
    ;




        graph.append("svg:text")
                        .attr("class", "title2")
                        .attr("x", 0)
                        .attr("y", 50)
                        .text("TEAM B")
                        .style({ "stroke": "Green", "fill": "Green", "stroke-width": "0px"});         
            graph.append("svg:path").attr("d", line3(data3)).style("stroke", "red");
                graph.append("svg:text")
                        .attr("class", "title3")
                        .attr("x", 0)
                        .attr("y", 70)
                        .text("team C")
                        .style({ "stroke": "Red", "fill": "Red", "stroke-width": "0px"});

I'm actually new to D3 and I've seen seveal other samples around and I'm not able to reproduce them on my js code. Can anyone help me? Regards

回答1:

I mentioned in the comments that "the line's data is the complete array, and figuring out where on the line the user's mouse is would require extra calculation". That's one reason to use "invisible circles" to grab mouse events over datapoints. Another is that you can make the radius of the circle much larger than the stroke width to make a larger area for mouse events. A third is that you can then decorate the circles on mouseover, like in the NVD3 line charts.

But what if you don't want extra circles crowding up your DOM. Or what if you want the mouseover event to be triggered anywhere on the line, not just on the points? You can still figure out where the mouse is relative to the line, and can use that to find the correct point in the data array for that line.

d3.selectAll("path")
    .on("mouseover", findValue);

function findValue(d, i) {
    var mouseX = d3.mouse(this.parentNode)[0];
    //find the mouse's horizontal coordinate relative to the drawing container

    var dataX = x.invert(mouseX); 
       //invert the scale to find the x value at the mouse point

    //scan through the data array to find the value closest to the mouse
    var j = d.length;
    while ((j--) && (d[j].x > dataX)); //repeat until false

    var datapoint;
    if (j >= 0) {
        //d[j] will now be the first datapoint *less than* the mousepoint
        //compare it with d[j+1] to see which is closer to the mouse:
        if ( isNaN(d[j+1]) || (dataX - d[j].x < d[j+1].x - dataX) )
            datapoint = d[j];
        else
            datapoint = d[j+1];
    } else {
        //all the values in the array were greater than the mouse point, 
        //so return the first point
        datapoint = d[0];
    }

    //Do something with your datapoint value:
    alert("Crossed line " + i + " near " + [datapoint.x, datapoint.y]);
}

Live example here: http://fiddle.jshell.net/c2mru/8/

The d value passed to the event handler function is the data object attached to the <path> element. That is normally the array of points on the line, although sometimes it is an object that contains the array of points as a property, in which case you'd have to amend this to suit. Using d3.mouse(container) you can figure out the position of the mouse in the relevant SVG coordinate system, and using the invert method of the scale you can figure out the position of the mouse relative to the horizontal axis. Then, assuming that your data is a normal line graph that doesn't repeat x-values, it's a simple matter of scrolling through the array of points to find the one closest to the mouse.

Note that this method won't work with @picus' original code, because that code doesn't actually use d3 methods to link data to the elements. (@picus, check out the Tutorials page for how to make working with d3 much easier!)