diff --git a/CHANGELOG.md b/CHANGELOG.md
index b06f3c9..662bc41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* JSON schema powered form ([#27](https://github.com/NLESC-JCER/cpp2wasm/issues/27))
* WebAssembly module ([#35](https://github.com/NLESC-JCER/cpp2wasm/issues/35))
+* Vega plot ([#15](https://github.com/NLESC-JCER/cpp2wasm/issues/15))
## [0.1.0] - 2020-06-04
diff --git a/README.md b/README.md
index e36e2f5..e040f16 100644
--- a/README.md
+++ b/README.md
@@ -766,11 +766,14 @@ To be able to use the `createModule` function, we will import the `newtonraphson
```{.html file=src/js/example.html}
-
-
-
+
+
+ Example
+
+
+
```
@@ -878,10 +881,13 @@ Like before we need a HTML page to run the JavaScript, but now we don't need to
```{.html file=src/js/example-web-worker.html}
-
-
+
+
+ Example web worker
+
+
```
@@ -920,8 +926,11 @@ we implement the React application in the `app.js` file.
```{.html file=src/js/example-app.html}
-
- <>
+
+
+ Example React application
+ <>
+
@@ -1177,8 +1186,11 @@ To render the application we need a HTML page. We will reuse the imports we did
```{.html file=src/js/example-jsonschema-form.html}
-
- <>
+
+
+ Example JSON schema powered form
+ <>
+
@@ -1192,7 +1204,7 @@ To use the [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema
```
-The form component is exported as `JSONSchemaForm.default` and can be aliases to `Form` with
+The form component is exported as `JSONSchemaForm.default` and can be aliases to `Form` for easy use with
```{.js #jsonschema-app}
// this JavaScript snippet is appended to <>
@@ -1249,12 +1261,12 @@ The `handleSubmit` function recieves the form input values and use the web worke
// this JavaScript snippet is appended to <>
const [root, setRoot] = React.useState(undefined);
-function handleSubmit({formData}, event) {
+function handleSubmit(submission, event) {
event.preventDefault();
const worker = new Worker('worker.js');
worker.postMessage({
type: 'CALCULATE',
- payload: formData
+ payload: submission.formData
});
worker.onmessage = function(message) {
if (message.data.type === 'RESULT') {
@@ -1311,4 +1323,291 @@ If you enter a negative number in the `epsilon` field the form will become inval
### Visualization
-The plots in web apllicatoin can be made using [vega-lite](https://vega.github.io/vega-lite/). Vega-lite is a JS library which accepts a JSON document describing the plot and generates interactive graphics.
+The plots in web application can be made using [vega-lite](https://vega.github.io/vega-lite/). Vega-lite is a JS library which accepts a JSON document describing the plot.
+
+To make an interesting plot we need more than one result. We are going to do a parameter sweep and measure how long each calculation takes.
+
+Lets make a new JSON schema for the form in which we can set a max, min and step for epsilon.
+
+```{.js #plot-app}
+// this JavaScript snippet is later referred to as <>
+const schema = {
+ "type": "object",
+ "properties": {
+ "epsilon": {
+ "title": "Epsilon",
+ "type": "object",
+ "properties": {
+ "min": {
+ "type": "number",
+ "minimum": 0,
+ "default": 0.0001
+ },
+ "max": {
+ "type": "number",
+ "minimum": 0,
+ "default": 0.001
+ },
+ "step": {
+ "type": "number",
+ "minimum": 0,
+ "default": 0.0001
+ }
+ },
+ "required": ["min", "max", "step"],
+ "additionalProperties": false
+ },
+ "guess": {
+ "title": "Initial guess",
+ "type": "number",
+ "minimum": -100,
+ "maximum": 100,
+ "default": -20
+ }
+ },
+ "required": ["epsilon", "guess"],
+ "additionalProperties": false
+}
+```
+
+We need to rewrite the worker to perform a parameter sweep.
+The worker will recieve a payload like
+
+```json
+{
+ "epsilon": {
+ "min": 0.0001,
+ "max": 0.001,
+ "step": 0.0001
+ },
+ "guess": -20
+}
+```
+
+The worker will send back an array containing objects with the root result, the input parameters and the duration in milliseconds.
+
+```json
+[{
+ "epsilon": 0.0001,
+ "guess": -20,
+ "root": -1,
+ "duration": 0.61
+}]
+```
+
+To perform the sweep we will first unpack the payload.
+
+```{.js #calculate-sweep}
+// this JavaScript snippet is later referred to as <>
+const {min, max, step} = message.data.payload.epsilon;
+const guess = message.data.payload.guess;
+```
+
+The result array needs to be initialized.
+
+```{.js #calculate-sweep}
+// this JavaScript snippet appended to <>
+const roots = [];
+```
+
+Lets use a [classic for loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) to iterate over requested the epsilons.
+
+```{.js #calculate-sweep}
+// this JavaScript snippet appended to <>
+for (let epsilon = min; epsilon <= max; epsilon += step) {
+```
+
+To measure the duration of a calculation we use the [performance.now()](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) method which returns a timestamp in milliseconds.
+
+```{.js #calculate-sweep}
+ // this JavaScript snippet appended to <>
+ const t0 = performance.now();
+ const finder = new module.NewtonRaphson(epsilon);
+ const root = finder.find(guess);
+ const duration = performance.now() - t0;
+```
+
+We append the root result object using [shorthand property names](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer) to the result array.
+
+```{.js #calculate-sweep}
+ // this JavaScript snippet appended to <>
+ roots.push({
+ epsilon,
+ guess,
+ root,
+ duration
+ });
+```
+
+To complete the sweep calculation we need to close the for loop and post the result.
+
+```{.js #calculate-sweep}
+ // this JavaScript snippet appended to <>
+}
+postMessage({
+ type: 'RESULT',
+ payload: {
+ roots
+ }
+});
+```
+
+The sweep calculation snippet (`<>`) must be run in a new web worker called `worker-sweep.js`.
+Like before we need to wait for the WebAssembly module to be initialized before we can start the calculation.
+
+```{.js file=src/js/worker-sweep.js}
+// this JavaScript snippet stored as src/js/worker-sweep.js
+importScripts('newtonraphsonwasm.js');
+
+onmessage = function(message) {
+ if (message.data.type === 'CALCULATE') {
+ createModule().then((module) => {
+ <>
+ });
+ }
+};
+```
+
+To handle the submit we will start a worker, send the form data to the worker, recieve the workers result and store it in the `roots` variable.
+
+```{.js #plot-app}
+// this JavaScript snippet is appended to <>
+const [roots, setRoots] = React.useState([]);
+
+function handleSubmit(submission, event) {
+ event.preventDefault();
+ const worker = new Worker('worker-sweep.js');
+ worker.postMessage({
+ type: 'CALCULATE',
+ payload: submission.formData
+ });
+ worker.onmessage = function(message) {
+ if (message.data.type === 'RESULT') {
+ const result = message.data.payload.roots;
+ setRoots(result);
+ worker.terminate();
+ }
+ };
+}
+```
+
+Now that we got data, we are ready to plot. We use the
+ [Vega-Lite specification](https://vega.github.io/vega-lite/docs/spec.html) to declare the plot.
+The specification for a scatter plot of the `epsilon` against the `duration` looks like.
+
+```{.js #vega-lite-spec}
+// this JavaScript snippet is later referred to as <>
+const spec = {
+ "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
+ "data": { "values": roots },
+ "mark": "point",
+ "encoding": {
+ "x": { "field": "epsilon", "type": "quantitative" },
+ "y": { "field": "duration", "type": "quantitative", "title": "Duration (ms)" }
+ },
+ "width": 800,
+ "height": 600
+};
+```
+
+To render the spec we use the [vegaEmbed](https://github.com/vega/vega-embed) module. The Vega-Lite specification is a simplification of the [Vega specification](https://vega.github.io/vega/docs/specification/) so wil first import `vega` then `vega-lite` and lastly `vega-embed`.
+
+```{.html #imports}
+
+
+
+```
+
+The `vegaEmbed()` function needs a DOM element to render the plot in.
+In React we must use the [useRef](https://reactjs.org/docs/hooks-reference.html#useref) hook to get a reference to a DOM element. As the DOM element needs time to initialize we need to use the [useEffect](https://reactjs.org/docs/hooks-effect.html) hook to only embed the plot when the DOM element is ready. The `Plot` React component can be written as
+
+```{.jsx #plot-component}
+// this JavaScript snippet is later referred to as <>
+function Plot({roots}) {
+ const container = React.useRef(null);
+
+ function didUpdate() {
+ if (container.current === null) {
+ return;
+ }
+ <>
+ vegaEmbed(container.current, spec);
+ }
+ const dependencies = [container, roots];
+ React.useEffect(didUpdate, dependencies);
+
+ return
;
+}
+```
+
+The App component can be defined and rendered with.
+
+```{.jsx file=src/js/plot-app.js}
+// this JavaScript snippet stored as src/js/plot-app.js
+<>
+
+<>
+
+function App() {
+ const Form = JSONSchemaForm.default;
+ const uiSchema = {
+ "guess": {
+ "ui:widget": "range"
+ }
+ }
+ const [formData, setFormData] = React.useState({
+
+ });
+
+ function handleChange(event) {
+ setFormData(event.formData);
+ }
+
+ <>
+
+ return (
+
+
+ <>
+
+
+ );
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('container')
+);
+```
+
+The html page should look like
+
+```{.html file=src/js/example-plot.html}
+
+
+
+
+ Example plot
+ <>
+
+
+
+
+
+
+
+```
+
+Like before we also need to host the files in a web server with
+
+```shell
+python3 -m http.server 8000
+```
+
+Visit [http://localhost:8000/src/js/example-plot.html](http://localhost:8000/src/js/example-plot.html) to see the epsilon/duration plot.
+
+Embedded below is the example app hosted on [GitHub pages](https://nlesc-jcer.github.io/cpp2wasm/src/js/example-plot.html)
+
+
+
+After the submit button is pressed the plot should show that the first calculation took a bit longer then the rest.
diff --git a/TESTING.md b/TESTING.md
index bff8b23..3177a48 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -2,12 +2,18 @@
To make sure [JavaScript and WebAssembly code snippets](README.md#JavaScript) and [Single page application](README.md#single-page-application) work we want have a tests for them.
-To test, we will use the [cypress](https://www.cypress.io/) JavaScript end to end testing framework.
+To test, we will use the [cypress](https://www.cypress.io/) JavaScript end to end testing framework.
Cypress can simulate user behavior such as clicking buttons etc. and checks expected result in a web browser.
-In the following example, we test if the example web page renders the answer `-1.00` when it is visited.
+In the following examples, we test if the example web pages render the answer `-1.00` when they are visited.
-Let's, first write a test for the direct WebAssembly example.
+To visit a web page we need to start a simple web server with using Python
+
+```shell
+python3 -m http.server 8000
+```
+
+Let's, first write a test for the [direct WebAssembly example](http://localhost:8000/src/js/example.html).
```{.js file=cypress/integration/example_spec.js}
// this JavaScript snippet is run by cypress and is stored as cypress/integration/example_spec.js
@@ -19,7 +25,7 @@ describe('src/js/example.html', () => {
});
```
-Second, a test for the WebAssembly called through a web worker.
+Second, a test for the WebAssembly called through a [web worker](http://localhost:8000/src/js/example-web-worker.html).
```{.js file=cypress/integration/example-web-worker_spec.js}
// this JavaScript snippet is run by cypress and is stored as cypress/integration/example-web-worker_spec.js
@@ -31,35 +37,50 @@ describe('src/js/example-web-worker.html', () => {
});
```
-And lastly, a test for the React/form/Web worker/WebAssembly combination.
-Let us also change the guess value.
+Third, a test for the [React/form/Web worker/WebAssembly combination](http://localhost:8000/src/js/example-app.html).
+Let us also change the initial guess value.
```{.js file=cypress/integration/example-app_spec.js}
describe('src/js/example-app.html', () => {
it('should render -1.00', () => {
cy.visit('http://localhost:8000/src/js/example-app.html');
+ // The initial value of the guess input field is -20 so we append a 0 and it becomes -200
cy.get('input[name=guess]').type('0');
- // TODO assert value is set
cy.contains('Submit').click();
cy.get('#answer').contains('-1.00');
});
});
```
-And another test for the full application, but now with JSON schema powered form.
+And similar test to the previous one, but now with [JSON schema powered form](http://localhost:8000/src/js/example-jsonschema-form.html).
```{.js file=cypress/integration/example-jsonschema-form_spec.js}
describe('src/js/example-jsonschema-form.html', () => {
it('should render -1.00', () => {
cy.visit('http://localhost:8000/src/js/example-jsonschema-form.html');
- cy.get('input[id=root_epsilon]').type('{selectall}0.1');
- // TODO assert value is set
+ // The JSON schema powered form uses a hierarchy of identifiers for each input field starting with `root`
+ // As the `epsilon` input field is a direct child of root, it has `root_epsilon` as an identifier
+ const input_selector = 'input[id=root_epsilon]';
+ // In initial guess input field we replace the default value with 0.1
+ cy.get(input_selector).type('{selectall}0.1');
cy.contains('Submit').click();
cy.get('#answer').contains('-1.00');
});
});
```
+And lastly a test for the [web application with a plot](http://localhost:8000/src/js/example-plot.html).
+
+```{.js file=cypress/integration/example-plot_spec.js}
+describe('src/js/example-plot.html', () => {
+ it('should render -1.00', () => {
+ cy.visit('http://localhost:8000/src/js/example-plot.html');
+ cy.contains('Submit').click();
+ // TODO assert plot has been plotted, see https://github.com/NLESC-JCER/cpp2wasm/issues/55
+ });
+});
+```
+
The test can be run with the following command:
```{.awk #test-wasm}
diff --git a/cypress/integration/example-app_spec.js b/cypress/integration/example-app_spec.js
index 65ea69e..d52c07c 100644
--- a/cypress/integration/example-app_spec.js
+++ b/cypress/integration/example-app_spec.js
@@ -1,8 +1,8 @@
describe('src/js/example-app.html', () => {
it('should render -1.00', () => {
cy.visit('http://localhost:8000/src/js/example-app.html');
+ // The initial value of the guess input field is -20 so we append a 0 and it becomes -200
cy.get('input[name=guess]').type('0');
- // TODO assert value is set
cy.contains('Submit').click();
cy.get('#answer').contains('-1.00');
});
diff --git a/cypress/integration/example-jsonschema-form_spec.js b/cypress/integration/example-jsonschema-form_spec.js
index ead379d..80c7209 100644
--- a/cypress/integration/example-jsonschema-form_spec.js
+++ b/cypress/integration/example-jsonschema-form_spec.js
@@ -1,8 +1,11 @@
describe('src/js/example-jsonschema-form.html', () => {
it('should render -1.00', () => {
cy.visit('http://localhost:8000/src/js/example-jsonschema-form.html');
- cy.get('input[id=root_epsilon]').type('{selectall}0.1');
- // TODO assert value is set
+ // The JSON schema powered form uses a hierarchy of identifiers for each input field starting with `root`
+ // As the `epsilon` input field is a direct child of root, it has `root_epsilon` as an identifier
+ const input_selector = 'input[id=root_epsilon]';
+ // In initial guess input field we replace the default value with 0.1
+ cy.get(input_selector).type('{selectall}0.1');
cy.contains('Submit').click();
cy.get('#answer').contains('-1.00');
});
diff --git a/cypress/integration/example-plot_spec.js b/cypress/integration/example-plot_spec.js
new file mode 100644
index 0000000..384d166
--- /dev/null
+++ b/cypress/integration/example-plot_spec.js
@@ -0,0 +1,7 @@
+describe('src/js/example-plot.html', () => {
+ it('should render -1.00', () => {
+ cy.visit('http://localhost:8000/src/js/example-plot.html');
+ cy.contains('Submit').click();
+ // TODO assert plot has been plotted, see https://github.com/NLESC-JCER/cpp2wasm/issues/55
+ });
+});
\ No newline at end of file
diff --git a/src/js/example-app.html b/src/js/example-app.html
index fab26d9..f9d45f7 100644
--- a/src/js/example-app.html
+++ b/src/js/example-app.html
@@ -1,15 +1,21 @@
-
-
-
-
-
-
-
-
-
-
+
+
+ Example React application
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/js/example-jsonschema-form.html b/src/js/example-jsonschema-form.html
index d3fc452..f9780ac 100644
--- a/src/js/example-jsonschema-form.html
+++ b/src/js/example-jsonschema-form.html
@@ -1,15 +1,21 @@
-
-
-
-
-
-
-
-
-
-
+
+
+ Example JSON schema powered form
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/js/example-plot.html b/src/js/example-plot.html
new file mode 100644
index 0000000..a31f9c0
--- /dev/null
+++ b/src/js/example-plot.html
@@ -0,0 +1,24 @@
+
+
+
+
+ Example plot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/js/example-web-worker.html b/src/js/example-web-worker.html
index 170077a..638a1e6 100644
--- a/src/js/example-web-worker.html
+++ b/src/js/example-web-worker.html
@@ -1,23 +1,26 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/js/example.html b/src/js/example.html
index 3ec769c..571503a 100644
--- a/src/js/example.html
+++ b/src/js/example.html
@@ -1,19 +1,22 @@
-
-
-
+
+
+ Example
+
+
+
\ No newline at end of file
diff --git a/src/js/jsonschema-app.js b/src/js/jsonschema-app.js
index 0237341..e625ba5 100644
--- a/src/js/jsonschema-app.js
+++ b/src/js/jsonschema-app.js
@@ -40,12 +40,12 @@ function App() {
// this JavaScript snippet is appended to <>
const [root, setRoot] = React.useState(undefined);
- function handleSubmit({formData}, event) {
+ function handleSubmit(submission, event) {
event.preventDefault();
const worker = new Worker('worker.js');
worker.postMessage({
type: 'CALCULATE',
- payload: formData
+ payload: submission.formData
});
worker.onmessage = function(message) {
if (message.data.type === 'RESULT') {
diff --git a/src/js/plot-app.js b/src/js/plot-app.js
new file mode 100644
index 0000000..2916ae0
--- /dev/null
+++ b/src/js/plot-app.js
@@ -0,0 +1,127 @@
+// this JavaScript snippet stored as src/js/plot-app.js
+// this JavaScript snippet is later referred to as <>
+function Heading() {
+ const title = 'Root finding web application';
+ return
{title}
+}
+
+// this JavaScript snippet is later referred to as <>
+function Plot({roots}) {
+ const container = React.useRef(null);
+
+ function didUpdate() {
+ if (container.current === null) {
+ return;
+ }
+ // this JavaScript snippet is later referred to as <>
+ const spec = {
+ "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
+ "data": { "values": roots },
+ "mark": "point",
+ "encoding": {
+ "x": { "field": "epsilon", "type": "quantitative" },
+ "y": { "field": "duration", "type": "quantitative", "title": "Duration (ms)" }
+ },
+ "width": 800,
+ "height": 600
+ };
+ vegaEmbed(container.current, spec);
+ }
+ const dependencies = [container, roots];
+ React.useEffect(didUpdate, dependencies);
+
+ return