This week's exercises are all about interactivity. You will update last week's gapminder visualization to support animating over the years and letting the user select which continent is shown.
For this exercise, you have to edit the file src/routes/week_7/exercises/ex_1.svelte
. Any changes you make to that file should show up below.
HTML provides several types of input tags as wel as a dedicated button tag that you can use to let people interact with your visualisations. Svelte also makes it easy to program what each input has to do with the on-directive.
Create two buttons, one that keeps track on the number of times you've clicked on it and one to reset the counter. Last week, we used bootstrap to style our buttons, you can do the same here.
For this exercise, you have to edit the file src/routes/week_7/exercises/ex_2.svelte
. Any changes you make to that file should show up below.
Interactivity is where Svelte shines; it automates a lot of processes that we
want to update when values change. However, it can sometimes be confusing to
reason about a svelte component. We do not always see at a glance what code
will be updated when values change. It is important to remember that code in
the script of a component is evaluated only when the component is constructed.
Code in the HTML markup is evaluated any time a variable that is mentioned
within the curly braces changes. If you want your script to re-evaluate
certain statements when a variable changes, you have to use the $:
notation:
<script>
export let myVariable;
console.log('This message is printed only once!')
$: console.log(`This messages prints every time myVariable changes: ${myVariable}`);
</script>
Remember that only variables that are mentioned within the scope of the $:
statement are monitored for changes! Also know that two reactive statements
do not cause an infinite loop when they change each other's values:
<script>
export let counter1=0;
export let counter2=0;
const increment1 = (_) => counter1+=1;
const increment2 = (_) => counter2+=1;
$: increment1(counter2);
$: increment2(counter1);
</script>
In this example, if you change the value of counter2
both counters
will be incremented once. If you change the value of counter1
only
counter2
will be incremented.
Sometimes, you also need to schedule the evaluation of a function, so that it
will be evaluated some time in the future. JavaScript's setInterval()
function can be used for that purpose. You give it a function and a delay-time
and it will keep on calling your function with your specified pause in between
calls until you either call clearInterval()
or close the page.
For this exercise, you have to build a countdown light like they use in F1
racing. There are five lights that are off initially, then they turn red
one-by-one from the right to the left. After all the lights are red, they
switch to green simultaneously. The necessary data arrays are already created.
Use the points
arrary as x-coordinates of the circle elements and
index
as the index of the left-most light that should be on. If a
light is on set the opacity to 1
, otherwise use 0.3
. You should use the second argument of the each-block to capture the index
of the current iteration. Write a function that reduces the index
every second. Once all lights are on, change color to darkgreen
and stop the function from reducing the index further. The animation should
only start when the visualisation is visible!
Hint: remember how you loaded data when the component was first rendered
last week!
For this exercise, you have to edit the files src/routes/week_7/exercises/ex_3.svelte
, src/routes/week_7/exercises/_ex_3_scatterplot.svelte
, and src/routes/week_7/exercises/_ex_3_controls.svelte
. Any changes you make to those file should show up below.
It's time to make the gapminder scatterplot interactive. We have already
prepared a static version for you. Note that we load the data in ex_3.svelte
as you learned last week. At the end of this exercise, we want to be able to
show all the years in the data as an animation. To do that, you have to add a play-pause
button, a reset button, and a slider that indicates and controls which year is
shown. The label of the slider should contain the currently shown year, so it is
easy to read! Finally, you have to add a drop-down menu which can be used to filter
which continent's countries are shown.
The visualization uses three components, each with a separate responsibility:
_ex_3_scatter.svelte
creates a scatter plot of the data it is given,
_ex_3_controls.svelte
contains the interactive elements that control
which year and continents are shown,
ex_3.svelte
manages the data, e.g., loading and extracting the
selected year and continents. JavaScript contains a very useful array manipulation
function that extracts items from an array that meet a certain condition:
const higherThanFive = array.filter(d => d > 5);
So, perform these steps:
_ex_3_controls.svelte
, add component properties for the
currently selected year and continents. The year should be stored as an
index into the
data
array (i.e., the value 0 corresponds to 1800 and the value
10 to 1810). Also add the described buttons, slider, and drop-down menu that
manipulate the value of these component properties. Note, the animation should
loop-back to the start when it reaches the end. The reset button should stop
the animation if it is active and set 1800 as the selected year.
Hint: to create the animation, remember how you scheduled a function to be
executed in an interval in the last exercise!
ex_3.svelte
, observe the selected year and continent as
exposed by _ex_3_controls.svelte
using a bind
.
Then, define a function that extracts the selected data using the selected
year and continents variables. Finally, only pass the selected data to the _ex_3_scatterplot.svelte
component.
You should now have an interactive GapMinder visualisation! As in Exercise 1, you can use boostrap to make the input elements look nice. Hint: does your visualisation break when the slider reaches the end? Then you probably made an off-by-one error by allowing the slider's value to reach 1 item passed the end of the data array!
Loading the data, please wait...
For this exercise, you have to edit the files src/routes/week_7/exercises/ex_4.svelte
and src/routes/week_7/exercises/_ex_4_scatter.svelte
. Any
changes you make to those file should show up below.
We have prepared a visualization that shows two scatter plots using the /data/cars-2.csv
dataset. Note that we construct two _ex_4_scatter.svelte
components
using a
spread operator
in the then-block of ex_4.svelte
:
<script>
import Scatter from './_ex_4_scatter.svelte';
...
const s1 = {
x: (d) => +d.Horsepower,
y: (d) => +d.Acceleration,
xLabel: 'Horsepower',
yLabel: 'Acceleration'
};
</script>
...
<Scatter {data} {...s1} />
...
Here, s1
's properties (x
, x
, xLabel
, and xLabel
) are passed to the Scatter
component
as individual component properties. The Scatter
component uses them
to extract the values it has to use for the x- and y-coordinates, and puts the
labels on their respective axes. This way, we can re-use the Scatter
component for two different scatter plots.
In this exercise, you will add brushing and linking to these two scatter plots, such that you can draw a rectangle in one plot, and the points that lie within that rectangle get highlighted in both plots. In the case that you draw a rectangle in both plots, only the points that lie within both rectangles should get highlighted.
d3.js provides the functionality that you need to draw these rectangles in the d3-brush
module. Like, with the axes, calling d3.js' brush()
function gives
you a function that takes a selected handle to an svg group element
and adds the brushing functionality to that element. You can use Svelte's
actions
to apply the brush to an element, just as we did for the axes. However, without
configuration, the brush will not do anything. You have tell the brush what it
has to do when a brush starts, moves, and ends. To do that, you have to call the
.on()
function on the brush:
brush()
.on('start', doStart)
.on('brush', doMove)
.on('end', doEnd)
So, to implement brushing and linking, peform these steps:
_ex_4_scatter.svelte
. One
that stores an array of boolean values indicating which points are selected
in both scatter plots and one that stores a similar array for the points
selected in just the local component.
ex_4.svelte
use a bind
to observe the local selections
of both plots. Then reactively define the global selection so that only points
that are selected in both plots are set to true
. Pass the global selection as a normal component property to the _ex_4_scatter.svelte
components.
range
variable for you that will store the
range of the selection rectangle if there is an active selection or null
otherwise. Configure a d3-brush so that the selection
property
of the argument it passes to your callbacks is written to our range
variable when the brush starts, moves, and ends. The values the brush gives
are in the coordinate system of the element it is applied to. So, apply the brush
to the SVG group element that contains our datapoints. This ensures that we do
not have to deal with the margins of our svg element when we compute whether
a point lies within the selection rectangle.
_ex_4_scatter.svelte
. Define a coords
variable that stores an {x, y}
object for each data point. Use it to plot the circles instead of re-computing
the x- and y-coordinates in the markup section.
range
and coords
variable to reactively update
the values of the local selection such that all data points are set to
true
if there is no active selection and
only the points that lie within the rectangle are set to true
when there is an active selection.
darkgray
, the fill opacity to 0.3
, and the stroke opacity to 0.5
when a point is not selected.
Now you should have a working brushing and linking implementation!
Loading the data, please wait...