Hands-on data visualization using svelte
This is a svelte (http://svelte.dev) version of the Processing/p5/vega tutorials that we published earlier. Svelte is a framework for creating web content, and very accessible for creating data visualisations. This tutorial holds numerous code snippets that can by copy/pasted and modified for your own purpose. The contents of this tutorial is available under the CC-BY license.
Table of contents
- Table of contents
- What is Svelte?
- Scalable Vector Graphics
- Styling
- Looping over datapoints
- Running svelte locally
- Loading data
- Recreating the flights visualisation
- Quick exercise: lines
- What we did not go into
- Animations
- Custom visuals
- Overview first, search and filter, and details on demand
- Deploying your visualisations
What is Svelte?
Svelte is a framework for building webpages. It makes it easier to build complexer websites, but is also a very good fit for data visualisation using SVG.
One of the strong points of Svelte is that it makes creating different components very simple, although we will not use these initially. Nevertheless, just to give you an idea, below is a simple example. The #each
and curly brackets will be new to you, but you should get the idea. In the top version of this code we create 2 different scatterplots where we create an svg
and write the code to loop over the datapoints two times (once for each dataset). Using Svelte, we can however easily create a Scatterplot
HTML element which simplifies the main code a lot.
The svelte website has a very good tutorial, available at https://svelte.dev/tutorial. We really recommend going through at least sections 1 through 7 (Introduction, Reactivity, Props, Logic, Events, Bindings, and Lifecycle). A lot of what follows below is based on that material, but with an emphasis on data visualisation.
HTML, CSS and javascript
In order to follow this tutorial, you have to understand that:
- HTML: provides the basic structure of the page, which can be enhanced using CSS and javascript. All HTML elements on a webpage together are called the Document Object Model or DOM.
- CSS: is used to control presentation, formatting and layout: what it looks like. The CSS is denoted by the
<style></style>
tags. - javascript: is used to control the behaviour of different elements and the page as a whole. The javascript section in a svelte file is surrounded by
<script></script>
tags.
You should have at least a little knowledge of what HTML is and how it works for the rest of this tutorial.
A Svelte file can contain the HTML, CSS and javascript together. Here is a very simple App.svelte
that includes all three of them:
In this example, we get a heading of level 1 (h1
) that is green (as defined in the <style>
element), and says “Hello World!” where the “World” is defined in the javascript section.
Using the Svelte REPL
Before we start creating files on our own machine and run a local server, we will look at the online svelte REPL (Read-Eval-Print-Loop), available at http://svelte.dev/repl.
On the REPL, you have the code at the left and the result at the right. At the image above there is only one file that you have to change at the moment, called App.svelte
.
The first time you open the REPL you will get the following code:
When you open that REPL link for the first time, you’ll be greeted with the following code:
You can clearly see the 3 different sections that can make up a svelte file:
- script: denoted by
<script></script>
tags - CSS: denoted by
<style></style>
tags - html: the rest (in this case consisting of an
h1
and ansvg
element)
The image above shows how these 3 parts work together. Let’s start with the HTML and see where the script
and style
parts come into play.
So let’s start creating data visualisations.
Scalable Vector Graphics
Scalable Vector Graphics (or SVG for short) are a way to create data visualisation in HTML pages. HTML has default elements such as h1
(header of level 1), ul
(unordered list), p
(paragraph), etc. Within an svg
element you can use elements such as circle
, rect
(rectangle) and so on. For a full list of elements you can use within svg
, see here.
Remove all code in the App.svelte
on the REPL, and replace it with the following:
You should now see a picture with 2 black circles and a rectangle.
Styling
Just black circles and rectangles aren’t that nice. Let’s give them some colour. We can do that using CSS. There are different places where you can define the CSS (inline, in the header, in a separate file), but in svelte this is normally done in a separate <style></style>
section. Let’s add this style
section to the script that you already have:
We now have a slightly nicer picture with blue circles and a green rectangle (with a red border). We’ve also given the SVG element itself a very light background so that we can see how big it is.
This is the image we now have:
CSS Selectors
All the HTML elements we’ve used can have attributes, such as cx
and cy
for circle
elements, and width
for svg
elements. Each element has a specific list of possible attributes (check their documentation), but you can also add your custom attributes if you want. There are however two special ones: id
and class
.
id
: Theid
attributes gives the element a specific - you guessed it - ID. That can be used in yourscript
andstyle
sections to identify a specific element.class
: Theclass
attribute assigns that element to a certain class (or group). We can use this to easily select different elements together. We can for example have a todo list where some of the tasks have been completed.
In the style
sections that we showed above, we used the element type (i.e. circle
, rect
and svg
) to identify which elements a certain style should be applied to. We can also use the id and class, though.
- To refer to an element with a specific ID, prepend that ID with a hash tag in the CSS.
- To refer to all elements of a certain class, prepend that class with a period in the CSS.
For example:
will result in:
As you can see, all items that are completed are greyed out.
You can also combine CSS selectors. For example, to target li
elements that have the class completed
, but not any other element with that same class, we can use the selector li.completed
.
CSS selectors are a very powerful way to select elements in an HTML page. They for example also one of the cornerstones when you want to create data visualisations in D3. For a full list, see here.
Looping over datapoints
Say we have a dataset of 10 datapoints, and we want to plot them all (like a scatterplot). One way of doing this would be:
It doesn’t make sense to hard-code the different circles in the HTML itself. It’d be better to define them as an array of data and loop over them.
In the script
section, let’s define a variable data
:
The svg
part can now be populated programmatically like this:
Conditional logic
Here we see for the first time how we can add logic (like conditionals and loops) in HTML in svelte. Normally, HTML does not allow this but svelte does provide that functionality. To add this logic in the HTML part of a svelte file, we put these commands between curly brackets {
and }
. Whereas in regular javascript an if
conditional looks like this:
In svelte this looks like:
Loop logic
Loops are similar, using the each
command. In regular javascript:
In svelte this looks like:
Again: see the svelte tutorial for another explanation of this, including else if
, etc.
IMPORTANT: Note that this syntax is for the HTML part of a svelte file, not for the script part which is regular javascript.
Back to the data visualisation we are building. We now get the same picture again, but didn’t have to specify the separate datapoints within the HTML anymore.
Our full script now looks like this:
For argument’s sake, let’s add an if
statement as well: we’ll add a value to all these points, and let the visual encoding (circle or rectangle) be dependent on that value.
In this example, we embedded an if
statement within the each
loop. This is the picture that we get:
Running svelte locally
Although extremely useful, you will at some point outgrow the REPL that we’ve used up to now. Still, you might go back to it regularly to quickly test something out.
We can also develop svelte applications (i.c. visualisations) locally, on our own machine. See the Getting Started for new developers page on how to get set up.
Although it is out of scope for this tutorial, it comes down to running npx degit sveltejs/template my-new-project
, and running npm install
in the new my-new-project
folder. See here for instructions on how to install npm
itself. When that’s finished you can run npm run dev
which will make the application available on http://localhost:5000 and automatically reload that page when you make changes to any of the files.
Loading data
Data visualisation relies on data. We can load data in several ways in javascript.
In the script
section
We’ve already seen above how to define datapoints within the script
section of a svelte file, so we’ll skip that here.
In a separate .js
file
You can also create a new file, e.g. called data.js
which has the following form:
See this screenshot of the REPL where there is both an App.svelte
and the newly-created data.js
. To access that data from the svelte file, use
A full example would be:
You can also give the data a name in the data.js
file. Either:
- use
import datapoints from './data'
inApp.svelte
andexport default [...]
indata.js
, or - use
import { flights } from './data'
inApp.svelte
andexport const flights = [...]
indata.js
Notice that in the former case we make the data available through the variable datapoints
(defined in App.svelte
), versus the variable flights
(defined in data.js
).
From an online CSV file
To be able to load external data in CSV format, we have to install the PapaParse npm package. To do so:
- Stop the
npm run dev
server. - Run
npm install papaparse
in the root folder of your svelte application. - Restart
npm run dev
.
Here’s a working example using data about Belgian beers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import Papa from 'papaparse';
let datapoints = [];
Papa.parse("http://vda-lab.github.io/assets/beers.csv", {
header: true,
download: true,
complete: function(results) {
datapoints = results.data
}
})
</script>
<ul>
{#each datapoints.slice(1,20) as datapoint}
<li>{datapoint.Merk} ({datapoint.Soort})</li>
{/each}
</ul>
Let’s walk over this code:
- line 2: import the
PapaParse
package - line 4: define a default value (i.e. empty array) for the
datapoints
variable - lines 5-11: this is where we load data into that variable. See the PapaParse documentation documentation about the configuration options you can use. In this case, we take the
results
when parsing iscomplete
and copy it into thedatapoints
variable in line 9. Try adding aconsole.log(results)
just before line 9 to see what theresults
object looks like in the console.
From an online JSON file
If the remote file you’re trying to read is in JSON format, we don’t need the PapaParse package but can use the Fetch API instead. Here’s the code to load a remote JSON file:
In a local csv or json file
The above CSV and JSON files are on a remote server. But what if we have the data on our own machine? Actually, this is very simple as we are running our own server. If you put the data file in the public
directory of your svelte project, you can access it directly, e.g. with
Papa.parse('http://localhost:5000/beers.csv', { ... })
, orfetch('http://localhost:5000/beers.json')
You can even just leave the URL itself:
Papa.parse('./beers.csv', { ... })
, orfetch('./beers.json')
Recreating the flights visualisation
We now have the very basic components to use svelte for data visualisation. Let’s use that to recreate the flights visualisation that we use in the other tutorials. This is what we’ll be building:
In this visualisation, we see departure airports worldwide. The size of the dot is bigger is the flights are longer distance. Blue dots denote domestic flights; red dots are international flights. With the slider at the top we can filter on flight distance: with the slider at the left we only show short-distance flights; with the slider on the right we only show long-distance flights.
Loading the data
The data for this visualisation is available at http://vda-lab.github.io/assets/svelte-flights.json
. However, there are 57,852 records in that file which is too much to visualise using SVG. We’d be generating that many circle
elements in the DOM. That’s why we’ll use only the first 3,000 records in that file.
We can check we’re loading the file correctly by just creating a list of the departure airport names:
This should give you the list:
- Balandino to Kazan
- Balandino to Tolmachevo
- Domododevo to Balandino
- Domododevo to Khrabrovo
- Domododevo to Kazan
- …
First attempt at plotting
The dataset also contains the latitude and longitude of the departure airports, in the variables from_lat
and from_long
. If we plot these out as a scatterplot we should get the map of the world. We replace the ul
in the example above with an svg
and add a circle for each datapoint. We’ll also add some style to make sure that we know where the boundaries of the SVG are, and if there is an overlap between datapoints.
This is the result:
…which is completely different from what we expected.
Actually this is logical: the values for longitude in the file range from -180 to +180, and those for latitude from -90 to +90. On the other hand, the range of pixels that we should use are from 0 to 800 for longitude and from 0 to 400 for latitude. So not only is everything compressed in one corner, but 3/4 of the points are even outside of the picture.
So we will have to rescale these values.
Rescaling longitude and latitude
Instead of using cx={datapoint.from_long}
we have to rescale that longitude from its original range (called its domain) to a new range. The formula to do this is:
Let’s put that in a function that we can use. Add the rescale
function to the script
section of your svelte file, and call it where we need to set cx
and cy
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
let datapoints = []
fetch("http://vda-lab.github.io/assets/svelte-flights.json")
.then(res => res.json())
.then(data => datapoints = data.slice(1,5000))
const rescale = function(x, domain_min, domain_max, range_min, range_max) {
return ((range_max - range_min)*(x-domain_min))/(domain_max-domain_min) + range_min
}
</script>
<style>
svg {
background-color: whitesmoke;
}
circle {
opacity: 0.3;
}
</style>
<svg width=800 height=400>
{#each datapoints as datapoint}
<circle cx={rescale(datapoint.from_long, -180, 180, 0, 800)}
cy={rescale(datapoint.from_lat, -90, 90, 0, 400)}
r=3 />
{/each}
</svg>
The new function is defined on lines 7 to 9 and used in lines 23 and 24.
Our new image:
This is better, but the world is upside down. This is because the origin [0,0] in SVG is in the top left, not the bottom left. We therefore have to flip the scale as well, and set the range to 400,0
instead of 0,400
for cy
. If we do that we’ll get the world the right side up.
Scaling the size of the circles
We also want to scale the size of the circles: small for short distance flights, and big for long distance flights. To do this we’ll need the minimum and maximum distance in the file, and can find out that this is 1 km and 15406 km. Let’s set the minimum radius of a circle to 2 pixels and the maximum to 10 pixels. This is our new file (remember the switch from 0,400
to 400,0
from above):
Our new image:
HTML class: setting the colour
Next, we want domestic flights to be blue and international flights to be red. We can do this because each flight has a from_country
and a to_country
. If these are the same we have a domestic flight, otherwise it is international. Here’s we’ll have to start using classes and CSS selectors (see above). We can for example set the default colour of the circles to blue, but give those flights that are international the class attribute of international
. We can then use that class attribute in the CSS to set the colour to red.
Let’s first do the styling bit:
This will set the default color of circles to blue, except when they have the international
class.
We’ll have to do that programmatically in some way. A single circle
element could look like this:
or
To add this, we will use an inline javascript expression:
Our final code looks like this:
Which gives the following (final) image:
Making the plot interactive with a slider
Let’s add an additional feature to the plot. We want to add a slider to filter the airports that are shown on the screen. As svelte is regular HTML, we can easily make use of all HTML elements, and there appears to be one for a slider.
There is an extensive list of input
element types, including color picker, button, radio button, password field, etc. But also range
which represents a slider. In the code snippet above, we set the minimum value to 1, the maximum value to 15,406 and the default value to 5,000.
The filtering itself
Let’s use an additional class on the circles: hidden
. Circles should be hidden unless their value for distance
is less than 1,000 km difference from what is selected with the slider. So if the slider is at 5,000, all flights that are either less than 4,000 or more than 6,000 km should be hidden. It’s good to first define the style. So add the following which will make the “hidden” circles not completely disappear, but makes them very transparent.
We can do the same as we did with international vs domestic flights: class:hidden={compare distance to slider value here}
. But how can we access the value of the slider? We can define it in the script
section. For example:
The full code looks like this:
This gives our final interactive tool (drag the slider: this visual is live):
Here’s a static screenshot:
Quick exercise: lines
See if you can adapt the previous script to generate the following image where departure airports are linked to their arrival airports.
What we did not go into
Obviously svelte is much more than what we’ve seen above. Actually, the most important things we have not even looked into. I will explain them very briefly below, but make sure that you have a look at the svelte tutorial at http://svelte.dev/tutorial.
Below is just an idea of some of the important concepts; I will not explain them in detail.
Components
What if we want to create two scatterplots instead of one, e.g. one for departure airports and one for arrival airports? We could duplicate the code for the SVG, like so:
It would be nicer if we could something like this instead:
Static image using Scatterplot component
Create a new subfolder of src
with the name components
, and create a new file named Scatterplot.svelte
.
We’ll move everything that is relevant to the scatterplot itself into this new file:
This component defines a datapoints
variable. Because of the export let
instead of just let
we can access this variable from outside. Now how do we do that? We have moved all scatterplot specific code from App.svelte
into this new component. App.svelte
will now look like this:
We first import this new Scatterplot
element that we created. We still have to load the data using the fetch API, but now we use the Scatterplot
element instead of the original SVG. Just to make things a bit more clear, we have renamed the original datapoints
to datapoints_from_app
. The Scatterplot
element takes a datapoints
attribute. This attribute exists because we defined the export let datapoints
in the component. The value of that datapoints
variable is what comes from datapoints_from_app
.
In this case, the SVG still always shows the departure airports. How do we change that so that we can also show the arrival airports? We have to replace the from_long
and from_lat
in the component with a variable. Let’s do that while setting from_long
and from_lat
as the default:
Notice that we added 2 new variables (long
and lat
) and use these in the value for cx
and cy
instead of the original from_long
and from_lat
.
Because we export these variables, they are now available in the App.svelte
and we can do this:
Reactivity
Another strong feature of svelte is its support for reactivity. Reactivity means that when some variable a
depends on a variable b
, and b
is changed, that the value for a
is automatically updated as well. This is what makes a tool like Excel so strong as well: if you have a cell in a spreadsheet with a formula =A1*2
, it will have the value of cell A1 multiplied by 2. If you change the value of A1, the value in this derived is automatically updated as well. Most programming languages do not have this baked in, but with svelte you do have that functionality.
We do this using the $:
pragma. For example:
Notice that we use bind:value
in the slider. Sliding left and right will now update the multiplied value as well.
Animations
It isn’t that hard to create animations using svelte and javascript. We’ll use the built-in setInterval
function. This function will run something over and over again at specific intervals (in milliseconds). For example, the following code will print “Hello” to the console every 3 seconds: setInterval(function(){ console.log("Hello"); }, 3000);
.
Let’s move a little rectangle across the screen:
In the following example, we rotate a line.
Custom visuals
We can actually create quite complex visuals. But let’s just make a pie chart as a proof-of-principle.
App.svelte
Pie.svelte
PieSlice.svelte
With this you can start changing colours of the slices, have different radii for different slices, add hover effects, etc.
Overview first, search and filter, and details on demand
Above we have see how to create single visuals using svelte. But what if we want to combine these, for example when you want to show an overview of some dataset, with next to it a more detailed view?
There are many ways of doing this
Let’s use the iris dataset for this.
Conditionally showing content
Consider the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
import Papa from 'papaparse';
let datapoints = []
Papa.parse("iris.csv", {
header: true,
download: true,
complete: function(results) {
datapoints = results.data
}
})
let selected_datapoint = undefined;
</script>
<style>
circle {
fill: steelblue;
fill-opacity: 0.5;
}
</style>
<svg width=400 height=400>
{#each datapoints as datapoint}
<circle cx={datapoint.sepal_length*50}
cy={datapoint.sepal_width*50}
r=3
on:mouseover={() => {selected_datapoint = datapoint}}
on:mouseout={() => {selected_datapoint = undefined}}/>
{/each}
</svg>
<br/>
{#if selected_datapoint != undefined}
Sepal length of selected flower: {selected_datapoint.sepal_length}
{/if}
Here is what that looks like if you hover over a datapoint:
(If your don’t have the iris dataset, you can point to this url, but make sure you remove the last empty line.)
So what happens here? We load the data using Papaparse as shown before, but also create a new variable selected_datapoint
and set it to undefined.
In the SVG element, each datapoint is drawn as a circle on the screen with the cx
and cy
depending on the sepal length and sepal width of data datapoint. The *50
is a quick hack here to spread the points more on the screen; you should use scaling here.
In the on:mouseover
and on:mouseout
attributes for the circle, we set or unset the selected_datapoint
. We don’t even need to create a separate function for that: in svelte you can include javascript code anywhere you use curley brackets. (Have you read the svelte tutorial yet at svelte.dev/tutorial? Check out the section “Events / Inline handlers”.) So when the mouse hovers over a circle, the variable selected_datapoint
is set to equal the datapoint linked to that circle; when the mouse leaves a circle, the selected_datapoint
is set to undefined
.
Now the magic happens in the last 3 lines: if the selected_datapoint
is not undefined
, a piece of text is shown with the sepal_length
of that datapoint. This is a very simple way of showing details.
A tooltip
In the example above, the piece of text is shown below the image itself. But of course we can put that anywhere we want. So why not let it follow the mouse like what a tooltip would do?
We can do this by giving the div
a “fixed” position which is located at the mouse coordinates, like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
import Papa from 'papaparse';
let datapoints = []
Papa.parse("iris.csv", {
header: true,
download: true,
complete: function(results) {
datapoints = results.data
}
})
let selected_datapoint = undefined;
let mouse_x, mouse_y;
const setMousePosition = function(event) {
mouse_x = event.clientX;
mouse_y = event.clientY;
}
</script>
<style>
circle {
fill: steelblue;
fill-opacity: 0.5;
}
#tooltip {
position: fixed;
background-color: white;
}
</style>
<svg width=400 height=400>
{#each datapoints as datapoint}
<circle
cx={datapoint.sepal_length*50}
cy={datapoint.sepal_width*50}
r=3
on:mouseover={(event) => {selected_datapoint = datapoint; setMousePosition(event)}}
on:mouseout={() => {selected_datapoint = undefined}}/>
{/each}
</svg>
<br/>
{#if selected_datapoint != undefined}
<div id="tooltip" style="left: {mouse_x + 10}px; top: {mouse_y - 10}px">
Sepal length of selected flower: {selected_datapoint.sepal_length}
</div>
{/if}
What we get with this code:
In the on:mouseover
event handler, we now update a mouse_x
and mouse_y
variable which reflect the current mouse position. This mouse_x
and mouse_y
are then used in the style
attribute of the div
at the bottom:
Finally in this example, we add CSS in two different ways: part of it (position
and background-color
) in the style
section, and the actual position (left
and top
in the style
attribute of the element itself. We can’t directly use {mouse_x}
or similar in the style
CSS section.
Add some padding and shadow, and you have a nice-looking tooltip.
An image as tooltip
We now have a div
that floats around the screen, but this can be anything, including an SVG. So let’s change that little div
with the following:
This is just a proof-of-principle showing that the line depends on the actual datapoint. So let’s make something a little bit nicer: a small shape that depends on the actual data. INTERACTIVE: Hover your mouse over a datapoint below, and see how for example the tooltips from datapoints on the right are different from those on the left.
(Here’s a screenshot)
In this little image, the white circle is the center. Up represents sepal length, right petal length, down sepal width, and left petal width. We can create this by changing the line
in the svg
to a path
:
As we had set the #tooltip
to fixed
in the CSS, the image will now follow the mouse. We use a lineGenerator
function (you can call it whatever you want) to create the actual path:
In this last bit of code, we first set e.g. sl
to +d.sepal_length
. We do this to force sl
to be a number, otherwise you’ll notice that the string generated for the return
statement will be incorrect. See the path
documentation for information on how to interpret the full string. But for example in the second line (" L " + (25+3*pl) + " 25"
) we draw a line to a certain point with x position 25+3*pl
and y position 25
. We multiplied the petal length with 3 just to make it scale a bit nicer using trial-and-error. Normally you’d write a little nice scaling function for this.
Merging the tooltip into the scatterplot itself
We can even go further, and actually have the circles that represent iris flowers not be circles but the actual shapes that we use for the tooltip. This is what we’ll create:
(Note that this is not an ideal visual due to the overlaps, but let’s look at this as a proof-of-principle…)
This is where svelte shines, as we can create custom HTML elements. So instead of circle
we can use Flower
. The App.svelte
can now be much smaller (because we also remove interactivity in this example):
Wat is new here, is the second line import Flower from './Flower.svelte';
, and the fact that we replace circle
with a Flower
element. But we need to have the Flower.svelte
file for this which describes the Flower
component:
The flower component returns a single group g
(see documentation) containing all information to draw a single datapoint. In this case, a path
is created and we put a little white circle at its center.
By the way, we don’t need to create those new variables sl
, sw
, etc. That is just to make the path
string easier to read.
This is a nice example of how you can create a visual design for a single datapoint and then combine these into larger plots.
Or we can draw the actual flowers where the size of the sepals and petals of the image correspond to the data.
… or to generate a grid of flowers:
(Note that we have the each
outside of the svg
instead of inside.)
Deploying your visualisations
It’s easy to deploy your app as well, for example using vercel. Create an account on vercel.com, install the vercel
NPM module, and run the vercel
command:
You will have to answer a couple of questions, but these are straightforward. As an example of such deployment, see here.
You can for example find a version of the flight visualisation here and the iris visualisation here.