Thursday, 5 March 2020

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

Every bar chart needs a scale. And the scale should fit the data it's meant for. The intervals can't be too granular, or it would be one tight squeeze. At the same time, making the intervals too large would pretty much make the scale useless.

So let's begin. Start by creating an array, scaleData. Then declare a variable, unitGrouping, initializing the value to 2. We don't make this a property of the config object because it's meant to be a temporary, throwaway variable.
filler
.style("width", function(d)
{
    return config.scaleWidth + "em";
})
.style("height", function(d)
{
    return config.legendHeight + "em";
});

var scaleData = [];
var unitGrouping = 2;

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";
});


So as mentioned earlier, the scale has to fit the data. If the maximum data value is high, of course the intervals should be bigger. Right now unitGrouping is 2. If the max property of the config object (which we calculated earlier) is more than 10, we set unitGrouping to 5. If it's more than 20, we set it to 10.
var scaleData = [];
var unitGrouping = 2;
if (config.max > 10) unitGrouping = 5;
if (config.max > 20) unitGrouping = 10;


Now, let's iterate from 0 to the config object's max property. We want to scale it up a bit to leave space at the top even after displaying the largest data value, so multiply that by 1.5. At the last part of the For loop, increment i by unitGrouping.
var scaleData = [];
var unitGrouping = 2;
if (config.max > 10) unitGrouping = 5;
if (config.max > 20) unitGrouping = 10;

for (var i = 0; i < (config.max * 1.5); i += unitGrouping)
{
}


Then in the For loop, push each value into the scaleData array. These are the values we will use to populate the scale.
var scaleData = [];
var unitGrouping = 2;
if (config.max > 10) unitGrouping = 5;
if (config.max > 20) unitGrouping = 10;

for (var i = 0; i < (config.max * 1.5); i += unitGrouping)
{
    scaleData.push(i);
}


Now, use scale. We're going to insert line tags and use the scaleData array as data. Each appended line tag should be styled using the CSS class barChartLine, which we'll create later.
var scaleData = [];
var unitGrouping = 2;
if (config.max > 10) unitGrouping = 5;
if (config.max > 20) unitGrouping = 10;

for (var i = 0; i < (config.max * 1.5); i += unitGrouping)
{
    scaleData.push(i);
}

scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartLine");


Every line tag has x1, x2, y1 and y2 properties. That's easy to figure out. My intention is for each notch to be 1em in length, and be aligned to the right side of the scale. Therefore, x1 should be the width of the scale (which is the scaleWidth property of the config object), minus 1. x2 will be just the value of scaleWidth.
scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "barChartLine")
.attr("x1", function(d)
{
    return (config.scaleWidth - 1) + "em";
})
.attr("x2", function(d)
{
    return config.scaleWidth + "em";
});


y1 and y2 will be the same, because each notch is a horizontal line. We want the notches to start from the bottom of the scale. So we take the height of the scale (which is the calculated value height) and deduct the value of d multiplied by the scale property of the config object. Since we used the scale property of the config object for the bars, we need to be consistent and use them here, too.
scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "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";
});


Let's not forget the CSS class barChartLine! Give it a 1px thickness and black outline.
.barLegendSvg text
{
    fill: rgba(0, 0, 0, 1);
    text-anchor: middle;
    font-weight: bold;


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


Good, good. Now it just needs some numbers.


Here, we use scale again to insert text tags. Again, we use scaleData as the dataset. The content is straightforward. For the most part, we use the value of d as is, unless it's 0. We don't want to display the 0 on the scale because that would just be silly.
scale.selectAll("line")
.data(scaleData)
.enter()
.append("line")
.attr("class", "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";
});

scale.selectAll("text")
.data(scaleData)
.enter()
.append("text")
.text(function(d)
{
    return (d == 0 ? "" : d);
});


Now for the x and y values of the text tags. x will be even further left of the line tags, so deduct 3 from the scaleWidth property of the config object instead of just 1. For y, do what we did for the y1 and y2 properties of the line tags, but adjust slightly, by deducting 0.25em from the final value so that the notches will be aligned in the center of those numbers.
scale.selectAll("text")
.data(scaleData)
.enter()
.append("text")
.attr("x", function(d)
{
    return (config.scaleWidth - 3) + "em";
})
.attr("y", function(d)
{
    return (height - (d * config.scale - 0.25)) + "em";
})
.text(function(d)
{
    return (d == 0 ? "" : d);
});




Now try viewing appearances instead of goals, since the numbers are bigger. Do the intervals change? Yes, since the maximum number is now 49, the scale changes to increments of 10 instead of 5!


The scale text is a little large, however. Let's try to style it. We'll make it black and then make it half the current size.
.barScaleSvg
{
    width: 5em;
    height: 20em;
    float: left;
    background-color: rgba(0, 255, 0, 0.1);
}

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

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


Refresh the page. You'll see that the text has shifted to the top left. That's because measurements of the layout and positioning are all in em, and that means reducing the font size will require a proportionate increase in the positioning values.


Do this. Adjust the x attribute. Instead of deducting 1em off the end, add 1em instead. As for the y attribute, every component in that equation has to be doubled.
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);
});


There you go. One last thing though.


This will align the numbers correctly.
.barScaleSvg text
{
    fill: rgba(0, 0, 0, 1);
    text-anchor: end;
    font-size: 0.5em;
}


Good job!


Adding a mean line

Now that we have a scale, it stands to reason that a line through the chart showing the average value would add to it greatly! We calculated the mean value earlier, and it's now the mean property of the config object. So now, in chart, just call the append() method. No need for all that other stuff because we're only appending one line and not going through an array of values. Give it the CSS class barChartDataMean.
chart.selectAll("text")
.data(dataSet.stats)
.enter()
.append("text")
.attr("x", function(d, i)
{
    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
    return (height - (d * config.scale) - 1) + "em";
})
.text(function(d)
{
    return d;
});

chart
.append("line")
.attr("class", "barChartDataMean");


For the x1 and x2 values, that's simple. It's from the left side of the chart to the right side, so naturally x1 is 0em and x2 is width.
chart
.append("line")
.attr("class", "barChartDataMean")
.attr("x1", function(d)
{
    return "0em";
})
.attr("x2", function(d)
{
    return width + "em";
});


And since it's a horizontal line, y1 and y2 will be the same. As in the bars, we'll use the value of height and deduct the value of the mean (multiplied by scale) properties of the config object.
chart
.append("line")
.attr("class", "barChartDataMean")
.attr("x1", function(d)
{
    return "0em";
})
.attr("y1", function(d)
{
    return (height - (config.mean * config.scale)) + "em";
})
.attr("x2", function(d)
{
    return width + "em";
})
.attr("y2", function(d)
{
    return (height - (config.mean * config.scale)) + "em";
});


Let's not forget to create the CSS class barChartDataMean. It's a 1 pixel dotted black line.
.barLegendSvg text
{
    fill: rgba(0, 0, 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(0, 0, 0, 1);
    stroke-width: 1px;
}


Do you see it? Try changing the values of the drop-down lists. The line should move with the data.


Next

The chart looks right, but we also want it to look nice. We will change the color scheme, mess about with the layout and add some D3 animations.

No comments:

Post a Comment