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