Visualizing R Data with SveltR
If the output target of your Rmd (R Markdown) content is HTML, R has some methods for turning R data into JavaScript or JSON data and printing the results within HTML tags.
After using R to read each pixel of this heightmap, the table (data.frame) is written to json …
## Loading required package: jsonlite
<div id="gl_data_in_html">
<script type="application/json">[{"X1":0.113,"X2":0.113,"X3":0.1142,"X4":0.1262,"X5":0.127,"X6":0.1261,"X7":0.1247,"X8":0.1181,"X9":0.1206,"X10":0.1213,"X11":0.1155,"X12":0.1203,"X13":0.1232,"X14":0.1267,"X15":0.1291,"X16":0.1302,"X17":0.132,"X18":0.132,"X19":0.1328,"X20":0.1313,"X21":0.1311,"X22":0.1476,"X23":0.1725,"X24":0.1894,"X25":0.1969,"X26":0.179,"X27":0.1494,"X28":0.1489,"X29":0.1661,"X30":0.1885,"X31":0.1893,"X32":0.1406,"X33":0.1297,"X34":0.1317,"X35":0.1399,"X36":0.1388,"X37":0.1481,"X38":0.1594,"X39":0.1567,"X40":0.174,"X41":0.2082,"X42":0.2465,"X43":0.2652,"X44":0.2297,"X45":0.197,"X46":0.2725,"X47":0.2912,"X48":0.2754,"X49":0.2683,"X50":0.2197,"X51":0.2106,"X52":0.2074,"X53":0.2139,"X54":0.2594,"X55":0.2692,"X56":0.2962,"X57":0.3287,"X58":0.2942,"X59":0.2544,"X60":0.2473,"X61":0.2493,"X62":0.2648,"X63":0.2775,"X64":0.2713}, ...
</script>
</div>
Note the type="application/json"
attribute, which prevents the browser from evaluating the contents of the <script>
tag as JavaScript (if that is intended) and allows some other code block to call:
<script>
var data = JSON.parse(document.getElementById('gl_data_in_html').children[0].innerHTML);
</script>
Then that same data can be used specify dimensions of the individual blocks in @sveltejs/gl
. Here’s the full source of the app.
VizRApp.svelte
:
<script>
import { onMount } from 'svelte';
import * as GL from '@sveltejs/gl';
import Controls from './components/Controls.svelte';
export let title;
let color = '#ff3e00';
const light = {};
let w = 1;
let h = 1;
let d = 1;
let webgl;
export let options = {
labels: [],
values: []
};
export let ranges = {
labels: [ "width", "height", "depth" ],
min: [ 0.1, 0.1, 0.1 ],
max: [ 5.0, 5.0 , 5.0 ],
step: [ 0.1, 0.1, 0.1 ],
values: []
};
// initial view
let location = new Float32Array([ 0, 10, 5 ]);
let target = new Float32Array([0, 1, 0]);
const captureViewDirection = (loc, tgt) => {
console.log("location: ", loc, "\n", "target: ", tgt);
return "";
};
function adjustColor (clr, height = 1) {
const r = parseInt('0x' + clr.substr(1, 2), 16),
g = parseInt('0x' + clr.substr(3, 2), 16),
b = parseInt('0x' + clr.substr(5, 2), 16);
const hr = Math.floor(r * (height / 0.25)),
hb = Math.floor(b * (height / 0.25));
return Math.abs((((hr < 255) ? hr : r) << 16) + (g << 8) + ((hb < 255) ? hb : b));
}
const data = JSON.parse(document.getElementById('gl_data_in_html').children[0].innerHTML);
const heightmap = [];
const gridSizeX = 10;
const gridSizeZ = 10;
for (let z=0; z < data.length; z++) {
const xx = [];
for (const x of Object.getOwnPropertyNames(data[z])) {
xx.push(data[z][x])
}
heightmap[z] = xx;
}
let controlInit;
/* This is a helper callback to bind custom uniforms/attributes
* and to pass custom buffers. I inserted a hook directly in the
* @sveltejs/gl source for this purpose:
* https://github.com/Real-Currents/SvelteGL/tree/real/currents
*/
let process_extra_shader_components = (gl, material, model) => {
// console.log("Process Extra Shader Components");
const program = material.program;
};
let updateWorld = (event) => {
console.log(event);
};
onMount(() => {
let frame;
if (typeof controlInit === 'function') {
controlInit();
}
const loop = () => {
frame = requestAnimationFrame(loop);
light.x = 3 * Math.sin(Date.now() * 0.001);
light.y = 2.5 + 2 * Math.sin(Date.now() * 0.0004);
light.z = 3 * Math.cos(Date.now() * 0.002);
if (ranges['values'].length > 0) {
w = ranges['values'][0];
h = ranges['values'][1];
d = ranges['values'][2];
} else {
ranges['values'] = [ w, h, d ];
}
};
loop();
return () => cancelAnimationFrame(frame);
});
</script>
<style>
.controls {
margin-top: -160px;
height: 128px;
}
</style>
<GL.Scene bind:gl={webgl} backgroundOpacity=1.0 process_extra_shader_components={process_extra_shader_components}>
<GL.Target id="center" location={[0, h/2, 0]}/>
<GL.OrbitControls maxPolarAngle={Math.PI / 2} {location} {target}>
{captureViewDirection(location, target)}
<GL.PerspectiveCamera {location} lookAt="center" near={0.01} far={1000}/>
</GL.OrbitControls>
<GL.AmbientLight intensity={0.3}/>
<GL.DirectionalLight direction={[-1,-1,-1]} intensity={0.5}/>
{#each Array(heightmap.length) as _, k}
{#each Array(heightmap[k].length) as _, i}
<!-- box -->
<GL.Mesh geometry={GL.box({ x: 0, y: 0, z: 0 , w: (gridSizeX / heightmap[i].length), h: (1 * heightmap[k][i]), d: (gridSizeZ / heightmap.length) })}
location={[ (-(gridSizeX / 2) + (i * (gridSizeX / heightmap[0].length))), 0, (-(gridSizeZ / 2) + (k * (gridSizeZ / heightmap.length))) ]}
rotation={[ 0, 0, 0]}
scale={[ w, h, d]}
uniforms={{ color: adjustColor(color, heightmap[k][i]) }}
/>
{/each}
{/each}
<!-- moving light -->
<GL.Group location={[light.x,light.y,light.z]}>
<GL.Mesh
geometry={GL.sphere({ turns: 36, bands: 36 })}
location={[0,0.2,0]}
scale={0.1}
uniforms={{ color: 0xffffff, emissive: 0xff0000 }}
/>
<GL.PointLight
location={[0,0,0]}
color={0xff0000}
intensity={0.6}
/>
</GL.Group>
</GL.Scene>
<Controls
bind:init={controlInit}
bind:color={color}
bind:options={options}
bind:rangeOptions={ranges}
bind:rangeValues={ranges.values}
bind:viewLocation={location}
bind:viewTarget={target}
title={title}
on:move={(event) => updateWorld(event)}/>
Controls.svelte
:
<script>
import { createEventDispatcher } from 'svelte';
export let title;
export let color = '#ff3e00';
export let options = [];
export let rangeOptions = [];
export let rangeValues = [];
export let viewLocation, viewTarget;
let dispatch = createEventDispatcher();
let formatPlayTime = (time) => "" + (new Date(time).toString());
let mouse_x = 0, mouse_y = 0, mouse_down = false, mouse_disabled = false;
let navContext;
let sinceLastMovementEvent = 0;
let isFullscreen = false;
let toggleFullscreen = function () {};
export const init = function () {
console.log("Initializing Controls...");
document.querySelectorAll('.controls h4').forEach(c => {
console.log(c);
const scrollLength = 3 * window.innerHeight / 4;
c.addEventListener('click', function (event) {
let scrollInterval = 33;
let scrollTime = 533;
let scrolled = 0
const startScroll = setInterval(function () {
if (scrolled < scrollLength) {
scroll({top: scrolled, left: 0});
}
scrolled += Math.floor(scrollLength / (scrollTime / scrollInterval));
}, scrollInterval);
});
c.title = "Click To See Article";
});
document.querySelectorAll('canvas').forEach(c => {
console.log(c);
toggleFullscreen = () => {
if (!isFullscreen) {
isFullscreen = true;
c.parentElement.className += " fullscreen"
for (const control of document.getElementsByClassName("controls")) {
control.className += " fullscreen";
}
} else {
isFullscreen = false;
c.parentElement.className = c.parentElement.className.replace("fullscreen", '');
for (const control of document.getElementsByClassName("controls")) {
control.className = control.className.replace("fullscreen", '');
}
}
}
c.addEventListener('keydown', function (event) {
const kbEvent = (event || window['event']); // cross-browser shenanigans
if (((new Date()).getTime() - sinceLastMovementEvent) > 66) {
// console.log(kbEvent);
sinceLastMovementEvent = (new Date()).getTime();
if (kbEvent['keyCode'] === 32) { // spacebar
kbEvent.preventDefault();
return true;
} else if (kbEvent['keyCode'] === 38 || kbEvent['keyCode'] === 87) { // up || W
dispatch('forward');
kbEvent.preventDefault();
return true;
} else if (kbEvent['keyCode'] === 40 || kbEvent['keyCode'] === 83) { // down || S
dispatch('backward');
kbEvent.preventDefault();
return true;
} else if (kbEvent['keyCode'] === 37 || kbEvent['keyCode'] === 65) { // left || A
dispatch('left');
kbEvent.preventDefault();
return true;
} else if (kbEvent['keyCode'] === 39 || kbEvent['keyCode'] === 68) { // right || D
dispatch('right');
kbEvent.preventDefault();
return true;
} else {
console.log('Keyboard Event: ', kbEvent['keyCode']);
return false;
}
}
});
c.addEventListener('wheel', function (event) {
const wheelEvent = (event || window['event']);
if (((new Date()).getTime() - sinceLastMovementEvent) > 66) {
sinceLastMovementEvent = (new Date()).getTime();
if (wheelEvent.deltaY < 0) {
dispatch('up');
} else if (wheelEvent.deltaY > 0) {
dispatch('down');
}
}
// wheelEvent.preventDefault();
});
});
};
</script>
<style>
.controls h4 {
color: black;
cursor: pointer;
pointer-events: all;
}
</style>
<div class="controls right">
<h4>{ title }</h4>
{#if (options['labels'].length > 0 && options['values'].length > 0)}
{#each options['values'] as option, o}
<label>
<input type="checkbox" bind:checked={option.value} /> {options['labels'][o]}
</label><br />
{/each}
{/if}
{#if (!!color)}
<label>
<input type="color" style="height: 40px" bind:value={color}>
</label>
{/if}
{#if (rangeOptions['labels'].length > 0 && rangeValues.length > 0)}
{#each rangeValues as option, o}
<label>
<input type="range" bind:value={option} min={rangeOptions['min'][o]} max={rangeOptions['max'][o]} step={rangeOptions['step'][o]} /><br />
{rangeOptions['labels'][o]}({option})
</label><br />
{/each}
{/if}
<label>
<button on:click="{toggleFullscreen}">{((isFullscreen) ? 'minimize' : 'maximize')}</button>
</label>
</div>