Showing posts with label D3. Show all posts
Showing posts with label D3. Show all posts

Friday, 8 May 2026

Ten Reasons To Keep Treating Job Interviews As Target Practice

For the longest time, I've been an advocate of getting practice in job interviews... using actual job interviews. I make no apology for this; in fact, at a certain stage in my career, I made it a point to do this regularly. And I will continue to advise professionals to keep applying for jobs and going for job interviews, even - or especially - if they have no current intention of switching jobs. Hey, life is about at least opening new doors to see what lies beyond, if not going through them.

Opening new doors.

Don't feel too bad about potentially wasting the time of the companies you apply at. How many companies post job ads and conduct interviews for positions they have no intention of filling, just so they can technically bypass the requirements set out by the TAFEP? Two can play at that game.

Plus, there are some really compelling reasons to do it. Here's a list of some really big ones.

1. Muscle memory

You know what they say about practice making perfect? Well, there's no such thing when it comes to interviews (or anything, really). But you can get awfully good at it. What about mock interviews? Well, mock interviews are great and all, but at some point you're going to have to stop hitting the punching bag and square off against someone who's actually trying to hit you back.

This won't hit back.

People also tell me that they have better things to do than to attempt interviews "for fun". This is a huge misrepresentation of what I'm recommending. Of course, if it's fun for you, that's a bonus. But it's not the main point.

Nothing about this is supposed to be fun. Think of this as working out, where you condition your body, strengthen it, in the event that one day it might be ready when called upon. This is the career interview equivalent of exercising in a gym.

2. Confidence

At the very least, you'll get a lot more comfortable at it.

Even when it's a high stakes interview where there's a lot riding on it and you actually care about the outcome, there's no denying that prior practice helps. A big part of you will feel like you've seen and done all this before. There will be a certain familiarity to the entire process that, due to the comfort said familiarity brings, will go a long way towards offsetting any nerves.

Staying chill.

People have remarked on how remarkably at ease I seem during job interviews. What they don't know is that this has nothing to do with my people skills. I still find other human beings tiresome and annoying. It's simply that after a few hundred interviews with actual companies and dealing with all their bullshit, it stops being fresh and exciting. As a result, it also stops being anxiety-inducing. Seriously, no matter how unique and special some interviewers seem to think their interview process is, the truth is that the vast majority of the time, it's like most other interview processes.

Tech round? Mock project? Whiteboard? I've seen it. I've done it. I've even conducted it. Nothing makes me nervous. Because almost none of it is new.

Reusing the boxing analogy from before, the best way to get over a fear of being punched in the face, is to get punched in the face frequently. Similarly, to get over a fear of rejection from interviewers, you need to get rejected often. From real interviews.

3. Develop market awareness

The tech industry is wide and varied. Even if you narrow it down to software development, or even further down to web development, the fact is that different employers have different requirements. They may need proficiency or even just passing familiarity with new software, new frameworks, new software libraries that you may never even have heard of.

And it's true that you don't know what you don't know. So if you don't even have any idea where to start improving, how are you going to improve? It certainly isn't going to be by repeating the same old stuff over and over. That may work for some industries. But no, not software development.

Laundry list of requirements.

But every time one applies for a new job, guess what - the application often comes with an oh-so-helpful wish list of skills that the employer is looking out for. Sure, some of it is laughably impractical, but this is generally where you can scan the list for things that look unfamiliar to you, and then start exploring.

You know the D3 and HighCharts demos I have done? They both started out as items on a job application list of requirements. I Googled the terms and the rest is history.

4. Broaden horizons

There are sights you don't get too see very often. Huge, sprawling offices that overlook the sparkling waves of Marina Bay. Fancy commercial towers. Lush lobbies.

Not to say you can't see those things without going for a job interview... but really, what better time? Surely not during your free time when you undoubtedly have more pressing concerns and better things to do?

See the sights.

Aside from the sights, sometimes companies have interesting ways of testing interviewers, such as through puzzles or case studies. Some of them were so fun I turned them into web demos. Tetris in vanilla JavaScript? Rock, Paper, Scissors app? All that came from some applicant assignment sheet.

Also, I know I earlier said that most interview questions are lame and formulaic. Most. Sometimes interviewers surprise you with a question you haven't heard before. That's very valuable. It not only expands your experience, it tests your ability to think on your feet.

Take the experience as a way to broaden your horizons. Add to the growing list of stuff you can later confidently claim to have seen.

5. Learn how to conduct

One side effect of having attended a whole shitload of interviews in your career, is that you learn how to conduct them.

Well... not quite.

Let me qualify that. You don't exactly learn how to conduct them. That kind of experience comes from actually conducting them, just as the know-how to get through job interviews comes form actually attending job interviews.

Conducting an
interview.

But you do learn an awful lot about how to conduct them. For example, you would know from firsthand experience how cringey some questions sound, such as the classic "where do you see yourself in five years?" and the ultimate douchebaggy "why should we hire you?", and hopefully avoid being such a cliché.

You would learn, from the interviewee's point of view, what makes a bad impression on them, and avoid falling into those traps.

6. Zero consequences

Using real companies and real job applications for practice, is like honing your craft in a real environment with no stakes whatsoever.

If you weren't all that (or even at all) interested in the job, it doesn't matter if you fail. Your ego might get bruised a little bit, but that's about it.

Target practice.

What are they going to do if they find out you were just using them for practice? Not hire you? Big deal. It's not like you were actually interested to begin with. Practice, remember? Are they going to sure no one ever hires you in the industry? Please, they wish they had this much clout. This is Singapore, not Silicon Valley.

My entire point is that there are no consequences for failure. This is a consequence-free activity dressed up as high-stakes operation. You pretend to yourself that this is a job you really want, though in actual fact you could care less. As interviews go, the only thing less stressful is a mock interview, and those are pretty lame.

7. Future-proofing

"But I'm happy where I am!"

Oh, you sweet summer child. You don't have to be unhappy where you are, to go for a job interview. Especially when the objective is practice. But it probably would add a huge dose of motivation.

If you're happy and
you know it...

It's a hedge against the eventuality that you no longer want to work in your current place of employment, for whatever reason. No relationship lasts forever, especially not one that's predicated on a nebulous human emotion like "happiness".

And when - when, not if - that happens, you'll be glad you bothered. Your resume's already somewhat up-to-date due to all the regular practice. The last interview's still fresh in your mind, along with all the land mines you stepped on in the process. You're ready to go.

At the very least, you'll have gotten a good idea of how much your skills and abilities are worth in the market.

8. Bragging

Anyone with even an ounce of ego likes to brag about their accomplishments. Well, sunshine, here's your chance. A sanctioned space for you to safely brag about all your professional accomplishments. Talk about how you solved the persistent rounding error, that intermittent asynchronous event bubbling issue, and optimized that query to reduce latency to milliseconds.

Not only are you expected to do so, you are encouraged to. That's the entire point - to sell yourself.

I'll confess - this is one of the biggest reasons for me to attend any job interview. Not only do I get to brag, I get to brag to an interviewer who knows exactly what I'm bragging about. That's not a small thing. You see, anyone can brag. But who you brag to makes a hell lot of difference.

Toot that horn!

As a tech, if you're bragging to people who don't have the know-how to properly appreciate the brilliance of what you're bragging about, it can be like casting pearls before swine.

But when you're speaking to someone who has a pretty good idea as to why your accomplishments are impressive, that is sweet. And the cherry on top? Impress that someone enough and you may just win a job offer.

9. Eureka moments

During interviews, you're asked to describe your current work, and the challenges you face. And this can have a few effects.

You'll talk about the problems you're tackling, and how you either solved them, or are attempting to solve them. And we all know what happens when you talk them through someone, especially if you're explaining those problems well enough: you get an Eureka moment. Honestly, a lot of problems are solved just by talking the problem through with someone; anyone actually. You don't really need to be in a job interview to get that Eureka moment.

Your Eureka moment.

But can you get that Eureka moment during a job interview when you're talking about your work? Yes, absolutely. I wouldn't make it the reason to do job interviews, but it's one of the possible side-effects.

10. Gaining Perspective

One thing may happen when you go for those interviews and discover all the things you wouldn't have otherwise. You may actually realize how good you have it. You may gain a newfound appreciation of your workplace, warts and all. If seeing how things are out there makes you happier at your job, that's value.

The grass is browner on the
other side.

Granted, some places seem more fun to work in. They're also potentially messy, have less benefits, and are a pain in the butt to commute to. It could be any number of things, or all of them.

In the process, you may just discover that you're really grateful to have this job. And that's fine. Just don't be too grateful. Your career depends on it.

I like to call it the "grass is browner" syndrome.

Final notes

I haven't followed my own advice in years. I do still think it's good - even great - advice. But for me, it's started to show diminishing returns. The gains just aren't there anymore once you've clocked in the hours. Still, what this means is that there are gains. I can walk into an interview and feel little to no anxiety. I can answer most questions glibly, and even if I don't impress sufficiently in the end, I'm rarely too bothered by it.

Do exercise caution. It's entirely possible that you'll become better at passing interviews than actually doing the work... and that's the last thing you should want.

Let's review this in five years!
T___T

Wednesday, 12 November 2025

Spot The Bug: The Underperforming Chart Scale

Hey, hey! Spot The Bug is back in town, and we're rolling out a new bug-fix story!

Can't wait to
squash another bug.

Working with D3 is one of my favorite pasttimes, especially when I have a new dataset to play with. However, there are some gotchas, and today's episode of Spot The Bug is a story of how one of these gotchas, well, got me.

I had some data that I wanted to plot a chart for. I truncated it a bit for brevity, but it will serve to illustrate the point. Note in particular, the column val.

datavals.csv
date,val
2024-11-01,8
2024-11-02,2
2024-11-03,17
2024-11-04,11
2024-11-05,25
2024-11-06,4
2024-11-07,7
2024-11-08,3
2024-11-09,8
2024-11-10,15


Here's the code I wrote to create an y-axis
<!DOCTYPE html>
<html>
  <head>
    <title>Chart</title>

    <style>
      svg
      {
        width: 800px;
        height: 500px;
      }
      
      .scaleTick, .scale
      {
        stroke: rgba(100, 100, 100, 1);
        stroke-width: 1px;
      }
      
      .scaleText
      {
        font: 8px verdana;
        fill: rgba(100, 100, 100, 1);
        text-anchor: end;
      }
    </style>

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

  <body>
    <svg>

    </svg>

    <script>
      d3.csv("datavals.csv", function(data)
      {
        var dataVals = [];

       for (var i = 0; i < data.length; i++)
       {
         dataVals.push(data[i].val);
       }

       var maxVal = d3.max(dataVals);

        var chart = d3.select("svg");
        chart.html("");

        chart
     .append("line")
     .attr("class", "scale")
     .attr("x1", "50px")
     .attr("y1", "50px")
     .attr("x2", "50px")
     .attr("y2", "450px");
    
        var pxPerUnit = Math.floor(400 / maxVal);
    
        var scale = [];
        for (var i = 450; i >= 50; i -= pxPerUnit)
        {
          scale.push(i);
        }
    
     chart.selectAll("line.scaleTick")
     .data(scale)
     .enter()
     .append("line")
     .attr("class", "scaleTick")
     .attr("x1", "40px")
     .attr("y1", function(d)
     {
     return d + "px";
     })
     .attr("x2", "50px")
     .attr("y2", function(d)
     {
     return d + "px";
     });
    
     chart.selectAll("text.scaleText")
     .data(scale)
     .enter()
     .append("text")
     .attr("class", "scaleText")
     .attr("x", "30px")
     .attr("y", function(d)
     {
     return d + "px";
     })
     .text(
     function(d, i)
     {
     return i;
     });     
      });
    </script>
  </body>
</html>

Here, I got a scale. However, something was wrong. The largest value in my dataset was 25. Why was the largest value here 8?


What Went Wrong

My first clue was here, near the top of the script where I declared maxVal. Because according to this line, max() was returning 8 as the largest value.
var maxVal = d3.max(dataVals);


This would explain why the scale started with the value 8 and worked its way down from there.

Why It Went Wrong

If the largest value in the dataVals array was 25 and the script thought the largest value was 8, there was one explanation for that. The script was comparing alphabetically rather than numerically. As text, "8" is always larger than "25". Basically, the dataVals array was being filled with strings rather than integers from the CSV file.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(data[i].val);
}

How I Fixed It

I used the parseInt() function to convert the data before pushing it into the dataVals array.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(parseInt(data[i].val));
}


And now it took 25 as the largest value! Because now dataVals was filled with integers rather than text, and it was comparing accordingly.


Moral of the Story

You'd think I would have learned to sanitize these things by now, especially in JavaScript, or any loosely-typed language. Apparently not!

Stay int-tellectual,
T___T

Saturday, 1 November 2025

The year I finally achieved that JLPT certification

Some say "better late than never". I got a huge dose of that this year as I obtained my Japanese Language Proficiency Test (JLPT) N5 Certificate.

Why was this overdue? Well, you see, because I actually began on this journey at the tender age of 15. I was a Polytechnic student at the time, and this was a supplementary module. At first, things went well. Having received education in Chinese, learning the basic sounds of Japanese was a breeze. Learning the Hiragana and Katakana writing systems wasn't much of a stretch as well; in fact it was significantly easier than reading and writing Chinese characters. Until we got to Kanji, which essentially was Chinese characters.

Ugh, more Chinese.

At which point I lost all interest.

Also, at this time, I had begun an obsession with writing code, and building stuff. Japanese was a pretty language, but it just didn't fit into my world the same way C++, SQL or even QBasic, did.

And the rest, as they say, is history. Even after graduation, not only did I neglect whatever little Japanese I had learned, the next few decades were a disastrous collection of bad career choices, even worse lifestyle choices, and sobering life lessons.

COVID-19 and Clubhouse

Fast-forward to 2021. COVID-19 had the world in its loathsome grip and I, like many others, sought human interaction online. That was when I discovered the Clubhouse app. Through it, I encountered numerous communities, not all of which spoke English, Mandarin, or even Cantonese.

A few of these were Japanese speakers. And some were even offering Japanese lessons. I dove right in, relearning all the basics I had forgotten. Many times, I would simply listen in, try to make sense of the conversation, and Google words that came up. Then I started engaging, hesitantly, in the conversations. I even participated in Mandarin-Japanese exchanges!

Little cultural exchanges.

It was slow going. I wrote an app to facilitate relearning Hiragana and Katakana. I practiced writing every day, religiously. But there was this feeling that I could do more.

That was when someone suggested that I take the JLPT. And from that day forth, I took things up a notch.

Training in earnest

My next step was to install Duolingo on my phone. For the next few years, I faithfully partook of the exercises daily. At the same time, I went onto YouTube to search for JLPT Listening exercises. I didn't really have a sense of how far I had progressed; I just knew I needed to keep going. I've never been brilliant; but what I am good at is being consistent.

One year later, I downloaded the Migii app, and paid the subscription fees for accessing sample JLPT exam questions. With Duolingo, the returns were diminishing. I needed to train a different set of linguistic muscles - the kind used for passing the exam. Duolingo had brought me to a certain point where I could read the Japanese exam questions without too much difficulty... now I had to practice answering them.

The next milestone was hit another year later when, while watching a Japanese TV show, with English subtitles, I noticed something odd. Almost constantly, the subtitles would read "It's OK". Even though "It's OK" was a perfectly reasonable thing to say in the context of the current point of the story, it was a pretty poor translation.

Phrase Literal Meaning English Translation
daijobu It's OK. "It's OK."
dou itashimashite You're welcome. "It's OK."
shinpai suna Don't worry. "It's OK."
anshin shite Relax. "It's OK."
ki ni shinai de Pay it no mind. "It's OK."


At this point, I found myself thinking - wow, these translators are so goddamn lazy! They just use "It's OK" for everything!

And my next thought was - hey, if I know this much, shouldn't I be taking the JLPT already?!

The JLPT Test Voucher

That was when I registered for the JLPT on the Japanese Cultural Society of Singapore website. and paid a hundred Singapore dollars for the privilege. A month later, the JLPT test voucher arrived in the mail. The train was in motion; there was no going back.

Meanwhile, I continued training religiously. With Listening practice, especially. Online chatter had it that the recordings would be played only once, so I trained myself similarly, by not rewinding and replaying.

The JLPT N5 Exam

It was July when the day arrived. I head for the Singapore Management Institute where the exam was held, and stood in line with what looked like mostly Burmese folks. Interesting.

The invigilator who took charge of the exam room I had been assigned to, seemed to be Japanese going by the way she spoke English.

Shading answers with a pencil.

The proceedings were charmingly old-school. We were given question and answer sheets. It was all multiple-choice, and the correct answer had to be shaded with a pencil for feeding into a scanning machine. Despite my numerous certificates, I hadn't done this since... 2015? This was because my last couple certifications were earned from doing the coursework and presentations, rather than standardized tests.

The toughest moment in the exam came from the Choukai, which was the Listening portion. Despite my best efforts, my concentration slipped at various points. It was with considerable relief that I handed in my question and answer sheets, and headed off.

In August, I logged on to the JLPT portal to check my results. I had passed, and my results were more than decent. I actually scored higher on Listening than other sections!

The real value of all this

Last September, I got my actual physical certification through the mail. It was a foregone conclusion by that time, but I still felt that little thrill of pride. Job well done, bro, I told my reflection. Not such a big deal in the larger picture, but we've got to celebrate our wins even if they're small. Especially if said win took thirty-three friggin' years to achieve.

My results.

The JLPT N5 Certification isn't going to change my life. As far as professional cred goes, it's barely a blip. Achieving the JLPT N5 Certification probably puts me on par with the average Japanese toddler where the language is concerned. As for the value of understanding multiple languages? That's not much of a flex. This is Southeast Asia; just about everyone and their dog is multilingual.

No; at my age, the act of learning is arguably more important than what's being learned. It helps stave off dementia.

The real value is knowing and affirming, that with sufficient motivation and putting in the time and effort, I can learn pretty much whatever I choose to learn. And that is powerful stuff. In an age where things are constantly and rapidly evolving, the ability to learn shit has become vital - not just in the tech industry, but for life itself.

In hindsight, I should have realized this. How did I learn ASP? PHP? Ruby? VueJS? D3? All by picking up a book, watching videos, visiting websites, and constant practice. Most of the time, it really is that simple. The methods vary, but at the end of the day, it's about the willingness to put in the hours.

The linguistic journey continues!

I do want to see if I can achieve the next rung, the JLPT N4 Certification. Again, I'm not really sure why. Just for the hell of it, I guess. It's not like I plan to visit Japan. It's not like I realistically have anything to prove to anyone.

On the other hand, Korean does seem pretty interesting.

Decisions, decisions...

Ganbarimashoo!
T___T

Friday, 10 October 2025

TeochewThunder: Year Eleven (Part 1/2)

Well, look who turns 11 this year! It's not me (I wish), but it's this blog, of course. This thing here might just be a substitute for the children I'm never planning to have.

Dear God, please no.

In all seriousness though, it occurs to me that the effort taken to maintain this blog and the website has pretty much kept me sane all these years. I read somewhere about journalling with regard to mental health, and it seems that this blog is a great example of journalling. Why's it different from venting on Facebook or X, you might ask?

Well, for one, Social Media posts tend to be a lot shorter and more unfiltered. Which can be a good thing, don't get me wrong, but not necessarily so if you want a more thorough internal audit. Blog posts go through several revisions, as we examine what's going on in our heads, and why, and maybe even how it pertains to the tech space. The final result is a more measured, more self-examined output into the stratosphere. As such, I consider my blogposts of higher quality than a simple vomiting of my initial reactions on Social Media platforms.

That isn't to say I haven't said stupid shit in the past. I absolutely have. But the beauty of time is that as the years go by, I can evolve into less of s shit-talker and more of a shit-thinker. Yikes, that didn't sound better, did it?

Dedication

Also, this is a blog I'm dedicated to.

Dedication is a measure of how consistent you're willing to be in your efforts even without applause or acknowledgement. It's a measure of how much of a shit I give. And I give a lot.

Think about it. In previous years, I could at least justify the effort by the way prospective employers would look at my entire online portfolio. These days, they don't do that anymore (also, I haven't been looking in a while) because even the demos I put out are kids' stuff. I like to think some of it is really well-done, but well-done or not, it's still kids' stuff. Those are just not the things people hire senior developers for, especially not in the age of Artificial Intelligence and Vibe Coding.

So no... there are no longer practical reasons for maintaining this effort. I do these things because I like doing these things.

That's not to say I don't occasionally benefit from a break. And October is my assigned month for that break. Other than this blogpost, there will be no other visible activity. Emphasis on the word visible.

Invisible hands, invisible effort.

You see, as in most software development, the value is largely in the stuff that users don't see. The optimizations. The security fixes. The fine-tuning in the back. That's not to say there's no value in the stuff that's visible, but sometimes I feel like a lot of that is just to placate laypersons who don't know any better.

That's a controversial statement which we should reserve for another day.

To my original point, there is going to be work done. Just not visible work. Mostly prep for year-end, and 2026.

Content

As with last year, I've been making an effort to use less profanities in my writing. Not because I necessarily think the odd (or even frequent) vulgarity is a bad thing, mind you. More because I don't want to develop an over-reliance on anything, not even swearing. I don't want to have to use foul language as a crutch to express myself. It's just poor form. To that end, I am limiting myself to using it only a few times a year in this blog, usually whenever I review a Black Mirror episode. I certainly won't be using them with the same frequency during, say, 2019 to 2022, around the COVID-19 pandemic.

Speaking of which, as the horrors of the past few years fade behind us, I'll hopefully be speaking less about COVID-19 from this year forth. It was a terrible few years, and my emotion-laden rants during that period are evidence of that, but it's time to move on.

You may have noticed that the posts are getting even shorter than they used to. This is not an accident; rather it is the natural evolution of this blog. I wasn't verbally verbose before (at least I hope not) but reading other blogposts and tuning out halfway has made me realize that the lack of attention span on the internet is a very real thing. As a result, I'm going to curb any impulse I may have, to belabor whatever points I may be making.

What else? Yeah I changed the TeochewThunder logo. Talked about that already, didn't I? Hope you like it. If you don't, too fucking bad, baby. It's staying.

Surprise!

This is a tech blog, so I talked a whole lot about tech this year, as always. In particular, I talked about Artificial Intelligence. I suspect that this will be happening with alarming regularity, especially with the frequency with which laypersons feel the need to chime in. Someone's got to show 'em their place! Just kidding... kinda.

As for web tutorials, there's been a nice mix that includes NodeJS and NextJS. and D3. Along with the almost obligatory HTML, CSS, JavaScript sprinkled with the occasional PHP, of course. I started learning NodeJS, as usual, for the heck of it. It increased my understanding of what I was doing with ReactJS and NextJS, so there was value in it.

I've continued to generate images from AI, but the pendulum has swung back somewhat and once again I've begun to see value in using stock photos.

Next

Highs, lows, hits and misses

Monday, 16 June 2025

Web Tutorial: D3 Combo Chart

A couple months ago, I pontificated at length about quitting smoking and working on my chinup game. There was a combo chart mentioned, and collected data. It is this combo chart that we will build today, in D3. The data is in this CSV file.

This is going to be a bare bones chart, no fancy animations and whatnot. To that end, let's begin with the HTML and D3 import. You will see that in the HTML, I have also included a h1 tag, an svg tag and a script tag.
<!DOCTYPE html>
<html>
  <head>
    <title>Quit Smoking!</title>

    <style>

    </style>

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

  <body>
    <h1>The Chinups - Cigarettes Combo Chart</h1>

    <svg>

    </svg>

    <script>

    </script>
  </body>
</html>


In the CSS, we set a width and height for the SVG, and some specs for the h1 tag. The specs for the SVG are more pertinent to this tutorial.
<style>
svg
{
  width: 800px;
  height: 500px;
}

h1
{
  font-family: verdana;
  font-size: 16px;
}

</style>


That really is it for the HTML. From this point on, it will be mostly about the JavaScript. In the script tag, we have a comboChart object. Aside from that, we also make a call to d3's csv() method, passing in the name of the file, combo.csv, and a callback. We're going to be developing these two blocks somewhat concurrently.
<script>
  let comboChart =
  {

  };

  d3.csv("combo.csv", function(data)
  {

  });
</script>


In comboChart, we have properties cigData and chinupData, which are arrays. We then have maxUnits and dataPoints, which are integers with a default value of 1.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1

  };

  d3.csv("combo.csv", function(data)
  {

  });
</script>


When parsing combo.csv, the dataPoints property is set to the length of data.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1
  };

  d3.csv("combo.csv", function(data)
  {
    comboChart.dataPoints = data.length;
  });
</script>


We then have a For loop to iterate through data. The figure in the Reps column gets pushed into the chinupData array. The figure in the Cigs column gets pushed into the cigData array.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1
  };

  d3.csv("combo.csv", function(data)
  {
    comboChart.dataPoints = data.length;

    for (var i = 0; i < data.length; i++)
    {
      comboChart.chinupData.push(parseInt(data[i].Reps));
      comboChart.cigData.push(parseInt(data[i].Cigs));
    }

  });
</script>


We then define maxReps. Using the max() method of d3, we want maxReps to contain the maximum value in chinupData. We then do the same for maxCigs, using it to contain the maximum value in cigData.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

});


And then we set maxUnits to maxReps or maxCigs, whichever value is higher.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

  if (comboChart.maxUnits < maxReps) comboChart.maxUnits = maxReps;
  if (comboChart.maxUnits < maxCigs) comboChart.maxUnits = maxCigs;

});


Finally, we call the drawCharts() method from the comboChart object.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

  if (comboChart.maxUnits < maxReps) comboChart.maxUnits = maxReps;
  if (comboChart.maxUnits < maxCigs) comboChart.maxUnits = maxCigs;

  comboChart.drawCharts();
});


We've not created drawCharts() yet. Well, no time like the present! In it, use the select() method of d3 to get the sole SVG object, and set it to the variable svgChart. Then clear svgChart.
let comboChart =
{
  cigData: [],
  chinupData: [],
  maxUnits: 1,
  dataPoints: 1,
  drawCharts: function()
  {
    var svgChart = d3.select("svg");
    svgChart.html("");
  }

}


Next, run the drawAxes() method, passing in svgChart as an argument.
let comboChart =
{
  cigData: [],
  chinupData: [],
  maxUnits: 1,
  dataPoints: 1,
  drawCharts: function()
  {
    var svgChart = d3.select("svg");
    svgChart.html("");

    this.drawAxes(svgChart);
  }
}


We'll want to create this method. It has a parameter, chart. In drawCharts(), chart was already passed when drawAxes() was called.
drawCharts: function()
{
  var svgChart = d3.select("svg");
  svgChart.html("");

  this.drawAxes(svgChart);
},
drawAxes: function(chart)
{
    
}


We append one horizontal line and one vertical line. Bearing in mind that we have a 800 by 500 pixel square, and we want a 50 pixel buffer around the perimeter, this is how the x1, x2, y1 and y2 attributes are calculated. We use the CSS classes axesVertical and axesHorizontal.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");    

}


These two CSS classes described a thin grey line.
svg
{
  width: 800px;
  height: 500px;
}

h1
{
  font-family: verdana;
  font-size: 16px;
}

.axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}


Here be your starting lines.


We then want to determine the amount of pixels dedicated to each point plotted on the horizontal and vertical axes. Since we have already determined maxUnits (the highest value in the entire dataset) and dataPoints (the total number of data rows in the entire dataset), we can define pxPerUnit as available vertical space (500 - 50 - 50 = 400) divided by maxUnits and pxPerPoint as (800 - 50 - 50 = 700) divided by dataPoints. And since we only want whole numbers, we'll use the floor() method on these results.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);  
  
}


We then define scale as an empty array. We will populate scale with the y-positions we want on the vertical axis. Remember that we start from 450 and end with 50 because we're implementing a 50 pixel buffer on the maximum heigh of 500 pixels. And we do in in steps of pxPerUnit.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }   
 
}


And then we go through chart to append line tags that are styled using the CSS class scaleTick. These will be about 10 pixels in length. The data used will be the scale array, which we've already populated with the appropriated values.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });    

}


We'll add scaleTick to this CSS specification.
.scaleTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}


And the ticks appear.


Then of course, we're going to need text. We will insert text tags, styled using the scaleText CSS class. The data used will be scale again, and the y attribute will depend on the current element of scale. The text itself, will reflect what the current index of scale is. And this, not-so-coincidentally, will be the value that the specific tick on the axis represents!
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  
    
}


In the CSS, scaleText is defined like this.
.scaleTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}

.scaleText
{
  font: 8px verdana;
  fill: rgba(100, 100, 100, 1);
  text-anchor: end;
}


Here, the text appears. Note that your maximum value is 10!


Now we want to plot data points along the horizontal axes. To that end, we create the axes array. Then we populate it in a way similar to what we did for scale, instead using 750 and 50 as the end points in the For loop, and pxPerPoint as the decrementor.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }    

}


Then we insert a bunch of 10 pixel vertical lines, styled using axesTick, into the chart. The data used is axes.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }

  chart.selectAll("line.axesTick")
  .data(axes)
  .enter()
  .append("line")
  .attr("class", "axesTick")
  .attr("x1", function(d)
  {
    return d + "px";
  })
  .attr("y1", "450px")
  .attr("x2", function(d)
  {
    return d + "px";
  })
  .attr("y2", "460px");   
 
}


We'll add axesTick to this CSS specification.
.scaleTick, .axesTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}


And the horizontal axis is populated with ticks.


At the end of this method, we will run the methods drawLines() and drawBars(), passing in chart, pxPerUnit and pxPerPoint as arguments.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }

  chart.selectAll("line.axesTick")
  .data(axes)
  .enter()
  .append("line")
  .attr("class", "axesTick")
  .attr("x1", function(d)
  {
    return d + "px";
  })
  .attr("y1", "450px")
  .attr("x2", function(d)
  {
    return d + "px";
  })
  .attr("y2", "460px");
  
  this.drawBars(chart, pxPerUnit, pxPerPoint);  
  this.drawLines(chart, pxPerUnit, pxPerPoint);  

}


Of course, we will need to create these methods.
  this.drawBars(chart, pxPerUnit, pxPerPoint);  
  this.drawLines(chart, pxPerUnit, pxPerPoint);  
},
drawLines: function(chart, pxPerUnit, pxPerPoint)
{

},
drawBars: function(chart, pxPerUnit, pxPerPoint)
{

}


Let's begin with drawLines(). We want to use the line portion of the combo chart to represent the smoking data. Thus, we declare cigData and set it to the value of the cigData array.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;
},


Now, in chart, we want to append line tags. cigData is the dataset we will use. The CSS class used for this is lineChart.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart");

},


In the CSS, lineChart is a two pixel red line.
.scaleText
{
  font: 8px verdana;
  fill: rgba(100, 100, 100, 1);
  text-anchor: end;
}

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


We set x1 like this. First, we define the variable val which is the index of the dataset, i. If it is currently not the first element in the dataset (i.e, i is greater than 0), then we decrement val.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }  
  })
;
},


And then we set x1 to val multiplied by pxPerPoint, plus 50 for the buffer. In effect, if it's the first element in the dataset, x1 does pretty much nothing because it's at 0. If not, it uses the previous position in the dataset.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  });
},


x2, of course, is the current index, i, multipled by pxPerPoint and plus 50.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
;
},


For y1, we do something similar to what we did for x1. Except that we initially set val to the current value of the dataset instead of the index. And if it's not the first element in the dataset, we set val to the previous value of cigData.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }  
  })
;
},


And then we set y1 by taking 450, which is position 0 on the vertical axis, and subtracting the product of val and pxPerUnit from it.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }
  
   return (450 - (val * pxPerUnit)) + "px";
  });
},


And we set y2 similarly, except we just use d in a more straightforward way.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }
  
   return (450 - (val * pxPerUnit)) + "px";
  })
  .attr("y2", function(d)
  {
   return (450 - (d * pxPerUnit)) + "px";
  })
;
},


You see that red line? That shows me starting the programme at 10 cigarettes a day, then moving my way down to 5 gradually over the next few months, and finally dropping to 0.


The next part, drawing the bar chart portion of the combo chart, is simple by comparison. We begin by declaring chinupData and setting it to the value of the chinupData array.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;
}


In chart, we append rect tags that are styled using the CSS class barChart. The data used for this is chinupData.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart");


In the CSS, the barChart CSS class has an orange background.
.lineChart
{
  stroke: rgba(255, 0, 0, 1);
  stroke-width: 2px;
}

.barChart
{
  fill: rgba(255, 200, 100, 1);
  stroke-width: 0px;
}


Let's do the easy part first. Every bar has the same width - pxPerPoint pixels.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("width", pxPerPoint + "px");
}


The height depends on the value of the current element in chinupData, d. We multiply d by pxPerUnit to get the height.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  })
;
}


The x attribute depends on the index, i, of the current element. We multiply it by pxPerPoint and add 50 for the horizontal buffer.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("x", function(d, i)
  {
    return ((i * pxPerPoint) + 50) + "px";
  })

  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  });
}


For y, we take 450 (which is position 0 on the vertical axis) and subtract from it the product of d and pxPerUnit. In essence, we subtract the height of the bar from 450 to get the starting y-position of the rect tag.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("x", function(d, i)
  {
    return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y", function(d)
  {
    return (450 - (d * pxPerUnit)) + "px";
  })

  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  });
}


And there's the orange bar chart. You can see where I started off at 1 chinup, then shot up to 3 within a week, and how I gradually worked my way up from there.


There, all done!

This was simple in comparison to all the stuff we've gone through before with D3. That is a deliberate choice on my part - to give you the Combo Chart without throwing too much stuff in.

Also - and yes, I just wanna brag here - I'm three days away from making it an entire year without a cigarette. Go, me!

Cig-nificantly yours,
T___T