Saturday 26 November 2022

Spot The Bug: No Face Given!

Tonight's episode of Spot The Bug is in, and it will feature HTML, jQuery and SVG.

HTML never ceases to surprise me. Every time I think I have its number, it flips the script on me. This was one such time, when I was trying to wrangle some dynamic SVG in jQuery.

All these bugs...

Basically, I had an SVG template for a face, and what I was trying to do was dynamically, on a click of a button, superimpose facial features onto that blank face. Sounds simple enough, right?

Here's the code for the HTML. Note that the JavaScript file is separate.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Facemaker</title>

        <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
        <script src="js/face.js"></script>

        <script>
            $(document).ready
            (
                () =>
                {
                    generateFace();
                }
            );
        </script>
    </head>
    
    <body>
        <table>
            <tr>
                <td style="width: 200px">
                    <label for="ddlEyes">Eyes
                        <select id="ddlEyes" onchange="generateFace()">
                            <option value="shades">Shades</option>
                            <option value="staring">Staring</option>
                            <option value="shut">Shut</option>
                        </select>
                    </label>
                    <br /><br />
                    <label for="ddlMouth">Mouth
                        <select id="ddlMouth" onchange="generateFace()">
                            <option value="grim">Grim</option>
                            <option value="lipstick">Lipstick</option>
                            <option value="smile">Smile</option>
                        </select>
                    </label>
                    <br /><br />
                    <label for="ddlNose">Nose
                        <select id="ddlNose" onchange="generateFace()">
                            <option value="big">Big</option>
                            <option value="narrow" selected>Narrow</option>
                            <option value="small">Small</option>
                        </select>
                    </label>
                </td>

                <td style="width: 200px">
                    <svg id="svgFace" style="width:200px; height:200px; outline: 1px solid black">

                    </svg>
                </td>
            </tr>
        </table>
    </body>
</html>


And here's the JavaScript code.
js/face.js
function generateFace()
{
    var eyesHTML = generateEyes($("#ddlEyes").val());
    var mouthHTML = generateMouth($("#ddlMouth").val());
    var noseHTML = generateNose($("#ddlNose").val());

    $("#svgFace").html("");

    var faceHTML = $("<ellipse />");
    faceHTML.attr("cx", 100);
    faceHTML.attr("cy", 100);
    faceHTML.attr("rx", 60);
    faceHTML.attr("ry", 80);
    faceHTML.attr("fill", "rgba(255, 200, 0, 1)");

    $("#svgFace").append(faceHTML);
    $("#svgFace").append(noseHTML);
    $("#svgFace").append(eyesHTML);
    $("#svgFace").append(mouthHTML);
}

function generateEyes(val)
{
    if (val == "shades")
    {
        var path = $("<path />");
        path.attr("d", "M55,65 C65,90 75,90 100,70 C125,90 135,90 145,65 Z");
        path.attr("fill", "black");

        return path;
    }

    if (val == "staring")
    {
        var text = $("<text />");
        text.html("O&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;O");
        text.attr("x", 100);
        text.attr("y", 80);
        text.attr("width", 100);
        text.attr("style", "font: 16px sans-serif");
        text.attr("stroke", "black");
        text.attr("stroke-width", 2);
        text.attr("text-anchor", "middle");

        return text;
    }

    if (val == "shut")
    {
        var text = $("<text />");
        text.html("U&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;U");
        text.attr("x", 100);
        text.attr("y", 80);
        text.attr("width", 100);
        text.attr("style", "font: 16px sans-serif");        
        text.attr("stroke", "black");
        text.attr("stroke-width", 2);
        text.attr("text-anchor", "middle");

        return text;
    }
}

function generateMouth(val)
{
    if (val == "grim")
    {
        var path = $("<path />");
        path.attr("d", "M80,150 C90,140 110,140 120,150 ");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }

    if (val == "lipstick")
    {
        var ellipse = $("<ellipse />");
        ellipse.attr("cx", 100);
        ellipse.attr("cy", 140);
        ellipse.attr("rx", 20);
        ellipse.attr("ry", 5);
        ellipse.attr("stroke", "red");
        ellipse.attr("stroke-width", 3);
        ellipse.attr("fill", "none");

        return ellipse;
    }        

    if (val == "smile")
    {
        var path = $("<path />");
        path.attr("d", "M60,120 C90,140 110,140 140,120 ");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }    
}

function generateNose(val)
{
    if (val == "big")
    {
        var polyline = $("<polyline />");
        polyline.attr("points", "60,60,90,60,80,120,120,120,110,60,140,60");
        polyline.attr("stroke", "black");
        polyline.attr("stroke-width", 2);
        polyline.attr("fill", "none");

        return polyline;
    }

    if (val == "small")
    {
        var path = $("<path />");
        path.attr("d", "M90,110 C95,115 105,115 110,110");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }

    if (val == "narrow")
    {
        var path = $("<path />");
        path.attr("d", "M60,60 C70,50 90,40 95,60 L100,110 L105,60 C110,40 130,50 140,60");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }
}


What went wrong

When selecting different options, the code should have cleared the template, and then generated a different face depending on the options selected. In fact, the code should have generated the face upon loading, depending on the default options set.

Alas, nothing happened no matter what I clicked.




Why it went wrong

Apparently, the browser does not interpret SVG tags in exactly the same way it does HTML tags. So using the append() method on an SVG would technically put the appropriate tags within the SVG, but it would not be rendered!

See what I mean? Inspecting the code shows that the tags were inside the SVG, but the browser was not reflecting it.




How I fixed it

Ultimately, all I needed was to append this line at the end of the function. What it does, is ensure that svgFace populates its own HTML with... well, its own HTML.
    $("#svgFace").append(faceHTML);
    $("#svgFace").append(noseHTML);
    $("#svgFace").append(eyesHTML);
    $("#svgFace").append(mouthHTML);
    
    $("#svgFace").html($("#svgFace").html());
}


And there, it worked!








Moral of the story

SVG was a relatively late addition to the HTML5 specification, and as such certain things have not quite caught up. Thankfully, the solution was very simple (if not entirely intuitive).

Way to (f)ace this one,
T___T

Sunday 20 November 2022

Film Review: Black Mirror Series Two (Part 3/3)

This episode, The Waldo Moment, is really more about media than it is about tech. Still a fun ride, nonetheless.

The Premise

Waldo is an animated character, a blue bear, voiced and controlled by Jamie. Waldo is crude, rude and obnoxious - and he's a huge hit. Things escalate during the election where Waldo is deployed to harass a local politician for ratings. And later Jamie finds out, to his dismay, that the character of Waldo has grown beyond his ability to manage.

The Characters

Daniel Rigby plays James "Jamie" Salter as a scruffy alcoholic who believes in nothing, and Waldo is an extension of that nihilism. The character comes off as extremely bitter, and there's a nasty side to him we see later on.

Chloe Pirrie is Gwendolyne Harris, the fresh-faced politician. Comes across as a little unsure of herself, and a bit later on, incredibly stupid. I mean, if you're a politician, letting your guard down to a total stranger and spending the night banging him is dumb enough, but letting him know potentially career-ending secrets? Wow.

Tobias Menzies
does a fantastic job as Liam Monroe. He's a stuffy and pompous politician, and very unlikeable. But seeing him constantly being Waldo's target is going to make you feel for him, after a while... especially since, in his private moments, it is revealed that he actually does believe in the causes he champions.

Michael Shaeffer as Gwen's Campaign Manager, Roy. Wears that perpetual frown like his life depended on it.

Christina Chong as Tamsin, who seems like a smart independent woman at first, but later acts like the typical yes-woman to Jack.

Jason Flemyng as Jack Napier, Jamie's Manager. He's pragmatic, amoral and gleefully opportunistic. Always looking out for that next hustle.

David Ajala is agent Jeff Carter who wants to make a deal with Jack and "Waldo". He appears only in one scene, but damn, does he steal it!

Pip Torrens as Philip Crane the pundit who interviews Waldo. I like the way he gets discomfited halfway through.

The Mood

It's the big-city vibes. Vibrant, noisy and colorful.



At the end, during the dystopian future depicted, things are a gloomy shade of blue and black.

What I liked

The white board showing the plans of the team elicited a snicker.


These mini-Waldos are pretty cute!


"Gwen and Bear It". Love the headline!


The names of the characters seem especially meaningful. "Jack Napier" itself strikes home because it is the name for The Joker's original name in Batman. And seconds earlier during a skit, "Batman" is name-dropped by Waldo. Coincidence? Maybe. But a neat one, if so. Especially when you consider how cynical and nihilistic Jack Napier is. Jamie "Salter" is meaningful too - the character is salty in language and demeanor. For Gwendolyne, what struck me was her last name, Harris. She gets pretty harried later on!

I really love the plot development later on when Jamie discovers that nobody in his audience actually cares about him as a person, and he's very much easily replaced. The lesson being that it's easy to be crude and obviously stand for nothing... but it's also nothing special. Any idiot could do it.

What I didn't

Jamie's motivation for ruining Gwendolyne's career just seemed a little too petty. I mean, a woman doesn't call you after a one-night stand because she's in the middle of an electoral campaign - an absolutely valid reason, by the way - and somehow that's justification for humiliating her on live TV?! Weird.

Also, I didn't get why Gwendolyne not actually intending to win the current campaign and just using it to raise her profile, was seen as so objectionable. After all, isn't seeing the big picture and taking the long view a perfectly sensible thing to do for anyone, much less someone running for public office? Like, realistically were they thinking that a relative unknown on the political scene would somehow win her maiden election?

The dystopian ending was a bit too far-fetched for believability. I mean, it was interesting and all, but the cognitive leap was just too much.

Conclusion

This episode was a study of the ugliness and stupidity of the human collective consciousness. Not much about tech, at all, but no less impressive in terms of execution.

My Rating

7.5 / 10

Final thoughts on Black Mirror Series Two

Series Two, for me, was a step down from Series One. Series One was consistently good throughout whereas Series Two hits the high notes with White Bear and kind of tamps it down with Be Right Back and The Waldo Moment. Doesn't stop this from being a fantastic concept, and definitely looking forward to Series Three!

Series Two was pretty... bearable!
T___T

Thursday 17 November 2022

Film Review: Black Mirror Series Two (Part 2/3)

Moving on to the next episode... White Bear. This is another utterly dystopian and depressing episode even by Black Mirror's exalted standards.

The Premise

A woman wakes up with a headache in what appears to be a strange house. She has no recollection of how she got here. The symbol below is shown on all the TV screens she encounters.



Things take a turn for the worse when she ventures outside and finds herself in the center of some kind of bizarre hunt. There's a nasty twist at the end when this turns out to be some kind of fucked up reality show.

The Characters

Lenora Crichlow is Victoria Skillane, and boy does she do some fine work here. Her pain, fear and distress are palpable at every turn, and even when I learned what she's done to deserve this cruel and unusual punishment, I didn't stop feeling sympathy.

Tuppence Middleton plays Jem, the ally Victoria encounters during the hunt, who turns out to be messing with her. Middleton didn't do such a great job, really. There was just something really off about how she acted that made me suspect her right from the get-go. Or maybe that's just some really exceptional acting. I can't quite decide.

Michael Smiley is the showrunner Baxter and one of the hunters. He's somehow even more menacing and scary when he's trying to be affable to the audience, than when he's acting like a gun-toting maniac. The accent is pretty fun to listen to, though.

Ian Bonar as Damian, who gets "killed" early. Later on when we see him again, he's pretty matter-of-fact about the job he's doing.

Elisabeth Hopper and Nick Bartlett as the other hunters. They seem to be having a lot of fun in their roles, and really, who wouldn't?

Imani Jackson as Jemima Sykes, the little black girl who was murdered. She's only seen in footage, and she is adorable! Which has the effect of making the crime seem worse, I suppose.

The Mood

The paranoia has been ratcheted up to unreal levels during this entire episode. When Victoria is being hunted by masked killers, there's shades of The Purge coupled with the ridiculous sight of people recording all this footage on their cellphones. It's unknown who is actually on Victoria's side, until the final moment where it's revealed that nobody is.

In fact, due to some crime she committed which, in turn due to brainwashing, she cannot recall, she's pretty much the center of a lot of hate at fever pitch.


After that, the scenes are more of the horror of being universally detested - there's no more mystery here.

What I liked

The setup is so chilling, OMG. These shots of victims being strung up on trees is so macabre.


It's great that little Jemima had a white mother and black father. That's pretty woke and inclusive without being too in-your-face about it.


Ouch. These guys have been repeating the scenario for how many days in a row now?! Victoria must totally stink after that many days of sweating with no time to take a bath!


Making this reality game show about Jemima's white teddy bear is just a devilishly neat touch.

The scenes that come on during the credits, showing everything that went on backstage, really nailed it for me.

What I didn't

That White Bear symbol looks like a Tetris block and really serves no purpose whatsoever. Ostensibly it was the same tattoo on Victoria's boyfriend's skin and used as the symbol for this hunt, but I wonder if the show would have been better without it.

It just seems unrealistic that in a three-on-one scenario, with the first hunter tangled up with Damian and Jem having a fire extinguisher with her and Victoria pretty much free to do anything, that they would just leave them there and run. Sure, it was an act, but that's what killed any realism for me at that point.

Conclusion

This episode does so much with a small main cast and an hour's running time. Like the last episode, there's almost zero humor in this one, but fuck it, it doesn't need any. The story spun around this extremely nasty punishment is masterfully done. It also manages to poke fun at the Bystander Syndrome, that phenomenon where people tend to just take cellphone footage of people in trouble, without even trying to help.

But the true horror is that the tragedy is being milked for profit. White Bear Monument Park is set up to ostensibly let the public take part in Victoria's punishment, but ultimately this feels like an excuse to cash in on little Jemima's grisly fate. What happens to Victoria, having to relive the terror of being hunted day after day, to finally be confronted by her crimes which she can no longer remember, is nothing short of horrific. Especially since her memory has been repeatedly erased and she's pretty much a clean slate with none of the murderous personality that led her to commit those crimes in the first place. The fact that Baxter and the audience are so self-righteous about it, really underlines the nastiness of Victoria's fate regardless of how horrible her original crime was. Trial by Social Media, anyone?

My Rating

9.5 / 10

Next

The Waldo Moment

Tuesday 15 November 2022

Film Review: Black Mirror Series Two (Part 1/3)

Time for more Black Mirror goodness, and this time we will be moving on to Series Two!


We have another three stories, all set in a reimagined UK where the prevalence of tech changes lives in the bleakest ways possible. And the horror of it all is, these changes are insidious, with positive developments the most obvious and the less positive aspects all but invisible until it's too late.

Warning

Strong language and the ugly parts of human nature acerbated by technology. Also, spoilers. You've been warned!

The Premise

The first story is Be Right Back, and it deals with the death of Ash Starmer. An A.I gleans data of his speech patterns and mannerisms from from footage of Ash himself, to create a facsimile of Ash for his grieving girlfriend. This eventually escalates to a physical manifestation instead of just text and audio.


The Characters

Domhnall Gleeson as Ash Starmer, a guy who's somewhat addicted to Social Media and is often pretty distracted. The actor later has to play a robotic version of himself, and he does a decent job, I suppose. At least he doesn't ham it up to unbelievable levels.

The insanely hot Hayley Atwell of Captain America fame is his snarky artist girlfriend Martha, who turns to A.I after his death in a desperate attempt to feel him once again. I found myself surprisingly sympathetic to her struggles.

Claire Keelan as her sister Naomi, who has her own family and is preoccupied with them all the time. She's caring and concerned, but has very little to say.

Sinead Matthews as Sara. She looks a little like Sarah Michelle Gellar who played Buffy the Vampire Slayer. That's how I remember her, anyway. She serves as a plot device to the A.I, signing her up for it and giving her the starter explanation as to how it works. She looks a right mess, by the way.

The Mood

Other than the original rainy nights, most of this episode takes place in good old country weather - sunny and cloudy. Later on, things get grim and foreboding but it never goes full creep. The music is slow and wistful, and the entire setting is more melancholy than scary.

What I liked

This is a recurring theme. Martha checks that Ash is paying attention to her, by saying the most outrageous things. That level of snark!


This is a nice touch. The two of them lived in Ash's grandma's house, and there's this shot of the doorframe being used to measure little Ash's height.

The storyline was good up to the whole A.I angle, where all the data was derived from Social Media. It's an interesting concept. The distinct message is that Social Media only shows a small percentage of what a person is really like. Ash Starmer's public persona was mostly sardonic and funny, but the A.I can't really speak much to his inner, private persona. Ultimately, the copy was hollow.

Generally, the setting is beautiful and serene. It's by a country seaside and all.

What I didn't

The entire storyline just got a bit much near the end. Seriously, a full-on sentient robot?!


Oh yeah, we all knew this was gonna devolve to the sexbot angle at some point.



The ending is a bit of a head-scratcher. So the robot is allowed to live, but in the attic, and Martha's daughter now has some kind of dad?

Conclusion

Not much to this plot. Just a lot of sorrow, and a heavy and somber atmosphere. I'll be honest - I liked the story a whole lot better before the robot showed up, and from there on, it all got increasingly ridiculous. Honestly, it would have been a nice touch if the robot turned homicidal or something.

This episode, quite tellingly, has a very small cast.

My Rating

6 / 10

Next

White Bear

Thursday 10 November 2022

A letter to a younger web developer

I turned 45 maybe five days ago. And this being my birthday month, I'm feeling whimsical.

You know how people ask what you'd say to your younger self? Well, in the unlikely event I was able to travel back in time to converse with the insecure, messed up individual I was back in my twenties, this is probably what I would say.

"Son, I know you're going through crazy times. That's how I remember them."

"You're going to have people come up to you and tell you how much money you should be having in your bank account at this age. Where you should be in your career. That you should be married. That you should have a kid on the way."

"You know what? Fuck these people. All of 'em. They make the same mistake everyone else does - they assume you want something just because they want it. None of them suspect, even for a moment, that you have zero intention of being even a little bit like them. If they had enough honesty with themselves, they would question what exactly was so special and unique about them, that anyone would want what they have."

Honest talk with self.

"But that's people. People are only good at being themselves. They're not good at being you."

"You know who's good at being you? No, it's not you. Don't listen to yourself - you're young and stupid and you don't know shit. Listen to me. I have had 45 years of experience being you. There is no one on this planet more qualified than I am, to talk about being you."

"So don't be kind to yourself. Be kind to me. Stop drinking. Stop fucking around. You are enjoying a position of immense privilege. You're a web developer. You code. Not only that, unlike just about everyone else who merely tolerate their jobs, you actually love what you do. You're part of an industry that will boom in the next few decades. Get your act together and ride that wave. You don't have to be the best at what you do. You don't even have to be above-average. You just have to be not mind-numbingly incompetent. And everything will come."

"Oh, and remember how you're now laughing at your friends who are married, and how they panic every time they get a missed call from the wife? How they have to go home early, and how they have to watch everything they do? Keep laughing, boy. Laugh while you can. In twenty years, you'll be the joke."

That's all!

I'm bad at ending letters, but this seemed a good place to stop. Is this a little short? Well, maybe... but less is more, right?

Your older web developer,
T___T

Sunday 6 November 2022

Web Tutorial: Highcharts Heatmap

The Heatmap makes a comeback!

In September, I walked you through how to create a Heatmap in D3. Now you will see how the process is like, using Highcharts. As to be expected, this has been reduced to largely a configuration task.

Let's begin with some HTML. You will see that the divs container and dashboard have already been styled, as has the label for the drop-down list in the dashboard, ddlStat.
<!DOCTYPE html>
<html>
    <head>
        <title>Heatmap</title>

        <style>
            #container
            {
                width: 100%;
                height: 600px;
            }

            #dashboard
            {
                width: 100%;
                height: 100px;
                text-align: center;
            }

            label
            {
                display: inline-block;
                width: 20em;
                font-family: verdana;
                color: rgba(200, 0, 0, 1);
            }
        </style>

        <script>

        </script>
    </head>

    <body>
        <div id="container">

        </div>

        <div id="dashboard">
            <label for="ddlStat">
                STATISTICS
                <select id="ddlStat">
                    <option value="appearances" selected>Appearances</option>
                    <option value="goals">Goals</option>
                </select>
            </label>
        </div>
    </body>
</html>


This is what the code produces. Very bare bones at the moment. You can see a large space left for the Heatmap, and the drop-down list.




Add a link to the Highcharts code, then the Heatmap module.
<head>
  <title>Heatmap</title>

  <style>
    #container
    {
      width: 100%;
      height: 600px;
    }

    #dashboard
    {
      width: 100%;
      height: 100px;
      text-align: center;
    }

    label
    {
      display: inline-block;
      width: 20em;
      font-family: verdana;
      color: rgba(200, 0, 0, 1);
    }
  </style>

  <script src="https://code.highcharts.com/highcharts.js"></script>
  <script src="https://code.highcharts.com/modules/heatmap.js"></script>


  <script>

  </script>
</head>


In the JavaScript, create the data object upon page load. This data is the same data that has been loaded in all the other Highcharts web tutorials so far. Make sure data is declared, outside of this.
<script>
  document.addEventListener("DOMContentLoaded", function () {
      data["2016/2017"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings"],
          "appearances": [41, 27, 29, 2],
          "goals": [12, 1, 13, 0]
      };

      data["2017/2018"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings", "Alex Oxlade-Chamberlain", "Mohd Salah"],
          "appearances": [54, 20, 44, 14, 42, 52],
          "goals": [27, 1, 20, 1, 5, 44]
      };

      data["2018/2019"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 44, 50, 2, 52, 41],
          "goals": [16, 1, 26, 0, 27, 1]
      };

      data["2019/2020"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [52, 40, 47, 43, 48, 39],
          "goals": [12, 4, 22, 8, 23, 2]
      };

      data["2020/2021"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 28, 48, 17, 51, 42],
          "goals": [9, 1, 16, 1, 21, 0]
      };  
         
    });
    
    const data = [];
</script>


After that, run the renderHeatmap() function, and create it.
<script>
  document.addEventListener("DOMContentLoaded", function () {
      data["2016/2017"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings"],
          "appearances": [41, 27, 29, 2],
          "goals": [12, 1, 13, 0]
      };

      data["2017/2018"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings", "Alex Oxlade-Chamberlain", "Mohd Salah"],
          "appearances": [54, 20, 44, 14, 42, 52],
          "goals": [27, 1, 20, 1, 5, 44]
      };

      data["2018/2019"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 44, 50, 2, 52, 41],
          "goals": [16, 1, 26, 0, 27, 1]
      };

      data["2019/2020"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [52, 40, 47, 43, 48, 39],
          "goals": [12, 4, 22, 8, 23, 2]
      };

      data["2020/2021"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 28, 48, 17, 51, 42],
          "goals": [9, 1, 16, 1, 21, 0]
      };

      renderHeatmap();                
    });

    function renderHeatmap()
    {

    }


    const data = [];
</script>


Make sure that renderHeatmap() runs when the value in ddlStat is changed, as well.
<select id="ddlStat" onchange="renderHeatmap()">
  <option value="appearances" selected>Appearances</option>
  <option value="goals">Goals</option>
</select>


Now, let us build on the renderHeatmap() function. In here, we want to grab the value of ddlStat. Assign the value to the variable stat.
function renderHeatmap()
{
  var stat = document.getElementById("ddlStat").value;
}


Declare players as an empty array (it won't be empty for long) and seasons as an array of keys from data. We need these for the x and y axes.
function renderHeatmap()
{
  var stat = document.getElementById("ddlStat").value;
  var players = [];
  var seasons = Object.keys(data);

}


Now we will populate the players array. Iterate through seasons using a For loop.
var players = [];
var seasons = Object.keys(data);

for(let i = 0; i < seasons.length; i++)
{

}


This will be a nested For loop. We iterate through the categories array of the current data element, pointed to by the current element in seasons.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {

    }

}


If the current element does not already exist in players, push the value in.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}


Next, it's time to define chart. Use the chart() method of the Highcharts object. Pass in the id of the container div, and a callback.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}

const chart = Highcharts.chart("container", {

});


Define the chart property. It will have its own properties. Let's give it a deep red border, round borders and a border width. That's just aesthetics, but the really important property is type, and the value will be "heatmap". The colors property will be an array containing that same color we gave the border.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    colors: ["rgba(200, 0, 0, 1)"]

});

 
We will next add title and subtitle properties.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },

    colors: ["rgba(200, 0, 0, 1)"]
});


So far so good!




Back to the renderHeatmap() function. Declare heatmap_values as an empty array.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}

var heatmap_values = [];


We are going to fill in heatmap_values, and it will be a slightly complicated process. Basically, heatmap_values is an array of arrays. Each of these sub-arrays has three elements - the X-axis index, the Y-axis index and the value. For that we have a nested For loop. For the outer loop, iterate through seasons. For the inner loop, iterate through players.
var heatmap_values = [];

for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {

    }
}


In the inner loop, declare index. The value of index is based on whether the current element of players is found in the categories array of the seasons array, pointed to by the current element of seasons.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);
    }
}


If index is -1, it means the value is not found. That means you push an array containing j, i and null into heatmap_values.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);

        if (index == -1)
        {
            heatmap_values.push([j, i, null]);
        }
        else
        {

        }

    }
}


If not, the value is present. Push an array containing j, i and the appropriate value into heatmap_values. The appropriate value in this case is based on i, stat and index.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);

        if (index == -1)
        {
            heatmap_values.push([j, i, null]);
        }
        else
        {
            heatmap_values.push([j, i, data[seasons[i]][stat][index]]);
        }
    }
}


Now in the object, declare xAxis and yAxis properties. These properties are objects, each with a categories property. The value of these properties are the players and seasons arrays respectively.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },
    colors: ["rgba(200, 0, 0, 1)"],
    xAxis:
    {
        categories: players
    },
    yAxis:
    {
        categories: seasons
    }

});


Next we have the series property. This object's properties are set as given. name is an empty string and borderWidth is 1. For data, we use what we created for heatmap_values. nullColor is black. This is for all null values in heatmap_values.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },
    colors: ["rgba(200, 0, 0, 1)"],
    xAxis:
    {
        categories: players
    },
    yAxis:
    {
        categories: seasons
    },                    
    series:
    [
        {
            name: "",
            borderWidth: 1,
            data: heatmap_values,
            nullColor: "rgba(0, 0, 0, 1)"
        }
    ]

});


OK, stuff is happening! You will see black spots in the Heatmap where values are null. Unfortunately, everything else is red.




What we will do here, is use the colorAxis property to create a color scheme. Like most things in Highcharts, it's an object itself. We set its min property to 0, which means the minimum value of the data set is set to 0. minColor is set to white, which means the color white corresponds to the value 0. maxColor is set to deep red.
xAxis:
{
    categories: players
},
yAxis:
{
    categories: seasons
},
colorAxis:
{
    min: 0,
    minColor: "rgba(255, 255, 255, 1)",
    maxColor: "rgba(200, 0, 0, 1)"
},

series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)"
    }
]


Now you see that the colors reflect the number of goals or appearances! And if you mouse over each of the squares, you will see the x, y and values that make up the color!




Add the dataLabels property within the object. It is an object as well. It has the properties enabled, which you set to true, and color. I have chosen yellow.
series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)",
        dataLabels:
        {
            enabled: true,
            color: "rgba(255, 255, 0, 1)"
        }

    }
]


Now you see the numbers appear.




Final touch!

Let's get to work on the tooltips. Right now, these tooltips are just showing x, y and number values. We can modify them to make sense. We will do that using the tooltip property.
colorAxis:
{
    min: 0,
    minColor: "rgba(255, 255, 255, 1)",
    maxColor: "rgba(200, 0, 0, 1)"
},
tooltip:
{

},  
                 
series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)",
        dataLabels:
        {
            enabled: true,
            color: "rgba(255, 255, 0, 1)"
        }
    }
]


The formatter property is a callback.
tooltip:
{
    formatter: function ()
    {

    }

},    


Now at this point you need to be familiar with the point object. It's all documented at this link. We use the data in that object to format a string to present the data nicely.
tooltip:
{
    formatter: function ()
    {
            return this.point.series['xAxis'].categories[this.point.x] + " " + (stat == "appearances" ? "made" : "scored") + " " + this.point.value + " " + stat + " in the " + this.point.series['yAxis'].categories[this.point.y] + " season.";
    }
},    


If the data was null, we just state that the player was not part of the club at that time period.
tooltip:
{
    formatter: function ()
    {
            if (this.point.value == null) return this.point.series['xAxis'].categories[this.point.x] + " was not part of the club during the " + this.point.series['yAxis'].categories[this.point.y] + " season.";

            return this.point.series['xAxis'].categories[this.point.x] + " " + (stat == "appearances" ? "made" : "scored") + " " + this.point.value + " " + stat + " in the " + this.point.series['yAxis'].categories[this.point.y] + " season.";
    }
},    


Now see what happens when you mouse over a square.




And if you mouse over a black square, it gives you this!




That was easy, eh? Highcharts takes away most of the hassle. Pity the commercial version is so darn expensive.

With heat and passion,
T___T

Tuesday 1 November 2022

That Sinking Feeling At Twitter

Looks like it finally happened. Elon Musk is the official owner of Twitter.

From the time I last spoke on this, there has been a series of twists in the saga before cumulating in Musk eventually completing the acquisition roughly three days ago. He trotted into the Twitter headquarters in San Francisco carrying a bathroom sink and Tweeting "Let that sink in."


Oh. My. God.

Being a peddler of corny jokes myself, ignoring the fact that I'm really not a big fan of Elon Musks's obnoxious tech-bro persona, who better qualified to make dad jokes than a father of ten? Just saying.

Not having a Twitter account myself, and therefore having very little skin in the game, I'm largely unconcerned with the acquisition. What actually did catch my attention was this open letter that Twitter employees wrote to Elon Musk to protest his planned layoffs of up to two-thirds of staff.

The Letter

I had several issues with the letter - its tone, content and the fact that large parts of it barely made any sense. Let's start with the letter in its entirety. I originally saw the letter here

Staff, Elon Musk, and Board of Directors:

We, the undersigned Twitter workers, believe the public conversation is in jeopardy.

Elon Musk's plan to lay off 75% of Twitter workers will hurt Twitter's ability to serve the public conversation. A threat of this magnitude is reckless, undermines our users' and customers' trust in our platform, and is a transparent act of worker intimidation.

Twitter has significant effects on societies and communities across the globe. As we speak, Twitter is helping to uplift independent journalism in Ukraine and Iran, as well as powering social movements around the world.

A threat to workers at Twitter is a threat to Twitter's future. These threats have an impact on us as workers and demonstrate a fundamental disconnect with the realities of operating Twitter. They threaten our livelihoods, access to essential healthcare, and the ability for visa holders to stay in the country they work in. We cannot do our work in an environment of constant harassment and threats. Without our work, there is no Twitter.

We, the workers at Twitter, will not be intimidated. We recommit to supporting the communities, organizations, and businesses who rely on Twitter. We will not stop serving the public conversation.

We call on Twitter management and Elon Musk to cease these negligent layoff threats. As workers, we deserve concrete commitments so we can continue to preserve the integrity of our platform.

We demand of current and future leadership:

Respect: We demand leadership to respect the platform and the workers who maintain it by committing to preserving the current headcount.

Safety: We demand that leadership does not discriminate against workers on the basis of their race, gender, disability, sexual orientation, or political beliefs. We also demand safety for workers on visas, who will be forced to leave the country they work in if they are laid off.

Protection: We demand Elon Musk explicitly commit to preserve our benefits, those both listed in the merger agreement and not (e.g. remote work). We demand leadership to establish and ensure fair severance policies for all workers before and after any change in ownership.

Dignity: We demand transparent, prompt and thoughtful communication around our working conditions. We demand to be treated with dignity, and to not be treated as mere pawns in a game played by billionaires.

Sincerely,

Twitter workers


So, let's unpack!

Elon Musk's plan to lay off 75% of Twitter workers will hurt Twitter's ability to serve the public conversation. A threat of this magnitude is reckless, undermines our users' and customers' trust in our platform, and is a transparent act of worker intimidation.

Wait, "worker intimidation"? What's with the "intimidation" part? What would Elon Musk be trying to intimidate them into doing? Layoffs after an acquisition aren't at all uncommon, so I really have no idea where this is coming from.

A threat to workers at Twitter is a threat to Twitter's future. These threats have an impact on us as workers and demonstrate a fundamental disconnect with the realities of operating Twitter. They threaten our livelihoods, access to essential healthcare, and the ability for visa holders to stay in the country they work in. We cannot do our work in an environment of constant harassment and threats. Without our work, there is no Twitter.

Seems a little pompous, but OK. Again, I'm not sure what this "harassment" thing is coming from.

We call on Twitter management and Elon Musk to cease these negligent layoff threats. As workers, we deserve concrete commitments so we can continue to preserve the integrity of our platform.

In a professional environment, nobody "deserves" anything. That's childish. It is not about what you deserve or don't deserve, but rather about what you can and can't get.

Respect: We demand leadership to respect the platform and the workers who maintain it by committing to preserving the current headcount.

Layoffs were going to happen, with or without the acquisition. While Elon Musk probably isn't going to really axe two-thirds of staff (at least, not right away, that would be insane), expecting no layoffs at all is utterly unrealistic, and including this demand undermines the credibility of this letter.

Safety: We demand that leadership does not discriminate against workers on the basis of their race, gender, disability, sexual orientation, or political beliefs. We also demand safety for workers on visas, who will be forced to leave the country they work in if they are laid off.

I was under the impression that companies in the USA already can't legally discriminate against workers on the basis of their race, gender, disability, sexual orientation, or political beliefs, in any case. So this seems a little redundant.

It could be argued that these workers, especially if they have been behind the uneven enforcement of moderation policies and turning Twitter into a left-wing echo chamber full of enthusiastically woke virtue-signallers, really have no leg to be standing on with regard to discrimination on the basis of political beliefs. But if I go this route, it could take all night... so I won't bother.

Protection: We demand Elon Musk explicitly commit to preserve our benefits, those both listed in the merger agreement and not (e.g. remote work). We demand leadership to establish and ensure fair severance policies for all workers before and after any change in ownership.

Oh, good luck with that! Elon Musk has a reputation of not being enamored with remote work at all. Anyone making this a demand is practically begging to be part of the layoffs.

And hold on, I thought Elon Musk was supposed to be "preserving the current headcount". Why would severance policies come in, then? This is a headscratcher.

Dignity: We demand transparent, prompt and thoughtful communication around our working conditions. We demand to be treated with dignity, and to not be treated as mere pawns in a game played by billionaires.

Were these people actually under the impression that they weren't pawns? Delusional. You work for Twitter. Not being a pawn of some sort is not an option.

Meet our demands, or else!

I also notice that the word "demand" is used a lot. That is frankly bizarre. When people make demands, unless they don't mind looking utterly ridiculous, they are usually in a position to be doing so. There are several demands in this letter... and no mention of any concrete consequences if these demands are not met. Like, what will these guys do if Musk decides to fire them anyway? Stop working at Twitter? How does this work? How is any of this supposed to work?

Final words

My sympathies go out to those who have lost their jobs, or are in a position to lose theirs. It's rarely fun. And it is absolutely natural to experience anxiety over this. But writing a letter like that really isn't the way to go. If you're going to be unemployed soon, the last thing you want to do is look stupid.

I sink, therefore I am.
T___T