Thursday 18 January 2018

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

Welcome back for the second part of this tutorial. Now that you've put up the labels, a lot of the functionality is already there and we just need to leverage on it. Much of this web tutorial is going to resemble the Wheel of Fortune we made a couple years back - except that instead of equal-sized pie slices, the pie slices here will likely be of different sizes.

First though, modify your CSS. We need to put the outlines back, but only for stuff we're adding into the pie_container div.
#pie_container, #pie_container div {outline: 1px solid #FFAA00;}


Got that? Great.


Here, add this code to clear the quad_wrapper_left and quad_wrapper_right divs. They're already empty right now, so you won't see any change. Also, we will define quads as an empty array and lastQuadAngle as an object, much the same way we did with pieces and lastPieceAngle.
            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 quads=[];
                var lastQuadAngle = {"prevangle":0,"newangle":0};

                var quad_wrapper;
                quad_wrapper = document.getElementById("quad_wrapper_right");
                quad_wrapper.innerHTML = "";
                quad_wrapper.style.backgroundColor = "";
                quad_wrapper = document.getElementById("quad_wrapper_left");
                quad_wrapper.innerHTML = "";

                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);
                }
            }


Now, in the For loop, we'll want to start presenting the data in the pieces array. First, set quads to the returned value of the getQuads() function, passing in the current value of pieces as an argument.
                for (var i = 0; i < pieces.length; i++)
                {
                    quads = getQuads(pieces[i]);

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


And create the getQuads() function.

Why is this necessary? Don't we already have the pieces?

Well, see, the pieces array give us the pie slice angles, colors and statistics. But we're dealing with HTML elements here and basically they're all squares. And each div can give you 90 degrees at most. Thus, if your angle is more than 90 degrees, you'll need to add more quads!

For example:

You have a 150 degree pie slice. You need a 90 degree quad, and another 90 degree quad rotated by 150 - 90 = 60 degrees to form the full 150 degrees.


You have a 190 degree pie slice. You need a 90 degree quad, another 90 degree quad, and a third 90 degree quad  rotated by 190 - 90 - 90 = 10 degrees to form the full 190 degrees.


You have a 285 degree pie slice. You need a 90 degree quad, another 90 degree quad a third 90 degree quad and a fourth 90 degree quad  rotated by 285 - 90 - 90 - 90 = 15 degrees to form the full 285 degrees.

OK, so back to the getQuads() function...

Here, quads is defined as an empty array, rem (short for "remainder") is the piece property of the piece object passed in, and quads is returned at the end of the function.
            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.style.WebkitTransform = "rotate(" + (midangle * -1) + "deg)";
                label.style.transform = "rotate(" + (midangle * -1) + "deg)";
                label_quad.appendChild(label);

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

            function getQuads(piece)
            {
                var quads = [];
                var rem = piece.piece;

                return quads;
            }

            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;
            }


Now, this section of code will last as long as the value of rem is more than 0. With each iteration, rem will be decremented by the maximum size of one quad, which is 90 degrees.
            function getQuads(piece)
            {
                var quads = [];
                var rem = piece.piece;

                while (rem>0)
                {               
                    rem = rem - 90;
                }

                return quads;
            }


Now we check if rem is less than or equal to 90 degrees. If so, push an object into the quads array with the color and the value of rem. If not, push a full quad into quads.
            function getQuads(piece)
            {
                var quads = [];
                var rem = piece.piece;

                while (rem>0)
                {                   
                    if ((rem <= 90)
                    {
                        quads.push
                        (
                            {
                                "color":piece.color,
                                "piece":rem
                            }
                        );
                    }
                    else
                    {
                        quads.push
                        (
                            {
                                "color":piece.color,
                                "piece":90
                            }
                        );
                    }

                    rem = rem - 90;
                }

                return quads;
            }


So if you had a piece that was 300 degrees and the color was #FF00FF, you would pass this into the getQuads() function...
    {
        "color": "#FF00FF",
        "piece": 300
    }


... and the quads array would look like this when you were done.
[
    {
        "color": "#FF00FF",
        "piece": 90   
    },
    {
        "color": "#FF00FF",
        "piece": 90   
    },
    {
        "color": "#FF00FF",
        "piece": 90   
    },
    {
        "color": "#FF00FF",
        "piece": 30   
    }
]



Back to the displayData() function, go to the For loop. Iterate through the quads array.
                for (var i = 0; i < pieces.length; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {

                    }

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


Set lastQuadAngle by running the placeQuad() function. Pass in the formula (1000 - (i * 10)) - j, the current element in quads, and the value of lastQuadAngle (which is initially 0). The formula is meant to determine the z-index property of each quad so they don't overlap in an awkward way.
                for (var i = 0; i < pieces.length; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


Now this is the final function we will be working on. The end is in sight! Create the placeQuad() function. It will accept three parameters - zindex, quad, and angle.
            function placeQuad(zindex, quad, angle)
            {

            }

            function getQuads(piece)
            {
                var quads = [];
                var rem = piece.piece;

                while (rem>0)
                {                   
                    if ((rem <= 90)
                    {
                        quads.push
                        (
                            {
                                "color":piece.color,
                                "piece":rem
                            }
                        );
                    }
                    else
                    {
                        quads.push
                        (
                            {
                                "color":piece.color,
                                "piece":90
                            }
                        );
                    }

                    rem = rem - 90;
                }

                return quads;
            }


First, declare the variable newangle. Then create a div element and set it to another variable, pie_quad. Set its properties using the quad object's color property, and zindex. It will be styled using the CSS class pie_quad.
            function placeQuad(zindex, quad, angle)
            {
                var newangle;
                var pie_quad = document.createElement("div");
                pie_quad.className = "pie_quad";
                pie_quad.style.backgroundColor = quad.color;
                pie_quad.style.zIndex = zindex;
            }


Let's create the CSS class pie_quad. Since it's a square that will take up half the area of its parent quad_wrapper (which in turn will take half half of the square div pie_container), width is 100% and height is 50%. margin-bottom is set to negative 100% because there will be multiple divs styled by pie_quad inside quad_wrapper, and they all have to overlap. We explicitly define the position property as relative and set the rotation point to the bottom left.
            .quad_wrapper
            {
                width: 50%;
                height: 100%;
                float: left;
            }

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

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


Now back to the placeQuad() function. Set newangle to the previous angle (the newangle property of the angle object) plus the value of the quad object's piece property. Then rotate pie_quad by that value minus 90 degrees.

Why?

See, if the new finishing angle is exactly 90 degrees, then we wouldn't need to rotate it at all. If it's more than 90 degrees, then we only need to rotate pie_quad by that difference, for the pie_quad to end up at exactly newangle degrees. If it's less than 90 degrees, then we rotate counter-clockwise.

Also, ensure that the function returns an object containing the previous and newest angles.
            function placeQuad(zindex, quad, angle)
            {
                var newangle;
                var pie_quad = document.createElement("div");
                pie_quad.className = "pie_quad";
                pie_quad.style.backgroundColor = quad.color;
                pie_quad.style.zIndex = zindex;

                newangle = angle.newangle + parseInt(quad.piece);
                pie_quad.style.WebkitTransform = "rotate(" + (newangle - 90) + "deg)";
                pie_quad.style.transform = "rotate(" + (newangle - 90) + "deg)";

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


Now, if the previous angle was more than 180 degrees, then we append pie_quad to quad_wrapper_left. If not, we append it to quad_wrapper_right.
            function placeQuad(zindex, quad, angle)
            {
                var newangle;
                var pie_quad = document.createElement("div");
                pie_quad.className = "pie_quad";
                pie_quad.style.backgroundColor = quad.color;
                pie_quad.style.zIndex = zindex;

                newangle = angle.newangle + parseInt(quad.piece);
                pie_quad.style.WebkitTransform = "rotate(" + (newangle - 90) + "deg)";
                pie_quad.style.transform = "rotate(" + (newangle - 90) + "deg)";

                var quad_wrapper;

                if (angle.newangle >= 180)
                {
                    quad_wrapper = document.getElementById("quad_wrapper_left");
                    quad_wrapper.appendChild(pie_quad);
                }
                else
                {
                    quad_wrapper = document.getElementById("quad_wrapper_right");
                    quad_wrapper.appendChild(pie_quad);
                }

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


We're going to test our code. Back to the displayData() function, change this so that only the first element of pieces and the first element of quads gets processed.
                for (var i = 0; i <= 0; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j <= 0 ; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


Look up the goals for 2009 to 2010. The first piece represents Fernando Torres, who scored the most number of goals at 18 out of a total of 51. This will take up more than a quarter of the circle, so why is the piece at exactly one quarter? That's because we've only processed one quad out of that piece!


Change the code to process all the quads of the first pieces element.
                for (var i = 0; i <= 0; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


This piece has two quads - one from 0 to 90 degrees and the other at 37 to 127 degrees. So now we have a piece that appears to go from 0 to 127 degrees!


Now let's process the next piece.
                for (var i = 0; i <= 1; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


Next is Dirk Kuyt at 9 goals. Since 9 is less than a quarter of 51, there is only one quad and it should take up 64 degrees. We place the quad, then rotate it clockwise until the final finishing angle is (127 + 64 = 191) degrees, which means we have to rotate it by (191 - 90 = 101) degrees. Since its z-index property is programmatically lower than the previous quad's, the rest of the yellow square gets cut off. Which is exactly what we want!


Let's push our luck with the next piece...
                for (var i = 0; i <= 2; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


Steven Gerrard also scored 9 goals, so his piece should be the same size as Dirk Kuyt's. But holy crap, what happened here?


Remember that if the last angle is more than 180 degrees, then the piece gets appended to quad_wrapper_left instead of quad_wrapper_right! So we need to add an adjustment to the CSS...

If the div styled by pie_quad is inside the quad_wrapper_left div, we push it 100% to the right!.
            .quad_wrapper
            {
                width: 50%;
                height: 100%;
                float: left;
            }

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

            #quad_wrapper_left .pie_quad
            {
                margin-left: 100%;
            }


There ya go. But it's still not correct because the divs aren't the same size. That's because the yellow div is overlapping the brown one.


So here we ensure that nothing overflows from divs styled with quad_wrapper.
            .quad_wrapper
            {
                width: 50%;
                height: 100%;
                float: left;
                overflow: hidden;
            }


This is still incorrect because now the brown div is larger than the yellow div, and they're supposed to be of the same size! The end angle is correct though.


What we need here is to handle the case where the quad starts before 180 degrees and ends after.
            function placeQuad(zindex, quad, angle)
            {
                var newangle;
                var pie_quad = document.createElement("div");
                pie_quad.className = "pie_quad";
                pie_quad.style.backgroundColor = quad.color;
                pie_quad.style.zIndex = zindex;

                newangle = angle.newangle + parseInt(quad.piece);
                pie_quad.style.WebkitTransform = "rotate(" + (newangle - 90) + "deg)";
                pie_quad.style.transform = "rotate(" + (newangle - 90) + "deg)";

                var quad_wrapper;

                if (angle.newangle >= 180)
                {
                    quad_wrapper = document.getElementById("quad_wrapper_left");
                    quad_wrapper.appendChild(pie_quad);
                }
                else
                {
                    quad_wrapper = document.getElementById("quad_wrapper_right");
                    quad_wrapper.appendChild(pie_quad);

                    if (newangle >= 180)
                    {

                    }
                }

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


Make a div, call it pie_quad_copy and style it identically to pie_quad. Then rotate it and append to quad_wrapper_left.
            function placeQuad(zindex, quad, angle)
            {
                var newangle;
                var pie_quad = document.createElement("div");
                pie_quad.className = "pie_quad";
                pie_quad.style.backgroundColor = quad.color;
                pie_quad.style.zIndex = zindex;

                newangle = angle.newangle + parseInt(quad.piece);
                pie_quad.style.WebkitTransform = "rotate(" + (newangle - 90) + "deg)";
                pie_quad.style.transform = "rotate(" + (newangle - 90) + "deg)";

                var quad_wrapper;

                if (angle.newangle >= 180)
                {
                    quad_wrapper = document.getElementById("quad_wrapper_left");
                    quad_wrapper.appendChild(pie_quad);
                }
                else
                {
                    quad_wrapper = document.getElementById("quad_wrapper_right");
                    quad_wrapper.appendChild(pie_quad);

                    if (newangle >= 180)
                    {
                        var pie_quad_copy = document.createElement("div");
                        pie_quad_copy.className = "pie_quad";
                        pie_quad_copy.style.backgroundColor = quad.color;
                        pie_quad_copy.style.zIndex = zindex;


                        pie_quad_copy.style.WebkitTransform = "rotate(" + (newangle - 90) + "deg)";                         pie_quad_copy.style.transform = "rotate(" + (newangle - 90) + "deg)";

                        quad_wrapper = document.getElementById("quad_wrapper_left");
                        quad_wrapper.appendChild(pie_quad_copy);
                    }
                }

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


Now they're equal.


Do the rest!
                for (var i = 0; i < pieces.length; i++)
                {
                    quads = getQuads(pieces[i]);

                    for (var j = 0; j < quads.length; j++)
                    {
                        lastQuadAngle = placeQuad((1000 - (i * 10)) - j, quads[j], lastQuadAngle);
                    }

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


Reset this!
#pie_container, #pie_container div {outline: 0px solid #FFAA00;}


You have a pie chart! Try changing the values of the drop-down lists. Does the data change?


Final Notes

You may have noticed two things.

Firstly, it's always largest to smallest piece. Secondly, the statistic always starts and ends with 0 degrees.

These are deliberate. We purposely ordered the elements and ensured that everything begins from angle zero, and not from anywhere else. So we will always be certain that the next piece is equal-sized or smaller, and if the current piece is more than 180 degrees in size, there is mathematically no way that the next piece is not smaller. So we cut off that case right off the bat.

What a tutorial, eh? I quad enjoyed it!
T___T

No comments:

Post a Comment