Monday 19 September 2022

Web Tutorial: D3 Heatmap (Part 1/3)

Welcome to another episode of D3 goodness!

Since 2018, I've been toying around with D3 and getting it to produce various charts - bar, line and pie - using Liverpool FC's football statistics. Today, I will focus on one more - the Heatmap. Heatmaps are great at visually representing two-dimensional data. With the other kinds of charts earlier mentioned, we were only able to view one dimension at a time whether it was by season or by player. With the Heatmap, we will visualize the data by season and player.

Recycling code

The layout will be largely the same as what we used for the Bar Chart. So let's take the code wholesale and make some changes. We will cut out whole swathes of code, but preserve what can be reused later.

For the HTML, we begin by changing the title.
<title>D3 Heatmap</title>


The styling will be changed because we want to temporarily change some values. Other changes will be less temporary. For instance, for the sake of good naming conventions, let's change every instance of "bar" to "hm". And since we will have two legends instead of a scale and a legend, let's change every instance of "Legend" to "LegendX" and every instance of "Scale" to "LegendY". Also, make sure that the styling for hmChartSvg's rect elements is empty for now. The background colors of the various placeholders are set to full opacity for the time being.

And before you do any of that, remove the classes barChartDataMean, barChartLine and barChartFadedLine. We won't be using any of those.
<style>
    body
    {
        font-size: 12px;
        font-family: arial;
    }

    .hmDashboard
    {
        height: 2em;
        width: 100%;
        text-align: center;
    }

    .hmChart
    {
        outline: 1px solid #000000;
        background-color: rgba(200, 0, 0, 1);
    }

    .hmChartContainer
    {
        margin: 0 auto 0 auto;
    }

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

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

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

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

    .hmChartSvg rect
    {

    }

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

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

    .hmLegendXSvg text
    {
        fill: rgba(255, 255, 0, 1);
        text-anchor: middle;
        font-weight: bold;
    }  
    /*
    .barChartDataMean
    {
        stroke: rgba(0, 0, 0, 1);
        stroke-width: 1px;
        stroke-dasharray: 1, 5;
    }  

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

    .barChartFadedLine
    {
        stroke: rgba(255, 255, 0, 0.2);
        stroke-width: 1px;
    }  
    */
</style>


In the HTML, aside from name changes to the divs, you also want to remove the drop-down list for years.
<div class="hmChartContainer">
    <div class="hmDashboard">
        <!--                 
        <select id="ddlYear">

        </select>
        -->

        <select id="ddlStat">

        </select>
    </div>

    <div class="hmChart">
        <svg class="hmLegendYSvg">

        </svg>

        <svg class="hmChartSvg">

        </svg>

        <br style="clear:both"/>

        <svg class="hmFillerSvg">

        </svg>

        <svg class="hmLegendXSvg">

        </svg>
    </div>
</div>


In the script tag, leave graphData alone. For config, make changes to the following properties. scaleWidth has been renamed to legendYWidth and legendHeight has been renamed to legendXHeight. mean has been removed as a property.
"scale": 12,
"dataWidth": 10,
"dataSpacing": 2,
"legendYWidth": 10,
"legendXHeight": 4,
"max": 0,
//"mean": 0,


For the setData() method, I could go through step by step what changes need to be made, but it would be easier to just clear everything out and start afresh.
"setData": function ()
{

}


Outside of the config object, remove this from the rest of the code. We will still need to determine the max property of the config object, just not here. And we no longer need the drop-down list for years.
/*
config.max = d3.max(graphData.cols, function(d)
{
    var maxStat = d3.max(d.stats, function(x)
    {
        return (x.goals > x.appearances ? x.goals : x.appearances);
    }
    );

    return maxStat;
}
);

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

ddlYear.selectAll("option")
.data(graphData.rows)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});
*/

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

ddlStat.selectAll("option")
.data(graphData.stats)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

config.setData();

//d3.select("#ddlYear").on("change", function() { config.setData(); });
d3.select("#ddlStat").on("change", function() { config.setData(); });


Let's make a change to getChartHeight(). Add datalength as a parameter, and use it to determine the final result.
"getChartHeight": function(datalength)
{
    //return (this.max * this.scale * 1.5);
    return (datalength * this.scale);
},


And a change to getChartWidth(). This will be highly simplified.
"getChartWidth": function(datalength)
{
    //return (datalength * (this.dataWidth + this.dataSpacing)) + (datalength * 0.5);
    return (datalength * this.dataWidth);
},


Now we will get to work on setData(). Begin by declaring stat and setting it to the selected value of the ddlStat drop-down list.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
}


Next, we declare height and width. height is calculated using the getChartHeight() method, and we will pass in, as an argument, the number of elements in the rows array of the graphData object. Likewise, width is calculated using the getChartWidth() method, and we will pass in, as an argument, the number of elements in the cols array of the graphData object. Remember that this is a chart with two axes - one for seasons and one for players.
"setData": function ()
{
    var stat = d3.select("#ddlStat").node().value;
    
    var height = config.getChartHeight(graphData.rows.length);
    var width = config.getChartWidth(graphData.cols.length);
}


While we're at it, let's declare some variables, assigning to them the elements found in the HTML. These will ultimately make up the visual structure of our chart.
var stat = d3.select("#ddlStat").node().value;

var container = d3.select(".hmChartContainer");
var wrapper = d3.select(".hmChart");
var legendY = d3.select(".hmLegendYSvg");
var chart = d3.select(".hmChartSvg");
var legendX = d3.select(".hmLegendXSvg");
var filler = d3.select(".hmFillerSvg");


var height = config.getChartHeight(graphData.rows.length);
var width = config.getChartWidth(graphData.cols.length);


Now what we do is set the width of container. It will use width, and the width of the Y-axis, the legendYWidth property. We don't set the height because soon we will be setting the height for its child, wrapper, which will cause container to expand vertically.
var height = config.getChartHeight(graphData.rows.length);
var width = config.getChartWidth(graphData.cols.length);

container
.style("width", function(d)
{
    return (width + config.legendYWidth) + "em";
});


For wrapper, we set height. This will use height and the height of the X-axis, the legendXHeight property.
container
.style("width", function(d)
{
    return (width + config.legendYWidth) + "em";
});

wrapper
.style("height", function(d)
{
    return (height + config.legendXHeight) + "em";
});


Now take a look! The deep red portion is wrapper. The blue, green, scarlet and black portions are the rest of the placeholders that have not yet had their heights and widths specified. Also notice the drop-down list at the top.




Let us now move on to the rest of the placeholders. We want the Y-axis to use the legendYWidth property for width, and use height for its height.
wrapper
.style("height", function(d)
{
    return (height + config.legendXHeight) + "em";
});

legendY
.style("width", function(d)
{
    return config.legendYWidth + "em";
})
.style("height", function(d)
{
    return height + "em";
})
.html("");


You see the green portion has expanded to what we have specified! That is our Y-axis.




chart is straightforward. We just use width and height.
legendY
.style("width", function(d)
{
    return config.legendYWidth + "em";
})
.style("height", function(d)
{
    return height + "em";
})
.html("");

chart
.style("height", function(d)
{
    return height + "em";
})
.style("width", function(d)
{
    return width + "em";
})
.html("");


The blue portion is chart, and it is taking shape.




We now specify the size of legendX.
chart
.style("height", function(d)
{
    return height + "em";
})
.style("width", function(d)
{
    return width + "em";
})
.html("");

legendX
.style("height", function(d)
{
    return config.legendXHeight + "em";
})
.style("width", function(d)
{
    return width + "em";
})
.html("");


The scarlet part, the X-axis, has now expanded! But it will look off until we finish the job with filler.




And here we have it...
legendX
.style("height", function(d)
{
    return config.legendXHeight + "em";
})
.style("width", function(d)
{
    return width + "em";
})
.html("");

filler
.style("width", function(d)
{
    return config.legendYWidth + "em";
})
.style("height", function(d)
{
    return config.legendXHeight + "em";
});


The black portion, filler, has had its size specified, and now we're in business. The deep red portion is no longer visible because it has been covered completely. Good job!




Next

We will work on the X and Y axes.

No comments:

Post a Comment