Before moving on to more advanced data bindings, we'll feature
one last example that introduces some additional elements of SVG
used in most visualizations. We'll start with a description of the
problem, the complete solution, and follow it with a explanation
of some of the new bits. So, let's graph!
But what? Histograms. Simple, and easy. And practical, too. Pretend
(or not, as necessary) that you play a
tabletop roleplaying game
as some character that hits things with other things. You just got
a sweet new things to hit other things with - an
eldritchian
quadruple axe (it's pretty cool, trust me). When you hit
things with this thing, you roll
2d4 + 2d6
for damage, assuming the enemy fails their sanity check, and half
rounding down otherwise. Now, you received
DJs boombox of "Call
Me Maybe" in your last quest (for those not familiar with the
game, that is a cursed amulet that makes everyone within 50m fail
their sanity checks) so the math here is going to be easy. Which is
good, because some members of your party just stare at you blindly
when you say
probability mass function and you just want
to demonstrate how awesome this new axe is. You need a pretty
picture to illustrate.
A picture of a histogram, of course.
We'll simulate 500 attacks with your axes and graph the
resulting histogram. And we'll pick, arbitrarily, 640x480 pixels
as the size. With a title and nicely-labeled axes for our axes.
<style type='text/css'>
svg {
border: 1px solid black;
background: white;
}
.axis .domain, .axis .tick {
stroke: #000;
fill: none;
}
.title {
fill: #666;
font-family: Helvetica, sans-serif; /* Helvetica is cool, right? */
text-anchor: middle;
font-size: 24px;
}
.bar {
fill: #fcc;
stroke: #444;
}
</style>
<div id='chart'></div>
var width = 640;
var height = 480;
var root = d3.select('#chart').append('svg')
.attr({
'width': width,
'height': height,
});
// Render the title.
var titleHeight = 50;
root.append('text')
.attr({
'class': 'title',
'x': width / 2,
'y': titleHeight / 2,
})
.text('Skull-splitting power!');
// Simulate 500 rolls of the axe.
var rollDie = function(numSides) {
return 1 + Math.floor(Math.random() * numSides);
};
var MAX_ROLL = 4 + 4 + 6 + 6;
var rollHisto = d3.range(MAX_ROLL + 1).map(function() { return 0; });
for (var i = 0; i < 500; i++) {
var rolled = rollDie(4) + rollDie(4) + rollDie(6) + rollDie(6);
rollHisto[rolled]++;
}
// Render our axis.
var yAxisWidth = 50;
var xAxisHeight = 50;
var xScale = d3.scale.linear()
.domain([0, rollHisto.length])
.range([yAxisWidth, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(rollHisto) * 1.2])
.range([height - xAxisHeight, titleHeight]);
var xAxis = d3.svg.axis().scale(xScale);
root.append('g')
.attr({
'class': 'x axis',
'transform': 'translate(0,' + (height - xAxisHeight) + ')',
})
.call(xAxis);
var yAxis = d3.svg.axis().scale(yScale).orient('left');
root.append('g')
.attr({
'class': 'y axis',
'transform': 'translate(' + yAxisWidth + ',0)',
})
.call(yAxis);
// Render the dice bars.
root.selectAll('rect.bar')
.data(rollHisto).enter()
.append('rect')
.attr({
'class': 'bar',
'x': function(d, i) { return xScale(i - 0.5); },
'width': xScale(1) - xScale(0),
'y': yScale,
'height': function(d) { return yScale(0) - yScale(d); },
});
Phew! That was quite a bit of code, and much of it new.
First order of business: this example used CSS. Yes, CSS. You see,
I know I mentioned this in the last lesson as an advanced topic.
Congratulations, your SVG is now advanced! Gold star! Styling your
charts the same way you would style your documents is done for the
same reason - keeping presentation separate from content is good.
It is a little more complicated with CSS as some things you think
of as presentation in CSS are, conceptually, part of the content in
SVG. Namely, the position and dimension of an element.
Some readers may have spotted another difference: some of those CSS
properties aren't valid. True,
text-anchor
isn't a
style property for HTML elements, but it works just fine for SVG.
The complete list of all valid properties to put in CSS is available
in the spec.
In addition to
text-anchor
, there is another
commonly used attribute:
dy
.
The
dy
of a
text
element is the amount,
in any units, the text should be offset from its defined
y
position. How is this useful? Vertically aligning text. There are
some "magic" values one can use with
dy
to get a variety
of effects. A
dy
of
0.3em
vertically
centers the text along the defined
y
position. A
dy
of
0.7em
aligns the top of the text
at the defined
y
position. And, obviously, a
dy
of
1em
moves the text down a
line from the defined
y
attribute. These numbers
aren't actually magic, but follow from the
definition of the em unit.
The next new bit is the
text
element used for the title. Just to confuse you,
text
in SVG behaves nothing like in HTML, nor like
rect
s
in SVG. Ignoring any centering or offsets, the
x
and
y
attributes determine the left and
baseline,
not top or mid, positions of the initial character. So far, so good.
The big annoyance: it
does not wrap.
It will overflow as necessary and you can't
easily prevent
this. It sucks. For serious text layout in SVG, I suggest two good
options. If you know your data, lay your text out by hand, assuming
everything will fit (which it will, because you can modify as
needed). If you don't know your data, I suggest using HTML - either
with
absolute
positioned nodes or with a
foreignObject
(to embed HTML within SVG). Neither of the HTML options are great,
mind you, but they do work.
Another option for laying out unknown text is to render, offscreen,
the text you wish to layout and use some of the
DOM interface methods
to query the size of the text, such as
getComputedTextLength
.
Alternatively, you can use the
getBBox
method (on the
SVGLocatable
DOM interface) to get the height as well.
Lets talk about the presentation a bit: we have a title that takes
up the top of the chart, and x-axis that takes up the bottom, and a
y-axis on the left. Each bar is
centered on the dice roll
it represents, and our data on how many times the dice summed to
n
is in
rollHisto[n]
.
The area for drawing bars in then spans from the end of the y-axis
to the edge of the chart.
You may have noticed a few variables scattered about the code - in
addition to the
width
and
height
of
previous examples, we additionally have
xAxisHeight
,
yAxisWidth
, and
titleHeight
. Knowing this,
our x scale should be obvious; the value
0
maps to
the edge of the y-axis, and the value
rollHisto.length - 1
(the highest data value to graph) should be close to, but not at,
the edge of the chart. Hence:
var xScale = d3.scale.linear()
.domain([0, rollHisto.length])
.range([yAxisWidth, width]);
Which, I might add, is a lot more obvious than the constant-less
version with a
range
of
[480, 50]
.
The y scale is similar, with the added complexity of it being
inverted; a value of
0
maps to the highest y-value,
namely
height - xAxisHeight
.
Going back to our
"not-so-rubbish" circle example,
we could improve it yet again. Instead of defining the
xScale
and
yScale
as having domains of
[0, 1]
,
we can define them as having a more
natural domain.
Like
[0, d3.max(circleRs)]
for the
xScale
and
[0, dataToY(d3.max(circleRs))]
for the
yScale
. The ranges,
then, would be
[0, width]
and
[0, height]
. Our scale
is no longer
perfect, but we could change the definition
of
width
and
height
to get the original
definition. This isn't as simple as I've led on, hence ignoring
it. But, generally, domains and ranges should be natural
from the domain of data you wish to render to the range of pixels
you have to render in.
The scales we have also let us do some nice tricks. The center
of each bar is at
xScale(i)
, as our scale is defined.
This means the left edge should be halfway between
i - 1
and
i
- which we can put directly into the definition as
xScale(i - 0.5)
. The
width
of each bar, then, should be the distance between
i - 0.5
and
i + 0.5
.
As our scale is linear, each bar has the same width and we simplify
this to
xScale(1) - xScale(0)
. As before, so again, the
y-scale is defined in a similar fashion, using the
yScale
to determine the
y
and
height
.
The next wrinkle is our axes - the most confusing, and new, piece
of code. The easiest part to explain is the wonderful
g
element. We use this element mostly for two purposes: it can have
children elements, and it allows one to apply a
transform
to those children. This element forms the bread-and-butter of
many interesting graphs, paralleling the uses of a
div
element in HTML. It allows elements to be grouped together, both
in the structure of the document and in the presentation.
We commonly use the
transform
attribute just mentioned for its
translate
form - to
offset a set of elements from their normal position by a set amount
in the
x
and
y
directions. The other forms,
which allow a full range of affine transformations, won't be covered
here.
So how does that help us? Well, the
d3.svg.axis
helper
isn't the smartest, or most configurable, kid in the shed. This is
by design (I think).
d3.svg.axis
is a function that
takes a
selection
and appends elements to it that
resemble a labeled axis. By default,
drawing a line from the start to the end of some scale along the x-axis,
positioned with a
y
of
0
, with ticks and labels
below. The simplistic interface doesn't allow us to specify where
this is positioned. If you browse the
documentation
you won't find any such method. So,
g
to the rescue!
Translating all the elements rendered for the axis down by
height - xAxisHeight
gets the desired position.
You'll also notice the docs don't mention anything about the
presentation of the axis elements, so I'll clue you in. There is
a
path
element that runs parallel to the axis with
a class of
domain
and many
line
elements
forming the perpendicular tick marks with, conveniently, a class
of
tick
. The
text
labels have no class. Given all of this information, we can easily
use CSS to change the display, even hiding bits if we don't like them
with a
stroke
and
fill
of none.
To determine which way the axis runs, and which side the labels
get placed on, you use the
orient
method. And, to define the scale used, the
scale
method is used.
The rendered axis goes from the start of the scale to the end, so
if you had used a not-so-obvious scale definition, it would not
work out so nicely. Like in our previous example of a y-scale
with a domain of [0, 1]
.
And that is about it. Except that pesky
call
method
we called with the
d3.svg.axis
object. This is a
helper method;
selection.call(fn)
is equivalent to
fn(selection); return selection;
- enabling method
chaining.