Tuesday, 4 July 2017

Web Tutorial: The Line Graph (Part 2/2)

Yo, dudes.

We last left off with what looked like a really messy line graph layout. This was deliberate, because now with all the individual column groups separated out, it's easier to test your data. Things are going to get a little math-heavy.

Let's begin by modifying the displayData() function. After deriving max, we declare a whole series of variables - percentage, nextpercentage, actual, nextactual, margintop and nextmargintop. These are the variables needed for node and line manipulation.

Then there are the variables node, line and tx. These are the variables that are objects. More on that later.

For now, create a nested For loop. The outer loop should iterate through the cols array of the graphdata object while the inner loop should iterate through the rows array of the graphdata object.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {

                    }
                }

                displayScale(max);
            }


Now, right in the inner loop, set actual by running the getStatistic() function. The function is pretty much identical to the one we created for the bar chart...
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                    }
                }

                displayScale(max);
            }

            function getStatistic(data,row,stat)
            {
                var temp;

                temp = data.stats.filter(function (x) {return x.year==row;});

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

                return 0;
            }


...except for this. This is necessary because in some cases, data would be undefined.
            function getStatistic(data,row,stat)
            {
                if (data==undefined) return 0;

                var temp;

                temp = data.stats.filter(function (x) {return x.year==row;});

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

                return 0;
            }


Now that we have the value of the statistic set to the variable actual, just as for the bar chart, we define percentage. margintop will use percentage to determine how many pixels the node needs to be from the top, going by a height of 80% of 500 pixels. Minus 5 because the node is 10 pixels in height and we want its center to be at the level specified, see?
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                        percentage = 100 - ((actual/max) * 100);
                        margintop = ((percentage/100 * (0.8*500)) -5);
                    }
                }

                displayScale(max);
            }


And the we set the variable node to the element whose id corresponds to the i and j value (remember setting ids for those nodes?), set the margin-top property using the margintop variable, and display the value of actual using the innerHTML property for good measure.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                        percentage = 100 - ((actual/max) * 100);
                        margintop = ((percentage/100 * (0.8*500)) -5);

                        node = document.getElementById("node_" + i + "_" +j);
                        node.style.marginTop = margintop + "px";
                        node.innerHTML = actual;
                    }
                }

                displayScale(max);
            }


Now, you see that the nodes have shifted. The values displayed in each node should correspond with the scale.


Next, you want to start rotating the lines so that they join to the next node. First, we need an If block to ensure that we only attempt this for columns preceding the final column (which only has a node and no line to manipulate).
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                        percentage = 100 - ((actual/max) * 100);
                        margintop = ((percentage/100 * (0.8*500)) -5);

                        node = document.getElementById("node_" + i + "_" +j);
                        node.style.marginTop = margintop + "px";
                        node.innerHTML = actual;

                        if (j<graphdata.rows.length-1)
                        {

                        }
                    }
                }

                displayScale(max);
            }


Before going further, I want to bring to your attention Pythagoras's Theorem. Your objective is to rotate the magenta line so that the end touches the next node. The length of the line will need to be increased as well, naturally. So as per the diagram below, we need the angle x and we need the hypotenuse, h. a is definitely 200 pixels due to the specification of the col_container CSS class.

The length of o needs to be derived. It's not as simple as 24 - 14 = 10. We have to take into account that the number of pixels is based on a height of 80% of 500 pixels!

But basically, once you have o and a, you can get h and x.


We'll begin by getting the statistic of the next node, running getStatistic() and setting the returned value to the variable nextactual. nextpercentage and nextmargintop are then derived the same way as percentage and margintop.

Then we set the variable tx by running getLineTransform(), which will return an object. We will pass in the difference between margintop and nextmargintop, and the number 200 as arguments. And of course, we'll create the getLineTransform() function.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                        percentage = 100 - ((actual/max) * 100);
                        margintop = ((percentage/100 * (0.8*500)) -5);

                        node = document.getElementById("node_" + i + "_" +j);
                        node.style.marginTop = margintop + "px";
                        node.innerHTML = actual;

                        if (j<graphdata.rows.length-1)
                        {
                            nextactual = getStatistic(graphdata.cols[i],graphdata.rows[j+1],stat);
                            nextpercentage = 100 - ((nextactual/max) * 100);
                            nextmargintop = ((nextpercentage/100 * (0.8*500)) -5);

                            tx = getLineTransform((margintop-nextmargintop),200);
                        }
                    }
                }

                displayScale(max);
            }

            function getLineTransform(o,a)
            {

            }


For the getLineTransform() function, we first declare the variables angle and hypotenuse. These will be returned in an object.
            function getLineTransform(o,a)
            {
                var angle, hypotenuse;

                return {"rotate":angle,"width":hypotenuse+100};
            }


If o is zero, this means that the next node has the same value as the previous node. This in turn means that the line does not need to be rotated, and the length of the line is the same as a.
            function getLineTransform(o,a)
            {
                var angle, hypotenuse;

                if (o==0)
                {
                    hypotenuse = a;
                    angle = 0;
                }
                else
                {

                }

                return {"rotate":angle,"width":hypotenuse+100};
            }


Otherwise, derive hypotenuse by using the hypot() method of the Math object. It will give you the square root of the sum of all arguments squared. Remember to use the absolute value of o instead.
            function getLineTransform(o,a)
            {
                var angle, hypotenuse;

                if (o==0)
                {
                    hypotenuse = a;
                    angle = 0;
                }
                else
                {
                    hypotenuse = Math.hypot(Math.abs(o),a).toFixed(0);
                }

                return {"rotate":angle,"width":hypotenuse+100};
            }


Then derive angle. The tangent of angle is o divided by a, so you can derive angle by getting the arctangent of the result of dividing o by a. Again, remember to use the absolute value of o. The result is in radians, and we need to convert it to degrees using the getDegrees() function.
            function getLineTransform(o,a)
            {
                var angle, hypotenuse;

                if (o==0)
                {
                    hypotenuse = a;
                    angle = 0;
                }
                else
                {
                    hypotenuse = Math.hypot(Math.abs(o),a).toFixed(0);

                    angle = getDegrees(Math.atan((Math.abs(o)/a))).toFixed(1);
                }

                return {"rotate":angle,"width":hypotenuse+100};
            }

            function getDegrees(radians) {
                return radians * 180 / Math.PI;
            };


Now, if o is greater than 0, it means that the value of the next node is higher and therefore the line needs to be rotated counter-clockwise rather than clockwise. So make sure the value is negative.
            function getLineTransform(o,a)
            {
                var angle, hypotenuse;

                if (o==0)
                {
                    hypotenuse = a;
                    angle = 0;
                }
                else
                {
                    hypotenuse = Math.hypot(Math.abs(o),a).toFixed(0);

                    angle = getDegrees(Math.atan((Math.abs(o)/a))).toFixed(1);

                    if (o>0)
                    {
                        angle = angle * -1;
                    }
                }

                return {"rotate":angle,"width":hypotenuse+100};
            }

            function getDegrees(radians) {
                return radians * 180 / Math.PI;
            };


Back to the displayData() function, we access the element that corresponds to the id derived from i and j, assign it to the variable line, then set the rotation and length of line using the object tx.
            function displayData()
            {
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(stat);
                var percentage,nextpercentage,actual,nextactual,margintop,nextmargintop;
                var node,line,tx;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    for (var j=0;j<graphdata.rows.length;j++)
                    {
                        actual = getStatistic(graphdata.cols[i],graphdata.rows[j],stat);
                        percentage = 100 - ((actual/max) * 100);
                        margintop = ((percentage/100 * (0.8*500)) -5);

                        node = document.getElementById("node_" + i + "_" +j);
                        node.style.marginTop = margintop + "px";
                        node.innerHTML = actual;

                        if (j<graphdata.rows.length-1)
                        {
                            nextactual = getStatistic(graphdata.cols[i],graphdata.rows[j+1],stat);
                            nextpercentage = 100 - ((nextactual/max) * 100);
                            nextmargintop = ((nextpercentage/100 * (0.8*500)) -5);

                            tx = getLineTransform((margintop-nextmargintop),200);

                            line = document.getElementById("line_" + i + "_" + j);
                            line.style.width = tx.width + "px";
                            line.style.WebkitTransform = "rotate(" + tx.rotate + "deg)";
                            line.style.transform = "rotate(" + tx.rotate + "deg)";
                        }
                    }
                }

                displayScale(max);
            }


And there you are!


Change this back,
            .col_container
            {
                height:100%;
                width:95%;
                float:left;
                margin-right:-100%;
            }


Taking shape now...


And of course, do this...
div {outline:0px solid #FFAA00;}


Here's your line graph! Try changing the value in the drop-down list. Do the lines and nodes shift?


There was even more math than the last one, and we even had to pull out good ol' Pythagoras. It's a hoot though - we got the job done! There's plenty of room for improvement, and plenty of variations on this idea, so have at it.

Wasn't that easy? There's node-thing to it,
T___T

No comments:

Post a Comment