Is it possible to add zoom and tooltip on the same line chart in d3js?

前端 未结 1 1107
猫巷女王i
猫巷女王i 2021-01-27 11:19

Currently I am learning d3js, one of the feature i like to implement is showing tooltip and zooming horizontally. I figured out how to add zooming in the chart (working

相关标签:
1条回答
  • 2021-01-27 11:56

    Of course it's possible to add a tooltip in d3, there are a lot of examples and even a dedicated package for older versions.

    You can choose to show a tooltip inside the SVG (as a rect with text) or outside, as a div. The benefit of outside is that the tooltip can overflow the SVG, the downside is that positioning can be more difficult, especially with scrolling.

    I show a very simple implementation below, using a DIV tooltip. I positioned the .zoom rect behind the circles, so they would catch the mouse events instead, and added on mouseenter and mouseleave event listeners.

    var data = [{
        date: "10:30:00",
        price: 36000
      },
      {
        date: "11:00:20",
        price: 40000
      },
      {
        date: "12:00:00",
        price: 38000
      },
      {
        date: "14:20:00",
        price: 50400
      }
    ];
    
    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("%H:%M:%S"); //"%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 area = d3.line()
      //.curve(d3.curveMonotoneX)
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.price);
      });
    
    var area2 = d3.line()
      .curve(d3.curveMonotoneX)
      .x(function(d) {
        return x2(d.date);
      })
      .y(function(d) {
        return y2(d.price);
      });
    
    
    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 context = svg.append("g")
      .attr("class", "context")
      .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
    
    var tooltip = d3.select('body')
      .append('div')
      .attr('id', 'tooltip')
      .style("transform", "translate(" + margin.left + "px," + margin.top + "px)")
      .classed('hide', true);
    
    function update() {
    
      for (var k in data) {
        type(data[k]);
      }
    
      x.domain(d3.extent(data, function(d) {
        return d.date;
      }));
      y.domain([0, d3.max(data, function(d) {
        return d.price;
      })]);
      x2.domain(x.domain());
      y2.domain(y.domain());
    
    
      focus.append("path")
        .datum(data)
        .attr("class", "area")
        .attr("d", area);
    
      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);
    
      focus.selectAll("circle")
        .data(data)
        .enter()
        .append("circle")
        .attr("class", "circle")
        .attr("r", 5)
        .style("fill", 'orange')
        .style("stroke", 'red')
        .style("stroke-width", "2")
        .attr("cx", function(d) {
          return x(d.date)
        })
        .attr("cy", function(d) {
          return y(d.price);
        })
        .on("mouseenter", function(d) {
          // Show the tooltip and position it correctly
          tooltip.classed('hide', false)
            .style('left', x(d.date).toString() + 'px')
            .style('top', y(d.price).toString() + 'px')
            .html("<p>Price: " + d.price + "</p>");
        })
        .on("mouseleave", function() {
          tooltip.classed('hide', true);
        });
    
      context.append("path")
        .datum(data)
        .attr("class", "area")
        .attr("d", area2);
    
      context.append("g")
        .attr("class", "axis axis--x")
        .attr("transform", "translate(0," + height2 + ")")
        .call(xAxis2);
    
      context.selectAll("circle")
        .data(data)
        .enter().append("circle")
        .attr("class", "circle")
        .attr("r", 1)
        .style("fill", 'blue')
        .style("stroke", 'red')
        .style("stroke-width", "2")
        .attr("cx", function(d) {
          return x(d.date)
        })
        .attr("cy", function(d) {
          return y(d.price);
        });
    
      context.append("g")
        .attr("class", "brush")
        .call(brush)
        .call(brush.move, x.range());
    
      // Insert the zoom rect *before* the circles, so the circles
      // are drawn in front of the recrt
      focus.insert("rect", "circle")
        .attr("class", "zoom")
        .attr("width", width)
        .attr("height", height)
        .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(".area").attr("d", area);
    
      focus.selectAll('.circle')
        .attr("cx", function(d) {
          return x(d.date)
        })
        .attr("cy", function(d) {
          return y(d.price);
        });
    
      focus.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(".area").attr("d", area);
    
      focus.selectAll('.circle')
        .attr("cx", function(d) {
          return x(d.date)
        })
        .attr("cy", function(d) {
          return y(d.price);
        });
    
      focus.select(".axis--x").call(xAxis);
      context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
      context.selectAll('.circle')
        .attr("cx", function(d) {
          return x(d.date)
        })
        .attr("cy", function(d) {
          return y(d.price);
        });
    }
    
    function type(d) {
      d.date = parseDate(d.date);
      d.price = +d.price;
      return d;
    }
    update();
    .area {
      fill: none;
      stroke: #a2dced;
      stroke-width: 2;
      clip-path: url(#clip);
    }
    
    .zoom {
      cursor: move;
      fill: none;
      pointer-events: all;
    }
    
    rect.selection {
      fill: green;
    }
    
    #tooltip {
      position: absolute;
      border: solid 1px black;
      background: white;
      margin: 20px;
    }
    
    .hide {
      opacity: 0;
    }
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <svg width="960" height="500"></svg>

    0 讨论(0)
提交回复
热议问题