9
\$\begingroup\$

I created two graphs in d3js: a bar chart and a donut chart. These two graphs use the same json object for data and share an input range and the radio button.

This is the code:

<html lang='en'>
    <head>
        <meta charset='utf-8'>

        <style>
            .bar {
                fill: steelblue;
            }
            .bar:hover {
                fill: brown;
            }

            .arc text {
              font: 10px sans-serif;
              text-anchor: middle;
            }

            .arc path {
              stroke: #fff;
            }
        </style>

        <title>Donut & Bar charts</title>
        <script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
        <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
    </head>

    <body>

        <div id='year-selector-container'>
            <input id='year-slider' class='year-range' type='range' min='2000' max='2005' value='2004'>
            Year: <span id='year-span'>2004</span>
        </div>

        <div id='type-selector-container'>
            <form id='type-selector'>
                <input type='radio' name='type-selector' id='rb-1' value='type1' checked />
                <label for='rb-1'>Type 1</label>

                <input type='radio' name='type-selector' id='rb-2' value='type2' />
                <label for='rb-2'>Type 2</label>

                <input type='radio' name='type-selector' id='rb-3' value='type3' />
                <label for='rb-3'>Type 3</label>
            </form>
        </div>

        <div id='barchart'></div>
        <div id='donutchart'></div>

        <script>
            /////////////////////////////////////////////////
            // SHARED CODE
            /////////////////////////////////////////////////
            var data = [
                {"year": 2000, "type1": 30, "type2": 40, "type3": 30},
                {"year": 2001, "type1": 75.5, "type2": 4.5, "type3": 30},
                {"year": 2002, "type1": 0, "type2": 90, "type3": 10},
                {"year": 2003, "type1": 15, "type2": 20, "type3": 65},
                {"year": 2004, "type1": 20, "type2": 40, "type3": 40},
                {"year": 2005, "type1": 50, "type2": 25, "type3": 25}
            ];

            var updateRadio = function() {
                typeSelected = document.querySelector('input[name=type-selector]:checked', '#type-selector').value; 
                console.log('typeSelected:', typeSelected);
                d3.selectAll('rect').remove();
                drawBar(getDataFromType(typeSelected));
            }

            var updateSlider = function() {
                var slider = document.getElementById('year-slider');
                var output = document.getElementById('year-span');
                yearSelected = slider.value;
                output.innerHTML = yearSelected;
                console.log('yearSelected:', yearSelected);
                d3.selectAll('.svgG').remove();
                drawDonut(getDataFromYear(yearSelected));
            }

            d3.select('#type-selector').on('change', updateRadio);
            d3.select('#year-slider').on('input', updateSlider); 

            var yearSelected = d3.select('#year-span').text();
            var typeSelected = document.querySelector('input[name=type-selector]:checked', '#type-selector').value; 
            console.log('yearSelected:', yearSelected);
            console.log('typeSelected:', typeSelected);

            /////////////////////////////////////////////////
            // BAR
            /////////////////////////////////////////////////
            var svgBar = d3.select("#barchart").append("svg").attr("width", 450).attr("height", 200);
            var marginBar = {top: 20, right: 20, bottom: 30, left: 40};
            var widthBar = +svgBar.attr("width") - marginBar.left - marginBar.right;
            var heightBar = +svgBar.attr("height") - marginBar.top - marginBar.bottom;
            var xBar = d3.scaleBand().rangeRound([0, widthBar]).padding(0.1);
            var yBar = d3.scaleLinear().rangeRound([heightBar, 0]);

            createBar(data);
            function createBar(data) {
                var g = svgBar.append("g").attr('id', 'gBar').attr("transform", "translate(" + marginBar.left + ", " + marginBar.top + ")");

                xBar.domain(data.map(function(d) { 
                    return d.year; 
                }));
                yBar.domain([0, d3.max(data, function(d) { 
                    if(typeSelected == 'type1') return d.type1;
                    if(typeSelected == 'type2') return d.type2;
                    if(typeSelected == 'type2') return d.type3; 
                })]);

                g.append("g")
                    .attr("class", "axis axis--x")
                    .attr("transform", "translate(0," + heightBar + ")")
                    .call(d3.axisBottom(xBar));

                g.append("g")
                    .attr("class", "axis axis--y")
                    .call(d3.axisLeft(yBar).ticks(10, "%"))
                    .append("text")
                    .attr("transform", "rotate(-90)")
                    .attr("y", 6)
                    .attr("dy", "0.71em")
                    .attr("text-anchor", "end");

                var initialData = getDataFromType('type1');
                drawBar(initialData, heightBar);
            }

            // return the right data
            function getDataFromType(type) {
                var arr = [];
                data.forEach(function(item) {
                    if(type == 'type1') arr.push({"year": item.year, "value": item.type1});
                    if(type == 'type2') arr.push({"year": item.year, "value": item.type2});
                    if(type == 'type3') arr.push({"year": item.year, "value": item.type3});
                });
                return arr;
            }

            // draw barchart
            function drawBar(mydata, height) {
                d3.select('#gBar').selectAll(".bar")
                    .data(mydata)
                    .enter().append("rect")
                    .attr("class", "bar")
                    .attr("x", function(d) { 
                        return xBar(d.year); 
                    })
                    .attr("y", function(d) {
                        return yBar(d.value); 
                    })
                    .attr("width", xBar.bandwidth())
                    .attr("height", function(d) { 
                        return heightBar - yBar(d.value); 
                    });
            } // end draw 

            /////////////////////////////////////////////////
            // DONUT
            /////////////////////////////////////////////////
            var widthDonut = 400;
            var heightDonut = 200;
            var radiusDonut = Math.min(widthDonut, heightDonut) / 2;
            var colorDonut = d3.scaleOrdinal()
                .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
            var arcDonut = d3.arc()
                .outerRadius(radiusDonut - 10)
                .innerRadius(radiusDonut - 60);
            var pieDonut = d3.pie()
                .sort(null)
                .value(function(d) { 
                    return d.value; 
                });
            var svgDonut = d3.select("#donutchart").append("svg")
                .attr("width", widthDonut)
                .attr("height", heightDonut);
            var dataNestDonut = d3.nest()
                    .key(function(d) {
                        return d.year;
                    })
                .object(data);

            createDonut();
            function createDonut() {
                var initialData = getDataFromYear(yearSelected);
                drawDonut(initialData);
            }

            // return the right data
            function getDataFromYear(year) {
                var filterData = dataNestDonut[year].slice();
                var arr = [{"typeData": "type1", "value": filterData[0].type1}, {"typeData": "type2", "value": filterData[0].type2}, {"typeData": "type3", "value": filterData[0].type3}];
                return arr;
            }

            function drawDonut(data) {
                var svgG = svgDonut.append("g")
                    .attr('class', 'svgG')
                    .attr("transform", "translate(" + widthDonut / 2 + "," + heightDonut / 2 + ")");

                var g = svgG.selectAll(".arc")
                    .data(pieDonut(data))
                    .enter()
                    .append("g")
                    .attr("class", "arc");

                g.append("path")
                    .attr("d", arcDonut)
                    .style("fill", function(d) { 
                        return colorDonut(d.data.typeData); 
                    });

                g.append("text")
                    .attr("transform", function(d) { 
                        return "translate(" + arcDonut.centroid(d) + ")"; 
                    })
                    .attr("dy", ".35em")
                    .text(function(d) { 
                        return d.data.typeData; 
                    });
            }            
        </script>

    </body>
</html>

The code works but I think there are smarter ways to organize it.

\$\endgroup\$
4
\$\begingroup\$

I don't see anything wrong with the approach you're using, which is changing the variables' names and writing the code as a whole piece. The reason for that is simple: you have two fundamentally different charts.

What I mean by that is that you don't have the same kind of chart printed several times, with different parameters, like this dataviz I made. In a case like that, DRY is a very important principle and it makes no sense writing every single little chart in a code with unique variables...

But your case is different: you have different charts. In a situation like this, when I have two, three or more charts in the same page, I like to write the code as a whole, as if I were telling a story (because that's what sometimes we do in data visualisation, we tell stories).

So, I'd start the code with the bar chart and end it with the donut chart, as a whole piece, giving specific names for all the selections (from the SVG to the smallest elements), scales, axes, listeners etc. A good practice is giving meaningful names to your selections and constants according to the charts, like svgBar, svgDonut, widthBar, widthDonut etc, as you did.

However, if that doesn't sound a good option to you, or if you're trying to merge codes that were originally written separately, you have alternatives.

IIFEs

One of them, out of many, is using IIFEs to namespace your code, so you can keep the same variables.

Something like this:

(function barChart(){
    //code here...
}());

(function donutChart(){
    //code here...
}());

Using that approach you can use the same variable in both charts, and they will not conflict:

(function barChart(){
    var svg = foo;
    //this reefers to the bars' SVG
}());

(function donutChart(){
    var svg = foo;
    //this refers to the donut's SVG
}());

But pay attention to this: even using IIFEs, if you do d3.select(foo), you'll select the first element D3 finds in that page matching the criterion, not depending on where that element is. On the other hand, if you do svg.select(foo), you'll select only the element inside the SVG of that specific code (be it the bar chart or the donut chart).

Back to your question

Since after your edit you provide a code using different variables for the different charts (bar and donut chart), I'll write a code review for that, not for the IIFE version.

Here are the main points:

  1. Don't use document.querySelector, it makes no sense in a D3 code. You're using D3! So, just do d3.select. When dealing with DOM elements, take care of using node().

  2. You're attaching a .on("change" listener to the form containing the radio buttons...

    d3.select('#type-selector').on('change', updateRadio);
    

    ... and then you're selecting the radio buttons:

    var updateRadio = function() {
        typeSelected = document.querySelector('input[name=type-selector]:checked', '#type-selector').value; 
    

    You don't need that. The whole thing can be just:

    d3.selectAll('input[name=type-selector]').on('change', function(){
        var typeSelected = this.value;
        //rest of the code
    });
    
  3. Your getDataFromType can be way simpler:

    function getDataFromType(type) {
        var arr = [];
        data.forEach(function(item) {
            arr.push({
                "year": item.year,
                "value": item[type]
            });
        });
        return arr;
    }
    

And here comes the important part:

  1. Since now you have a simpler listener and a data filter, you don't need that awkward drawBar and createBar functions. Put everything inside createBar. That way, you can also have simpler scales and axes (check my code below).

  2. This is by far the most important advice: you are deleting elements to paint the SVG again:

    d3.selectAll('rect').remove();
    

    Don't do that!. Instead of that, use the D3 enter-update-exit selections (here, enter and update only):

    var bars = gBar.selectAll(".bar")
        .data(data);
    
    bars.enter().append("rect")
        .attr("class", "bar")
        .merge(bars)
        .attr("x", function(d) {
            return xBar(d.year);
        })
        .attr("y", function(d) {
            return yBar(d.value);
        })
        .attr("width", xBar.bandwidth())
        .attr("height", function(d) {
            return heightBar - yBar(d.value);
        });
    
  3. Do the same changes for the donut section:

    d3.select('#year-slider').on('input', function() {
        yearSelected = this.value;
        d3.select("#year-span").text(yearSelected);
        drawDonut(getDataFromYear(yearSelected));
    });
    

All together, this is the refactored code:

<html lang='en'>

  <head>
    <meta charset='utf-8'>

    <style>
      .bar {
        fill: steelblue;
      }

      .bar:hover {
        fill: brown;
      }

      .arc text {
        font: 10px sans-serif;
        text-anchor: middle;
      }

      .arc path {
        stroke: #fff;
      }

    </style>

    <title>Donut & Bar charts</title>
    <script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
    <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
  </head>

  <body>

    <div id='year-selector-container'>
      <input id='year-slider' class='year-range' type='range' min='2000' max='2005' value='2004'> Year: <span id='year-span'>2004</span>
    </div>

    <div id='type-selector-container'>
      <form id='type-selector'>
        <input type='radio' name='type-selector' id='rb-1' value='type1' checked />
        <label for='rb-1'>Type 1</label>

        <input type='radio' name='type-selector' id='rb-2' value='type2' />
        <label for='rb-2'>Type 2</label>

        <input type='radio' name='type-selector' id='rb-3' value='type3' />
        <label for='rb-3'>Type 3</label>
      </form>
    </div>

    <div id='barchart'></div>
    <div id='donutchart'></div>

    <script>
      /////////////////////////////////////////////////
      // SHARED CODE
      /////////////////////////////////////////////////
      var data = [{
          "year": 2000,
          "type1": 30,
          "type2": 40,
          "type3": 30
        },
        {
          "year": 2001,
          "type1": 75.5,
          "type2": 4.5,
          "type3": 30
        },
        {
          "year": 2002,
          "type1": 0,
          "type2": 90,
          "type3": 10
        },
        {
          "year": 2003,
          "type1": 15,
          "type2": 20,
          "type3": 65
        },
        {
          "year": 2004,
          "type1": 20,
          "type2": 40,
          "type3": 40
        },
        {
          "year": 2005,
          "type1": 50,
          "type2": 25,
          "type3": 25
        }
      ];

      d3.selectAll('input[name=type-selector]').on('change', function() {
        typeSelected = this.value;
        createBar(getDataFromType(typeSelected));
      });

      d3.select('#year-slider').on('input', function() {
        yearSelected = this.value;
        d3.select("#year-span").text(yearSelected);
        drawDonut(getDataFromYear(yearSelected));
      });

      var yearSelected = d3.select('#year-span').text();
      var typeSelected = d3.select('input[name=type-selector]:checked', '#type-selector').node().value;

      /////////////////////////////////////////////////
      // BAR
      /////////////////////////////////////////////////
      var svgBar = d3.select("#barchart").append("svg").attr("width", 450).attr("height", 200);
      var marginBar = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 40
      };
      var widthBar = +svgBar.attr("width") - marginBar.left - marginBar.right;
      var heightBar = +svgBar.attr("height") - marginBar.top - marginBar.bottom;
      var xBar = d3.scaleBand().rangeRound([0, widthBar]).padding(0.1);
      var yBar = d3.scaleLinear().rangeRound([heightBar, 0]);
      var gBar = svgBar.append("g").attr('id', 'gBar').attr("transform", "translate(" + marginBar.left + ", " + marginBar.top + ")");

      var gX = gBar.append("g")
        .attr("class", "axis axis--x")
        .attr("transform", "translate(0," + heightBar + ")");

      var gY = gBar.append("g")
        .attr("class", "axis axis--y")

      createBar(getDataFromType(typeSelected));

      function createBar(data) {

        xBar.domain(data.map(function(d) {
          return d.year;
        }));
        yBar.domain([0, d3.max(data, function(d) {
          return d.value
        })]);

        gX.call(d3.axisBottom(xBar));

        gY.call(d3.axisLeft(yBar).ticks(10, "%"))
          .append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", 6)
          .attr("dy", "0.71em")
          .attr("text-anchor", "end");

        var bars = gBar.selectAll(".bar")
          .data(data);

        bars.enter().append("rect")
          .attr("class", "bar")
          .merge(bars)
          .attr("x", function(d) {
            return xBar(d.year);
          })
          .attr("y", function(d) {
            return yBar(d.value);
          })
          .attr("width", xBar.bandwidth())
          .attr("height", function(d) {
            return heightBar - yBar(d.value);
          });

      }

      // return the right data
      function getDataFromType(type) {
        var arr = [];
        data.forEach(function(item) {
          arr.push({
            "year": item.year,
            "value": item[type]
          });
        });
        return arr;
      }

      /////////////////////////////////////////////////
      // DONUT
      /////////////////////////////////////////////////
      var widthDonut = 400;
      var heightDonut = 200;
      var radiusDonut = Math.min(widthDonut, heightDonut) / 2;
      var colorDonut = d3.scaleOrdinal()
        .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
      var arcDonut = d3.arc()
        .outerRadius(radiusDonut - 10)
        .innerRadius(radiusDonut - 60);
      var pieDonut = d3.pie()
        .sort(null)
        .value(function(d) {
          return d.value;
        });
      var svgDonut = d3.select("#donutchart").append("svg")
        .attr("width", widthDonut)
        .attr("height", heightDonut);
      var dataNestDonut = d3.nest()
        .key(function(d) {
          return d.year;
        })
        .object(data);

      createDonut();

      function createDonut() {
        var initialData = getDataFromYear(yearSelected);
        drawDonut(initialData);
      }

      // return the right data
      function getDataFromYear(year) {
        var filterData = dataNestDonut[year].slice();
        var arr = [{
          "typeData": "type1",
          "value": filterData[0].type1
        }, {
          "typeData": "type2",
          "value": filterData[0].type2
        }, {
          "typeData": "type3",
          "value": filterData[0].type3
        }];
        return arr;
      }

      function drawDonut(data) {
        var svgG = svgDonut.append("g")
          .attr('class', 'svgG')
          .attr("transform", "translate(" + widthDonut / 2 + "," + heightDonut / 2 + ")");

        var g = svgG.selectAll(".arc")
          .data(pieDonut(data))
          .enter()
          .append("g")
          .attr("class", "arc");

        g.append("path")
          .attr("d", arcDonut)
          .style("fill", function(d) {
            return colorDonut(d.data.typeData);
          });

        g.append("text")
          .attr("transform", function(d) {
            return "translate(" + arcDonut.centroid(d) + ")";
          })
          .attr("dy", ".35em")
          .text(function(d) {
            return d.data.typeData;
          });
      }

    </script>

  </body>

</html>

\$\endgroup\$
  • \$\begingroup\$ If I used an IIFE approach, how could I manage the shared code? For example the one related to radio buttons or sliders.. \$\endgroup\$ – Katherine Maurus May 24 '18 at 13:16
  • \$\begingroup\$ Namespace the events. Have a look here: jsfiddle.net/jd0xm442. Also, in your example, you don't need the same event for both charts: the slider changes the donut only, and the radio buttons change the bars only. \$\endgroup\$ – Gerardo Furtado May 25 '18 at 3:28
  • \$\begingroup\$ @KatherineMaurus Did you see the example showing how to namespace the events? Also, if the issue is finished, you already have two answers in your question, it's a good idea accepting one of them. \$\endgroup\$ – Gerardo Furtado Jun 2 '18 at 5:13
  • \$\begingroup\$ Sorry Gerardo, I had completely forgotten to accept the answer. Thank you for reminding me.Your answer was very useful to me. Thank you! \$\endgroup\$ – Katherine Maurus Jun 2 '18 at 5:38
2
\$\begingroup\$

functions don't need to accept data as parameter

The function createBar accepts data as a parameter but as it is currently written, could just access that global variable. As mentioned in Gerardo's answer it would be wise to limit the scope of variables using IIFEs.

jQuery

Your code doesn't appear to use any jQuery (though perhaps you removed any jQuery code when you added your code to your post - if that is the case, then ignore this). There is no point to adding the script tag for jquery.min.js. While that file is minimized, it is 29 KB of useless data being downloaded for users everytime, plus time for the browser to parse it, etc. For more information on this front, see You Might Not Need jQuery.

Cache DOM references

DOM lookups aren't exactly cheap, so it is advisable to store DOM references in a variable, and then utilize those variables whenever needed. For instance, there are two lookups in updateSlider(). Those can be moved outside the function - perhaps it would be wise to move all the code into a DOM loaded callback (e.g. using document.addEventListener() for the DOMContentLoaded event)

document.addEventListener("DOMContentLoaded", function(event) {
    //store DOM references
    var slider = document.getElementById('year-slider');
    var output = document.getElementById('year-span');

    var updateSlider = function() {
        yearSelected = slider.value;
        output.innerHTML = yearSelected;
        d3.selectAll('.svgG').remove();
        drawDonut(getDataFromYear(yearSelected));
    };
});

Shared data, HTML, etc.

I know you originally wanted to know how to share data between separate pages, and while that question isn't really here anymore and I shouldn't do this, I can't help but want to give a solution to that question. One technique is to store the data separately and fetch it via AJAX, or alternatively have the server side code include it (e.g. with PHP, Java, NodeJS, Python, ASP, Ruby, C#, ColdFusion, etc.) after fetching the data from a file, database, etc.. You could also render the common HTML with such a scripting language and have that abstracted to a single function/template/file/etc.

To fetch data with AJAX, there are nice wrappers like the fetch API or libraries like reqwest and superagent. If you were actually using jQuery, then you could use its AJAX functions (e.g. .get(), .post(), etc.). See an example in this plunker using fetch().

And the common javascript could be abstracted out to a separate javascript file (e.g. updateRadio(), updateSlider())

\$\endgroup\$
  • \$\begingroup\$ Thank you, very helpful answer! I would like to do something similar to your example shown in the plunker but instead of using links to show individual charts, I would like these to always be present on the same page but I would like to separate the code so I want to have donut.html, donut.js, bar. html and bar.js.. \$\endgroup\$ – Katherine Maurus May 24 '18 at 12:41
  • \$\begingroup\$ Okay - are you still using Java on the server-side? Or another scripting language (like I mentioned in the answer)? If so, that would be a convenient way to include the data in both places, as well as abstract out common HTML and other resources. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ May 24 '18 at 15:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.