Threlte 3D-Dataviz Tutorial

Creating your first 3D data visualization using Threlte and D3

Over the past few years, Svelte combined with D3 have proven to be an excellent combination for the creation of compelling data visualizations. The declarative nature of Svelte combined with the powerful mathematical capabilities provided by D3 allows you to whip out intricate datavizes without getting lost in the sometimes complicated syntax of D3. So far this power combo has mainly been limited to 2D data vizualizations that either use SVG or Canvas. This however might all change with the maturation of the Threlte library.

What is Threlte?

Basically Threlte is a 3D framework for the web that is built on top of Svelte and Three.js. Three.js is a JavaScript library that streamlines the process of creating 3D animations and visualizations on the web. It provides a powerful set of tools for creating and manipulating 3D objects, and is built on top of WebGL, a web standard for rendering 3D graphics that harnesses the power of the users GPU. Now Threlte allows you to create and render three.js scenes declaratively and state-driven in Svelte apps.” This means that you can use all the powerfull components of the three.js library, while taking advantage of the declarative syntax from Svelte. Threlte provides strictly typed components that allow you to quickly and easily build three.js scenes with deep reactivity and interactivity right out-of-the-box.

But wait, why would you consider creating 3D data visualizations?

For a long time, 3D data visualizations have had a poor reputation and they often remind us about the 3d flying piecharts you could make in the older versions of microsoft office. However, the field of data visualization has progressed significantly since then. Yes, 3D can distort perception and should therefore not be used for accuracy-heavy projects, but when you are looking to convey an idea or captivate your audience 3D graphs can be a great addition to your toolbox.

What to expect in this article?

  1. Setting-up Threlte locally
  2. Making our first scene with Threlte.
  3. Making a 3D barchart plot with Threlte

What not to expect:

This is not a beginner's guide to Svelte or D3. Furthermore it assumes a foundational understanding of HTML, CSS, and JavaScript. Prior knowledge of three.js is a nice, but for the scope of this tutorial not required. Are you new to Svelte or D3 or Three.js? Check-out these resources before you continue.
  1. Svelte: Svelte official tutorial
  2. D3: D3 official tutorial
  3. Three.js: Three.js tutorial
Important Note:
This tutorial makes use of Threlte 6 which is still in active development and you should expect breaking changes. This version of Threlte is currently available as a preview release and can be installed with the tag 'next'.

1. How to set-up Threlte locally

Creating a local environment

In this part we will look into how to set up Threlte on a local environment. To follow along, you'll need Node.js ≥v6.x and npm installed and set up with IDE of your preference.
We start by setting up our Svelte environment by initializing Svelte-kit, the official application framework from Svelte. Open the terminal in your project folder and execute the following commands:
npm create svelte@latest ThrelteTutorial
You will be prompted with the set-up wizzard of Svelte. Create a skeleton project and navigate into the folder that was just created with:
cd ThrelteTutorial
And install Threlte/core, Threlte/extras, threejs and D3 as follows:
npm install three d3 @threlte/core@next @threlte/extras@next 
Next, within the project folder open the vite.config.js file and replace the content with the following:
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'

export default defineConfig({
	plugins: [sveltekit()],
	ssr: {
		noExternal: ['three']	}
});
Finally inside ./src/ create a new folder named lib with an App.Svelte component. Import it into the +page.svelte.
Now initialize the project with:
npm run dev
For now this is all we need to start making 3D data visualizations in the browser!

Creating our first scene

The Canvas component

Now we have our environment ready, we can start with setting up a <Canvas> component. This component stands at the root of your Threlte scene.
<Canvas> sets up a renderer and predefines some useful configurations while also establishing a default camera and lighting setup, so you don't have to worry about those details when you start.

It's good practice to encapsulate your entire scene within the <Canvas> component. To do this, let's create a Svelte file named Scene.svelte and include it in the App.svelte file.
#App.svelte

<script>
    import { Canvas } from '@threlte/core'
    import Scene from './Scene.svelte'
</script>
<Canvas>
    <Scene />
</Canvas>

Adding an object to the scene

Let's move on to adding an object to our scene. As the goal of this tutorial is to create a bar chart, we'll start by creating a single bar. Setting up an object in Threlte requires three components - a geometry, a material, and a mesh. The geometry defines the structure of the object, while the material determines its appearance. Finally, the mesh combines the geometry and the material into a single object that can be placed in the canvas.

With the traditional three.js library creating a box object will look like this:
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh()

// "attaching" the objects to the mesh
mesh.geometry = geometry
mesh.material = material
Now let's have a look at how we can do this in a declarative set-up using Threlte.

The main building block of the Threlte application is the <T> component. It's a generic component that we can use to render any Three.js object in a declarative way. By using Threlte we can rewrite the equivalent of the previous three.js code as follow:
#Scene.svelte

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

<T.Mesh>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>
Congratulations! You just made your first Threlte component. You should now see a black square. There is still some work to do to make this into a 3d data visualization. Let's make our scene a little more interesting by adding some color. We can easily achieve this by setting the color argument of the material component to a color value. The color didn't change! Don't worry, all we need is a little extra lighting, we'll fixt this soon.
<T.MeshStandardMaterial color={'#B392AC'} />
firstImage

Camera and light

Adding a custom Camera to our scene

As mentioned before, the <Canvas> component comes with a default camera. However for our scene we want to have a bit more control over our camera. We can do this by adding a Perspective camera component to our scene as follows:
#Scene.svelte

<script>
    import { T } from '@threlte/core'
</script>
    
<T.Mesh>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.PerspectiveCamera/>

We can position the camera by providing the component with a position argument that takes an array with x, y and z coordinates. By default, the camera will look in a horizontal straight line. To make sure that the camera is pointed to our cube, we need to instruct the camera to look at the center of the scene as soon as it is created.
To accommodate this we will need to use a on:create event combined with a reference and the lookAt method.
#Scene.svelte

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

<T.Mesh>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.PerspectiveCamera 	
    makeDefault
    position={[20, 20, 20]}
    on:create={({ ref }) => {
    ref.lookAt(0, 1, 0);
}}/>
You should now see a small hexagon on your screen.

Adding custom lighting to our scene

To make this scene a little bit more interesting we need to add some custom lights.
The most commonly used light objects in Three.js are the ambient light and directional light. The ambient light globally illuminates all objects in the scene equally, but it does not cast shadows as it does not have a direction. This is where the directional light component comes in. The directional light emanates from a specific point and therefore requires a positional argument. By adding an intensity argument, we can adjust the light emitted by our light sources.

Let's add these lights to our scene as follows:
#Scene.svelte

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

<T.Mesh>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.PerspectiveCamera
    makeDefault
    position={[20, 20, 20]}
    on:create={({ ref }) => {
        ref.lookAt(0, 1, 0);
    }}
/>

<T.AmbientLight intensity={0.2} />
<T.DirectionalLight position={[10, 5, 0]} />
firstImage

Transforming objects and casting shadows

Transforming objects

We can move the box by passing a position array as an argument to the T.Mesh component, similar to how we moved the camera and directional light earlier. However, we can also set the y position directly via a pierced prop using the dot notation position.y = 1 .This method has the computational benefit of not having to reassess the x and z positions when re-rendering the scene.

In addition to position, we can also use arguments to rotate and scale our object. Since our goal is to create a bar chart, let's scale the y-axis of the object to transform our cube into a single bar.
#Scene.svelte

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

<T.Mesh scale.y={5}>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.PerspectiveCamera
    makeDefault
    position={[20, 20, 20]}
    on:create={({ ref }) => {
        ref.lookAt(0, 1, 0);
    }}
/>

<T.AmbientLight intensity={0.2} />
<T.DirectionalLight position={[10, 5, 0]} />

Adding a floor and casting shadows

In order to provide a more realistic sense of depth and dimensionality, we will add a floor to our 3D scene. To achieve this, we need to create a new Mesh object using a CircleGeometry and StandardMaterial. When creating the CircleGeometry, it is necessary to provide a constructor argument for the radius and number of segments that the circle will be constructed from. For our current purpose, 72 segments will suffice. Finally we will need to rotate the circle along the x axis so we can use it as a floor.
#Scene.svelte

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

<T.Mesh scale.y={5}>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.Mesh rotation.x={-90 * (Math.PI / 180)}>
    <T.CircleGeometry args={[30, 72]} />
    <T.MeshStandardMaterial />
</T.Mesh>


<T.PerspectiveCamera
    makeDefault
    position={[20, 20, 20]}
    on:create={({ ref }) => {
        ref.lookAt(0, 1, 0);
    }}
/>

<T.AmbientLight intensity={0.2} />
<T.DirectionalLight position={[10, 5, 0]} />
To enhance our scene further, let's add some shadows. We want the light to cast the shadow of the bar onto our newly created floor. To achieve this, we need to add a castShadow argument to our directional light and to our bar component. Additionally, we need to add a receiveShadow argument to the floor component and bar component. With these simple modifications, our scene now has shadows.
#Scene.svelte

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

<T.Mesh castShadow scale.y={5}>
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
</T.Mesh>

<T.Mesh receiveShadow rotation.x={-90 * (Math.PI / 180)}>
    <T.CircleGeometry args={[30, 72]} />
    <T.MeshStandardMaterial />
</T.Mesh>


<T.PerspectiveCamera
    makeDefault
    position={[20, 20, 20]}
    on:create={({ ref }) => {
        ref.lookAt(0, 1, 0);
    }}
/>

<T.AmbientLight intensity={0.2} />
<T.DirectionalLight castShadow position={[10, 5, 0]} />
firstImage

The Data, rainfall in Heathrow

Importing Data

To create a data visualization, we first need to import the relevant data into our scene. For this visualization, we will be analyzing the rainfall in Heathrow from 2010 to 2012. We can easily load the data in the correct format by using csvParse and autoType functions from the D3.js library. To begin, we need to import the D3.js library into our scene with:
import * as d3 from 'd3'
To ensure that the data is available before we start rendering the scene, we need to use an asynchronous function placed in the onMount Svelte lifecycle function. For this, we can use d3.csv to read our data from a CSV file. Add the followin lines inside the script tag of our scene component:
#Scene.svelte

async function load() {
    const url= ‘https://raw.githubusercontent.com/stefanpullen/TutorialData/main/heathrow.csv’;
    const response = await fetch(url);
    if(response.ok) {
        data = csvParse(await response.text(), autoType);
        }
    }

onMount(() => {
    load();
});

D3 and 3D

Setting up D3 scales

Now that we have imported the data into our scene, we can start using the powerful scales of the d3.js library to map the values of our dataset to the Three.js canvas. We can use different types of scales depending on the data. For this visualization, we will use a linear scale for the y-axis and a categorical scale band for the x- and z-axes.

To set up a scale in d3, we need to provide a domain and a range. The domain specifies the extent of the data we want to map. For the y-axis, we will use the temperature range (minimum and maximum) of our dataset, which we can easily obtain with the d3.extent method. For the x- and z-axes, we will use the unique values of the corresponding fields in the correct order.

Once we have defined our domains, we need to specify where these values will be mapped to on the screen by setting up the range of the scales. For our example, we will set the x-range from -10 to 10, the y-range from 0 to 5, and the z-range from 0 to 4. These are just relative values in Three.js, so you can adjust them according to your needs, as long as you also adjust your camera accordingly.

As we are loading the data asynchronously, we can take advantage of Svelte's reactivity to set our scales. This ensures that the scales will be updated as soon as the data is ready.
$: xScale = d3
    .scaleBand()
    .domain(data.map((d) => d.month))
    .range([-10, 10]);

$: yScale = d3
    .scaleLinear()
    .domain(d3.extent(data.map((d) => d.rain)))
    .range([0, 5]);

$: zScale = d3
    .scaleLinear()
    .domain(d3.extent(data.map((d) => d.year)))
    .range([0, 4]);

Creating a 3d bar chart

Let's create our first 3D bar chart now that everything is set up. It's a simple process that involves looping over our dataset using a Svelte Each block and utilizing the scales we created to determine the position and size of each bar.

It's important to keep in mind that unlike SVG, Threlte centers the origin point on the object. Therefore, we'll need to adjust the y position by half of the bar's size to compensate for this.

Here's what the code should look like:
{#each data as d}
    <T.Mesh
        position.x={xScale(d.month)}
        scale.y={yScale(d.rain)}
        position.z={zScale(d.year)}
        position.y={yScale(d.rain) / 2}
        castShadow
    >
        <T.BoxGeometry />
        <T.MeshStandardMaterial color={'#B392AC'} />
    </T.Mesh>
{/each}
Congratulations on successfully creating your first 3D data visualization with Threlte! However, the current state of the chart may not be as visually appealing or informative as you would like it to be. In the upcoming tutorial, we will enhance this visualization by incorporating axes, annotations, and interactivity, which will make the chart more useful and engaging for your audience.
firstImage

Refactoring

Creating svelte components

Before we wrap up, we can improve the readability of this project by setting up reusable components. First let's set up the bars component,
Bar.svelte

<script>
    import { T } from '@threlte/core';
    export let x;
    export let z;
    export let y;
    export let color;
</script>


<T.Mesh
    position.x={x}
    position.y={y /2}
    position.z={z}
    scale.y={y}
    castShadow
    receiveShadow
>
    <T.BoxGeometry />
    <T.MeshStandardMaterial {color} />
</T.Mesh>
The same logic can be applied to the camera, light and floor. After creating these components, load them into the scene. The final result should look like this:

The end of part 1

Great job! You have successfully completed part 1 of the Threlte dataviz tutorial. Throughout this section, we covered essential topics such as setting up a local environment, creating our first scene in Threlte, and ultimately, building our first 3D data visualization.

In the upcoming tutorial, we will further enhance our 3D visualization by adding annotations, animations, and interactivity. These advanced features will make our visualization more engaging and interactive, enabling us to convey our data more effectively.

See you in part2!