diff --git a/book/_static/element_pdf_and_cdf.html b/book/_static/element_pdf_and_cdf.html new file mode 100644 index 0000000..d41fc47 --- /dev/null +++ b/book/_static/element_pdf_and_cdf.html @@ -0,0 +1,392 @@ + + + + + + + + + +
+ + +
+ + + + \ No newline at end of file diff --git a/book/_toc.yml b/book/_toc.yml index 0c50d6c..50cd248 100644 --- a/book/_toc.yml +++ b/book/_toc.yml @@ -32,20 +32,25 @@ parts: - file: basic-features/recipes.md - file: features/overview sections: - - file: features/hypothesis - file: features/remove_from_publish - - file: features/grasple.md - file: features/custom_toc.md - file: features/custom_download.md - - file: features/h5p.md - - file: features/live_code.md + - file: features/hypothesis + - file: features/interactive_element.ipynb sections: - - file: features/widgets.ipynb - - file: features/exercise_checking.ipynb - - file: features/jupyterlite/jupyterlite + - file: features/grasple.md + - file: features/h5p.md + - file: features/adding_basic_HTML_elements.md + - file: features/python_interactivity.md sections: - - file: features/jupyterlite/configuration - - file: features/jupyterlite/including-jupyterlite + - file: features/live_code.md + sections: + - file: features/widgets.ipynb + - file: features/exercise_checking.ipynb + - file: features/jupyterlite/jupyterlite + sections: + - file: features/jupyterlite/configuration + - file: features/jupyterlite/including-jupyterlite - caption: Examples diff --git a/book/features/adding_basic_HTML_elements.md b/book/features/adding_basic_HTML_elements.md new file mode 100644 index 0000000..4a4fa30 --- /dev/null +++ b/book/features/adding_basic_HTML_elements.md @@ -0,0 +1,866 @@ +# 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): + + + +If we want to embed such figures in the teachbook, you can do so via an iframe which references a html file in the _static folder: + +```html + +``` + +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 one piece at a time. Let us begin from the top. + +In the first line, we specify that this file is a HTML file. In the header (``), we can load packages we need later on. Within the ` + + + + +``` + +Next, we create a `
`. This is an HTML element that groups other HTML elements. In our case, it only contains a [SVG](https://en.wikipedia.org/wiki/SVG) named `"click"`. This SVG will serve as the canvas on which we will later place the interactive content. + +```html + +
+ + +
+``` + +So far, all of this has just been setup. The real magic begins in the HTML's ``, which contains the ` + + +``` + +## HTML code for the pdf/cdf element + +Here is the code for the element above in full: + +```html + + + + + + + + + +
+ + +
+ + + + + + +``` \ No newline at end of file diff --git a/book/features/figures/simple-city.png b/book/features/figures/simple-city.png new file mode 100644 index 0000000..6cbfd7e Binary files /dev/null and b/book/features/figures/simple-city.png differ diff --git a/book/features/interactive_element.ipynb b/book/features/interactive_element.ipynb new file mode 100644 index 0000000..b29afd2 --- /dev/null +++ b/book/features/interactive_element.ipynb @@ -0,0 +1,754 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "hideCode": true, + "hidePrompt": true + }, + "source": [ + "# Interactive elements not including Python\n", + "\n", + "To add interactivity to the book, you can make use of interactive elements which are embedded in the book. You've four options:\n", + "\n", + " - Admonition: question provided in normal markdown language with answers in a drowdown menu. There's no feedback delivered to the student, only the correct answer no matter what the student's thinking/work.\n", + " - 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:\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", + "A pre-fabricated bridge design is being considered for river crossings in a remote region of the world, as shown in Figure 1. Cities 1, 2 and 3 are labelled C1, C2 and C3, with bridges labelled B1-B4.\n", + "\n", + "```{figure} ./figures/simple-city.png\n", + "---\n", + "width: 600px\n", + "name: exercise-simple-city\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "hideCode": true, + "hidePrompt": true + }, + "source": [ + "## Admonition\n", + "If the probability of failure for an individual bridge is 0.1 per year, compute the probability that you cannot drive from City 1 to City 2\n", + "\n", + "\n", + "```{admonition} Answer\n", + ":class: tip, dropdown\n", + "\n", + "$$\n", + " P_{f_{C1-C2}} = P_{f_{B1}} * P_{f_{B4-B3-B2}} = P_{f_{B1}} * (1 - (1 - P_{f_{B4}})(1 - P_{f_{B3}}*P_{f_{B2}}))\n", + "$$\n", + "$$\n", + " P_{f_{C1 - C2}} = 0.1 * (1 - (1 - 0.1)(1 - 0.1 * 0.1)) = 0.0109\n", + "$$\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "hideCode": true, + "hidePrompt": true + }, + "source": [ + "## JupyterQuiz" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "hideCode": true, + "hidePrompt": true, + "tags": [ + "disable-execution-page", + "disable-download-page", + "remove-input" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "var questionsabVDrvSsNeVg=[{\"question\": \"If the probability of failure for an individual bridge is 0.1 per year, compute the probability that you cannot drive from City 1 to City 2:\", \"type\": \"numeric\", \"answers\": [{\"type\": \"range\", \"value\": 0.0109, \"correct\": true, \"feedback\": \"Correct.\"}, {\"type\": \"default\", \"feedback\": \"Try again\"}]}];\n // Make a random ID\nfunction makeid(length) {\n var result = [];\n var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n var charactersLength = characters.length;\n for (var i = 0; i < length; i++) {\n result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n }\n return result.join('');\n}\n\n// Choose a random subset of an array. Can also be used to shuffle the array\nfunction getRandomSubarray(arr, size) {\n var shuffled = arr.slice(0), i = arr.length, temp, index;\n while (i--) {\n index = Math.floor((i + 1) * Math.random());\n temp = shuffled[index];\n shuffled[index] = shuffled[i];\n shuffled[i] = temp;\n }\n return shuffled.slice(0, size);\n}\n\nfunction printResponses(responsesContainer) {\n var responses=JSON.parse(responsesContainer.dataset.responses);\n var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
  1. Copy the text in this cell below \"Answer String\"
  2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
  3. Select the whole \"Replace Me\" text
  4. Paste in your answer string and press shift-Enter.
  5. Save the notebook using the save icon or File->Save Notebook menu item



  6. Answer String:
    ';\n console.log(responses);\n responses.forEach((response, index) => {\n if (response) {\n console.log(index + ': ' + response);\n stringResponses+= index + ': ' + response +\"
    \";\n }\n });\n responsesContainer.innerHTML=stringResponses;\n}\nfunction check_mc() {\n var id = this.id.split('-')[0];\n //var response = this.id.split('-')[1];\n //console.log(response);\n //console.log(\"In check_mc(), id=\"+id);\n //console.log(event.srcElement.id) \n //console.log(event.srcElement.dataset.correct) \n //console.log(event.srcElement.dataset.feedback)\n\n var label = event.srcElement;\n //console.log(label, label.nodeName);\n var depth = 0;\n while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n label = label.parentElement;\n console.log(depth, label);\n depth++;\n }\n\n\n\n var answers = label.parentElement.children;\n\n //console.log(answers);\n\n\n // Split behavior based on multiple choice vs many choice:\n var fb = document.getElementById(\"fb\" + id);\n\n\n\n\n if (fb.dataset.numcorrect == 1) {\n // What follows is for the saved responses stuff\n var outerContainer = fb.parentElement.parentElement;\n var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n if (responsesContainer) {\n //console.log(responsesContainer);\n var response = label.firstChild.innerText;\n if (label.querySelector(\".QuizCode\")){\n response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n }\n console.log(response);\n //console.log(document.getElementById(\"quizWrap\"+id));\n var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n console.log(\"Question \" + qnum);\n //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n var responses=JSON.parse(responsesContainer.dataset.responses);\n console.log(responses);\n responses[qnum]= response;\n responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n printResponses(responsesContainer);\n }\n // End code to preserve responses\n \n for (var i = 0; i < answers.length; i++) {\n var child = answers[i];\n //console.log(child);\n child.className = \"MCButton\";\n }\n\n\n\n if (label.dataset.correct == \"true\") {\n // console.log(\"Correct action\");\n if (\"feedback\" in label.dataset) {\n fb.textContent = jaxify(label.dataset.feedback);\n } else {\n fb.textContent = \"Correct!\";\n }\n label.classList.add(\"correctButton\");\n\n fb.className = \"Feedback\";\n fb.classList.add(\"correct\");\n\n } else {\n if (\"feedback\" in label.dataset) {\n fb.textContent = jaxify(label.dataset.feedback);\n } else {\n fb.textContent = \"Incorrect -- try again.\";\n }\n //console.log(\"Error action\");\n label.classList.add(\"incorrectButton\");\n fb.className = \"Feedback\";\n fb.classList.add(\"incorrect\");\n }\n }\n else {\n var reset = false;\n var feedback;\n if (label.dataset.correct == \"true\") {\n if (\"feedback\" in label.dataset) {\n feedback = jaxify(label.dataset.feedback);\n } else {\n feedback = \"Correct!\";\n }\n if (label.dataset.answered <= 0) {\n if (fb.dataset.answeredcorrect < 0) {\n fb.dataset.answeredcorrect = 1;\n reset = true;\n } else {\n fb.dataset.answeredcorrect++;\n }\n if (reset) {\n for (var i = 0; i < answers.length; i++) {\n var child = answers[i];\n child.className = \"MCButton\";\n child.dataset.answered = 0;\n }\n }\n label.classList.add(\"correctButton\");\n label.dataset.answered = 1;\n fb.className = \"Feedback\";\n fb.classList.add(\"correct\");\n\n }\n } else {\n if (\"feedback\" in label.dataset) {\n feedback = jaxify(label.dataset.feedback);\n } else {\n feedback = \"Incorrect -- try again.\";\n }\n if (fb.dataset.answeredcorrect > 0) {\n fb.dataset.answeredcorrect = -1;\n reset = true;\n } else {\n fb.dataset.answeredcorrect--;\n }\n\n if (reset) {\n for (var i = 0; i < answers.length; i++) {\n var child = answers[i];\n child.className = \"MCButton\";\n child.dataset.answered = 0;\n }\n }\n label.classList.add(\"incorrectButton\");\n fb.className = \"Feedback\";\n fb.classList.add(\"incorrect\");\n }\n // What follows is for the saved responses stuff\n var outerContainer = fb.parentElement.parentElement;\n var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n if (responsesContainer) {\n //console.log(responsesContainer);\n var response = label.firstChild.innerText;\n if (label.querySelector(\".QuizCode\")){\n response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n }\n console.log(response);\n //console.log(document.getElementById(\"quizWrap\"+id));\n var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n console.log(\"Question \" + qnum);\n //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n var responses=JSON.parse(responsesContainer.dataset.responses);\n if (label.dataset.correct == \"true\") {\n if (typeof(responses[qnum]) == \"object\"){\n if (!responses[qnum].includes(response))\n responses[qnum].push(response);\n } else{\n responses[qnum]= [ response ];\n }\n } else {\n responses[qnum]= response;\n }\n console.log(responses);\n responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n printResponses(responsesContainer);\n }\n // End save responses stuff\n\n\n\n var numcorrect = fb.dataset.numcorrect;\n var answeredcorrect = fb.dataset.answeredcorrect;\n if (answeredcorrect >= 0) {\n fb.textContent = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n } else {\n fb.textContent = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n }\n\n\n }\n\n if (typeof MathJax != 'undefined') {\n var version = MathJax.version;\n console.log('MathJax version', version);\n if (version[0] == \"2\") {\n MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n } else if (version[0] == \"3\") {\n MathJax.typeset([fb]);\n }\n } else {\n console.log('MathJax not detected');\n }\n\n}\n\nfunction make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n var shuffled;\n if (shuffle_answers == \"True\") {\n //console.log(shuffle_answers+\" read as true\");\n shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n } else {\n //console.log(shuffle_answers+\" read as false\");\n shuffled = qa.answers;\n }\n\n\n var num_correct = 0;\n\n\n\n shuffled.forEach((item, index, ans_array) => {\n //console.log(answer);\n\n // Make input element\n var inp = document.createElement(\"input\");\n inp.type = \"radio\";\n inp.id = \"quizo\" + id + index;\n inp.style = \"display:none;\";\n aDiv.append(inp);\n\n //Make label for input element\n var lab = document.createElement(\"label\");\n lab.className = \"MCButton\";\n lab.id = id + '-' + index;\n lab.onclick = check_mc;\n var aSpan = document.createElement('span');\n aSpan.classsName = \"\";\n //qDiv.id=\"quizQn\"+id+index;\n if (\"answer\" in item) {\n aSpan.innerHTML = jaxify(item.answer);\n //aSpan.innerHTML=item.answer;\n }\n lab.append(aSpan);\n\n // Create div for code inside question\n var codeSpan;\n if (\"code\" in item) {\n codeSpan = document.createElement('span');\n codeSpan.id = \"code\" + id + index;\n codeSpan.className = \"QuizCode\";\n var codePre = document.createElement('pre');\n codeSpan.append(codePre);\n var codeCode = document.createElement('code');\n codePre.append(codeCode);\n codeCode.innerHTML = item.code;\n lab.append(codeSpan);\n //console.log(codeSpan);\n }\n\n //lab.textContent=item.answer;\n\n // Set the data attributes for the answer\n lab.setAttribute('data-correct', item.correct);\n if (item.correct) {\n num_correct++;\n }\n if (\"feedback\" in item) {\n lab.setAttribute('data-feedback', item.feedback);\n }\n lab.setAttribute('data-answered', 0);\n\n aDiv.append(lab);\n\n });\n\n if (num_correct > 1) {\n outerqDiv.className = \"ManyChoiceQn\";\n } else {\n outerqDiv.className = \"MultipleChoiceQn\";\n }\n\n return num_correct;\n\n}\nfunction check_numeric(ths, event) {\n\n if (event.keyCode === 13) {\n ths.blur();\n\n var id = ths.id.split('-')[0];\n\n var submission = ths.value;\n if (submission.indexOf('/') != -1) {\n var sub_parts = submission.split('/');\n //console.log(sub_parts);\n submission = sub_parts[0] / sub_parts[1];\n }\n //console.log(\"Reader entered\", submission);\n\n if (\"precision\" in ths.dataset) {\n var precision = ths.dataset.precision;\n // console.log(\"1:\", submission)\n submission = Math.round((1 * submission + Number.EPSILON) * 10 ** precision) / 10 ** precision;\n // console.log(\"Rounded to \", submission, \" precision=\", precision );\n }\n\n\n //console.log(\"In check_numeric(), id=\"+id);\n //console.log(event.srcElement.id) \n //console.log(event.srcElement.dataset.feedback)\n\n var fb = document.getElementById(\"fb\" + id);\n fb.style.display = \"none\";\n fb.textContent = \"Incorrect -- try again.\";\n\n var answers = JSON.parse(ths.dataset.answers);\n //console.log(answers);\n\n var defaultFB = \"\";\n var correct;\n var done = false;\n answers.every(answer => {\n //console.log(answer.type);\n\n correct = false;\n // if (answer.type==\"value\"){\n if ('value' in answer) {\n if (submission == answer.value) {\n if (\"feedback\" in answer) {\n fb.textContent = jaxify(answer.feedback);\n } else {\n fb.textContent = jaxify(\"Correct\");\n }\n correct = answer.correct;\n //console.log(answer.correct);\n done = true;\n }\n // } else if (answer.type==\"range\") {\n } else if ('range' in answer) {\n //console.log(answer.range);\n if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n fb.textContent = jaxify(answer.feedback);\n correct = answer.correct;\n //console.log(answer.correct);\n done = true;\n }\n } else if (answer.type == \"default\") {\n defaultFB = answer.feedback;\n }\n if (done) {\n return false; // Break out of loop if this has been marked correct\n } else {\n return true; // Keep looking for case that includes this as a correct answer\n }\n });\n\n if ((!done) && (defaultFB != \"\")) {\n fb.innerHTML = jaxify(defaultFB);\n //console.log(\"Default feedback\", defaultFB);\n }\n\n fb.style.display = \"block\";\n if (correct) {\n ths.className = \"Input-text\";\n ths.classList.add(\"correctButton\");\n fb.className = \"Feedback\";\n fb.classList.add(\"correct\");\n } else {\n ths.className = \"Input-text\";\n ths.classList.add(\"incorrectButton\");\n fb.className = \"Feedback\";\n fb.classList.add(\"incorrect\");\n }\n\n // What follows is for the saved responses stuff\n var outerContainer = fb.parentElement.parentElement;\n var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n if (responsesContainer) {\n console.log(submission);\n var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n //console.log(\"Question \" + qnum);\n //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n var responses=JSON.parse(responsesContainer.dataset.responses);\n console.log(responses);\n if (submission == ths.value){\n responses[qnum]= submission;\n } else {\n responses[qnum]= ths.value + \"(\" + submission +\")\";\n }\n responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n printResponses(responsesContainer);\n }\n // End code to preserve responses\n\n if (typeof MathJax != 'undefined') {\n var version = MathJax.version;\n console.log('MathJax version', version);\n if (version[0] == \"2\") {\n MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n } else if (version[0] == \"3\") {\n MathJax.typeset([fb]);\n }\n } else {\n console.log('MathJax not detected');\n }\n return false;\n }\n\n}\n\nfunction isValid(el, charC) {\n //console.log(\"Input char: \", charC);\n if (charC == 46) {\n if (el.value.indexOf('.') === -1) {\n return true;\n } else if (el.value.indexOf('/') != -1) {\n var parts = el.value.split('/');\n if (parts[1].indexOf('.') === -1) {\n return true;\n }\n }\n else {\n return false;\n }\n } else if (charC == 47) {\n if (el.value.indexOf('/') === -1) {\n if ((el.value != \"\") && (el.value != \".\")) {\n return true;\n } else {\n return false;\n }\n } else {\n return false;\n }\n } else if (charC == 45) {\n var edex = el.value.indexOf('e');\n if (edex == -1) {\n edex = el.value.indexOf('E');\n }\n\n if (el.value == \"\") {\n return true;\n } else if (edex == (el.value.length - 1)) { // If just after e or E\n return true;\n } else {\n return false;\n }\n } else if (charC == 101) { // \"e\"\n if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n // Prev symbol must be digit or decimal point:\n if (el.value.slice(-1).search(/\\d/) >= 0) {\n return true;\n } else if (el.value.slice(-1).search(/\\./) >= 0) {\n return true;\n } else {\n return false;\n }\n } else {\n return false;\n }\n } else {\n if (charC > 31 && (charC < 48 || charC > 57))\n return false;\n }\n return true;\n}\n\nfunction numeric_keypress(evnt) {\n var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n\n if (charC == 13) {\n check_numeric(this, evnt);\n } else {\n return isValid(this, charC);\n }\n}\n\n\n\n\n\nfunction make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n\n\n\n //console.log(answer);\n\n\n outerqDiv.className = \"NumericQn\";\n aDiv.style.display = 'block';\n\n var lab = document.createElement(\"label\");\n lab.className = \"InpLabel\";\n lab.textContent = \"Type numeric answer here:\";\n aDiv.append(lab);\n\n var inp = document.createElement(\"input\");\n inp.type = \"text\";\n //inp.id=\"input-\"+id;\n inp.id = id + \"-0\";\n inp.className = \"Input-text\";\n inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n if (\"precision\" in qa) {\n inp.setAttribute('data-precision', qa.precision);\n }\n aDiv.append(inp);\n //console.log(inp);\n\n //inp.addEventListener(\"keypress\", check_numeric);\n //inp.addEventListener(\"keypress\", numeric_keypress);\n /*\n inp.addEventListener(\"keypress\", function(event) {\n return numeric_keypress(this, event);\n }\n );\n */\n //inp.onkeypress=\"return numeric_keypress(this, event)\";\n inp.onkeypress = numeric_keypress;\n inp.onpaste = event => false;\n\n inp.addEventListener(\"focus\", function (event) {\n this.value = \"\";\n return false;\n }\n );\n\n\n}\nfunction jaxify(string) {\n var mystring = string;\n\n var count = 0;\n var loc = mystring.search(/([^\\\\]|^)(\\$)/);\n\n var count2 = 0;\n var loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n\n //console.log(loc);\n\n while ((loc >= 0) || (loc2 >= 0)) {\n\n /* Have to replace all the double $$ first with current implementation */\n if (loc2 >= 0) {\n if (count2 % 2 == 0) {\n mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\[\");\n } else {\n mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, \"$1\\\\]\");\n }\n count2++;\n } else {\n if (count % 2 == 0) {\n mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\(\");\n } else {\n mystring = mystring.replace(/([^\\\\]|^)(\\$)/, \"$1\\\\)\");\n }\n count++;\n }\n loc = mystring.search(/([^\\\\]|^)(\\$)/);\n loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n //console.log(mystring,\", loc:\",loc,\", loc2:\",loc2);\n }\n\n //console.log(mystring);\n return mystring;\n}\n\n\nfunction show_questions(json, mydiv) {\n console.log('show_questions');\n //var mydiv=document.getElementById(myid);\n var shuffle_questions = mydiv.dataset.shufflequestions;\n var num_questions = mydiv.dataset.numquestions;\n var shuffle_answers = mydiv.dataset.shuffleanswers;\n var max_width = mydiv.dataset.maxwidth;\n\n if (num_questions > json.length) {\n num_questions = json.length;\n }\n\n var questions;\n if ((num_questions < json.length) || (shuffle_questions == \"True\")) {\n //console.log(num_questions+\",\"+json.length);\n questions = getRandomSubarray(json, num_questions);\n } else {\n questions = json;\n }\n\n //console.log(\"SQ: \"+shuffle_questions+\", NQ: \" + num_questions + \", SA: \", shuffle_answers);\n\n // Iterate over questions\n questions.forEach((qa, index, array) => {\n //console.log(qa.question); \n\n var id = makeid(8);\n //console.log(id);\n\n\n // Create Div to contain question and answers\n var iDiv = document.createElement('div');\n //iDiv.id = 'quizWrap' + id + index;\n iDiv.id = 'quizWrap' + id;\n iDiv.className = 'Quiz';\n iDiv.setAttribute('data-qnum', index);\n iDiv.style.maxWidth =max_width+\"px\";\n mydiv.appendChild(iDiv);\n // iDiv.innerHTML=qa.question;\n \n var outerqDiv = document.createElement('div');\n outerqDiv.id = \"OuterquizQn\" + id + index;\n // Create div to contain question part\n var qDiv = document.createElement('div');\n qDiv.id = \"quizQn\" + id + index;\n \n if (qa.question) {\n iDiv.append(outerqDiv);\n\n //qDiv.textContent=qa.question;\n qDiv.innerHTML = jaxify(qa.question);\n outerqDiv.append(qDiv);\n }\n\n // Create div for code inside question\n var codeDiv;\n if (\"code\" in qa) {\n codeDiv = document.createElement('div');\n codeDiv.id = \"code\" + id + index;\n codeDiv.className = \"QuizCode\";\n var codePre = document.createElement('pre');\n codeDiv.append(codePre);\n var codeCode = document.createElement('code');\n codePre.append(codeCode);\n codeCode.innerHTML = qa.code;\n outerqDiv.append(codeDiv);\n //console.log(codeDiv);\n }\n\n\n // Create div to contain answer part\n var aDiv = document.createElement('div');\n aDiv.id = \"quizAns\" + id + index;\n aDiv.className = 'Answer';\n iDiv.append(aDiv);\n\n //console.log(qa.type);\n\n var num_correct;\n if ((qa.type == \"multiple_choice\") || (qa.type == \"many_choice\") ) {\n num_correct = make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id);\n if (\"answer_cols\" in qa) {\n //aDiv.style.gridTemplateColumns = 'auto '.repeat(qa.answer_cols);\n aDiv.style.gridTemplateColumns = 'repeat(' + qa.answer_cols + ', 1fr)';\n }\n } else if (qa.type == \"numeric\") {\n //console.log(\"numeric\");\n make_numeric(qa, outerqDiv, qDiv, aDiv, id);\n }\n\n\n //Make div for feedback\n var fb = document.createElement(\"div\");\n fb.id = \"fb\" + id;\n //fb.style=\"font-size: 20px;text-align:center;\";\n fb.className = \"Feedback\";\n fb.setAttribute(\"data-answeredcorrect\", 0);\n fb.setAttribute(\"data-numcorrect\", num_correct);\n iDiv.append(fb);\n\n\n });\n var preserveResponses = mydiv.dataset.preserveresponses;\n console.log(preserveResponses);\n console.log(preserveResponses == \"true\");\n if (preserveResponses == \"true\") {\n console.log(preserveResponses);\n // Create Div to contain record of answers\n var iDiv = document.createElement('div');\n iDiv.id = 'responses' + mydiv.id;\n iDiv.className = 'JCResponses';\n // Create a place to store responses as an empty array\n iDiv.setAttribute('data-responses', '[]');\n\n // Dummy Text\n iDiv.innerHTML=\"Select your answers and then follow the directions that will appear here.\"\n //iDiv.className = 'Quiz';\n mydiv.appendChild(iDiv);\n }\n//console.log(\"At end of show_questions\");\n if (typeof MathJax != 'undefined') {\n console.log(\"MathJax version\", MathJax.version);\n var version = MathJax.version;\n setTimeout(function(){\n var version = MathJax.version;\n console.log('After sleep, MathJax version', version);\n if (version[0] == \"2\") {\n MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n } else if (version[0] == \"3\") {\n MathJax.typeset([mydiv]);\n }\n }, 500);\nif (typeof version == 'undefined') {\n } else\n {\n if (version[0] == \"2\") {\n MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n } else if (version[0] == \"3\") {\n MathJax.typeset([mydiv]);\n } else {\n console.log(\"MathJax not found\");\n }\n }\n }\n return false;\n}\n/* This is to handle asynchrony issues in loading Jupyter notebooks\n where the quiz has been previously run. The Javascript was generally\n being run before the div was added to the DOM. I tried to do this\n more elegantly using Mutation Observer, but I didn't get it to work.\n\n Someone more knowledgeable could make this better ;-) */\n\n function try_show() {\n if(document.getElementById(\"abVDrvSsNeVg\")) {\n show_questions(questionsabVDrvSsNeVg, abVDrvSsNeVg); \n } else {\n setTimeout(try_show, 200);\n }\n };\n \n {\n // console.log(element);\n\n //console.log(\"abVDrvSsNeVg\");\n // console.log(document.getElementById(\"abVDrvSsNeVg\"));\n\n try_show();\n }\n ", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import json\n", + "from jupyterquiz import display_quiz\n", + "with open(\"jupyterquizexample.json\", \"r\") as file:\n", + " questions=json.load(file)\n", + "\n", + "display_quiz(questions, border_radius=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "hideCode": true, + "hidePrompt": true + }, + "source": [ + "## H5p\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "hideCode": true, + "hidePrompt": true + }, + "source": [ + "## Grasple\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interactive widget without kernel\n", + "\n", + "The example below is taken from the FEM-module by Frans van der Meer.\n", + "When using Plotly graphs, make sure the notebook is executed in Jupyter Lab / Jupyter Notebook for the figures to be shown in the book. Running the code in VS code might break the result in the book, although the output is visible in VS code. Alternatively, add the following lines of code to your notebook to have a valid output in the book as well:\n", + "\n", + "```python\n", + "import plotly.io as pio\n", + "pio.renderers.default = 'notebook'\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "%matplotlib widget\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import plotly.subplots as sp\n", + "import plotly.graph_objects as go\n", + "import plotly.io as pio\n", + "pio.renderers.default = 'notebook'" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6241088f78f046be86f2e814448e8790", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAIyUlEQVR4nO3WMQEAIAzAMMC/5+ECjiYKenbPzCwAADLO7wAAAN4ygAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIMIABAjAEEAIgxgAAAMQYQACDGAAIAxBhAAIAYAwgAEGMAAQBiDCAAQIwBBACIMYAAADEGEAAgxgACAMQYQACAGAMIABBjAAEAYgwgAECMAQQAiDGAAAAxBhAAIMYAAgDEGEAAgBgDCAAQYwABAGIuJnkHvKensmIAAAAASUVORK5CYII=", + "text/html": [ + "\n", + "
    \n", + "
    \n", + " Figure\n", + "
    \n", + " \n", + "
    \n", + " " + ], + "text/plain": [ + "Canvas(toolbar=None)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "plt.rcParams.update({'font.size': 12, 'lines.linewidth': 2})\n", + "\n", + "def shape_functions(total_length, num_nodes, node_x):\n", + " l = total_length / (num_nodes - 1)\n", + "\n", + " if node_x == 0:\n", + " x = np.array([node_x, node_x + l])\n", + " y = np.array([1.0, 0.0])\n", + " elif node_x == total_length:\n", + " x = np.array([node_x - l, node_x])\n", + " y = np.array([0.0, 1.0])\n", + " else:\n", + " x = np.array([node_x - l, node_x, node_x + l])\n", + " y = np.array([0.0, 1.0, 0.0])\n", + "\n", + " return x, y\n", + "\n", + "def shape_functions_for_plot(num_nodes, beam_length = 15):\n", + "\n", + " x_positions = np.linspace(0, beam_length, num_nodes)\n", + "\n", + " data = {}\n", + "\n", + " for node_num, x_coords in enumerate(x_positions):\n", + "\n", + " x, y = shape_functions(beam_length, num_nodes, x_coords)\n", + " data[node_num + 1] = {'x': x, 'y': y}\n", + "\n", + " return data" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "def linear_lagrange_shape_function(x, nodes_x_values, nodes_displacements):\n", + " \"\"\"\n", + " Compute the linear Lagrange shape function for a given x.\n", + " Args:\n", + " x (float): The x-coordinate.\n", + " x0 (float): Left node position.\n", + " x1 (float): Right node position.\n", + " Returns:\n", + " float: The shape function value.\n", + " \"\"\"\n", + "\n", + " ratio = (x - nodes_x_values[0]) / (nodes_x_values[1] - nodes_x_values[0])\n", + "\n", + " return (1 - ratio) * nodes_displacements[0] + ratio * nodes_displacements[1]\n", + "\n", + "def function(x):\n", + " \"\"\"\n", + " Define the function to be plotted.\n", + " Args:\n", + " x (numpy.ndarray): Input values.\n", + " Returns:\n", + " numpy.ndarray: Corresponding function values.\n", + " \"\"\"\n", + " return np.sin(x) * 0.14*x\n", + "\n", + "def shape_functions_applied(num_elements):\n", + " \"\"\"\n", + " Plot linear Lagrange shape functions on a beam.\n", + " Args:\n", + " num_elements (int): Number of elements (segments).\n", + " \"\"\"\n", + "\n", + " beam_length = 15\n", + "\n", + " num_nodes = num_elements + 1\n", + " x_at_nodes = np.linspace(0, beam_length, num_nodes)\n", + "\n", + " y_at_nodes = function(x_at_nodes)\n", + "\n", + " x_data = np.linspace(0, beam_length, 1000)\n", + "\n", + " # Use NumPy to compute y_vals efficiently\n", + " y_vals = np.zeros_like(x_data)\n", + "\n", + " for element_number in range(num_elements):\n", + " mask = (x_at_nodes[element_number] <= x_data) & (x_data <= x_at_nodes[element_number + 1])\n", + " y_vals[mask] = linear_lagrange_shape_function(x_data[mask], x_at_nodes[element_number:element_number + 2], y_at_nodes[element_number:element_number + 2])\n", + "\n", + " return x_data, function(x_data), y_vals\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "tags": [ + "remove-input" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Colors\n", + "grey = '#eeeeee' # light grey fill\n", + "\n", + "# Prepare all the graphs that are to be plotted\n", + "range_elements = np.concatenate((np.arange(1, 7, 1), np.arange(7, 35, 4), np.arange(35, 55, 8)))\n", + "\n", + "# Create subplots\n", + "fig = sp.make_subplots(rows=1, cols=2, subplot_titles=(\"Shape functions\", \"Exact and approximated solution\"))\n", + "\n", + "base_traces_left = 2 # Number of traces in the left plot that are always visible\n", + "base_traces_right = 0 # Number of traces in the right plot that are always visible\n", + "\n", + "# Add traces to the subplots\n", + "x_data, y_data, y_vals = shape_functions_applied(10)\n", + "\n", + "# Add left plot traces (Cumulative Influx and Cumulative Outflux)\n", + "fig.add_trace(\n", + " go.Scatter(\n", + " visible=True, # Show for the first value of a\n", + " x=x_data,\n", + " y=y_data,\n", + " # line=dict(color='#6a8ba4'),\n", + " mode='lines',\n", + " name='Exact solution'\n", + " ),\n", + " row=1,\n", + " col=2\n", + ")\n", + "\n", + "\n", + "# Add traces to the subplots\n", + "for a in range_elements:\n", + "\n", + " x_data, y_data, y_vals = shape_functions_applied(a)\n", + "\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " visible = True if a == range_elements[0] else False, # Show for the first value of a\n", + " x=x_data,\n", + " y=y_vals,\n", + " # line=dict(color='#FFA500'),\n", + " mode='lines',\n", + " name='Numerical solution'\n", + " ),\n", + " row=1,\n", + " col=2\n", + " )\n", + "\n", + "\n", + "# Add traces to the subplots\n", + "for a in range_elements:\n", + "\n", + " data = shape_functions_for_plot(a + 1)\n", + "\n", + " for i in range(1, a + 2):\n", + "\n", + " info = data[i]\n", + " x, y = info['x'], info['y']\n", + "\n", + " # Add right plot traces (Cumulative Outflux for each 'a' value)\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " visible = True if a == range_elements[0] else False,\n", + " x=x,\n", + " y=y,\n", + " mode='lines',\n", + " name=f'Node shape for node {i}'\n", + " ),\n", + " row=1,\n", + " col=1\n", + " )\n", + "\n", + "# Create and add slider\n", + "steps = []\n", + "for i in range(0, range_elements.shape[0]):\n", + "\n", + " a = range_elements[i]\n", + " visarray = [False] * len(fig.data)\n", + "\n", + " visarray[0] = True\n", + " visarray[i + 1] = True\n", + "\n", + " start_of_second_plot = 1 + len(range_elements)\n", + "\n", + " if i==0:\n", + " start_index=start_of_second_plot\n", + " else:\n", + " start_index = start_of_second_plot + np.sum(range_elements[0:i]) + i\n", + "\n", + " num_lines = range_elements[i] + 1\n", + " end_index = start_index + num_lines\n", + "\n", + " visarray[start_index:end_index] = [True] * (end_index - start_index)\n", + " \n", + " step = dict(\n", + " method=\"update\",\n", + " args=[{\"visible\": visarray}],\n", + " label=str(round(range_elements[i], 1))\n", + " )\n", + " steps.append(step)\n", + "\n", + "sliders = [dict(\n", + " active=0, # Show the first value of 'a' initially\n", + " currentvalue={\"prefix\": r\"Number of elements: \"},\n", + " steps=steps\n", + ")]\n", + "\n", + "fig.update_layout(\n", + " sliders=sliders,\n", + " title=\"Applying shape functions\",\n", + " legend_title=\"Legend\",\n", + " legend=dict(\n", + " x=1.05, # Adjust the legend position\n", + " y=0.5\n", + " )\n", + ")\n", + "\n", + "fig.update_xaxes(title_text='x', row=1, col=1, range=[x_data[0], x_data[-1]])\n", + "fig.update_xaxes(title_text='x', row=1, col=2, range=[x_data[0], x_data[-1]])\n", + "\n", + "# Update y-axis properties\n", + "fig.update_yaxes(title_text='Node shape value', row=1, col=1, range=[-0.1, 1.1])\n", + "fig.update_yaxes(title_text='y', row=1, col=2, range=[-2.1, 2.1])\n", + "\n", + "fig.show()" + ] + } + ], + "metadata": { + "celltoolbar": "Tags", + "hide_code_all_hidden": true, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/book/features/jupyterquizexample.json b/book/features/jupyterquizexample.json new file mode 100644 index 0000000..51e7c86 --- /dev/null +++ b/book/features/jupyterquizexample.json @@ -0,0 +1,18 @@ +[ + { + "question": "If the probability of failure for an individual bridge is 0.1 per year, compute the probability that you cannot drive from City 1 to City 2:", + "type": "numeric", + "answers": [ + { + "type": "range", + "value": 0.0109, + "correct": true, + "feedback": "Correct." + }, + { + "type": "default", + "feedback": "Try again" + } + ] +} +] \ No newline at end of file diff --git a/book/features/live_code.ipynb b/book/features/live_code.ipynb new file mode 100644 index 0000000..eef495b --- /dev/null +++ b/book/features/live_code.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Python-enabled interactivity\n", + "\n", + "Our book has been enable to run Python code live in the browser (thanks Max!). This page contains some installation instructions and the other sections show how to use this functionality to create interactive figures and feedback on code\n", + "\n", + "To see this happening, click {fa}`rocket` --> {guilabel}`Live Code` on the top right corner of this page" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "thebe-init" + ] + }, + "outputs": [], + "source": [ + "print('hello world')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up Python live coding\n", + "To set up the Python live coding you need to add our [own sphinx-thebe extension](https://gitlab.tudelft.nl/interactivetextbooks-citg/extensions/sphinx-thebe) to your book. This extensions doesn't rely on a 3rd party like Binder and it supports local python execution and custom features. Therefore, you need to add some lines to `requirements.txt` and `_config.yml`\n", + "\n", + "For `requirements.txt` add the following lines:\n", + "```\n", + "--extra-index-url https://gitlab.tudelft.nl/api/v4/projects/11239/packages/pypi/simple\n", + "sphinx-thebe ~= 0.9.9\n", + "```\n", + "This will download the correct version of the sphinx extension when the book is build on the server (which loads the required packages from `requirements.txt`)\n", + "\n", + "Afterwards, this sphinx extension needs to be enabled in your book. This can be done by adding the following lines to `_config.yml`:\n", + "\n", + "```\n", + "launch_buttons:\n", + " thebe: true\n", + "\n", + "sphinx:\n", + " config:\n", + " html_js_files:\n", + " - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js\n", + " thebe_config:\n", + " use_thebe_lite: true\n", + " exclude_patterns: [\"**/_*.yml\", \"**/*.md\", \"**/*.ipynb\"]\n", + "```\n", + "\n", + "The `html_js_files` link calls for some required javascript files. `use_thebe_lite` makes sure you initiate our adapted sphinx thebe extensions. The `exclude_patterns` makes sure you import all files so that they can be accessed from your code, except for the files matching the patterns shown. By `launch_buttons` you initiate the {fa}`rocket` --> {guilabel}`Live Code` on the top right corner of every page generated from a `.ipynb` file. \n", + "\n", + "Note, if you're messing around with `html_theme_options` (for example when adding more buttons), all button behaviour is affected. In that case, as opposed to the jupyter book documentation, all buttons have to be specified from within `sphinx: html_theme_options` where you might also specify other buttons as well (ie. the [code repository button](https://jupyterbook.org/en/stable/basics/repository.html)).\n", + "\n", + "```\n", + "sphinx:\n", + " config:\n", + " html_js_files:\n", + " - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js\n", + " thebe_config:\n", + " use_thebe_lite: true\n", + " exclude_patterns: [\"**/_*.yml\", \"**/*.md\", \"**/*.ipynb\"]\n", + " html_theme_options:\n", + " repository_url: \"hello!\"\n", + " use_repository_button: true\n", + " launch_buttons:\n", + " thebe: true\n", + "```\n", + "\n", + "## Instructions: local build\n", + "\n", + "To test the fuctionality, you have to run a local server, otherwise the interactivity doens't work. So, you cannot just open the `index.html` file any more. This can be done with the following steps:\n", + "1. build the book by running ` jupyter book build `. If you are using Windows you should use the Git Bash terminal. Set this as your default in VS Code, then open it using \"New Terminal.\" If you have not already done so, you will need to allow Git Bash to use your Python libraries by running the command `conda init bash`.\n", + "2. Set up a server by running `python -m http.server 8000`.\n", + "3. Open the book by going to [http://localhost:8000/](http://localhost:8000/) in your browser. You may need to reload the page once the rebuild is complete. You can do this on Chrome by right-clicking somewhere on the page, select \"Inspect\", open the \"Network\" tab, then reload with `CTRL+R`. Or try `ctrl`+`F5`\n", + "4. Go to any page that is created from a Jupyter notebook\n", + "\n", + "If you push to main, you can test the interactivity on the book-draft website as well.\n", + "\n", + "## Custom cell tags\n", + "With the extended functionality of live code, additional cell tags have been developed to be added to cells in any .ipynb file, next to the [existing tags] of the jupyter book (https://jupyterbook.org/en/stable/content/metadata.html):\n", + "- `disable-execution-cell` disables the ability to execute the cell when thebe is activated. This might be useful if you notebook file includes interactivity for only a part of the coding cells\n", + "- `disable-execution-page` disables the ability to execute the entire page. This might be useful if you use python code for creating a page, but the code is not part of the actual content of the book.\n", + "- `auto-execute-page` automatically starts the thebe live coding functionality whenever the page is opened. This is particularly useful if you're using widgets on a page.\n", + "- `thebe-init` directly starts running this cell as soon as the thebe live coding functionality starts.\n", + "- `thebe-remove-input-init`, as `thebe-init`, starts the cell directly as soon as the thebe live coding functionality starts. However, the input is not shown to the students (in static and interactive mode). The original `remove-input` tag will not work as it deletes the entire cell, so it can never be executed.\n", + "\n", + "## Additional packages\n", + "\n", + "The python kernel doesn't have all packages standard included. Some of the most used packages which are included are:\n", + "- `python`\n", + "- `numpy`\n", + "- `scipy`\n", + "- `sympy`\n", + "- `matplotlib`\n", + "- `ipywidgets`\n", + "\n", + "If you'd like to install more package, you can do so by added a codecell (preferably hidden using `thebe-remove-input-init`) with the following command:\n", + "\n", + "```python\n", + "import micropip\n", + "await micropip.install(\"package_name\")\n", + "```\n", + "\n", + "This will install the packages: Micropip will look at the Pyodide package index, but also at the general PyPi index. If a package is pure python (i.e. no C extensions), then it can also be used by Pyodide.\n", + "\n", + "If the packages are not included in Pyodide, you can use `pip:\n", + "\n", + "```python\n", + "%pip install package_name\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/book/features/live_code.md b/book/features/live_code.md deleted file mode 100644 index a431e7a..0000000 --- a/book/features/live_code.md +++ /dev/null @@ -1,90 +0,0 @@ -# Python-enabled interactivity - -Our book has been enable to run Python code live in the browser (thanks Max!). This page contains some installation instructions and the other sections show how to use this functionality to create interactive figures and feedback on code - -## Setting up Python live coding -To set up the Python live coding you need to add our [own sphinx-thebe extension](https://gitlab.tudelft.nl/interactivetextbooks-citg/extensions/sphinx-thebe) to your book. This extensions doesn't rely on a 3rd party like Binder and it supports local python execution and custom features. Therefore, you need to add some lines to `requirements.txt` and `_config.yml` - -For `requirements.txt` add the following lines: -``` ---extra-index-url https://gitlab.tudelft.nl/api/v4/projects/11239/packages/pypi/simple -sphinx-thebe ~= 0.9.9 -``` -This will download the correct version of the sphinx extension when the book is build on the server (which loads the required packages from `requirements.txt`) - -Afterwards, this sphinx extension needs to be enabled in your book. This can be done by adding the following lines to `_config.yml`: - -``` -launch_buttons: - thebe: true - -sphinx: - config: - html_js_files: - - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js - thebe_config: - use_thebe_lite: true - exclude_patterns: ["**/_*.yml", "**/*.md", "**/*.ipynb"] -``` - -The `html_js_files` link calls for some required javascript files. `use_thebe_lite` makes sure you initiate our adapted sphinx thebe extensions. The `exclude_patterns` makes sure you import all files so that they can be accessed from your code, except for the files matching the patterns shown. By `launch_buttons` you initiate the {fa}`rocket` --> {guilabel}`Live Code` on the top right corner of every page generated from a `.ipynb` file. - -Note, if you're messing around with `html_theme_options` (for example when adding more buttons), all button behaviour is affected. In that case, as opposed to the jupyter book documentation, all buttons have to be specified from within `sphinx: html_theme_options` where you might also specify other buttons as well (ie. the [code repository button](https://jupyterbook.org/en/stable/basics/repository.html)). - -``` -sphinx: - config: - html_js_files: - - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js - thebe_config: - use_thebe_lite: true - exclude_patterns: ["**/_*.yml", "**/*.md", "**/*.ipynb"] - html_theme_options: - repository_url: "hello!" - use_repository_button: true - launch_buttons: - thebe: true -``` - -## Instructions: local build - -To test the fuctionality, you have to run a local server, otherwise the interactivity doens't work. So, you cannot just open the `index.html` file any more. This can be done with the following steps: -1. build the book by running ` jupyter book build `. If you are using Windows you should use the Git Bash terminal. Set this as your default in VS Code, then open it using "New Terminal." If you have not already done so, you will need to allow Git Bash to use your Python libraries by running the command `conda init bash`. -2. Set up a server by running `python -m http.server 8000`. -3. Open the book by going to [http://localhost:8000/](http://localhost:8000/) in your browser. You may need to reload the page once the rebuild is complete. You can do this on Chrome by right-clicking somewhere on the page, select "Inspect", open the "Network" tab, then reload with `CTRL+R`. Or try `ctrl`+`F5` -4. Go to any page that is created from a Jupyter notebook - -If you push to main, you can test the interactivity on the book-draft website as well. - -## Custom cell tags -With the extended functionality of live code, additional cell tags have been developed to be added to cells in any .ipynb file, next to the [existing tags] of the jupyter book (https://jupyterbook.org/en/stable/content/metadata.html): -- `disable-execution-cell` disables the ability to execute the cell when thebe is activated. This might be useful if you notebook file includes interactivity for only a part of the coding cells -- `disable-execution-page` disables the ability to execute the entire page. This might be useful if you use python code for creating a page, but the code is not part of the actual content of the book. -- `auto-execute-page` automatically starts the thebe live coding functionality whenever the page is opened. This is particularly useful if you're using widgets on a page. -- `thebe-init` directly starts running this cell as soon as the thebe live coding functionality starts. -- `thebe-remove-input-init`, as `thebe-init`, starts the cell directly as soon as the thebe live coding functionality starts. However, the input is not shown to the students (in static and interactive mode). The original `remove-input` tag will not work as it deletes the entire cell, so it can never be executed. - -## Additional packages - -The python kernel doesn't have all packages standard included. Some of the most used packages which are included are: -- `python` -- `numpy` -- `scipy` -- `sympy` -- `matplotlib` -- `ipywidgets` - -If you'd like to install more package, you can do so by added a codecell (preferably hidden using `thebe-remove-input-init`) with the following command: - -```python -import micropip -await micropip.install("package_name") -``` - -This will install the packages: Micropip will look at the Pyodide package index, but also at the general PyPi index. If a package is pure python (i.e. no C extensions), then it can also be used by Pyodide. - -If the packages are not included in Pyodide, you can use `pip: - -```python -%pip install package_name -``` diff --git a/book/features/python_interactivity.md b/book/features/python_interactivity.md new file mode 100644 index 0000000..b63d8aa --- /dev/null +++ b/book/features/python_interactivity.md @@ -0,0 +1,5 @@ +# Python-enabled interactivity + +In general, there are two option to include python-enabled interactivity embedded in your book: + - Using our [own sphinx-thebe extension](live_code.md) which makes code cells in your `.ipynb` book pages editable and executable by the reader! Opposed to the original sphinx-thebe extensions, our extension runs python in the reader's browser and doesn't rely on an external webserver for the python kernel. We highly recommend this extension as it allows you to use the strength of combining text/figures and code of a jupyter book with interactivity! + - Using [JupyterLite](jupyterlite/jupyterlite.md) to create 'iframe-like' Ipython consoles / jupyterbook / jupyter lab environments in your book. As these 'iframes' are separate modules added to your page, the integration with the other content in your book is less flexible as with our sphinx-thebe extension. \ No newline at end of file diff --git a/book/features/widgets.ipynb b/book/features/widgets.ipynb index 361cb93..36bff96 100644 --- a/book/features/widgets.ipynb +++ b/book/features/widgets.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Widgets\n", + "# Ipywidgets\n", "\n", "If you publish widgets in a jupyter book they might (depending on your implementation) require an active python kernel for the output to be interactive. Using the [thebe-integration](live_code.md), this is possible. Note: You can circumvent the use of a kernel by using packages which don't need such a kernel, for example the [non-kernel-based widgets using Plotly](interactive_element).\n", "\n", diff --git a/book/installation-and-setup/jupyter-book-setup.md b/book/installation-and-setup/jupyter-book-setup.md index 50e4247..a808dd3 100644 --- a/book/installation-and-setup/jupyter-book-setup.md +++ b/book/installation-and-setup/jupyter-book-setup.md @@ -2,10 +2,20 @@ Jupyter Book is just a regular Python package, so it can be installed by using `pip` or `conda`: - pip install jupyter-book +``` +pip install jupyter-book +``` Nevertheless, it's always a good idea make a fresh `conda` environment when starting a new project. +## Build a book + +As soon as you've installed jupyter-book you can build your book locally. The official [jupyter-book](https://jupyterbook.org/en/stable/basics/build.html) documentation is quite extensive. Only one note, it's advised to use `--all` whenever building a book to make sure the table of contents fully updates: + +``` +jupyter-book build --all +``` + ## View the book locally Once the build process is completed successfully, you will see the file location in your terminal output. Simply open this in the browser (you may have to copy/paste). diff --git a/book/workflows/githubtemplate.md b/book/workflows/githubtemplate.md index 739ab55..33c52bf 100644 --- a/book/workflows/githubtemplate.md +++ b/book/workflows/githubtemplate.md @@ -1,4 +1,6 @@ -# Get started using our GitHub template! +# Example using our GitHub template + +This page shows you how as an example how to create your own Jupyter Book. It including making an edit to a predefined book and hosting that book online. It doesn't elaborate on the collaborative functionalities of Git. To get started making your Jupyter Book with our functionalities, fork our template book https://github.com/TeachBooks/template :