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

No comments:

Post a Comment