d3-delaunay 线不显示

d3-delaunay lines not showing

我正在尝试在一个折线图上实现 D3-delaunay,该折线图以圆圈作为线穿过的点。我认为我做的一切都是对的,但我似乎看不到出现的叠加线。我在这里做错了什么?我正在使用本网站 (https://observablehq.com/@didoesdigital/9-may-2020-d3-scatterplot-with-voronoi-tooltips) 上的代码作为参考,并在我的线图中实现它。如果这个参考不合适,那么请提供另一个类似于我可以实现的散点图的模板。

class Linechart extends React.Component {
  constructor(props) {
    super(props)
    this.createLineChart = this.createLineChart.bind(this)
  }

  metricToPercent(metric) {
    return (metric / 2 + 0.5) * 100;
  };

  scoreToDescrip(score) {
    if (score >= 0.6) {
      return "Good";
    } else if (score >= 0) {
      return "Average";
    } else {
      return "Poor";
    }
  };

  componentDidMount() {
    this.createLineChart()
  }

  componentDidUpdate() {
    this.createLineChart()
  }

  createLineChart() {
    var margin = {
        top: 85,
        right: 60,
        bottom: 60,
        left: 80
      },
      width = 960 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;

    var node = this.node
    var divObj = d3.select(node)
    var svgObj = divObj
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    //Read the data
    d3.json("https://raw.githubusercontent.com/QamarFarooq/data-for-testing/main/experience_scores.json").then(function(data) {
      var dataArrayWithRemovedEmptyDates = []

      // Remove all elements in the array that dont have companyReviewDate property
      data.forEach(function(d, i) {
        if (d.hasOwnProperty('companyReviewDate')) {
          dataArrayWithRemovedEmptyDates.push(d)
        }
      })

      // Transform `companyReviewDate` into an actual date
      dataArrayWithRemovedEmptyDates.forEach(function(d) {
        d.companyReviewDate = new Date(d.companyReviewDate);
      })

      // Remove all whitespace in CompanyBusinessName, it creates problems 
      data.forEach(function(d, i) {
        d.companyBusinessName = d.companyBusinessName.split(' ').join('');
      })

      // group the data: I want to draw one line per group
      var sumstat = d3.nest()
        .key(function(d) {
          return d.companyBusinessName;
        })
        .entries(dataArrayWithRemovedEmptyDates);

      console.log(sumstat)

      // Define the div for the tooltip
      var tooltip = divObj
        .append("div")
        .attr("class", "tooltip")
        .style("position", "absolute")
        .style("z-index", "10")
        .style("visibility", "hidden")
        .style("background-color", "white")
        .style("box-shadow", "0 0 4px #000000")
        .style("padding", "10px")

      const monthNames = ["January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December"
      ];

      var d = new Date();

      tooltip.append("div")
        .attr("class", "tooltipDate")
        .html(monthNames[d.getMonth()] + " " + "(" + d.getFullYear() + ")")
        .style("font-size", "20px")
        .style("text-align", "center")

      tooltip.append("div")
        .attr("class", "tooltipName")
        .style("text-align", "center")
        .style("color", "grey")

      tooltip.append("div")
        .attr("class", "tooltipTitle")
        .style("text-align", "center")
        .html("Customer Sentiment")
        .style("padding-top", "10px")

      tooltip.append("div")
        .attr("class", "tooltipScore")
        .style("text-align", "center")
        .style("color", 'DarkGrey')
        .style("font-size", "20px")

      tooltip.append("div")
        .attr("class", "tooltipPerception")
        .style("text-align", "center")


      // Add title for linechart
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 25)
        .attr("x", 110)
        .attr("y", -50)
        .text("Customer Experience Score");

      // Add X axis --> it is a date format
      var x = d3.scaleTime()
        .domain(d3.extent(data, function(d) {
          return d.companyReviewDate;
        }))
        .range([0, width]);
      svgObj.append("g")
        .attr("transform", "translate(0," + height + ")")
        .attr("stroke-width", "0.3")
        .style("opacity", "0.5")
        .call(d3.axisBottom(x).tickSize(-height).tickFormat('').ticks(6))

      // ticks
      svgObj.append("g")
        .style("opacity", "0.7")
        .style("font", "14px times")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x).ticks(5));

      // Add Y axis  
      var y = d3.scaleLinear()
        .domain([0, d3.max(data, function(d) {
          return +d.customerExperienceScore;
        })])
        .range([height, 0]);
      svgObj.append("g")
        .attr("stroke-width", "0.3")
        .style("opacity", "0.5")
        .call(d3.axisLeft(y).tickSize(-width).tickFormat('').ticks(5))

      // ticks
      svgObj.append("g")
        .style("opacity", "0.7")
        .style("font", "14px times")
        .call(d3.axisLeft(y).ticks(5));

      // Add X axis label:
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 20)
        .attr("x", width / 2 + margin.left)
        .attr("y", height + 50)
        .style("fill", d3.color("grey"))
        .text("Company Review Date");

      // Add Y axis label:
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 20)
        .attr("transform", "rotate(-90)")
        .attr("x", -height / 2 + 40)
        .attr("y", -margin.left + 25)
        .style("fill", d3.color("grey"))
        .text("Customer Experience Score")

      // color palette
      var key = sumstat.map(function(d) {
        return d.key
      }) // list of group names

      var color = d3.scaleOrdinal()
        .domain(key)
        .range(['#e41a1c', '#377eb8', '#4daf4a'])

      // Add one DOT in the legend for each name.
      svgObj.selectAll(".dots")
        .data(key)
        .enter()
        .append("circle")
        .attr("cx", function(d, i) {
          return 250 + i * 120
        })
        .attr("cy", -30)
        .attr("r", 7)
        .style("fill", function(d) {
          return color(d)
        })

      // Add LABEL for legends of each dot.
      svgObj.selectAll(".labels")
        .data(key)
        .enter()
        .append("text")
        .style("fill", d3.color("grey"))
        .attr("x", function(d, i) {
          return 270 + i * 120
        })
        .attr("y", -28)
        .text(function(d) {
          return d
        })
        .attr("text-anchor", "left")
        .style("alignment-baseline", "middle")


      // Highlight individual line and show tooltip
      var highlightAndShowTooltip = function(d) {

        // this means i am on top of dot circle
        if (d.key == null) {
          console.log("I am on top of a dot circle of key " + d.companyBusinessName)

          //show tooltip
          tooltip.style("visibility", "visible")

          //Data for Tooltip
          tooltip.selectAll(".tooltipName")
            .html(d.key)

          var score = 12 //this will be dynamic, for now i just set it to 12 to test it out

          tooltip.selectAll(".tooltipScore")
            .html("<span style='color: #cb9f9e;'>" + score + "</span> / 100")

          // first every group turns grey
          svgObj.selectAll(".line")
            .transition().duration(200)
            .style("opacity", "0.5")

          svgObj.selectAll(".dot")
            .transition().duration(200)
            .style("opacity", "0.5")

          // Second the hovered line takes its color
          svgObj.selectAll("." + d.companyBusinessName)
            .transition().duration(200)
            .style("stroke", color(d.companyBusinessName))
            .style("opacity", "1")

          svgObj.selectAll("." + d.companyBusinessName)
            .transition().duration(200)
            .style("stroke", color(d.companyBusinessName))
            .style("opacity", "1")
        }

        // this means i am on top of line
        else if (d.companyBusinessName == null) {
          var selected_line = d.key

          console.log("i am on top of line " + d.key)

          // first every group turns grey
          svgObj.selectAll(".line")
            .transition().duration(200)
            .style("opacity", "0.5")

          svgObj.selectAll(".dot")
            .transition().duration(200)
            .style("opacity", "0.5")

          // Second the hovered line takes its color
          svgObj.selectAll("." + selected_line)
            .transition().duration(200)
            .style("stroke", color(selected_line))
            .style("opacity", "1")

          svgObj.selectAll("." + selected_line)
            .transition().duration(200)
            .style("stroke", color(selected_line))
            .style("opacity", "1")

        }
      }

      // UnHighlight and hide tooltip
      var doNotHighlightAndHideTooltip = function(d) {

        //hide tooltip
        tooltip.style("visibility", "hidden")

        //return other lines back to normal opacity
        svgObj.selectAll(".line")
          .transition().duration(200).delay(50)
          .style("stroke", function(d) {
            return (color(d.key))
          })
          .style("opacity", "1")

        svgObj.selectAll(".dot")
          .transition().duration(200).delay(50)
          .style("stroke", function(d) {
            return (color(d.companyBusinessName))
          })
          .style("opacity", "1")
      }

      // keep showing tooltip as cursor moves along line
      var keepShowingTooltip = function(d) {
        tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px")
      }

      // Draw the line
      svgObj.selectAll(".line")
        .data(sumstat)
        .enter()
        .append("path")
        .attr("class", function(d) {
          return "line " + d.key
        }) // 2 class for each line: 'line' and the group name
        .attr("fill", "none")
        .attr("stroke", function(d) {
          return color(d.key)
        })
        .attr("stroke-width", 4.5)
        .attr("d", function(d) {
          return d3.line()
            .curve(d3.curveMonotoneX)
            .x(function(d) {
              return x(d.companyReviewDate);
            })
            .y(function(d) {
              return y(+d.customerExperienceScore);
            })
            (d.values)
        });


      // This is the experiment with voronoi
      // Draw dots on points
      svgObj.selectAll(".dot")
        //i am using the raw data array, NOT the nested array
        .data(dataArrayWithRemovedEmptyDates)
        .enter()
        .append("circle")
        .attr("class", function(d, i) {
          return i;
        })
        .style("fill", "white")
        .style("stroke-width", "3px")
        .style("stroke", function(d) {
          return color(d.companyBusinessName)
        })
        .attr("cx", function(d) {
          return x(d.companyReviewDate);
        })
        .attr("cy", function(d) {
          return y(d.customerExperienceScore);
        })
        .attr("r", 5.5)

      var voronoi = d3.Delaunay
        .from(dataArrayWithRemovedEmptyDates, d => x(d.x), d => y(d.y))
        .voronoi([margin.left, margin.top, width - margin.right, height - margin.bottom]); // ensures voronoi is limited to the

      //Create the voronoi grid
      svgObj.append("g")
        .attr("class", "voronoiWrapper")
        .selectAll("path")
        .data(dataArrayWithRemovedEmptyDates)
        .enter()
        .append("path")
        .attr("opacity", 0.5)
        .attr("stroke", "#ff1493") // Hide overlay
        .attr("fill", "none")
        .style("pointer-events", "all")
        .attr("d", (d, i) => voronoi.renderCell(i))
        .on("mouseover", highlightAndShowTooltip)
        .on("mouseout", doNotHighlightAndHideTooltip);
    })

  }
  render() {
    return <div ref = {
      node => this.node = node
    }
    className = "example_div" > < /div>
  }
}

ReactDOM.render( <Linechart / >, document.querySelector('body'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3-delaunay@5.3.0/dist/d3-delaunay.min.js"></script>

你没有仔细研究你的数据结构:你没有使用 d.xd.y,你有自己的名字:

.from(dataArrayWithRemovedEmptyDates, d => x(d.companyReviewDate), d => y(d.customerExperienceScore))

我还删除了 Voronoi 的边距。原因是 svgObj 对您来说不是 svg,而是带有转换的 g。这意味着您已经正确地应用了边距,所以如果您在此处添加它们,您将应用它们两次。所有这些都会产生以下结果:

class Linechart extends React.Component {
  constructor(props) {
    super(props)
    this.createLineChart = this.createLineChart.bind(this)
  }

  metricToPercent(metric) {
    return (metric / 2 + 0.5) * 100;
  };

  scoreToDescrip(score) {
    if (score >= 0.6) {
      return "Good";
    } else if (score >= 0) {
      return "Average";
    } else {
      return "Poor";
    }
  };

  componentDidMount() {
    this.createLineChart()
  }

  componentDidUpdate() {
    this.createLineChart()
  }

  createLineChart() {
    var margin = {
        top: 85,
        right: 60,
        bottom: 60,
        left: 80
      },
      width = 960 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;

    var node = this.node
    var divObj = d3.select(node)
    var svgObj = divObj
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    //Read the data
    d3.json("https://raw.githubusercontent.com/QamarFarooq/data-for-testing/main/experience_scores.json").then(function(data) {
      var dataArrayWithRemovedEmptyDates = []

      // Remove all elements in the array that dont have companyReviewDate property
      data.forEach(function(d, i) {
        if (d.hasOwnProperty('companyReviewDate')) {
          dataArrayWithRemovedEmptyDates.push(d)
        }
      })

      // Transform `companyReviewDate` into an actual date
      dataArrayWithRemovedEmptyDates.forEach(function(d) {
        d.companyReviewDate = new Date(d.companyReviewDate);
      })

      // Remove all whitespace in CompanyBusinessName, it creates problems 
      data.forEach(function(d, i) {
        d.companyBusinessName = d.companyBusinessName.split(' ').join('');
      })

      // group the data: I want to draw one line per group
      var sumstat = d3.nest()
        .key(function(d) {
          return d.companyBusinessName;
        })
        .entries(dataArrayWithRemovedEmptyDates);

      console.log(sumstat)

      // Define the div for the tooltip
      var tooltip = divObj
        .append("div")
        .attr("class", "tooltip")
        .style("position", "absolute")
        .style("z-index", "10")
        .style("visibility", "hidden")
        .style("background-color", "white")
        .style("box-shadow", "0 0 4px #000000")
        .style("padding", "10px")

      const monthNames = ["January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December"
      ];

      var d = new Date();

      tooltip.append("div")
        .attr("class", "tooltipDate")
        .html(monthNames[d.getMonth()] + " " + "(" + d.getFullYear() + ")")
        .style("font-size", "20px")
        .style("text-align", "center")

      tooltip.append("div")
        .attr("class", "tooltipName")
        .style("text-align", "center")
        .style("color", "grey")

      tooltip.append("div")
        .attr("class", "tooltipTitle")
        .style("text-align", "center")
        .html("Customer Sentiment")
        .style("padding-top", "10px")

      tooltip.append("div")
        .attr("class", "tooltipScore")
        .style("text-align", "center")
        .style("color", 'DarkGrey')
        .style("font-size", "20px")

      tooltip.append("div")
        .attr("class", "tooltipPerception")
        .style("text-align", "center")


      // Add title for linechart
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 25)
        .attr("x", 110)
        .attr("y", -50)
        .text("Customer Experience Score");

      // Add X axis --> it is a date format
      var x = d3.scaleTime()
        .domain(d3.extent(data, function(d) {
          return d.companyReviewDate;
        }))
        .range([0, width]);
      svgObj.append("g")
        .attr("transform", "translate(0," + height + ")")
        .attr("stroke-width", "0.3")
        .style("opacity", "0.5")
        .call(d3.axisBottom(x).tickSize(-height).tickFormat('').ticks(6))

      // ticks
      svgObj.append("g")
        .style("opacity", "0.7")
        .style("font", "14px times")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x).ticks(5));

      // Add Y axis  
      var y = d3.scaleLinear()
        .domain([0, d3.max(data, function(d) {
          return +d.customerExperienceScore;
        })])
        .range([height, 0]);
      svgObj.append("g")
        .attr("stroke-width", "0.3")
        .style("opacity", "0.5")
        .call(d3.axisLeft(y).tickSize(-width).tickFormat('').ticks(5))

      // ticks
      svgObj.append("g")
        .style("opacity", "0.7")
        .style("font", "14px times")
        .call(d3.axisLeft(y).ticks(5));

      // Add X axis label:
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 20)
        .attr("x", width / 2 + margin.left)
        .attr("y", height + 50)
        .style("fill", d3.color("grey"))
        .text("Company Review Date");

      // Add Y axis label:
      svgObj.append("text")
        .attr("text-anchor", "end")
        .attr("font-size", 20)
        .attr("transform", "rotate(-90)")
        .attr("x", -height / 2 + 40)
        .attr("y", -margin.left + 25)
        .style("fill", d3.color("grey"))
        .text("Customer Experience Score")

      // color palette
      var key = sumstat.map(function(d) {
        return d.key
      }) // list of group names

      var color = d3.scaleOrdinal()
        .domain(key)
        .range(['#e41a1c', '#377eb8', '#4daf4a'])

      // Add one DOT in the legend for each name.
      svgObj.selectAll(".dots")
        .data(key)
        .enter()
        .append("circle")
        .attr("cx", function(d, i) {
          return 250 + i * 120
        })
        .attr("cy", -30)
        .attr("r", 7)
        .style("fill", function(d) {
          return color(d)
        })

      // Add LABEL for legends of each dot.
      svgObj.selectAll(".labels")
        .data(key)
        .enter()
        .append("text")
        .style("fill", d3.color("grey"))
        .attr("x", function(d, i) {
          return 270 + i * 120
        })
        .attr("y", -28)
        .text(function(d) {
          return d
        })
        .attr("text-anchor", "left")
        .style("alignment-baseline", "middle")


      // Highlight individual line and show tooltip
      var highlightAndShowTooltip = function(d) {

        // this means i am on top of dot circle
        if (d.key == null) {
          console.log("I am on top of a dot circle of key " + d.companyBusinessName)

          //show tooltip
          tooltip.style("visibility", "visible")

          //Data for Tooltip
          tooltip.selectAll(".tooltipName")
            .html(d.key)

          var score = 12 //this will be dynamic, for now i just set it to 12 to test it out

          tooltip.selectAll(".tooltipScore")
            .html("<span style='color: #cb9f9e;'>" + score + "</span> / 100")

          // first every group turns grey
          svgObj.selectAll(".line")
            .transition().duration(200)
            .style("opacity", "0.5")

          svgObj.selectAll(".dot")
            .transition().duration(200)
            .style("opacity", "0.5")

          // Second the hovered line takes its color
          svgObj.selectAll("." + d.companyBusinessName)
            .transition().duration(200)
            .style("stroke", color(d.companyBusinessName))
            .style("opacity", "1")

          svgObj.selectAll("." + d.companyBusinessName)
            .transition().duration(200)
            .style("stroke", color(d.companyBusinessName))
            .style("opacity", "1")
        }

        // this means i am on top of line
        else if (d.companyBusinessName == null) {
          var selected_line = d.key

          console.log("i am on top of line " + d.key)

          // first every group turns grey
          svgObj.selectAll(".line")
            .transition().duration(200)
            .style("opacity", "0.5")

          svgObj.selectAll(".dot")
            .transition().duration(200)
            .style("opacity", "0.5")

          // Second the hovered line takes its color
          svgObj.selectAll("." + selected_line)
            .transition().duration(200)
            .style("stroke", color(selected_line))
            .style("opacity", "1")

          svgObj.selectAll("." + selected_line)
            .transition().duration(200)
            .style("stroke", color(selected_line))
            .style("opacity", "1")

        }
      }

      // UnHighlight and hide tooltip
      var doNotHighlightAndHideTooltip = function(d) {

        //hide tooltip
        tooltip.style("visibility", "hidden")

        //return other lines back to normal opacity
        svgObj.selectAll(".line")
          .transition().duration(200).delay(50)
          .style("stroke", function(d) {
            return (color(d.key))
          })
          .style("opacity", "1")

        svgObj.selectAll(".dot")
          .transition().duration(200).delay(50)
          .style("stroke", function(d) {
            return (color(d.companyBusinessName))
          })
          .style("opacity", "1")
      }

      // keep showing tooltip as cursor moves along line
      var keepShowingTooltip = function(d) {
        tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px")
      }

      // Draw the line
      svgObj.selectAll(".line")
        .data(sumstat)
        .enter()
        .append("path")
        .attr("class", function(d) {
          return "line " + d.key
        }) // 2 class for each line: 'line' and the group name
        .attr("fill", "none")
        .attr("stroke", function(d) {
          return color(d.key)
        })
        .attr("stroke-width", 4.5)
        .attr("d", function(d) {
          return d3.line()
            .curve(d3.curveMonotoneX)
            .x(function(d) {
              return x(d.companyReviewDate);
            })
            .y(function(d) {
              return y(+d.customerExperienceScore);
            })
            (d.values)
        });


      // This is the experiment with voronoi
      // Draw dots on points
      svgObj.selectAll(".dot")
        //i am using the raw data array, NOT the nested array
        .data(dataArrayWithRemovedEmptyDates)
        .enter()
        .append("circle")
        .attr("class", function(d, i) {
          return i;
        })
        .style("fill", "white")
        .style("stroke-width", "3px")
        .style("stroke", function(d) {
          return color(d.companyBusinessName)
        })
        .attr("cx", function(d) {
          return x(d.companyReviewDate);
        })
        .attr("cy", function(d) {
          return y(d.customerExperienceScore);
        })
        .attr("r", 5.5)

      var voronoi = d3.Delaunay
        .from(dataArrayWithRemovedEmptyDates, d => x(d.companyReviewDate), d => y(d.customerExperienceScore))
        .voronoi([0, 0, width, height]); // ensures voronoi is limited to the

      console.log(voronoi, dataArrayWithRemovedEmptyDates);
      //Create the voronoi grid
      svgObj.append("g")
        .attr("class", "voronoiWrapper")
        .selectAll("path")
        .data(dataArrayWithRemovedEmptyDates)
        .enter()
        .append("path")
        .attr("opacity", 0.5)
        .attr("stroke", "#ff1493") // Hide overlay
        .attr("fill", "none")
        .style("pointer-events", "all")
        .attr("d", (d, i) => {
          return voronoi.renderCell(i);
        })
        .on("mouseover", highlightAndShowTooltip)
        .on("mouseout", doNotHighlightAndHideTooltip);
    })

  }
  render() {
    return <div ref = {
      node => this.node = node
    }
    className = "example_div" > < /div>
  }
}

ReactDOM.render( <Linechart / >, document.querySelector('body'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3-delaunay@5.3.0/dist/d3-delaunay.min.js"></script>