Yukino Kondo
Yukino Kondo

Reputation: 93

Update multi-line graph with D3

I am trying to play around with D3 and do some visualization using COVID-19 data, but I cannot seem to figure out how to update the multi-line graph with a button. This is what I currently have:

<!DOCTYPE html>
<meta charset="utf-8" />
<html lang="en">
<style>
  .line1 {
    fill: none;
    stroke: darkcyan;
    stroke-width: 2.5px;
  }
  
  .line2 {
    fill: none;
    stroke: red;
    stroke-width: 2.5px;
  }
  
  .line3 {
    fill: none;
    stroke: green;
    stroke-width: 2.5px;
  }
  
  .axis path,
  .axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
  }
  
  .grid line {
    stroke: lightgrey;
    stroke-opacity: 0.7;
    shape-rendering: crispEdges;
  }
  
  .grid path {
    stroke-width: 0;
  }
  
  .legend rect {
    fill: white;
    stroke: black;
    opacity: 0.8;
  }
</style>

<head>
  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body>
  <h1>Total Confirmed Coronavirus Cases in Germany</h1>

  <div id="option">
    <input name="updateButton" type="button" value="Linear" onclick="linear()" />
    <input name="updateButton" type="button" value="Logarithmic" onclick="logarithmic()" />
  </div>

  <script>
    // write your d3 code here..

    var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = 1000 - margin.left - margin.right,
      height = 600 - margin.top - margin.bottom;

    // parse the date / time
    var parseTime = d3.utcParse("%Y-%m-%dT%H:%M:%S%Z");
    var formatDate = d3.timeFormat("%m-%d");

    // set the ranges
    var x = d3.scaleUtc().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);
    var logy = d3.scaleLog().range([height, 0]);

    // Define the axes
    var xAxis = d3.axisBottom(x);
    var yAxis = d3.axisLeft(y);
    var logyAxis = d3.axisLeft(logy);

    // define the 1st line
    var valueline1 = d3
      .line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.confirmed);
      });

    // define the 2nd line
    var valueline2 = d3.line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.deaths);
      });

    // define the 3rd line
    var valueline3 = d3.line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.recovered);
      });


    // append the svg obgect to the body of the page
    // appends a 'group' element to 'svg'
    // moves the 'group' element to the top left margin
    var svg = d3
      .select("body")
      .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 + ")");

    // gridlines in x axis function
    function x_gridlines() {
      return d3.axisBottom(x)
    }

    // gridlines in y axis function
    function y_gridlines() {
      return d3.axisLeft(y)
    }

    // gridlines in y axis function
    function logy_gridlines() {
      return d3.axisLeft(logy)
    }

    d3.json(
      "https://api.covid19api.com/total/dayone/country/germany"
    ).then(function(data) {

      data.forEach(function(d) {
        d.country = d.Country
        d.date = parseTime(d.Date);
        d.confirmed = d.Confirmed;
        d.deaths = d.Deaths;
        d.recovered = d.Recovered;
      });

      // Scale the range of the data
      x.domain(d3.extent(data, function(d) {
        return d.date;
      }));
      y.domain([0, d3.max(data, function(d) {
        return Math.max(d.confirmed, d.deaths, d.recovered);
      })]);

      // Add the valueline path.
      svg.append("path")
        .data([data])
        .attr("class", "line1")
        .attr("d", valueline1);

      // Add the valueline2 path.
      svg.append("path")
        .data([data])
        .attr("class", "line2")
        .attr("d", valueline2);

      // Add the valueline3 path.
      svg.append("path")
        .data([data])
        .attr("class", "line3")
        .attr("d", valueline3);

      // Add the X Axis
      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

      // Add the Y Axis
      svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);

      // add the X gridlines
      svg.append("g")
        .attr("class", "x grid")
        .attr("transform", "translate(0," + height + ")")
        .call(x_gridlines()
          .tickSize(-height)
          .tickFormat("")
        )

      // add the Y gridlines
      svg.append("g")
        .attr("class", "y grid")
        .call(y_gridlines()
          .tickSize(-width)
          .tickFormat("")
        )

      legend = svg.append("g")
        .attr("class", "legend")
        .attr("transform", "translate(50,30)")
        .style("font-size", "12px")
        .call(d3.legend)
    });

    // ** Update data section (Called from the onclick)
    function logarithmic() {

      // Get the data again
      d3.json(
        "https://api.covid19api.com/total/dayone/country/germany"
      ).then(function(data) {

        data.forEach(function(d) {
          d.country = d.Country
          d.date = parseTime(d.Date);
          d.confirmedlog = Math.log(d.Confirmed);
          d.deathslog = Math.log(d.Deaths);
          d.recoveredlog = Math.log(d.Recovered);
        });

        // Scale the range of the data again
        x.domain(d3.extent(data, function(d) {
          return d.date;
        }));

        y.domain([0,
          d3.max(data, function(d) {
            return Math.max(d.confirmedlog, d.deathslog, d.recoveredlog);
          })
        ]);

        // Select the section we want to apply our changes to
        var svg = d3.select("body").data(data).transition();

        // Make the changes
        svg.select(".line1") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1(d.confirmedlog);
          })

        svg.select(".line2") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1(d.deathslog);
          });

        svg.select(".line3") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1(d.recoveredlog);
          });

        svg.select(".y.axis") // change the y axis
          .duration(750)
          .call(logyAxis);
        svg.select(".y.grid")
          .duration(750)
          .call(logy_gridlines()
            .tickSize(-width)
            .tickFormat(""))
      });
    }

    // ** Update data section (Called from the onclick)
    function linear() {

      // Get the data again
      d3.json(
        "https://api.covid19api.com/total/dayone/country/germany"
      ).then(function(data) {

        data.forEach(function(d) {
          d.country = d.Country
          d.date = parseTime(d.Date);
          d.confirmed = d.Confirmed;
          d.deaths = d.Deaths;
          d.recovered = d.Recovered;
        });

        // Scale the range of the data again 
        x.domain(d3.extent(data, function(d) {
          return d.date;
        }));
        y.domain([0, d3.max(data, function(d) {
          return d.confirmed, d.deaths, d.recovered;
        })]);

        // Select the section we want to apply our changes to
        var svg = d3.select("body").transition();

        // Make the changes
        svg.select(".line1") // change the line
          .duration(750)
          .attr("d", valueline1(data));

        svg.select(".line2") // change the line
          .duration(750)
          .attr("d", valueline2(data));

        svg.select(".line3") // change the line
          .duration(750)
          .attr("d", valueline3(data));

        svg.select(".x.axis") // change the x axis
          .duration(750)
          .call(xAxis);
        svg.select(".y.axis") // change the y axis
          .duration(750)
          .call(yAxis);
        svg.select(".y.grid") // change the y gridlines
          .duration(750)
          .call(y_gridlines()
            .tickSize(-width)
            .tickFormat("")
          );

      });
    }
  </script>
</body>

</html>

What I'm having issues with is toggling from the default to the Logarithmic view. Whenever I click on the button to toggle, the lines all disappear. I have a function called: logarithmic() and the code within the function is as below:

function logarithmic() {

            // Get the data again
            d3.json(
                "https://api.covid19api.com/total/dayone/country/germany"
            ).then(function (data) {

                data.forEach(function (d) {
                    d.country = d.Country
                    d.date = parseTime(d.Date);
                    d.confirmedlog = Math.log(d.Confirmed);
                    d.deathslog = Math.log(d.Deaths);
                    d.recoveredlog = Math.log(d.Recovered);
                });

                // Scale the range of the data again
                x.domain(d3.extent(data, function (d) { return d.date; }));

                y.domain([0,
                d3.max(data, function (d) {return Math.max(d.confirmedlog, d.deathslog, d.recoveredlog);
                })]);

                // Select the section we want to apply our changes to
                var svg = d3.select("body").data(data).transition();

                // Make the changes
                svg.select(".line1")   // change the line
                    .duration(750)
                    .attr("d", function(d) { return valueline1(d.confirmedlog); })

                svg.select(".line2")// change the line
                    .duration(750)
                    .attr("d", function(d) { return valueline1(d.deathslog); });

                svg.select(".line3")   // change the line
                    .duration(750)
                    .attr("d", function(d) { return valueline1(d.recoveredlog); });

                svg.select(".y.axis") // change the y axis
                    .duration(750)
                    .call(logyAxis);
                svg.select(".y.grid")
                    .duration(750)
                    .call(logy_gridlines()
                        .tickSize(-width)
                        .tickFormat(""))
            });
}

Any help would be greatly appreciated! Or if there are any best practices for updating multi-line graphs, any tips would be greatly appreciated as well. Thank you very much

Upvotes: 2

Views: 97

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

Well, unfortunately your code has several issues and right now it's far from the best D3 (or JavaScript) practices.

Besides binding data to the body and reassigning selection names, among other things, the very first issue that caught my eye was downloading the (same) data again for the updates. That's completely unnecessary.

In this very quick refactor I'm simply calculating the log values already. Pay attention to the fact that logarithm of zero in JS is minus infinity. So, just check it:

d.confirmedlog = d.Confirmed ? Math.log(d.Confirmed) : 0;
d.deathslog = d.Deaths ? Math.log(d.Deaths) : 0;
d.recoveredlog = d.Recovered ? Math.log(d.Recovered) : 0;

Then, for each line, change the y method of the line generator. For instance:

svg.select(".line1") // change the line
    .duration(750)
    .attr("d", function(d) {
        return valueline1.y(function(e) {
            return y(e.confirmedlog);
        })(d);
    })

Here is the code:

<!DOCTYPE html>
<meta charset="utf-8" />
<html lang="en">
<style>
  .line1 {
    fill: none;
    stroke: darkcyan;
    stroke-width: 2.5px;
  }
  
  .line2 {
    fill: none;
    stroke: red;
    stroke-width: 2.5px;
  }
  
  .line3 {
    fill: none;
    stroke: green;
    stroke-width: 2.5px;
  }
  
  .axis path,
  .axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
  }
  
  .grid line {
    stroke: lightgrey;
    stroke-opacity: 0.7;
    shape-rendering: crispEdges;
  }
  
  .grid path {
    stroke-width: 0;
  }
  
  .legend rect {
    fill: white;
    stroke: black;
    opacity: 0.8;
  }
</style>

<head>
  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body>
  <h1>Total Confirmed Coronavirus Cases in Germany</h1>

  <div id="option">
    <input name="updateButton" type="button" id="option1" value="Linear" />
    <input name="updateButton" type="button" id="option2" value="Logarithmic" />
  </div>

  <script>
    // write your d3 code here..

    var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = 1000 - margin.left - margin.right,
      height = 600 - margin.top - margin.bottom;

    // parse the date / time
    var parseTime = d3.utcParse("%Y-%m-%dT%H:%M:%S%Z");
    var formatDate = d3.timeFormat("%m-%d");

    // set the ranges
    var x = d3.scaleUtc().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);
    var logy = d3.scaleLog().range([height, 0]);

    // Define the axes
    var xAxis = d3.axisBottom(x);
    var yAxis = d3.axisLeft(y);
    var logyAxis = d3.axisLeft(logy);

    // define the 1st line
    var valueline1 = d3
      .line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.confirmed);
      });

    // define the 2nd line
    var valueline2 = d3.line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.deaths);
      });

    // define the 3rd line
    var valueline3 = d3.line()
      .x(function(d) {
        return x(d.date);
      })
      .y(function(d) {
        return y(d.recovered);
      });


    // append the svg obgect to the body of the page
    // appends a 'group' element to 'svg'
    // moves the 'group' element to the top left margin
    var svg = d3
      .select("body")
      .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 + ")");

    // gridlines in x axis function
    function x_gridlines() {
      return d3.axisBottom(x)
    }

    // gridlines in y axis function
    function y_gridlines() {
      return d3.axisLeft(y)
    }

    // gridlines in y axis function
    function logy_gridlines() {
      return d3.axisLeft(logy)
    }

    d3.json(
      "https://api.covid19api.com/total/dayone/country/germany"
    ).then(function(data) {

      data.forEach(function(d) {
        d.country = d.Country
        d.date = parseTime(d.Date);
        d.confirmed = d.Confirmed;
        d.deaths = d.Deaths;
        d.recovered = d.Recovered;
        d.confirmedlog = d.Confirmed ? Math.log(d.Confirmed) : 0;
        d.deathslog = d.Deaths ? Math.log(d.Deaths) : 0;
        d.recoveredlog = d.Recovered ? Math.log(d.Recovered) : 0;
      });

      // Scale the range of the data
      x.domain(d3.extent(data, function(d) {
        return d.date;
      }));
      y.domain([0, d3.max(data, function(d) {
        return Math.max(d.confirmed, d.deaths, d.recovered);
      })]);

      // Add the valueline path.
      svg.append("path")
        .data([data])
        .attr("class", "line1")
        .attr("d", valueline1);

      // Add the valueline2 path.
      svg.append("path")
        .data([data])
        .attr("class", "line2")
        .attr("d", valueline2);

      // Add the valueline3 path.
      svg.append("path")
        .data([data])
        .attr("class", "line3")
        .attr("d", valueline3);

      // Add the X Axis
      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

      // Add the Y Axis
      svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);

      // add the X gridlines
      svg.append("g")
        .attr("class", "x grid")
        .attr("transform", "translate(0," + height + ")")
        .call(x_gridlines()
          .tickSize(-height)
          .tickFormat("")
        )

      // add the Y gridlines
      svg.append("g")
        .attr("class", "y grid")
        .call(y_gridlines()
          .tickSize(-width)
          .tickFormat("")
        )

      d3.select("#option1").on("click", linear);
      d3.select("#option2").on("click", logarithmic);

      // ** Update data section (Called from the onclick)
      function logarithmic() {
        y.domain([0,
          d3.max(data, function(d) {
            return Math.max(d.confirmedlog, d.deathslog, d.recoveredlog);
          })
        ]);

        // Select the section we want to apply our changes to
        var svg = d3.select("body").transition();

        // Make the changes
        svg.select(".line1") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1.y(function(e) {
              return y(e.confirmedlog);
            })(d);
          })

        svg.select(".line2") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1.y(function(e) {
              return y(e.deathslog);
            })(d);
          });

        svg.select(".line3") // change the line
          .duration(750)
          .attr("d", function(d) {
            return valueline1.y(function(e) {
              return y(e.recoveredlog);
            })(d);
          });

        svg.select(".y.axis") // change the y axis
          .duration(750)
          .call(logyAxis);
        svg.select(".y.grid")
          .duration(750)
          .call(logy_gridlines()
            .tickSize(-width)
            .tickFormat(""))
      }

      // ** Update data section (Called from the onclick)
      function linear() {

        y.domain([0, d3.max(data, function(d) {
          return d.confirmed, d.deaths, d.recovered;
        })]);

        // Select the section we want to apply our changes to
        var svg = d3.select("body").transition();

        // Make the changes
        svg.select(".line1") // change the line
          .duration(750)
          .attr("d", valueline1);

        svg.select(".line2") // change the line
          .duration(750)
          .attr("d", valueline2);

        svg.select(".line3") // change the line
          .duration(750)
          .attr("d", valueline3);

        svg.select(".x.axis") // change the x axis
          .duration(750)
          .call(xAxis);
        svg.select(".y.axis") // change the y axis
          .duration(750)
          .call(yAxis);
        svg.select(".y.grid") // change the y gridlines
          .duration(750)
          .call(y_gridlines()
            .tickSize(-width)
            .tickFormat("")
          );
      };
    });
  </script>
</body>

</html>

However, I'm afraid that your code has so many issues that it should be completely refactored (please don't take it personally). Now that it's a (kind of) working code I believe you can ask for help on Code Review. But, if you do, make sure before posting that you read their help section, since Code Review is quite different from Stack Overflow.

Upvotes: 1

Related Questions