d3 工具提示 - 附加圆未能遵循折线图的值

d3 tooltip - appended circle failing to follow values of line graph

我在 d3 中有一个垂直线图,我正在尝试以直线和圆圈的形式添加一个工具提示,它跟随光标在垂直方向上的移动。后者有效,但圆圈未能遵循路径 - 我尝试了不同的变体,但圆圈从未遵循路径,它目前仅遵循图表的 y 轴(例如,参见附图)。我已经为水平图实现了相同的效果,但是当我尝试调整垂直图的代码时,我无法让圆正常工作。

我在下面的代码中提供了一个示例,对 javascript 来说还是很新,所以代码有点乱。

带圆圈(红色)的图表截图未能遵循路径:

function test(test_data) {

// setup params
var margin_ = {top: 30, right: 60, bottom: 30, left: 20},
width_ = 300
height_ = 700



// Add svg
var line_graph = d3.select("#my_dataviz_test")
.append("svg")
  .attr("width", width_ + 100)
  .attr("height", height_)
.append("g")
  .attr("transform",
        "translate(" + margin_.left + "," + margin_.top + ")");


d3.csv(test_data, 

    function(d){
    return { output_time_ref: d.output_time_ref = +d.output_time_ref,
             output_time: d3.timeParse("%d/%m/%Y %H:%M")(d.output_time),
             prediction: d.prediction = +d.prediction,
            }
    },

    function(data) {

      // Add x axis
      var x_test = d3.scaleLinear()
        .domain([0, d3.max(data, function(d) { return +d.prediction; })])
        .range([ 0, width_ ]);
      line_graph.append("g")
        .attr("transform", "translate(" + 0 + "," + height_ + ")")
        .call(d3.axisBottom(x_test).tickSizeOuter(0).tickSizeInner(0).ticks(2))
        .select(".domain").remove();

      // Add Y axis
      var y_test = d3.scaleLinear()
        .domain([0, d3.max(data, function(d) { return +d.output_time_ref; })])
        .range([ height_, 0 ]);
      line_graph.append("g")
        .call(d3.axisLeft(y_test).tickSizeOuter(0).tickSizeInner(0).ticks(5))
        .select(".domain").remove();

      // Add the line
      path_test = line_graph.append("path") 
        .datum(data)
        .attr("fill", "none")
        .attr("fill", "steelblue")
        .attr("fill-opacity", 0.2)
        .attr("stroke", "steelblue")
        .attr("stroke-width", 1)
        .attr("d", d3.line()
          .curve(d3.curveBasis)
          .x(function(d) { return x_test(d.prediction) })
          .y(function(d) { return y_test(d.output_time_ref) })
          )


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

  mouseG2
    .append("path")
    .attr("class", "mouse-line2")
    .style("stroke", "#393B45") 
    .style("stroke-width", "0.5px")
    .style("opacity", 0.75)


  mouseG2.append("text")
    .attr("class", "mouse-text2")

  var totalLength2 = path_test.node().getTotalLength();


  var mousePerLine2 = mouseG2.selectAll('.mouse-per-line2')
    .data(data)
    .enter()
    .append("g")
    .attr("class", "mouse-per-line2");

  mousePerLine2.append("circle")
    .attr("r", 8)
    .style("stroke", 'red')
    .style("fill", "none")
    .style("stroke-width", "2px")
    .style("opacity", "0");



    mouseG2
      .append('svg:rect') 
      .attr('width', width_) 
      .attr('height', height_) 
      .attr('fill', 'none')
      // .attr('opacity', 0.2)
      .attr('pointer-events', 'all')
    .on('mouseout', function() {
      d3.select("#my_dataviz_test")
        .selectAll(".mouse-per-line2 circle")
        .style("opacity", "0"); })


  var mouseover = function(d) {
    d3.select("#my_dataviz_test")
        .select(".mouse-line2")
        .style("opacity", "1")
        .select(".mouse-text2")
        .style("opacity", "1")
        .select(".mouse-per-line2 circle")
        .style("opacity", "1");


      /////////////////////////////////////////////////// 
      d3.select("#my_dataviz_test")
      var mouse2 = d3.mouse(this);

       d3.select("#my_dataviz_test")
        .select(".mouse-text2")
        .attr("y", mouse2[1])
        .attr("transform", "translate(" + (mouse2[1]+60) + "," + (mouse2[1]+5) + ") rotate(90)")

      d3.select("#my_dataviz_test")
        .select(".mouse-line2")
        .attr("d", function() {
          var d = "M" + width_ + "," + mouse2[1];
          d += " " + 0 + "," + mouse2[1];
          return d;
        })

      d3.select("#my_dataviz_test")
        .selectAll(".mouse-per-line2")
        .attr("transform", function(d, i) {

          var beginning2 = 0,
              end2 = totalLength2
              target2 = null;

          while (true){

            target2 = Math.floor((beginning2 + end2) / 2);


            var pos2 = path_test.node().getPointAtLength(target2);

            if ((target2 === end2 || target2 === beginning2) && pos2.y !== mouse2[1]) {
                break;
            }
            if (pos2.y > mouse2[1]) { end2 = target2; }
            else if (pos2.y < mouse2[1]) { beginning2 = target2; }

            else {break}; 
          }


          d3.select("#my_dataviz_test").select('circle')
            .style("opacity", 1)


          return "translate(" + (pos2.x) + "," + mouse2[1] +")";

        });

        /////////////////////////////////////////////////// 

  }
  var mouseleave = function(d) {
    d3.select("#my_dataviz_test")
      .select(".mouse-line2")
      .style("opacity", "0")
    d3.select("#my_dataviz_test")
      .select(".circle")
      .style("opacity", "0")
  }

line_graph
      .on("mouseover", mouseover)
      .on("mouseleave", mouseleave)


})

}
test_data.csv:

output_time_ref,output_time,prediction
0,04/01/2013 00:00,0
1,04/01/2013 00:30,0
2,04/01/2013 01:00,0
3,04/01/2013 01:30,0
4,04/01/2013 02:00,0
5,04/01/2013 02:30,0
6,04/01/2013 00:00,0
7,04/01/2013 03:30,0
8,04/01/2013 04:00,0
9,04/01/2013 04:30,8.17E-05
10,04/01/2013 05:00,0.002014463
11,04/01/2013 05:30,0.01322314
12,04/01/2013 06:00,0.033264463
13,04/01/2013 06:30,0.059607438
14,04/01/2013 07:00,0.098553719
15,04/01/2013 07:30,0.145661157
16,04/01/2013 08:00,0.186983471
17,04/01/2013 08:30,0.225206612
18,04/01/2013 09:00,0.267561983
19,04/01/2013 09:30,0.314049587
20,04/01/2013 10:00,0.334710744
21,04/01/2013 10:30,0.350206612
22,04/01/2013 11:00,0.359504132
23,04/01/2013 11:30,0.375
24,04/01/2013 12:00,0.393595041
25,04/01/2013 12:30,0.396694215
26,04/01/2013 13:00,0.393595041
27,04/01/2013 13:30,0.385330579
28,04/01/2013 14:00,0.367768595
29,04/01/2013 14:30,0.344008264
30,04/01/2013 15:00,0.320247934
31,04/01/2013 15:30,0.297520661
32,04/01/2013 16:00,0.273760331
33,04/01/2013 16:30,0.254132231
34,04/01/2013 17:00,0.216942149
35,04/01/2013 17:30,0.167355372
36,04/01/2013 18:00,0.123966942
37,04/01/2013 18:30,0.080785124
38,04/01/2013 19:00,0.041115702
39,04/01/2013 19:30,0.015805785
40,04/01/2013 20:00,0.002489669
41,04/01/2013 20:30,2.67E-05
42,04/01/2013 21:00,1.24E-05
43,04/01/2013 21:30,0
44,04/01/2013 22:00,0
45,04/01/2013 22:30,0
46,04/01/2013 23:00,0
47,04/01/2013 23:30,0

您可以使用输入数据 y 精确计算 x

const y = d3.event.layerY - margin_.top;
const curY = y_test.invert(y);
const minY = Math.floor(curY);
const maxY = Math.ceil(curY);
if (data[minY] && data[maxY]) {
  const yDelta = curY - minY;
  const minP = data[minY].prediction;
  const maxP = data[maxY].prediction;
  const curP = minP + (maxP - minP) * yDelta;
  const xPos = x_test(curP)
  ...
}

在代码段中看到它正在运行:

const csvData = `output_time_ref,output_time,prediction
0,04/01/2013 00:00,0
1,04/01/2013 00:30,0
2,04/01/2013 01:00,0
3,04/01/2013 01:30,0
4,04/01/2013 02:00,0
5,04/01/2013 02:30,0
6,04/01/2013 00:00,0
7,04/01/2013 03:30,0
8,04/01/2013 04:00,0
9,04/01/2013 04:30,8.17E-05
10,04/01/2013 05:00,0.002014463
11,04/01/2013 05:30,0.01322314
12,04/01/2013 06:00,0.033264463
13,04/01/2013 06:30,0.059607438
14,04/01/2013 07:00,0.098553719
15,04/01/2013 07:30,0.145661157
16,04/01/2013 08:00,0.186983471
17,04/01/2013 08:30,0.225206612
18,04/01/2013 09:00,0.267561983
19,04/01/2013 09:30,0.314049587
20,04/01/2013 10:00,0.334710744
21,04/01/2013 10:30,0.350206612
22,04/01/2013 11:00,0.359504132
23,04/01/2013 11:30,0.375
24,04/01/2013 12:00,0.393595041
25,04/01/2013 12:30,0.396694215
26,04/01/2013 13:00,0.393595041
27,04/01/2013 13:30,0.385330579
28,04/01/2013 14:00,0.367768595
29,04/01/2013 14:30,0.344008264
30,04/01/2013 15:00,0.320247934
31,04/01/2013 15:30,0.297520661
32,04/01/2013 16:00,0.273760331
33,04/01/2013 16:30,0.254132231
34,04/01/2013 17:00,0.216942149
35,04/01/2013 17:30,0.167355372
36,04/01/2013 18:00,0.123966942
37,04/01/2013 18:30,0.080785124
38,04/01/2013 19:00,0.041115702
39,04/01/2013 19:30,0.015805785
40,04/01/2013 20:00,0.002489669
41,04/01/2013 20:30,2.67E-05
42,04/01/2013 21:00,1.24E-05
43,04/01/2013 21:30,0
44,04/01/2013 22:00,0
45,04/01/2013 22:30,0
46,04/01/2013 23:00,0
47,04/01/2013 23:30,0`;

var margin_ = {top: 30, right: 60, bottom: 30, left: 20},
width_ = 300
height_ = 700

// Add svg
var line_graph = d3.select("#my_dataviz_test")
    .append("svg")
  .attr("width", width_ + 100)
  .attr("height", height_)
    .append("g")
  .attr("transform",
        "translate(" + margin_.left + "," + margin_.top + ")");
        
const point = line_graph.append('circle')
    .attr('r', 5)
  .style('fill', 'red');
        
const data = d3.csvParse(csvData).map(d => ({
    output_time_ref: +d.output_time_ref,
  output_time: d3.timeParse("%d/%m/%Y %H:%M")(d.output_time),
  prediction: +d.prediction,
}));
console.log(data);

      // Add x axis
var x_test = d3.scaleLinear()
        .domain([0, d3.max(data, d => d.prediction)])
        .range([ 0, width_ ]);
        
      line_graph.append("g")
        .attr("transform", "translate(" + 0 + "," + height_ + ")")
        .call(d3.axisBottom(x_test).tickSizeOuter(0).tickSizeInner(0).ticks(2))
        .select(".domain").remove();

      // Add Y axis
 var y_test = d3.scaleLinear()
        .domain([0, d3.max(data, d => +d.output_time_ref)])
        .range([ height_, 0 ]);
      
line_graph.append("g")
        .call(d3.axisLeft(y_test).tickSizeOuter(0).tickSizeInner(0).ticks(5))
        .select(".domain").remove();

      // Add the line
path_test = line_graph.append("path") 
        .datum(data)
        .attr("fill", "none")
        .attr("fill", "steelblue")
        .attr("fill-opacity", 0.2)
        .attr("stroke", "steelblue")
        .attr("stroke-width", 1)
        .attr("d", d3.line()
          .curve(d3.curveBasis)
          .x(function(d) { return x_test(d.prediction) })
          .y(function(d) { return y_test(d.output_time_ref) })
          )

line_graph.on('mousemove', () => {
  const y = d3.event.layerY - margin_.top;
  const curY = y_test.invert(y);
  const minY = Math.floor(curY);
  const maxY = Math.ceil(curY);
  if (data[minY] && data[maxY]) {
    const yDelta = curY - minY;
    const minP = data[minY].prediction;
    const maxP = data[maxY].prediction;
    const curP = minP + (maxP - minP) * yDelta;
    const xPos = x_test(curP)
    // console.log(xPos);
    point
        .attr('cx', xPos)
      .attr('cy', y)
  }  
  
  // line_graph
  // y_test
})        


/*
var mouseG2 = line_graph
    .append("g")
    .attr("class", "mouse-over-effects");

  mouseG2
    .append("path")
    .attr("class", "mouse-line2")
    .style("stroke", "#393B45") 
    .style("stroke-width", "0.5px")
    .style("opacity", 0.75)

  mouseG2.append("text")
    .attr("class", "mouse-text2")

  var totalLength2 = path_test.node().getTotalLength();

  var mousePerLine2 = mouseG2.selectAll('.mouse-per-line2')
    .data(data)
    .enter()
    .append("g")
    .attr("class", "mouse-per-line2");

  mousePerLine2.append("circle")
    .attr("r", 8)
    .style("stroke", 'red')
    .style("fill", "none")
    .style("stroke-width", "2px")
    .style("opacity", "0");

    mouseG2
      .append('svg:rect') 
      .attr('width', width_) 
      .attr('height', height_) 
      .attr('fill', 'none')
      // .attr('opacity', 0.2)
      .attr('pointer-events', 'all')
    .on('mouseout', function() {
      d3.select("#my_dataviz_test")
        .selectAll(".mouse-per-line2 circle")
        .style("opacity", "0"); })
  
  var mouseover = function(d) {
    d3.select("#my_dataviz_test")
        .select(".mouse-line2")
        .style("opacity", "1")
        .select(".mouse-text2")
        .style("opacity", "1")
        .select(".mouse-per-line2 circle")
        .style("opacity", "1");

      d3.select("#my_dataviz_test")
      var mouse2 = d3.mouse(this);

       d3.select("#my_dataviz_test")
        .select(".mouse-text2")
        .attr("y", mouse2[1])
        .attr("transform", "translate(" + (mouse2[1]+60) + "," + (mouse2[1]+5) + ") rotate(90)")

      d3.select("#my_dataviz_test")
        .select(".mouse-line2")
        .attr("d", function() {
          var d = "M" + width_ + "," + mouse2[1];
          d += " " + 0 + "," + mouse2[1];
          return d;
        })

      d3.select("#my_dataviz_test")
        .selectAll(".mouse-per-line2")
        .attr("transform", function(d, i) {

          var beginning2 = 0,
              end2 = totalLength2
              target2 = null;

          while (true){

            target2 = Math.floor((beginning2 + end2) / 2);


            var pos2 = path_test.node().getPointAtLength(target2);

            if ((target2 === end2 || target2 === beginning2) && pos2.y !== mouse2[1]) {
                break;
            }
            if (pos2.y > mouse2[1]) { end2 = target2; }
            else if (pos2.y < mouse2[1]) { beginning2 = target2; }

            else {break}; 
          }


          d3.select("#my_dataviz_test").select('circle')
            .style("opacity", 1)


          return "translate(" + (pos2.x) + "," + mouse2[1] +")";

        });

 
  }
  
 var mouseleave = function(d) {
    d3.select("#my_dataviz_test")
      .select(".mouse-line2")
      .style("opacity", "0")
    d3.select("#my_dataviz_test")
      .select(".circle")
      .style("opacity", "0")
  }

line_graph
      .on("mouseover", mouseover)
      .on("mouseleave", mouseleave);
*/
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="my_dataviz_test" />