Tuesday 26 May 2020

Spot The Bug: It's all about the breaks

Bug-hunting season is always on, and today we've got something that might not quite qualify as a bug since it appears to be working, unless you're super anal like I sometimes can be.
Give it up for Spot The Bug!

Still, it's a good lesson to learn, and will come in useful as a general rule of thumb. You'll see what I mean in a minute.

nl2br() is a PHP function that purportedly replaces new lines with HTML breaks. And we usually use it for cases where the raw string input is straight from the textbox and we want the text to display nicely on a web page. So let's say I had code like this.
<!DOCTYPE html>
<html>
<head>
    <title>nl2br test</title>
</head>
<body>
    <?php
    $str = "";

    if (isset($_POST["btnSubmit"]))
    {
        $str = $_POST["txtString"];
    }

    ?>

    <?php
        echo nl2br($str);
    ?>
    <hr />

    <h1>TEST FORM</h1>
    <form method="POST">
        <textarea name="txtString" cols="30" rows="10"><?php echo $str; ?></textarea>
        <button name="btnSubmit">Test</button>
    </form>
</body>

</html>


There's a form containing a text box and a submit button. And let's say I input this text and click the button.


With the nl2br() function applied, it looks like this.


Now my intention here was to write a function that worked just like nl2br(), just for a start, with the intention to add on to it later. Don't ask me why. Seems like a silly thing to do in hindsight, but there you go.
<!DOCTYPE html>
<html>
    <head>
        <title>nl2br test</title>
    </head>
    <body>
        <?php
        $str = "";

        if (isset($_POST["btnSubmit"]))
        {
            $str = $_POST["txtString"];
        }

        function replaceBreaks($str)
        {
            return str_replace("\r\n", "<br />", $str);
        }
        ?>

        <?php
            echo nl2br($str);
        ?>
        <hr />

        <h1>TEST FORM</h1>
        <form method="POST">
            <textarea name="txtString" cols="30" rows="10"><?php echo $str; ?></textarea>
            <button name="btnSubmit">Test</button>
        </form>
    </body>
</html>


Not only that, I wanted to print both the output from nl2br() and my function, and do a direct comparison using a conditional. Kind of like a very rudimentary unit test.
<!DOCTYPE html>
<html>
    <head>
        <title>nl2br test</title>
    </head>
    <body>
        <?php
        $str = "";

        if (isset($_POST["btnSubmit"]))
        {
            $str = $_POST["txtString"];
        }

        function replaceBreaks($str)
        {
            return str_replace("\r\n", "<br />", $str);
        }
        ?>

        <h1>RAW STRING</h1>
        <?php
            echo $str;
        ?>
        <hr />

        <h1>NL2BR</h1>
        <?php
            echo nl2br($str);
        ?>
        <hr />

        <h1>NEW FUNCTION</h1>
        <?php
            echo replaceBreaks($str);
        ?>
        <hr />

        <h1>NEW FUNCTION = NL2BR?</h1>
        <?php
            echo (nl2br($str) == replaceBreaks($str) ? "PASS" : "FAIL");
        ?>
        <hr />

        <h1>TEST FORM</h1>
        <form method="POST">
            <textarea name="txtString" cols="30" rows="10"><?php echo $str; ?></textarea>
            <button name="btnSubmit">Test</button>
        </form>
    </body>
</html>


And whoops! It said "FAIL". But why? The output looked identical.


What went wrong

And yes, it did appear to be identical. Appear being the operative word. When I viewed the source, this is what I saw.

The br tags were inserted, but the first set of output still had line breaks!


Why it went wrong

That was when I started taking a closer look at the nl2br() function, right at the source of truth. Most people are under the impression that nl2br() replaces carriage returns and line breaks with br tags.

But nope. This is what nl2br() actually does.
Returns string with <br /> or <br> inserted before all newlines (\r\n, \n\r, \n and \r).


That meant that instead of this...
Best regards<br />John Smith<br />CEO


...I was getting this!
Best regards<br />\r\nJohn Smith<br />\r\nCEO


How I fixed it

This was what I should have been doing for my function.
    function replaceBreaks($str)
    {
        return str_replace("\r\n", "<br />\r\n", $str);
    }


There it was... that beautiful PASS.


Moral of the story

There are a couple takeaways from this.

Just because text output looks correct on a web page, it doesn't necessarily mean the output is correct. It just means that the output is displayed correctly, or in this case, acceptably.

Also, I might have mentioned this before, but it bears repeating. When involving any built-in function, always be very clear what it actually does. I may have been the idiot who thought nl2br() only replaced carriage returns and new lines with HTML breaks, but I wasn't the only one.

Them's the breaks, kiddo!
T___T

Wednesday 20 May 2020

Five ways to print a string ten times

Programming can be fun because there's not just one way to do anything. With any programming language, at your disposal, you have a set of tools - loops, variables, arrays, objects and functions - and with them, a programmer can solve problems. Not only can problems be solved, those problems can be solved in a multitude of ways using these same tools.

Today will be one such example. The problem is simple - print your name 10 times. I will walk you through multiple solutions in PHP, each markedly different from the last, from the most obvious to the most intricate.

1. Straight up printing

Yes, this is a solution. I did say "most obvious", but this is clumsy, inelegant, and will quickly fail you if you need to scale up.
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";
echo "TeochewThunder<br />";


2. Looping

Here, I present multiple solutions, but they all use basically same method - a looping construct. Safe enough, but boring. There are even more variations in a language like Ruby, with Until and Do-Until loops, but we won't cover them here.

A For loop
for ($i = 0; $i < 10; $i++)
{
    echo "TeochewThunder<br />";
}


A While loop
$i = 0;

while ($i < 10)
{
   echo "TeochewThunder<br />";
   $i ++;  
}


A Do-While loop
$i = 0;

do
{
   echo "TeochewThunder<br />";
   $i ++;  
} while ($i < 10)


3. A Foreach loop

This one uses an array containing 10 elements, all of them your name. Then you just iterate through the array with a simple Foreach loop. It's great if the elements in the array are different from each other, but pretty silly if they're all the same.

$arr = array("TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder", "TeochewThunder");

foreach ($arr as $name)
{
    echo $name . "<br />";
}


4. Recursion

Now this is interesting. Here, we write a function that calls itself repeatedly, each time increasing the value of the argument i, and only stops when i is equal to max. Recursion is a useful software engineering trick, but perhaps overkill for this use.

printName("TeochewThunder<br />", 0, 10);

function printName($name, $i, $max)
{
    if ($i < $max)
    {
        echo $name;
        printName($name, $i + 1, 10);
    }
    else
    {
        return;
    }
}


5. The Replace trick

This is a nice one. Again, it's overkill, but so sly! You have a string of 10 asterisks, and simply replace each asterisk with the string you want with a basic str_replace() function call, then print the result. And if you need to print more times? Simply run str_replace() before the final str_replace() call, substituting each asterisk with multiple asterisks.

More for if you want to challenge someone to print the same string multiple times without looping.
$str = "**********";
$str = str_replace("*", "TeochewThunder<br />", $str);

echo $str;


And there you have it...

... five ways to accomplish the same thing. Programming food for thought!

**********,
T___T

Wednesday 13 May 2020

Web Tutorial: D3 Line Chart (Part 2/2)

In the last part, we changed the naming conventions, the labels and the drop-down list to reflect our new direction. Now let's truly turn this sucker into a line graph. The scale, legend and filler are fine. It's the actual chart area we need to change.

Get down to the part where we're inserting rect tags into chart. Change them to circle tags.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return height + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return "0em";
})           
.transition()
.duration(500)
.attr("y", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


Since these are circle tags, we no longer want to specify x and y attributes, but rather cx and cy. Also, we will not be specifying width, but rather r, which is the radius. We won't specify height at all, so get rid of it.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("cy", function(d)
{
    return height + "em";
})
.attr("r", function(d)
{
    return (config.dataWidth) + "em";
})
//.attr("height", function(d)
//{
//    return "0em";
//})           
.transition()
.duration(500)
.attr("cy", function(d)
{
    return (height - (d * config.scale)) + "em";
})
//.attr("height", function(d)
//{
//    return (d * config.scale) + "em";
//});


cy is currently fine as it is. But cx needs to be the center of the circle. So we add half of the dataWidth property into the equation.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
    return height + "em";
})
.attr("r", function(d)
{
    return (config.dataWidth) + "em";
})          
.transition()
.duration(500)
.attr("cy", function(d)
{
    return (height - (d * config.scale)) + "em";
});


Just for fun, let's set r to animate as well, starting from 0 to dataWidth.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
    return height + "em";
})
.attr("r", function(d)
{
    return "0em";
})
.transition()
.duration(500)
.attr("cy", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("r", function(d)
{
    return (config.dataWidth) + "em";
});


Also, the CSS needs to be modified. We're no longer styling rect but circle tags.
.lineChartSvg circle
{
    fill: rgba(255, 255, 0, 1);
}


Uh-oh. Guess what we did wrong?


Let's change dataWidth and dataSpacing in the config object.
var config =
{
    "scale": 0.5,
    "dataWidth": 0.5,
    "dataSpacing": 10,
    "scaleWidth": 6,
    "legendHeight": 4,
    "max": 0,
    "mean": 0,


Looking good now!


Time to add lines. Instead of modifying something, we'll be adding new code. We begin with the standard D3 stuff, appending lines into chart. The data is the stats array of the dataSet object, and styling is the CSS class lineChartDataLine which we'll create.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
    return height + "em";
})
.attr("r", function(d)
{
    return "0em";
})
.transition()
.duration(500)
.attr("cy", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("r", function(d)
{
    return (config.dataWidth) + "em";
});

chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");

chart.selectAll("text")
.data(dataSet.stats)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
    return (height - (d * config.scale) - 1) + "em";
})
.text(function(d)
{
    return d;
});


Our motive here is to join all the circle tags with line tags. So we'll specify x1, y1, x2 and y2 attributes.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine")
.attr("x1", function(d)
{

})
.attr("y1", function(d)
{

})
.attr("x2", function(d)
{

})
.attr("y2", function(d)
{

});


But only from the second element in the stats array, will you know what the values of x2 and y2 are. So for the first element in the stats array, the line stays within the first circle and does not extend out to the next circle. That means for x1, we return the same value we did for the circle. We need to use i here, to keep track of the index.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{

})
.attr("x2", function(d)
{

})
.attr("y2", function(d)
{

});


But this applies only if it is not the first element in the stats array. Declare val and set to i. Check if i is greater than 0. If it is, that means we decrement val (to set it to the previous element's index) and use it to calculate the returned result. If it's the first element, there's no previous element, so just use the first element's values.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{

})
.attr("x2", function(d)
{

})
.attr("y2", function(d)
{

});


For y1, we'll also need i. But this will be done slightly differently. If we're dealing with the first element, meaning i is 0, then we'll use use the value of d as what we did for the circle tags.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d)
{

})
.attr("y2", function(d)
{

});


But if it's not the first element, then we have to take the value of the previous element.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d)
{

})
.attr("y2", function(d)
{

});


For x2, we're just using the current element. Note that for x1 and x2, we follow what we did for circles because we want the line to begin and end in the middle of each circle.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{

});


y2 will not require i. It uses the same return value as the circle's cy attribute, without val.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


Don't forget to write the CSS class lineChartDataLine. Make it 2 pixels thick and yellow.
.lineChartLine
{
    stroke: rgba(255, 255, 0, 1);
    stroke-width: 1px;
}  

.lineChartDataLine
{
    stroke: rgba(255, 255, 0, 1);
    stroke-width: 2px;
}

.lineChartFadedLine
{
    stroke: rgba(255, 255, 0, 0.2);
    stroke-width: 1px;


Hey, I still see no lines!

Ah, that's because we already appended a series of faded lines before attempting to append these yellow lines. This won't work a second time with the same tags... or will it?

Let's try this. Instead of putting in "line" as the argument for the selectAll() method, use "line.lineChartDataLine" which basically means select all line tags styled using lineChartDataLine.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


Here are your lines.


Let's animate those lines, shall we?

We'll animate y1 and y2. So let's chain a transition() method and a duration() method with an argument of 500.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.transition()
.duration(500);


Then make a copy of the y1 and y2 specifications.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.transition()
.duration(500)
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (d * config.scale)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


And set the first y1 and y2 specification to return height instead, which will set it right at the bottom of the chart.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                  
    }

    return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{
   return height + "em";
})
.attr("x2", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
   return height + "em";
})
.transition()
.duration(500)
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = dataSet.stats[i - 1];
    }

    return (height - (d * config.scale)) + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


You'll see this animates quite nicely when you select different values from the drop-down lists!


But you know, don't take my word for it. Here's a demo!




Final notes

This web tutorial was way shorter than it could have been. You'll see that this code was written in a way that made modification easy.

I can't take all the credit of course. D3 certainly facilitates easy maintenance.

Laying it on the line for you!
T___T


Sunday 10 May 2020

Web Tutorial: D3 Line Chart (Part 1/2)

Following the D3 web tutorial in March, it's time to naturally progress to line charts in D3. This web tutorial will involve a lot less than the previous one because we'll be reusing a lot of stuff.

That's right, I won't be walking you through the layout and stuff. If you need that, go back to the first D3 tutorial. We will be taking code from there wholesale and modifying it to give us a line chart. How cool is that?!

So let's copy the entire code here. First of all, let's change everything that says "bar" to "line". This will serve to keep naming conventions straight and avoid confusion.
<!DOCTYPE html>
<html>
    <head>
        <title>D3 Line Chart</title>

        <style>
            body
            {
                font-size: 12px;
                font-family: arial;
            }

            .lineDashboard
            {
                height: 2em;
                width: 100%;
                text-align: center;
            }

            .lineChart
            {
                outline: 1px solid #000000;
                background-color: rgba(200, 0, 0, 1);
            }

            .lineChartContainer
            {
                margin: 0 auto 0 auto;
            }

            .lineScaleSvg
            {
                width: 5em;
                height: 20em;
                float: left;
                background-color: rgba(0, 255, 0, 0);
            }

            .lineScaleSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: end;
                font-size: 0.5em;
            }

            .lineChartSvg
            {
                width: 20em;
                height: 20em;
                float: left;
                background-color: rgba(0, 0, 255, 0);
            }

            .lineChartSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: middle;
                font-weight: bold;
            } 

            .lineChartSvg rect
            {
                fill: rgba(255, 255, 0, 1);
            }

            .lineFillerSvg
            {
                width: 5em;
                height: 3em;
                float: left;
                background-color: rgba(0, 0, 0, 0);
            }

            .lineLegendSvg
            {
                width: 20em;
                height: 3em;
                float: left;
                background-color: rgba(255, 0, 0, 0);
            }

            .lineLegendSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: middle;
                font-weight: bold;
            } 

            .lineChartDataMean
            {
                stroke: rgba(0, 0, 0, 1);
                stroke-width: 1px;
                stroke-dasharray: 1, 5;
            } 

            .lineChartLine
            {
                stroke: rgba(255, 255, 0, 1);
                stroke-width: 1px;
            }  

            .lineChartFadedLine
            {
                stroke: rgba(255, 255, 0, 0.2);
                stroke-width: 1px;
            }      
        </style>

        <script src="https://d3js.org/d3.v3.min.js"></script>
    </head>

    <body>
        <div class="lineChartContainer">
            <div class="lineDashboard">
                <select id="ddlYear">

                </select>

                <select id="ddlStat">

                </select>
            </div>

            <div class="lineChart">
                <svg class="lineScaleSvg">

                </svg>

                <svg class="lineChartSvg">

                </svg>

                <br style="clear:both"/>

                <svg class="lineFillerSvg">

                </svg>

                <svg class="lineLegendSvg">

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

        <script>
        var graphData =
        {
            "cols":
            [
                {
                    "title": "Adam Lallana",
                    "stats":
                    [
                        {"year": 2015, "goals": 7, "appearances": 49},
                        {"year": 2016, "goals": 8, "appearances": 35},
                        {"year": 2017, "goals": 0, "appearances": 15},
                        {"year": 2018, "goals": 0, "appearances": 16}
                    ]
                },
                {
                    "title": "Sadio Mané",
                    "stats":
                    [
                        {"year": 2016, "goals": 13, "appearances": 29},
                        {"year": 2017, "goals": 20, "appearances": 44},
                        {"year": 2018, "goals": 26, "appearances": 50}
                    ]
                },
                {
                    "title": "Roberto Firminho",
                    "stats":
                    [
                        {"year": 2015, "goals": 11, "appearances": 49},
                        {"year": 2016, "goals": 12, "appearances": 41},
                        {"year": 2017, "goals": 27, "appearances": 54},
                        {"year": 2018, "goals": 16, "appearances": 48}
                    ]
                },
                {
                    "title": "Divock Origi",
                    "stats":
                    [
                        {"year": 2015, "goals": 10, "appearances": 33},
                        {"year": 2016, "goals": 11, "appearances": 43},
                        {"year": 2017, "goals": 0, "appearances": 1},
                        {"year": 2018, "goals": 7, "appearances": 21}
                    ]
                },
                {
                    "title": "Daniel Sturridge",
                    "stats":
                    [
                        {"year": 2015, "goals": 13, "appearances": 25},
                        {"year": 2016, "goals": 7, "appearances": 27},
                        {"year": 2017, "goals": 3, "appearances": 14},
                        {"year": 2018, "goals": 4, "appearances": 27}
                    ]
                },
                {
                    "title": "James Milner",
                    "stats":
                    [
                        {"year": 2015, "goals": 7, "appearances": 45},
                        {"year": 2016, "goals": 7, "appearances": 40},
                        {"year": 2017, "goals": 1, "appearances": 47},
                        {"year": 2018, "goals": 7, "appearances": 45}
                    ]
                },
            ],
            "rows": [2015, 2016, 2017, 2018],
            "stats": ["goals", "appearances"]
        };

        var config =
        {
            "scale": 0.5,
            "dataWidth": 10,
            "dataSpacing": 1,
            "scaleWidth": 6,
            "legendHeight": 4,
            "max": 0,
            "mean": 0,
            "getChartHeight": function()
            {
                return (this.max * this.scale * 1.5);
            },
            "getChartWidth": function(datalength)
            {
                return (datalength * (this.dataWidth + this.dataSpacing)) + (datalength * 0.5);
            },
            "setData": function ()
            {
                var year = d3.select("#ddlYear").node().value;
                var stat = d3.select("#ddlStat").node().value;

                var dataSet =
                {
                  "labels": [],
                  "stats": []
                };

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

                    if (filtered.length > 0)
                    {
                      dataSet.labels.push(graphData.cols[i].title);
                      dataSet.stats.push(filtered[0][stat]);
                    }
                }

                config.mean = d3.mean(dataSet.stats, function(d) { return d; });

                var container = d3.select(".lineChartContainer");
                var wrapper = d3.select(".lineChart");
                var scale = d3.select(".lineScaleSvg");
                var chart = d3.select(".lineChartSvg");
                var legend = d3.select(".lineLegendSvg");
                var filler = d3.select(".lineFillerSvg");

                var height = config.getChartHeight();
                var width = config.getChartWidth(graphData.cols.length);

                container
                .style("width", function(d)
                {
                    return (width + config.scaleWidth) + "em";
                });

                wrapper
                .style("height", function(d)
                {
                    return (height + config.legendHeight) + "em";
                });

                scale
                .style("width", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .style("height", function(d)
                {
                    return height + "em";
                })
                .html("");
               
                chart
                .style("height", function(d)
                {
                    return height + "em";
                })
                .style("width", function(d)
                {
                    return width + "em";
                })
                .html("");

                legend
                .style("height", function(d)
                {
                    return config.legendHeight + "em";
                })
                .style("width", function(d)
                {
                    return width + "em";
                })
                .html("");

                filler
                .style("width", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .style("height", function(d)
                {
                    return config.legendHeight + "em";
                });

                var scaleData = [];
                var unitGrouping = 2;
                if (config.max > 10) unitGrouping = 5;
                if (config.max > 20) unitGrouping = 10;

                for (var i = 0; i < (config.max * 1.5); i += unitGrouping)
                {
                    scaleData.push(i);
                }

                scale.selectAll("line")
                .data(scaleData)
                .enter()
                .append("line")
                .attr("class", function(d)
                {
                    return (d == 0 ? "" : "lineChartLine");
                })
                .attr("x1", function(d)
                {
                    return (config.scaleWidth - 1) + "em";
                })
                .attr("y1", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                });

                scale.selectAll("text")
                .data(scaleData)
                .enter()
                .append("text")
                .attr("x", function(d)
                {
                    return (config.scaleWidth + 1) + "em";
                })
                .attr("y", function(d)
                {
                    return ((height * 2) - ((d * config.scale * 2) - 0.25)) + "em";
                })
                .text(function(d)
                {
                    return (d == 0 ? "" : d);
                });

                scale
                .append("line")
                .attr("class", "lineChartLine")
                .attr("x1", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y1", function(d)
                {
                    return "0em";
                })
                .attr("x2", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y2", function(d)
                {
                    return height + "em";
                });

                chart.selectAll("line")
                .data(scaleData)
                .enter()
                .append("line")
                .attr("class", "lineChartFadedLine")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                });

                chart.selectAll("rect")
                .data(dataSet.stats)
                .enter()
                .append("rect")
                .attr("x", function(d, i)
                {
                    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
                })
                .attr("y", function(d)
                {
                    return height + "em";
                })
                .attr("width", function(d)
                {
                    return (config.dataWidth) + "em";
                })
                .attr("height", function(d)
                {
                    return "0em";
                })           
                .transition()
                .duration(500)
                .attr("y", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                })
                .attr("height", function(d)
                {
                    return (d * config.scale) + "em";
                });

                chart.selectAll("text")
                .data(dataSet.stats)
                .enter()
                .append("text")
                .attr("x", function(d, i)
                {
                    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
                })
                .attr("y", function(d)
                {
                    return (height - (d * config.scale) - 1) + "em";
                })
                .text(function(d)
                {
                    return d;
                });

                chart
                .append("line")
                .attr("class", "lineChartDataMean")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return (height - (config.mean * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (config.mean * config.scale)) + "em";
                });

                legend.selectAll("text")
                .data(dataSet.labels)
                .enter()
                .append("text")
                .attr("x", function(d, i)
                {
                    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
                })
                .attr("y", function(d)
                {
                    return config.dataSpacing + "em";
                })
                .text(function(d)
                {
                    return d;
                });

                legend
                .append("line")
                .attr("class", "lineChartLine")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return "0em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return "0em";
                });
            }
        };

        config.max = d3.max(graphData.cols, function(d)
        {
            var maxStat = d3.max(d.stats, function(x)
            {
                return (x.goals > x.appearances ? x.goals : x.appearances);
            }
            );

            return maxStat;
        }
        );

        var ddlYear = d3.select("#ddlYear");

        ddlYear.selectAll("option")
        .data(graphData.rows)
        .enter()
        .append("option")
        .property("selected", function(d, i)
        {
            return i == 0;
        })
        .attr("value", function(d)
        {
            return d;
        })
        .text(function(d)
        {
            return d;
        });

        var ddlStat = d3.select("#ddlStat");

        ddlStat.selectAll("option")
        .data(graphData.stats)
        .enter()
        .append("option")
        .property("selected", function(d, i)
        {
            return i == 0;
        })
        .attr("value", function(d)
        {
            return d;
        })
        .text(function(d)
        {
            return d;
        });

        config.setData();

        d3.select("#ddlYear").on("change", function() { config.setData(); });
        d3.select("#ddlStat").on("change", function() { config.setData(); });
        </script>
    </body>
</html>


So far so good? The code should still perform the same. You should still get a bar chart when you run it. What we'll do next, is change a drop-down list. You see, now we don't want to choose data by year. We want to choose data by player, and see the line chart track progress over the years. So rename the drop-down list below...
<div class="lineDashboard">
    <select id="ddlPlayer">

    </select>

    <select id="ddlStat">

    </select>
</div>


Then we're going to change this block of code near the bottom of the screen. All instances of "ddlYear" will be changed to "ddlPlayer".
var ddlPlayer = d3.select("#ddlPlayer");

ddlPlayer.selectAll("option")
.data(graphData.rows)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

var ddlStat = d3.select("#ddlStat");

ddlStat.selectAll("option")
.data(graphData.stats)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

config.setData();

d3.select("#ddlPlayer").on("change", function() { config.setData(); });
d3.select("#ddlStat").on("change", function() { config.setData(); });


This block needs to be further changed. In the data, use the cols array of the graphData object instead of rows. And instead of returning d for value and text, return the title property of d, which is the player's name.
ddlPlayer.selectAll("option")
.data(graphData.cols)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d.title;
})
.text(function(d)
{
    return d.title;
});


You'll notice right away that your layout looks wrong. That's to be expected because we introduced a JavaScript error somewhere with all these changes. But fret not... our first drop-down list is duly changed. It now displays the list of player names!


In the setData() method, you need to select ddlPlayer instead of ddlYear. While you're at it, change the name of the variable from "year" to "player".
"setData": function ()
{
    var player = d3.select("#ddlPlayer").node().value;
    var stat = d3.select("#ddlStat").node().value;

    var dataSet =
    {
      "labels": [],
      "stats": []
    };

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

        if (filtered.length > 0)
        {
          dataSet.labels.push(graphData.cols[i].title);
          dataSet.stats.push(filtered[0][stat]);
        }
    }


This For loop determines the labels, if you recall. We'll be making substantial changes to it. First, remove that line, you won't need it.
for (var i = 0; i < graphData.cols.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    }
}


Instead, set filtered outside of the For loop. It's straightforward because you're simply getting the one element in the cols array whose title property corresponds to player.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < graphData.cols.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    }
}


Instead of iterating through the cols array, iterate through the stats array of the first (and only) element in filtered. You won't need the If block either, so get rid of it.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < filtered[0].stats.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    //if (filtered.length > 0)
    //{
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    //}
}


Remember, we want to display year in the labels instead of player names, so change this accordingly.
//if (filtered.length > 0)
//{
  dataSet.labels.push(filtered[0].stats[i].year);
  dataSet.stats.push(filtered[0][stat]);
//}


And here, we'll still obtain the statistic, but instead take reference from the appropriate element in stats.
//if (filtered.length > 0)
//{
  dataSet.labels.push(filtered[0].stats[i].year);
  dataSet.stats.push(filtered[0].stats[i][stat]);
//}


Remove the commented out lines; it's neater.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < filtered[0].stats.length;  i++)
{
    dataSet.labels.push(filtered[0].stats[i].year);
    dataSet.stats.push(filtered[0].stats[i][stat]);
}


Your page should display fine now, but it's still displaying a bar chart. Still, the labels have changed!


Next

Now that it appears to be working, let's make it work correctly.