Turn on suggestions

Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.

Showing results for

- Home
- /
- SAS Communities Library
- /
- Creating a 3D Residual Plot with #D3Thursday

Options

- RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content

- Article History
- RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content

Views
5,360

#D3Thursday is back again with a one-off post. This time we will be looking at how we can use the popular 3D graphics library Three.js to create a 3D residual plot!

In this post we will take a look at how we can turn a list of coordinates into a 3D surface and how we can use 2D text elements in our 3D visualization to create tick values.

Our 3D residual plot takes 4 inputs: the first covariate (x-dimension), the second covariate (z-dimension), the predicted value for a given covariate pairing (z-dimension), and the residual for a given covariate pairing (z-dimension). How then do we take a series of (x, y, z) coordinates and create a surface from them? The standard way to do this is by creating a series of 3 triangles connecting points in 3-dimensional space. In order to do this we will assume all values are defined for the 2D matrix formed by covariate 1 and covariate 2. In other words, if the values of covariate 1 are [1, 2, 3] and the values of covariate 2 are [4, 5, 6] then there must be a predicted value for all 9 pairings of covairate 1 and covariate 2. If this assumption is met, creating a surface from our data set becomes trivial.

let index, geometry, mesh;

GEOMETRIES = [];

for (let i = 0; i < COV2.length - 1; i++) {

for (let j = 0; j < COV1.length - 1; j++) {

index = i * COV1.length + j;

for (let k = 0; k < 2; k++) {

// Create geometry

geometry = new THREE.Geometry();

if (k) {

geometry.vertices.push(

new THREE.Vector3(

COV1_SCALE(DATA[index + COV1.length + 1].cov1),

RESULT_SCALE(DATA[index + COV1.length + 1].pred),

COV2_SCALE(DATA[index + COV1.length + 1].cov2)

)

);

} else {

geometry.vertices.push(

new THREE.Vector3(

COV1_SCALE(DATA[index].cov1),

RESULT_SCALE(DATA[index].pred),

COV2_SCALE(DATA[index].cov2)

)

);

}

geometry.vertices.push(

new THREE.Vector3(

COV1_SCALE(DATA[index + 1].cov1),

RESULT_SCALE(DATA[index + 1].pred),

COV2_SCALE(DATA[index + 1].cov2)

)

);

geometry.vertices.push(

new THREE.Vector3(

COV1_SCALE(DATA[index + COV1.length].cov1),

RESULT_SCALE(DATA[index + COV1.length].pred),

COV2_SCALE(DATA[index + COV1.length].cov2)

)

);

geometry.faces.push(new THREE.Face3(0, 1, 2));

geometry.computeFaceNormals();

// Create mesh from custom geometry and add to scene

mesh = new THREE.Mesh(geometry, PREDICTION_MATERIAL);

GEOMETRIES.push(mesh);

SCENE.add(mesh);

}

}

}

In order to create a triangle geometry there are a few steps:

- Create a new geometry
- Push the vertices of the triangle
- Define the face of the triangle using the pushed vertices
- Compute the face normal
- Create a mesh from the geometry and add it to the scene

It should be noted that we are using a double sided material here so the order of the vertices doesn't matter. If you wanted to create a single sided material, then the order of the vertices in the face would determine the direction of the face normal and which side is lit.

There are several methods used to integrate text in Three.js, each with it's own trade offs. You could import a model of a 3D text object, create your own 3D model of a text object in Three.js, create a custom texture with your text on it, or use bitmap fonts mapped to buffer geometries. For this post, we will look at a simpler method than any of these. We will use standard HTML div elements overlayed on our Three.js canvas to create the illusion of 2D text elements in our 3D space.

// Append axes values

d3.select("body")

.selectAll(".x-axis-text")

.data(COV1_TICKS)

.enter()

.append("div")

.classed("axis-text x-axis-text", true)

.html(function(d) {

return Math.round(d * 100) / 100;

})

.each(function(d) {

this.position = new THREE.Vector3(COV1_SCALE(d), -0.53, +0.5);

})

.each(setXY);

...

// Render loop function

function render() {

...

// Update axis text positions

d3.selectAll(".axis-text")

.each(setXY);

}

...

// Set XY values of element in window given XYZ in world coordinates

function setXY() {

const vector = this.position.clone();

const canvas = RENDERER.domElement;

vector.project(CAMERA);

vector.x = Math.round(

(0.5 + vector.x / 2) * (canvas.width / window.devicePixelRatio)

);

vector.y = Math.round(

(0.5 - vector.y / 2) * (canvas.height / window.devicePixelRatio)

);

d3.select(this)

.style("top", vector.y + "px")

.style("left", vector.x + "px");

}

The only challenge in using div elements for the text elements in our Three.js visualization is mapping a given position in world coordinates (x, y, z) to screen coordinates (x, y). Luckily, Three.js makes the mathematics of this projection simple by providing the *project* method. This allows you to quickly convert from 3D space into a 2D coordinate system. All that's left after that is scaling the values output from the *project* method to the pixel size of your display. There is one problem with the result of the above code, however.

Because the axis text elements are rendered as div elements on top of the canvas the text elements don't disappear of they are obscured by the prediction surface or if the camera has zoomed past them. We can remedy this problem by toggling the *display* property on the divs depending on the camera position and whether not they are obscured from view.

// Update axis text positions

d3.selectAll(".axis-text")

.style("display", setDisplay)

.each(setXY);

...

// Determine whether to display text based on text position and camera position

function setDisplay() {

const vector = this.position.clone();

const camToText = CAMERA.position.distanceTo(vector);

vector.project(CAMERA);

// Update text raycaster with new camera location

TEXT_RAYCASTER.setFromCamera(vector, CAMERA);

// Get intersections from raycaster

const intersects = TEXT_RAYCASTER.intersectObjects(GEOMETRIES);

let intersected = false;

for (let i = 0; i < intersects.length; i++) {

if (intersects[i].distance < camToText) {

intersected = true;

break;

}

}

return intersected || CAMERA.position.length() < this.position.length()

? "none"

: "initial";

}

This method isn't perfect, since it only checks if the origin position of the text element is obscured, not every position the element occupies. This solution is, however, highly performant and simpler than the integration of 3D text elements. For more information on some of the other methods of creating text elements in your Three.js project check out the link in the additional resources section below.

This post was a one-off, but in the coming weeks #D3Thursday will return with a new series of posts with new charts focusing on the challenge of visualizing clinical trials data! Stay posted for additional #D3Thursday content and be sure to reach out with any ideas you have for visualizations or interesting data you think deserve a post!

Remember to follow these articles on the SAS Communities Library and the #D3Thursday Twitter hashtag. Comment below or Tweet your questions and comments!

Comments

10-10-2019
01:42 PM

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content

10-10-2019
01:42 PM

Awesome, thanks a lot.

10-22-2019
04:50 PM

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content

10-22-2019
04:50 PM

Dear Ryan, please help me with replacing the simulated data by a real data array (same variable names as yours) to make it work as DDC in Visual Analytics.

Thanks

Thanks

10-24-2019
09:23 AM

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content

10-24-2019
09:23 AM

Hi Arne,

Thanks for reaching out. A technical consultant from your local SAS office is reaching out to assist you in using your own data in this visualization.

Ryan

**Don't miss out on SAS Innovate - Register now for the FREE Livestream!**

Can't make it to Vegas? No problem! Watch our general sessions LIVE or on-demand starting April 17th. Hear from SAS execs, best-selling author Adam Grant, Hot Ones host Sean Evans, top tech journalist Kara Swisher, AI expert Cassie Kozyrkov, and the mind-blowing dance crew iLuminate! Plus, get access to over 20 breakout sessions.

Data Literacy is for **all**, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.

Article Labels

Article Tags

- Find more articles tagged with:
- D3 Thursday