Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 zoom and mouseover tooltip

Tags:

d3.js

I'm having issues trying to make mouseover works on my D3 graph (I'm not an D3 expert). I managed to make line and points sincronized with zoom but unable to show toolptip on point over, not sure if I'm missing something, as you can see in the image below everything is working fine.

You can my code using the button below. Thanks for the help.

var data = JSON.parse('{"success":true,"response":[{"product_date":"2019-09-01","value":{"temperature":"30.6029968261719"}},{"product_date":"2019-09-02","value":{"temperature":"30.1170043945312"}},{"product_date":"2019-09-03","value":{"temperature":"30.0830078125"}},{"product_date":"2019-09-04","value":{"temperature":"30.2479858398438"}},{"product_date":"2019-09-05","value":{"temperature":"30.9110107421875"}},{"product_date":"2019-09-06","value":{"temperature":"31.3150024414062"}},{"product_date":"2019-09-07","value":{"temperature":"31.2909851074219"}},{"product_date":"2019-09-08","value":{"temperature":"30.7149963378906"}},{"product_date":"2019-09-09","value":{"temperature":"30.010009765625"}},{"product_date":"2019-09-10","value":{"temperature":"29.7990112304688"}},{"product_date":"2019-09-11","value":{"temperature":"29.6549987792969"}},{"product_date":"2019-09-12","value":{"temperature":"30.0769958496094"}},{"product_date":"2019-09-13","value":{"temperature":"30.0830078125"}},{"product_date":"2019-09-14","value":{"temperature":"29.8619995117188"}},{"product_date":"2019-09-15","value":{"temperature":"30.0029907226562"}},{"product_date":"2019-09-16","value":{"temperature":"30.1080017089844"}},{"product_date":"2019-09-17","value":{"temperature":"30.6979980469"}},{"product_date":"2019-09-18","value":{"temperature":"30.3139953613"}},{"product_date":"2019-09-19","value":{"temperature":"30.5180053710938"}},{"product_date":"2019-09-20","value":{"temperature":"30.3720092773"}},{"product_date":"2019-09-21","value":{"temperature":"29.8710021973"}},{"product_date":"2019-09-22","value":{"temperature":"29.7460021972656"}},{"product_date":"2019-09-23","value":{"temperature":"29.5769958496"}},{"product_date":"2019-09-24","value":{"temperature":"29.1159973145"}},{"product_date":"2019-09-25","value":{"temperature":"28.908996582"}}]}');

      var svg = d3.select("svg"),
          margin = {top: 20, right: 20, bottom: 110, left: 40},
          margin2 = {top: 430, right: 20, bottom: 30, left: 40},
          width = +svg.attr("width") - margin.left - margin.right,
          height = +svg.attr("height") - margin.top - margin.bottom,
          height2 = +svg.attr("height") - margin2.top - margin2.bottom;

      var parseDate = d3.timeParse("%b %Y");

      var x = d3.scaleTime().range([0, width]),
          x2 = d3.scaleTime().range([0, width]),
          y = d3.scaleLinear().range([height, 0]),
          y2 = d3.scaleLinear().range([height2, 0]);

      var xAxis = d3.axisBottom(x),
          xAxis2 = d3.axisBottom(x2),
          yAxis = d3.axisLeft(y);

      var brush = d3.brushX()
          .extent([[0, 0], [width, height2]])
          .on("brush end", brushed);

      var zoom = d3.zoom()
          .scaleExtent([1, Infinity])
          .translateExtent([[0, 0], [width, height]])
          .extent([[0, 0], [width, height]])
          .on("zoom", zoomed);

      var line1 = d3.line()
          .curve(d3.curveMonotoneX)
          .x(function(d) { return x(d.date) })
          .y(function(d) { return y(d.value) });

      var line2 = d3.line()
          .curve(d3.curveMonotoneX)
          .x(function(d) { return x2(d.date) })
          .y(function(d) { return y2(d.value) });

      svg.append("defs").append("clipPath")
          .attr("id", "clip")
        .append("rect")
          .attr("width", width)
          .attr("height", height);

      var focus = svg.append("g")
          .attr("class", "focus")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      var points = svg.append('g')
          .attr("clip-path", "url(#clip)")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

      var context = svg.append("g")
          .attr("class", "context")
          .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

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

      

      let vdata = data.response;

      // format the data
      vdata.forEach((d) => {
          d.date = d3.timeParse("%Y-%m-%d")(d.product_date);
          d.value = +d.value.temperature;
      });

      x.domain(d3.extent(vdata, function(d) { return d.date; }));
      y.domain([0, d3.max(vdata, function(d) { return d.value; })]);
      x2.domain(x.domain());
      y2.domain(y.domain());

      focus.append("path")
          .datum(vdata)
          .attr("clip-path", "url(#clip)")
          .attr("class", "line")
          .attr("fill", "none")
          .attr("stroke", "steelblue")
          .attr("stroke-width", 1.5)
          .attr("d", line1);

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

      focus.append("g")
          .attr("class", "axis axis--y")
          .call(yAxis);

      points.selectAll(".dot")
          .data(vdata)
          .enter().append("circle")
          .attr("class", "dot")
          .attr("cx", function(d, i) { return x(d.date) })
          .attr("cy", function(d) { return y(d.value) })
          .attr("r", 3)
          .attr("pointer-events", "all")
          .on("mouseover", function(d) {
            div.transition()		
                .duration(200)		
                .style("opacity", .9);		
            div	.html(parseDate(d.date) + "<br/>"  + f(d.value))	
                .style("left", (d3.event.pageX) + "px")		
                .style("top", (d3.event.pageY - 28) + "px");
          })
          .on("mouseout", function(d) {
            div.transition()		
                .duration(500)		
                .style("opacity", 0);
          });
      
      points.append("g")
          .attr("class", "axis axis--y")
          .call(yAxis);

      context.append("path")
          .datum(vdata)
          .attr("class", "line")
          .attr("fill", "none")
          .attr("stroke", "steelblue")
          .attr("stroke-width", 1.5)
          .attr("d", line2);

      context.append("g")
          .attr("class", "axis axis--x")
          .attr("transform", "translate(0," + height2 + ")")
          .call(xAxis2);

      context.append("g")
          .attr("class", "brush")
          .call(brush)
          .call(brush.move, x.range());

      svg.append("rect")
          .attr("class", "zoom")
          .attr("width", width)
          .attr("height", height)
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
          .call(zoom);

      function brushed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
        var s = d3.event.selection || x2.range();
        x.domain(s.map(x2.invert, x2));
        focus.select(".line").attr("d", d3.line()
                  .x(function(d) { return x(d.date) })
                  .y(function(d) { return y(d.value) })
                  .curve(d3.curveMonotoneX)
                );
        focus.select(".axis--x").call(xAxis);

        points.selectAll('circle')
                .attr("cx", function(d, i) { return x(d.date) })
                .attr("cy", function(d) { return y(d.value) });
        points.select(".axis--x").call(xAxis);

        svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
            .scale(width / (s[1] - s[0]))
            .translate(-s[0], 0));
      }

      function zoomed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
        var t = d3.event.transform;
        x.domain(t.rescaleX(x2).domain());
        focus.select(".line").attr("d", d3.line()
                  .x(function(d) { return x(d.date) })
                  .y(function(d) { return y(d.value) })
                  .curve(d3.curveMonotoneX)
                );

        points.selectAll('circle')
                .attr("cx", function(d, i) { return x(d.date) })
                .attr("cy", function(d) { return y(d.value) });
        points.select(".axis--x").call(xAxis);

        focus.select(".axis--x").call(xAxis);
        context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
      }

      function type(d) {
        d.date = parseDate(d.date);
        d.value = +d.value;
        return d;
      }
.area {
      fill: steelblue;
      clip-path: url(#clip);
    }

    .zoom {
      cursor: move;
      fill: none;
      pointer-events: all;
    }

    .dot {
      fill: steelblue;
      stroke: #fff;
    }

    div.tooltip {	
      position: absolute;			
      text-align: left;			
      width: 70px;					
      height: 28px;					
      padding: 3px;				
      font: 11px sans-serif;		
      background: lightsteelblue;	
      border: 0px;		
      border-radius: 3px;			
      pointer-events: none;			
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="960" height="500"></svg>

enter image description here

like image 259
Jaime Valdez Avatar asked Nov 04 '25 10:11

Jaime Valdez


1 Answers

In order to do zoom interaction you have placed an invisible rectangle over your plot area:

  svg.append("rect")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(zoom);

You do this after you append the g that holds the plot, so the rectangle is over the points.

The pointer events are intercepted by this rectangle before they can interact with the points.

What you can do instead is append the rectangle to the g that is selected as focus and lower it with selection.lower() (this just moves it so it is the first child of the g, which has the effect of drawing it first or under the plot):

  focus.append("rect")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
      .lower();
      .call(zoom);

However now we have the opposite problem (though it is slight): the plot intercepts mouse events meaning that one cannot zoom if the mouse is over the plot, so we could modify this slightly:

  focus.append("rect")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
      .lower();

      focus.call(zoom);

However, as focus is not a selection of the g that holds the points, when the mouse is over a point, the graph won't zoom (when the mouse is over the line, it will though). Likewise, as the axes are part of the g selected in focus, one can zoom when the mouse is over the axes.

This may be a problem, perhaps, not, but to address it, we can rejig the DOM structure a bit to ensure everything in the plot area (above the bottom axis and to the right of the vertical axis) interacts with the zoom, but nothing else.

To do so we add the points as children of the focus g, and call the axes on a different g (I've added a new one for you):

      var axes = svg.append("g")
         .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

      var points = focus.append('g')  // no need to add a transform, focus already has one.
          .attr("clip-path", "url(#clip)")

Of course we'll need to update all the places where we call an axis to call it on the new g. Altogether we get:

var data = JSON.parse('{"success":true,"response":[{"product_date":"2019-09-01","value":{"temperature":"30.6029968261719"}},{"product_date":"2019-09-02","value":{"temperature":"30.1170043945312"}},{"product_date":"2019-09-03","value":{"temperature":"30.0830078125"}},{"product_date":"2019-09-04","value":{"temperature":"30.2479858398438"}},{"product_date":"2019-09-05","value":{"temperature":"30.9110107421875"}},{"product_date":"2019-09-06","value":{"temperature":"31.3150024414062"}},{"product_date":"2019-09-07","value":{"temperature":"31.2909851074219"}},{"product_date":"2019-09-08","value":{"temperature":"30.7149963378906"}},{"product_date":"2019-09-09","value":{"temperature":"30.010009765625"}},{"product_date":"2019-09-10","value":{"temperature":"29.7990112304688"}},{"product_date":"2019-09-11","value":{"temperature":"29.6549987792969"}},{"product_date":"2019-09-12","value":{"temperature":"30.0769958496094"}},{"product_date":"2019-09-13","value":{"temperature":"30.0830078125"}},{"product_date":"2019-09-14","value":{"temperature":"29.8619995117188"}},{"product_date":"2019-09-15","value":{"temperature":"30.0029907226562"}},{"product_date":"2019-09-16","value":{"temperature":"30.1080017089844"}},{"product_date":"2019-09-17","value":{"temperature":"30.6979980469"}},{"product_date":"2019-09-18","value":{"temperature":"30.3139953613"}},{"product_date":"2019-09-19","value":{"temperature":"30.5180053710938"}},{"product_date":"2019-09-20","value":{"temperature":"30.3720092773"}},{"product_date":"2019-09-21","value":{"temperature":"29.8710021973"}},{"product_date":"2019-09-22","value":{"temperature":"29.7460021972656"}},{"product_date":"2019-09-23","value":{"temperature":"29.5769958496"}},{"product_date":"2019-09-24","value":{"temperature":"29.1159973145"}},{"product_date":"2019-09-25","value":{"temperature":"28.908996582"}}]}');

      var svg = d3.select("svg"),
          margin = {top: 20, right: 20, bottom: 110, left: 40},
          margin2 = {top: 430, right: 20, bottom: 30, left: 40},
          width = +svg.attr("width") - margin.left - margin.right,
          height = +svg.attr("height") - margin.top - margin.bottom,
          height2 = +svg.attr("height") - margin2.top - margin2.bottom;

      var parseDate = d3.timeParse("%b %Y");

      var x = d3.scaleTime().range([0, width]),
          x2 = d3.scaleTime().range([0, width]),
          y = d3.scaleLinear().range([height, 0]),
          y2 = d3.scaleLinear().range([height2, 0]);

      var xAxis = d3.axisBottom(x),
          xAxis2 = d3.axisBottom(x2),
          yAxis = d3.axisLeft(y);

      var brush = d3.brushX()
          .extent([[0, 0], [width, height2]])
          .on("brush end", brushed);

      var zoom = d3.zoom()
          .scaleExtent([1, Infinity])
          .translateExtent([[0, 0], [width, height]])
          .extent([[0, 0], [width, height]])
          .on("zoom", zoomed);

      var line1 = d3.line()
          .curve(d3.curveMonotoneX)
          .x(function(d) { return x(d.date) })
          .y(function(d) { return y(d.value) });

      var line2 = d3.line()
          .curve(d3.curveMonotoneX)
          .x(function(d) { return x2(d.date) })
          .y(function(d) { return y2(d.value) });

      svg.append("defs").append("clipPath")
          .attr("id", "clip")
        .append("rect")
          .attr("width", width)
          .attr("height", height);

      var focus = svg.append("g")
          .attr("class", "focus")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
          
      var axes = svg.append("g")
         .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

      var points = focus.append('g')
          .attr("clip-path", "url(#clip)")


      var context = svg.append("g")
          .attr("class", "context")
          .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

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

      

      let vdata = data.response;

      // format the data
      vdata.forEach((d) => {
          d.date = d3.timeParse("%Y-%m-%d")(d.product_date);
          d.value = +d.value.temperature;
      });

      x.domain(d3.extent(vdata, function(d) { return d.date; }));
      y.domain([0, d3.max(vdata, function(d) { return d.value; })]);
      x2.domain(x.domain());
      y2.domain(y.domain());

      focus.append("path")
          .datum(vdata)
          .attr("clip-path", "url(#clip)")
          .attr("class", "line")
          .attr("fill", "none")
          .attr("stroke", "steelblue")
          .attr("stroke-width", 1.5)
          .attr("d", line1);

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

      axes.append("g")
          .attr("class", "axis axis--y")
          .call(yAxis);

      points.selectAll(".dot")
          .data(vdata)
          .enter().append("circle")
          .attr("class", "dot")
          .attr("cx", function(d, i) { return x(d.date) })
          .attr("cy", function(d) { return y(d.value) })
          .attr("r", 3)
          .attr("pointer-events", "all")
          .on("mouseover", function(d) {
            div.transition()		
                .duration(200)		
                .style("opacity", .9);		
            div	.html(parseDate(d.date) + "<br/>"  + (d.value))	
                .style("left", (d3.event.pageX) + "px")		
                .style("top", (d3.event.pageY - 28) + "px");
          })
          .on("mouseout", function(d) {
            div.transition()		
                .duration(500)		
                .style("opacity", 0);
          });
      
      points.append("g")
          .attr("class", "axis axis--y")
          .call(yAxis);

      context.append("path")
          .datum(vdata)
          .attr("class", "line")
          .attr("fill", "none")
          .attr("stroke", "steelblue")
          .attr("stroke-width", 1.5)
          .attr("d", line2);

      context.append("g")
          .attr("class", "axis axis--x")
          .attr("transform", "translate(0," + height2 + ")")
          .call(xAxis2);

      context.append("g")
          .attr("class", "brush")
          .call(brush)
          .call(brush.move, x.range());

      focus.append("rect")
          .attr("class", "zoom")
          .attr("width", width)
          .attr("height", height)
          .lower();
          
      focus.call(zoom);

      function brushed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
        var s = d3.event.selection || x2.range();
        x.domain(s.map(x2.invert, x2));
        focus.select(".line").attr("d", d3.line()
                  .x(function(d) { return x(d.date) })
                  .y(function(d) { return y(d.value) })
                  .curve(d3.curveMonotoneX)
                );
        axes.select(".axis--x").call(xAxis);

        points.selectAll('circle')
                .attr("cx", function(d, i) { return x(d.date) })
                .attr("cy", function(d) { return y(d.value) });
        points.select(".axis--x").call(xAxis);

        svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
            .scale(width / (s[1] - s[0]))
            .translate(-s[0], 0));
      }

      function zoomed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
        var t = d3.event.transform;
        x.domain(t.rescaleX(x2).domain());
        focus.select(".line").attr("d", d3.line()
                  .x(function(d) { return x(d.date) })
                  .y(function(d) { return y(d.value) })
                  .curve(d3.curveMonotoneX)
                );

        points.selectAll('circle')
                .attr("cx", function(d, i) { return x(d.date) })
                .attr("cy", function(d) { return y(d.value) });
        points.select(".axis--x").call(xAxis);

        axes.select(".axis--x").call(xAxis);
        context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
      }

      function type(d) {
        d.date = parseDate(d.date);
        d.value = +d.value;
        return d;
      }
.area {
      fill: steelblue;
      clip-path: url(#clip);
    }

    .zoom {
      cursor: move;
      fill: none;
      pointer-events: all;
    }

    .dot {
      fill: steelblue;
      stroke: #fff;
    }

    div.tooltip {	
      position: absolute;			
      text-align: left;			
      width: 70px;					
      height: 28px;					
      padding: 3px;				
      font: 11px sans-serif;		
      background: lightsteelblue;	
      border: 0px;		
      border-radius: 3px;			
      pointer-events: none;			
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="960" height="500"></svg>
like image 66
Andrew Reid Avatar answered Nov 07 '25 15:11

Andrew Reid



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!