Sunday 4 June 2017

Web Tutorial: The Bar Chart (Part 1/2)

When you want to display data, there are quite a number of options available to you - bar graphs, pie graphs and line graphs, among others.

Bar graphs are meant for comparing numerical data between different groups, or progress over time, though it's fairly limited in the case of the latter. Today, we'll be creating an animated bar graph in HTML, CSS and JavaScript.

For data, we will be using statistics collected from Wikipedia, pertaining to the football club I support - Liverpool FC. We'll compare appearances and goals from six players across four seasons.

Let's get started!
Here's the HTML. We'll create placeholders for CSS and JavaScript.
<!DOCTYPE html>
<html>
    <head>
        <title>Bar Graph</title>

        <style>

        </style>

        <script>

        </script>
    </head>
   
    <body>

    </body>
</html>


Here, we have a temporary styling for div elements, giving them a nice orange outline. And we specify that the graph_container div is 500px in height. In the HTML, we have the graph_container div and two drop down lists with ids of ddlRow and ddlStat respectively.
<!DOCTYPE html>
<html>
    <head>
        <title>Bar Graph</title>

        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }
        </style>

        <script>

        </script>
    </head>
   
    <body>
        <div id="graph_container">

        </div>

        <select id="ddlRow">

        </select>

        <select id="ddlStat">

        </select>
    </body>
</html>


See the result!


Create two more divs within the graph_container div, and give them ids of scale_container and col_container respectively.
        <div id="graph_container">
            <div id="scale_container">

            </div>

            <div id="col_container">

            </div>
        </div>


Style col_container and scale_container like so. scale_container is the placeholder for the scale of the bar graph while col_container holds the data. scale_container has only a 20px width and its height is 80% of graph_container. Its border at the bottom is a fine black line. col_container is just as tall as the graph_container div, and the width is set to 95% of graph_container's. Both of these divs have the float property set to left, so they align nicely left.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Take a look at it now. You have a nice thin column for the scale (with a black border on the bottom) and a larger space for your data.


All good? We'll be populating these placeholder divs soon. First, change your HTML.
<body onload="populate();">


Then add the populate() function.
        <script>
            function populate()
            {

            }
        </script>


The data

Before working on the populate() function, let's create the data. Here we define an object, graphdata, with three properties - cols, rows and stats. Each of these is an array.
        <script>
            var graphdata =
            {
                "cols":[],
                "rows":[],
                "stats":[]
            };

            function populate()
            {

            }
        </script>


Let's begin defining values for rows. These years represent the seasons which the datasets are for. Each season begins in the middle of the year, and ends in the middle of next year. But it's enough to just take the starting year.
            var graphdata =
            {
                "cols":[],
                "rows":[2007,2008,2009,2010],
                "stats":[]
            };


Now, we define what stats there are for any dataset. Namely, goals and appearances.
            var graphdata =
            {
                "cols":[],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


The next one is going to be huge. Each element in the cols array is an object. Each object has a property, title, and an array, stats. Here, the title for the first element is the name of one of Liverpool FC's most prolific strikers, ever.
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":[]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


Now, add the stats. Each element in the stats array is, in turn, another object with the properties year, goals and appearances. Boy, this guy really did score a lot of goals, didn't he?!
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":
                        [
                            {"year":2007,"goals":24,"appearances":33},
                            {"year":2008,"goals":14,"appearances":24},
                            {"year":2009,"goals":18,"appearances":22},
                            {"year":2010,"goals":9,"appearances":23},
                        ]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


Add the other players in the cols array.
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":
                        [
                            {"year":2007,"goals":24,"appearances":33},
                            {"year":2008,"goals":14,"appearances":24},
                            {"year":2009,"goals":18,"appearances":22},
                            {"year":2010,"goals":9,"appearances":23},
                        ]
                    },
                    {
                        "title":"Steven Gerrard",
                        "stats":
                        [
                            {"year":2007,"goals":11,"appearances":36},
                            {"year":2008,"goals":16,"appearances":34},
                            {"year":2009,"goals":9,"appearances":31},
                            {"year":2010,"goals":4,"appearances":33},
                        ]
                    },
                    {
                        "title":"Dirk Kuyt",
                        "stats":
                        [
                            {"year":2007,"goals":3,"appearances":34},
                            {"year":2008,"goals":12,"appearances":32},
                            {"year":2009,"goals":9,"appearances":38},
                            {"year":2010,"goals":13,"appearances":37},
                        ]
                    },
                    {
                        "title":"Ryan Babel",
                        "stats":
                        [
                            {"year":2007,"goals":4,"appearances":30},
                            {"year":2008,"goals":3,"appearances":27},
                            {"year":2009,"goals":4,"appearances":25},
                            {"year":2010,"goals":1,"appearances":1},
                        ]
                    },
                    {
                        "title":"Yossi Benayoun",
                        "stats":
                        [
                            {"year":2007,"goals":4,"appearances":30},
                            {"year":2008,"goals":8,"appearances":32},
                            {"year":2009,"goals":6,"appearances":30},
                        ]
                    },
                    {
                        "title":"David N'gog",
                        "stats":
                        [
                            {"year":2008,"goals":2,"appearances":14},
                            {"year":2009,"goals":5,"appearances":24},
                            {"year":2010,"goals":2,"appearances":25},
                        ]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };

Let's hammer this into shape!

Add this to the populate() function. There are some undefined variables, and there's the variable graph, which we will peg to the div col_container.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");
            }


Now do this. Create a For loop that iterates across the cols array of the graphdata object.

Then, within the loop, create a div element and use the variable col to store that object. Then set the class of that object to col_section. And append that object to graph.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    graph.appendChild(col);
                }
            }


Modify your CSS. Add the col_section CSS class. The width will be 100px but the height will be 100% of its parent. Don't mix the two up! And of course, the float property of this is left, to properly align everything.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Run your code. You'll see 6 columns have been added inside the col_container div! One for each footballer we added in the graphdata object!


We're going to do more now. We'll create a div element, set it to the variable container and set the class to data_container, then append container within col before appending col within graph.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    col.appendChild(container);

                    graph.appendChild(col);
                }
            }


And then we'll add more styling, this time the data_container CSS class. The margin properties ensure that it will be centered within its parent, and only occupy 80% height and width. The background property is set to transparent explicitly, just in case.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Now you see that within the columns is a smaller column!


Now we're going to do the same with the fill variable, setting the class to data_fill. But before appending it within container, we will create a paragraph element and set it to the variable p. Then we'll append p within fill before appending fill within container. We'll give each of these elements an id as well.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    col.appendChild(container);

                    graph.appendChild(col);
                }
            }


Now we add the CSS styles data_fill and value_label.

data_fill is meant for styling the visual representation of the bars. It will fill 100% of the height and width of its parent. I've set the background color to red.

value_label is meant for styling the numerical data. I've set the color to white. The rest of it is cosmetic styling of the font, which you can alter without affecting functionality (much).
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }

            .data_fill
            {
                width:100%;
                height:100%;
                color:#000000;
                background-color:#FF0000;
                text-align:center;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                margin-top:0%;
            }

            .value_label
            {
                color:#FFFFFF;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


You can see that the data columns are filled with red. Just as planned. You won't see the labels because we haven't given them any data. Yet.


Now add this! Create a div element, set it to the label variable with a class of data_label, and the innerHTML property to the title property of the graphdata array element you are currently accessing within the For loop. But instead of appending it within container, append it within col after container.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }
            }


OK, now add the data_label CSS style. width is set to 100% and height to 20%. The margin property ensures that the div stays centered. The rest is font styling, so do as you please.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }

            .data_label
            {
                margin: 0 auto 0 auto;
                text-align:center;
                width:100%;
                height:20%;
                color:#000000;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
            }

            .data_fill
            {
                width:100%;
                height:100%;
                color:#000000;
                background-color:#FF0000;
                text-align:center;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                margin-top:0%;
            }

            .value_label
            {
                color:#FFFFFF;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


And there we have the names of the labels on the bottom! It's not nicely aligned yet, but we can take care of that later.


Now outside of the For loop, we have new code. Remember the drop-down lists? We're going to populate them with data. First, define two variables, ddl and option.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }

                var ddl,option;
            }


Now set the variable ddl to refer to the object ddlRow. And for each element in the rows array of the graphdata object, create an option and populate it, then add it to the drown-down list ddlRow. The text is slightly different from the value. That's because in football parlance, as previously mentioned, each season starts in the middle of the year and ends in the middle of next year. So a season that started in 2008 would be more appropriately known as "Season 2008/2009".
                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }


Take a gander!


Now do the same for ddlStat...
                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }

                ddl = document.getElementById("ddlStat");

                for (var i=0;i<graphdata.stats.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.stats[i];
                    option.value = graphdata.stats[i];
                    ddl.add(option);
                }


And both your drop-down lists are populated!


That won't do anything for us right now of course. We'll need a data scale. Let's start by defining a new function, displayData(), and calling it at the end of populate().
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }


                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }

                ddl = document.getElementById("ddlStat");

                for (var i=0;i<graphdata.stats.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.stats[i];
                    option.value = graphdata.stats[i];
                    ddl.add(option);
                }

                displayData();
            }

            function displayData()
            {

            }


Start by grabbing the selected values from the ddlRow and ddlStat drop-down lists. Then run the values through the getMaxStatistic() function and use the result in the displayScale() function. We'll, of course, have to create those functions.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                displayScale(max);
            }

            function getMaxStatistic(row,stat)
            {

            }
           
            function displayScale(maxval)
            {

            }


We'll work on the getMaxStatistic() function next. First, define a variable max with a value of 1. Then declare the variable temp.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;
            }


Next, iterate through the cols array of the graphdata object using a For loop. Within it, process the stats array of the current element of the cols array using the filter() method to match the year property with that of the row parameter of the getMaxStatistic() function. This means that if "2008" is selected, all elements with the year for "2008" will be returned in an array. That array is set to the variable temp.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    temp = graphdata.cols[i].stats.filter(function (x) {return x.year==row;});
                }
            }


If temp is an empty array, that means no data for the year was found, which is a possibility. If not, there should be only one row. Grab the first (and only) element in the temp array with the statistic defined by the stat parameter of the getMaxStatistic() function. If it's more than max, set max to that value.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    temp = graphdata.cols[i].stats.filter(function (x) {return x.year==row;});

                    if (temp.length>0)
                    {
                        if (temp[0][stat]>max)
                        {
                            max = temp[0][stat];
                        }   
                    }
                }
            }


Finally, we conduct a Modulus test. If max is divisible by 10, return max with 10 added. If not, deduct the remainder so that max is divisible by 10, then return max with 10 added.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    temp = graphdata.cols[i].stats.filter(function (x) {return x.year==row;});

                    if (temp.length>0)
                    {
                        if (temp[0][stat]>max)
                        {
                            max = temp[0][stat];
                        }   
                    }
                }

                if (max % 10 == 0)
                {
                    return max+10;
                }
                else
                {
                    return max - (max % 10) + 10;
                }
            }


Now for the displayScale() function! We passed the maximum statistic of the dataset through this function, remember? Here, we grab the graph_scale element and set it to the variable scale. We set the innerHTML property to a blank value to clear the contents of scale.

Next, we define two new variables, units and data_unit. units is the value passed into this function (which is the maximum value of the dataset) divided by 10.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;
            }


Now we're going to work backwards, because on a scale, the top number is displayed first. So our For loop starts from units and works its way down to 1, decrementally.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;

                for (var i=units;i>=1;i--)
                {

                }
            }


Here, we set data_unit to a created div element, then set its class to scale_unit. Its height is defined by a percentage of its parent. We determine the percentage by dividing 100 by units. Finally, the div is labelled with the value of (i x 10), which means the scale is always in intervals of 10. Then we append the created div within scale.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;

                for (var i=units;i>=1;i--)
                {
                    data_unit = document.createElement("div");
                    data_unit.className = "scale_unit";
                    data_unit.style.height = (100/units) + "%";
                    data_unit.innerHTML = i * 10;

                    scale.appendChild(data_unit);
                }
            }


Here's the styling for the CSS class scale_unit. The width is 100% of its parent, and the border for the top and left edges are set to a thin black line. The rest is all cosmetic.
            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }

            .scale_unit
            {
                width:100%;
                border-top:1px solid #000000;
                border-left:1px solid #000000;
                color:#FF0000;
                text-align:right;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }



Aaaaand you have a scale!


This is a bit messy, so just change the code here.
div {outline:0px solid #FFAA00;}


Nice. Still a bit messy, but it works. We have a placeholder for all the data we're about to manipulate!


Editor's Note: The dataset for Fernando Torres was found to be incorrect, and later, edited accordingly. As such, some of the screenshots are also incorrect.

Next

Altering the display to fit the data.



No comments:

Post a Comment