Saturday, 15 October 2022

TeochewThunder: Year Eight (Part 2/2)

For this blogging year, the usual features were there - reviews, web tutorials, listicles and so on. I'm glad to report that the popular posts didn't all fall into one category or other. At the very least, it indicates that readers appreciate a variety of blogpost types. Well, except for web tutorials. The readership for those were consistently low.

Viewership for blogposts usually peak at the two-week mark, so they will be judged based on that. The statistics are from LinkedIn and Google.

Top Picks

These are the blogposts that were the cream of the crop. They hit high viewership numbers. Half of them were about current affairs. The rest were made up of a listicle, a review and a dry technical piece.

Crowd-pleasers!

ONE Pass to rule them all was one of my most recent blogposts. I used no profanities in there, but damn, the shade I threw would make an umbrella blush.

The old-fashioned way to skirt the OCBC Phishing Scam was written as a thought piece on something that happened in Singapore last xxx. No surprises there; this was a hot topic on our little island.

Reference Review: The Clean Coder: A Code of Conduct for Professional Programmers
; now, this one was a surprise. My reviews generally haven't had all that much success. Add to the fact that I wasn't excessively complimentary towards Uncle Bob...

Ten Lessons From The Art Of War Applied To Software Development, a fun listicle I put together after attending some rooms on the Clubhouse app.

Discrete And Continuous Data Defined is a headscratcher. This was a dry a piece as I've ever done and honestly I'm not at all sure why it got so popular.

Elon Musk and the Twitter Takeover was an expected hit; after all, it was the topic for a while there regardless of whether or not you were a techie.

Excellent Response

This group encompasses blogposts that got a decent amount of readership, though not enough to propel them to the next tier.

Cleared that bar.

Using Trello outside of the professional workplace was a little geek-out piece of how I used this software in my everyday life.

Software Review: Power BI had a decent enough amount of clicks, which was probably better than I hoped for.

Google moves to 2FA! underperformed, really. Maybe it just wasn't that interesting.

Five Software Development Takeaways From the COP Saga in 2021 was a piece about Singapore politics, though I tied it into tech practices.

Film Review: Black Mirror Series One astonished me because I didn't think people would be interested in a series that premiered so long ago.

Trail of the Catfish was something I wrote about an experience I witnessed and was only peripherally involved in. It had a fair bit of traction on Facebook (because people who use Facebook love that shit) but not so much on LinkedIn.

OK-ish

These posts got a bit of attention, but statistically they got the short end of the stick. Most of them were dry technical blogposts, so no surprise there.

Good reads.

Tech Wizardry in the Beijing Winter Olympics
and Ten Pieces Of African Wisdom In Software Development were pieces I felt deserved more views, but it is what it is.

Functional Terminology, The Different Categories of Data Analytics, Mean, Median and Mode in Python, Thoughts About HTML5, Different Number Types in Datasets and Code indentation reduction using Guard Clauses were wholly technical, nothing exciting here.

Reading Books In Modern Technology
was something whimsical I did. Probably could have written it better.

App Review: Bazooka Boy
was a mediocre review about a mediocre app. Probably got the views it deserved.

Special Mentions

These were blogposts that were especially poignant in the year 2022. They might not have obtained all that much traction, but I'm going to mention them here because they were personally important to me.

Spotlight!

How I became a Married Software Developer chronicles the arduous years leading up to my marriage, and is closely tied to my career as a software developer.

Ten Cheap Shots At Internet Explorer is a glorious, glorious day for me and I refuse to let the lack of views dampen it.

That's all for now!

It has been a pretty decent year, considering this is the eighth straight year I'm doing this. It's not been an outstanding year by any means, but I like to think I at least met a certain standard. If you're reading this, kudos for your continued support!

Thanks for your kind 8-tention!
T___T

Tuesday, 11 October 2022

TeochewThunder: Year Eight (Part 1/2)

2022 so far has had me working from home, just like 2021. The main difference that my wife has been back from China since the last quarter of the previous year. And my efforts to acclimatize to the inevitable changes in my lifestyle could fill an entire book, never mind a blogpost.

Suffice to say, my blogging and coding time has met a few roadblocks, because there are times my duties as a husband take precedence. As they should. I'm happy to say, however, that I've managed to stay pretty constant, even though the average length of my blogposts has significantly decreased. Part of that could also be due to a conscious effort towards brevity. Honestly, I take no pleasure in droning on like some pompous, oblivious jackass.

No more commute!

This is the third year in a row I spent the majority of my time working from home. This gave me greater freedom to blog, or at least research new ideas. At least, the time I saved on commuting definitely contributed.

As soon as the workday is done, I can go right to coding, or blogging, instead of waiting to get home, and probably getting physically drained from the commute. It can't be overstated how much this has improved my work on the blog.

Trello

If anything good came from the utter shitshow that was my job at this company, it was that I took careful note of all the tools they used, and adopted those practices as much as I could. One of these tools was Trello. It's no exaggeration to say that it practically runs my life now.


I have written a blogpost about how I use Trello outside of the workplace. It helps greatly in keeping consistent, which is a giant step in trying to achieve any kind of success, no matter how small. Perhaps I'll elaborate on this sometime later in the year.

My blogging and coding tasks are now scheduled via Trello. I'm having a blast, though a one-man experience for Trello probably is not conducive to training for a real team environment. Still, it's something.

Audio Social Media

A large part of last year and the entirety of this year was spent weaning myself off of Facebook and using the app known as Clubhouse. And to a lesser extent, LinkedIn. If I'm to be perfectly honest, my distancing from Facebook began more than a few years back, and my introduction to Clubhouse pretty much accelerated the process.


On Clubhouse, I've found myself moving in several circles - American, African, Japanese, Chinese and Malaysian - and the subsequent conversations I've had, have spawned a multitude of ideas. These ideas sometimes translate into blogposts (see Ten Lessons From The Art Of War Applied To Software Development and Ten Pieces Of African Wisdom In Software Development) or code projects (notably The Japanese Language Trainer App). Upon the relaxation of border controls, I've even had friends from Clubhouse come to Singapore to meet up offline.

All in all, my time on Clubhouse has been pretty fruitful, despite it often looking otherwise.

LinkedIn

As mentioned previously, there has been a conscious move from Facebook to LinkedIn. A large part of this reason is audience.

While I used to share completed blogposts on Facebook, at some point I began to realize that I was preaching to the wrong crowd. Facebook users pretty much just want to watch funny cat videos and share memes. The content of my blogposts can sometimes be very technical, and consequently pretty dry. I've found that sharing my blogposts on LinkedIn, where the majority of my connections are professional rather than personal, have gained greater traction.


Now, when I have blogposts that add value to my professional profile, I share them on LinkedIn. My rather more spicy rants about employers have been confined to Facebook, where they gain plenty of traction on their own.

Next

Let's take a look at how popular some of this year's blogposts were.

Wednesday, 28 September 2022

Joining Tables In SQL

In a typical relational database, data is often distributed in different tables. This is by design; normalization is utilized for scalability and data integrity. However, this leads to a common challenge. What if the data required is found in multiple different tables?


Joining tables!

That is where Joins come in. There are several types of Joins, and in order to make this as system-agnostic as possible, I will be covering Joins that are common to most database systems. For examples, we will be using the following tables below.

tblPlayers

Id Name Club RoleId
1 Alisson Becker Liverpool 1
2 Roberto Firmino Liverpool 6
3 Erling Haaland Manchester City 6
4 Bruno Fernandes Manchester United 0
5 Harry Kane Tottenham 6
6 Richarlisson Tottenham 6
7 Andy Robertson Liverpool 3
8 Édouard Mendy Chelsea 1
9 Adam Smith Bournemouth 4
10 Nathan Redmond Southampton 5
11 James Tarkowski Everton 2
12 David De Gea Manchester United 1
13 Anthony Martial Manchester United 0
14 John Stones Manchester City 3
15 Adam Lallana Brighton 0
16 Ben White Arsenal 2
17 Marquinhos Arsenal 0
18 James Maddison Leicester City 0
19 Jonny Evans Leicester City 0
20 Jamie Vardy Leicester City 0


tblRoles

Id Name Type
1 Goalkeeper DEF
2 Center-back DEF
3 Full-back DEF
4 Defensive Midfielder MID
5 Attacking Midfielder MID
6 Forward ATK
7 Winger MID


Since I'm a soccer nut, there's a table of football players, tblPlayers, and a table of roles, tblRoles. I know that modern football has evolved beyond static roles, but bear with me; this is only an example. In tblPlayers, the RoleId column is a foreign key to tblRoles, corresponding to that table's Id column.

Not all the rows in tblPlayers has a corresponding entry in tblRoles. Some of the entries in the RoleId column is a 0, which corresponds to nothing in tblRoles. While I do know the information, I have deliberately done this to illustrate excluded rows in different Joins.

Inner Joins

When one table is joined to another via an Inner Join, it means that all rows from the first table where the foreign key matches with an entry in the second table, are returned.

A sample INNER JOIN query
SELECT p.Id as Id, p.Name, r.Name as Role
FROM tblPlayers p  as Name
INNER JOIN tblRoles r
ON r.Id = p.RoleId

13 results
Id Name Role
1 Alisson Becker Goalkeeper
2 Roberto Firmino Forward
3 Erling Haaland Forward
5 Harry Kane Forward
6 Richarlisson Forward
7 Andy Robertson Full-back
8 Édouard Mendy Goalkeeper
9 Adam Smith Defensive Midfielder
10 Nathan Redmond Attacking Midfielder
11 James Tarkowski Center-back
12 David De Gea Goalkeeper
14 John Stones Full-back
16 Ben White Center-back


There are only 13 out of 20 players in the result set. You can see that players such as James Maddison and Jamie Vardy do not make the list. This is because their RoleId is 0, which does not have a corresponding value in the Id column of tbLRoles.

Venn Diagram of INNER JOIN



Outer Joins

There are two kinds of Outer Joins - the Left Outer Join and the Right Outer Join. Normally, this is shortened to just "Left Join" or "Right Join" respectively. This means that all the rows in one table are returned, with null values from the other table where no data is available.

A sample LEFT JOIN query
SELECT p.Id as Id, p.Name, r.Name as Role
FROM tblPlayers p  as Name
LEFT JOIN tblRoles r
ON r.Id = p.RoleId

20 results
Id Name Role
1 Alisson Becker Goalkeeper
2 Roberto Firmino Forward
3 Erling Haaland Forward
5 Harry Kane Forward
6 Richarlisson Forward
7 Andy Robertson Full-back
8 Édouard Mendy Goalkeeper
9 Adam Smith Defensive Midfielder
10 Nathan Redmond Attacking Midfielder
11 James Tarkowski Center-back
12 David De Gea Goalkeeper
13 Anthony Martial NULL
14 John Stones Full-back
15 Adam Lallana NULL
16 Ben White Center-back
17 Marquinhos NULL
18 James Maddison NULL
19 Jonny Evans NULL
20 Jamie Vardy NULL


In a Left Join, the first table takes precedence. Therefore, in the resultant table, all records in tblPlayers are returned - a full 20 out of 20. However, those who have a RoleId of 0 will have a null value in the Role column because there is no such value in the tblRoles table.

Venn Diagram of LEFT JOIN



A sample RIGHT JOIN query
SELECT p.Id as Id, p.Name, r.Name as Role
FROM tblPlayers p  as Name
RIGHT JOIN tblRoles r
ON r.Id = p.RoleId

13 results
Id Name Role
1 Alisson Becker Goalkeeper
2 Roberto Firmino Forward
3 Erling Haaland Forward
5 Harry Kane Forward
6 Richarlisson Forward
7 Andy Robertson Full-back
8 Édouard Mendy Goalkeeper
9 Adam Smith Defensive Midfielder
10 Nathan Redmond Attacking Midfielder
11 James Tarkowski Center-back
12 David De Gea Goalkeeper
14 John Stones Full-back
16 Ben White Center-back
NULL NULL Winger


In a Right Join, the second table takes precedence. Therefore, in the resultant table, all records in tblRoles are present. There may be repeats where RoleId is referenced more than once. You will see that for the role "Winger", there is an entry; however, since there are no entries in tbPLayers that ave this role, the value for the Name column is null.

Venn Diagram of RIGHT JOIN



Last words on Joins!

Joins are incredibly useful when data needs to be assembled. And often, this is necessary to create a coherent and comprehensive data set.

What Joins to use, however, are the usual challenge. It is up to the individual programmer or data analyst to decide what is best for the current context.

Thanks for JOINing,
T___T

Friday, 23 September 2022

Web Tutorial: D3 Heatmap (Part 3/3)

It's time to fill in the Heatmap's rect tags with colors. The idea here is that all of them have the same color, but different opacity. So the highest value will be at 100% opacity, and a 0 value will be at 0% opacity, with all other values at varying proportional opacities. Oh, and no value will be plain black.

For this, we need the max property in the config object. We'll do it near the beginning of the setData() method.

We set the max property by using the max() method of the d3 object. For the first argument, that will be the cols property of the graphData object. The second argument will be a callback. The callback should have d as a parameter.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
    
    config.max = d3.max(graphData.cols, function(d)
        {

        }
    );

    
    var container = d3.select(".hmChartContainer");


In the callback, declare maxStat and return it.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
    
    config.max = d3.max(graphData.cols, function(d)
        {
            var maxStat;

            return maxStat;

        }
    );

    var container = d3.select(".hmChartContainer");


Now set maxStat. It will use, again, the max() method of the d3 object. So we will use d here, passing in the stats property of d. d, in this context, is the current element of the cols property of graphData. The second parameter is a callback with x as the parameter.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
    
    config.max = d3.max(graphData.cols, function(d)
        {
            var maxStat = d3.max(d.stats, function(x)
                {
                    
                }
            )
;

            return maxStat;
        }
    );

    var container = d3.select(".hmChartContainer");


In here, return the element of x pointed to by stat. This means that the maximum value of the stat chosen in ddlStat is returned from each particular year, and then in turn, the maximum stat across all the years is returned. So the max property of config is the greatest selected stat (appearances or goals) from the entire dataset.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
    
    config.max = d3.max(graphData.cols, function(d)
        {
            var maxStat = d3.max(d.stats, function(x)
                {
                    return x[stat];
                }
            );

            return maxStat;
        }
    );

    var container = d3.select(".hmChartContainer");


Now that we've defined the max property, we can proceed to define the fill attribute in the rect tags.
chart.selectAll("rect.rect_" + graphData.rows[r])
.data(graphData.cols)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return (i * config.dataWidth) + "em";
})
.attr("y", function(d)
{
    return (r * config.scale) + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})           
.attr("height", function(d)
{
    return (config.scale) + "em";
})
.attr("fill", function(d)
{

})
;


Define arr as the array returned after running stats through the filter() method, ensuring that the year property of each element of the player's stats matches the current element of the rows property, pointed to by r. There should be only one element.
.attr("fill", function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});
})


We then define opacity, giving it a value of 0. Then we return a color value string using rgba() with a color of red, but with opacity as the last value.
.attr("fill", function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 0;

    return "rgba(200, 0, 0, " + opacity + ")";

})


Now to define opacity. If the length of arr is not 1, that means there is no value for that player for that year. The color returned is a solid black.
.attr("fill", function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 0;

    if (arr.length == 1)
    {
        
    }
    else
    {
        return "rgba(0, 0, 0, 1)";
    }


    return "rgba(200, 0, 0, " + opacity + ")";
})


If there is data, we take the opacity by grabbing the first (and only) element of arr, get the stat required, and divide it by config.max. It should not be a zero, but feel free to add some defensive programming should you see fit. The value should be less than or equal to max, so the division should result in a value less than 1.
.attr("fill", function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 0;

    if (arr.length == 1)
    {
        opacity = (arr[0][stat] / config.max);
    }
    else
    {
        return "rgba(0, 0, 0, 1)";
    }

    return "rgba(200, 0, 0, " + opacity + ")";
})


Now you see this! The specification was red, but this is blue.




That's because the underlying color is blue, therefore messing with the opacity changed that. So just change the background color in the CSS, to a solid white.
.hmChartSvg
{
    width: 20em;
    height: 20em;
    float: left;
    background-color: rgba(255, 255, 255, 1);
}


And now you should see this!




While we're at it, change the opacity of all the other color specifications to zero opacity. And also remove the outline for rect tags. We won't need it.
.hmLegendYSvg
{
    width: 5em;
    height: 20em;
    float: left;
    background-color: rgba(0, 255, 0, 0);
}

.hmLegendYSvg text
{
    fill: rgba(255, 255, 0, 1);
    text-anchor: end;
}

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

.hmChartSvg rect
{
    stroke: rgba(255, 255, 255, 0);
    stroke-width: 1;
}

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

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


There you go.




Adding animation

Here's one final touch! Add these few lines so that the opacity will start out at 0, but graduate to the true opacity 2 seconds later.
.attr("height", function(d)
{
    return (config.scale) + "em";
})
.attr("fill", function(d)
{
    return "rgba(200, 0, 0, 0)";
})
.transition()
.duration(2000)

.attr("fill", function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 0;

    if (arr.length == 1)
    {
        opacity = (arr[0][stat] / config.max);
    }
    else
    {
        return "rgba(0, 0, 0, 1)";
    }

    return "rgba(200, 0, 0, " + opacity + ")";
});


Or we could have some fun with this, and do this instead in the duration() method. This is basically sort of repeating the code for determining opacity. The default returned is still 2 seconds, but if there is a new opacity it's multiplied by the default. Hence, the higher the opacity, the slower the animation.
.duration(/*2000*/function(d)
{
    var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 2000;

    if (arr.length == 1)
    {
        opacity *= (arr[0][stat] / config.max);
    }

    return opacity;                   
})


Or try this! Use the index, i. Change opacity to 1 second, and multiply it by the value of i. This way, all the Heatmap's leftmost columns will appear first, all the way to the right!
.duration(function(d, i)
{
    //var arr = d.stats.filter((x) => { return x.year == graphData.rows[r];});

    var opacity = 1000;

    //if (arr.length == 1)
    //{
    //   opacity *= (arr[0][stat] / config.max);
    //}

    return opacity * i;                   
})


I really, truly hope you have some fun with this. There's a great deal of experimentation to be had.

Transparently yours,
T___T

Wednesday, 21 September 2022

Web Tutorial: D3 Heatmap (Part 2/3)

Let us concentrate on filling up the left and bottom of the heatmap. These are the Y and X axes, respectively. The Y-axis will show seasons, while the X-axis will show players.

Start on legendY, using the rows array of the graphData object as data, and append text tags. This should be a straightforward value.
filler
.style("width", function(d)
{
    return config.legendYWidth + "em";
})
.style("height", function(d)
{
    return config.legendXHeight + "em";
});

legendY.selectAll("text")
.data(graphData.rows)
.enter()
.append("text")
.text(function(d)
{
    return d;
});


Now we will set the x attribute. We'll use half the value of legendYWidth.
legendY.selectAll("text")
.data(graphData.rows)
.enter()
.append("text")
.attr("x", function(d)
{
    return (config.legendYWidth / 2) + "em";
})

.text(function(d)
{
    return d;
});


Now set the y attribute. Logically, the text will appear lower with each iteration of the array. So we will use the index, i. We'll add 1 to i to make sure that the value is at least 1, then multiply that by the scale property. And because we want the text position to be vertically in the middle, we should subtract half the value of scale from that result.
legendY.selectAll("text")
.data(graphData.rows)
.enter()
.append("text")
.attr("x", function(d)
{
    return (config.legendYWidth / 2) + "em";
})
.attr("y", function(d, i)
{
    return (((i + 1) * config.scale) - (config.scale / 2)) + "em";
})

.text(function(d)
{
    return d;
});


Finally, make this change to the CSS. The text tags in the hmLegendYSvg class should use the default font size.
.hmLegendYSvg text
{
    fill: rgba(255, 255, 0, 1);
    text-anchor: end;
    /*font-size: 0.5em;*/
}


The green column is now filled with years in yellow font!




Time to work on the X-axis. As before, we're adding text tags. We are using the cols array of the graphData object. This time, d is an object rather than a string, so we return the title attribute of d.
legendY.selectAll("text")
.data(graphData.rows)
.enter()
.append("text")
.attr("x", function(d)
{
    return (config.legendYWidth / 2) + "em";
})
.attr("y", function(d, i)
{
    return (((i + 1) * config.scale) - (config.scale / 2)) + "em";
})
.text(function(d)
{
    return d;
});

legendX
.selectAll("text")
.data(graphData.cols)
.enter()
.append("text")
.text(function(d)
{
    return d.title;
});


The x attribute is the horizontal positioning of the text. This will increase with each iteration of the data, so we use the index, i. We multiply the value of i by the dataWidth property. This time, we don't add 1 beforehand because a 0 is acceptable. And after that, we add half the value of dataWidth. Because we want the text to begin from the center line. If you check the CSS for the text-anchor property for text tags within hmLegendXSvg CSS class, it is set to middle. The combination of this ensures that the text tags, no matter how long they are, always align to the middle of that allotted space.
legendX
.selectAll("text")
.data(graphData.cols)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * config.dataWidth) + (config.dataWidth / 2)) + "em";
})

.text(function(d)
{
    return d.title;
});


And for the y attribute, we use half the value of legendXHeight.
legendX
.selectAll("text")
.data(graphData.cols)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * config.dataWidth) + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
    return (config.legendXHeight / 2) + "em";
})

.text(function(d)
{
    return d.title;
});


And now you can see the player names appear on the X-axis!




Putting in the data

Let us begin with making some placeholders. We need a For loop to iterate through the elements of the rows array of graphData.
legendX
.selectAll("text")
.data(graphData.cols)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * config.dataWidth) + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
    return (config.legendXHeight / 2) + "em";
})
.text(function(d)
{
    return d.title;
});

for (let r = 0; r <= graphData.rows.length; r++)
{

}


And for each element, we append a row of rect tags into chart. In the selectAll() method, we just give it a class which we will never use, just to give each row of elements some uniqueness. For data, we use the cols array of the graphData object.
for (let r = 0; r <= graphData.rows.length; r++)
{
    chart.selectAll("rect.rect_" + graphData.rows[r])
    .data(graphData.cols)
    .enter()
    .append("rect");  
            
}


Each rect uses dataWidth as width and scale as height.
for (let r = 0; r <= graphData.rows.length; r++)
{
    chart.selectAll("rect.rect_" + graphData.rows[r])
    .data(graphData.cols)
    .enter()
    .append("rect")
    .attr("width", function(d)
    {
        return (config.dataWidth) + "em";
    })           
    .attr("height", function(d)
    {
        return (config.scale) + "em";
    })
;                 
}


For the x attribute, the rect will appear further to the right progressively per row. So we use i as the index and multiply it by dataWidth (which is the width of each rect).
for (let r = 0; r <= graphData.rows.length; r++)
{
    chart.selectAll("rect.rect_" + graphData.rows[r])
    .data(graphData.cols)
    .enter()
    .append("rect")
    .attr("x", function(d, i)
    {
        return (i * config.dataWidth) + "em";
    })

    .attr("width", function(d)
    {
        return (config.dataWidth) + "em";
    })           
    .attr("height", function(d)
    {
        return (config.scale) + "em";
    });                 
}


For the y attribute, the row will appear lower for each iteration of the rows array, so have the value as r multiplied by scale (which is the height of each rect).
for (let r = 0; r <= graphData.rows.length; r++)
{
    chart.selectAll("rect.rect_" + graphData.rows[r])
    .data(graphData.cols)
    .enter()
    .append("rect")
    .attr("x", function(d, i)
    {
        return (i * config.dataWidth) + "em";
    })
    .attr("y", function(d)
    {
        return (r * config.scale) + "em";
    })

    .attr("width", function(d)
    {
        return (config.dataWidth) + "em";
    })           
    .attr("height", function(d)
    {
        return (config.scale) + "em";
    });                 
}


In the CSS, this class is no longer needed. Remove the properties.
.hmChartSvg text
{
    /*
    fill: rgba(255, 255, 0, 1);
    text-anchor: middle;
    font-weight: bold;
    */
}


Change it to target rect tags instead, and add specifications for stroke and stroke-width. For this one, I am using a white color.
.hmChartSvg rect
{
    /*
    fill: rgba(255, 255, 0, 1);
    text-anchor: middle;
    font-weight: bold;
    */
    stroke: rgba(255, 255, 255, 1);
    stroke-width: 1;

}


So now you can see the individual rect tags!




Next

The basic placeholders are there, along with X and Y axes. We will follow up with filling in the Heatmap with colors!