Workshop: HTML, Javascript, SVG, D3

Home page

HTML

The next sections build up to the file example.html in the workshop files.

Open a text editor, create a file called, hello.html and type the following:

<!doctype html>

<title>My page</title>

<p>Hello.</p>

In your web browser, go to File / Open file... in the menus and open the file. Or you can enter the URL file:///, and navigate to your file.

Windows users may need to type in file:/// and then the full path to the file in the URL bar, using forward slashes, something like file:///c:/myfiles/jsfoot/hello.html.

What’s all this?

  • <!doctype html> tells the browser that this page is written in HTML5, the latest standard, and that it doesn’t need to emulate any quirks of older browsers.

  • <title>...</title> sets the title of the page, visible in your tab bar.

  • <p>...</p> denotes a paragraph of text. There are many different tags like this to tell the browser the meaning of text in the page (paragraph, heading, list, table, division, etc etc), and so how to display it.

Developer tools in your browser

In your browser, find the web developer tools.

Chrome:

  • [ ⋮ ] / More Tools / Developer Tools

Firefox:

  • [ ☰ ] / More Tools / Web Developer Tools

Safari:

  • Go to Safari / Settings / Advanced and check [✓] Show features for web developers.
  • Go to Developer / Show Web Inspector.

Notice the document consists of text within tags within tags. It’s a “tree”.

We left out some standard tags, which is allowed in HTML5 and the browser has filled them in:

<!DOCTYPE html>
<html> 
    <head> ... </head> 
    <body> ... </body> 
</html>

SVG

Add the following text to the end of your page:

<svg width="600" height="600">
    <circle cx="50" cy="50" r="10" />
</svg>

We’ve embedded an SVG picture in our page. We’re also starting to use tags with attributes such as width and cx.

The <circle> tag doesn’t actually contain anything further, so it’s written <circle ... />. (HTML is somewhat inconsistent about where this is used and where it isn’t.)

JavaScript

Add the following to the end of your HTML file:

<script>
"use strict";

console.log("Hello from JavaScript!");
</script>

The magic line "use strict"; enables some extra error checking, you should always include it.

The console.log("...") displays text in the console of the web developer tools.

Your HTML file should now look like example.html in the workshop files, and look like this in your browser.

D3

This section builds up to the file exampleD3.html in the workshop files.

Modify text

To easily find things in our document to modify, we can give them an id attribute.

Create a new HTML page with these tags:

<!doctype html>

<title>My D3 page</title>

<p id="mymessage">Hello.</p>

<button id="mybutton">Button</button>

Now we add some Javascript. We’ll be using the D3 package. Make sure the file d3.js is in the same directory as your HTML page.

D3 will give us a way to alter our document. (It’s also not hard to do this without D3.)

<script src="d3.js"></script>

<script>
"use strict";

let message = d3.select("#mymessage");
let button = d3.select("#mybutton");

message.text("Hello from D3.");
</script>

Here:

  • We use the D3 function select to find elements in the document.
  • #mymessage is a CSS selector. The # means select the element with the given ID.
  • We store these elements into some variables. In Javascript variables are declared using let. (Or several other ways! let is a good default though.)
  • D3 has wrapped each element in an object that makes it easy to alter it. We use the text method to modify the message element.
Tip

We could also load D3 directly from a CDN with:

<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.js"></script>

However this adds an external dependency to our page. For our page to keep working in the long term, jsdelivr.net would also need to keep existing.

Respond to a button click

Now add the following Javascript:

button.on("click", event => {
    message.text("Button pressed.");
});

Ok, there’s a bit going on here:

  • event => { ... } is a concise way to create a function. This function takes one argument, event.
  • We pass the function to a method called on. Passing a function as an argument will be a common pattern with D3.
  • When the button emits a "click" event, the function will be called. The function is passed an event object (which we ignore here).

Our web page is now interactive!

Your HTML file should now look like exampleD3.html in the workshop files, and look like this in your browser.

D3 with data

The remainder of the workshop builds up to the file examplePlots.html in the workshop files.

Load data

Create a new HTML page with the usual stuff, as before.

First let’s load the data and D3. Your HTML will have to be in the same directory as the files spots.js and d3.js.

<script src="spots.js"></script>

<script src="d3.js"></script>

Now in the console you can type spots to examine the dataset.

Scatter-plot

Now let’s display the data as a scatter-plot.

<svg width="600" height="600" id="plot1"> </svg>

<script>
"use strict";

let plot1 = d3.select("#plot1");

plot1.selectAll("circle")
    .data(spots)
    .join("circle")
    .attr("r", 2)
    .attr("cx", d => d.umapX)
    .attr("cy", d => d.umapY);
</script>

Ok, there’s a fair bit going on here:

  • D3 has been written so you can chain together method calls in a pipeline.

  • .selectAll("circle") selects any existing <circle /> elements within #plot1. There aren’t any yet, but this will be important later! This also means we are dealing with a selection of elements within #plot1 and not #plot1 itself.

  • .data(spots) associates a dataset with the selection. For more advanced/dynamics plots, it might be necessary to specify a key at this step too.

  • .join("circle") is where the magic happens. The selection of elements is made to match the dataset, creating and deleting elements as necessary. The "circle" here says to create <circle /> elements if new elements are needed. In more dynamics plots, the details of this can be customized, for example animating the creation and deletion of elements.

  • Now there is a <circle /> element associated with item in our dataset!

  • .attr("r", 2) gives all elements an "r" (radius) of 2.

  • .attr("cx", d => d.umapX) similarly updates "cx" but now the value is computed from the associated data using the function we have provided. Similarly for "cy".

Mouse interaction

Earlier we responded to a "click" event on a button. Here we will respond to a "mousemove" event in our SVG picture.

let plot1 = d3.select("#plot1");

let mousePos = [150,150];

function update() {
    for(let spot of spots) {
        spot.highlight = (spot.umapX-mousePos[0])**2 + (spot.umapY-mousePos[1])**2 <= 30**2; 
    }

    plot1.selectAll("circle")
        .data(spots)
        .join("circle")
        .attr("r", 2)
        .attr("cx", d => d.umapX)
        .attr("cy", d => d.umapY)
        .attr("fill", d => d.highlight ? "#ff0000" : "#0000ff");
}

plot1.on("mousemove", event => {
    mousePos = d3.pointer(event);
    update();
})

update();

Ok, a lot to unpack here. Also this is Paul’s opinionated version of how to write this code!

  • We have some state now, in the form of mousePos, which should affect what is displayed.
  • We meet another common syntax for defining functions, function update() { ... }.
  • Our update() function brings the web page into sync with what we would like to display. update() is “idempotent”, and we feel free to call it whenever something changes.
  • When the mouse moves, we store the new mouse position and then update().
  • Within the function, we decide which spots to highlight, using a for loop, and then use this to update the fill color of each circle.
  • Finally, we call update() to set up the display initially.

This pattern of code makes it easy to keep your display correctly up to date with the internal state of your program. What is displayed is a function of the internal state. D3’s join feature helps with this synchronization. It handles the details of adding and removing DOM elements to match your current data.

As your visualization becomes more complex, and if performance becomes a problem, you might start building some shortcuts into update() to only update things as needed. For very complex projects, you could use a package such as React. It is also a principle in React that what the user sees is a function of the internal state.

Exercise: Add a second plot

The example developed above can be found in exercise.html. We’re going to add to it.

We have a microscope image of the tissue that these spots are located in. Add a second plot with this image.

<svg width="600" height="600" id="plot2">
    <image href="tissue.png" width="600" height="599" />
</svg>

Update the JavaScript code to show points in this second plot, now using the imageX and imageY attributes of each spot. These spots should also be highlighted when you mouse over the original scatter-plot.

Your HTML file should now look like examplePlots.html in the workshop files, and look like this in your browser.

Share with the world

Create an account on GitHub.

Create a new repository.

Upload necessary files. You can use git from the command line, or upload files using the website by pressing the “+” button.

  • Your HTML file.
  • The other files it needs: spots.js, d3.js, tissue.png.

Go to “Settings”, then “Pages”, and under “Branch” choose the “main” branch in the drop-down.

After a minute or two, your page will be accessible to anyone at the url:

https://myUsername.github.io/myRepo/myFile.html

substituting myUsername, myRepo and myFile as appropriate.

Further possible refinements

  • The highlight could be made to work for both scatter-plots.
  • A fully fleshed out interaction scheme may need to listen to multiple types of events. For example the highlight should disappear when it leaves a scatter-plot by responding to "mouseleave" events.
  • Usually there is a scale that maps data to screen position. d3.scale can help with this.
  • A plot usually has a visual indication of the scale. d3.axis can provide this.