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

前端 未结 1 1105
猫巷女王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("

    Price: " + d.price + "

    "); }) .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;
    }
    
    

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