Tuesday, 27 June 2017

App Review: AndroIRC

Some of you who were teenagers in the mid 90s when the Internet was just taking off, might remember this online chat phenomenon called Internet Relay Chat (IRC). These days you have WhatsApp, SnapChat and what-have-you, but back then, people actually got into public chatrooms and, well, chatted.

OK, I guess they still do, but IRC was one of the fore-runners for that kind of activity. And it's still going strong today by the looks of it.

AndroIRC splash screen

And now we have a nice little IRC client for Android, quaintly known as AndroIRC, created by Madalynn.

The team

The Premise

AndroIRC is basically just an IRC client put on mobile, with a typical mobile interface. Instead of clicking on stuff and typing on a physical keyboard, now you tap, swipe and use an on-screen keyboard.

On-screen keyboard


The Aesthetics

The default theme is cobalt blue and white, though you can change the colors via the settings. AndroIRC claims to be "fully customizable", but honestly, I don't even know what the hell that is supposed to mean. There's definitely going to be a limit as to what the user can change.

Font size change

Thankfully, one can change the text sizes. The default size is a little bit of a strain on my aging eyes.

All that said, the design is clean and simple enough, and facilitates the app's purpose as an IRC client. It's not beautiful, and doesn't need to be.

The Experience

Chatting on IRC is not a new experience. Thanks to the mobile revolution, chatting online while not bound to a desktop is also not a new experience. But the experience of doing IRC on mobile manages to be familiar and novel at the same time.

If you're used to a larger screen, AndroIRC may take some getting used to.

The Interface

Basically you have a console window that shows you system messages, any number of chatroom windows that opens whenever you join a room, and any number of windows displaying the conversations of any users you are chatting with.

The functions you're familiar with, and probably typed a lot back in those days, are working. Though some of them are less accessible than others. And using the "/" key, which is an absolute necessity if you're going to be typing commands, can be rather inconvenient on a mobile platform.

Select network

Log screen

Enter chatroom

Chat with user
If you were an IRC god in those days, using your keyboard like a pro, you may find yourself severely hampered by this one. Instead of an up/down button to scroll through your last typed message, you have to press the volume control. Copying/pasting is a little less convenient. At times, it can be a pain, but this is not something that is particular to AndroIRC. In fact, the little inconveniences should be the same for all mobile-based IRC clients.

What I liked

The nostalgia! Oh boy! Makes me feel like a teen again!

It's free!

They say having to IRC on such a tiny interface is a pain. Well, the extreme pain points come when you're chatting with multiple users, in multiple chatrooms, on multiple servers. I can certainly see that point. However, that's not my style at any rate, and I think having the interface restricting personal excesses, is not a bad thing per se.

What I didn't

Swiping left to pull out the list of users in a chatroom is not intuitive, and sometimes you end up swiping to a different chat or chatroom instead, which is plain annoying.

User list

When you're busy chatting with someone, a new chat initiated by anther user will immediately and rudely interrupt you, disrupting whatever you were typing. If you had typed a pretty long sentence, tough luck, chum. It's gone.

There's no function to ignore somebody by tapping on his or her nick.

No ignore function

Instead, you have to type out the command the old-fashioned way.
/ignore [nick]


Seriously?! Come on, it's the ignore function! How can you have a kick/kickban function but not an ignore function? How does that even make sense?

Speaking of missing shortcuts for commonly-used functions, AndroIRC does not appear to have a quick way to list channels either. Instead, you have to type this.
/list


Conclusion

Could have been great.

It's a nifty little app that does what it's supposed to. And for the most part, it's effective (not beautifully so, but still), but for a few niggling annoyances and a lack of some very basic features.

My Rating

7/10

/me likes this app!
T___T

Tuesday, 20 June 2017

Macintosh Matrimony

Wedding bells a-ringing!

Remember back in March, I wrote a piece on a friend who loved Apple? Well, my buddy here just took his love to a whole new level.

His name is Jermyn, and a couple weeks back, he held a wedding shoot with his girlfriend-turned-wife Suat Huang (lovely woman, by the way) at - get this - the newly opened Apple Store along Orchard Road.

How's that for devotion?!

Photograph by Weili Yip Creations

Then something even more glorious happened. The story got picked up by various news outlets. At first, it was mostly those from Singapore, such as The Straits Times and Mothership.

Then Mashable and The Inquirer and Time picked up the story. Then Zing, a news outlet from Vietnam, where they're just returned from their honeymoon. Then UOL Estilo, from Brazil. Then WAPA, from Puerto Rico. Rambler from Mother Russia. And finally, so far, Sohu from China!

Holy shit.

Dudes, they say the path of true love never did run smooth. In your case, it traveled the goddamn globe and back. According to Jermyn, it was all the wife's idea. Gotta say, that's brilliant.

Hey, but I have something these news outlets don't. For one, I was actually a guest at the wedding. I was there. I even put on pants (there's an inside joke in that circle about how I don't wear pants, but that's nothing more than a vicious lie. I do wear pants... sometimes).

From left: His Teochewness,
the bride, the groom,
Cheryl Tay
and Jarrod Luo.
And I have pics to prove it! Suck on that, Mashable et al!

Positively orgasMac idea.
T___T

Friday, 16 June 2017

Spot The Bug: The Luhn Algorithm Validator

The bugs are creeping back, and thankfully, so is Spot The Bug. No rest for the wicked!

Come out, come out,
wherever you are...

Two years back, I wrote some buggy code for a Luhn algorithm validator, in Java. For those who don't know what the hell a Luhn algorithm is, it's a surface-level check to see if an entered credit card number is actually a valid credit card number.

Now that I look at it, I don't know what the heck I was thinking that time. I wouldn't have written it like that, say, last week.

The user is supposed to be prompted with an input box, to input his number. The input is then tested to ensure that it contains numbers only, and is 10 characters in length. If it passes this test, then I run it through the Luhn test. If not, the input box keeps prompting the user till the user inputs a value that is numeric and 10 characters in length.

Sound simple enough? No way I could screw that up, right? Nuh-uh. Think again.

What went wrong

Things worked fine when I entered a valid number. This is 10 characters, all numeric.




So is this.



It also worked when I entered a string that wasn't 10 characters, then entered a valid number.


But when I entered something alphanumeric, the input box would keep coming up even if I subsequently entered a valid number.

Why it went wrong

Here's the code. The problem wasn't in the isValidLuhn() function, so I've truncated the code for that (it's kind of long).

See, I had this bright idea to run the string through a For loop and grab the number of non-numeric characters, incrementing the variable alpha each time I found one. And at the end of it, if the variable alpha was more than 0 or the number of characters was not 10, the process repeated.

package luhn;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
                Boolean acceptable=false;
                String pin = "";
                int alpha = 0;
       
                while (!acceptable) {
                        pin=JOptionPane.showInputDialog(null, "Please enter a 12 digit number.", "Your Credit Card Number", JOptionPane.QUESTION_MESSAGE);
           
                        for (int i=0; i < pin.length(); i++) {
                            if (!Character.isDigit(pin.charAt(i))) {
                                    alpha++;
                            }
                        }
           
                        if (pin.length()==10&&alpha==0) acceptable=true;
                }
       
                if (isValidLuhn(pin)) {
                    System.out.println("Credit card number is valid");
                } else {
                    System.out.println("Credit card number is invalid");
                }   
    }
   
    public static boolean isValidLuhn(String str) {
        ...
    }
}


Now, this was just horribly inefficient and error-prone, but that aside, there was a glaring logical flaw in the design. If I entered a non-numeric character, the variable alpha would change to a non-zero value, and the variable acceptable would still be false, thus perpetuating the loop. But no matter what I entered later, alpha would either remain the same (if I entered a valid numeric string) or get incremented further (if I entered an alphanumeric string).

How I fixed it

Aside from rewriting this hideous thing and adopting a more modular format? Tempting, but a far quicker fix would be just to reset alpha to 0 at the beginning of the While loop!
package luhn;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
                Boolean acceptable=false;
                String pin = "";
                int alpha = 0;
       
                while (!acceptable) {
                        pin=JOptionPane.showInputDialog(null, "Please enter a 12 digit number.", "Your Credit Card Number", JOptionPane.QUESTION_MESSAGE);
                        alpha = 0;

                        for (int i=0; i < pin.length(); i++) {
                            if (!Character.isDigit(pin.charAt(i))) {
                                    alpha++;
                            }
                        }
           
                        if (pin.length()==10&&alpha==0) acceptable=true;
                }
       
                if (isValidLuhn(pin)) {
                    System.out.println("Credit card number is valid");
                } else {
                    System.out.println("Credit card number is invalid");
                }   
    }
   
    public static boolean isValidLuhn(String str) {
        ...
    }
}


Moral of the story

Boy, that really made me feel stupid. On the other hand, I'm glad to report that I code a lot better now than I did two years ago. Damn, that was horrible.

The takeaway for this is, if exiting a loop depends on a variable, always reset the variable before operating on it! Or, you know, write code in such a way that you don't have to.

If you get silly bugs sometimes, remember you're not aLuhn!
T___T

Sunday, 11 June 2017

Keep Calm and Carry On

It was with a certain amount of bemusement with which I observed a certain Internet war spark off recently in sunny Singapore.

Shrey Bhargava, an aspiring actor, wrote a post on Facebook detailing his racist treatment during an audition for upcoming Singapore movie Ah Boys To Men 4. This was soon rebutted by our very own Xiaxue, Singapore's foremost influencer (a fancy douchebaggy word for "blogger") with a colorful series of posts that dissected Shrey's lamentations. Things heated up, escalating quickly, with name-calling and personal attacks abounding from both sides.

The minorities (Malays, Indians, Eurasians)  will invariably tell you that the majority (Chinese) did most of it (some might even claim that only the majority were behaving badly), and the majority will tell you that the minorities started it first. All rubbish. We're big boys and girls - whatever you do, own it.

Things came to a head when someone made a police report against Shrey. Now that was uncalled for.

Are Singaporeans racist? Why, sure we are. Perhaps not overtly, and I like to think that we've made progress as a nation since the dark days our Hokkien and Teochew forefathers duked it out on the docks. We might be guilty of "casual racism", whatever the hell that means. The majority might be guilty of innumerable microaggressions towards the minorities, and unfair race-based bias. I'd even venture to say that the minorities are just as capable of being racist as the majority. Some seem to be against racism only when it's being directed at them.

I noticed some trends during this war. Four main groups came out to play.

The minorities, those feeling outraged at yet another instance of racism in Singapore, railed bitterly and passionately about "majority privilege" and how the Chinese were "trying to put the uppity Indian back in his place".

The majority decided that Shrey was whiny and unprofessional (I don't actually disagree there), and needed to get over himself. I'm pretty sure I recall seeing personal attacks and racial slurs being thrown around. Like apunehneh, kelingkia, etc. Seriously, guys? This is 2017. Don't you feel stupid using those words?

A certain segment of Chinese were "woke" (again, another douchebaggy term) and decided that they should use their majority privilege to defend the minorities and wag their cyber-fingers at how their fellow Chinese were behaving.

And the last segment were just watching this all unfold (and undoubtedly munching on the proverbial popcorn), occasionally making a wisecrack here and there when the temptation got too strong, but staying out of it for the most part. Yep, that sounds like me all right.

Moral of the story? Let me put it this way. Here's an acronym for you: Let's Understand, Most People Are Ridiculous. If LUMPAR sounds suspiciously like the Hokkien term for scrotum, that's not a coincidence.

Chill, baby, chill

But that's neither here nor there. This isn't about debating who's more guilty of being racist. Screw that - we all are. It's about learning to stay chill on the Internet - whether it's on Facebook, WhatsApp or Twitter.

I'm only using this recent case as an example. There have been numerous other flame wars in recent memory - Trump supporters vs Clinton supporters (though I fail to see why Singaporeans think their opinion as to who would be a better US President is so goddamn important when none of us get to vote), LGBTQ vs "Traditional Asian Values" (boy, we're really piling on the douchebaggy terms today, aren't we?)...

There are certain things you might want to bear in mind if you want to remain chill on the Internet.

Having the last word is overrated

Some guys just can't let it go. They need to have the last word, hammer the other party into submission. For what? Dude, this is the Internet. There is no gain. Most of you have jobs, families and very real life struggles. What does winning at a pissing contest do for you?

Learn to walk away. Communication has changed in the last decade. Now, to get a message across, you can do so instantly without needing to see the other person face-to-face. Is that a good thing? Not always. Instant replies means that the rate in which you deliver inflammatory content also goes up, without giving you time to vet your own vitriol. That's bad, very bad. Being behind a computer screen also means the probability of getting punched in the face for saying something stupid goes down to zero percent. No immediate consequences - always a bad thing.

When Facebook notifies you that someone has replied to your post, and from prior experience you know that this person's remarks are likely to be snarky, insulting and utterly useless except to get a rise out of you, the most sensible thing you can do is not reply. No, wait, scratch that. The most sensible thing you can do is not even read it!

Ostrich behavior.

What, you think that this sounds cowardly? You think this sounds like ostrich behavior? No, it isn't. Blocking and unfriending is way more extreme. Blocking someone denies him the opportunity to speak his mind. That's just not right. People should be allowed to speak their minds. But you don't have to grant them an audience. Accord them the right to speak their mind and at the same time, exercise your right not to read their drivel while getting the fuck on with your life. I've had people single me out in WhatsApp chat groups for verbal abuse before. Do I respond? No, I turn off WhatsApp and play a few rounds of One Man Army: Epic Warrior. When people eventually calm down, they tend to understand how ridiculous they looked moments before.

There's no need to belabor the point. Let it go.

Anger is a barrier to effective communication

Regarding the Shrey Fray (as I like to call it), I had the dubious pleasure of having an Eurasian friend scream at me over WhatsApp regarding this incident.
Because this sends the message to Non chinese that we cannot even talk about race issues without you chinese turning around and putting us in our place despite us having a lifetime of being on the receiving end of your "know your place" put downs

Whoa, hold up. "You Chinese?" That sounded suspiciously racist. I thought this guy was against racism?

But see, here's the thing. People say stupid shit and make utter fucking fools of themselves when they're angry. It's one of the unshakeable laws of the universe. Just because someone is being stupid doesn't mean you need to join them.

Anger is a barrier to
effective communication.

Strong emotion is listed as one of the barriers to effective communication. I didn't make that shit up; if you'd gone through Polytechnic education, it's part of Communication Skills 101. Someone who is angry (not even necessarily with you)  isn't likely in the right frame of mind to listen to your calm, logical and rational analysis as to why his or her anger is misdirected. Stop talking; you're wasting your time and probably making that person even angrier.

Anger can be helpful if channeled productively. Anger vented over the Internet is mostly impotent rage. (Face it, if you had any power to change the situation that was angering you, you'd be doing it instead of raving on the Internet, right?) And when vessels of impotent rage collide, shit happens. Don't be one of those vessels. It helps no one, least of all yourself.

Just because you have the right, doesn't mean you should

People sometimes conflate the right to do something, with the necessity of such an action. Do not make that mistake. I have the right to sit in my bamboo rocking chair and smoke till my lungs bleed. I have the right to eat whatever crap I want and turn into Jabba The Hutt. I have the right to fantasize about Angelina Jolie 24 hours a day until my hand (and probably my dick) falls off. Do I recommend any of those courses of action? No, I most certainly do not.

Mama mia.

Did Shrey Bhargava have the right to voice his concerns in that Facebook post, however whiny he sounded? Of course. Did people have the right to respond, however nastily? Sure. Were other people perfectly within their rights in responding just as nastily? Without a doubt. Did the lamer who made that police report have the right to do so? Pains me to say it, but yes. Everyone was within their legal rights. And that's where the problem lies. Singapore's laws say you can do those things - it doesn't follow that you should do them.

Refer to the above incident again with my Eurasian friend. Did he make a racist remark while railing against racism? Sure. Did I have the right to be offended? Undoubtedly. Could I be blamed for wanting to respond in kind? Probably not.

But did I take offense? Did I escalate the situation with a few choice remarks of my own? Oh hell, no.

Look, I'm not claiming to be all mature and shit. I didn't escalate the situation not because I felt I owed him, or because I felt I deserved it, or even because I valued this friendship too much.

It was for a far simpler reason.

In six months, I will turn 40 and officially be a senior citizen. This is beneath me. I had every right to be upset - I just had neither the time nor the inclination. Besides, at my age, mustering anything stronger than mild annoyance is a bit of a challenge.

Chill out, dudes!

This is only the Internet. Your lives are more than that. Much, much more.

Stay cool. It'll preserve your shrey-nity. (heh heh)
T___T

Tuesday, 6 June 2017

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

Now that we have data, and placeholders to display that data with, let's focus on displaying the data.

Add this to your displayData() function. We'll declare four more variables - fill, p, percentage and actual. Then we'll use a For loop to iterate through the cols array of the graphdata object.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                var fill,p;
                var percentage,actual;

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

                }

                displayScale(max);
            }


Here, we set actual to the figure derived from the getStatistic() function which we've created below.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                var fill,p;
                var percentage,actual;

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

                displayScale(max);
            }

            function getStatistic(data,row,stat)
            {

            }


Then we'll use actual and max to define the percentage that the red bar should go down by. percentage, in this case, would be the 100% less the percentage of the actual statistic, actual, against the maximum statistic, max.
                for (var i=0;i<graphdata.cols.length;i++)
                {
                    actual = getStatistic(graphdata.cols[i],row,stat);
                    percentage = 100 - ((actual/max) * 100);
                }


Next, we set fill to the element whose id we defined in the previous part of this tutorial, and set the margin-top property to the value of percentage. To obtain the exact number of pixels the red bar should be offset by, we'll use this value, divided by 100 (because it's a percentage, see?) and multiply it by 80% of 500! Why 80% of 500? Well, the containing element is 80% of its containing element's height, which in turn is 100% of the graph_container's height, which is 500 pixels! Go look at the CSS again if you are in doubt!

And of course, we'll set p to the paragraph element whose id we also defined in the previous part of this tutorial, and use the innerHTML property to display the value of actual.
                for (var i=0;i<graphdata.cols.length;i++)
                {
                    actual = getStatistic(graphdata.cols[i],row,stat);
                    percentage = 100 - ((actual/max) * 100);

                    fill = document.getElementById("fill_" +i);
                    fill.style.marginTop = (percentage/100 * (0.8*500)) + "px";

                    p = document.getElementById("p_" +i);
                    p.innerHTML = actual;
                }


Now we're going to work on the getStatistic() function. It's even simpler than the getMaxStatistic() function we wrote earlier. It's similar, except that in this case, you don't need the entire data object. You just need the appropriate dataset, which is passed in as the first argument. Similar to getMaxStatistic(), row and stat are passed in as arguments.

Here, we define a variable, temp, and use the filter() method to return data the same way we did with getMaxStatistic(). If there isn't such a row, the temp array will be empty and you'll return 0. Otherwise, you return the required statistic of the first (and only) element of temp.
            function getStatistic(data,row,stat)
            {
                var temp;

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

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

                return 0;
            }


Ohhhkay, the data appears to be correct. Check the data at the top of the red columns and see that it corresponds to the scale! But this is messy and we're going to clean it up.


Add the overflow property to the CSS class data_container, and set the value to hidden.
            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }


Better now!


Let's test the dataset by altering the code for the drop-down lists. So the drop-down lists will call displayData() all over again when the value changes.
        <select id="ddlRow" onchange="displayData();">

        </select>

        <select id="ddlStat" onchange="displayData();">

        </select>


So now when you change the values, you should see the data change with them!


The transition is a little abrupt, so do this...
            .data_fill
            {
                width:100%;
                height:100%;
                color:#000000;
                background-color:#FF0000;
                text-align:center;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                margin-top:0%;
                -webkit-transition:all 1s;
                transition:all 1s;
            }


Displaying the Average

OK, so far so good. But let's add more value to your bar chart. Let's display the average value! To do that, let's add some HTML. So you have a div with an id of average_container, and a div nested inside it with an id of average_fill. Inside it is a paragraph tag with the id average_label.
        <div id="graph_container">
            <div id="average_container">
                <div id="average_fill">
                    <p id="average_label"></p>
                </div>
            </div>           

            <div id="scale_container">

            </div>

            <div id="col_container">

            </div>
        </div>


And here's the CSS styling. I've given average_container and average_fill a nice orange outline for better visibility.

average_container, same as data_container, has 80% of the parent div, graph_container. That's not a coincidence. margin-bottom is set to -100% because average_container comes before scale_container and col_container in the HTML and we don't want it to force them out of the graph_container div. The overflow property is set to hidden for the exact same reason it's set to hidden for the data_container class. Because we'll be performing a similar operation with the average data.

The width of average_fill div is set to 100% of average_container's. I've given it a dotted black top border. text alignment, and transition are just there to make things look pretty, so just ignore them if you want to.

average_label's styling is purely cosmetic.
            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            #average_container
            {
                height:80%;
                margin-bottom:-100%;
                overflow:hidden;
                outline:1px solid #FF4400;
            }

            #average_fill
            {
                width:100%;
                border-top:1px dotted #000000;
                text-align:center;
                -webkit-transition:all 1s;
                transition:all 1s;
                outline:1px solid #FF4400;
            }

            #average_label
            {
                width:100%;
                color:#000000;
                font-weight:bold;
                font-family:verdana;
                text-align:center;
            }           

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }


There you should see the orange outline of average_container, and the black dotted line at the top from average_fill. We didn't specify a height for average_fill or a value to put in average_label, yet, so average_fill's height is still at the minimum.

Let's add a bit to our displayData() function, at the end. Here, we declare a variable, average, and define its value by running the getAverageStatistic() function which we'll create right after this.
Then we define the variables average_container, average_fill and average_label; and then we assign them to their similarly named counterparts in the DOM.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                var fill,p;
                var percentage,actual;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    actual = getStatistic(graphdata.cols[i],row,stat);
                    percentage = 100 - ((actual/max) * 100);

                    fill = document.getElementById("fill_" +i);
                    fill.style.marginTop = (percentage/100 * (0.8*500)) + "px";

                    p = document.getElementById("p_" +i);
                    p.innerHTML = actual;
                }

                displayScale(max);

                var average = getAverageStatistic(row,stat);
               
                var average_container = document.getElementById("average_container");
                var average_fill = document.getElementById("average_fill");
                var average_label = document.getElementById("average_label");
            }

            function getAverageStatistic(row,stat)
            {

            }


Next, we again define percentage as 100% less the percentage of average against max. average_container's width is defined as 100 pixels multiplied by the number of columns (that is, the number of elements in the cols array of the graphdata object). average_fill is pushed downwards proportionally to the average value of the dataset. Finally, we populate the content of average_label with the value of average.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                var fill,p;
                var percentage,actual;

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    actual = getStatistic(graphdata.cols[i],row,stat);
                    percentage = 100 - ((actual/max) * 100);

                    fill = document.getElementById("fill_" +i);
                    fill.style.marginTop = (percentage/100 * (0.8*500)) + "px";

                    p = document.getElementById("p_" +i);
                    p.innerHTML = actual;
                }

                displayScale(max);

                var average = getAverageStatistic(row,stat);
               
                var average_container = document.getElementById("average_container");
                var average_fill = document.getElementById("average_fill");
                var average_label = document.getElementById("average_label");

                percentage = 100 - ((average/max) * 100);
                average_container.style.width =  (graphdata.cols.length * 100) + "px";
                average_fill.style.marginTop = (percentage/100 * (0.8*500)) + "px";
                average_label.innerHTML = "Average: " + average;
            }

            function getAverageStatistic(row,stat)
            {

            }


And of course, we'll need to work on the function getAverageStatistic(). It's actually pretty similar to the getMaxStatistic() function. We have to iterate through the entire graphdata object's cols array. But instead of getting the maximum value, we want the average value. So here we add all the relevant values and then divide by the total number of columns. The number is fixed to one decimal place using the toFixed() method.
            function getAverageStatistic(row,stat)
            {
                var average=0;
                var temp;

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

                    if (temp.length>0)
                    {
                        average = average + temp[0][stat];
                    }
                }

                return (average / (graphdata.cols.length)).toFixed(1);
            }


Here, we've put in a If conditional to prevent the unlikely scenario that cols is an empty array, because we really don't want a divide-by-zero error.
            function getAverageStatistic(row,stat)
            {
                var average=0;
                var temp;

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

                    if (temp.length>0)
                    {
                        average = average + temp[0][stat];
                    }
                }

                if (graphdata.cols.length == 0)
                {
                    return 0;
                }
                else
                {
                    return (average / (graphdata.cols.length)).toFixed(1);
                }
            }


Here you go. If you change the dataset, you'll see the average value shift!


You might want to change these outline properties now.
            #average_container
            {
                height:80%;
                margin-bottom:-100%;
                overflow:hidden;
                outline:0px solid #FF4400;
            }

            #average_fill
            {
                width:100%;
                border-top:1px dotted #000000;
                text-align:center;
                -webkit-transition:all 1s;
                transition:all 1s;
                outline:0px solid #FF4400;
            }


Pretty! But there's something missing...


The Final Touch

Just add this line to your data_container CSS class.
            .data_label
            {
                margin: 0 auto 0 auto;
                text-align:center;
                width:100%;
                height:20%;
                color:#000000;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                border-top:1px solid #000000;
            }           


OK, looking pretty much complete now. There's a nice black border on top of the column labels, forming the illusion of a solid black line separating the labels from the data.

That's the end of our web tutorial!

I had fun. Geeky, mathemathical fun, but fun nonetheless. We'll do more charts in future, so stay tuned.

Editor's Note: The dataset for Fernando Torres was found to be incorrect, and later, edited accordingly. As such, some of the screenshots are also incorrect.

You have a bar chart. Col-gratulations!
T___T

Sunday, 4 June 2017

Web Tutorial: The Bar Chart (Part 1/2)

When you want to display data, there are quite a number of options available to you - bar graphs, pie graphs and line graphs, among others.

Bar graphs are meant for comparing numerical data between different groups, or progress over time, though it's fairly limited in the case of the latter. Today, we'll be creating an animated bar graph in HTML, CSS and JavaScript.

For data, we will be using statistics collected from Wikipedia, pertaining to the football club I support - Liverpool FC. We'll compare appearances and goals from six players across four seasons.

Let's get started!
Here's the HTML. We'll create placeholders for CSS and JavaScript.
<!DOCTYPE html>
<html>
    <head>
        <title>Bar Graph</title>

        <style>

        </style>

        <script>

        </script>
    </head>
   
    <body>

    </body>
</html>


Here, we have a temporary styling for div elements, giving them a nice orange outline. And we specify that the graph_container div is 500px in height. In the HTML, we have the graph_container div and two drop down lists with ids of ddlRow and ddlStat respectively.
<!DOCTYPE html>
<html>
    <head>
        <title>Bar Graph</title>

        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }
        </style>

        <script>

        </script>
    </head>
   
    <body>
        <div id="graph_container">

        </div>

        <select id="ddlRow">

        </select>

        <select id="ddlStat">

        </select>
    </body>
</html>


See the result!


Create two more divs within the graph_container div, and give them ids of scale_container and col_container respectively.
        <div id="graph_container">
            <div id="scale_container">

            </div>

            <div id="col_container">

            </div>
        </div>


Style col_container and scale_container like so. scale_container is the placeholder for the scale of the bar graph while col_container holds the data. scale_container has only a 20px width and its height is 80% of graph_container. Its border at the bottom is a fine black line. col_container is just as tall as the graph_container div, and the width is set to 95% of graph_container's. Both of these divs have the float property set to left, so they align nicely left.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Take a look at it now. You have a nice thin column for the scale (with a black border on the bottom) and a larger space for your data.


All good? We'll be populating these placeholder divs soon. First, change your HTML.
<body onload="populate();">


Then add the populate() function.
        <script>
            function populate()
            {

            }
        </script>


The data

Before working on the populate() function, let's create the data. Here we define an object, graphdata, with three properties - cols, rows and stats. Each of these is an array.
        <script>
            var graphdata =
            {
                "cols":[],
                "rows":[],
                "stats":[]
            };

            function populate()
            {

            }
        </script>


Let's begin defining values for rows. These years represent the seasons which the datasets are for. Each season begins in the middle of the year, and ends in the middle of next year. But it's enough to just take the starting year.
            var graphdata =
            {
                "cols":[],
                "rows":[2007,2008,2009,2010],
                "stats":[]
            };


Now, we define what stats there are for any dataset. Namely, goals and appearances.
            var graphdata =
            {
                "cols":[],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


The next one is going to be huge. Each element in the cols array is an object. Each object has a property, title, and an array, stats. Here, the title for the first element is the name of one of Liverpool FC's most prolific strikers, ever.
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":[]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


Now, add the stats. Each element in the stats array is, in turn, another object with the properties year, goals and appearances. Boy, this guy really did score a lot of goals, didn't he?!
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":
                        [
                            {"year":2007,"goals":24,"appearances":33},
                            {"year":2008,"goals":14,"appearances":24},
                            {"year":2009,"goals":18,"appearances":22},
                            {"year":2010,"goals":9,"appearances":23},
                        ]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };


Add the other players in the cols array.
            var graphdata =
            {
                "cols":
                [
                    {
                        "title":"Fernando Torres",
                        "stats":
                        [
                            {"year":2007,"goals":24,"appearances":33},
                            {"year":2008,"goals":14,"appearances":24},
                            {"year":2009,"goals":18,"appearances":22},
                            {"year":2010,"goals":9,"appearances":23},
                        ]
                    },
                    {
                        "title":"Steven Gerrard",
                        "stats":
                        [
                            {"year":2007,"goals":11,"appearances":36},
                            {"year":2008,"goals":16,"appearances":34},
                            {"year":2009,"goals":9,"appearances":31},
                            {"year":2010,"goals":4,"appearances":33},
                        ]
                    },
                    {
                        "title":"Dirk Kuyt",
                        "stats":
                        [
                            {"year":2007,"goals":3,"appearances":34},
                            {"year":2008,"goals":12,"appearances":32},
                            {"year":2009,"goals":9,"appearances":38},
                            {"year":2010,"goals":13,"appearances":37},
                        ]
                    },
                    {
                        "title":"Ryan Babel",
                        "stats":
                        [
                            {"year":2007,"goals":4,"appearances":30},
                            {"year":2008,"goals":3,"appearances":27},
                            {"year":2009,"goals":4,"appearances":25},
                            {"year":2010,"goals":1,"appearances":1},
                        ]
                    },
                    {
                        "title":"Yossi Benayoun",
                        "stats":
                        [
                            {"year":2007,"goals":4,"appearances":30},
                            {"year":2008,"goals":8,"appearances":32},
                            {"year":2009,"goals":6,"appearances":30},
                        ]
                    },
                    {
                        "title":"David N'gog",
                        "stats":
                        [
                            {"year":2008,"goals":2,"appearances":14},
                            {"year":2009,"goals":5,"appearances":24},
                            {"year":2010,"goals":2,"appearances":25},
                        ]
                    },
                ],
                "rows":[2007,2008,2009,2010],
                "stats":["goals","appearances"]
            };

Let's hammer this into shape!

Add this to the populate() function. There are some undefined variables, and there's the variable graph, which we will peg to the div col_container.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");
            }


Now do this. Create a For loop that iterates across the cols array of the graphdata object.

Then, within the loop, create a div element and use the variable col to store that object. Then set the class of that object to col_section. And append that object to graph.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    graph.appendChild(col);
                }
            }


Modify your CSS. Add the col_section CSS class. The width will be 100px but the height will be 100% of its parent. Don't mix the two up! And of course, the float property of this is left, to properly align everything.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Run your code. You'll see 6 columns have been added inside the col_container div! One for each footballer we added in the graphdata object!


We're going to do more now. We'll create a div element, set it to the variable container and set the class to data_container, then append container within col before appending col within graph.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    col.appendChild(container);

                    graph.appendChild(col);
                }
            }


And then we'll add more styling, this time the data_container CSS class. The margin properties ensure that it will be centered within its parent, and only occupy 80% height and width. The background property is set to transparent explicitly, just in case.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


Now you see that within the columns is a smaller column!


Now we're going to do the same with the fill variable, setting the class to data_fill. But before appending it within container, we will create a paragraph element and set it to the variable p. Then we'll append p within fill before appending fill within container. We'll give each of these elements an id as well.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    col.appendChild(container);

                    graph.appendChild(col);
                }
            }


Now we add the CSS styles data_fill and value_label.

data_fill is meant for styling the visual representation of the bars. It will fill 100% of the height and width of its parent. I've set the background color to red.

value_label is meant for styling the numerical data. I've set the color to white. The rest of it is cosmetic styling of the font, which you can alter without affecting functionality (much).
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }

            .data_fill
            {
                width:100%;
                height:100%;
                color:#000000;
                background-color:#FF0000;
                text-align:center;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                margin-top:0%;
            }

            .value_label
            {
                color:#FFFFFF;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


You can see that the data columns are filled with red. Just as planned. You won't see the labels because we haven't given them any data. Yet.


Now add this! Create a div element, set it to the label variable with a class of data_label, and the innerHTML property to the title property of the graphdata array element you are currently accessing within the For loop. But instead of appending it within container, append it within col after container.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }
            }


OK, now add the data_label CSS style. width is set to 100% and height to 20%. The margin property ensures that the div stays centered. The rest is font styling, so do as you please.
        <style>
            div {outline:1px solid #FFAA00;}

            #graph_container
            {
                height:500px;
            }

            #col_container
            {
                height:100%;
                width:95%;
                float:left;
            }

            .col_section
            {
                width:100px;
                height:100%;
                float:left;
            }

            .data_container
            {
                margin: 0 auto 0 auto;
                width:80%;
                height:80%;
                background-color:transparent;
                overflow:hidden;
            }

            .data_label
            {
                margin: 0 auto 0 auto;
                text-align:center;
                width:100%;
                height:20%;
                color:#000000;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
            }

            .data_fill
            {
                width:100%;
                height:100%;
                color:#000000;
                background-color:#FF0000;
                text-align:center;
                font-weight:bold;
                font-family:verdana;
                font-size:0.8em;
                margin-top:0%;
            }

            .value_label
            {
                color:#FFFFFF;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }

            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }
        </style>


And there we have the names of the labels on the bottom! It's not nicely aligned yet, but we can take care of that later.


Now outside of the For loop, we have new code. Remember the drop-down lists? We're going to populate them with data. First, define two variables, ddl and option.
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }

                var ddl,option;
            }


Now set the variable ddl to refer to the object ddlRow. And for each element in the rows array of the graphdata object, create an option and populate it, then add it to the drown-down list ddlRow. The text is slightly different from the value. That's because in football parlance, as previously mentioned, each season starts in the middle of the year and ends in the middle of next year. So a season that started in 2008 would be more appropriately known as "Season 2008/2009".
                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }


Take a gander!


Now do the same for ddlStat...
                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }

                ddl = document.getElementById("ddlStat");

                for (var i=0;i<graphdata.stats.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.stats[i];
                    option.value = graphdata.stats[i];
                    ddl.add(option);
                }


And both your drop-down lists are populated!


That won't do anything for us right now of course. We'll need a data scale. Let's start by defining a new function, displayData(), and calling it at the end of populate().
            function populate()
            {
                var col, container, label, fill, p;
                var graph = document.getElementById("col_container");

                for (var i=0;i<graphdata.cols.length;i++)
                {
                    col = document.createElement("div");
                    col.className = "col_section";

                    container = document.createElement("div");
                    container.className = "data_container";

                    fill = document.createElement("div");
                    fill.className = "data_fill";
                    fill.id = "fill_" + i;

                    p = document.createElement("p");
                    p.className = "value_label";
                    p.id = "p_" + i;

                    fill.appendChild(p);
                    container.appendChild(fill);

                    label = document.createElement("div");
                    label.className = "data_label";
                    label.innerHTML = graphdata.cols[i].title;

                    col.appendChild(container);
                    col.appendChild(label);

                    graph.appendChild(col);
                }


                var ddl,option;

                ddl = document.getElementById("ddlRow");

                for (var i=0;i<graphdata.rows.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.rows[i] + " - " + (parseInt(graphdata.rows[i])+1);
                    option.value = graphdata.rows[i];
                    ddl.add(option);
                }

                ddl = document.getElementById("ddlStat");

                for (var i=0;i<graphdata.stats.length;i++)
                {
                    option = document.createElement("option");
                    option.text = graphdata.stats[i];
                    option.value = graphdata.stats[i];
                    ddl.add(option);
                }

                displayData();
            }

            function displayData()
            {

            }


Start by grabbing the selected values from the ddlRow and ddlStat drop-down lists. Then run the values through the getMaxStatistic() function and use the result in the displayScale() function. We'll, of course, have to create those functions.
            function displayData()
            {
                var row = document.getElementById("ddlRow").value;
                var stat = document.getElementById("ddlStat").value;

                var max = getMaxStatistic(row,stat);
                displayScale(max);
            }

            function getMaxStatistic(row,stat)
            {

            }
           
            function displayScale(maxval)
            {

            }


We'll work on the getMaxStatistic() function next. First, define a variable max with a value of 1. Then declare the variable temp.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;
            }


Next, iterate through the cols array of the graphdata object using a For loop. Within it, process the stats array of the current element of the cols array using the filter() method to match the year property with that of the row parameter of the getMaxStatistic() function. This means that if "2008" is selected, all elements with the year for "2008" will be returned in an array. That array is set to the variable temp.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

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


If temp is an empty array, that means no data for the year was found, which is a possibility. If not, there should be only one row. Grab the first (and only) element in the temp array with the statistic defined by the stat parameter of the getMaxStatistic() function. If it's more than max, set max to that value.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

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

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


Finally, we conduct a Modulus test. If max is divisible by 10, return max with 10 added. If not, deduct the remainder so that max is divisible by 10, then return max with 10 added.
            function getMaxStatistic(row,stat)
            {
                var max=1;
                var temp;

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

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

                if (max % 10 == 0)
                {
                    return max+10;
                }
                else
                {
                    return max - (max % 10) + 10;
                }
            }


Now for the displayScale() function! We passed the maximum statistic of the dataset through this function, remember? Here, we grab the graph_scale element and set it to the variable scale. We set the innerHTML property to a blank value to clear the contents of scale.

Next, we define two new variables, units and data_unit. units is the value passed into this function (which is the maximum value of the dataset) divided by 10.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;
            }


Now we're going to work backwards, because on a scale, the top number is displayed first. So our For loop starts from units and works its way down to 1, decrementally.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;

                for (var i=units;i>=1;i--)
                {

                }
            }


Here, we set data_unit to a created div element, then set its class to scale_unit. Its height is defined by a percentage of its parent. We determine the percentage by dividing 100 by units. Finally, the div is labelled with the value of (i x 10), which means the scale is always in intervals of 10. Then we append the created div within scale.
            function displayScale(maxval)
            {
                var scale = document.getElementById("scale_container");
                scale.innerHTML="";

                var units = maxval/10;
                var data_unit;

                for (var i=units;i>=1;i--)
                {
                    data_unit = document.createElement("div");
                    data_unit.className = "scale_unit";
                    data_unit.style.height = (100/units) + "%";
                    data_unit.innerHTML = i * 10;

                    scale.appendChild(data_unit);
                }
            }


Here's the styling for the CSS class scale_unit. The width is 100% of its parent, and the border for the top and left edges are set to a thin black line. The rest is all cosmetic.
            #scale_container
            {
                height:80%;
                width:20px;
                float:left;
                border-bottom:1px solid #000000;
            }

            .scale_unit
            {
                width:100%;
                border-top:1px solid #000000;
                border-left:1px solid #000000;
                color:#FF0000;
                text-align:right;
                font-weight:bold;
                font-family:verdana;
                font-size:0.5em;
            }



Aaaaand you have a scale!


This is a bit messy, so just change the code here.
div {outline:0px solid #FFAA00;}


Nice. Still a bit messy, but it works. We have a placeholder for all the data we're about to manipulate!


Editor's Note: The dataset for Fernando Torres was found to be incorrect, and later, edited accordingly. As such, some of the screenshots are also incorrect.

Next

Altering the display to fit the data.