D3 鼠标交互问题,圆圈未出现在数据点

D3 mouse interactivity issues, circles not appearing at data points

我正在以 class 格式实现一些 D3 代码,以便我有一个可重复使用的图表。

报错如下:

GetElementsByClassName returns 一个长度为 0 的 HTMLCollection,但要选择的元素已 class 正确编辑。

Circles 出现在 x0 和 yMax,而不是数据位置(问题可能与第一个有关)。

Text 未附加到圆圈且不可见(这可能在圆圈起作用时有效)。

我正在实施 this pretty much exactly as it looks,除了我将工具提示放在 2/4 的行上,并且我正在使用 class.

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="graph"></div>

  <script>
    class Chart {
      constructor(opts) {
        this.data = opts.data;
        this.element = opts.element;
      }

      draw() {
        this.width = this.element.offsetWidth;
        this.height = this.width / 2;
        this.padding = 50;
        this.margin = {
          top: 20,
          bottom: 20,
          left: 30,
          right: 50
        };

        this.element.innerHTML = '';
        const svg = d3.select(this.element).append('svg');
        svg.attr('width', this.width);
        svg.attr('height', this.height);

        this.plot = svg.append('g')
            .attr('transform', `translate(${this.margin.left},${this.margin.top})`);

        this.createScales();
        this.addAxes();
        this.addLine();
        this.tTip();
      }

      createScales() {
        this.keynames = d3.scaleOrdinal();

        this.keynames.domain(Object.keys(this.data[0]).filter(key => key !== 'date'));

        this.keymap = this.keynames.domain().map(
          keyname => ({
            name: keyname, values: this.data.map(
              d => ({ date: d.date, key: +d[keyname] })
            )
          })
        );

        const m = this.margin;

        const xExtent = d3.extent(this.data, d => d.date);

        const yExtent = [0, d3.max(this.keymap, d => d3.max(d.values, function (v) { return v.key }))];

        this.xScale = d3.scaleTime()
            .range([0, this.width - m.right])
            .domain(xExtent).nice();

        this.yScale = d3.scaleLinear()
            .range([this.height - (m.top + m.bottom), 0])
            .domain(yExtent).nice();
      }

      addAxes() {
        const m = this.margin;

        const xAxis = d3.axisBottom()
          .scale(this.xScale);

        const yAxis = d3.axisLeft()
          .scale(this.yScale);

        this.plot.append("g")
            .attr("class", "x axis")
            .attr("transform", `translate(0, ${this.height - (m.top + m.bottom)})`)
            .call(xAxis.ticks(8));

        this.plot.append("g")
            .attr("class", "y axis")
            .call(yAxis.ticks(4))
          .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .attr("fill", "black")
            .style("text-anchor", "end")
            .text("$USD");
      }

      addLine() {
        const line = d3.line()
          .x(d => this.xScale(d.date))
          .y(d => this.yScale(d.key));

        this.plot.append('g')
          .selectAll('path')
          .data(this.keymap)
          .join('path')
            .classed('line', true)
            .attr('d', function (d) { return line(d.values) })
            .style('stroke', this.lineColor || 'red')
            .style('fill', 'none');
      }
      
      tTip(){
        let mouseG = this.plot.append("g")
      .attr("class", "mouse-over-effects");

    mouseG.append("path") 
      .attr("class", "mouse-line")
      .style("stroke", "rgba(50,50,50,1)")
      .style("stroke-width", "0.5px")
      .style("opacity", "0");

    var lines = document.getElementsByClassName('.standard'); //issue here

    let mousePerLine = mouseG.selectAll('.mouse-per-line')
      .data(this.keymap)
      .enter()
      .append("g") //join instead of append?
      .attr("class", "mouse-per-line");

    mousePerLine.append("circle") //join instead of append?
      .attr("r", 4)
      .style("stroke", "black"
    )
      .style("fill", "blue"
    )
      .style("fill-opacity", "0.3")
      .style("stroke-width", "1px")
      .style("opacity", "0");

    mousePerLine.append("text") //join instead of append?
      .attr("transform", function(d){
        if (d.name == 'aapl') {
          return "translate(-50,30)"
        } else {
          return "translate(-50, -30)"
        }
      }).style("text-shadow",
      " -2px -2px 0 #FFF, 0   -2px 0 #FFF, 2px -2px 0 #FFF, 2px  0   0 #FFF, 2px  2px 0 #FFF, 0    2px 0 #FFF,-2px  2px 0 #FFF,-2px  0   0 #FFF");

    mouseG.append('svg:rect') 
      .attr('width', this.width) 
      .attr('height', this.height)
      .attr('x', '0')
      .attr('fill', 'none')
      .attr('pointer-events', 'all')
      .on('mouseout', function() { 
        d3.select(".mouse-line")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "0");
      })
      .on('mouseover', function() { 
        d3.select(".mouse-line")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "1");
      })
      .on('mousemove', () => { 
        let mouse = d3.pointer(event);
        d3.select(".mouse-line")
          .attr("d", () => {
            let d = "M" + mouse[0] + "," + this.height;
            d += " " + mouse[0] + "," + 0;
            return d;
          });

        d3.selectAll(".mouse-per-line")
          .attr("transform", (d, i) => {
            let xDate = this.xScale.invert(mouse[0]),
                bisect = d3.bisector(d => d.date).right,
                idx = bisect(d.values, xDate);

            let beginning = 0,
                end = lines[i].getTotalLength(),
                target = null;
            while (true){
              let target = Math.floor((beginning + end) / 2),
                  pos = lines[i].getPointAtLength(target); //issue here
              if ((target === end || target === beginning) && pos.x !== mouse[0]) {
                  break;
              }
              if (pos.x > mouse[0]){
                end = target;
              }
              else if (pos.x < mouse[0]){
                beginning = target;
              }
              else break; //position found
            }

            d3.select(this).select('text')
              .text( () => { "$" +                        this.yScale.invert(pos.y).toFixed(2)})
            return "translate(" + mouse[0] + "," + pos.y +")";
          })
          .style('font-family', 'Helvetica')
          .style('font-size', '11px')
          .style('letter-spacing', '1px')
          .style('text-transform', 'uppercase');
        });
      }

      setColor(newColor) {
        this.plot.select('.line')
          .style('stroke', newColor);

        this.lineColor = newColor;
      }

      setData(data) {
        this.data = data;

        this.draw();
      }
    }

    const chart = new Chart({ element: document.querySelector('#graph') });

    const data = d3.csvParse(`Date,AAPL,SMA_AAPL,TSLA,SMA_TSLA
2018-12-31,38.33848571777344,,66.55999755859375,
2019-01-02,38.382225036621094,,62.02399826049805,
2019-01-03,34.55907440185547,,60.071998596191406,
2019-01-04,36.03437805175781,,63.53799819946289,
2019-01-07,35.95417022705078,,66.99199676513672,
2019-01-08,36.63956832885742,,67.06999969482422,
2019-01-09,37.26177215576172,,67.70600128173828,
2019-01-10,37.380863189697266,,68.99400329589844,
2019-01-11,37.013858795166016,,69.4520034790039,
2019-01-14,36.4572868347168,,66.87999725341797,
2019-01-15,37.20343780517578,,68.88600158691406,
2019-01-16,37.657936096191406,,69.20999908447266,
2019-01-17,37.88154602050781,,69.46199798583984,
2019-01-18,38.11487579345703,,60.45199966430664,
2019-01-22,37.259342193603516,,59.784000396728516,
2019-01-23,37.410030364990234,,57.518001556396484,
2019-01-24,37.113521575927734,,58.301998138427734,
2019-01-25,38.34333801269531,,59.40800094604492,
2019-01-28,37.988487243652344,,59.2760009765625,
2019-01-29,37.59474182128906,,59.492000579833984,
2019-01-30,40.16377258300781,,61.75400161743164,
2019-01-31,40.453006744384766,,61.40399932861328,
2019-02-01,40.472450256347656,,62.44200134277344,
2019-02-04,41.622066497802734,,62.577999114990234,
2019-02-05,42.33420181274414,,64.2699966430664,
2019-02-06,42.34878158569336,,63.444000244140625,
2019-02-07,41.546722412109375,,61.50199890136719,
2019-02-08,41.59553909301758,,61.15999984741211,
2019-02-11,41.35633087158203,,62.56800079345703,
2019-02-12,41.71269989013672,38.606483713785806,62.36199951171875,63.48539975484212
2019-02-13,41.539398193359375,38.71318079630534,61.63399887084961,63.32119979858398
2019-02-14,41.69073486328125,38.823464457194014,60.75400161743164,63.278866577148435
2019-02-15,41.59797286987305,39.05809440612793,61.57600021362305,63.32899996439616
2019-02-19,41.72246551513672,39.247697321573895,61.12799835205078,63.24866663614909
2019-02-20,41.990962982177734,39.44892374674479,60.512001037597656,63.032666778564455
2019-02-21,41.75419616699219,39.619411341349284,58.24599838256836,62.738533401489256
2019-02-22,42.22041702270508,39.78469950358073,58.94200134277344,62.44640007019043
2019-02-25,42.5279655456543,39.95626958211263,59.75400161743164,62.13840001424153`, function (d) {
      function removeNaN(e, c) {
        if (e > 0) { return e; } else { return c; }
      }
      return {
        date: d3.timeParse("%Y-%m-%d")(d.Date),
        aapl: +d.AAPL,
        tsla: +d.TSLA,
        aapl_sma: removeNaN(+d.SMA_AAPL, +d.AAPL),
        tsla_sma: removeNaN(+d.SMA_TSLA, +d.TSLA)
      };
    });

    chart.setData(data);
  </script>
</body>

</html>

如您所见,鼠标交互非常糟糕,所以我希望有人能提供帮助。

箭头函数和正则函数之间存在一些
修复了一些错误:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="graph"></div>

  <script>
    let pos = null;
    class Chart {
      constructor(opts) {
        this.data = opts.data;
        this.element = opts.element;
      }

      draw() {
        this.width = this.element.offsetWidth;
        this.height = this.width / 2;
        this.padding = 50;
        this.margin = {
          top: 20,
          bottom: 20,
          left: 30,
          right: 50
        };

        this.element.innerHTML = '';
        const svg = d3.select(this.element).append('svg');
        svg.attr('width', this.width);
        svg.attr('height', this.height);

        this.plot = svg.append('g')
          .attr('transform', `translate(${this.margin.left},${this.margin.top})`);

        this.createScales();
        this.addAxes();
        this.addLine();
        this.tTip();
      }

      createScales() {
        this.keynames = d3.scaleOrdinal();

        this.keynames.domain(Object.keys(this.data[0]).filter(key => key !== 'date'));

        this.keymap = this.keynames.domain().map(
          keyname => ({
            name: keyname, values: this.data.map(
              d => ({ date: d.date, key: +d[keyname] })
            )
          })
        );

        const m = this.margin;

        const xExtent = d3.extent(this.data, d => d.date);

        const yExtent = [0, d3.max(this.keymap, d => d3.max(d.values, function (v) { return v.key }))];

        this.xScale = d3.scaleTime()
          .range([0, this.width - m.right])
          .domain(xExtent).nice();

        this.yScale = d3.scaleLinear()
          .range([this.height - (m.top + m.bottom), 0])
          .domain(yExtent).nice();
      }

      addAxes() {
        const m = this.margin;

        const xAxis = d3.axisBottom()
          .scale(this.xScale);

        const yAxis = d3.axisLeft()
          .scale(this.yScale);

        this.plot.append("g")
          .attr("class", "x axis")
          .attr("transform", `translate(0, ${this.height - (m.top + m.bottom)})`)
          .call(xAxis.ticks(8));

        this.plot.append("g")
          .attr("class", "y axis")
          .call(yAxis.ticks(4))
          .append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", 6)
          .attr("dy", ".71em")
          .attr("fill", "black")
          .style("text-anchor", "end")
          .text("$USD");
      }

      addLine() {
        const line = d3.line()
          .x(d => this.xScale(d.date))
          .y(d => this.yScale(d.key));

        this.plot.append('g')
          .selectAll('path')
          .data(this.keymap)
          .join('path')
          .classed('line', true)
          .attr('d', function (d) { return line(d.values) })
          .style('stroke', this.lineColor || 'red')
          .style('fill', 'none');
      }

      tTip() {
        let mouseG = this.plot.append("g")
          .attr("class", "mouse-over-effects");

        mouseG.append("path")
          .attr("class", "mouse-line")
          .style("stroke", "rgba(50,50,50,1)")
          .style("stroke-width", "0.5px")
          .style("opacity", "0");

        var lines = document.getElementsByClassName('line'); //issue here

        let mousePerLine = mouseG.selectAll('.mouse-per-line')
          .data(this.keymap)
          .enter()
          .append("g") //join instead of append?
          .attr("class", "mouse-per-line");

        mousePerLine.append("circle") //join instead of append?
          .attr("r", 4)
          .style("stroke", "black"
          )
          .style("fill", "blue"
          )
          .style("fill-opacity", "0.3")
          .style("stroke-width", "1px")
          .style("opacity", "0");

        mousePerLine.append("text") //join instead of append?
          .attr("transform", function (d) {
            if (d.name == 'aapl') {
              return "translate(-50,30)"
            } else {
              return "translate(-50, -30)"
            }
          }).style("text-shadow",
            " -2px -2px 0 #FFF, 0   -2px 0 #FFF, 2px -2px 0 #FFF, 2px  0   0 #FFF, 2px  2px 0 #FFF, 0    2px 0 #FFF,-2px  2px 0 #FFF,-2px  0   0 #FFF");

        mouseG.append('svg:rect')
          .attr('width', this.width)
          .attr('height', this.height)
          .attr('x', '0')
          .attr('fill', 'none')
          .attr('pointer-events', 'all')
          .on('mouseout', function () {
            d3.select(".mouse-line")
              .style("opacity", "0");
            d3.selectAll(".mouse-per-line circle")
              .style("opacity", "0");
            d3.selectAll(".mouse-per-line text")
              .style("opacity", "0");
          })
          .on('mouseover', function () {
            d3.select(".mouse-line")
              .style("opacity", "1");
            d3.selectAll(".mouse-per-line circle")
              .style("opacity", "1");
            d3.selectAll(".mouse-per-line text")
              .style("opacity", "1");
          })
          .on('mousemove', () => {
            let mouse = d3.pointer(event);
            d3.select(".mouse-line")
              .attr("d", () => {
                let d = "M" + mouse[0] + "," + this.height;
                d += " " + mouse[0] + "," + 0;
                return d;
              });

            d3.selectAll(".mouse-per-line")
              .attr("transform", function (d, i) {
                let xDate = chart.xScale.invert(mouse[0]),
                  bisect = d3.bisector(d => d.date).right,
                  idx = bisect(d.values, xDate);

                let beginning = 0,
                  end = lines[i].getTotalLength(),
                  target = null;
                while (true) {
                  let target = Math.floor((beginning + end) / 2);
                  pos = lines[i].getPointAtLength(target); //issue here
                  if ((target === end || target === beginning) && pos.x !== mouse[0]) {
                    break;
                  }
                  if (pos.x > mouse[0]) {
                    end = target;
                  }
                  else if (pos.x < mouse[0]) {
                    beginning = target;
                  }
                  else break; //position found
                }

                d3.select(this).select('text')
                  .text("$" + chart.yScale.invert(pos.y).toFixed(2))
                return "translate(" + mouse[0] + "," + pos.y + ")";
              })
              .style('font-family', 'Helvetica')
              .style('font-size', '11px')
              .style('letter-spacing', '1px')
              .style('text-transform', 'uppercase');
          });
      }

      setColor(newColor) {
        this.plot.select('.line')
          .style('stroke', newColor);

        this.lineColor = newColor;
      }

      setData(data) {
        this.data = data;

        this.draw();
      }
    }

    const chart = new Chart({ element: document.querySelector('#graph') });

    const data = d3.csvParse(`Date,AAPL,SMA_AAPL,TSLA,SMA_TSLA
2018-12-31,38.33848571777344,,66.55999755859375,
2019-01-02,38.382225036621094,,62.02399826049805,
2019-01-03,34.55907440185547,,60.071998596191406,
2019-01-04,36.03437805175781,,63.53799819946289,
2019-01-07,35.95417022705078,,66.99199676513672,
2019-01-08,36.63956832885742,,67.06999969482422,
2019-01-09,37.26177215576172,,67.70600128173828,
2019-01-10,37.380863189697266,,68.99400329589844,
2019-01-11,37.013858795166016,,69.4520034790039,
2019-01-14,36.4572868347168,,66.87999725341797,
2019-01-15,37.20343780517578,,68.88600158691406,
2019-01-16,37.657936096191406,,69.20999908447266,
2019-01-17,37.88154602050781,,69.46199798583984,
2019-01-18,38.11487579345703,,60.45199966430664,
2019-01-22,37.259342193603516,,59.784000396728516,
2019-01-23,37.410030364990234,,57.518001556396484,
2019-01-24,37.113521575927734,,58.301998138427734,
2019-01-25,38.34333801269531,,59.40800094604492,
2019-01-28,37.988487243652344,,59.2760009765625,
2019-01-29,37.59474182128906,,59.492000579833984,
2019-01-30,40.16377258300781,,61.75400161743164,
2019-01-31,40.453006744384766,,61.40399932861328,
2019-02-01,40.472450256347656,,62.44200134277344,
2019-02-04,41.622066497802734,,62.577999114990234,
2019-02-05,42.33420181274414,,64.2699966430664,
2019-02-06,42.34878158569336,,63.444000244140625,
2019-02-07,41.546722412109375,,61.50199890136719,
2019-02-08,41.59553909301758,,61.15999984741211,
2019-02-11,41.35633087158203,,62.56800079345703,
2019-02-12,41.71269989013672,38.606483713785806,62.36199951171875,63.48539975484212
2019-02-13,41.539398193359375,38.71318079630534,61.63399887084961,63.32119979858398
2019-02-14,41.69073486328125,38.823464457194014,60.75400161743164,63.278866577148435
2019-02-15,41.59797286987305,39.05809440612793,61.57600021362305,63.32899996439616
2019-02-19,41.72246551513672,39.247697321573895,61.12799835205078,63.24866663614909
2019-02-20,41.990962982177734,39.44892374674479,60.512001037597656,63.032666778564455
2019-02-21,41.75419616699219,39.619411341349284,58.24599838256836,62.738533401489256
2019-02-22,42.22041702270508,39.78469950358073,58.94200134277344,62.44640007019043
2019-02-25,42.5279655456543,39.95626958211263,59.75400161743164,62.13840001424153`, function (d) {
      function removeNaN(e, c) {
        if (e > 0) { return e; } else { return c; }
      }
      return {
        date: d3.timeParse("%Y-%m-%d")(d.Date),
        aapl: +d.AAPL,
        tsla: +d.TSLA,
        aapl_sma: removeNaN(+d.SMA_AAPL, +d.AAPL),
        tsla_sma: removeNaN(+d.SMA_TSLA, +d.TSLA)
      };
    });

    chart.setData(data);
  </script>
</body>

</html>

我没有列出所有这些,而是​​创建了一个 diff 文件。 Download 并检查。左边是原代码。