Draw data

For data visualizations on the web D3 is the frame of reference, a massive JavaScript library which has been adopted by data practitioners and is in use as an essential dependency for more abstract frameworks and charting libraries. Among its founding features the library highlights the adoption of web standards and the ability to view source, to analyze the output through the Document Object Model; something which promotes an environment ripe for experimentation, but also something which causes a steep learning curve.

Before understanding D3 you benefit from knowledge of its key ingredients: HTML for content, CSS for style, JavaScript for interactions and SVG for graphics. Luckily, exploring these parts independently simplifies access to D3. Svelte is here a fitting extension, providing one of the most clear manners to create and manipulate DOM nodes.

Consider an array of pseudo-random decimal numbers.

const data = [2.2, 3.8, 3.6, 4.3, 4.4, 4.9, 5.1, 5.2, 6.1];

You can show the values in different ways, not in the least in a regular table sectioned in two columns to represent the numbers and the respective indexes. In HTML you do this through the table element and dedicated child nodes:

  • thead and tbody as two general containers to introduce the table and present data itself

  • tr to structure the rows

  • th and td for the real values, the headings and data points

<table>
  <thead>
    <tr>
        <th>Index</th><th>Data</th>
    </tr>
  </thead>
  <tbody><!-- ... --></tbody>
</table>

Instead of writing the rows in the tbody element directly, Svelte introduces the concept of an #each block, a utility to iterate through a collection and consider its items. The syntax is quite understandable: following the declaration of the block between curly braces and a mention of the array you gain access to the items with the as keyword. Optionally, you also find the index as a second argument. In the block itself you write whichever markup sequence you want to produce every single time — here a table row with the desired cell pair.

{#each data as value, i}
  <tr>
    <td>{i}</td>
    <td>{value}</td>
  </tr>
{/each}

It may not seem much, but this is already one type of visualization, one could even argue a data-driven document. And if not remarkable, extremely expressive and straight to the point.

IndexData
02.2
13.8
23.6
34.3
44.4
54.9
65.1
75.2
86.1

Certainly, you may look to data visualizations for visuals, perhaps a graphic to study the overall trend, the magnitude of the different values. This can be achieved with scalable vector graphics and specifically the svg element. Here the viewBox attribute helps to size the display with four values, two for the position of the origin, which you can set at zero for peace of mind, and two for the dimensions.

<svg viewBox="0 0 ? ?">
  <!-- ... -->
</svg>

How wide and how tall should the frame be? Given the simple data structure, a short list of numbers in a relatively small range, we can get by with two references: the length of the array and the maximum value. The approach is far from perfect, but lets us focus on the drawing itself.

<svg viewBox="0 0 {data.length} {Math.max(...data)}">
  <!-- ... -->
</svg>

In the body of the svg element you repeat the logic used for the table’s body, but this time to add a visual, a drawing, like a nice round shape. For this purpose SVG has a circle element and three matching attributes: cx and cy for the coordinates of the center, r for the radius. The radius can be a fixed number, enough to be noticeable. The position changes with each iteration, but the assumptions we made once again dumb down the answer. Horizontally you can use the index while vertically, and preciously, the value itself.

{#each data as value, i}
  <circle r="0.1" cx={i} cy={value} />
{/each}

Unfortunately, the code leads to one major issue and two minor annoyances, an inevitable price to pay for all the simplifications. Some of the shapes are cropped, partially visible. Most troubling, the result is the opposite of what you would think, with greater numbers actually lower and closer to the bottom. At fault is the viewBox attribute and the fundamental concept of the SVG coordinate system.

viewBox describes the visible area, a rectangle in which you will see the shapes; outside of this space the shapes are still drawn, but not shown. Since the index in the array starts at 0, the very first circle is therefore and naturally chopped. You can fix this by offsetting all shapes by half a unit, so that the dots are centered each in their imaginary column.

(0.5, 2.2)

Something similar is at work for another circle, the one which represents the maximum value. Here the shape is cut off to show only the top half, since the element is positioned exactly on the vertical edge. But why at the bottom? In SVG the coordinate system set through the viewBox means you draw from the x, y origin toward the right and the bottom. If you want to achieve something closer to a cartesian coordinate system, you have to therefore flip the picture, you need to draw from the bottom up. In practical terms, you need to draw subtracting the value of the data points from the total height.

06.14.44.41.7

At this point it might make sense to describe the width and the height of canvas next to the data array, to reference the values whenever needed.

const width = data.length;
const height = Math.max(...data);

In the viewBox attribute.

<svg viewBox="0 0 {width} {height}">
  <!-- ...circles -->
</svg>

And in the shapes you can now draw correctly, with higher values resolving in smaller vertical offset and closer to the top.

<circle r="0.1" cx={0.5 + i} cy={height - value} />

Having a separate reference for the width and the height also helps to draw the maximum value in full — just increase the height to add a bit of breathing space.

const height = Math.ceil(Math.max(...data)) + 1;

You have a first, rough visual, but more importantly, and hopefully, you know just how to plot data with something you can think of as a marker. This marker can be a circle, but virtually anything else.

You might want to have a label above the shapes; in SVG you can with a text element. You’ll have to discover a new set of attributes to position, to size the text, but once you do, perhaps for a short string introducing the chart in the top left corner.

<text x="0.2" y="0.6" font-size="0.4">
  Records
</text>

You can tweak the syntax in the each block to show the real numbers in the right place, above the circles.

<text
  x={0.5 + i}
  y={height - value - 0.3}
  font-size="0.35"
  text-anchor="middle"
>
  {value}
</text>

To delimit the graph you might lament the two axis. You can draw these with line elements, or again a path element if you are adventurous and try the d attribute.

<g stroke-width="0.06">
  <line x1="0" x2={width} y1={height} y2={height} />
  <line x1="0" x2="0" y1="0" y2={height} />
</g>

Here the stroke is not drawn entirely — the viewBox continues to cut the lines halfway, but in this particular instance you won’t mind too much. For this and all the issues popped up on the journey, for a more robust result you need more complex logic. And possibly the assistance of a utility like D3 with the firm assurance that the result is much the same. Improved, but still HTML, still SVG — you’ll figure it out.

2.23.83.64.34.44.95.15.26.1Records