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.
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>
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.
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.
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?
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.
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!
Create a barchart visualising the given fictive dataset with the number of viewers on popular streaming services.
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!
_ex_7_scatterplot.svelte
in ex_7.svelte
.
_ex_7_scatterplot.svelte
, export a property called data to
which you can provide the data in the ex_7.svelte
component.
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.
xScale(+d.value)
.