Week 6

For this week's exercises, we will cover how to build custom visualizations using Svelte. We will show some key d3.js functionalities and explain how to go from JavaScript objects to elements on the DOM. You will use SVG as plotting back-end and experiment with scales, colors, and axes to build a static version of the famous Gapminder visualization by Hans Rosling.

Exercise 1 (1 pt.)

For this exercise, you have to edit the file src/routes/week_6/exercises/ex_1.svelte. Any changes you make to that file should show up below.

Scalable Vector Graphics (SVG) is an XML based specification for constructing graphics. It describes images as a combination of graphical primitives, like circles, rectangles, and lines. SVG is very usefull for custom visualizations, as we can just tell the browser what to show where, rather than having to deal with low-level graphics APIs. The only downside of SVG for our use is that it can get slow when you are dealing with many data points. In that case you should consider using a Canvas instead.

Below is a basic example of an SVG element for a static visualisation. It is important to understand the coordinate system of an SVG element, as that is central to plotting the data where you want it. By default, the origin of an SVG element is at the top-left: the x-axis points to the right and the y-axis points to the bottom. We often want to reserve some space for axes and legends. In the example, we defined the margin variable which specifies how much space should be reserved on each side. The margins are coloured dark-blue while the content-area is light-blue. As you can see, the content-area's origin has moved away from the SVG element's origin. In addition, its width and height have shrunk. The innerWidth and innerHeight variables compute the new size of the content-area and we used a grouping (g) tag that specifies the translation of the content-area's origin. This means that, when you are mapping data to the content-area, the x- and y-coordinates range from 0 to innerWidth and innerHeight, respectively.

<script> 
  const width = 436;
  const height = 300;
  const margin = { top: 10, right: 10, bottom: 50, left: 50 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;
</script>

<svg {width} {height}>
  <g transform='translate({margin.left}, {margin.top})'>
      ...
  </g>
</svg>
yx

The two most useful SVG elements for data visualization are the circle (<circle cx=0 cy=0 r=10 />) and rectangle (<rect x=0 y=0 width=10 height=10 />). You can style both using CSS or by specifying attributes on the element. Common styling attributes include the fill, stroke, and opacity.

Place a yellow rectangle with a width and height of 40 pixels so that it is centered in the content-area. Now, add a skyblue circle with a radius of 20 pixels on the bottom-right corner of the rectangle.

Exercise 2 (1 pt.)

For this exercise, you have to edit the file src/routes/week_6/exercises/ex_2.svelte. Any changes you make to that file should show up below.

Sometimes, your visualization should scale to the space available to it on a web-page, but you may not know how much that is ahead of time. One way to resolve this issue is to use Svelte's reactivity (which we will cover next week) to observe the size of the SVG element on the page and update your values accordingly. Another approach is using SVG's viewBox attribute. The viewBox allows you to specify the SVG's coordinate system irrespective of the actual SVG element's size. It takes 4 arguments, namely the minimum x value, the minimum y value, the width, and the height of the coordinate space you want to use: <svg viewBox='0 0 100 100'></svg>.

Change the SVG element to use a viewBox instead of specifying the width and height and update the width and height values so that the size of circle is similar to the one in Exercise 1.

Exercise 3 (1 pt.)

For this exercise, you have to edit the file src/routes/week_6/exercises/ex_3.svelte. Any changes you make to that file should show up below.

Scales map values from one dimension onto values in another dimension. Typically, we need scales to map some attribute of the data onto the x- and y-coordinates of our visualization. The possible range of values we want to map onto the coordinates is called the domain. The possible range of values we want to map those values to is called the range. So, the data provides the domain, and the coordinates of the SVG provide the range. You can also use scales to map values onto color rather than space, but that is for another exercise.

d3.js provides several very useful scale functions in the d3-scale module. They all follow the same pattern. The function you import is used to construct a scale:

import { scaleLinear } from 'd3-scale';
const scale = scaleLinear();

Once you have constructed the scale, it acts as a function that takes a value of the domain and returns a value in the range. It also has helper functions you can use to specify the minimum and maximum value of the domain and range. If you do not specify any values, both the domain and the range lie between 0 and 1, which is not useful for us!

Plot the values in the values array as circles with a radius of 10 pixels centered on the y-axis, where the value is mapped to the x-coordinate using a logarithmic scale. Hint: if it does not look like you expect, what is the logarithm of zero?

Exercise 4 (1 pt.)

For this exercise, you have to edit the file src/routes/week_6/exercises/ex_4.svelte. Any changes you make to that file should show up below.

Now lets add an axis to the previous exercise so that we can read the x-coordinates of the circles. You will need to import axisBottom from the d3-axis module and select from the d3-selection module. Similar to the scales, the functions that you import construct the axis-function. However, in this case, you need to specify which scale to use when you construct the axis. Once you have constructed an axis, it is a function that takes a handle to the DOM element and fills that DOM element with the parts that build up the axis. Typically we add a specific g tag to use as DOM element for an axis, remember to apply a translation so that the content of the element is in the bottom margin of the SVG.

Calling functions with a handle to a DOM element is different from calling normal functions, as those handles only exist when the DOM is constructed. In Svelte, you can use the <g use:myFunction></g> syntax to specify an action, i.e., a function that has to be called with the handle to an element as argument as soon as it becomes available. One final point, d3.js uses selections as handles to elements instead of raw HTML handles. So the axis-function has to be called with xAxis(select(handle)) rather than xAxis(handle)!

Copy your answer from Exercise 3 to this exercise. Then, add an x-axis to the visualization. Also add a text label indicating that it is the x-axis, use the currentcolor for the label and center the label on the x-axis.

Exercise 5 (1 pt.)

For this exercise, you have to edit the file src/routes/week_6/exercises/ex_5.svelte. Any changes you make to that file should show up below.

For this exercise, the values array is changed into an array of objects. Each object within the array contains an x- and y-coordinate, and a category for coloring. Use the d3-scale module again to plot both x- and y-values as circles to the DOM. Open the file and complete the exercise by defining linear scales for both the x- and y-coordinates of the circles. Use the innerWidth and innerHeight variables as the outer coordinates of the scales' range. Now, use a categorical scale such as scaleOrdinal() to map de categories to the color values. You can use a predefined colorscale of the d3-scale-chromatic module for this (e.g. schemeDark2). To map all different values of the 'category' key to the domain of your ordinal scale, you can use the extent() function from the d3-array module. This function operates on arrays, you can also specify an accessor function so the function knows which variable of the object it should look at. Next, plot the y-values about 10px above the circle elements and assign them the class valueLabel.

As a bonus: transform into a lollipop chart by adding svg line attributes from the bottom towards the circle elements.

Hint: Coordinates in SVG start in the upper-left corner!

Exercise 6 (1 pt.)

Create a barchart visualising the given fictive dataset with the number of viewers on popular streaming services.

  1. Import the necessary scales from the d3-scale module.
  2. Configure the x- and y-scales.
  3. Use svelte logic to loop through the data and create a rectangle for each entry.
  4. Add an axis to the left of the chart based on the scale you defined for the number of viewers.
  5. Lable the left axis
  6. Add an axis to the bottom of the chart based on the scale you defined for the streaming services.
Exercise 7 (1 pt.)

For this exercise, you have to edit the files src/routes/week_6/exercises/ex_7.svelte and src/routes/week_6/exercises/_ex_7_scatterplot.svelte. Any changes you make to those file should show up below.

Its time to combine all what you've learned so far and build a static version of the gapminder visualization by Hans Rosling. Use the data stored in static/data/gapminder.json. The data includes an array of objects storing two keys in each object; 'countries' and 'year'. For this exercise, you will need to extract the first object from the array (year 1800) and use the array stored in the 'countries' key as data to base your scatterplot visualization on.

d3.js provides several functions in the d3-fetch module that load data. All of them folow the same structure. For example, to load a hypothetical file myFile.csv located in the static folder, you would use:

csv('/myFile.csv')

Unfortunately, this function will not return the content of the file you are trying to load. Loading a file can take quite a lot of time, and your browser cannot afford to do nothing while it waits for the file to load. So, file-loading functions are asynchronous. They do the actual loading in the background and give you a promise as return value instead. You can use this promise to access the content of the file when the loading has finished.

Svelte also does not recommend loading data directly in the script of a component. Instead, data should be loaded the first time a component is actually shown on screen. Svelte provides the onMount() function for this purpose. It takes a function as argument and will run that function the first time your component is added to the DOM. There is also an onDestroy() function that you can use to clean up any dynamic resources you used.

Combining both points, the code to load a file becomes:

let data = null;
onMount(async () => {
  data = await csv('/myFile.csv');
});

Here, we initialize the data variable to null, indicating that the data is not available yet. Then, in the onMount() function we define an asynchronous callback. Within that callback, we use the await keyword to obtain the content of the file from the promise returned by csv() funtion call. Essentially, await tells JavaScript to wait until the task of a promise is done and return the value that task created. Then, we assign the content of the file to the data variable. Note that we do not know when the data will be loaded, only that it will happen. So, in the markup of our component, we should only create the components that use the data when it is available!

  1. Import _ex_7_scatterplot.svelte in ex_7.svelte.
  2. In _ex_7_scatterplot.svelte, export a property called data to which you can provide the data in the ex_7.svelte component.
  3. Load the data in ex_7.svelte, extract the countries for 1800 and pass them on to _ex_7_scatterplot.svelte. While the data is not loaded, you should show a message indicating the data is being loaded.
  4. Build the scatterplot based on the values 'income' and 'life_exp'. Hint: when your data contains missing value (as is the case with this dataset), data loading functions interpret entire columns with missing values as textual instead of numerical. That usually leads to difficult to debug behaviour as it breaks our scales. You can use a trick to force JavaScript to treat values as a number in scales: xScale(+d.value).
  5. Create an additional scale for the radius based on the 'population'.
  6. Color the circles based on the 'continent'.
  7. Add axes and labels.
  8. Add a title.
  9. Add a label for the year "1800" to the SVG.