Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
All contents are Copyright (c) to BlueGreen Labs (BV) 2023.

A limited license to version 1.0 of this template is granted to
the Geocomputation and Earth Observation at the University of Bern.

For the WebR dynamic R tooling we relied on community input by
Ethan White under an MIT license as specified below.

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# R book template

A quarto project template to use for courses and other documentation separated from the main BlueGreen Labs website

To make sure that the template works "out of the box" you will have to include all branches (click the tick box which isn't marked by default).

![Screenshot from 2023-11-27 09-59-25](https://github.com/bluegreen-labs/R_book_template/assets/1354258/c3eb89cb-85d7-454a-ac44-b888da14e5c5)

If you forgot to tick the box you will need to run the `quarto publish gh-pages` command in a CLI terminal (not the R terminal).
2 changes: 1 addition & 1 deletion book/_extensions/coatless/webr/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: webr
title: Embedded webr code cells
author: James Joseph Balamuta
version: 0.0.1
version: 0.3.6
quarto-required: ">=1.2.198"
contributes:
filters:
Expand Down
10 changes: 10 additions & 0 deletions book/_extensions/coatless/webr/monaco-editor-init.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script src="https://cdn.jsdelivr.net/npm/[email protected]/min/vs/loader.js"></script>
<script type="module" id="webr-monaco-editor-init">

// Configure the Monaco Editor's loader
require.config({
paths: {
'vs': 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs'
}
});
</script>
31 changes: 31 additions & 0 deletions book/_extensions/coatless/webr/template.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: "WebR-enabled code cell"
format: html
engine: knitr
#webr:
# show-startup-message: false # Display status of webR initialization
# show-header-message: false # Check to see if COOP&COEP headers are set for speed.
# packages: ['ggplot2', 'dplyr'] # Pre-install dependencies
# home-dir: "/home/rstudio" # Customize where the working directory is
# base-url: '' # Base URL used for downloading R WebAssembly binaries
# service-worker-url: '' # URL from where to load JavaScript worker scripts when loading webR with the ServiceWorker communication channel.
filters:
- webr
---

## Demo

This is a webr-enabled code cell in a Quarto HTML document.

```{webr-r}
1 + 1
```

```{webr-r}
fit = lm(mpg ~ am, data = mtcars)
summary(fit)
```

```{webr-r}
plot(pressure)
```
204 changes: 204 additions & 0 deletions book/_extensions/coatless/webr/webr-context-interactive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<button class="btn btn-default btn-webr" disabled type="button" id="webr-run-button-{{WEBRCOUNTER}}">Loading
webR...</button>
<div id="webr-editor-{{WEBRCOUNTER}}"></div>
<div id="webr-code-output-{{WEBRCOUNTER}}" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
<script type="module">
// Retrieve webR code cell information
const runButton = document.getElementById("webr-run-button-{{WEBRCOUNTER}}");
const outputDiv = document.getElementById("webr-code-output-{{WEBRCOUNTER}}");
const editorDiv = document.getElementById("webr-editor-{{WEBRCOUNTER}}");

// Add a light grey outline around the code editor
editorDiv.style.border = "1px solid #eee";

// Load the Monaco Editor and create an instance
let editor;
require(['vs/editor/editor.main'], function () {
editor = monaco.editor.create(editorDiv, {
value: `{{WEBRCODE}}`,
language: 'r',
theme: 'vs-light',
automaticLayout: true, // TODO: Could be problematic for slide decks
scrollBeyondLastLine: false,
minimap: {
enabled: false
},
fontSize: '17.5rem', // Bootstrap is 1 rem
renderLineHighlight: "none", // Disable current line highlighting
hideCursorInOverviewRuler: true // Remove cursor indictor in right hand side scroll bar
});

// Dynamically modify the height of the editor window if new lines are added.
let ignoreEvent = false;
const updateHeight = () => {
const contentHeight = editor.getContentHeight();
// We're avoiding a width change
//editorDiv.style.width = `${width}px`;
editorDiv.style.height = `${contentHeight}px`;
try {
ignoreEvent = true;

// The key to resizing is this call
editor.layout();
} finally {
ignoreEvent = false;
}
};

// Helper function to check if selected text is empty
function isEmptyCodeText(selectedCodeText) {
return (selectedCodeText === null || selectedCodeText === undefined || selectedCodeText === "");
}

// Registry of keyboard shortcuts that should be re-added to each editor window
// when focus changes.
const addWebRKeyboardShortCutCommands = () => {
// Add a keydown event listener for Shift+Enter to run all code in cell
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {

// Retrieve all text inside the editor
executeCode(editor.getValue());
});

// Add a keydown event listener for CMD/Ctrl+Enter to run selected code
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {

// Get the selected text from the editor
const selectedText = editor.getModel().getValueInRange(editor.getSelection());
// Check if no code is selected
if (isEmptyCodeText(selectedText)) {
// Obtain the current cursor position
let currentPosition = editor.getPosition();
// Retrieve the current line content
let currentLine = editor.getModel().getLineContent(currentPosition.lineNumber);

// Propose a new position to move the cursor to
let newPosition = new monaco.Position(currentPosition.lineNumber + 1, 1);

// Check if the new position is beyond the last line of the editor
if (newPosition.lineNumber > editor.getModel().getLineCount()) {
// Add a new line at the end of the editor
editor.executeEdits("addNewLine", [{
range: new monaco.Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1),
text: "\n",
forceMoveMarkers: true,
}]);
}

// Run the entire line of code.
executeCode(currentLine);

// Move cursor to new position
editor.setPosition(newPosition);
} else {
// Code to run when Ctrl+Enter is pressed with selected code
executeCode(selectedText);
}
});
}

// Register an on focus event handler for when a code cell is selected to update
// what keyboard shortcut commands should work.
// This is a workaround to fix a regression that happened with multiple
// editor windows since Monaco 0.32.0
// https://github.com/microsoft/monaco-editor/issues/2947
editor.onDidFocusEditorText(addWebRKeyboardShortCutCommands);

// Register an on change event for when new code is added to the editor window
editor.onDidContentSizeChange(updateHeight);

// Manually re-update height to account for the content we inserted into the call
updateHeight();
});

// Function to execute the code (accepts code as an argument)
async function executeCode(codeToRun) {
// Disable run button for code cell active
runButton.disabled = true;

// Create a canvas variable for graphics
let canvas = undefined;

// Initialize webR
await globalThis.webR.init();

// Setup a webR canvas by making a namespace call into the {webr} package
await webR.evalRVoid("webr::canvas(width={{WIDTH}}, height={{HEIGHT}})");

// Capture output data from evaluating the code
const result = await webRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
captureStreams: true,
captureConditions: false//,
// env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0
});

// Start attempting to parse the result data
try {

// Stop creating images
await webR.evalRVoid("dev.off()");

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
const out = result.output.filter(
evt => evt.type == "stdout" || evt.type == "stderr"
).map((evt) => evt.data).join("\n");

// Clean the state
const msgs = await webR.flush();

// Output each image stored
msgs.forEach(msg => {
// Determine if old canvas can be used or a new canvas is required.
if (msg.type === 'canvas'){
// Add image to the current canvas
if (msg.data.event === 'canvasImage') {
canvas.getContext('2d').drawImage(msg.data.image, 0, 0);
} else if (msg.data.event === 'canvasNewPage') {
// Generate a new canvas element
canvas = document.createElement("canvas");
canvas.setAttribute("width", 2 * {{WIDTH}});
canvas.setAttribute("height", 2 * {{HEIGHT}});
canvas.style.width = "700px";
canvas.style.display = "block";
canvas.style.margin = "auto";
}
}
});

// Nullify the outputDiv of content
outputDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as text
const code = document.createElement("code");
code.innerText = out;
pre.appendChild(code);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
const p = document.createElement("p");
p.appendChild(canvas);
outputDiv.appendChild(p);
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
runButton.disabled = false;
}
}

// Add a click event listener to the run button
runButton.onclick = function () {
executeCode(editor.getValue());
};
</script>
90 changes: 90 additions & 0 deletions book/_extensions/coatless/webr/webr-context-output.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<div id="webr-code-output-{{WEBRCOUNTER}}" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
<script type="module">
// Retrieve webR code cell information
const outputDiv = document.getElementById("webr-code-output-{{WEBRCOUNTER}}");

// Function to execute the code (accepts code as an argument)
async function executeOutputOnlyCode(codeToRun) {
// Create a canvas variable for graphics
let canvas = undefined;

// Initialize webR
await globalThis.webR.init();

// Setup a webR canvas by making a namespace call into the {webr} package
await webR.evalRVoid("webr::canvas(width={{WIDTH}}, height={{HEIGHT}})");

// Capture output data from evaluating the code
const result = await webRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
captureStreams: true,
captureConditions: false//,
// env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0
});

// Start attempting to parse the result data
try {

// Stop creating images
await webR.evalRVoid("dev.off()");

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
const out = result.output.filter(
evt => evt.type == "stdout" || evt.type == "stderr"
).map((evt) => evt.data).join("\n");

// Clean the state
const msgs = await webR.flush();

// Output each image stored
msgs.forEach(msg => {
// Determine if old canvas can be used or a new canvas is required.
if (msg.type === 'canvas'){
// Add image to the current canvas
if (msg.data.event === 'canvasImage') {
canvas.getContext('2d').drawImage(msg.data.image, 0, 0);
} else if (msg.data.event === 'canvasNewPage') {
// Generate a new canvas element
canvas = document.createElement("canvas");
canvas.setAttribute("width", 2 * {{WIDTH}});
canvas.setAttribute("height", 2 * {{HEIGHT}});
canvas.style.width = "700px";
canvas.style.display = "block";
canvas.style.margin = "auto";
}
}
});

// Nullify the outputDiv of content
outputDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as text
const code = document.createElement("code");
code.innerText = out;
pre.appendChild(code);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
const p = document.createElement("p");
p.appendChild(canvas);
outputDiv.appendChild(p);
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
}
}

// Run the code
executeOutputOnlyCode(`{{WEBRCODE}}`)
</script>
9 changes: 9 additions & 0 deletions book/_extensions/coatless/webr/webr-context-setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script type="module">
// Initialization WebR
await globalThis.webR.init();

// Run R code without focusing on storing data.
await globalThis.webR.evalRVoid(`
{{WEBRCODE}}
`)
</script>
Loading