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

你离开我真会死。 提交于 2021-02-17 06:52:04

问题


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 fiddle) but feeling little complex in adding tooltip when hover over the points. Is it possible in d3js. Because when zooming we are adding rect element on the svg element. if we add the rect element in the chart means how to make this tooltip works. Need some help from d3 ninjas.

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 + ")");

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

  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());

  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(".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;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="500"></svg>

回答1:


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>


来源:https://stackoverflow.com/questions/63879042/is-it-possible-to-add-zoom-and-tooltip-on-the-same-line-chart-in-d3js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!