People ask me how I write my interactive tutorials. I can point at the HTML+CSS+JS but that doesn’t show the process. On this page I’ll recreate the first half of my line drawing tutorial, showing the an implementation using D3.js v4. The implementation style will be similar if you use jQuery. I also have another page showing an implementation using the Vue/React/Svelte declarative style.
In this tutorial I’m implementing diagrams using SVG instead of Canvas or WebGL. I usually choose SVG in part because DOM-manipulating libraries like D3 and Vue make HTML and SVG easier to work with.
The goal is to implement interactive diagrams like this:
The line drawing tutorial was a medium sized project for me, with multiple diagrams, multiple layers in each diagram, draggable handles, and scrubbable numbers. I’ll include pointers to the code in each step.
This is an interactive tutorial about making interactive tutorials.
You should know some Javascript to follow the tutorial. It will help if you know some SVG and HTML. If you’re interested in making your own interactive pages, I recommend trying to recreate the diagrams yourself while following the tutorial.
I usually start with a basic web page template that includes scripts, footers, and one blank SVG diagram:
I attach an id=
to an html element so that I can get to it from Javascript, using document.getElementById
or its d3 equivalent. Sometimes I’ll attach it to the <svg>
and sometimes to a <div>
outside the svg. For this page, I want interactive elements outside the svg so I’m putting the id on the <div>
.
I’ll omit the header and footer from the rest of the examples. Click the filename on the upper right to see the entire page up to that point. Using a viewBox
on <svg>
tells it the coordinate system to for drawing. We can use that to keep a consistent coordinate system even if the diagram is resized.
Sometimes I’ll add a diagram first and then add some text; other times I’ll start with the text and then figure out the diagrams. For this page I’ll start with a diagram.
The tutorial is about drawing lines on a square grid, so I need to draw a grid and also draw lines. I’ll draw a grid with Javascript:
Although d3 has a pattern for creating multiple elements with selectAll()
+ data()
+ enter()
, I don’t need that here so I’m just creating the elements directly:
I tried a few different grid sizes. I could parameterize it and calculate it properly but often times I will hard-code it when I’m just starting out, and only calculate it if I need to. Here my svg is 550px wide and I picked squares that are 22px, so 25 of them fit across. Vertically I can fit 10 squares in 220px so I changed the svg height from 200 to 220 to fit.
Those of you who know SVG might choose to use viewBox
or transform
to change the coordinate system to place points at the center of each grid square instead of at the top left, and also to scale things so that each grid square is 1 unit across instead of scale
pixels. I did this in the original article but I didn’t for this tutorial.
The main algorithm I’m trying to demonstrate on the page is drawing a line on a grid. I need to implement that algorithm and a visualization for it.
Hooray, it works!
This is just the beginning. It’s a working implementation of the algorithm and a working diagram. But it’s not interactive.
What I most often do for interaction is let the reader change the inputs to the algorithm and then I show the outputs. For line drawing, the inputs are the two endpoints, A
and B
in the code.
Great! It’s pretty easy with d3-drag. To help the reader know which elements are interactive, I set the CSS cursor:move
over the draggable circles.
This code lets me update the inputs A
and B
but it doesn’t recalculate the output line.
To be able to update the line, I need to move the drawing code into a function that I can call again, and I also need to reuse the <rect>
elements I’ve previously created. It’s useful to use d3’s enter-exit pattern here; it will let me reuse, create, or remove elements as my data changes. To use it, I need a container for the <rect>
elements; I put it in a variable gPoints
. I also need to separate the logic for the algorithm (function pointsOnLine
) from the logic for drawing (function redraw
).
Great! Now I have an interactive diagram. But this isn’t an explanation.
To explain how an algorithm works, I sometimes break it down into the steps of the execution and sometimes into the steps of the code. For a tutorial like my introduction to A*, I showed the execution. For line drawing, I want to show the steps of the code:
- Linear interpolation of numbers
- Linear interpolation of points
- Number of steps in the line
- Snap to grid
Since I’m going to have multiple diagrams, it’ll be useful to encapsulate all those global variables and functions into a diagram object.
A pattern is starting to form, but I haven’t made use of it yet. There’s a <g>
for each visual layer:
- the grid
- the line
- the draggable handles
Each of these layers has some code to draw it initially and sometimes some code to update it. As I add more layers to the diagram I’ll do something better with the draw and update code.
In this section I don’t actually have a diagram, but I do have some interaction, so I’m going to use a diagram object anyway without an svg. I want to drag a number left and right to change it, and see how it affects some calculations. Take a look at Bret Victor’s Tangle library for inspiration. You might want to use his library directly. For this page I’m using d3-drag instead.
How do I want this to work?
- I want to be able to “scrub” (drag) a number left/right.
- I want to choose the formatting (e.g.
1.00
vs1.0
vs1
). - I want to be able to run the update function when the number changes.
There are other things that you may want for scrubbable numbers but these are all I need for this tutorial. Within a named <div>
section I’ll find all the <span data-name="XYZ">
and turn them into scrubable numbers stored in field XYZ of the diagram object.
Scrubbable numbers are cool but a little bit tricky. I’m using d3-drag to tell me how far left/right the mouse was dragged. Then I scale the relative mouse position from -100 pixels to +100 pixels to the desired low–high range, using a linear scaling (see positionToValue
):
When the reader drags the number, I update the display of the number, and I also call the diagram’s update
function to update any other aspect of the diagram:
I set the CSS to cursor:col-resize
so that the reader can see it’s interactive.
This “diagram” doesn’t really fit the model that the rest of the diagrams use so it is a bit hacky. That’s ok though. Sometimes it’s easier to keep the hack than to try to build a general abstraction that’s only used once.
Using linear interpolation of numbers, I want to display the linear interpolation of points. I want a diagram that lets you modify 0 ≤ t ≤ 1, and shows the resulting point. Until now I had the final algorithm written in pointsOnLine
. To split up the diagrams I also need to split the line drawing algorithm into separate steps.
I’m starting to organize things in terms of layers, which have the creation code and the update code:
Note that this diagram does not show the line drawn on a grid. That’s another reason I want to use diagram layers for this page. While working on this diagram I commented out the code for drawing the line.
There are now two diagrams on the page. Both display the grid. The first diagram displays the grid line. The second diagram shows a non-grid line and also the interpolated point. Well, that’s what should happen, but I broke the first diagram while making the second one work. There will be more diagrams soon. I need a way to make all of them work.
When I’m writing a tutorial that requires multiple diagrams, each with different features, I like to divide the diagrams into layers, and then stack them on top of each other. There are four layers in the previous diagram: grid, track, interpolation point, and drag handle. Click “Show layers” to see them:
I’ll create each layer by a method that adds a <g>
for a layer, then adds its update function to the diagram object. Here’s the code for managing the update functions:
I no longer need to put the <g>
elements into fields in the diagram object (e.g. gGrid
, gHandles
, etc.); they can remain local variables in the add
functions. Look at addTrack()
now:
I can now assemble a diagram by calling the addXYZ()
functions:
I don’t have a generic layer system that I use across my tutorials. I make one specific for each tutorial that needs it. Each tutorial’s needs have been different. The one here is only 8 lines of code; it’s not worth writing a separate library for that.
The third diagram has yet a different visualization layer, but it will be easier to implement now that I have layers. Until now I had the line drawing algorithm choose N. I need to separate that out too.
The drawing code turns out to be similar to the previous case so I made it read either t or N:
I also need to extend my scrubbable number implementation. Previously, I wanted floating point values but here I want it to round to an integer. An easy way to implement this is to parseFloat
the formatted output; that way it works no matter how many digits I’m rounding to. See the formatter
in makeScrubbableNumber()
.
The labels are another layer:
The labels are overlapping the drag handles. I’ll fix this soon.
We already know how to round numbers to the nearest integer. To snap points to the grid we can round both the x
and y
values.
I drew this on another layer.
I also want to tell people when they’ve reached the optimal N, which is max(Δx,Δy).
This diagram has all the essential components but it could look nicer.
Although I have the essentials working, I usually spend some time making the diagrams look good and feel good.
- Special cases: the code I originally wrote fails when the line distance is zero, e.g. when the start and end points are the same. I added a special case in
interpolationPoints
forN == 0
. - Bounds checking: the reader can drag the markers outside the grid; I restricted that movement.
- Ease of styling: it’s often better to specify the styles in CSS so that I can override them for a specific diagram.
- Ease of choosing colors: it’s usually better to use HSL colors than RGB colors; they’re much easier to work with. My rule of thumb is to start with medium colors
hsl(h,50%,50%)
for any 0 ≤ h < 360, and then tweak saturation and lightness. - Relative importance: the drag handles should have a bolder look than minor elements like the numeric labels on the interpolation points.
- Grid style: I think the grid looks better when the borders aren’t drawn. It also makes it easier to see the numeric labels.
- Track style: I made the “track” line wider, lighter, and translucent so that the reader can see the grid underneath.
- Midpoints style: with a larger track, I made the interpolation points smaller to fit inside the track.
- Drawn line style: I made the line squares translucent so the reader can see the track underneath. It also lets the reader see when the same square gets drawn twice.
- Drag handle style: the handles look nicer when they fit into the track, and they also stop interfering with the labels. The problem is then the mouse sensitive area becomes too small. I solved this by having two circles. The visible circle is small and fits into the track; there’s also an invisible circle that catches the mouse events.
- Scrubbable number style: the number is way too small and it’s especially hard to get on touch devices. I made the area larger and forced it to keep a consistent size as the number of digits changes.
- Interaction feedback: I change the CSS
cursor
when the mouse is over an interactable element (drag handles and the scrubbable numbers). I also draw drop shadows. The combination makes the interactive elements respond immediately, telling the reader that they’re the right things to click on.
There are lots more ways to make the page better but these will give a lot of benefit for little effort.
Here’s what the diagrams look like with the nicer styling.
Click the “13/index.html” link to see the full page and the “source” link to see the source code. The source code for the example tutorial is public domain, so please copy/fork it and use it to make your own tutorials!
This is my first full tutorial about making tutorials. Feedback? Comment below, or comment on the trello card.