Skip to content
Advertisement

D3 mouse interactivity issues, circles not appearing at data points

I’m implementing some D3 code in class format so that I have a reusable chart.

The errors are as follows:

GetElementsByClassName returns an HTMLCollection of length 0, but elements to be selected are classed correctly.

Circles appear at x0 and yMax, rather than at data positions (Issue is probably connected to the first).

Text is not appended to circle and not visible (This might work when the circles work).

I’m implementing this pretty much exactly as it looks, with the exception that I’m putting tooltips on 2/4 of the lines, and I’m using a 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>

As you can see the mouse interactions are pretty janky so I hope someone can help.

Advertisement

Answer

There are some differences between arrow function and regular function.
Fixed some more errors:

<!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>

Instead of listing all of them, I’ve created a diff file. Download and check. Left side is the original code.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement