Tuesday, 16 January 2018

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

Sometime last year, I concluded two truly awesome (to me, at least) web tutorials on Bar Charts and Line Graphs using HTML, a lot of CSS, and a bit of JavaScript. Today, I bring you... the Pie Chart.

Pie Charts are primarily used to show relative amounts, or percentages. And they're useful for determining, at a glance, what the majority statistic is. Bar Charts can do that too - they're versatile that way - but not as well as a Pie Chart. There's going to be a fair amount of math involved in this one. You'll need to understand percentages and geometry, at the very least.

For the purposes of this web tutorial, we will be using the same dataset that was employed during the last two web tutorials. We'll also be reusing some functions and CSS classes.

Without further ado...

Here's the beginning HTML. You'll see that I've included, in the interest of saving time, some of the HTML and CSS we used during the Bar Chart and Line Graph web tutorials. The graph_container div is there, as well as legend_container div, and the two dropdown lists, ddlRow and ddlStat.

For the JavaScript, I've included the dataset graphdata, as well as the functions populate(), getMaxStatistic(), displayData() and displayLegend(). There will be changes to some of the stuff, but as with the previous web tutorials, populate() will call displayData() and displayLegend() at the end. displayLegend() and getMaxStatistic() are unchanged from the last tutorial.

The page loads populate() and the dropdown lists trigger displayData(), as per the last web tutorials.

For the CSS, I've left the styling for the graph_container largely intact (except it's now a 400 pixel square and centered), and the styling for the legend is identical to the Line Graph web tutorial's.
<!DOCTYPE html>
<html>
    <head>
        <title>Pie Chart</title>

        <style>
            #graph_container
            {
                height: 400px;
                width: 400px;
                margin: 0 auto 0 auto;
            }       

            #legend_container
            {
                color: #FFFFFF;
                font-weight: bold;
                font-family: verdana;
                font-size: 1em;
                width: 50%;
                margin: 5% auto 0 auto;
                padding: 0.5em;
            }

            .legend_row
            {
                width: 100%;
                height: 1.5em;
            }

            #legend_row: after
            {
                display: block;
                content: "";
                clear: both;
            }

            .legend_color
            {
                width: 1em;
                height: 1em;
                float: left;
            }

            .legend_label
            {
                height: 1em;
                float: left;
                color: #000000;
                font-weight: bold;
                font-family: verdana;
                font-size: 1em;
                margin-left: 1em;
            }
        </style>

        <script>
            var graphdata =
            {
                "cols":
                [
                    {
                        "title": "Fernando Torres",
                        "color": "#FF00FF",
                        "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",
                        "color": "#440000",
                        "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",
                        "color": "#FFFF00",
                        "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",
                        "color": "#00AA00",
                        "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",
                        "color": "#000044",
                        "stats":
                        [
                            {"year": 2007, "goals": 4, "appearances": 30},
                            {"year": 2008, "goals": 8, "appearances": 32},
                            {"year": 2009, "goals": 6, "appearances": 30},
                        ]
                    },
                    {
                        "title": "David N'gog",
                        "color": "#006699",
                        "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"]
            };

            function populate()
            {
                displayLegend();
                displayData();
            }

            function displayData()
            {

            }

            function getMaxStatistic(haystack, needle)
            {
                for (var i = 0; i < haystack.length; i++)
                {   
                    if (haystack[i].stats < haystack[needle].stats)
                    {
                        return false;
                    }
                }

                return true;
            }

            function displayLegend()
            {
                var legend = document.getElementById("legend_container");
                legend.innerHTML = "";

                var color, label;
                var row;

                for (var i = 0; i < graphdata.cols.length; i++)
                {
                    color = document.createElement("div");
                    color.className = "legend_color";
                    color.style.backgroundColor = graphdata.cols[i].color;

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

                    row = document.createElement("div");
                    row.className = "legend_row";

                    row.appendChild(color);
                    row.appendChild(label);
                    legend.appendChild(row);
                }
            }
        </script>
    </head>
   
    <body onload="populate();">
        <div id="graph_container">

        </div>

        <div id="legend_container">

        </div>

        <select id="ddlRow" onchange="displayData();">

        </select>

        <select id="ddlStat" onchange="displayData();">

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


So let's do this now. Set all divs to have a red outline.
            div {outline: 1px solid #FFAA00;}

            #graph_container
            {
                height: 400px;
                width: 400px;
                margin: 0 auto 0 auto;
            }


For the sake of visibility, this is what you have now.


Let's start with some smaller stuff. Populate the drop-down lists as shown. If you've been following the previous web tutorials, this should be clear; I'd rather not repeat myself.
            function populate()
            {
                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);
                }

                displayLegend();
                displayData();
            }


Your drop-down lists! Right now, they don't do anything (other than fire off the empty displayData() function), but that's fine.


OK, down to (more) serious business! Within your graph_container div, you should have a div for your data labels, id label_wrapper, followed by a div for your pie, id pie_container.
        <div id="graph_container">
            <div id="label_wrapper">

            </div>

            <div id="pie_container">

            </div>
        </div>


Here's the styling. Both of these divs will occupy the graph_container div fully (100% height and width) and floated left. pie_container will be circular, so the border-radius property is at 50%, and overflow is set to hidden. label_wrapper will overlap pie_container, so the margin-right property is set to negative 100% while a display and z-index property needs to be specified. You won't see any change in your browser after refreshing it. It's a perfect overlap.
            div {outline:0px solid #FFAA00;}

            #graph_container
            {
                height: 400px;
                width: 400px;
                margin: 0 auto 0 auto;
            }

            #pie_container
            {
                height: 100%;
                width: 100%;
                border-radius: 50%;
                float: left;
                overflow: hidden;
            }           

            #label_wrapper
            {
                height: 100%;
                width: 100%;
                margin-right: -100%;
                float: left;
                position: relative;
                z-index: 2000;
            }


Now we're gonna add another div inside label_wrapper. This will have an id of label_quad_wrapper.
        <div id="graph_container">
            <div id="label_wrapper">
                <div id="label_quad_wrapper">

                </div>
            </div>

            <div id="pie_container">

            </div>
        </div>


It'll be styled this way. It's going to occupy the top right quarter of the label_wrapper div.
            #label_wrapper
            {
                height: 100%;
                width: 100%;
                margin-right: -100%;
                float: left;
                position: relative;
                z-index: 2000;
            }

            #label_quad_wrapper
            {
                height: 50%;
                width: 50%;
                margin-left: 50%;
            }

            #legend_container
            {
                color: #FFFFFF;
                font-weight: bold;
                font-family: verdana;
                font-size: 1em;
                width: 50%;
                margin: 5% auto 0 auto;
                padding: 0.5em;
            }




Next, we add two divs in the pie_container div. One will be quad_wrapper_left and the other will be quad_wrapper_right. The ids are important! They will both have the style quad_wrapper.
        <div id="graph_container">
            <div id="label_wrapper">
                <div id="label_quad_wrapper">

                </div>
            </div>

            <div id="pie_container">
                <div id="quad_wrapper_left" class="quad_wrapper">

                </div>

                <div id="quad_wrapper_right" class="quad_wrapper">

                </div>
            </div>
        </div>


This is how we'll style these. They will take up both halves of the pie_container div.
            #pie_container
            {
                height: 100%;
                width: 100%;
                border-radius: 50%;
                float: left;
                overflow: hidden;
            }           

            .quad_wrapper
            {
                width: 50%;
                height: 100%;
                float: left;
            }

            #label_wrapper
            {
                height: 100%;
                width: 100%;
                margin-right: -100%;
                float: left;
                position: relative;
                z-index: 2000;
            }


See that? pie_container has two equal-sized divs, one taking the left side, and one taking the right. And because it's overlapped with label_wrapper, the label_quad_wrapper div can still be seen on the top right corner!


Now for the hard part...

We'll be working with the data, to put in the labels.

First, let's define a class for those labels.

The class label_quad will be used to store those pie slices we'll be putting in label_quad_wrapper. The pie slices will be the full size of label_quad_wrapper, floated left, and they'll be rotated by the bottom left corner.

data_label is another class. I'm setting the text to black for now. But the other properties are mostly cosmetic. I set the position property explicitly to relative for these two classes so there's no misunderstanding on the part of the browser.

            #label_quad_wrapper
            {
                height: 50%;
                width: 50%;
                margin-left: 50%;
            }

            .label_quad
            {
                width: 100%;
                height: 100%;
                margin-bottom: -100%;
                -webkit-transform-origin: 0% 100%;
                transform-origin: 0% 100%;
                position: relative;
            }

            .data_label
            {
                color: #000000;
                font-weight: bold;
                font-family: verdana;
                font-size: 0.8em;
                position: relative;
            }

            #legend_container
            {
                color: #FFFFFF;
                font-weight: bold;
                font-family: verdana;
                font-size: 1em;
                width: 50%;
                margin: 5% auto 0 auto;
                padding: 0.5em;
            }


Now that we've defined those classes, let's get to work with the JavaScript. Pay close attention, because there'll be quite a few moving parts in this one. We're just going to place the data labels first.

Here, we start with the displayData() function. Get the current values of the ddlStat and ddlRow drop-down lists and assign them to the variables stat and row, respectively.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;
            }


Create the variable nonzero and set it to the value of a function, getNonZero(), with row and stat passed in as arguments. Then create the function getNonZero. It'll take in two parameters - you guessed it - row and stat. The purpose of this function is to return only values that are non-zero... because showing data that is zero in a pie chart just doesn't make much sense, does it?
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
            }

            function getNonZero(row, stat)
            {

            }


First, define an array, nonzero. Then variables temp and player. At the end of the function, you will return the array nonzero.
            function getNonZero(row, stat)
            {
                var nonzero = [];
                var temp;
                var player;

                return nonzero;
            }


Now, let's iterate through the cols array of the graphdata object. In the For loop, get the stats array filtered. We only want those whose year property match the row variable. Set the resultant array to the variable temp.
            function getNonZero(row, stat)
            {
                var nonzero = [];
                var temp;
                var player;

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

                return nonzero;
            }


Now, we only want to do stuff if the temp array is not empty. There should be only one item in the array, so reference temp[0], and grab the property corresponding with stat. This being a JSON object, an array is really another type of object, so we can reference the temp[0] object's properties by treating it as a two-dimensional array! Again, we only want to do stuff if the stat in question is greater than zero.
            function getNonZero(row, stat)
            {
                var nonzero = [];
                var temp;
                var player;

                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] > 0)
                        {

                        }   
                    }
                }

                return nonzero;
            }


If there's non-zero data, set player to a newly created object and assign it two properties - color and stats. Grab the appropriate data from the graphdata and temp[0] object, then add the resultant object to the nonzero array. Basically, this function gives you an array of objects corresponding to the year and statistic selected in the drop-down lists, and their corresponding color and value in the graphdata object! And makes sure the values selected are non-zero, of course.
            function getNonZero(row, stat)
            {
                var nonzero = [];
                var temp;
                var player;

                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] > 0)
                        {
                            player =
                            {
                                "color": graphdata.cols[i].color,
                                "stats": temp[0][stat]   
                            };

                            nonzero.push(player);
                        }   
                    }
                }

                return nonzero;
            }


For instance, if you passed "2009" and "goals" into the getNonZero() function, you would get an array of objects like this.
[
    {
        "color": "#FF00FF",
        "stats": 18   
    },
    {
        "color": "#440000",
        "stats": 9   
    },
    {
        "color": "#FFFF00",
        "stats": 9   
    },
    {
        "color": "#00AA00",
        "stats": 4   
    },
    {
        "color": "#000044",
        "stats": 6   
    },
    {
        "color": "#006699",
        "stats": 5   
    }
]


So back to the displayData() function. After obtaining the array of non-zero statistics, run it through the getSorted() function and assign the result to the sorted variable. And then create the getSorted() function.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
            }

            function getNonZero(row, stat)
            {
                var nonzero = [];
                var temp;
                var player;

                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] > 0)
                        {
                            player =
                            {
                                "title": graphdata.cols[i].title,
                                "color": graphdata.cols[i].color,
                                "stats": temp[0][stat]   
                            };

                            nonzero.push(player);
                        }   
                    }
                }

                return nonzero;
            }

            function getSorted(nonzero)
            {

            }


We first define sorted as an empty array, and temp as a variable, assigning the value of nonzero to it. At the end of the function, we return sorted. This function basically accepts the array of non-zero values, nonzero, and sorts them from largest to smallest. There's a reason why we need this. It will all be clear later.
            function getSorted(nonzero)
            {
                var sorted = [];
                var temp = nonzero;

                return sorted;
            }


Now we'll be running the next segment of code for as long as temp is not an empty array. If temp has only one element, we add that element to sorted, then remove it from temp using the splice() method. That would make temp an empty array, which is when the loop ends.
            function getSorted(nonzero)
            {
                var sorted = [];
                var temp = nonzero;

                while (temp.length > 0)
                {
                    if (temp.length == 1)
                    {
                        sorted.push(temp[0]);
                        temp.splice(0, 1);
                    }
                    else
                    {

                    }
                }

                return sorted;
            }


But if temp has more than one element, we iterate through temp. And run the getMaxStatistic() function, passing in temp and the current pointer.
            function getSorted(nonzero)
            {
                var sorted = [];
                var temp = nonzero;

                while (temp.length > 0)
                {
                    if (temp.length == 1)
                    {
                        sorted.push(temp[0]);
                        temp.splice(0, 1);
                    }
                    else
                    {
                        for (var i = 0; i < temp.length; i++)
                        {   
                            if (getMaxStatistic(temp, i))
                            {

                            }
                        }
                    }
                }

                return sorted;
            }


If the result is true, push the current element in the temp array into sorted, then remove that element from temp. We already have the getMaxStatistic() function. It will return true if the element in the temp array pointed to by i, has the highest stat in the array.

So what this does, is probably not all that efficient... but it works and that's all we really need from it right now.
            function getSorted(nonzero)
            {
                var sorted = [];
                var temp = nonzero;

                while (temp.length > 0)
                {
                    if (temp.length == 1)
                    {
                        sorted.push(temp[0]);
                        temp.splice(0, 1);
                    }
                    else
                    {
                        for (var i = 0; i < temp.length; i++)
                        {   
                            if (getMaxStatistic(temp, i))
                            {
                                sorted.push(temp[i]);
                                temp.splice(i, 1);
                            }
                        }
                    }
                }

                return sorted;
            }


After using the getSorted() function, the returned data should look like this.
[
    {
        "color": "#FF00FF",
        "stats": 18   
    },
    {
        "color": "#440000",
        "stats": 9   
    },
    {
        "color": "#FFFF00",
        "stats": 9   
    },
    {
        "color": "#000044",
        "stats": 6   
    },
    {
        "color": "#006699",
        "stats": 5   
    },
    {
        "color": "#00AA00",
        "stats": 4   
    },
]


Got all that? Great? We're going back to the displayData() function now...

Now that we have sorted which is the array holding the sorted statistics from largest to smallest, we pass the array into the getPieces() function as an argument and set the variable pieces to the result. This function will derive all the pieces required to hold the data. We'll get to that in a moment.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
                var pieces = getPieces(sorted);
            }


In the getPieces() function, we first define a variable, total, intitialized to 0. Then an empty array, pieces. Then we have another two variables, currentPiece and currentTotalPiece. currentTotalPiece is also initialized to 0. pieces is returned at the end of the function.
            function getSorted(nonzero)
            {
                var sorted = [];
                var temp = nonzero;

                while (temp.length > 0)
                {
                    if (temp.length == 1)
                    {
                        sorted.push(temp[0]);
                        temp.splice(0, 1);
                    }
                    else
                    {
                        for (var i = 0; i < temp.length; i++)
                        {   
                            if (getMaxStatistic(temp, i))
                            {
                                sorted.push(temp[i]);
                                temp.splice(i, 1);
                            }
                        }
                    }
                }

                return sorted;
            }

            function getPieces(sorted)
            {
                var total = 0;
                var pieces = [];
                var currentPiece;
                var currentTotalPiece = 0;

                return pieces;
            }

            function getMaxStatistic(haystack, needle)
            {
                for (var i = 0; i < haystack.length; i++)
                {   
                    if (haystack[i].stats < haystack[needle].stats)
                    {
                        return false;
                    }
                }

                return true;
            }


Next, we will obtain total by iterating through the array sorted and tallying up all the stats.
            function getPieces(sorted)
            {
                var total = 0;
                var pieces = [];
                var currentPiece;
                var currentTotalPiece = 0;

                for (var i = 0; i < sorted.length; i++)
                {   
                    total += sorted[i].stats;
                }

                return pieces;
            }


Next, we iterate through the sorted array in reverse order. Why? Because sorted is sorted from largest stat to smallest. We want to process the largest stats first. And why is that? Relax, it'll be clear soon.

Our aim here is to determine how many degrees, out of a possible 360 degrees (which, geometrically, is what a circle offers), that each stat will take.

Inside the loop, if i is 0, that means you are at the first element of sorted (and smallest stat), in which case the size of currentPiece is 360 degrees minus currentTotalPiece. eBar that in mind, for now.

If not, we derive currentPiece by calculating the ratio, which is the current stat divided by total. Then we multiple that by 360 to get the number of degrees the stat will take. Using the toFixed() method with 0 as an argument, will ensure that the decimal places are trimmed off. However, we want to use currentPiece in another calculation next, so we have to use the parseInt() function. currentTotalPiece is the number of degrees that have been used up by your pieces so far, so it will be incremented by currentPiece. Of course, when you get to the final and smallest stat, we merely have to take 360 less currentTotalPiece!
            function getPieces(sorted)
            {
                var total = 0;
                var pieces = [];
                var currentPiece;
                var currentTotalPiece = 0;

                for (var i = 0; i < sorted.length; i++)
                {   
                    total += sorted[i].stats;
                }

                for (var i = sorted.length - 1; i >= 0; i--)
                {   
                    if (i == 0)
                    {
                        currentPiece = 360 - currentTotalPiece;
                    }
                    else
                    {
                        currentPiece = ((sorted[i].stats/total) * 360).toFixed(0);
                        currentTotalPiece += parseInt(currentPiece);
                    }
                }

                return pieces;
            }


Of course, with each piece that is being processed, create an object with the color, stats, and the piece property, which is set to the value of currentPiece. Then added it to the pieces array. Since we're using the push() method and the sorted array is being iterated through in reverse order, that means we'll get a sorted array with the largest piece, to the smallest piece!
            function getPieces(sorted)
            {
                var total = 0;
                var pieces = [];
                var currentPiece;
                var currentTotalPiece = 0;

                for (var i = 0; i < sorted.length; i++)
                {   
                    total += sorted[i].stats;
                }

                for (var i = sorted.length - 1; i >= 0; i--)
                {   
                    if (i == 0)
                    {
                        currentPiece = 360 - currentTotalPiece;
                    }
                    else
                    {
                        currentPiece = ((sorted[i].stats / total) * 360).toFixed(0);
                        currentTotalPiece += parseInt(currentPiece);
                    }

                    pieces.push
                    (
                        {
                            "color": sorted[i].color,
                            "stats": sorted[i].stats,
                            "piece": currentPiece
                        }
                    );
                }

                return pieces;
            }


The data derived from getPieces() should be this...
[
    {
        "color": "#FF00FF",
        "stats": 18,
        "piece": 127   
    },
    {
        "color": "#440000",
        "stats": 9,
        "piece": 64
    },
    {
        "color": "#FFFF00",
        "stats": 9,
        "piece": 64
    },
    {
        "color": "#000044",
        "stats": 6,
        "piece": 42   
    },
    {
        "color": "#006699",
        "stats": 5,
        "piece": 35
    },
    {
        "color": "#00AA00",
        "stats": 4,
        "piece": 28   
    }
]


Next thing we do is clear the label_quad_wrapper div.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
                var pieces = getPieces(sorted);

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.innerHTML = "";
            }


Now declare an object, lastPieceAngle, and put in properties prevangle and newangle. Initialize both to 0. Set the value of lastPieceAngle to the function placeLabel(), using the first piece in pieces and lastPieceAngle as arguments. Then create the placeLabel() function.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
                var pieces = getPieces(sorted);

                var lastPieceAngle = {"prevangle": 0, "newangle": 0};

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.innerHTML = "";

                lastPieceAngle = placeLabel(pieces[0], lastPieceAngle);
            }

            function placeLabel(piece, angle)
            {

            }


We'll get started with the placeLabel() function now. Declare newangle and midangle as variables. Since each pie slice is supposed to begin and end at a particular angle, we derive newangle from the newangle variable, and the newangle property of the angle object becomes the previous angle, prevangle.

As for midangle, I'll explain in the next few paragraphs.
            function placeLabel(piece, angle)
            {
                var newangle, midangle;

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


Here, we define the variable label_quad as a newly created div element, and set its class to "label_quad". Remember, we created the CSS class earlier!
            function placeLabel(piece, angle)
            {
                var newangle, midangle;
                var label_quad = document.createElement("div");
                label_quad.className = "label_quad";

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


Here, we append label_quad as a child within label_quad_wrapper. Then we create a span element and assign it to the variable label. We give label a class of data_label (which we've already defined in the CSS), set label's text to the stat property of the piece object in the parameter, and append label as a child within label_quad!
            function placeLabel(piece, angle)
            {
                var newangle, midangle;
                var label_quad = document.createElement("div");
                label_quad.className = "label_quad";

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.appendChild(label_quad);

                var label = document.createElement("span");
                label.className = "data_label";
                label.innerHTML = piece.stats;
                label_quad.appendChild(label);

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


See that? You've just placed your first label. Change the value in the Seasons drop-down list to "2009 to 2010". The number should change to 18! Fernando Torres scored 18 goals in the 2009 to 2010 season, and naturally it's the highest stat of that season, so it comes first. However, we want it to appear in the middle of the slice. 18 out of 51 total goals (count all the goals scored in that season as a whole) is almost a third of the pie, so the number 18 should appear much further along.


Here, newangle is set to the ending angle of the last piece, plus the size of the current piece.
            function placeLabel(piece, angle)
            {
                var newangle, midangle;
                var label_quad = document.createElement("div");
                label_quad.className = "label_quad";

                newangle = angle.newangle + parseInt(piece.piece);

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.appendChild(label_quad);

                var label = document.createElement("span");
                label.className = "data_label";
                label.innerHTML = piece.stats;
                label_quad.appendChild(label);

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


midangle is the angle right in the middle of the current slice. We define midangle as the size of the current slice, piece.piece, divided by two, and added to the previous angle, angle.newangle.  Then we rotate label_quad by midangle.
            function placeLabel(piece, angle)
            {
                var newangle, midangle;
                var label_quad = document.createElement("div");
                label_quad.className = "label_quad";

                newangle = angle.newangle + parseInt(piece.piece);
                midangle = ((parseInt(piece.piece)) / 2) + angle.newangle;

                label_quad.style.WebkitTransform = "rotate(" + midangle + "deg)";
                label_quad.style.transform = "rotate(" + midangle + "deg)";

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.appendChild(label_quad);

                var label = document.createElement("span");
                label.className = "data_label";
                label.innerHTML = piece.stats;
                label_quad.appendChild(label);

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


There. Since 18 would occupy roughly a third of the pie (120 degrees), half of that is about 60 degrees, which is what we've rotated the div by!


Now let's add another label.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
                var pieces = getPieces(sorted);

                var lastPieceAngle = {"prevangle": 0, "newangle": 0};

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.innerHTML = "";

                lastPieceAngle = placeLabel(pieces[0], lastPieceAngle);
                lastPieceAngle = placeLabel(pieces[1], lastPieceAngle);
            }


Your next label is a 9. It takes up a fifth of the total of 51 goals, so its size is about 20% of the pie (roughly 70 degrees) and half of that is abut 35 degrees! 35 degrees from the last angle of 120 (or so) degrees, that is. If math isn't your thing or my explanation just sucks, worry not, The next part of this tutorial will make things visually clear.


Now, adding all these labels one by one is a pain in the ass, so let's put this in a For loop.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;
                var row = document.getElementById("ddlRow").value;

                var nonzero = getNonZero(row, stat);
                var sorted = getSorted(nonzero);
                var pieces = getPieces(sorted);

                var lastPieceAngle = {"prevangle": 0, "newangle": 0};

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.innerHTML = "";

                for (var i = 0; i < pieces.length; i++)
                {
                    lastPieceAngle = placeLabel(pieces[i], lastPieceAngle);
                }
            }


There you go! But hell, it's devilishly hard to read the numbers when they're all rotated, so...


...let's rotate the label the other direction by the same number of degrees!
            function placeLabel(piece, angle)
            {
                var newangle, midangle;
                var label_quad = document.createElement("div");
                label_quad.className = "label_quad";

                newangle = angle.newangle + parseInt(piece.piece);
                midangle = ((newangle - angle.newangle) / 2) + angle.newangle;

                label_quad.style.WebkitTransform = "rotate(" + midangle + "deg)";
                label_quad.style.transform = "rotate(" + midangle + "deg)";

                var label_quad_wrapper = document.getElementById("label_quad_wrapper");
                label_quad_wrapper.appendChild(label_quad);

                var label = document.createElement("span");
                label.className = "data_label";
                label.innerHTML = piece.stats;
                label.style.WebkitTransform = "rotate(" + (midangle * -1) + "deg)";
                label.style.transform = "rotate(" + (midangle * -1) + "deg)";
                label_quad.appendChild(label);

                return {"prevangle" : angle.newangle, "newangle": newangle};
            }


And we'll have to make the CSS class data_label rotatable by giving it a display property of inline-block. While we're there, let's make it rotate by its exact center by setting the transform-origin property.
            .data_label
            {
                color: #000000;
                font-weight: bold;
                font-family: verdana;
                font-size: 0.8em;
                position: relative;
                display: inline-block;
                -webkit-transform-origin: 50% 50%;
                transform-origin: 50% 50%;
            }


There you go.


Just set this back...
div {outline: 0px solid #FFAA00;}


Gettin' clear as shit!


Next

Time to start coloring the pie chart. You don't want to miss this!

No comments:

Post a Comment