I encountered a problem at work one day: I needed to create a set of state transition diagrams for the documentation of a simple XML parser I had written. The documentation is done in HTML, so my first idea was to draw the diagrams as GIF or PNG images. Unfortunately, the only drawing tool available was Microsoft Paint – in a version that would only save files in BMP format.
Now, there are no problems – only opportunities. However, this seemed to be an insurmountable opportunity. I wanted the diagrams to contain links to other diagrams, and since the rest of the text was in a fluid layout where the user could alter the text size, it would be nice to have the same features in the diagrams. But you can't draw diagrams with HTML and CSS, right? I mean, right?
Actually, you can …
The example below is the state transition diagram for the initial state of my parser. The links don't lead anywhere, so don't bother clicking on them. Do change the text size in your browser, though, and look at the example with styles turned off if your browser lets you.
The whole diagram is wrapped in a <div> which has its
id attribute specified as diagram. (If you expect
to have more than one diagram in a page, use class instead.) The
diagram has the following style sheet rule (irrelevant declarations are
omitted for brevity):
#diagram {
position: relative;
height: 24em;
}
position:relative is very important. The children of this
<div> are absolutely positioned. Absolutely positioned
elements are positioned relative to the nearest positioned ancestor. Since we
want them to be positioned relative to the diagram itself, we have to make it
positioned. By setting position:relative without specifying an
offset, it will become positioned without being moved.
The height declaration is just to reserve the necessary
vertical space, so that any text following the diagram is pushed down. Since
all children of the diagram are positioned, it would otherwise collapse
entirely.
Now, let's look at how a state is specified:
<div class="row3 col1 size1">
<h3>HTML</h3>
<ul>
<li class="circ-right"><samp>*</samp><span> (any charater) is allowed.</span></li>
<li class="down height1"><samp><</samp><span> causes a transition to
the </span><a href="#" class="size3">OpenContentTag</a><span> state.</span></li>
</ul>
</div>
This <div>'s appearance is determined by the following
style sheet rules:
#diagram div {
position: absolute;
border: 0.05em solid #000;
}
.row3 { top: 15em }
.col1 { left: 3em }
.size1 { width: 4em }
The first rule establishes that the state boxes are absolutely positioned
(relative to the diagram, as we made sure of above), and that they have a
solid black border. Why use ems for the border width? What's
wrong with the good ol' pixel? That has to do with the infamous box model of
CSS, where any padding and border
width is added to the specified width and height of an element. Since we
will need very precise locations for our arrows later on, we'll have to take
the border width into consideration. We'll want to use ems for
the width of the boxes, to accommodate changes in text size, so we're stuck
with the same unit for the borders as well. The value 0.05em all
but ensures that the border will be one pixel wide, unless the user has a
really large font – like 30 pixels (in which case he'll probably need a
two-pixel border to see it anyway).
The three classes specify the position and size of the box. You could just
use style attributes, of course, but this provides better
separation between content and style, and also makes it easier to move boxes
around.
The first element inside the state box is:
<h3>HTML</h3>
The style sheet will center the text and increase the line height, to give some vertical space above and below the state name. (You'll have to use padding if your text can span multiple rows.)
The transitions (arrows) are specified in an unordered list, from which all
margins and padding has been removed, and whose list style is set to
none.
Each list item specifies one transition. Let's look at the first one:
<li class="circ-right">...</li>
A number of style sheet rules affect this one:
#diagram li {
position: absolute;
margin: 0;
padding: 0;
}
#diagram .circ-right {
top: 0.5em;
left: 100%;
margin-left: 0.05em;
width: 1em;
height: 1em;
border: 0.05em solid #000;
border-left: 0;
}
The first rule specifies that all list items that are descendants of the
diagram <div> are absolutely positioned, just as the state
boxes. It will also remove any margins and padding that browsers may be
tempted to add to them.
The second rule defines the particulars for the circ-right
class. It has a thin, black border on three sides. It is one em
square. The top:0.5em declaration just pushes it down a bit.
left:100% means that the "box" containing the list item will
appear just to the right of the parent element (the state box
<div>). But that's where the border is, so we add
margin-left:0.05em to clear that.
But what about the arrowhead? We'll soon come to that. Let's look at the
content of the list item. There are two inline elements inside the
<li>. The first is simple:
<samp>*</samp>
This is the "symbol" that causes the transition, and we want it to appear next to the transition itself. The style sheet defines:
#diagram .circ-right samp {
position: absolute;
left: 100%;
}
In other words: put the sample text to the right of the list item's box.
The second child of the <li> is a
<span>, but it doesn't appear anywhere in the diagram!
Style sheet:
#diagram li span {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
OK, setting the size to nothing and using overflow:hidden will
make the text disappear. Hopefully, a screen reader would still read it, and
a non-CSS browser will display it
as if the <span> tags weren't there. But what's with the
position:absolute? Well, there's another rule in the style
sheet:
#diagram .circ-right span {
top: -0.3em;
left: 0;
border-top: 0.3em solid #fff;
border-right: 0.5em solid #000;
border-bottom: 0.3em solid #fff;
}
What?! First you say that it should be invisible, then you start positioning it and drawing fat borders? And where's the left border? Are you on drugs?
Remember that arrowhead we touched on lightly a minute ago? Well, here it
is. The trick of using three thick borders, two of which in the background
color, to make triangles is nothing new. And what's an arrowhead, if not a
triangle? By carefully positioning it (relative to its parent; the
<li>), we can make it appear as an arrowhead on our
"arrow".
IE5.x/Win won't play
fair, though. It will insist on making the element box one em
high, no matter what. That will put the arrowhead in the wrong place. I've
found a reasonable fix: set the font size to zero, and use pixels for the
arrowhead borders. It won't scale, but it will draw the arrowheads.
The second item in the list looks similar:
<li class="down height1"><samp><</samp><span> causes a transition to the </span><a href="#" class="size3">OpenContentTag</a><span> state.</span></li>
The list item has another transition class, down, which
defines a straight line going down from the bottom of the state box.
#diagram .down {
top: 100%;
left: 50%;
margin-top: 0.05em;
border-left: 0.05em solid #000;
}
top:100% will push it below the state box, and
margin-top:0.05em will clear the box's border.
left:50% will put the left edge of the list item at the
horizontal center of the state box. Finally, the border-left
declaration draws the arrow line. The height1 class specified
for the list item controls the length of the line. Then the arrowhead,
defined for the <span> again:
#diagram .down span {
bottom: 0;
left: -0.3em;
border-top: 0.5em solid #000;
border-right: 0.3em solid #fff;
border-left: 0.3em solid #fff;
}
Similar to the one above, but using other borders and a slightly different positioning.
But wait a minute. There are two
<span>s in that list item! Yes, and they'll both draw an
identical arrowhead in the exact same place. If that offends your sense of
aesthetics, feel free to define a class, or find a better way of
distinguishing them. It would be so easy to do this with an adjacent selector
(#diagram .down samp+span), but a certain frequently used
browser from a major corporation doesn't understand it.
But why are there two <span>s in the first place?
That's because of the <a> in the middle. That defines a
"complex" state, which is supposedly defined in a separate state diagram. It
is displayed as a light gray box with a dotted border, thanks to the following
style sheet rule:
#diagram div li a {
display: block;
position: absolute;
border: 0.05em dotted #666;
background-color: #eee;
line-height: 2;
text-align: center;
}
Plus a few rules that specify the text color for the various pseudo-classes. The placement of those gray boxes are determined by the class of the transition (list item) that contain them. In this case:
#diagram .down a {
top: 100%;
left: 0;
}
In other words, at the bottom of the line, left-aligned. But that doesn't look good; we'd want the arrow to hit the gray box at its horizontal center. So we need another rule:
a.size3 { margin-left: -5em }
(The size3 class is defined to be 10em wide, and
is used for the size of the gray box. A negative margin equal to half the
width will accomplish what we're looking for.)
OK, but that still doesn't explain why the <a> isn't
inside the <span>. Unfortunately, it can't be. We need
overflow:hidden on the <span> to hide the
text, and that will cause the <a> to be hidden as well, no
matter what we do. So we'll have to break up the <span>
and have the <a> as an immediate child of the
<li> instead.
The rest is no different. The class of the list item
determine where they are positioned in relation to the state box, and which
borders are visible. There are a few cases that aren't covered in this demo
(classes like left, down-left, etc.),
but those are trivial. They are left as an exercise for the reader,
as it used to say in my old college text books.
If you find this interesting or useful, just look at the document's
source code and feel free to use it as you please. The
CSS is all there in the
<head>. That's just for this sample file, of course. In
reality, it would go in an external style sheet.
I think that this will degrade reasonably gracefully in a non-CSS browser. It looks fair enough without styles.
The only drawback as I see it is that the markup code is a bit bloated. (Apart from the fact that I'm using a markup language and a style sheet language to accomplish something that ought to be done with SVG, of course.) I've tried to keep the number of classes to a minimum.
The CSS isn't optimized for performance, but for readability. Hold the hate mail, please.
Oh, and of course this validates as HTML 4.01 Strict and CSS 2. (It would be just as easy to make it validate as XHTML 1.1, but I can't be bothered to write a PHP script to do the content negotiation just for this.)
I have tested this with the following browsers under Windows XP:
It works the same in all three browsers. Of course,
IE5.x (Windows) causes problems, but I
didn't have access to that one at the time of writing this. I
couldn't make it work with the relatively sized arrowheads in that one,
when I first tried this at work.
IE5.5 insisted on making the
<div> one em high, even if it was empty.
That made the arrows appear too high, and since there is no
border-color:transparent, they would obscure
anything underneath. The only solution I could come up with was to set the
font size to zero and use pixels for the arrowheads. I haven't tried it
properly with this new and improved version (just
IE6 in quirks mode), but I included
that hack just in case.
I've noticed that Mozilla may lose some borders on the state boxes if
you scroll down past the diagram and then scroll back up again (slowly). A
refresh will take care of that, though. Since I've used auto
for the horizontal margins, Mozilla will dutifully center the diagram even if
the window is too narrow for it to fit. The result is that the left-most
part of the diagram is inaccessible. That has nothing to do with the
diagram-drawing technique itself, though.
I know nothing about Mac browsers, and I haven't had a chance yet to try it under Linux. If you do, I'd appreciate any information you can give me.
Update: Michael was kind enough to send me a screenshot of
the results in Safari 1.1 on Mac OS X. It appears as if the borders and
"arrow" lines aren't displayed. My guess is that it computes
0.05em to something less than 1px and rounds
downward to zero. You could use pixels for the borders, if you don't
mind a slight overlap at one end of the "arrows".
If you'd like to comment on this, please send me an e-mail. I won't include a real mailto link, because I'm so tired of spam, but I'm sure you'll be able to figure it out anyway, especially if you're using Opera7 …
E-mail: tool-man AT home DOT se