CSS-based scatter plots
The simplest way to show the relationship of two variables in a data set, a scatter plot is also a perfect chance to talk about how CSS positioning works: how to place an element precisely within a space, with a consistent visual shape, regardless of screen size. HTML and CSS turn out to be well-suited for this task, even in comparison to SVG and canvas.
Once complete, our very basic demo chart (measuring "joy" versus "dogs", for what it's worth) should look something like this:
CSS position basics
The easiest way to think of the "position" style is that it sets an element's frame of reference for layout. By default, every element in the browser starts with the style position: static. That means that as long as it's not floated, it positions itself according to the standard document flow: it's either inline with text, or it's one of many blocks "stacked" vertically in the page.
By setting an element's position to "relative," it still initially places itself in document flow. But now, if you set one of the additional offset styles ("top", "bottom", "left", or "right"), you can shift the element visually by the given amount. This is only a visual shift—it still leaves a "hole" behind in the layout where it was actually placed. Using relative positioning this way should be fairly rare. However, it becomes more important in conjunction with the next value.
An "absolute" value for CSS position means that you're removing that element from document flow entirely. It will no longer count for layout of other elements, text won't flow around it, and it no longer has the standard block size of 100% page width. Instead, it takes its size and position cues from the first ancestor element with a non-static position.
That means that a common arrangement for data visualizations, where we want to precisely place dots or icons on a "field" within our page, is to make a container that's position: relative to serve as the new coordinate system reference (or "origin") for position: absolute elements inside. Which, if you inspect it, is exactly how our dots are placed above.
There are two other position values that you might use, albeit more rarely than these other three. A "fixed" position is much like an absolute, but its offset and dimensions are determined by the window's viewport, not another element. On the other hand, a "sticky" element lives in normal document flow until it crosses a boundary within the viewport, at which point it becomes fixed (you often see this used for sidebar menus that attach themselves to the frame so that they're always visible).
Placing our dots
Back to our scatter plot, we start with one element that will serve as our chart's background. It is positioned relative, so that our dots can be placed inside using absolute positioning. A negative margin of half the dot's height and width will "center" it on the desired coordinate. The aspect-ratio property will also let us keep our scatter plot square, no matter how wide it ends up being.
.scatter-area {
position: relative;
background: #eee;
border-bottom: 1px solid black;
border-left: 1px solid black;
max-width: 400px;
margin: auto;
aspect-ratio: 1 / 1;
}
.scatter-area .dot {
border-radius: 100%;
background: rgba(0, 0, 128, .4);
border: 1px solid darkblue;
position: absolute;
width: 20px;
height: 20px;
margin: -10px;
}
Creating our dots is just a matter of looping through our data, generating an element for each dot, and adding it to the ".scatter-area" container. The scaleX and scaleY used below should look familiar from the section on scaling functions, with one change: scaleY has to flip its axis by subtracting the scaled value from 100%. That's because the Y value of CSS starts at zero and increases as we move down, but our scatter plot puts zero at the bottom and increases as values move up.
It's useful to have our scaling functions as common infrastructure, because they're useful for drawing more than just dots. If this were a real graphic, I would want to add grid lines to it, and those are just 1px wide/tall elements positioned behind the dots, using the same scaling as the scatter plot.
var scaleX = v => v / bounds.x * 100;
var scaleY = v => 100 - (v / bounds.y * 100);
data.forEach(function(d, i) {
var dot = document.createElement("div");
dot.className = "dot";
dot.style.left = scaleX(d.joy) + "%";
dot.style.top = scaleY(d.dogs) + "%";
grid.appendChild(dot);
});
We can add tooltips to each dot by placing inserting HTML into the dot with a ".tooltip" class, but hiding it unless the dot matches the :hover pseudo-class. Because the tooltips are also positioned absolutely, they take their positioning cues from the dot that contain them, but they don't distort its size (since position: absolute removes an element from layout calculation). The pointer-events: none also stops them from being too "sticky" in browsers that support it.
.scatter-area .tooltip {
position: absolute;
background: white;
padding: 8px;
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, .2);
font-size: 15px;
width: 100px;
display: none;
top: 50%;
left: 50%;
z-index: 999;
pointer-events: none;
}
.scatter-area .dot:hover .tooltip {
display: block;
}
The code shown above is not entirely complete, and you should use your developer tools to explore some of the other niceties (such as the tooltips that "flip" from left to right when they cross the x-axis). You can also adapt this technique any time that you need to place elements within a defined space—I like to use it, along with percentage-based widths and heights, to create "annotated" images, similar to the facial tagging seen on many social networks.