Search code examples
javascriptd3.jsstacked-area-chart

Unable to show the tooltip values for stacked area chart in a single tooltip


<!DOCTYPE html>
<html>

<head>
  <script data-require="[email protected]" data-semver="3.5.3" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
  <style>
    body {
      font: 10px sans-serif;
    }
    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }
    .x.axis path {
      display: none;
    }
    .line {
      fill: none;
      stroke: steelblue;
      stroke-width: 1.5px;
    }
  </style>
</head>

<body>
  <script>
    var myData = "date	New York	San Francisco	Austin\n\
20111001	63.4	62.7	72.2\n\
20111002	58.0	59.9	67.7\n\
20111003	53.3	59.1	69.4\n\
20111004	55.7	58.8	68.0\n\
20111005	64.2	58.7	72.4\n\
20111006	58.8	57.0	77.0\n\
20111007	57.9	56.7	82.3\n\
20111008	61.8	56.8	78.9\n\
20111009	69.3	56.7	68.8\n\
20111010	71.2	60.1	68.7\n\
20111011	68.7	61.1	70.3\n\
20111012	61.8	61.5	75.3\n\
20111013	63.0	64.3	76.6\n\
20111014	66.9	67.1	66.6\n\
20111015	61.7	64.6	68.0\n\
20111016	61.8	61.6	70.6\n\
20111017	62.8	61.1	71.1\n\
20111018	60.8	59.2	70.0\n\
20111019	62.1	58.9	61.6\n\
20111020	65.1	57.2	57.4\n\
20111021	55.6	56.4	64.3\n\
20111022	54.4	60.7	72.4\n";

    var margin = {
        top: 20,
        right: 80,
        bottom: 30,
        left: 50
      },
      width = 500 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;

    var parseDate = d3.time.format("%Y%m%d").parse;

    var x = d3.time.scale()
      .range([0, width]);

    var y = d3.scale.linear()
      .range([height, 0]);

    var color = d3.scale.category20();

    var xAxis = d3.svg.axis()
      .scale(x)
      .orient("bottom");

    var yAxis = d3.svg.axis()
      .scale(y)
      .orient("left");

    var line = d3.svg.line()
      .interpolate("basis")
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.temperature);
      });

    var svg = d3.select("body").append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var data = d3.tsv.parse(myData);

    color.domain(d3.keys(data[0]).filter(function(key) {
      return key !== "date";
    }));

    data.forEach(function(d) {
      d.date = parseDate(d.date);
    });

    var cities = color.domain().map(function(name) {
      return {
        name: name,
        values: data.map(function(d) {
          return {
            date: d.date,
            temperature: +d[name]
          };
        })
      };
    });

    x.domain(d3.extent(data, function(d) {
      return d.date;
    }));

    y.domain([
      d3.min(cities, function(c) {
        return d3.min(c.values, function(v) {
          return v.temperature;
        });
      }),
      d3.max(cities, function(c) {
        return d3.max(c.values, function(v) {
          return v.temperature;
        });
      })
    ]);

    var legend = svg.selectAll('g')
      .data(cities)
      .enter()
      .append('g')
      .attr('class', 'legend');

    legend.append('rect')
      .attr('x', width - 20)
      .attr('y', function(d, i) {
        return i * 20;
      })
      .attr('width', 10)
      .attr('height', 10)
      .style('fill', function(d) {
        return color(d.name);
      });

    legend.append('text')
      .attr('x', width - 8)
      .attr('y', function(d, i) {
        return (i * 20) + 9;
      })
      .text(function(d) {
        return d.name;
      });

    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

    svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
      .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Temperature (ºF)");

    var city = svg.selectAll(".city")
      .data(cities)
      .enter().append("g")
      .attr("class", "city");

    city.append("path")
      .attr("class", "line")
      .attr("d", function(d) {
        return line(d.values);
      })
      .style("stroke", function(d) {
        return color(d.name);
      });

    city.append("text")
      .datum(function(d) {
        return {
          name: d.name,
          value: d.values[d.values.length - 1]
        };
      })
      .attr("transform", function(d) {
        return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
      })
      .attr("x", 3)
      .attr("dy", ".35em")
      .text(function(d) {
        return d.name;
      });

    var mouseG = svg.append("g")
      .attr("class", "mouse-over-effects");

    mouseG.append("path") // this is the black vertical line to follow mouse
      .attr("class", "mouse-line")
      .style("stroke", "black")
      .style("stroke-width", "1px")
      .style("opacity", "0");

    var lines = document.getElementsByClassName('line');

    var mousePerLine = mouseG.selectAll('.mouse-per-line')
      .data(cities)
      .enter()
      .append("g")
      .attr("class", "mouse-per-line");

    mousePerLine.append("rect")
      .attr("width", width / 2)
      .attr("height", 30)
      .style("padding", "5px")
      .style("stroke", function(d) {
        return color(d.name);
      })
      .style("fill", function(d) {
        return color(d.name);
      })
      .style("stroke-width", "1px")
      .style("opacity", "0")
      .attr('x', 10);


    mousePerLine.append("circle")
      .attr("r", 5)
      .style("stroke", function(d) {
        return color(d.name);
      })
      .style("fill", function(d) {
        return color(d.name);
      })
      .style("stroke-width", "1px")
      .style("opacity", "0");

    mousePerLine.append("text")
      .attr("transform", "translate(15,13)");

    mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
      .attr('width', width) // can't catch mouse events on a g element
      .attr('height', height)
      .attr('fill', 'none')
      .attr('pointer-events', 'all')
      .on('mouseout', function() { // on mouse out hide line, circles and text
        d3.select(".mouse-line")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line rect")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "0");
      })
      .on('mouseover', function() { // on mouse in show line, circles and text
        d3.select(".mouse-line")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line rect")
          .style("opacity", "0.5");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "1");
      })
      .on('mousemove', function() { // mouse moving over canvas
        var mouse = d3.mouse(this);
        d3.select(".mouse-line")
          .attr("d", function() {
            var d = "M" + mouse[0] + "," + height;
            d += " " + mouse[0] + "," + 0;
            return d;
          });

        d3.selectAll(".mouse-per-line")
          .attr("transform", function(d, i) {
            console.log(width / mouse[0])
            var xDate = x.invert(mouse[0]),
              bisect = d3.bisector(function(d) {
                return d.date;
              }).right;
            idx = bisect(d.values, xDate);

            var beginning = 0,
              end = lines[i].getTotalLength(),
              target = null;

            while (true) {
              target = Math.floor((beginning + end) / 2);
              pos = lines[i].getPointAtLength(target);
              if ((target === end || target === beginning) && pos.x !== mouse[0]) {
                break;
              }
              if (pos.x > mouse[0]) end = target;
              else if (pos.x < mouse[0]) beginning = target;
              else break; //position found
            }

            d3.select(this).select('text')
              .text(y.invert(pos.y).toFixed(2));

            return "translate(" + mouse[0] + "," + pos.y + ")";
          });
      });
  </script>
</body>

</html>

Hello. I am new to D3. I am trying to make a stacked area chart using D3 and on mouse hover I want to show all the multiple data values inside a single rectangle for each area chart. Till now I have achieved to show the data values on mouse hover in separate individual rectangles for each data value. Please help me with some suggestions/ tips about how to achieve this. I am really stuck and struggling. Thanks for any help in advance.


Solution

  • Ok, this is as ugly as it gets. I some how managed to solve the issue(I am also new to d3).

    I will explain how I solved. There should and will be better ways to achieve your goal.

    1. First I added a single group instead of three groups
    2. Next added data separately for each svg element i.e, rect, text and circle.
    3. on mouse hover, I selected the mouse-per-line class elements then, I transformed each svg element individually.
    4. For appending the rectangle, I stored the heights in an array and took the average for setting the position of rectangle. Similarly did the same transformation for text.

    Once again, mine is just a working model, there will/should be more elegant ways to achieve the same.

    If someone can modify or suggest edits, it would be helpful. Hope this helps.

        var myData = "date	New York	San Francisco	Austin\n\
    20111001	63.4	62.7	72.2\n\
    20111002	58.0	59.9	67.7\n\
    20111003	53.3	59.1	69.4\n\
    20111004	55.7	58.8	68.0\n\
    20111005	64.2	58.7	72.4\n\
    20111006	58.8	57.0	77.0\n\
    20111007	57.9	56.7	82.3\n\
    20111008	61.8	56.8	78.9\n\
    20111009	69.3	56.7	68.8\n\
    20111010	71.2	60.1	68.7\n\
    20111011	68.7	61.1	70.3\n\
    20111012	61.8	61.5	75.3\n\
    20111013	63.0	64.3	76.6\n\
    20111014	66.9	67.1	66.6\n\
    20111015	61.7	64.6	68.0\n\
    20111016	61.8	61.6	70.6\n\
    20111017	62.8	61.1	71.1\n\
    20111018	60.8	59.2	70.0\n\
    20111019	62.1	58.9	61.6\n\
    20111020	65.1	57.2	57.4\n\
    20111021	55.6	56.4	64.3\n\
    20111022	54.4	60.7	72.4\n";
    
        var margin = {
            top: 20,
            right: 80,
            bottom: 30,
            left: 50
          },
          width = 500 - margin.left - margin.right,
          height = 500 - margin.top - margin.bottom;
    
        var parseDate = d3.time.format("%Y%m%d").parse;
    
        var x = d3.time.scale()
          .range([0, width]);
    
        var y = d3.scale.linear()
          .range([height, 0]);
    
        var color = d3.scale.category20();
    
        var xAxis = d3.svg.axis()
          .scale(x)
          .orient("bottom");
    
        var yAxis = d3.svg.axis()
          .scale(y)
          .orient("left");
    
        var line = d3.svg.line()
          .interpolate("basis")
          .x(function(d) {
            return x(d.date);
          })
          .y(function(d) {
            return y(d.temperature);
          });
    
        var svg = d3.select("body").append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
          .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
        var data = d3.tsv.parse(myData);
    
        color.domain(d3.keys(data[0]).filter(function(key) {
          return key !== "date";
        }));
    
        data.forEach(function(d) {
          d.date = parseDate(d.date);
        });
    
        var cities = color.domain().map(function(name) {
          return {
            name: name,
            values: data.map(function(d) {
              return {
                date: d.date,
                temperature: +d[name]
              };
            })
          };
        });
    
        x.domain(d3.extent(data, function(d) {
          return d.date;
        }));
    
        y.domain([
          d3.min(cities, function(c) {
            return d3.min(c.values, function(v) {
              return v.temperature;
            });
          }),
          d3.max(cities, function(c) {
            return d3.max(c.values, function(v) {
              return v.temperature;
            });
          })
        ]);
    
        var legend = svg.selectAll('g')
          .data(cities)
          .enter()
          .append('g')
          .attr('class', 'legend');
    
        legend.append('rect')
          .attr('x', width - 20)
          .attr('y', function(d, i) {
            return i * 20;
          })
          .attr('width', 10)
          .attr('height', 10)
          .style('fill', function(d) {
            return color(d.name);
          });
    
        legend.append('text')
          .attr('x', width - 8)
          .attr('y', function(d, i) {
            return (i * 20) + 9;
          })
          .text(function(d) {
            return d.name;
          });
    
        svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")")
          .call(xAxis);
    
        svg.append("g")
          .attr("class", "y axis")
          .call(yAxis)
          .append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", 6)
          .attr("dy", ".71em")
          .style("text-anchor", "end")
          .text("Temperature (ºF)");
    
        var city = svg.selectAll(".city")
          .data(cities)
          .enter().append("g")
          .attr("class", "city");
    
        city.append("path")
          .attr("class", "line")
          .attr("d", function(d) {
            return line(d.values);
          })
          .style("stroke", function(d) {
            return color(d.name);
          });
    
        city.append("text")
          .datum(function(d) {
            return {
              name: d.name,
              value: d.values[d.values.length - 1]
            };
          })
          .attr("transform", function(d) {
            return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
          })
          .attr("x", 3)
          .attr("dy", ".35em")
          .text(function(d) {
            return d.name;
          });
    
        var mouseG = svg.append("g")
          .attr("class", "mouse-over-effects");
    
        mouseG.append("path") // this is the black vertical line to follow mouse
          .attr("class", "mouse-line")
          .style("stroke", "black")
          .style("stroke-width", "1px")
          .style("opacity", "0");
    
        var lines = document.getElementsByClassName('line');
    
        var mousePerLine = mouseG.selectAll('.mouse-per-line')
          .data([cities])
          .enter()
          .append("g")
          .attr("class", "mouse-per-line");
    
        mousePerLine.selectAll('.mouse-per-line')
          .data([cities])
          .enter().append("rect")
          .attr("width", width / 2)
          .attr("height", 90)
          .style("padding", "5px")
          .style("stroke", function(d) {
            return color(d.name);
          })
          .style("fill", function(d) {
            return color(d.name);
          })
          .style("stroke-width", "1px")
          .style("opacity", "0")
          .attr('x', 10)
    	  .attr('y',-45);
    
    
        mousePerLine.selectAll('.mouse-per-line')
          .data(cities)
          .enter().append("circle")
          .attr("r", 5)
          .style("stroke", function(d) {
            return color(d.name);
          })
          .style("fill", function(d) {
            return color(d.name);
          })
          .style("stroke-width", "1px")
          .style("opacity", "0");
    
        mousePerLine.selectAll('.text')
          .data(cities)
          .enter().append("text")
          .attr("transform", "translate(15,13)");
    
        mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
          .attr('width', width) // can't catch mouse events on a g element
          .attr('height', height)
          .attr('fill', 'none')
          .attr('pointer-events', 'all')
          .on('mouseout', function() { // on mouse out hide line, circles and text
            d3.select(".mouse-line")
              .style("opacity", "0");
            d3.selectAll(".mouse-per-line rect")
              .style("opacity", "0");
            d3.selectAll(".mouse-per-line circle")
              .style("opacity", "0");
            d3.selectAll(".mouse-per-line text")
              .style("opacity", "0");
          })
          .on('mouseover', function() { // on mouse in show line, circles and text
            d3.select(".mouse-line")
              .style("opacity", "1");
            d3.selectAll(".mouse-per-line rect")
              .style("opacity", "0.5");
            d3.selectAll(".mouse-per-line circle")
              .style("opacity", "1");
            d3.selectAll(".mouse-per-line text")
              .style("opacity", "1");
          })
          .on('mousemove', function() { // mouse moving over canvas
            var mouse = d3.mouse(this);
            d3.select(".mouse-line")
              .attr("d", function() {
                var d = "M" + mouse[0] + "," + height;
                d += " " + mouse[0] + "," + 0;
                return d;
              });
    		d3.selectAll(".mouse-per-line")
              .attr("foo", function(d, i) {
                var xDate = x.invert(mouse[0]);
                var  bisect;
    			var  heights = [];
    			
    			d3.selectAll('circle')
    				.attr("transform", function(d,j) {
    					bisect = d3.bisector(function(d) {
                    return d.date;
                  }).right;
                idx = bisect(d.values, xDate);
    
                var beginning = 0,
                  end = lines[i].getTotalLength(),
                  target = null;
    
                while (true) {
                  target = Math.floor((beginning + end) / 2);
                  pos = lines[j].getPointAtLength(target);
                  if ((target === end || target === beginning) && pos.x !== mouse[0]) {
                    break;
                  }
                  if (pos.x > mouse[0]) end = target;
                  else if (pos.x < mouse[0]) beginning = target;
                  else break; //position found
                }
    				heights[j] = pos.y;
    				return "translate(" + mouse[0] + "," + pos.y + ")";
    				});
    			var avgheight = 0;	
    			for(var z = 0; z < heights.length ; z++){
    				avgheight = avgheight + heights[z];
    			}	
    			avgheight = avgheight/d.length;
    						d3.select(this).selectAll('rect')
    				.attr("transform", function(d,i) {
    					return "translate(" + mouse[0] + "," + avgheight + ")";
    				});
    			var rectangleText = "";
    			for(var t = 1; t < heights.length ; t++) {
    				rectangleText = rectangleText + "<br/>" + y.invert(heights[t]).toFixed(2);
    			}	
                d3.select(this).selectAll('text').text(function(d,i) { return y.invert(heights[i]).toFixed(2)}).attr("transform", function(d,i) {
    					return "translate(" + mouse[0] + "," + (avgheight  + 30 - (i*25)) + ")";
    				}).attr("dx", '20px');			
                return "translate(" + mouse[0] + "," + pos.y + ")";
              });
          });
    body {
          font: 10px sans-serif;
        }
        .axis path,
        .axis line {
          fill: none;
          stroke: #000;
          shape-rendering: crispEdges;
        }
        .x.axis path {
          display: none;
        }
        .line {
          fill: none;
          stroke: steelblue;
          stroke-width: 1.5px;
        }
    <!DOCTYPE html>
    <html>
    
    <head>
      <script data-require="[email protected]" data-semver="3.5.3" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
    </head>
    
    <body>
      
    </body>
    
    </html>