Tuesday, 2 June 2020

Career Lessons From A Casanova

Life brings you lessons from the weirdest places. And around the age of thirty, an employer of mine delivered a few real beauties that I found myself still using a decade later.

There's no polite way to say this - my employer was a pussyhound.

Some bar patrons I knew (from way back when I used to drink) were prone to bragging about their sexual conquests. Y'know, hookers they managed to obtain free sex from as opposed to paying for it. I know it sounds weird the way I say it, but it seemed to be a thing back then. Thing is, my employer didn't even need to brag about those - he seemed to get them just like that. Maybe it was his boyish charm. The ladies loved him.

He was married, but had side-chicks coming out of his arse. Some of his side-chicks were picked up from nightclubs and bars. He wooed them, screwed them and dumped them with panache. Occasionally, in addition he would roam the back alleys of Geylang and massage parlors for cheap exotic women. His libido probably exceeded my passion for programming.

Convince yourself that you want it

One of his messier affairs was with a woman he picked up at a low-end nightclub. Not only was he getting free sex from her, it had gotten to the point where he was putting her up at an apartment he had rented. She had essentially become his mistress. Shit, at that point she even knew his actual full name. I asked him where he saw this going. Did he foresee a future together? Was he serious about her? Was he willing to get a divorce for her?

Convince yourself.

His response was telling. He admitted that in the cold light of day, there was no way forward. But the only way he had successfully managed to obtain all that free pussy up to now was because the women could feel his sincerity beneath his charm. And they felt that sincerity because he had fooled himself into being sincere. Basically, he wasn't that good an actor. He needed to actually believe, against all logic, that the relationships with them could work.

Don't be afraid of rejection

Contrary to what others thought, my employer didn't have a hundred percent success rate with whatever girl he tried to pick up. He didn't even have an fifty percent success rate. I must have heard him use the same cheesy witticisms to nightclub girls a thousand times. But yes, for roughly every ten he paid to bed, there would be one sucker who fell for his charm, and gave it to him for free.

Fishing for women.

His philosophy? I'm just trying to get laid, he said. Doesn't matter if I have to pay for it. It's just money. If I like her, I put in more effort. I'm not looking for happily-ever-after, just happily-for-one-night. And from there, happily-for-a-few-weeks. If it doesn't work out, move on. All I've wasted is a few hours. I don't fixate on one target, and feel miserable if I fail. It's like fishing - you try it long enough, something's going to bite. Even if nothing bites, enjoy the process. Learn from it.

Don't let them think they're the only one

Unlike most of the other wannabe-Romeos chatting up girls in bars, my employer didn't subscribe to the strategy of "make them feel like the only one". No, he believed that these women knew what they were getting into. They understood that they basically followed the money and that the men who patronized them were womanizing bastards. While he wouldn't insult such a woman's intelligence by telling her she was the only one, he wasn't going to rub it in her face either.

Only you...

The thing is not to let her think she's the only one, he liked to say. That gives her no motivation to try harder. Women are competitive by nature; and when you have to depend on horny men for a living, you're very competitive. No, the thing is to make her think that of all the women you're screwing, she's the most important one right now. She knows there are other women, and she's high on your list of priorities, with potential to go higher. That gives her the incentive to keep it that way.

How I applied these lessons

I've left this guy's firm since. I have leveled up in ways I never suspected possible. And I have attended countless job interviews, even while being happily employed somewhere else. Sometimes it's because I really want that job opportunity. Sometimes it's just for practice.

Some of these companies are large tech firms. Some are not so large. Some aren't even tech firms, but have an IT department. Some are located in some remote ass-end of Singapore. But there's one thing that these companies I interviewed for, all have in common. I've tried to convince myself that I actually want to work for them.

Sometimes it doesn't make sense. I live in the West. Why would I want to travel all the way to a firm in the East end, every day? Why would I leave my current company, a big-name operator, for some ikan bilis no-name firm? Why would I give up that generous pay package for something much leaner? And here's the thing - it doesn't have to make sense. My aim is to interview successfully, and get the offer. Whether or not I eventually accept the offer is an entirely different issue. The goals are clear. Interview successfully. Get that job offer.

If I want to interview successfully, interviewers have to be convinced that I genuinely want the job. And since I'm not that good an actor, I have to convince myself that I really fucking want that job. Without that, the interview is a bust from the get-go.

I'll get rejected, of course. I'm not quite that good. But rejection doesn't sting as hard. Firstly, no matter how successfully I convinced myself that I really wanted that job, the effect ends once I leave the interview. Being rejected is actually a relief. And in the event that I really wanted that job, rejection is a chance for me to review what I did wrong and figure out how to be a more attractive option. It's merely part of the process; nothing more. Have I mentioned that before?

Interviews are a process
like any other.

Lastly, I've also figured out that companies seem to take you more seriously if they know you're reviewing other prospects. Those don't have to be competing job offers - just interviews lined up. The mentality here appears to be that candidates who are currently employed or entertaining other prospects, have options; and therefore are more desirable. Candidates who don't have options can wait like all the other losers. So I have learned not to be shy about telling my interviewers that there are other companies I am speaking to - and believe it or not, it's usually true.

There was a company I interviewed for back in 2013. At first the interview wasn't going spectacularly well, but then I mentioned that I had another interview lined up after this - at a company I hadn't even applied for (it was a head-hunted position). Within a couple hours of leaving the interview, the COO called up to make an offer.

There have been other instances of successful interviews, and in each of them, I didn't exactly make it a point to mention that I was exploring other options. But I certainly wasn't coy about it when posed a question point-blank. This may or may not have anything to do with the interview's success, but I bet it didn't hurt any.

The takeaway

I've had to adjust my approach and mentality towards my career. It's paid off mightily.

And all because of some things this Casanova said to me back in 2010. Trust me to use them on tech job interviews rather than women, eh?

Be a lover, not a loser!
T___T

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.