IxD:
Web:
Viz:
 

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.

heightmap 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 to 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 className="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>
Update navigation to read /content/posts/visualize-r.htmlAdd Svelte Main App

* Please contact John for details and demos: john@real-currents.com

Unless otherwise noted here, this content is licensed under the Creative Commons Attribution-ShareAlike 3.0 License, 2009-2025
Creative Commons Licence