Monday 30 March 2020

App Review: Wilderness Survival

Here's a game with a difference. It's not an RPG, there are almost zero animations and definitely no fancy storyline.



I'm pleased to present the unimaginatively-named Wilderness Survival. Its creator is a dude by the name of Juuso Hietalahti and it started as a little project on the Google Play Store. I played it years ago in its infancy, donated a few dollars towards its development and watched it grow to the beast it is today.

The Premise

You, the player, are in some kind of predicament. In Easy Mode, your car has broken down in the middle of a storm and you need to make your way to safety. In harder modes, you need to trek your way through increasingly hostile terrain. In all cases, hunger, cold, dehydration and poisoning are your constant enemy... and that's if you don't meet wild beasts.



On the way to survival, you will need to examine your inventory often to ensure that your equipment is conducive to your survival - clean drinking water, food, fire-starting materials, rope and so on. You'll also have to create or procure this equipment from the surrounding terrain.

The Aesthetics

This game is visually grim. The colors are stark shades of orange, brown and black. It's stylish, but also not very inviting and deliberately so. Really puts you in the mood.



The hand-drawings of your equipment can come off as really basic, but it fits the theme superbly.


The Experience

This game is really basic, and unless you have some allergy to reading, pretty damn fun. It's like reading a book of your survival, as you make choice after choice. There are immersive elements too, so if you're an obsessive type of reader, this could be for you.

The Interface

It's all tapping on various options to bring you to the next interface. Nothing else required. In fact, it's very much inventory management with a ticking clock in the background.


What I liked

Difficulty levels. You get to ease yourself into the game without being overwhelmed by the several options available to you.




As in probably any survival scenario, luck plays a huge part. Make the wrong choice and you die. Make the correct choice and you might still die. There are so many ways to perish in this game - starvation, hypothermia, dehydration, to name a few - that you very rarely get a chance to catch your metaphorical breath.





The challenges. One of my main gripes used to be that there were very few. Fast forward a couple years later, there's a decent amount of these to meet.


The atmosphere is terrific, what with sound, vibration and the screen turning red to show you that you're in deep shit and the game isn't fucking around. No, really. Play with sound on, yo.


Item creation combinations to get all sorts of cool survival shit. This is like 80% of the game right there.


Things getting done slower and with a higher failure rate when you're low on energy. Intense!


It's a small download. Easy on your battery life, too.

What I didn't

It's possible for a lake to run out of fish in just a couple days from a single fisherman?! Come on.

There's next to no clue as to where to go next. I mean, a friggin' compass and a general direction would be nice. Instead, all we get is terrain descriptions and a distance.


All that fishing and trap-setting and water-gathering can get repetitive. But I guess it's an unavoidable part of the whole survival shtick.

You may find all that inventory management tiresome. It's certainly not for everyone.

Conclusion

Wilderness Survival can be an annoying game to play at times especially if you hate inventory management. But there's just something charmingly basic about the entire concept. Should be good for a few days (or even weeks) of gameplay while you figure out how to not die in the game.

My Rating

7 / 10

Give it a go. You'll have a wild time.
T___T

Wednesday 25 March 2020

A COVID-19 Election

It's getting to be a habit, me talking about the COVID-19 outbreak. I was planning to write about something else this week - something techy and probably boring as shit - but this seemed important.

On the 13th of this month, the new electoral boundaries were released. This may mean nothing to readers outside of Singapore, but over here, it's huge. Traditionally, the Singapore General Elections follow soon after. That's the time every four years, that politicians throw shade at each other over passionate and well-staged political rallies, we all vote however we wish, and the ruling party gets reelected... most of them, anyway. That's the Singapore General Elections in a very simplistic nutshell.

(Courtesy of ChannelNewsAsia)

This year is a bit different, however. With the outbreak situation hanging over everyone's heads, a situation where being in a crowd could get you infected with COVID-19, most people are naturally concerned with their impending mortality. The ruling party was accused of political opportunism. After all, who is likely to risk a change in Government during a crisis like this? I'm pretty sure that this, while not the entire reason for the timing, certainly didn't hurt.

But when you're the ruling party in a democracy like Singapore, in an outbreak situation like this, you don't have much choice. Actually, scratch that. The ruling party does have choices. It's just they have only bad choices, and worse choices.

Hold an election during the outbreak of COVID-19? Bad, bad choice.

Hold it a year later when things the situation could be messier? Worse choice. Much worse.

Honestly, I'm not a fan of the ruling party. In fact, if I weren't a software developer, I'd have a lot less sympathy for them right now.

You know how some users expect the system they're using to be secure?  A secure system needs to adhere to certain standards. Each user needs to be authenticated by the system. No user or intruder should see any more information than they're supposed to. Sensitive information should be encrypted. Passwords need to be of a certain length, and conform to accepted conventions.

System security

But then those very same users complain if the authentication inconveniences them, and sometimes bargain with developers for short-cuts.

I'm sorry, but if you want a secure system, you're going to have to take everything that comes with it. Otherwise, it wouldn't be a secure system. You can tell me that the interface sucks and to improve on it - that I'll gladly do. But you can't gripe about having to change passwords, or ask me not to encrypt data if it slows down the system. Well, actually you can... and you can expect me to never take you seriously again.

Zero inconvenience means you can get into a system where it recognizes you without you having to provide any kind of authentication that can't be faked by an intruder. Security inconveniences people. That's what it's supposed to do. And ideally, it inconveniences intruders more than it does legitimate users.

Some users only want security from a system as long as that security doesn't inconvenience them. Similarly, it seems some citizens are all for the concept of democracy but can't handle the ensuing obligations. Singapore is a democracy. That's not an accident. It's a cornerstone of our National Pledge - a pledge that was drilled into our young minds growing up.

We, the citizens of Singapore, pledge ourselves as one united people, regardless of race, language or religion, to build a democratic society, based on justice and equality, so as to achieve happiness, prosperity and progress for our nation.


And democracy comes with certain legalities that we are bound by. According to the Constitution, our Government cannot legally remain in power past April 2021 without an official result from an election. That's non-negotiable, COVID-19 or no COVID-19. You can't simply say "it's an emergency, let's do away with the election and just let them remain in power until the crisis is over". That's not how a democracy works. If we allowed our Constitution to be so casually amended, Singapore would become a joke in the eyes of the world.

The Singapore Government is legally obliged to hold an election. There are no two ways about it.

But they have up to April 2021!

That's what I keep hearing. "Why not hold it a year later when things are better?"

"When" things are better? When? Anybody who can offer such a bold guarantee should be predicting stock market trends for a living. I'm not sure if you got the memo, but this situation is forecast to last at least a year, and beyond. There is no guarantee - not even a reassuring probability - that things will be better in a year. In fact, things have gone decidedly south since late last year when the virus first reared its ugly head.

Feeling lucky?

Do you really expect our Government - or any Government, for that matter - to take that kind of gamble knowing what's at stake?

You don't have to like it. I sure as hell don't. In an ideal situation, this would not even be an issue. But there is nothing ideal about what's going on right now. It is what it is. We're going to have to deal with it.

Reactions

The ruling party's political opponents were definitely having a field day slinging accusations of the ruling party jeopardizing the public’s health and well-being for political gain, and acting irresponsibly, among other things.

You know what, that's fine. It's politics. I expect a certain amount of mud-slinging and no small measure of drama.

Bring on the drama.

But if you are going to accuse the ruling party of such things, citing our health and well-being as a concern and then proceed to contest anyway - an act which ensures that the public will need to vote and thus risk their health and well-being - I will consider you an accomplice, and treat you accordingly.

If you think that's unfair, perhaps next time you level accusations, you will consider the possibility that someone is going to challenge you to put your money where your mouth is.

Conclusion

Make no mistake, having an election in the midst of a COVID-19 outbreak is a huge inconvenience. Potentially even a fatal one. But you know how some of us Singapore dudes like to talk a big game about how we spent two years (or more) of our lives fulfilling our National Service obligations and preparing to die for our sovereignty and all that shit? Well, this time you might actually get a chance to do so.

Our Government has weighed their options and decided that preserving the legitimacy of our democracy is worth the risk. What will be your reply? How seriously do you take your democracy?

Democracy or death,
T___T

Saturday 21 March 2020

Contact Tracing With TraceTogether

As the pandemic that is COVID-19 rages on unchecked throughout Europe, Singapore's approach to battling this outbreak has won international praise. To be sure, it hasn't all been smooth sailing. Singaporeans tend to fall into two extreme categories - those who sow distrust in the Government, adopting an every-man-for-himself approach and obstinately refusing to cooperate; and those who complacently think that the Government will handle this without them needing to lift a finger to do their part. Both are equally toxic to a situation like this, and potential poster boys for Darwin's Theory of Natural Selection.

But back to the topic. Singapore's approach has been to aggressively track down people whom infectees have been in contact with, so as to identify other possible infectees and potentially prevent more avenues of infection. It's worked - to a point. While the numbers of infected people have been increasing, without these efforts, one shudders to imagine how much worse it could have been by now.


And now, with the aid of mobile technology, Singaporeans have the opportunity (one might even say, a duty) to help.

GovTech, in conjunction with other Government statutory boards, has released a mobile app named TraceTogether. Once installed and the appropriate permissions granted, TraceTogether runs in the background and via Bluetooth, keeps records of other cellphones in the vicinity running the same app. Should any one of the owners of these phones be diagnosed with COVID-19, the Government is able to locate all those people who were in close proximity to the infectee.

This is a concept eerily similar to the plot devices of many modern movies. And it's awesome.

Of course, it would be unethical if the Singapore Government surveilled you in such a way without your express permission; thus the app takes pains to acquire it during the setup.





And even after setup, the app does not automatically upload the collected data to the Government. No, you have to explicitly do it yourself.


The setup is pretty easy, though not without some annoyances here and there. Friends inform me that the app doesn't run in the background on iOS products. The app sends an OTP but then immediately auto-approves the OTP without the user specifically keying it in, leading to some confusion. The app doesn't seem to sale very well, flashing a server overload when more than a certain number of people started signing up. The text and buttons aren't well-spaced in spots. Things like that.

But it would be grossly unfair to quibble over all that. TraceTogether is obviously a Minimum Viable Product that was hastily hammered out over the course of a couple sprints, and as such, there are sure to be a few rough edges. The concept is terrific and has the potential to greatly ease the task of contact tracing. That's all that matters at the moment.

Stuff like this is why I was interested in joining GovTech before. Projects that matter. Projects that make a difference. The dev team at GovTech sure lives in exciting times!

Final Note

Please install the app, and encourage others to do likewise. It could make all the difference.

Without a trace of irony,
T___T

Sunday 15 March 2020

More About Foreign Talent in the Tech Sector

The ongoing Wuhan Coronavirus scare dredged up some rather ugly behavior towards China Nationals in recent weeks. It seemed as though fear and panic had resulted in years of civility being flushed down the proverbial toilet, and human nature reasserting itself in ugly ways.

But that's wishful thinking. That underlying current of xenophobia was around long before we ever heard the name "Wuhan". The simmering resentment that foreigners take well-paying jobs away from Singaporeans. The feeling of superiority over other Southeast Asians. Among so many other things.

This is not, by the way, confined to the Chinese Singaporeans who make up the majority of Singapore's population. I've had an Eurasian Singaporean complain bitterly to me about being the target of racism one moment, and wax lyrical about how all Filipinos are two-faced scoundrels the next.

Common complaints about foreigners

They take well-paying jobs away from locals. Firstly, well-paying jobs in an air-conditioned office are not a God-given right. They're something we have to strive for, and they don't automatically belong to us. All the foreigners are guilty of, is offering employers an alternative. If you only have a job because no one was fighting you for it, that's not much of an accomplishment.

They steal our women. Two things. Firstly, local women don't automatically belong to local men. See my earlier point. Secondly, local women don't belong to anyone period, because they are not cattle. To accuse foreigners of "stealing our women" is both gross and backward. Local lads, be better than this.

Same goes for women who complain that foreign women "steal your men". We're not your men. Level the fuck up and you won't have that problem.

They don't fit in with our culture. What culture? A culture that's been a melting pot of different races, religions and languages for the past fifty-odd years? I see no change in that dynamic.

I see assimilating foreigners as a task akin to contributions into a code repository. The code comes from different developers, continuously, all from different backgrounds and programming styles, and is merged into a coherent whole that is far larger than the sum of all its parts. Programming is a team sport; and - surprise, surprise - so is the exercise of nation-building. Of course, as in programming, you are going to get rubbish code and bugs. But with time and patience, it all eventually gets straightened out and the code base is all the better for it.

They crowd this island. I'll grant you that. On weekends, some of the places I frequent, like the local swimming complexes, are packed with foreigners getting in my way. I put up with it as a natural consequence of attracting foreign talent.

A crowd in sunny Singapore.

There is a common thread to these complaints - that foreigners have the audacity to compete with "True Blue Born-and-bred" Singaporeans for jobs, partners and space. Where you were born and bred is an accident of birth, nothing more. Expecting special treatment simply for having been born on this island speaks of supreme entitlement and smacks of loser-stink. Naturalized citizens are citizens all the same, and I refuse to treat them any different.

I have two grandfathers, both of whom were born in the province of Guangdong, China, though they were from different cities - one Foshan and one Chaozhou. I'm pretty sure neither spoke English when they immigrated to Singapore, and might not have started even on their respective deathbeds. My story isn't unique by any means - there are at most four to five generations of people who can legitimately claim to be "True Blue Born-and-bred Singaporeans".

(Four to five generations, and people want to act like being a "True Blue Born-and-bred Singaporean" is some mark of a great civilization spanning centuries? Really?)

I'm not unfamiliar with these complaints - decades ago, I may have voiced some of them myself. I thought many of the foreigners employed here were shitty and backward. It was only after a period of serious introspection that I realized it wasn't foreigners (shitty or otherwise) that stood in my way. It was my own damn attitude. That was when I put my own ego aside, rolled up my sleeves and got to the serious business of salvaging my career. And the things I learned about both myself and foreigners in the process were not anything I had anticipated.

One caveat, though...

I don't mind that foreigners take up well-paying jobs. I don't mind that many of them earn more money than I do. If they wish to work here and share their expertise with Singaporeans, I welcome it and am grateful for the exchange.

Where my goodwill ends, however, is when foreigners attempt to meddle in my country's politics. I have an absolute zero-tolerance policy on this. The issue of Singapore's Governance is one for Singaporeans to resolve... among Singaporeans. Foreigners are not welcome to tell me, or even suggest, whom I should vote for.

Singaporean politics are
for Singaporeans only.

I do not attempt to tell Americans who they should elect as President. When my Malaysian tenants discussed politics in my house during the 2018 Malaysian General Elections, even though I was none-too-thrilled with the thought of Dr Mahathir resuming power, I never once tried to sway their vote. As a Singaporean, I inevitably had an opinion on that matter; it was however not my place to impose it.

Long story short; I don't interfere in the politics of other countries, and I expect foreigners to return the goddamn courtesy. And since I don't differentiate between "True Blue Born-and-bred Singaporeans" and naturalized citizens, I naturally include ex-Singaporeans in the category of "foreigners". It's only fair.

Foreigners I've worked with

So yes, I've heard plenty of anecdotes about how foreign talents are a bunch of trash. But, as they say, nothing beats personal experience. Here are some of my findings about foreigners I've been privileged (and in some cases, condemned) to work with. My findings, of course, are particular to the tech sector. Your mileage may vary.

Filipinos. Colloquially known as "Pinoys", they have a reputation for being conniving, two-faced and underhanded. I've worked with plenty of Pinoys. A couple of them were really lazy and unmotivated, and there might have been some of them who fit the "underhanded" stereotype to a T. But for the most part, I've found them jovial and boisterous, and fun-loving. And very cheeky. Not bad traits to have. And a fair number of them were actually (gasp!) competent.

Everything I know about SEO, I began with the basics from a Filipino ex-colleague. And she was always obliging when I called her to ask questions, long after our professional relationship had ended.

Foreigners at work.

Indians. Not our local variety, but the ones from the great continent of India herself. People like to further divide them into North and South Indians, but I really couldn't give less of a shit. In the workplace, they're notorious for being bullying, loud-mouthed braggarts who show off every chance they get, but dodge the hard work. I've worked alongside many Indians as well. We have a cordial relationship, and they can be a humorous bunch. Maybe one has conformed to the negative stereotypes, but I wouldn't tar them all with the same brush. And again, while competency ranged from inept to talented, for the most part, they were pretty good.

From Indian blogs, I learned finer points about coding. One Indian ex-colleague helped me level up tremendously in my CSS. I probably know a lot more CSS than he does now, but it was that dude who gave me that leg-up when I needed it.

Burmese. I'm not aware of any stereotypes concerning the Burmese though I've had many Burmese colleagues. Generally, they've always been friendly and willing to share their knowledge. Although, most of them do seem to have a problem getting their point across. Also, I really hate their food.

What did I learn from the Burmese? Mostly, a lot of C#. I also learned the importance of soft skills, because they were terrible at it. Not that they weren't pleasant, but many of them simply didn't inspire confidence in product owners. I like to think of it as a communication gap that needs to be bridged.

Chinese. Not the local variety (like myself) but those hailing from the mainland. People say they're egoistical and lack social graces. I've certainly come across more than a few fitting that description. But almost all of them were very good at what they did. Almost without exception, they were ambitious and driven - both competitive and (usually) competent. And some of them displayed a level of street savvy that left the stereotypical crafty Pinoy in the shade.

Most of the Chinese I worked with were Middle Managerial level. They were the ones I aspired to be like - both able to delegate and technically excellent.

Of all these foreigners I worked with, did some of them exhibit negative traits? Did some of them have the unfortunate tendency to talk out of their arse? Did some of them have habits that annoyed the living Beejeezus out of me? Were some of them so incompetent that I was amazed they remembered how to breathe? Undoubtedly. But those are not racial traits. Those are individual traits and should be taken as such. I've not always been the most competent and conscientious co-worker, and sometimes I appreciate a little bit of understanding and patience from my colleagues.

The thing about stereotypes is, if we're willing to assign them to foreigners, we have to be equally willing to accept stereotypes about ourselves. If I went by the experiences my organization had hiring Singaporeans, we would undoubtedly be pigeonholed as a bunch of incompetent, spoiled and entitled losers who want cushy, well-paying jobs without having the qualifications to do them. Are we? Undeniably, some of us are. But all of us, as a whole? Does that sound rational to you?

If the answer is "no", then we really need to rethink the stereotypes we so readily assign others. For example, stop automatically associating Pinoys with maids, Thais with hookers and Bangladeshis with construction workers. It does everybody a disservice.

Adapt or perish!

Globalization is here to stay. This is an increasingly connected world due to the Internet, and walling ourselves away in silos isn't going to help anyone.

I worked alongside foreigners for years. I took the opportunity to learn my craft from them. My tenants are foreigners, and they add to my already excessive income. I married a foreigner. In short, I accepted the fact that foreign talent is here to stay, and adapted so that the situation worked in my favor.

It's not the first time I wrote about foreigners in the tech sector. My stance hasn't changed; we do need them. Foreigners have flooded this island for the past decade, for good or ill. It is my honest opinion that everyone should adapt to this. After all, if adaptability isn't a particularly strong trait of yours, there is nowhere on God's green earth you would ever be comfortable in.

If you can't adapt, nowhere is home.

Singapore is no isolated backwater no matter what some of us wish. It is a First World Nation, and we, as a people, need to live up to that label.

Your "True Blue Born-and-bred" Singaporean,
T___T


Wednesday 11 March 2020

Some Rumination On Internal Links

When your page has a lot of content and scrolling up and down rapidly becomes an annoyance, Internal Links are your best friend. In this day and age of reading content on mobile devices and therefore tolerating the limitations on screen width, this has become more important than ever.

What Internal Links do is that when clicked on, they redirect you to another section of the page you're currently on.

Internal Links aren't new. They've been around since HTML's birth. It doesn't seem so long ago that I was using Internal Links like they were going out of fashion. Although, time may have passed faster than I like to admit. Because they did go out of fashion; or rather, the method of implementation did.

Let me explain...

How we used to implement Internal Links was, first define an anchor tag, with a name attribute. Here, I've used an ultra-long page with three thick paragraphs as an example.
<a name="top">
 
<h1>Header</h1>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ac erat egestas, congue nibh nec, sagittis ipsum. Maecenas et lacinia quam, sagittis blandit odio. Etiam nec dolor est. Maecenas quis libero ullamcorper, vulputate tellus vitae, viverra magna. Praesent eu lectus finibus, pellentesque augue id, dignissim sem. Maecenas vitae leo at augue dapibus maximus. Sed condimentum ipsum eu magna sollicitudin rutrum. Morbi libero neque, consequat sed metus ac, ultricies vehicula elit. Donec sed laoreet ex. Cras auctor, justo placerat tincidunt dictum, nisi purus ultrices risus, eleifend suscipit neque tellus sit amet ligula. Aenean quis vestibulum justo, eget tempor dolor. Pellentesque aliquet ornare sem, vitae porta odio mattis sed.
</p>

<p>
Maecenas placerat mollis suscipit. Duis non sollicitudin magna. Maecenas blandit eget tortor at feugiat. Nam eget scelerisque ex, hendrerit imperdiet nisi. Vestibulum felis felis, congue eget neque ut, auctor placerat urna. Suspendisse molestie dapibus lobortis. Donec venenatis porta tristique. Donec ullamcorper, dui eu dictum congue, velit nisl rutrum augue, et vehicula odio tellus eu lacus. Ut eros lacus, finibus eu lacus nec, tincidunt laoreet diam. Nullam id tellus laoreet, sagittis leo sit amet, dignissim ligula. Morbi lacus turpis, ultricies et libero at, egestas cursus libero. Aenean consectetur tempor sapien vel lacinia. Cras odio nulla, pretium vitae euismod non, placerat at sem. Maecenas gravida placerat fringilla. In quam purus, lobortis et interdum nec, interdum a felis. Praesent ligula ligula, semper quis malesuada in, venenatis a ligula.
</p>

<p>
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus eu est a justo hendrerit condimentum. Vestibulum id ullamcorper urna. Ut ut rutrum lorem, at dignissim sem. Vestibulum ac leo urna. Mauris sollicitudin bibendum leo, sed pretium quam sodales nec. Proin convallis ut nunc commodo fringilla.
</p>


Then create another a tag, this time with the href attribute using the pound (#) sign and the name of your anchor.
<a name="top">
 
<h1>Header</h1>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ac erat egestas, congue nibh nec, sagittis ipsum. Maecenas et lacinia quam, sagittis blandit odio. Etiam nec dolor est. Maecenas quis libero ullamcorper, vulputate tellus vitae, viverra magna. Praesent eu lectus finibus, pellentesque augue id, dignissim sem. Maecenas vitae leo at augue dapibus maximus. Sed condimentum ipsum eu magna sollicitudin rutrum. Morbi libero neque, consequat sed metus ac, ultricies vehicula elit. Donec sed laoreet ex. Cras auctor, justo placerat tincidunt dictum, nisi purus ultrices risus, eleifend suscipit neque tellus sit amet ligula. Aenean quis vestibulum justo, eget tempor dolor. Pellentesque aliquet ornare sem, vitae porta odio mattis sed.
</p>

<p>
Maecenas placerat mollis suscipit. Duis non sollicitudin magna. Maecenas blandit eget tortor at feugiat. Nam eget scelerisque ex, hendrerit imperdiet nisi. Vestibulum felis felis, congue eget neque ut, auctor placerat urna. Suspendisse molestie dapibus lobortis. Donec venenatis porta tristique. Donec ullamcorper, dui eu dictum congue, velit nisl rutrum augue, et vehicula odio tellus eu lacus. Ut eros lacus, finibus eu lacus nec, tincidunt laoreet diam. Nullam id tellus laoreet, sagittis leo sit amet, dignissim ligula. Morbi lacus turpis, ultricies et libero at, egestas cursus libero. Aenean consectetur tempor sapien vel lacinia. Cras odio nulla, pretium vitae euismod non, placerat at sem. Maecenas gravida placerat fringilla. In quam purus, lobortis et interdum nec, interdum a felis. Praesent ligula ligula, semper quis malesuada in, venenatis a ligula.
</p>

<p>
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus eu est a justo hendrerit condimentum. Vestibulum id ullamcorper urna. Ut ut rutrum lorem, at dignissim sem. Vestibulum ac leo urna. Mauris sollicitudin bibendum leo, sed pretium quam sodales nec. Proin convallis ut nunc commodo fringilla.
</p>

<a href="#top">Go to Top</a>


This is the page. Scroll down until you see a "Go to Top" link.


When you click on the "Go to Top" link, it brings you back to the top of the page!


I've got Good News and Bad News...

The good news is, Internal Links are still around.

The bad news is, what I've just shown you has been deprecated since XHTML. Yep, it's been that long. Don't take my word for it - it's in the XHTML specification. Some browsers may still render it that way, but don't count on it.

We've been using HTML5 for a decade now and my brain is still stuck at old clunky classic HTML where Internal Links are concerned, for some reason. Go figure.

The New Way

Instead of a name attribute, define the anchor by using the id attribute, like so.
<a id="top">

<h1>Header</h1>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ac erat egestas, congue nibh nec, sagittis ipsum. Maecenas et lacinia quam, sagittis blandit odio. Etiam nec dolor est. Maecenas quis libero ullamcorper, vulputate tellus vitae, viverra magna. Praesent eu lectus finibus, pellentesque augue id, dignissim sem. Maecenas vitae leo at augue dapibus maximus. Sed condimentum ipsum eu magna sollicitudin rutrum. Morbi libero neque, consequat sed metus ac, ultricies vehicula elit. Donec sed laoreet ex. Cras auctor, justo placerat tincidunt dictum, nisi purus ultrices risus, eleifend suscipit neque tellus sit amet ligula. Aenean quis vestibulum justo, eget tempor dolor. Pellentesque aliquet ornare sem, vitae porta odio mattis sed.
</p>

<p>
Maecenas placerat mollis suscipit. Duis non sollicitudin magna. Maecenas blandit eget tortor at feugiat. Nam eget scelerisque ex, hendrerit imperdiet nisi. Vestibulum felis felis, congue eget neque ut, auctor placerat urna. Suspendisse molestie dapibus lobortis. Donec venenatis porta tristique. Donec ullamcorper, dui eu dictum congue, velit nisl rutrum augue, et vehicula odio tellus eu lacus. Ut eros lacus, finibus eu lacus nec, tincidunt laoreet diam. Nullam id tellus laoreet, sagittis leo sit amet, dignissim ligula. Morbi lacus turpis, ultricies et libero at, egestas cursus libero. Aenean consectetur tempor sapien vel lacinia. Cras odio nulla, pretium vitae euismod non, placerat at sem. Maecenas gravida placerat fringilla. In quam purus, lobortis et interdum nec, interdum a felis. Praesent ligula ligula, semper quis malesuada in, venenatis a ligula.
</p>

<p>
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus eu est a justo hendrerit condimentum. Vestibulum id ullamcorper urna. Ut ut rutrum lorem, at dignissim sem. Vestibulum ac leo urna. Mauris sollicitudin bibendum leo, sed pretium quam sodales nec. Proin convallis ut nunc commodo fringilla.
</p>

<a href="#top">Go to Top</a>


Why this makes sense

This makes things more consistent. You know how jQuery uses that very same pound (#) sign to denote the id of a DOM element? The example below assigns the example object to the variable obj.
var obj = $("#example");


Yep, in XHTML (and HTML5), the pound (#) sign for Internal Links will refer to the id attribute as well, as opposed to the name attribute.

This has been a public service announcement from your friendly neighborhood web crawler developer!

#kthnxbye,
T___T


Saturday 7 March 2020

Web Tutorial: D3 Bar Chart (Part 4/4)

There are so many ways the look and feel of this chart could be improved.Let's begin. This will be fun, I swear.

Lines

Let's begin with a few lines. Visually, well-placed lines help increase the readability of a chart.

The scale needs a vertical line, for sure. Just add one line tag, style it using the existing CSS class barChartLine.
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", "barChartLine");

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 - (d * config.scale)) + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


Then set the x1, x2, y1 and y2 values. Since this is a vertical line on the right side of the scale, x1 and x2 will share the same value, which is the scaleWidth property of the config object.
scale
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return config.scaleWidth + "em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
});


For y1, it's 0em because it starts at the top. y2's value is height, because it will end at the bottom.
.append("line")
.attr("class", "barChartLine")
.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";
});


Do you see the vertical line? Makes the scale look better already, doesn't it?


Let's do the same for the legend. In legend, append a line tag and style is using barChartLine.
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", "barChartLine");


This is a horizontal line, spanning from the left to the right of the chart area. So x2 is obviously 0em, and x2's value is width.
legend
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
});


y1 and y2 have the same value, and since the line will be on the top edge of the legend, the value is 0em.
legend
.append("line")
.attr("class", "barChartLine")
.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";
});


There's the horizontal line beneath all the bars.


Now let's have horizontal lines across the chart, to visually aid in deciphering the values.

First, create the barChartFadedLine CSS class. It's a black line, but at 20% opacity.
.barChartLine
{
    stroke: rgba(0, 0, 0, 1);
    stroke-width: 1px;
}  

.barChartFadedLine
{
    stroke: rgba(0, 0, 0, 0.2);
    stroke-width: 1px;
}


We place the code here because the lines have to appear behind the bars. For the data() method, we use scaleData which has already been calculated. We will style the lines using the CSS class we just created.
scale
.append("line")
.attr("class", "barChartLine")
.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", "barChartFadedLine");

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 - (d * config.scale)) + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


These are all horizontal lines spanning the left and right side of the chart, so x1 is 0em and x2 is width. Am I sounding repetitive yet?
chart.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartFadedLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
});


y1 and y2 will share the same value - refer to the scale in the previous part of the tutorial, it's the exact same logic.
chart.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartFadedLine")
.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";
});


Nice!

Stabilizing chart size

When we select different years or stats, the data changes and the chart size changes with it. That's how it was designed. But that's kind of annoying, and it's hard to fit this chart on a page with other things if it's going to constantly change size.

So, what if we just calculated the maximum value regardless of year or statistic, and then used that consistently?

Delete this line.
config.mean = d3.mean(dataSet.stats, function(d) { return d; });
//config.max = d3.max(dataSet.stats, function(d) { return d; });


Now, outside of the config object and just before declaring the ddlYear variable, we set the max property of the config object. Why do we declare that outside? That's because it's computationally expensive, and we don't want to be repeating it every time we run the setData() method, especially when the result is the same no matter what data we are viewing.

Again, we use the max() method of the d3 object. Pass in the cols array of the graphData object.
    }
};

config.max = d3.max(graphData.cols, function(d)
{

}
);

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


We're going to create a nested function, reusing max(). Declare the variable maxStat, and return it.
config.max = d3.max(graphData.cols, function(d)
{
    var maxStat;

    return maxStat;
}
);


We set maxStat using the max() method again, and this time, we pass in the stats array of d, which is the current object being traversed from cols.
config.max = d3.max(graphData.cols, function(d)
{
    var maxStat = d3.max(d.stats, function(x)
    {

    }
    );

    return maxStat;
}
);


Here, we want to return either the goals or appearances stat, whichever is greater.
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;
}
);


Oh yeah. Come to daddy, gorgeous! Now no matter what data you are viewing, the chart stays enlarged!


Color Scheme

This is a Liverpool statistic bar chart, yes? So let's make it red and yellow. First, set the various colors we've given the SVGs to 0% opacity, effectively giving them no color.
.barScaleSvg
{
    width: 5em;
    height: 20em;
    float: left;
    background-color: rgba(0, 255, 0, 0);
}

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

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

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


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

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


Set the background color for the barChart CSS class to a deep red.
.barChart
{
    outline: 1px solid #000000;
    background-color: rgba(200, 0, 0, 1);
}


What a dramatic difference!


Now let's set text and lines to yellow. Also, set all rect tags in the barChartSvg CSS class, to have a yellow background.
.barScaleSvg text
{
    fill: rgba(255, 255, 0, 1);
    text-anchor: end;
    font-size: 0.5em;
}

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

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


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

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

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

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


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


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

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


Beautiful!


Let's clean up a bit. There's one tiny line that's sticking out at the bottom of the scale. In this segment, just ensure that if d is 0, we don't style it using barChartLine. It's lazy, but meh, it works.
scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", function(d)
{
    return (d == 0 ? "" : "barChartLine");
})
.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";
});


Nice and clean.


Animation

Just for fun, eh?

We want the bars to "grow" from the bottom of the chart. D3 has animation functions too. Let's use them!

We are going to animate the y and height attributes. And the initial state is that the bars are flat against the bottom of the chart. Therefore, height is 0em, and y is height.
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";
});


Use the transition() method and chain it with the duration() method, putting in 500 as an argument so that the animation lasts half a second.
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);


Now, set y and height to the same values they were before we started messing with this code!
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";
});


Here's a look at the final product. I've set the scale property of the config object to 0.2, just so there's less scrolling.




That's all!

It really looks like using D3 to build a chart involves a lot of work. But if you've been through the vanilla JavaScript version of this web tutorial, you would have observed that a lot of the tasks are automated here, the code is easier to test, and things look a lot more consistent across browsers. These are the benefits of using a library like D3, benefits you would only realize if you have done things the hard way.

Bar humbug,
T___T