Threlte 3D-Dataviz Tutorial

Part 3: Interactivity

Welcome to the third and final part of our 3D data visualization tutorial! In this section, we will focus on interactivity, a crucial aspect of 3D visualizations that allows users to explore and analyze data in real-time. We'll cover a range of interactive functionalities, from basic camera control to more advanced features like data filtering, hovering effects, and tooltips. Let's pick up where we left off in Part 2!

Camera Interactions

Up until now, our camera has been fixed in position. However, with just a few lines of code, we can easily enable mouse control over the camera's position. To achieve this, we'll include the <OrbitControls> component as a child of our existing <T.PerspectiveCamera> component. Open the Camera.svelte file and add the OrbitControls as a child, like this:
#Camera.svelte

<script>
    import { T } from '@threlte/core'
    import { OrbitControls } from '@threlte/extras';
</script>


<T.PerspectiveCamera
    makeDefault
    position={[22, 15,22]}
    on:create={({ ref }) => {
        ref.lookAt(0, 0, 4);
    }}>	
    <OrbitControls
    enableDamping
    target={[0, 4, 0]}
    />
</T.PerspectiveCamera>
Now, you can drag, zoom, and rotate the camera to explore the data visualization from different angles. The enableDamping property adds a smooth camera movement, and the target property takes an array with the interactive camera's target point.

Setting up the data filter controls

The current data visualization might appear cluttered. To get a clearer view, let's display the rainfall data for each year separately. We'll create a filter to achieve this by adding a radio input for each year. Since this requires basic HTML elements, we'll set them up outside the <Canvas> component to keep things organized. Open the +page.svelte file and add the radio input. Also, set the z-index to 1 in the CSS block to ensure the radio input appears on top of the canvas component.

Open the +page.svelte file and inside the script tag declare a variable named selectedYear and set it to “all”. Now we can set-up the radio input and bind the input values to selectedYear value. Lastly we should not forget to, set the z-index of our radio-inputs class to 1 in the CSS tag to ensure the radio input appears on top of the canvas component.

We will need this value eventually inside our Bar.svelte component. To make the selectedYear value available there we can pass it as a prop from +page.svelte to Scene.svelte and then again to Bar.svelte.

Your +page.svelte should now look like this:
#+page.svelte
        
<script>
    import { Canvas } from '@threlte/core'
    import Scene from './Scene.svelte'
    let selectedYear = "all";
</script>

<div class="radio-inputs">
    <label>
        <input type="radio" bind:group={selectedYear} value={2010} /> 2010
    </label>
    <label>
        <input type="radio" bind:group={selectedYear} value={2011} /> 2011
    </label>
    <label>
        <input type="radio" bind:group={selectedYear} value={2012} /> 2012
    </label>
    <label>
        <input type="radio" bind:group={selectedYear} value={'all'} /> All
    </label>
</div>

<div class="canvas-wrapper">
    <Canvas>
        <Scene {selectedYear} />
    </Canvas>
</div>

<style>
    .canvas-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
    }

.radio-inputs {
    z-index:1
    }
</style>
Your Scene.svelte should now include the following lines of code:
#Scene.svelte
        
<script>
.....
    export let selectedYear
....
</script>

....
{#each data as d}
    <Bar
        selectedYear={selectedYear}
        rain={d.rain}
        year={d.year}
        x={xScale(d.month)}
        y={ yScale(d.rain)}
        z={zScale(d.year)}
        color={'#B392AC'}
        castShadow
    />
{/each}

Applying the filter to our chart

Now thats out of the way, we can focus on the actual magic that happens inside the Bar.svelte component. First, declare the selectedYear value and the year value from the data source. Now to create a smooth transition when filtering data, we'll use Svelte motion's spring feature. Import spring from svelte/motion to set up a spring for the height value, which corresponds to the bar height in the chart. Replace the y value in the mesh with the new height value. Make sure to use a $ symbol before height to subscribe to changes in its value.

Now, add an if statement inside the script tag with the filtering logic as described below.
Bar.svelte

<script>
    import { T } from '@threlte/core';
	import { spring } from 'svelte/motion'

	export let selectedYear;
	export let year;
    export let rain;
	export let x;
	export let z;
	export let y;
	export let color;

    const height = spring(y)

    $: if (year === selectedYear ) {
        height.set(y)
    }
    else if (selectedYear == 'all') {
            height.set(y)
    }
    else {
            height.set(0.01)
    }
</script>

<T.Mesh
	position.x={x}
	position.y={($height /2)}
	position.z={z}
	scale.y={$height}
    castShadow
    receiveShadow
>
	<T.BoxGeometry />
	<T.MeshStandardMaterial {color} />
</T.Mesh>
Well done! You can now filter the data by year!

Hover effect

To enhance user experience, we'll add a hover effect and a tooltip that shows the amount of rain per month when you hover your mouse over a bar. To enable interactivity in Threlte, import interactivity from threlte extras and call the interactivity function inside the Bar.svelte component.
Now, let's create another spring called hover and set it to 0. We'll add pointerenter and pointerleave event listeners to the mesh of our bars and update the hover value accordingly. Next we can add this hover value to the y position to make our bar float.
#Bar.svelte

<T.Mesh
  position.x={x}
  position.y={($height /2) + $hover }
  position.z={z}
  scale.y={$height}
  castShadow
  receiveShadow
  on:pointerenter={ hover.set(1)}
  on:pointerleave={ hover.set(0)}
>
Alright, By adding a subtle hover effect, the Visualization is really comming to life. You can now hover your mouse over a bar and it will gently rise. However in its current form, the effect also apllies to the bars that we filterd out. To prevent this from happening we can add a ternary statements to the event listeners so that the hover effect only works on the year that we have selected.

  on:pointerenter={() => year == selectedYear ? hover.set(1) : null}
  on:pointerleave={() => year == selectedYear ? hover.set(0) : null}

Tooltip

We will wrap up our data viz by creating a tooltip that will show the exact rainfall when you hover over a bar with your mouse. We can easily achieve this by combining the text placement lessons fromthe previous tutorial, with the interactivity we learned in this chapter.
We want our labels to appear on top of each bar. Let’s set up a text component. We can reuse the position parameters that we used to position and hover our bar elements. Next we add some styling to make sure the fontsize is ok and the numbers are placed at the center of the bar.
<Text
    text={rain + " ml"}
    position.x={x}
    position.y={($height + $hover ) +1.8}
    position.z={z}
    color="#230612"
    fontSize={0.8}
    fillOpacity={$hover}
    anchorX="center"
/>
Box.svelte should now look like this
Box.svelte

<script>
    import { T } from '@threlte/core';
    import { interactivity, Text } from '@threlte/extras'
    import { spring } from 'svelte/motion'

    interactivity()
    export let selectedYear;
    export let year;
    export let x;
    export let z;
    export let y;
    export let color;
    export let rain

    const height = spring(y)
    const hover = spring(0)

    $: if (year === selectedYear ) {
        height.set(y)
    }
    else if (selectedYear == 'all') {
        height.set(y)
    }
    else {
        height.set(0.01)
    }
</script>

<T.Mesh
    position.x={x}
    position.y={($height /2) + $hover }
    position.z={z}
    scale.y={$height}
    castShadow
    receiveShadow
    on:pointerenter={() => year == selectedYear ? hover.set(1) : null}
    on:pointerleave={() => year == selectedYear ? hover.set(0) : null}
>
    <T.BoxGeometry />
    <T.MeshStandardMaterial {color} />
</T.Mesh>

<Text
    text={rain + " ml"}
    position.x={x}
    position.y={($height + $hover ) +1.8}
    position.z={z}
    color="#230612"
    fontSize={0.8}
    fillOpacity={$hover}
    anchorX="center"
/>
And we're done. You can explore the full code here, feel free to fork and remix:

The end of part 3

This concludes our tutorial series on how to create 3D data visualizations with Svelte and Threlte. Throughout this series, we went from a blank canvas to a fully interactive 3D bar chart.

You now have the foundations to build your own 3D data visualization projects. If you have any questions or would like to see more content like this, don't hesitate to reach out. We hope you enjoyed the tutorial!