Merge branch 'draft-HTML-guide' into 'main'

Added a first draft of the HTML/JavaScript tutorial.

See merge request interactivetextbooks-citg/jupyter-book-manual!56
This commit is contained in:
Tom van Woudenberg 2024-03-15 14:56:34 +01:00
commit e8b328d0cb
4 changed files with 1259 additions and 1 deletions

View file

@ -0,0 +1,392 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body{
margin: 0; overflow: hidden;
font-family: Helvetica, sans-serif;
}
</style>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_datavisualization">
<svg id="click" xmlns="http://www.w3.org/2000/svg">
</svg>
</div>
<body>
<script>
// Get the viewport height and width
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit to viewport
var height = vw * 0.5;
var width = vw;
// Create the canvas. We will use only part of it for the main plot
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
function linspace(start, end, resolution) {
var spacing = [];
// Go through a for-loop
var i;
for (i = 0; i < resolution; i++) {
spacing.push(start + (end - start) * i / (resolution - 1))
}
return spacing; // The function returns the linspace between p1 and p2
}
function standard_Gaussian_pdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
normalize = math.dotDivide(
1,
math.sqrt(
math.dotMultiply(
math.dotMultiply(
2,
math.PI),
math.pow(
std,
2))));
var i;
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
expon = math.exp(
-math.dotDivide(
math.pow(
x[i] - mean,
2),
math.dotMultiply(
2,
math.pow(
std,
2))))
temp = math.dotMultiply(
normalize,
expon)
result.push(temp)
}
return result
}
function standard_Gaussian_cdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
temp = 0.5 * (1 + math.erf(x[i] /math.sqrt(2)))
result.push(temp)
}
return result
}
// Define a subplot for the standard normal
const x_limits = [-3, 3];
const y_limits_pdf = [0, 1 / math.sqrt(2 * math.pi) * 1.2];
const window_x_pdf = [width * 0.1, width * 0.5];
const window_y_pdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_pdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_pdf)
const xScale_pdf_inverse = d3.scaleLinear()
.domain(window_x_pdf)
.range([x_limits[0], x_limits[1]])
const yScale_pdf = d3.scaleLinear()
.domain([y_limits_pdf[0], y_limits_pdf[1]])
.range([window_y_pdf[1], window_y_pdf[0]])
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_pdf[1].toString() + ")")
.call(d3.axisBottom(xScale_pdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_pdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_pdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_pdf))
.style("font-size", (12 * width / 600).toString() + "px");
svg.append("text")
.attr("transform",
"translate(" + (width * 0.03).toString() + "," + (math.mean(window_y_pdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("probability density")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_pdf[1] - window_x_pdf[0])/2 + window_x_pdf[0]).toString() + "," + (window_y_pdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("pdf")
.style("font-size", (16 * width / 600).toString() + "px")
// Evaluate the normal pdf
var x = linspace(-3, 3, 201);
var pdf = standard_Gaussian_pdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_pdf(0))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_pdf");
// Get the data for the path
var data_pdf = [];
for (i = 0; i < x.length; i++) {
data_pdf.push({
x: x[i],
y: pdf[i]
})
}
var valueline_pdf = d3.svg.line()
.x(function(d) {
return xScale_pdf(d.x);
})
.y(function(d) {
return yScale_pdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_pdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
var fill_x, fill_pdf
var xpos = 0;
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_fill))
.attr("fill", "#999999")
.attr("stroke", "None") //"#4794c1")
.attr("id", "fill")
.lower()
// Define a subplot for the standard normal
const y_limits_cdf = [0, 1];
const window_x_cdf = [width * 0.585, width * 0.985];
const window_y_cdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_cdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_cdf)
const xScale_cdf_inverse = d3.scaleLinear()
.domain(window_x_cdf)
.range([x_limits[0], x_limits[1]])
const yScale_cdf = d3.scaleLinear()
.domain([y_limits_cdf[0], y_limits_cdf[1]])
.range([window_y_cdf[1], window_y_cdf[0]])
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_cdf[1].toString() + ")")
.call(d3.axisBottom(xScale_cdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_cdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-family", "arial")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_cdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_cdf))
.style("font-size", (12 * width / 600).toString() + "px");
//.call(d3.axisLeft(yScale).tickFormat(""));
svg.append("text")
.attr("transform",
"translate(" + (width * 0.525).toString() + "," + (math.mean(window_y_cdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("cumulative probability")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_cdf[1] - window_x_cdf[0])/2 + window_x_cdf[0]).toString() + "," + (window_y_cdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("cdf")
.style("font-size", (16 * width / 600).toString() + "px")
// Evaluate the normal pdf
var cdf = standard_Gaussian_cdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_cdf(0))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_cdf");
// Get the data for the path
var data_cdf = [];
for (i = 0; i < x.length; i++) {
data_cdf.push({
x: x[i],
y: cdf[i]
})
}
var valueline_cdf = d3.svg.line()
.x(function(d) {
return xScale_cdf(d.x);
})
.y(function(d) {
return yScale_cdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_cdf(data_cdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
svg.append('line')
.attr('x1', window_x_cdf[0])
.attr('x2', window_x_cdf[1])
.attr('y1', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('stroke', '#000000')
.attr("stroke-width", 3 * height / 600)
.style("stroke-dasharray", ((10 * height / 600).toString()+","+(5 * height / 600).toString()))
.attr("id","line_cdf")
.lower();
// Shift the marker around on mouseover; restrict it to the contour
var movex, movey, xpos
svg
.on("mousemove", function() {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
if (movex >= window_x_pdf[0] && movex <= window_x_pdf[1]) {
xpos = xScale_pdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
} else if (movex >= window_x_cdf[0] && movex <= window_x_cdf[1]) {
xpos = xScale_cdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
d3.select("#fill_cdf")
.attr('y', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('height', window_y_cdf[1] - yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
}
});
</script>
</body>
</html>

View file

@ -39,6 +39,7 @@ parts:
sections:
- file: features/grasple.md
- file: features/h5p.md
- file: features/adding_basic_HTML_elements.md
- file: features/live_code.md
sections:
- file: features/widgets.ipynb

View file

@ -0,0 +1,863 @@
# Adding basic interactive HTML/JavaScript elements
## Embedding HTML files
A straightforward way to create interactive web-based elements is to code them directly using HTML with JavaScript. This allows us to build elements such as the one below, which shows the connection between a probability density function (pdf) and the associated cumulative density function (cdf):
<iframe src="../_static/element_pdf_and_cdf.html" width="600" height="400" frameborder="0"></iframe>
If we want to embed such figures in the teachbook, you can do so via an iframe which reference a html file in the _static folder:
```html
<iframe src="../_static/element_pdf_and_cdf.html" width="600" height="400" frameborder="0"></iframe>
```
Here, the src path moves up one subdirectory, then enters the _static folder, and loads the element_pdf_and_cdf.html file. So how do we create this figure?
## Creating a basic interactive HTML/JavaScript element
At the bottom of this page, I will provide the full code for the interactive element above. In this section, let us go through the code piece by piece. Let us begin by looking at the start of the code.
We begin by specifying that this file is a HTML file. In the header, we can load packages we need later on. Within the `<script>` tags, we are loading three different libraries from web servers: `math.js`, which provides functions for basic mathematical operations, as well as two versions of the library `d3.js`, which contains many many useful functions for web interactivity.
The `<style>` tag below defines [CSS](https://en.wikipedia.org/wiki/CSS) properties that style the element. Two classic settings for the `body` are `margin: 0`, which makes sure that the useable space extends all the way to the edges, and `overflow: hidden`, which prevents scrolling if the element extends beyond its intended borders. We will scale out content to the available space later on.
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body{
margin: 0; overflow: hidden;
font-family: Helvetica, sans-serif;
}
</style>
</head>
```
Next, we create a `<div>`, an HTML element that can group other HTML elements, that contians a [SVG](https://en.wikipedia.org/wiki/SVG) named `"click"`. This SVG will serve as the canvas on which we will place the interactive content.
```html
<!-- Create a div where the graph will take place -->
<div id="my_datavisualization">
<svg id="click" xmlns="http://www.w3.org/2000/svg">
</svg>
</div>
```
So far, all of this has just been setup. The real magic begins in the `<script>` environment, which we place inside the HTML's `<body>`. This is where we code the actual content of the interactive element in JavaScript.
The first few lines of our JavaScript code are simple. To scale out element with the size of its container (the iframe we created above), the code first reads out the width `vw` and height `vh` of its container. Here, we want to scale my interactive element to be twice as wide as tall, so we define the `height` of the canvas as half of its `width`. We then slect the `"click"` canvas we created before, and set its height and width to the new dimensions we just computed.
```html
<body>
<script>
// Get the viewport height and width
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit to viewport
var height = vw * 0.5;
var width = vw;
// Create the canvas. We will use only part of it for the main plot
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
```
Next, we may want to define some functions for the functionality of our element. The first function `linspace` is a simple helper function that creates `resolution` points between a `start` and `end` value, similar to `numpy.linspace` in Python. The second and third functions simply evaluate the pdf and cdf of a standard Gaussian distribution for a given `x` value.
```html
function linspace(start, end, resolution) {
var spacing = [];
// Go through a for-loop
var i;
for (i = 0; i < resolution; i++) {
spacing.push(start + (end - start) * i / (resolution - 1))
}
return spacing; // The function returns the linspace between p1 and p2
}
function standard_Gaussian_pdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
normalize = math.dotDivide(
1,
math.sqrt(
math.dotMultiply(
math.dotMultiply(
2,
math.PI),
math.pow(
std,
2))));
var i;
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
expon = math.exp(
-math.dotDivide(
math.pow(
x[i] - mean,
2),
math.dotMultiply(
2,
math.pow(
std,
2))))
temp = math.dotMultiply(
normalize,
expon)
result.push(temp)
}
return result
}
function standard_Gaussian_cdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
temp = 0.5 * (1 + math.erf(x[i] /math.sqrt(2)))
result.push(temp)
}
return result
}
```
Now, let us prepare the two subplots for the pdf and cdf. First, we define what values we want to plot on the x-axis and y-axis. We focus here on x-values between -3 and +3, and for the pdf, we want to see the maximum of the distribution and add a small padding factor of `1.2`. In JavaScript, we have a lot of control on how we want to create the interactive plot. This means that we must define where exactly we want to place our subplots on the canvas. Let us start by defining the horizontal extent of the subplot for the pdf from 10% of the width to 50% of the width, and its height from 10% of the canvas's height to 89% of the canvas's height (recall that our canvas is twice as wide as it is tall). Mind that in Javascript, 0% width and 0% height is defined in the top-left corner.
Next, we create three helper functions. If we want to add an element to the canvas, we must define its size and position in pixels. For plotting, this is inconvenient, so here we define three functions that convert x-values to horizontal pixels (`xScale_pdf`), horizontal pixels to x-values (`xScale_pdf_inverse`), and pdf-values to vertical pixels (`yScale_pdf`).
```html
// Define a subplot for the standard normal
const x_limits = [-3, 3];
const y_limits_pdf = [0, 1 / math.sqrt(2 * math.pi) * 1.2];
const window_x_pdf = [width * 0.1, width * 0.5];
const window_y_pdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_pdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_pdf)
const xScale_pdf_inverse = d3.scaleLinear()
.domain(window_x_pdf)
.range([x_limits[0], x_limits[1]])
const yScale_pdf = d3.scaleLinear()
.domain([y_limits_pdf[0], y_limits_pdf[1]])
.range([window_y_pdf[1], window_y_pdf[0]])
```
With these functions in hand, let us draw the first subplot. We start by drawing the x-axis and its label first, where we define the font-sizes relative to the canvas's width to ensure it scales with its containers size. Next, we do the same for the y-axis. Finally, we add a label on top. Note that we define all placement positions in terms of pixels (`width` and related variables).
```html
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_pdf[1].toString() + ")")
.call(d3.axisBottom(xScale_pdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_pdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_pdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_pdf))
.style("font-size", (12 * width / 600).toString() + "px");
svg.append("text")
.attr("transform",
"translate(" + (width * 0.03).toString() + "," + (math.mean(window_y_pdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("probability density")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_pdf[1] - window_x_pdf[0])/2 + window_x_pdf[0]).toString() + "," + (window_y_pdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("pdf")
.style("font-size", (16 * width / 600).toString() + "px")
```
Now, let us add the actual content. We start by creating 201 equally-spaced points on the x-axis between -3 and +3 using the `linspace` function we created above, then evaluate the corresponding densities using the `standard_Gaussian_pdf` function we also created. The first real element we add to the subplot is a simple round marker that indicates where we are on the pdf. We can add geometric shapes like circles via a syntax like `svg.append("circle)`, then define its attributes or style properties via the `.attr()` syntax. A circle requires a radius attribute `r`, a horizontal position `cx`, and a vertical position `cy`. We initially place our marker at x=0. Note how we use the Scale functions we created earlier to convert subplot dimensions into pixels. The `fill` and `stroke-width` properties define the style of the marker. An important property is the `id`, which we can use later to select and update this marker.
Next, we concatenate our `x` and `pdf` vectors into a list of dictionaries, which we then convert into pixel coordinates. Finally, we append it as a `path` via the `svg.append("path")` function. In addition, we want to represent that the cdf (which we will later plot on the right) represents the integral of all pdf values to the left of the current x-value. To this end, we want to create a filled shape that highlights this area. We can do so by creating another path that follows the pdf from the left border to the currently selected value (initially: `x=0`), then moves back to the subplot origin along the x-axis. The path-information `data_fill` is created the same way as before, but now we add it as a `svg.append("path")` that has the `fill` attribute, which - as the name implies - fills the region inside it with a solid color. We also assign a `class` to this line, which makes selecting all elements of this class later easy.
```html
// Evaluate the normal pdf
var x = linspace(-3, 3, 201);
var pdf = standard_Gaussian_pdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_pdf(0))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_pdf");
// Get the data for the path
var data_pdf = [];
for (i = 0; i < x.length; i++) {
data_pdf.push({
x: x[i],
y: pdf[i]
})
}
var valueline_pdf = d3.svg.line()
.x(function(d) {
return xScale_pdf(d.x);
})
.y(function(d) {
return yScale_pdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_pdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
var fill_x, fill_pdf
var xpos = 0;
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_fill))
.attr("fill", "#999999")
.attr("stroke", "None") //"#4794c1")
.attr("id", "fill")
.lower()
```
The next block essentially repeats the same procedure for the right subplot. Go through the code and see if you recognize parallels to the code above.
```html
// Define a subplot for the standard normal
const y_limits_cdf = [0, 1];
const window_x_cdf = [width * 0.585, width * 0.985];
const window_y_cdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_cdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_cdf)
const xScale_cdf_inverse = d3.scaleLinear()
.domain(window_x_cdf)
.range([x_limits[0], x_limits[1]])
const yScale_cdf = d3.scaleLinear()
.domain([y_limits_cdf[0], y_limits_cdf[1]])
.range([window_y_cdf[1], window_y_cdf[0]])
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_cdf[1].toString() + ")")
.call(d3.axisBottom(xScale_cdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_cdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-family", "arial")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_cdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_cdf))
.style("font-size", (12 * width / 600).toString() + "px");
//.call(d3.axisLeft(yScale).tickFormat(""));
svg.append("text")
.attr("transform",
"translate(" + (width * 0.525).toString() + "," + (math.mean(window_y_cdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("cumulative probability")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_cdf[1] - window_x_cdf[0])/2 + window_x_cdf[0]).toString() + "," + (window_y_cdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("cdf")
.style("font-size", (16 * width / 600).toString() + "px")
// Evaluate the normal pdf
var cdf = standard_Gaussian_cdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_cdf(0))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_cdf");
// Get the data for the path
var data_cdf = [];
for (i = 0; i < x.length; i++) {
data_cdf.push({
x: x[i],
y: cdf[i]
})
}
var valueline_cdf = d3.svg.line()
.x(function(d) {
return xScale_cdf(d.x);
})
.y(function(d) {
return yScale_cdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_cdf(data_cdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
svg.append('line')
.attr('x1', window_x_cdf[0])
.attr('x2', window_x_cdf[1])
.attr('y1', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('stroke', '#000000')
.attr("stroke-width", 3 * height / 600)
.style("stroke-dasharray", ((10 * height / 600).toString()+","+(5 * height / 600).toString()))
.attr("id","line_cdf")
.lower();
```
So far, this element is static. It's time to introduce some interactivity! This is something we add in the last block of this code. We start by defining three new variables: `movex` (x-position of the mouse), `movey` (y-position of the mouse), and `xpos` (what x-value we currently are looking at).
Make interactive elements with d3 is extremely easy. Here, for instance, we simply use the syntax `svg.on("mousemove")`, which calls a function whenever the mouse moves over the canvas. We then define an unnamed function that reads out the current coordinates of the cursor (`d3.event.x` and `d3.event.y`). For this element, we choose to only update the figure if the cursor's x-position is in one of the subplots. We can test this with a simple if-clause.
If the cursor's x-position is within the first subplot (i.e., `(movex >= window_x_pdf[0] && movex <= window_x_pdf[1]) == True`), then we convert the x-position to an x-value using the `xScale_pdf_inverse` function we created before. We then select the circular position markers for the pdf (`d3.select("#marker_pdf")`) and cdf (`d3.select("#marker_cdf")`) we created before via their IDs, and update their positions in both subplots. Likewise, we update the fill path in the pdf plot (`d3.select("#fill")`) with the new `xpos`, and update the horizontal bar in the cdf subplot (`d3.select("#line_cdf")`).
One of the nice things about these interactive JavaScript elements is that you can make the updates to your elements as complex as you want, as long as you find a way to code it. Here, we really just update the properties of pre-existing elements, but in principle you can also dynamically create new elements, destroy old elements, and so on. Finally, we close all open environments, and are done with this simple interactive element!
```html
// Shift the marker around on mouseover; restrict it to the contour
var movex, movey, xpos
svg
.on("mousemove", function() {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
if (movex >= window_x_pdf[0] && movex <= window_x_pdf[1]) {
xpos = xScale_pdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
} else if (movex >= window_x_cdf[0] && movex <= window_x_cdf[1]) {
xpos = xScale_cdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
d3.select("#fill_cdf")
.attr('y', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('height', window_y_cdf[1] - yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
}
});
</script>
</body>
</html>
```
## HTML code for the pdf/cdf element
This is the code for the element above:
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body{
margin: 0; overflow: hidden;
font-family: Helvetica, sans-serif;
}
</style>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_datavisualization">
<svg id="click" xmlns="http://www.w3.org/2000/svg">
</svg>
</div>
<body>
<script>
// Get the viewport height and width
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit to viewport
var height = vw * 0.5;
var width = vw;
// Create the canvas. We will use only part of it for the main plot
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
function linspace(start, end, resolution) {
var spacing = [];
// Go through a for-loop
var i;
for (i = 0; i < resolution; i++) {
spacing.push(start + (end - start) * i / (resolution - 1))
}
return spacing; // The function returns the linspace between p1 and p2
}
function standard_Gaussian_pdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
normalize = math.dotDivide(
1,
math.sqrt(
math.dotMultiply(
math.dotMultiply(
2,
math.PI),
math.pow(
std,
2))));
var i;
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
expon = math.exp(
-math.dotDivide(
math.pow(
x[i] - mean,
2),
math.dotMultiply(
2,
math.pow(
std,
2))))
temp = math.dotMultiply(
normalize,
expon)
result.push(temp)
}
return result
}
function standard_Gaussian_cdf(x) {
mean = 0;
std = 1;
// Create a dummy variable
var result = [];
for (i = 0; i < x.length; i++) {
// Evaluate the first element of the Gaussian mixture
temp = 0.5 * (1 + math.erf(x[i] /math.sqrt(2)))
result.push(temp)
}
return result
}
// Define a subplot for the standard normal
const x_limits = [-3, 3];
const y_limits_pdf = [0, 1 / math.sqrt(2 * math.pi) * 1.2];
const window_x_pdf = [width * 0.1, width * 0.5];
const window_y_pdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_pdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_pdf)
const xScale_pdf_inverse = d3.scaleLinear()
.domain(window_x_pdf)
.range([x_limits[0], x_limits[1]])
const yScale_pdf = d3.scaleLinear()
.domain([y_limits_pdf[0], y_limits_pdf[1]])
.range([window_y_pdf[1], window_y_pdf[0]])
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_pdf[1].toString() + ")")
.call(d3.axisBottom(xScale_pdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_pdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_pdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_pdf))
.style("font-size", (12 * width / 600).toString() + "px");
svg.append("text")
.attr("transform",
"translate(" + (width * 0.03).toString() + "," + (math.mean(window_y_pdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("probability density")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_pdf[1] - window_x_pdf[0])/2 + window_x_pdf[0]).toString() + "," + (window_y_pdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("pdf")
.style("font-size", (16 * width / 600).toString() + "px")
// Evaluate the normal pdf
var x = linspace(-3, 3, 201);
var pdf = standard_Gaussian_pdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_pdf(0))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_pdf");
// Get the data for the path
var data_pdf = [];
for (i = 0; i < x.length; i++) {
data_pdf.push({
x: x[i],
y: pdf[i]
})
}
var valueline_pdf = d3.svg.line()
.x(function(d) {
return xScale_pdf(d.x);
})
.y(function(d) {
return yScale_pdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_pdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
var fill_x, fill_pdf
var xpos = 0;
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
svg.append("path")
.attr("class", "line")
.attr("d", valueline_pdf(data_fill))
.attr("fill", "#999999")
.attr("stroke", "None") //"#4794c1")
.attr("id", "fill")
.lower()
// Define a subplot for the standard normal
const y_limits_cdf = [0, 1];
const window_x_cdf = [width * 0.585, width * 0.985];
const window_y_cdf = [height * 0.1, height * 0.89];
// Get scaling functions for the x scale and the y_scale
const xScale_cdf = d3.scaleLinear()
.domain([x_limits[0], x_limits[1]])
.range(window_x_cdf)
const xScale_cdf_inverse = d3.scaleLinear()
.domain(window_x_cdf)
.range([x_limits[0], x_limits[1]])
const yScale_cdf = d3.scaleLinear()
.domain([y_limits_cdf[0], y_limits_cdf[1]])
.range([window_y_cdf[1], window_y_cdf[0]])
// Draw the x axis
svg
.append("g")
.attr("transform", "translate(0," + window_y_cdf[1].toString() + ")")
.call(d3.axisBottom(xScale_cdf).ticks(5))
.style("font-size", (12 * width / 600).toString() + "px")
svg.append("text")
.attr("transform",
"translate(" + (math.mean(window_x_cdf)).toString() + "," + (height * 0.995).toString() + ")")
.style("text-anchor", "middle")
.text("x")
.style("font-family", "arial")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the y axis
svg
.append("g")
.attr("transform", "translate(" + (window_x_cdf[0]).toString() + ",0)")
.attr("id", "mainxaxis")
.call(d3.axisLeft(yScale_cdf))
.style("font-size", (12 * width / 600).toString() + "px");
//.call(d3.axisLeft(yScale).tickFormat(""));
svg.append("text")
.attr("transform",
"translate(" + (width * 0.525).toString() + "," + (math.mean(window_y_cdf)).toString() + ") rotate(270)")
.style("text-anchor", "middle")
.text("cumulative probability")
.style("font-size", (12 * width / 600).toString() + "px")
// Draw the subplot label
svg.append("text")
.attr("transform",
"translate(" + ((window_x_cdf[1] - window_x_cdf[0])/2 + window_x_cdf[0]).toString() + "," + (window_y_cdf[0]).toString() + ")")
.style("text-anchor", "middle")
.text("cdf")
.style("font-size", (16 * width / 600).toString() + "px")
// Evaluate the normal pdf
var cdf = standard_Gaussian_cdf(x);
svg.append("circle")
.attr("r", 10 * height / 600)
.attr("cx", xScale_cdf(0))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([0])))
.attr("fill", "#666666") // "#c3e7f9"
.attr("stroke-width", 5 * height / 600)
.attr("id", "marker_cdf");
// Get the data for the path
var data_cdf = [];
for (i = 0; i < x.length; i++) {
data_cdf.push({
x: x[i],
y: cdf[i]
})
}
var valueline_cdf = d3.svg.line()
.x(function(d) {
return xScale_cdf(d.x);
})
.y(function(d) {
return yScale_cdf(d.y);
});
svg.append("path")
.attr("class", "line")
.attr("d", valueline_cdf(data_cdf))
.attr("fill", "none")
.attr("stroke-width", 3 * height / 600)
.attr("stroke", "#666666") //"#4794c1")
.attr("id", "thatline")
svg.append('line')
.attr('x1', window_x_cdf[0])
.attr('x2', window_x_cdf[1])
.attr('y1', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([0])))
.attr('stroke', '#000000')
.attr("stroke-width", 3 * height / 600)
.style("stroke-dasharray", ((10 * height / 600).toString()+","+(5 * height / 600).toString()))
.attr("id","line_cdf")
.lower();
// Shift the marker around on mouseover; restrict it to the contour
var movex, movey, xpos
svg
.on("mousemove", function() {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
if (movex >= window_x_pdf[0] && movex <= window_x_pdf[1]) {
xpos = xScale_pdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
} else if (movex >= window_x_cdf[0] && movex <= window_x_cdf[1]) {
xpos = xScale_cdf_inverse(movex);
d3.select("#marker_pdf")
.attr("cx", xScale_pdf(xpos))
.attr("cy", yScale_pdf(standard_Gaussian_pdf([xpos])));
d3.select("#marker_cdf")
.attr("cx", xScale_cdf(xpos))
.attr("cy", yScale_cdf(standard_Gaussian_cdf([xpos])));
fill_x = linspace(x_limits[0], xpos, 201);
fill_pdf = standard_Gaussian_pdf(fill_x);
fill_x.push(xpos)
fill_x.push(x_limits[0])
fill_pdf.push(0)
fill_pdf.push(0)
var data_fill = [];
for (i = 0; i < fill_x.length; i++) {
data_fill.push({
x: fill_x[i],
y: fill_pdf[i]
})
}
d3.select("#fill")
.attr("d", valueline_pdf(data_fill))
d3.select("#fill_cdf")
.attr('y', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('height', window_y_cdf[1] - yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
d3.select("#line_cdf")
.attr('y1', yScale_cdf(standard_Gaussian_cdf([xpos])))
.attr('y2', yScale_cdf(standard_Gaussian_cdf([xpos])))
.lower();
}
});
</script>
</body>
</html>
```

View file

@ -15,7 +15,9 @@
" - JupyterQuiz: code-based implementation of multiple-choiche questions and numerical answer questions. This is a basic open course tool which requires you to write your quiz questions in Python language as a `dict` or to a `.json` file.\n",
" - H5p: TU-Delft-licensed tool to create questions (and many other interactive elements) using an [online GUI](https://tudelft.h5p.com/). These elements are embedded into the book using an iframe which requires very little coding. This elements are easier to implement than the JupyterQuiz and have more features (although proper numerical answer questions are missing). More information on this is provided in [this subchapter](h5p.md)\n",
" - Grasple-exercises: TU-Delft-licensed tool to creating math-based quiz questions. Again, these elements are embedded into the book using an iframe which requiries very little coding. These elements are very well suited to math-based problems as it allows analytical evaluation of numerical values and formulas. Furthermore, Grasple allows parameterization (not required for formative exercises). More information on this is provided in [this subchapter](grasple.md)\n",
" - Non-kernel-based widgets: You can create interactive widgets with Python which doesn't need a kernel to run online. The use of the [plotly](https://plotly.com/python/getting-started/) package is an example which allows this use.\n",
" - Non-kernel-based widgets: You can create interactive widgets with Python which doesn't need a kernel to run online:\n",
" - You can create html/javascript widgets, an example is shown in [this subchapter](adding_basic_HTML_elements.md)\n",
" - The use of the [plotly](https://plotly.com/python/getting-started/) package is an example which allows this use.\n",
"\n",
"Examples of each type is shown below based on the following problem:\n",
"\n",