Creating a rotating 3d cube in Tableau
After all the fun I’ve been having recently with coordinates and polygons, I decided to see if I could take the next step – into the 3rd dimension! (queue dramatic music and crappy 3d glasses). No, I don’t currently see any practical use, but it sounds like fun!
In principle, all that we are doing is mapping a 3d object to the 2d x and y coordinates that Tableau handles so well.
To keep things as simple as possible, I’m using a cube, the simplest of 3d objects to create.
I foresee the following steps / challenges:
- Creating the spreadsheet to define the cube
- Converting the 3d coordinates of the cube into 2d coordinates
- Adding a perspective calculation so that the sides further away are smaller and are distorted to create a 3d affect
- Adding occlusion – hiding faces that should not be seen
- Adding rotation
Getting out of Flatland
For 3d, we simply add the third dimension, z, which is perpendicular (90°) to both x and y:
Don’t worry, the forth dimension, time, will come later. And, not being a string theorist, I won’t go beyond that.
To make the maths as easy as possible, I’ll be working with a 2x2x2 cube centred around zero, such that all coordinates are a combination of +1 and -1:
I’ve numbered the nodes (corners) from 1 to 8. The order of these will become important when I want to fill in the sides.
If you stare at the cube above, it is impossible to be sure whether the face 1, 2, 3, 4 is at the front or back of the cube. Indeed, if your stare at it, the “front” face will flip every now and again. This is called the Necker Cube illusion. This is the reason why we need to add a perspective calculation so that the face at the back is smaller and looks further away.
The 3d coordinates for the 8 corners are:
NodeID | x | y | z |
1 | 1 | 1 | 1 |
2 | 1 | -1 | 1 |
3 | -1 | -1 | 1 |
4 | -1 | 1 | 1 |
5 | 1 | 1 | -1 |
6 | 1 | -1 | -1 |
7 | -1 | -1 | -1 |
8 | -1 | 1 | -1 |
I used the above as a basis for my starting spreadsheet and added the locations of the centres of each face as well as the order of nodes clockwise around each of them. See sample below
(I added a link to download the spreadsheet later. It’s not quite correct yet!)
The centres of each of the faces lie long one of the axes. The coordinates of each will be needed to later be able to calculate the distance of each face from the viewer and therefore be able to calculate which faces can be seen or should be occluded. I’ve called the coordinates x-start, etc. as these starting coordinates will be basis to work out coordinates after rotation.
Getting to the point
So much for theory, let’s start building in Tableau. Connecting to the above source, dragging x (not Sum(x)!) to columns, y to rows and adding a filter to only show Topology Type Node produced the expected square:
At least it’s kind of a square. Using the same trick with a blank PNG as a map background as per last time, square’s up the plot.
I can be deep too – adding perspective
Object that are further away look smaller. This is the key aspect of perspective that I want to include. I’m assuming that the viewer (“camera”) is simply sitting on the z-axis. I therefore need a calculation that will make the x and y distances smaller the further away the nodes are from the viewer. This can get very complicated, so I’m going with a very simplified version. I’ll use a parameter, p.Distance to cube, which will represent the position of the viewer relative to the centre of the cube (0,0,0).
The calculation for distance between the node, [z distance to each node], is [p.Distance to cube]–[Z-Start]. I will use an adjusted value for z later, so this is just a proof of concept for now. Note that this only calculates the distance to the node along the z-axis and ignores the distance along the x and y axes, but it’s good enough here.
To provide the scaling, divide the x and y values by [z distance to each node] to give the calculated fields [x 2d] and [y 2d]. I needed to redo my map background trick as I had changed the fields for x an y. Drawing a line along the Node IDs, I get the shape I was expecting. Adjusting the distance parameter works too. As I get closer, the difference between the sizes of the squares does what I would expect:
About face
Now I need to create the polygons for the faces. I dragged these to a new chart and got a big fat nothing right-clicking on the chart and selecting view data showed what was going on. The problem was in the source sheet. I needed to separate out the coordinates for the corners and middles of the faces. I split my source data into two and joined them on Node ID. There may well be a more efficient way of doing this, but it works here. The two tables are now:
Adding the faces to the viz, using [FaceID] for colour and setting the colours to 25% transparency showed me that I was almost there, but there was something amiss with the path around the faces :
Looking at my source sheet, I noticed that I was telling Tableau which Nodes surround each Face, but the order was not explicit, so I added another column for the path:
With [Node ID] in detail and [Path] on Path, I get what I’m looking for:
You got me spinning round
This is where the maths gets more complicated!
I made the face colours transparent so that I could see them all. The original idea was to be able to rotate the cube to make the other faces visible. For this, I added three new rotation parameters, one for each axis. A complete rotation in one axis is just over 6 Radians (2π, or 360°), so I set my parameters to go from -5 to +5 with increments of 0.5.
To work out the adjusted coordinates, I need to switch coordinate system, from Cartesian to Polar. In the source, the starting positions for the corners are x, y, z coordinates. What I need now are the starting angles and radii. This is because I need to add angles according to the parameters above. All radii will stay the same at around 1.732 (root 3, like Pythagoras, but in 3d).
Then it all went Pete Tong
This is the point at which I started to lose my grip on what I was doing. The calculations just weren’t coming out right. To check, I translated x coordinates to polar, then translated them straight back again, but I was losing the polarity. An x value of -1 was being converted to +1. In the end, and after a night’s sleep, I plotted a simple COS curve and the issue became obvious:
A negative angle of -45° (π/2) generates the same COS value as +45°, so when converting back, there is no way for Tableau to know if it should return -45°? So it doesn’t! Not only that, but any time a coordinate value is converted into an angle then converted back again, there are always two values that the reverse calculation can take.
This was starting to get beyond what I actually wanted to practice here. I naïvely thought that the rotation calculations would be relatively straightforward and that I’d be able to work them out from scratch. I spent hours on various websites, looking at different projection systems and conversion methods, but didn’t get very far. Often due to the issues above.
Solution? Steal!
I came across an excellent blog by Bora Beran. You can see the viz he created in the blog here on Tableau Public. The basis of his method is based on rotation matrices. Feel free to immerse yourself in the topic on Wikipedia.
I won’t go into the detail, but the resulting calculations are as below:
I added my perspective calculation back in to get the following:
All works well except for the rotation about the x-axis when I have perspective included. I suspect that this has to do with the rotation method rotating the axes, not just the nodes. This means that my distance calculation is not correct and therefore distorts the cube when rotating around that axis.
I think.
I’ll leave the control in the dashboard anyway so that it is available to play with.
About face
Now I want to colour the faces. However, they must be filled in in the correct order, otherwise faces at the back are shown in front of faces in front of them. This is called occlusion. I understand this to be a major topic in 3d graphics, especially for games, as there is no need to process graphical elements that cannot be seen and is therefore a method used to increase efficiency and speed.
Occlusion in this case is pretty straightforward, however. All I need to do is to sort the faces according to how far away their centres are from the viewer.
I expected this to be a repeat of all of the calculations above, but based on the centre coordinates for each face (the right-hand three columns in my Face table, above).
In the end, it was much simpler than that and those columns in my spreadsheet were not needed. The average distance of the nodes around the face is all I need to calculate the distance to the centre of the face. And this calculation can conveniently be handled within Tableau’s sort function! After dragging [Face ID] to colour, I clicked the pill’s drop-down to get the sorting options and sorted on the [z_rotated] field that I already had:
I set the opacity of the faces to 90% so that you can see a little of the back of the cube, dragged the sheet to a dashboard and it’s done!
You can see it on Tableau Public here.
Outstanding issues and questions:
- How to get the rotations and perspectives working for all axes?
- Is there a “best practice” for creating the source file? Could / should the nodes and faces have been in one sheet?