Skip to content

Commit ad7ca36

Browse files
authored
Merge pull request #26 from NLESC-JCER/vega-15
Vega 15
2 parents 33eca05 + 20d6f3e commit ad7ca36

14 files changed

+623
-87
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* JSON schema powered form ([#27](https://github.com/NLESC-JCER/cpp2wasm/issues/27))
1313
* WebAssembly module ([#35](https://github.com/NLESC-JCER/cpp2wasm/issues/35))
14+
* Vega plot ([#15](https://github.com/NLESC-JCER/cpp2wasm/issues/15))
1415

1516
## [0.1.0] - 2020-06-04
1617

README.md

Lines changed: 316 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -766,11 +766,14 @@ To be able to use the `createModule` function, we will import the `newtonraphson
766766
```{.html file=src/js/example.html}
767767
<!doctype html>
768768
<!-- this HTML page is stored as src/js/example.html -->
769-
<html>
770-
<script type="text/javascript" src="newtonraphsonwasm.js"></script>
771-
<script>
772-
<<wasm-promise>>
773-
</script>
769+
<html lang="en">
770+
<head>
771+
<title>Example</title>
772+
<script type="text/javascript" src="newtonraphsonwasm.js"></script>
773+
<script>
774+
<<wasm-promise>>
775+
</script>
776+
</head>
774777
</html>
775778
```
776779

@@ -878,10 +881,13 @@ Like before we need a HTML page to run the JavaScript, but now we don't need to
878881
```{.html file=src/js/example-web-worker.html}
879882
<!doctype html>
880883
<!-- this HTML page is stored as src/js/example-web-worker.html -->
881-
<html>
882-
<script>
883-
<<worker-consumer>>
884-
</script>
884+
<html lang="en">
885+
<head>
886+
<title>Example web worker</title>
887+
<script>
888+
<<worker-consumer>>
889+
</script>
890+
</head>
885891
</html>
886892
```
887893

@@ -920,8 +926,11 @@ we implement the React application in the `app.js` file.
920926
```{.html file=src/js/example-app.html}
921927
<!doctype html>
922928
<!-- this HTML page is stored as src/js/example-app.html -->
923-
<html>
924-
<<imports>>
929+
<html lang="en">
930+
<head>
931+
<title>Example React application</title>
932+
<<imports>>
933+
</head>
925934
<div id="container"></div>
926935
927936
<script type="text/babel" src="app.js"></script>
@@ -1177,8 +1186,11 @@ To render the application we need a HTML page. We will reuse the imports we did
11771186
```{.html file=src/js/example-jsonschema-form.html}
11781187
<!doctype html>
11791188
<!-- this HTML page is stored as src/jsexample-jsonschema-form.html -->
1180-
<html>
1181-
<<imports>>
1189+
<html lang="en">
1190+
<head>
1191+
<title>Example JSON schema powered form</title>
1192+
<<imports>>
1193+
</head>
11821194
<div id="container"></div>
11831195
11841196
<script type="text/babel" src="jsonschema-app.js"></script>
@@ -1192,7 +1204,7 @@ To use the [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema
11921204
<script src="https://unpkg.com/@rjsf/core/dist/react-jsonschema-form.js"></script>
11931205
```
11941206

1195-
The form component is exported as `JSONSchemaForm.default` and can be aliases to `Form` with
1207+
The form component is exported as `JSONSchemaForm.default` and can be aliases to `Form` for easy use with
11961208

11971209
```{.js #jsonschema-app}
11981210
// this JavaScript snippet is appended to <<jsonschema-app>>
@@ -1249,12 +1261,12 @@ The `handleSubmit` function recieves the form input values and use the web worke
12491261
// this JavaScript snippet is appended to <<jsonschema-app>>
12501262
const [root, setRoot] = React.useState(undefined);
12511263

1252-
function handleSubmit({formData}, event) {
1264+
function handleSubmit(submission, event) {
12531265
event.preventDefault();
12541266
const worker = new Worker('worker.js');
12551267
worker.postMessage({
12561268
type: 'CALCULATE',
1257-
payload: formData
1269+
payload: submission.formData
12581270
});
12591271
worker.onmessage = function(message) {
12601272
if (message.data.type === 'RESULT') {
@@ -1311,4 +1323,291 @@ If you enter a negative number in the `epsilon` field the form will become inval
13111323

13121324
### Visualization
13131325

1314-
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.
1326+
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.
1327+
1328+
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.
1329+
1330+
Lets make a new JSON schema for the form in which we can set a max, min and step for epsilon.
1331+
1332+
```{.js #plot-app}
1333+
// this JavaScript snippet is later referred to as <<jsonschema-app>>
1334+
const schema = {
1335+
"type": "object",
1336+
"properties": {
1337+
"epsilon": {
1338+
"title": "Epsilon",
1339+
"type": "object",
1340+
"properties": {
1341+
"min": {
1342+
"type": "number",
1343+
"minimum": 0,
1344+
"default": 0.0001
1345+
},
1346+
"max": {
1347+
"type": "number",
1348+
"minimum": 0,
1349+
"default": 0.001
1350+
},
1351+
"step": {
1352+
"type": "number",
1353+
"minimum": 0,
1354+
"default": 0.0001
1355+
}
1356+
},
1357+
"required": ["min", "max", "step"],
1358+
"additionalProperties": false
1359+
},
1360+
"guess": {
1361+
"title": "Initial guess",
1362+
"type": "number",
1363+
"minimum": -100,
1364+
"maximum": 100,
1365+
"default": -20
1366+
}
1367+
},
1368+
"required": ["epsilon", "guess"],
1369+
"additionalProperties": false
1370+
}
1371+
```
1372+
1373+
We need to rewrite the worker to perform a parameter sweep.
1374+
The worker will recieve a payload like
1375+
1376+
```json
1377+
{
1378+
"epsilon": {
1379+
"min": 0.0001,
1380+
"max": 0.001,
1381+
"step": 0.0001
1382+
},
1383+
"guess": -20
1384+
}
1385+
```
1386+
1387+
The worker will send back an array containing objects with the root result, the input parameters and the duration in milliseconds.
1388+
1389+
```json
1390+
[{
1391+
"epsilon": 0.0001,
1392+
"guess": -20,
1393+
"root": -1,
1394+
"duration": 0.61
1395+
}]
1396+
```
1397+
1398+
To perform the sweep we will first unpack the payload.
1399+
1400+
```{.js #calculate-sweep}
1401+
// this JavaScript snippet is later referred to as <<calculate-sweep>>
1402+
const {min, max, step} = message.data.payload.epsilon;
1403+
const guess = message.data.payload.guess;
1404+
```
1405+
1406+
The result array needs to be initialized.
1407+
1408+
```{.js #calculate-sweep}
1409+
// this JavaScript snippet appended to <<calculate-sweep>>
1410+
const roots = [];
1411+
```
1412+
1413+
Lets use a [classic for loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) to iterate over requested the epsilons.
1414+
1415+
```{.js #calculate-sweep}
1416+
// this JavaScript snippet appended to <<calculate-sweep>>
1417+
for (let epsilon = min; epsilon <= max; epsilon += step) {
1418+
```
1419+
1420+
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.
1421+
1422+
```{.js #calculate-sweep}
1423+
// this JavaScript snippet appended to <<calculate-sweep>>
1424+
const t0 = performance.now();
1425+
const finder = new module.NewtonRaphson(epsilon);
1426+
const root = finder.find(guess);
1427+
const duration = performance.now() - t0;
1428+
```
1429+
1430+
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.
1431+
1432+
```{.js #calculate-sweep}
1433+
// this JavaScript snippet appended to <<calculate-sweep>>
1434+
roots.push({
1435+
epsilon,
1436+
guess,
1437+
root,
1438+
duration
1439+
});
1440+
```
1441+
1442+
To complete the sweep calculation we need to close the for loop and post the result.
1443+
1444+
```{.js #calculate-sweep}
1445+
// this JavaScript snippet appended to <<calculate-sweep>>
1446+
}
1447+
postMessage({
1448+
type: 'RESULT',
1449+
payload: {
1450+
roots
1451+
}
1452+
});
1453+
```
1454+
1455+
The sweep calculation snippet (`<<calculate-sweep>>`) must be run in a new web worker called `worker-sweep.js`.
1456+
Like before we need to wait for the WebAssembly module to be initialized before we can start the calculation.
1457+
1458+
```{.js file=src/js/worker-sweep.js}
1459+
// this JavaScript snippet stored as src/js/worker-sweep.js
1460+
importScripts('newtonraphsonwasm.js');
1461+
1462+
onmessage = function(message) {
1463+
if (message.data.type === 'CALCULATE') {
1464+
createModule().then((module) => {
1465+
<<calculate-sweep>>
1466+
});
1467+
}
1468+
};
1469+
```
1470+
1471+
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.
1472+
1473+
```{.js #plot-app}
1474+
// this JavaScript snippet is appended to <<plot-app>>
1475+
const [roots, setRoots] = React.useState([]);
1476+
1477+
function handleSubmit(submission, event) {
1478+
event.preventDefault();
1479+
const worker = new Worker('worker-sweep.js');
1480+
worker.postMessage({
1481+
type: 'CALCULATE',
1482+
payload: submission.formData
1483+
});
1484+
worker.onmessage = function(message) {
1485+
if (message.data.type === 'RESULT') {
1486+
const result = message.data.payload.roots;
1487+
setRoots(result);
1488+
worker.terminate();
1489+
}
1490+
};
1491+
}
1492+
```
1493+
1494+
Now that we got data, we are ready to plot. We use the
1495+
[Vega-Lite specification](https://vega.github.io/vega-lite/docs/spec.html) to declare the plot.
1496+
The specification for a scatter plot of the `epsilon` against the `duration` looks like.
1497+
1498+
```{.js #vega-lite-spec}
1499+
// this JavaScript snippet is later referred to as <<vega-lite-spec>>
1500+
const spec = {
1501+
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
1502+
"data": { "values": roots },
1503+
"mark": "point",
1504+
"encoding": {
1505+
"x": { "field": "epsilon", "type": "quantitative" },
1506+
"y": { "field": "duration", "type": "quantitative", "title": "Duration (ms)" }
1507+
},
1508+
"width": 800,
1509+
"height": 600
1510+
};
1511+
```
1512+
1513+
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`.
1514+
1515+
```{.html #imports}
1516+
<script src="https://cdn.jsdelivr.net/npm/vega@5.13.0"></script>
1517+
<script src="https://cdn.jsdelivr.net/npm/vega-lite@4.13.0"></script>
1518+
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6.8.0"></script>
1519+
```
1520+
1521+
The `vegaEmbed()` function needs a DOM element to render the plot in.
1522+
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
1523+
1524+
```{.jsx #plot-component}
1525+
// this JavaScript snippet is later referred to as <<plot-component>>
1526+
function Plot({roots}) {
1527+
const container = React.useRef(null);
1528+
1529+
function didUpdate() {
1530+
if (container.current === null) {
1531+
return;
1532+
}
1533+
<<vega-lite-spec>>
1534+
vegaEmbed(container.current, spec);
1535+
}
1536+
const dependencies = [container, roots];
1537+
React.useEffect(didUpdate, dependencies);
1538+
1539+
return <div ref={container}/>;
1540+
}
1541+
```
1542+
1543+
The App component can be defined and rendered with.
1544+
1545+
```{.jsx file=src/js/plot-app.js}
1546+
// this JavaScript snippet stored as src/js/plot-app.js
1547+
<<heading-component>>
1548+
1549+
<<plot-component>>
1550+
1551+
function App() {
1552+
const Form = JSONSchemaForm.default;
1553+
const uiSchema = {
1554+
"guess": {
1555+
"ui:widget": "range"
1556+
}
1557+
}
1558+
const [formData, setFormData] = React.useState({
1559+
1560+
});
1561+
1562+
function handleChange(event) {
1563+
setFormData(event.formData);
1564+
}
1565+
1566+
<<plot-app>>
1567+
1568+
return (
1569+
<div>
1570+
<Heading/>
1571+
<<jsonschema-form>>
1572+
<Plot roots={roots}/>
1573+
</div>
1574+
);
1575+
}
1576+
1577+
ReactDOM.render(
1578+
<App/>,
1579+
document.getElementById('container')
1580+
);
1581+
```
1582+
1583+
The html page should look like
1584+
1585+
```{.html file=src/js/example-plot.html}
1586+
<!doctype html>
1587+
<!-- this HTML page is stored as src/js/plot-form.html -->
1588+
<html lang="en">
1589+
<head>
1590+
<title>Example plot</title>
1591+
<<imports>>
1592+
<head>
1593+
<body>
1594+
<div id="container"></div>
1595+
1596+
<script type="text/babel" src="plot-app.js"></script>
1597+
</body>
1598+
</html>
1599+
```
1600+
1601+
Like before we also need to host the files in a web server with
1602+
1603+
```shell
1604+
python3 -m http.server 8000
1605+
```
1606+
1607+
Visit [http://localhost:8000/src/js/example-plot.html](http://localhost:8000/src/js/example-plot.html) to see the epsilon/duration plot.
1608+
1609+
Embedded below is the example app hosted on [GitHub pages](https://nlesc-jcer.github.io/cpp2wasm/src/js/example-plot.html)
1610+
1611+
<iframe width="100%" height="1450" src="https://nlesc-jcer.github.io/cpp2wasm/src/js/example-plot.html" /></iframe>
1612+
1613+
After the submit button is pressed the plot should show that the first calculation took a bit longer then the rest.

0 commit comments

Comments
 (0)