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