~ / teaching / InfoVis / practical works / Interactive HorizonGraph
© 2024— - Renaud Blanch
The goal of this practical work is to build an interactive visualisation that uses a complex visual mapping: horizon graphs.
You will build a visualisation that allow to explore the NOAA Global Surface Temperature Dataset (NOAAGlobalTemp). The NOAAGlobalTemp data set contains global surface temperatures in gridded (5° × 5°) and monthly resolution time series (from 1850 to present time) data files.
Get the archive that contains the dataset; the version of D3.js and topojson that you are going to use; and a visualisation template. Unzip it and get used to its content.
The data/noaa-global-temp
directory contains the datasets and a README that describes the naming scheme for the files are their structure.
The data/world-atlas
directory contains the geographical data to build a world map (last question).
The vendor
directory contains the D3.js version 7.8.5 code and the topojson version 3.0.2 code.
The viz
directory contains a visualisation template that produces HTML numbered lists of the top-10 hotest and coolest year for the region between latitudes 30N and 60N.
To test this template, you need to start a web server. The simplest way to go is using python in a terminal as shown below:
% tar xzvf anomaly.tgz x anomaly/data/ [...] x anomaly/vendor [...] x anomaly/viz/ x anomaly/viz/anomaly.html % cd anomaly/ % python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 ...
You can then open in a new tab your local version of the visualisation that should display the year lists, as the online version does.
The first step consists in building a line graph for the annual time series of a given region (e.g., 30N.60N). The sample visualisation shows how load (lines 53–55) and parse (lines 23–30) the data from the fixed width text format used. The visualisation will use a visual mapping that projects the whole tiem series onto a single mark (polyline) with the following encoding:
For the horizontal axis, a time scale is the most appropriate, and its domain can be computed from the years present in the dataset. For the vertical axis, using a fixed domain that goes from -T_MAX to +T_MAX will allow to make visual comparisons between series.
const T_MAX = 5;
To draw the line, add a SVG path
element to your plot, and use the line generator as its d
attribute to generate the drawing commands from the data that you can pass to the path element using the selection.datum
method.
Finally, add an horizontal and a vertical axis; and an horizontal line that show the 0°C baseline to complete the graph.
When this first step is completed, the visualisation should look like this (the graph size is 200×700 px plus margins to put the axis):
The second step consists in slicing the graph in ten 1°C slices.
To do so, you can combine two features of SVG: use
elements that allow to draw multiple times another element with different attributes each time (here the fill color); and clipPath
that allow to cut through an element and display only part of it (here a rectangle with 1°C height for each °C).
To achieve the result shown below, follow those steps:
let slices = d3.range(-T_MAX, T_MAX);
clipPath
with a rectangle inside; and a SVG use
that links to the path depicting the data (you will have to add an id
attribute to the path so that you can refer to it in the href
attribute of the use
element), and that is clipped by the rectangle (you will have to add an id
attribute to the clip path so that you can refer to it in the clip-path
attribute of the use
element).
fill
attribute for the use.
If you transform the clipping rectangles with an appropriate translation computed from the temperature slice it represents, you should get the following graph:
The third step consists in superimposing slices:
You should now have a first horizon graph:
The last step of the visualisation is to go from a single horizon graph representing annual data to small multiples representing monthly data. To do so, you will need to change the following things:
text2array
function should be updated to handle the second column as month (beware that javascript dates use 0-indexed numbers as month numbers) and the third as year;
aravg.mon.*
should be loaded;
d3.group
;
You can use the d3.timeFormat
function with the %b
specifier to generate the month labels for the small multiples and get a result like this one:
The first interaction consists in a vertical cursor that will allow the reading of the exact anomaly values.
For each graph, add a group (with class cursor
) that have two parts: a group (class reticle
) with a vertical line that goes from -1 to 1°C and a circle centered at 0; and a text element (class label
) on the right of that group.
To move the cursors, the movement of the mouse cursor over the graphs should be captured.
In order to do so, add a filled, but transparent, rectangle on top of the visualisation and register a mousemove
event handler for this rectangle using D3's event handling mechanism.
This update_year
handler can use the pointer
function to extract the mouse cursor position in local coordinates, and then the invert
method of the horizontal scale to convert that position to a date.
This date can then be used to set the current year by looking into the ordered list of years (using d3.least
or d3.bisect
).
This current year can then be used to: translate the cursor
groups to the right abscissa; and, after a lookup in the timeseries, to translate the reticle
groups to the right ordinate.
The circle of the reticle should match the position of the curve (see image below).
The label
texts should be updated with the anomaly value, formatted with 3 digits and a sign ('+.3f'
format specifier).
Some details can be fine-tuned as shown in the capture below to get a nice-looking result:
monospace
d font-family
for the text;
stroke
; then a second time without stroke is a way to get a nice border around the text that improves its readability on non-uniform background (using a single text
element and a use
element to reference it with different styling properties is the most efficient way to achieve that in SVG);
fill
ing depending on the sign of the anomaly reenforce the distinction between positive and negative anomalies;
[-T_MAX, 0, T_MAX]
to a gradient that interpolates from blue to white to red;
The next step consists in making possible to choose the dataset to visualize and to animate the transition between datasets.
To do so, first start with the different types of surfaces:
surfaces
variable that contains the three types of surfaces (land, ocean and both) to build a select
ion widget
(you can have a look at the source of the initial step of the Gapminder subject for an exemple of how to create an HTML drop-down menu with D3);
update_data
function that will be triggered by change
events on the drop-down menu.
This function will load the chosen dataset; select the paths of the graphs; and update the data of those paths and recompute the d
attribute from the new data.
The update mechanism is illustrated in this Observable notebook;
update_year
function to update the cursors with the new anomaly values.
Now, also add a drop-down for the different latitudes available.
You may find convenient to define an array of the existing latitudes
and the to use it for the drop-down construction, e.g.:
const latitudes = [ ['90S', '90N'], ['90S', '00N', '90N'], ['90S', '20S', '20N', '90N'], ['90S', '60S', '30S', '00N', '30N', '60N', '90N'], ['60S', '60N'], ]; var dropdown_lats = p.append('select'); dropdown_lats.selectAll('optgroup') .data(latitudes.reverse()) .join('optgroup') .attr('class', 'zone') .selectAll('.bloc') .data(d => d3.zip(d, d.slice(1))) .join('option') .attr('class', ([s, n]) => `bloc bloc-${s}-${n}`) .text(([s, n]) => `${s} — ${n}`) .attr('value', ([s, n]) => `${s}.${n}`);
Finally, register the update_data
function to be also triggered by change
events on this drop-down; add an animated transition so the horizon graphs morph slowly when the data changes; and start from zeroed data at the loading of the page.
The animation and interaction should look somewhat similar to the ones in this video capture:
The last part consists in adding a graphical interaction for the selection of latitudes.
Propose your own interaction.
Below are two exemples: on the left, simple rectangles generated from latitudes
are clickable; and on the right a more advanced visualisation with a world map is used.
vendor/topojson/
folder; and so are the world land boundaries in the data/world-atlas/
folder.
You can get started on using those with this Observable notebook and the d3-geo documentation.
Below is an example of how to load the map and display the globe:
d3.json('../data/world-atlas/land-110m.json') .then(topo => topojson.feature(topo, topo.objects.land)) .then(geo => { let margin = {top: 20, right: 10, bottom: 50, left: 120}, width = 360 - margin.left - margin.right, height = 300 - margin.top - margin.bottom; const phi = -5; let angle = 0; let projection = d3.geoOrthographic() .fitSize([width, height], {type: 'Sphere'}) .rotate([0, phi]); let path = d3.geoPath(projection); let map = body.insert('svg', '#plot') .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`) .attr('stroke', 'black') .attr('stroke-width', .5) .attr('fill', 'white'); // sphere map.selectAll('.sphere') .data([ {type: 'Sphere'}, d3.geoGraticule().stepMinor([30, 30])(), geo, ]) .join('path') .attr('class', 'sphere') .attr('d', path);
last update: jan. 10, 2025