Showing posts with label CSS. Show all posts
Showing posts with label CSS. Show all posts

Saturday, 19 July 2025

Five Reasons to learn Web Development in 2025

Recent events such as the rise of generative A.I, have made tech work a little less attractive than it used to be. Web development, in particular, has suffered. That's probaby because a large chunk of web development is automatable, and even before A.I came on the scene, there had been numerous tools such as Content Management Systems and Low-code development platforms.

Thus, web development being automated by A.I was par for the course.

Robots writing websites.

Still, not all is lost. While web development might have lost much of its luster, there are still good, strong reasons to pick it up in ones tech career. Unlike the tech reporters and HR executives who write listicles like these, I have actually been a web developer before. I speak from experience, my dudes. And following are some of the most compelling reasons I had, in no particular order of importance, for going down this path.

1. No complicated installation

Ever tried to learn a language like PHP or Java? Every single one of these languages requires you to set up some kind of compiler or interpreter environment. PHP requires an Apache server. Java needs the Java Runtime Environment. You can write all the code you want, but until the code gets compiled or interpreted by the environment that you have to install and set up, you're not getting even a Hello World program done.

All you need is a browser.

HTML, CSS and JavaScript, however, do not. All of them already run in any major browser - Firefox, Chrome, and so on. In effect, the environment is right there for you.

This is not to say that you will never need to do any complicated installation. But for the basic building blocks - again, HTML, CSS and JavaScript - of web development, you don't. You will need to do that when you want to pick up a server-side language and maybe databases and definitely for the NodeJS style of development. But for basic stuff? Even the slightly more advanced stuff? Nope, not even a little bit. That is a lot more than you could ever say about other programming languages or platforms.

2. Good skill spread

When you learn web development, you learn HTML, CSS and JavaScript as a base starting point. That's already a good spread right there.

HTML and CSS are where you learn front-end and possibly even design. When you learn JavaScript, in addition to all the things you pick up when learning a programming language such as operators, arrays, branching and iterative logic, you also learn asynchronous operations and DOM manipulation.

A good spread of tools.

That's not to say that other tech disciplines don't have their own unique perks. But where it comes to the skill spread, web development wins. I don't think anything else even comes close.

Once you get past the basic toolset of HTML, CSS and JavaScript, back-end programming and databases will come into play. It's never just web development. Even if you are averse to the thought of being a humble web developer for the rest of your career, there are far worse places to start.

3. Resources

Now, when I say "resources", I don't just mean documentation, references and learning materials, though there's plenty of that, yes. But web development is not special in that regard because any other tech discipline boasts plenty of learning resources and a community dedicated to helping each other learn.

A good learning
community.

Though, in this case, web development has something extra.

You see, every humble HTML page on the internet can have its source viewed and played with in the browser, reverse engineered, and so on. Every URL on the internet is potentially a resource for learning, much like how I learned to cobble together JavaScript widgets decades ago.

In contrast, it's not possible to just take any desktop application and reverse-engineer the code, because the code has already been compiled and is no longer human-readable.

4. Ready use case

Often, when learning a programming language, it's helpful to be able to use newly-acquired skills to build something, so as to really hammer home the muscle memory. Something both relevant and useful, preferably. Not that Hello World programs don't have their place, but if one wishes to level up, better use cases are the order of the day.

And with web development, those use cases are almost too easy to find. Web development creates web pages, at the minimum. And after that, at varying levels of complexity, web applications. One does not have to stretch too far to find something worth building... and because it already exists, you know that it is both worth building and possible to build.

Applying what you learn.

My larger point is that what you learn can readily be applied. Not just in creating and editing websites, but in general software development. This also means that your chances of landing a job with that skillset cannot be understated. In this day and age, web developers are perhaps not nearly as in demand as they were a decade ago, or paid nearly as well, but the skillset goes beyond just web development.

For example, a lot of existing software already leverage things like REST API endpoints. These are basically URLs, which are pretty much the backbone of the web. REST is an almost inescapable part of the whole web development deal. Ergo, if you deal in web development, at some point you are going to be dealing with REST endpoints, which overlaps a large part of software development regardless of discipline.

Or even mobile development. In case you weren't aware, a large chunk of mobile tech is basically HTML, CSS and JavaScript.

I could go on... but do I really need to?

5. No gatekeeping

In the legal profession, there's the Bar Exam. In the medical profession, there's the Medical Regulatory Authority. In tech? Other than job interviews which exist at almost every industry, there's almost no gatekeeping in tech. Even the requirement for Degrees of Diplomas is not a really hard one.

When I say "no gatekeeping", I don't mean that nobody tries to gatekeep. The fact is that many people try to gatekeep, but it just doesn't work because to gatekeep, one needs a unified set of standards. It's almost impossible to establish said standards in a landscape as varied as tech, whose goalposts shift constantly.

The gatekeeper.

And while this inability to gatekeep exists in many areas of tech, none moreso than web development. HTML, CSS and JavaScript are fairly stable at this point, but these are just the base technologies. Their offshoots - frameworks, libraries and the like - keep springing up like mushrooms. And when you consider databases and backend programming languages, the possibilities multiply even more.

All in all, one could come in anytime in web development, and still be relatively fresh and relevant. No one can stop you from making and publishing web pages and applications, not in the same way they can stop you from practising law. You don't need a license to write code, so nobody can revoke it.

Some clarifications

The reasons stated here are in relation to those for choosing other tech fields. Why, for instance, web development when you could go for Data Analytics or cybersecurity? Reasons specific to web development.

I was inspired to compile this list because there are a lot of vague, generic and - to be brutally honest - trite lists out there on the web that extol the virtues of web development. Hopefully this is a better list.

<html>Bye for now,</html>
T___T

Saturday, 12 July 2025

Web Tutorial: The Cigarette Break Browser-based Screensaver

Following the one-year anniversary of having quit smoking, now that I no longer take smoke breaks, I do the next best thing - I make my browser take smoke breaks. Heh heh. For real, though. This occasion's web tutorial is centered around the idea of browser idle time. When the browser is left alone for a suitable amount of time, a screensaver comes up! And in this case, I want it to feature a smoking cigarette.

And the more time passes, the shorter I want the cigarette to get. Oh, and there needs to be animation.

Can I squeeze all of that into a single-part web tutorial? Let's find out!

Some HTML as usual. There's some Lorem Ipsum text, and a div with the id overlay. In the CSS, we want to set the outline for all divs to red.
<!DOCTYPE html>
  <html>
  <head>
    <title>Screensaver</title>
    <style>
      div { outline: 1px solid red; }
    </style>
    <script>
    </script>
  </head>

  <body>
    <h1>Some sample text. Leave this screen alone for 5 seconds to see the popup!</h1>    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et justo sit amet libero interdum bibendum at nec dui. Mauris lacinia sapien nec blandit dapibus. Nunc suscipit in nunc in finibus. Praesent mi arcu, convallis auctor auctor luctus, gravida eget enim. Nunc quis finibus nisl, semper hendrerit nisl. Curabitur faucibus, velit varius efficitur suscipit, nibh ipsum bibendum turpis, placerat elementum nulla lorem eget diam. Phasellus lobortis aliquet leo, non aliquet mauris volutpat vel. Aliquam facilisis dui id sapien dignissim vulputate. Nullam cursus convallis sem vel maximus. Sed ac sollicitudin justo, at vehicula ligula. Praesent sit amet tellus massa. Nam nec varius ipsum.</p>
    <p>Cras varius, nisl vitae lobortis sodales, purus ligula dictum eros, volutpat finibus neque sem et ligula. Vivamus id odio varius, blandit elit sit amet, scelerisque enim. Sed luctus molestie leo, suscipit ultricies nisl elementum id. Donec lacus erat, laoreet vel viverra vel, aliquam vitae elit. Vestibulum venenatis congue lacus a facilisis. Nulla condimentum, metus volutpat rhoncus maximus, purus mauris imperdiet dolor, at bibendum lacus justo nec risus. Praesent scelerisque libero magna, at lobortis justo convallis vel. Suspendisse cursus, odio ultricies auctor laoreet, ligula leo vulputate diam, et efficitur mauris nisl et urna. Quisque sit amet rutrum magna. Aenean at vestibulum urna. Nam ornare justo a tortor molestie auctor. Quisque at malesuada mi, volutpat feugiat sem.</p>
    <p>Vestibulum luctus tempor ligula. Nulla id tortor ut est rutrum viverra. Etiam nec sapien id massa egestas dapibus at ut ipsum. Nunc sed tortor euismod, aliquet arcu et, commodo justo. Proin pretium vel neque sed maximus. In vitae vestibulum quam. Integer quis ex in ligula varius tempus. Donec diam arcu, faucibus eu aliquet nec, dapibus at sem. Sed convallis urna neque, tristique condimentum mauris condimentum vel. Praesent eu interdum quam, at ullamcorper augue. Mauris euismod odio libero, vel pharetra lorem egestas et. Nam laoreet ultricies venenatis. Integer ante risus, commodo nec eros ac, convallis tempus magna. Maecenas dictum lacus magna.</p>
    <p>Proin eu iaculis felis, sed lobortis lectus. Pellentesque malesuada diam eu porttitor aliquam. Fusce posuere dapibus odio vitae suscipit. Mauris consectetur, tortor et pulvinar auctor, ante eros ornare nunc, non fringilla neque quam sed erat. In velit turpis, ultricies ut egestas in, porttitor eget erat. Sed et risus molestie, maximus mauris eu, accumsan sem. Sed pellentesque feugiat elit. Integer nisl nulla, condimentum eu purus id, vestibulum commodo ligula. Phasellus consectetur, justo in mollis egestas, nisl felis laoreet ipsum, ac feugiat nunc ante sit amet risus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla in enim lacus. Proin nulla dui, dictum at porttitor et, consectetur ac augue. Aliquam fringilla ex nec diam efficitur, vel vehicula mi molestie. Phasellus pellentesque in sem vitae porta. Nulla ut ultricies enim. Duis eu mauris sit amet lectus mattis efficitur molestie non magna.</p>

    <div id="overlay">
    </div>  </body>
</html>


Now, the modal popup is going to be visible by default - for now. For that we have the styling for overlay. position will be absolute, and we will set height and width properties to cover the entire screen with a translucent black background. left and top properties will be at 0px so as to begin on the top left corner of the screen. And of course, display is set to block to be visible by default.
<style>
  div { outline: 0px solid red; }

  #overlay
  {
    width: 100%;
 
    height: 100%;
 
    position: absolute;
 
    left: 0px;
    top: 0px;
    background-color: rgba(0, 0, 0, 0.5);
    display: block; 
 }

</style>


The first step of many steps!


Let's follow up by adding a div, styled with the CSS class content, in there.
<div id="overlay">
  <div class="content">

  </div>

</div>


We'll style it like this - a square with a black background and rounded corners set in the middle of overlay.
<style>
  div { outline: 0px solid red; }

  #overlay
  {
    width: 100%;    height: 100%;    position: absolute;    left: 0px;    top: 0px;    background-color: rgba(0, 0, 0, 0.5);    display: block;  }

  .content
  {
    width: 500px;
    height: 500px;
    margin: 5% auto 0 auto;
    background-color: rgba(0, 0, 0, 1);
    border-radius: 30px;
  }
</style>


Yep, here's that black square. Impossible to miss.


We introduce a new div inside that one, and style it using the CSS class cigarette.
<div id="overlay">
  <div class="content">
    <div class="cigarette">
    </div>
  </div>
</div>


This is how we style cigarette. It is a long vertical rectangle set in the middle of its parent, content. Think of it as a holder for all the divs that will make up the components of the cigarette.
.content
{
  width: 500px;
  height: 500px;
  margin: 5% auto 0 auto;
  background-color: rgba(0, 0, 0, 1);
  border-radius: 30px;
}

.cigarette
{
  width: 50px;
  height: 400px;
  margin: 10% auto 0 auto;
}


All in position, so far!



Now we'll do the individual parts of the cigarette. We have two divs - the first styled using body and the second styled using butt.
<div class="cigarette">
  <div class="body">

  </div>

  <div class="butt">
  
  </div>

</div>


body takes up full width but only 300 out of the 400 pixels on offer. There is explicitly no background.
.cigarette
{
  width: 50px;
  height: 400px;
  margin: 10% auto 0 auto;
}

.body
{
  width: 100%;
  height: 300px;
  background-color: none;
}


butt, similarly, takes up fill width and has only 100 pixels height. We have as its background a linear gradient, going from brown, yellow, then brown again.
.cigarette
{
  width: 50px;
  height: 400px;
  margin: 10% auto 0 auto;
}

.butt
{
  width: 100%;
  height: 100px;
  background-color: rgba(255, 170, 0, 1);
  background: linear-gradient(90deg,rgba(255, 170, 0, 1) 0%, rgba(255, 255, 170, 1) 50%, rgba(255, 170, 0, 1) 100%);
}


.body
{
  width: 100%;
  height: 300px;
  background-color: none;
}


This is what it looks like now. We'll be working on the empty-looking div next.


In body, we nest two more divs. Instead of classes, they will have ids because this will make them easier to manipulate via JavaScript later. The ids are empty (because that div represents "empty" space) and burnable (because it's the part of the cigarette that gets "burned").
<div class="body">
  <div id="empty">

  </div>

  <div id="burnable">

  </div>

</div>


body has 300 pixels in height, so empty takes up 50 and burnable takes up 250. burnable has a linear background that goes from light grey, white, then light grey again.
.body
{
  width: 100%;
  height: 300px;
  background-color: none;
}

#empty
{
  width: 100%;
  height: 50px;
}

#burnable
{
  width: 100%;
  height: 250px;
  background-color: rgba(255, 255, 255, 1);
  background: linear-gradient(90deg, rgba(230, 230, 230, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(230, 230, 230, 1) 100%);
}


So now we have an unlit cigarette.


In burnable, we have a div styled using the tip CSS class.
<div id="burnable">
  <div class="tip">

  </div>

</div>


tip has a height of 5 pixels and takes up full width; that's all there is to it.
#burnable
{
  width: 100%;
  height: 250px;
  background-color: rgba(255, 255, 255, 1);
  background: linear-gradient(90deg, rgba(230, 230, 230, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(230, 230, 230, 1) 100%);
}

.tip
{
  width: 100%;
  height: 5px;
}


In that div, let's add three divs, each styled using the ember CSS class.
<div id="burnable">
  <div class="tip">
    <div class="ember"></div>
    <div class="ember"></div>
    <div class="ember"></div>  </div>
</div>


ember is floated left and has an orange background.
.tip
{
  width: 100%;
  height: 5px;
}

.ember
{
  float: left;
  background-color: rgba(250, 150, 0, 1);
}


However, height and width vary according to their order. There are three divs in there, styled using ember, and we use the nth-of-type pseudoselector, passing in either "odd" or "even" and then adjusting the width and height accordingly. The middle (or second) "ember" is supposed to be the largest one.
.ember
{
  float: left;
  background-color: rgba(250, 150, 0, 1);
}

.ember:nth-of-type(odd)
{
  width:15px;
  height:3px;
}

.ember:nth-of-type(even)
{
  width:20px;
  height:5px;
}


Just for fun, let's add a bit of animation. This is totally superfluous. The animation name is emberglow and we want it to last for 2 seconds, run forever, and alternate smoothly between states. You'll see that for emberglow, I basically have background go from orange to red.
.ember
{
  float: left;
  background-color: rgba(250, 150, 0, 1);
  animation-name: emberglow;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-direction: alternate;

}

.ember:nth-of-type(odd)
{
  width:15px;
  height:3px;
}

.ember:nth-of-type(even)
{
  width:20px;
  height:5px;
}

@keyframes emberglow
{
  from { background-color: rgba(250, 150, 0, 1); }
  to { background-color: rgba(255, 0, 0, 1); }
}


There you go, a glowing tip! I feel like there's a filthy joke I could insert here, but let's move on...


In empty, we insert divs. Several divs. But let's just start with three. Style them using the CSS class smoke.
<div id="empty">
  <div class="smoke"></div>
  <div class="smoke"></div>
  <div class="smoke"></div>

</div>


Give each of them a random height, weight, margin-top and margin-left property. Do bear in mind that the effective width of empty is 50 pixels due to its parents, so the sum of margin-left and half of width shouldn't exceed 50, otherwise we'll have an overflow problem. Similarly, if we subtract half of width from margin-left, it should not be less than 0. width and height should be the same. You can have negative values for margin-top if you want the divs to overlap.
<div id="empty">
  <div class="smoke" style="width:15px;height:15px;margin-top:5px;margin-left:25px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-2px;margin-left:10px;"></div>
  <div class="smoke" style="width:18px;height:18px;margin-top:0px;margin-left:5px;"></div>
</div>


smoke has a translucent white background and a nice fuzzy grey outline by way of the box-shadow property.
#empty
{
  width: 100%;
  height: 50px;
  background-color: rgba(0, 0, 0, 1);
}

.smoke
{
  background-color: rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
}

#burnable
{
  width: 100%;
  height: 250px;
  background-color: rgba(255, 255, 255, 1);
  background: linear-gradient(90deg, rgba(230, 230, 230, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(230, 230, 230, 1) 100%);
}


And that's how the first three divs would look like.


Add several more! Make sure width, height, margin-top and margin-left properties vary.
<div id="empty">
  <div class="smoke" style="width:15px;height:15px;margin-top:5px;margin-left:25px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-2px;margin-left:10px;"></div>
  <div class="smoke" style="width:18px;height:18px;margin-top:0px;margin-left:5px;"></div>
  <div class="smoke" style="width:10px;height:10px;margin-top:-5px;margin-left:30px;"></div>
  <div class="smoke" style="width:15px;height:15px;margin-top:0px;margin-left:20px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-2px;margin-left:15px;"></div>
  <div class="smoke" style="width:8px;height:8px;margin-top:-5px;margin-left:13px;"></div>
  <div class="smoke" style="width:12px;height:12px;margin-top:-5px;margin-left:8px;"></div>
  <div class="smoke" style="width:8px;height:8px;margin-top:0px;margin-left:22px;"></div>
  <div class="smoke" style="width:15px;height:15px;margin-top:-2px;margin-left:5px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-5px;margin-left:15px;"></div>
  <div class="smoke" style="width:15px;height:15px;margin-top:-2px;margin-left:5px;"></div>
  <div class="smoke" style="width:12px;height:12px;margin-top:-5px;margin-left:8px;"></div>
  <div class="smoke" style="width:8px;height:8px;margin-top:0px;margin-left:30px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-2px;margin-left:10px;"></div>
  <div class="smoke" style="width:8px;height:8px;margin-top:0px;margin-left:10px;"></div>
  <div class="smoke" style="width:15px;height:15px;margin-top:0px;margin-left:15px;"></div>
  <div class="smoke" style="width:20px;height:20px;margin-top:-5px;margin-left:10px;"></div>
  <div class="smoke" style="width:8px;height:8px;margin-top:-2px;margin-left:30px;"></div>
  <div class="smoke" style="width:10px;height:10px;margin-top:-2px;margin-left:15px;"></div>
  <div class="smoke" style="width:15px;height:15px;margin-top:-0px;margin-left:20px;"></div>

</div>


And you'll see their red outlines even if they're not visible after the glowing tip otherwise.


Now, at random, have each div styled using smoke1 or smoke2.
<div id="empty">
  <div class="smoke smoke1" style="width:15px;height:15px;margin-top:5px;margin-left:25px;"></div>
  <div class="smoke smoke1" style="width:20px;height:20px;margin-top:-2px;margin-left:10px;"></div>
  <div class="smoke smoke2" style="width:18px;height:18px;margin-top:0px;margin-left:5px;"></div>
  <div class="smoke smoke1" style="width:10px;height:10px;margin-top:-5px;margin-left:30px;"></div>
  <div class="smoke smoke2" style="width:15px;height:15px;margin-top:0px;margin-left:20px;"></div>
  <div class="smoke smoke2" style="width:20px;height:20px;margin-top:-2px;margin-left:15px;"></div>
  <div class="smoke smoke1" style="width:8px;height:8px;margin-top:-5px;margin-left:13px;"></div>
  <div class="smoke smoke2" style="width:12px;height:12px;margin-top:-5px;margin-left:8px;"></div>
  <div class="smoke smoke1" style="width:8px;height:8px;margin-top:0px;margin-left:22px;"></div>
  <div class="smoke smoke2" style="width:15px;height:15px;margin-top:-2px;margin-left:5px;"></div>
  <div class="smoke smoke2" style="width:20px;height:20px;margin-top:-5px;margin-left:15px;"></div>
  <div class="smoke smoke1" style="width:15px;height:15px;margin-top:-2px;margin-left:5px;"></div>
  <div class="smoke smoke2" style="width:12px;height:12px;margin-top:-5px;margin-left:8px;"></div>
  <div class="smoke smoke1" style="width:8px;height:8px;margin-top:0px;margin-left:30px;"></div>
  <div class="smoke smoke2" style="width:20px;height:20px;margin-top:-2px;margin-left:10px;"></div>
  <div class="smoke smoke2" style="width:8px;height:8px;margin-top:0px;margin-left:10px;"></div>
  <div class="smoke smoke1" style="width:15px;height:15px;margin-top:0px;margin-left:15px;"></div>
  <div class="smoke smoke2" style="width:20px;height:20px;margin-top:-5px;margin-left:10px;"></div>
  <div class="smoke smoke1" style="width:8px;height:8px;margin-top:-2px;margin-left:30px;"></div>
  <div class="smoke smoke2" style="width:10px;height:10px;margin-top:-2px;margin-left:15px;"></div>
  <div class="smoke smoke1" style="width:15px;height:15px;margin-top:-0px;margin-left:20px;"></div>
</div>


This is for more animation. These CSS classes each call an animation, with different durations. However, both of these animations will run forever, and alternate back and forth.
.smoke
{
  background-color: rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
}

.smoke1
{
  animation-name: smokebubble1;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

.smoke2
{
  animation-name: smokebubble2;
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}


#burnable
{
  width: 100%;
  height: 250px;
  background-color: rgba(255, 255, 255, 1);
  background: linear-gradient(90deg, rgba(230, 230, 230, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(230, 230, 230, 1) 100%);
}


Here are their animations. The box-shadow and margin-left properties are animated. I won't bother showing screenshots because the range of motion is rather limited.
.smoke1
{
  animation-name: smokebubble1;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

.smoke2
{
  animation-name: smokebubble2;
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

@keyframes smokebubble1
{
  from { box-shadow: 0 0 11px rgba(255, 255, 255, 1); margin-left: 26px; }
}

@keyframes smokebubble2
{
  from { box-shadow: 0 0 12px rgba(255, 255, 255, 0.8); margin-left: 24px; }
}


#burnable
{
  width: 100%;
  height: 250px;
  background-color: rgba(255, 255, 255, 1);
  background: linear-gradient(90deg, rgba(230, 230, 230, 1) 0%, rgba(255, 255, 255, 1) 50%, rgba(230, 230, 230, 1) 100%);
}


In the JavaScript, we define the object idleCounter. In it, there are the properties lastActivity and msToPopup. lastActivity is a timestamp and defaults to null. msToPopup is an integer, defaults to 5, and defines the number of seconds of idle time encountered before the popup happens.
<script>
  var idleCounter =
  {
    lastActivity: null,
    msToPopup: 5
  };

</script>


We then have the method, startTimer(). What it does is set the observeLastActivity() method to run every second.
<script>
  var idleCounter =
  {
    lastActivity: null,
    msToPopup: 5,
    startTimer: function()
    {
      setInterval(
        () =>
        {
          this.observeLastActivity();
        }, 
       1000
      );
    }
  };
</script>


And we want this method to run as soon as the page is loaded.
<script>
  var idleCounter =
  {
    lastActivity: null,
    msToPopup: 5,
    startTimer: function()
    {
      setInterval(
        () =>
        {
          this.observeLastActivity();
        },
        5000
      );
    }
  };

  window.onload = () =>
  {
    idleCounter.startTimer();
  };
</script>


Create observeLastActivity(). If lastActivity is falsy (not defined or just null) then we run the setLastActivity() method. We'll run the popup() method regardless.
var idleCounter =
{
  lastActivity: null,
  msToPopup: 5,
  popupOpen: false,
  observeLastActivity: function()
  {
    if (!this.lastActivity) this.setLastActivity();
    this.popup();
  },

  startTimer: function()
  {
    setInterval(
      () =>
      {
        this.observeLastActivity();
      },
      5000
    );
  }
};


So we have two new methods to create. Start with setLastActivity(). This basically involves setting lastActivity to the current time, then running popup().
var idleCounter =
{
  lastActivity: null,
  msToPopup: 5,
  setLastActivity: function()
  {
    this.lastActivity = new Date();
    this.popup();
  },
  observeLastActivity: function()
  {
    if (!this.lastActivity) this.setLastActivity();
    this.popup();
  },
  startTimer: function()
  {
    setInterval(
      () => 
      {
        this.observeLastActivity(); 
     }, 
     5000
    );
  }
};


Now for the popup() method. Define d as the current time. Then use the getTime() method on d and lastActivity to get the number of milliseconds since the first day of 1970, for each timestamp, and subtract to get diff. Of course d will always be greater than lastActivity, even if by a couple milliseconds.
var idleCounter =
{
  lastActivity: null,
  msToPopup: 5,
  popupOpen: false,
  setLastActivity: function()
  {
    this.lastActivity = new Date();
    this.popup();
  },
  observeLastActivity: function()
  {
    if (!this.lastActivity) this.setLastActivity();
    this.popup();
  },
  startTimer: function()
  {
    setInterval(
      () => 
      {
        this.observeLastActivity(); 
     }, 
     5000
    );
  }
  popup: function()
  {
    var d = new Date();
    var diff = d.getTime() - this.lastActivity.getTime();
  }
};


Then divide diff (which is in milliseconds) by 1000 to get the number of seconds. And create a conditional to check if diff is now greater or equal to msToPopup.
popup: function()
{
  var d = new Date();
  var diff = d.getTime() - this.lastActivity.getTime();
  diff = diff / 1000;

  if (diff >= this.msToPopup)
  {

  }
  else
  {

  }

}


Define overlay as the modal, empty as the empty div and burnable as the burnable div. This is where it gets exciting.
popup: function()
{
  var d = new Date();
  var diff = d.getTime() - this.lastActivity.getTime();
  diff = diff / 1000;

  var overlay = document.getElementById("overlay");
  var empty = document.getElementById("empty");
  var burnable = document.getElementById("burnable");


  if (diff >= this.msToPopup)
  {

  }
  else
  {

  }
}


Now if diff is greater or equal to msToPopup, we want to display the modal by setting the display property of its style object to block. By default, we will set empty's height to 50 pixels and burnable's height to 250 pixels. If not, we hide overlay.
popup: function()
{
  var d = new Date();
  var diff = d.getTime() - this.lastActivity.getTime();
  diff = diff / 1000;

  var overlay = document.getElementById("overlay");
  var empty = document.getElementById("empty");
  var burnable = document.getElementById("burnable");

  if (diff >= this.msToPopup)
  {
    overlay.style.display = "block";
    empty.style.height = "50px";
    burnable.style.height = "250px";

  }
  else
  {
    overlay.style.display = "none";
  }
}


Here's the fun part. If diff is greater or equal to twice msToPopup, set empty's height to 80 pixels and burnable's height to 220 pixels. It will still add up to 300 pixels. The effect is that if more idle time has passed, empty will be taller and burnable will be shorter.
popup: function()
{
  var d = new Date();
  var diff = d.getTime() - this.lastActivity.getTime();
  diff = diff / 1000;

  var overlay = document.getElementById("overlay");
  var empty = document.getElementById("empty");
  var burnable = document.getElementById("burnable");

  if (diff >= this.msToPopup)
  {
    overlay.style.display = "block"; 
    empty.style.height = "50px";
    burnable.style.height = "250px";

    if (diff >= (this.msToPopup * 2))
    {
       empty.style.height = "80px";
       burnable.style.height = "220px";
    }
  }
  else
  {
    overlay.style.display = "none";
  }
}


And so on, and so forth. I've set a few cases here and you should feel free to add more.
popup: function()
{
  var d = new Date();
  var diff = d.getTime() - this.lastActivity.getTime();
  diff = diff / 1000;

  var overlay = document.getElementById("overlay");
  var empty = document.getElementById("empty");
  var burnable = document.getElementById("burnable");

  if (diff >= this.msToPopup)
  {
    overlay.style.display = "block";
    empty.style.height = "50px";
    burnable.style.height = "250px";

    if (diff >= (this.msToPopup * 2))
    {
       empty.style.height = "80px";
       burnable.style.height = "220px";
    }

    if (diff >= (this.msToPopup * 3))
    {
       empty.style.height = "100px";
       burnable.style.height = "200px";
    }

    if (diff >= (this.msToPopup * 5))
    {
       empty.style.height = "200px";
       burnable.style.height = "100px";
    }

    if (diff >= (this.msToPopup * 10))
    {
       empty.style.height = "250px";
      burnable.style.height = "50px";
    }

  }
  else
  {
    overlay.style.display = "none";
  }
}


And while we're at it, disable the red outline.
div { outline: 0px solid red; }


When you first refresh the browser, there should be no modal because the popup() method detects that 5 seconds of inactivity have not passed, so the modal remains invisible. Wait for 5 seconds, and it should come up!


At 10 seconds, the cigarette grows shorter.


At 15 seconds...


At 25 seconds...


At 50 seconds...


For the final touch, add these two lines. This ensures that if you move the mouse or press a keyboard button, setLastActivity() gets run and will result in the modal disappearing until another 5 seconds of inactivity passes.
window.onload = () =>
{
  idleCounter.startTimer();

  document.body.addEventListener("keypress", ()=> { idleCounter.setLastActivity(); });
  document.body.addEventListener("mousemove", ()=> { idleCounter.setLastActivity(); });

};


Well done!

We just implemented a browser screen saver. Cool, right?!

Don't be idle now!
T___T

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