Saturday 7 March 2020

Web Tutorial: D3 Bar Chart (Part 4/4)

There are so many ways the look and feel of this chart could be improved.Let's begin. This will be fun, I swear.

Lines

Let's begin with a few lines. Visually, well-placed lines help increase the readability of a chart.

The scale needs a vertical line, for sure. Just add one line tag, style it using the existing CSS class barChartLine.
scale.selectAll("text")
.data(scaleData)
.enter()
.append("text")
.attr("x", function(d)
{
    return (config.scaleWidth + 1) + "em";
})
.attr("y", function(d)
{
    return ((height * 2) - ((d * config.scale * 2) - 0.25)) + "em";
})
.text(function(d)
{
    return (d == 0 ? "" : d);
});

scale
.append("line")
.attr("class", "barChartLine");

chart.selectAll("rect")
.data(dataSet.stats)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


Then set the x1, x2, y1 and y2 values. Since this is a vertical line on the right side of the scale, x1 and x2 will share the same value, which is the scaleWidth property of the config object.
scale
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return config.scaleWidth + "em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
});


For y1, it's 0em because it starts at the top. y2's value is height, because it will end at the bottom.
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return config.scaleWidth + "em";
})
.attr("y1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
})
.attr("y2", function(d)
{
    return height + "em";
});


Do you see the vertical line? Makes the scale look better already, doesn't it?


Let's do the same for the legend. In legend, append a line tag and style is using barChartLine.
legend.selectAll("text")
.data(dataSet.labels)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
    return config.dataSpacing + "em";
})
.text(function(d)
{
    return d;
});

legend
.append("line")
.attr("class", "barChartLine");


This is a horizontal line, spanning from the left to the right of the chart area. So x2 is obviously 0em, and x2's value is width.
legend
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
});


y1 and y2 have the same value, and since the line will be on the top edge of the legend, the value is 0em.
legend
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("y1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
})
.attr("y2", function(d)
{
    return "0em";
});


There's the horizontal line beneath all the bars.


Now let's have horizontal lines across the chart, to visually aid in deciphering the values.

First, create the barChartFadedLine CSS class. It's a black line, but at 20% opacity.
.barChartLine
{
    stroke: rgba(0, 0, 0, 1);
    stroke-width: 1px;
}  

.barChartFadedLine
{
    stroke: rgba(0, 0, 0, 0.2);
    stroke-width: 1px;
}


We place the code here because the lines have to appear behind the bars. For the data() method, we use scaleData which has already been calculated. We will style the lines using the CSS class we just created.
scale
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return config.scaleWidth + "em";
})
.attr("y1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
})
.attr("y2", function(d)
{
    return height + "em";
});

chart.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartFadedLine");

chart.selectAll("rect")
.data(dataSet.stats)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


These are all horizontal lines spanning the left and right side of the chart, so x1 is 0em and x2 is width. Am I sounding repetitive yet?
chart.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartFadedLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
});


y1 and y2 will share the same value - refer to the scale in the previous part of the tutorial, it's the exact same logic.
chart.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartFadedLine")
.attr("x1", function(d)
{
    return "0em";
})
.attr("y1", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d)
{
    return width + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


Nice!

Stabilizing chart size

When we select different years or stats, the data changes and the chart size changes with it. That's how it was designed. But that's kind of annoying, and it's hard to fit this chart on a page with other things if it's going to constantly change size.

So, what if we just calculated the maximum value regardless of year or statistic, and then used that consistently?

Delete this line.
config.mean = d3.mean(dataSet.stats, function(d) { return d; });
//config.max = d3.max(dataSet.stats, function(d) { return d; });


Now, outside of the config object and just before declaring the ddlYear variable, we set the max property of the config object. Why do we declare that outside? That's because it's computationally expensive, and we don't want to be repeating it every time we run the setData() method, especially when the result is the same no matter what data we are viewing.

Again, we use the max() method of the d3 object. Pass in the cols array of the graphData object.
    }
};

config.max = d3.max(graphData.cols, function(d)
{

}
);

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


We're going to create a nested function, reusing max(). Declare the variable maxStat, and return it.
config.max = d3.max(graphData.cols, function(d)
{
    var maxStat;

    return maxStat;
}
);


We set maxStat using the max() method again, and this time, we pass in the stats array of d, which is the current object being traversed from cols.
config.max = d3.max(graphData.cols, function(d)
{
    var maxStat = d3.max(d.stats, function(x)
    {

    }
    );

    return maxStat;
}
);


Here, we want to return either the goals or appearances stat, whichever is greater.
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;
}
);


Oh yeah. Come to daddy, gorgeous! Now no matter what data you are viewing, the chart stays enlarged!


Color Scheme

This is a Liverpool statistic bar chart, yes? So let's make it red and yellow. First, set the various colors we've given the SVGs to 0% opacity, effectively giving them no color.
.barScaleSvg
{
    width: 5em;
    height: 20em;
    float: left;
    background-color: rgba(0, 255, 0, 0);
}

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

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

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


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

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


Set the background color for the barChart CSS class to a deep red.
.barChart
{
    outline: 1px solid #000000;
    background-color: rgba(200, 0, 0, 1);
}


What a dramatic difference!


Now let's set text and lines to yellow. Also, set all rect tags in the barChartSvg CSS class, to have a yellow background.
.barScaleSvg text
{
    fill: rgba(255, 255, 0, 1);
    text-anchor: end;
    font-size: 0.5em;
}

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

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


.barChartSvg rect
{
    fill: rgba(255, 255, 0, 1);
}

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

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

.barLegendSvg 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;
}


Beautiful!


Let's clean up a bit. There's one tiny line that's sticking out at the bottom of the scale. In this segment, just ensure that if d is 0, we don't style it using barChartLine. It's lazy, but meh, it works.
scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", function(d)
{
    return (d == 0 ? "" : "barChartLine");
})
.attr("x1", function(d)
{
    return (config.scaleWidth - 1) + "em";
})
.attr("y1", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
})
.attr("y2", function(d)
{
    return (height - (d * config.scale)) + "em";
});


Nice and clean.


Animation

Just for fun, eh?

We want the bars to "grow" from the bottom of the chart. D3 has animation functions too. Let's use them!

We are going to animate the y and height attributes. And the initial state is that the bars are flat against the bottom of the chart. Therefore, height is 0em, and y is height.
chart.selectAll("rect")
.data(dataSet.stats)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return height + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return "0em";
});


Use the transition() method and chain it with the duration() method, putting in 500 as an argument so that the animation lasts half a second.
chart.selectAll("rect")
.data(dataSet.stats)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return height + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return "0em";
})           
.transition()
.duration(500);


Now, set y and height to the same values they were before we started messing with this code!
chart.selectAll("rect")
.data(dataSet.stats)
.enter()
.append("rect")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
    return height + "em";
})
.attr("width", function(d)
{
    return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
    return "0em";
})           
.transition()
.duration(500)
.attr("y", function(d)
{
    return (height - (d * config.scale)) + "em";
})
.attr("height", function(d)
{
    return (d * config.scale) + "em";
});


Here's a look at the final product. I've set the scale property of the config object to 0.2, just so there's less scrolling.




That's all!

It really looks like using D3 to build a chart involves a lot of work. But if you've been through the vanilla JavaScript version of this web tutorial, you would have observed that a lot of the tasks are automated here, the code is easier to test, and things look a lot more consistent across browsers. These are the benefits of using a library like D3, benefits you would only realize if you have done things the hard way.

Bar humbug,
T___T

No comments:

Post a Comment