Workshop: HTML, Javascript, SVG, D3
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");
.text("Hello from D3.");
message</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.
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.
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");
.selectAll("circle")
plot1.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) {
.highlight = (spot.umapX-mousePos[0])**2 + (spot.umapY-mousePos[1])**2 <= 30**2;
spot
}
.selectAll("circle")
plot1.data(spots)
.join("circle")
.attr("r", 2)
.attr("cx", d => d.umapX)
.attr("cy", d => d.umapY)
.attr("fill", d => d.highlight ? "#ff0000" : "#0000ff");
}
.on("mousemove", event => {
plot1= d3.pointer(event);
mousePos 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.
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.