diff --git a/06-json.md b/06-json.md index 4789099..943fcfe 100644 --- a/06-json.md +++ b/06-json.md @@ -90,7 +90,7 @@ cat_list.push({weight : 6 , past_weight_values : [5.9, 5.3, 6.1], name : 'Snowba This process is called 'nesting'. -> # Nesting {.challenge} +> ## Nesting {.challenge} > 1. Append the array by a third cat, not entering a name or weight. > 1. Do all animals have to have the same attribute fields? > 1. Use the console of your browser to read the values of your object. @@ -124,7 +124,7 @@ to convert it into a string. JavaScript provides an easy way to do this. Data can get converted using `JSON.stringify()`. -> # Arrays of objects {.challenge} +> ## Arrays of objects {.challenge} > 1. Append a third cat to the array, not entering a name or weight. > 1. Do all animals have to have the same attribute fields? > 1. Use the console of your browser to read the values of your object. @@ -149,7 +149,7 @@ When the map function is called using `cat_list.map`, it loops through all eleme in `cat_list`, calls each temporarily `cat`, and creates a new list `dog_list`, which will have one dog per cat. The function `concat` simply concatenates two strings. -> # How much do dogs weigh? {.challenge} +> ## How much do dogs weigh? {.challenge} > 1. Let's assume dog misters weigh twice as much as their feline version. Make > the new array reflect this. diff --git a/07-d3setup.md b/07-d3setup.md index 085d3f6..b264da1 100644 --- a/07-d3setup.md +++ b/07-d3setup.md @@ -56,13 +56,13 @@ The first thing we need, is of course our data, which is stored in 'nations.json D3 provides a handy function to read in `json`-files: ~~~{.d3} -d3.json("resources/nations.json", function(nations) { } +d3.json("code/nations.json", function(nations) { }); ~~~ This line probably needs a little explanation and we'll go through it bit by bit: * `d3.json()` is called the function call. In this case, we have a function that reads in a json file, parses it, and is also able to do something with the parsed data on the way. -* The first argument `"resources/nations.json"` tells the function where to get the data we want to have parsed. +* The first argument `code/nations.json"` tells the function where to get the data we want to have parsed. * `function(...){...}` is called the callback function. It is a so-called 'inline' function, which means it has no name (we're only operating in the object space here). This also means we can't use this function anywhere else in our code. The code we put inside the curly brackets is the code that's run once d3.json() is called and the data is loaded. * D3 assigns the name `nations` to the parsed object it returns. We can only use 'nations' within the callback function, this means our code only knows of `nations` inside the curly brackets. * What seems unusual, but is actually quite common, is that this function call doesn't return anything. It is simply executed and displayed (if we tell it to), but no value is returned. @@ -90,7 +90,7 @@ fill now. We'll have a picture frame (an SVG-element), our drawing area (a g-element), and in that drawing area, we'll have separate elements for both axes and the area for our circles. -Firt, we need to link the JavaScript and HTML environement so that we have writing access +First, we need to link the JavaScript and HTML environment so that we have writing access to the HTML. To do this, we use the `.select()`. This lets us grab an element by specifying its ID. @@ -111,9 +111,20 @@ This is equivalent to writing:
~~~ -in the HTML file. We chose to append because we now have access to the SVG element without the need to seperately select it by ID. +in the HTML file. We chose to append because we now have access to the SVG element without the need to separately select it by ID. -We also create the canvas inside the frame: +We can open our HTML file in a browser and see that only the heading is actually visible on the page. +It is normal as our drawing is currently empty. +To materialize our empty drawing and be sure it is here, let's add a thick red border around it, by adding the following in the `main.css` file: + +~~~{.css} +svg { + border: 5px solid red; +} +~~~ + +Refreshing the page should make it clear that our empty drawing actually exists. +Now, we also create the canvas inside the frame: ~~~{.js} // Create canvas inside frame. @@ -124,7 +135,7 @@ Let's set up the dimensions for our elements that we want to use: ~~~{.js} // Set margins, width, and height. -var margin = {top: 19.5, right: 19.5, bottom: 19.5, left: 39.5}; +var margin = {top: 21.5, right: 21.5, bottom: 21.5, left: 39.5}; var frame_width = 960; var frame_height = 350; var canvas_width = frame_width - margin.left - margin.right; @@ -139,6 +150,8 @@ frame.attr("width", frame_width); frame.attr("height", frame_height); ~~~ +Refreshing the page will show us the new size for the SVG drawing. + The canvas element will have to fit nicely into the frame. To make it fit, we set a transform attribute and use the translate function. @@ -147,12 +160,21 @@ a transform attribute and use the translate function. canvas.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); ~~~ -> # Time for a challenge I think! {.challenge} +> ## Time for a challenge I think! {.challenge} > Let us consolidate our understanding of SVGs and D3 selectors. - -> 1. Add a circle element to the HTML using the SVG markup we learnt in lesson 3. +> +> 1. Create a new SVG element and a circle element using the SVG markup we learnt in lesson 3. > 2. Use `d3.select` to get a reference to the circle. > 3. Once the circle reference is obtained, make the radius 40px, the border black and the colour green. - +> > HINT: use the `attr` and `style` methods on the circle object obtained. +> ## Understanding the "transform". {.challenge} +> +> +> 1. Use Javascript code to Create a new circle inside our "canvas", positioned in 0, 0, with a radius of 5px. +> 2. Check that the circle now shows up. +> 3. Play with the values that are put for the "translate" transform in order to understand what is the reference frame used. +> +> HINT: refresh your page in your browser every time you do a modification. + diff --git a/08-d3enter.md b/08-d3enter.md index 9bfa8e6..3b95a0a 100644 --- a/08-d3enter.md +++ b/08-d3enter.md @@ -69,7 +69,7 @@ canvas.append("g") We add a transform attribute to move the axis to the bottom of the plotting area (instead of having it across the top). There are a number of transform options, but here we are just using `translate` and pass in the amount to shift the axis in the x and y directions, respectively. Here we shift it only in the y direction (i.e. down) by an amount given by height of the canvas. We also give it a class, just in case we might want to select the axis later in our code. -> # We might need a y-axis, too {.challenge} +> ## We might need a y-axis, too {.challenge} > 1. Create a linear scale for the y-axis, with 10 being the minimum and 85 being the maximum value. Then, add the axis to the canvas. We're slowly getting there. Having our two axes, we can now finally add our data. @@ -112,7 +112,7 @@ The attributes `cx` and `cy` define the position of the centre of the circle and arbitrary number... for now. -> # A new dimension {.challenge} +> ## A new dimension {.challenge} > Change the code so that the radius of the circles represents the population. First, create a 'sqrt' scale with a minimum of 0 and a maximum of 5e8. The range should be between 0 and 40. Also, don't forget to include a mapping function for the scale for population. diff --git a/09-d3exit.md b/09-d3exit.md index 514b336..fec294d 100644 --- a/09-d3exit.md +++ b/09-d3exit.md @@ -15,6 +15,27 @@ Our plot is pretty busy. We might not want to display everything all the time. The goal for this lesson is to update the plot based on what kind of data we want to display. +First, we will reorganize our Javascript program. +It currently mixes two concerns: + +- one-time initialization: setting up the SVG element and its sub elements, positioning and sizing them, etc; +- data manipulation (everything related to the `dot` variable), that we actually might want to execute again when data changes. + +To be able to execute the data-manipulation code multiple times, we will isolate it in a new function that we decide to name `refreshData`, and call this function (so that the code is actually executed). + +~~~{.js} +// define the function with the data-manipulation code +function refreshData() { + var dot = data_canvas.selectAll(".dot").data(nations, function(d){return d.name}); + + dot.enter().append("circle")......... +} + +// call the function once +refreshData(); +~~~ + +Now we can resume our original task: plotting only a sub-part of the data. First, we need to find a way to filter our data. We use the function `filter` to do this. Similar to previous functions (e.g. `map`), this function iterates over each of the elements in the array `nations`, temporarily calling it `nation`. It only includes elements in the new array `filtered_nations` if the function evaluates to 'true' for that element. Here this will be the case for nations whose population in 2009 was larger than 10,000,000. @@ -25,7 +46,7 @@ var filtered_nations = nations.filter(function(nation){ }); ~~~ -> # Filtering by region {.challenge} +> ## Filtering by region {.challenge} > You might have noticed that our data contains information about the region in > which a country is. > @@ -66,10 +87,10 @@ if (this.checked) { // adding data points } ~~~ -This `if`-statement gets executed every time a checkbox is checked. To add the data points, we can use the `push`-function, which adds one object to an array at a time. -First, we filter the nations we want to add, calling them `new_nations`. Next, we are looping through all new nations and add one at a time to the array `filtered_nations`. +This `if`-statement gets executed every time a checkbox is checked. To add the data points, we can use the `concat`-function, which concatenates two arrays and returns a new array. +First, we filter the nations we want to add, calling them `new_nations`. Next, we concatenate this new array to the existing one and save it back to the array `filtered_nations`. -We also have to initialise `filtered_nations` at the top of our script. Remember that there is a difference between the object and the name space, so in order to keep `nations` the way it is, we need to map the values instead of just using `=`. +We also have to initialise `filtered_nations` at the top of our script, or at least before our `refreshData` function. Remember that there is a difference between the object and the name space, so in order to keep `nations` the way it is, we need to map the values instead of just using `=`. ~~~{.js} var filtered_nations = nations.map(function(nation) { return nation; }); @@ -87,29 +108,14 @@ Whereas before `enter()` was used to append new elements to the plot, `exit()` i A good, brief explanation of this linking between data and elements on the page can be found [here](http://bost.ocks.org/mike/join/). This article discusses the three important functions used for this: `enter`, `exit`, and a third function `update` that we will get to shortly. -> # Removing elements {.challenge} +> ## Removing elements {.challenge} > 1. Using an `else` case after the `if` statement, create a filter that removes elements from `filtered_data` that correspond to the checkbox that was just unchecked. (i.e. `else { filtered_nations = <--- fill in this bit --->}`). -> # Another new dimension {.challenge} +> ## Another new dimension {.challenge} > 1. Have the colour of circles represent the region. Use category20() to make a scale. You will then need to add `.style("fill", function(d) { <-- fill in this bit ---> });` to the enter() function. -As a last step, let's move `enter()` and `exit()` into a separate function. This will become useful when we want to update the data from different elements on the page. - -~~~{.js} -function update() { - var dot = data_canvas.selectAll(".dot").data(filtered_nations, function(d){return d.name}); - - dot.enter().append("circle").attr("class","dot") - .style("fill", function(d) { return colorScale(d.region); }); - .attr("cx", function(d) { return xScale(d.income[d.income.length-1]); }) // this is how attr knows to work with the data - .attr("cy", function(d) { return yScale(d.lifeExpectancy[d.lifeExpectancy.length-1]); }) - .attr("r", function(d) { return rScale(d.population[d.population.length-1]); }); - - dot.exit().remove(); -} -~~~ - -This means that we now have to call the update function from our event listener after updating `filtered_nations` based on the checkbox change: +As we have isolated the data-processing code in the `refreshData` function, we can now refresh the data every time the checkbox is clicked. +This means that we now have to call the `refreshData` function from our event listener after updating `filtered_nations` based on the checkbox change: ~~~{.js} d3.selectAll(".region_cb").on("change", function() { @@ -120,12 +126,10 @@ d3.selectAll(".region_cb").on("change", function() { } else { // remove data points from the data that match the filter filtered_nations = filtered_nations.filter(function(nation){ return nation.region != type;}); } - update(); + refreshData(); }); ~~~ -In order to create the plot when we first load the page, we will also have to call `update()` outside of our event listeners once. - By the end of this lesson, your page should look something like this: diff --git a/10-d3update.md b/10-d3update.md index 207dccb..b4bf406 100644 --- a/10-d3update.md +++ b/10-d3update.md @@ -28,16 +28,16 @@ To get the index (rather than the actual year), we can simply subtract the first var year_idx = parseInt(document.getElementById("year_slider").value)-1950; ~~~ -Updating the year becomes quite simple. All we need to do is add another event listener that changes the year the moment we touch the slider. The event we want to listen for is called `input`. We then execute the `update()` function we wrote earlier. +Updating the year becomes quite simple. All we need to do is add another event listener that changes the year the moment we touch the slider. The event we want to listen for is called `input`. We then execute the `refreshData()` function we wrote earlier. ~~~{.js} d3.select("#year_slider").on("input", function () { year_idx = parseInt(this.value) - 1950; - update(); + refreshData(); }); ~~~ -So far, the update function only knows how to handle new data (`.enter`) and removed data (`.exit`), but not what to do when we update data. +So far, the `refreshData` function only knows how to handle new data (`.enter`) and removed data (`.exit`), but not what to do when we update data. In addition to `d3.enter()` and `d3.exit()`, D3 also offers `d3.transition` to handle updating data. First, we need to define how to transition between data points. We might want to interpolate between to values linearly over the duration of 200 ms, like this: ~~~{.js} @@ -64,7 +64,7 @@ dot.transition().ease("linear").duration(200) > * elastic(a, p) - simulates an elastic band; may extend slightly beyond 0 and 1. > * [more here](https://github.com/mbostock/d3/wiki/Transitions#d3_ease) -> # Play time {.challenge} +> ## Play time {.challenge} > D3 is incredible versatile. Try out different transitions and if you have time, maybe try drawing rectangles instead of circles. Next, we might want to create a tooltip. Let's go have a look at what's already out there. @@ -147,15 +147,26 @@ function calc_mean(region_data) { } ~~~ -> # The master challenge {.challenge} +> ## The master challenge {.challenge} > It's time to put together everything you've learned. Write code that displays (and updates) the mean values that we just computed as little crosses in the graph for the different regions. -> # ...style! {.challenge} +> ## ...style! {.challenge} > Add axis labels and make the fonts pretty. -> # Using different data formats {.challenge} +> ## Using different data formats {.challenge} > What if you don't have your data in JSON format? Change your code to load in nations.csv instead of nations.json and have it produce the same plot. +> ## Generate the checkboxes automatically {.challenge} +> Currently, we have manually created checkboxes in the HTML file. +> Using the `region_names` variable that is a list of all region names, create the checkboxes using javascript and/or d3. + +> ## Get the region names from the data {.challenge} +> The `region_names` array is currently filled in manually. +> You want to replace this by an automatic extraction from the actual data file. +> +> - using the `nations` data and the `map` function, extract the `region` of each nation, +> - using the `d3.set` function that creates a set (without duplicate values) and the `.values` function on a set (that return an array), filter the `region_names` array so that it does not contain duplicates anymore + By the end of this lesson, your page should look something like this: