This library makes 3D geometry easy so that you can easily build 3D plots using ggplot. The below is a set of plots built using ggplot with the underlying data processed using this library and stitched together into a GIF to emphasise the 3D-ness.
The functions are a little low level so you may need some programming comfort to use it at this point. The code in the README.Rmd file should give you some ideas on how to use it. The source code is disgusting so go into it at your own peril. Some day it will be clean.
Bug reports are welcome! Feature requests are also welcome but this is a side project for me so they will be worked on at a lower priority and at author's discretion.
cat('')
rm(list = ls()) # functionality library(POV) library(ggplot2) # for markdown library(knitr)
# the point from where we'll view the data mFinalVieWCoordinates = cbind(10,10,10) # the point which dictates the screen on which we'll view the data mFinalScreenCoordinates = cbind(0,0,0) # the data we'll be plotting df = data.frame( x = c(0,0,0,0,1,1,1,1) + 2, y = c(0,0,1,1,0,0,1,1) + 2, z = c(0,1,0,1,0,1,0,1) + 2 ) # can't yet model infinity, which is actually what we want. 999 is just some large number nInfinitySubstitute = 999 # cosmetic cBackgroundColor = '#666666' opts_chunk$set( dev.args=list( bg=cBackgroundColor ) )
The data in this example is just a cube with eight corners specified. We're going to draw a 3D scatter plot of these 8 points with their projections / shadows on each of the three planes.
kable(df, format = 'markdown')
We calculate the shadow of the data on a plane as if the light source was very large, and very far away from the points, perpendicular to the plane. Think of it as the noon sun casting a shadow on the ground where the ground is the plane. In any other orientation, the shadow will be a little distorted. Play around with the parameters to see the results
We will do this thrice, once for each plane - the XY, the YZ, and the XZ plane.
The projection on the XY plane is:
# The screen coordinates, the object and the light source coordinates have to be # in line for the shadow to be correctly projected - analogous to the noon sun # casting a shadow on the ground. In this case, the line stretches parallel to # the Z axis with its x and y coordinates equal to the centre of the object. # You can try playing with the coordinates to understand the distortion in the # shadows # XY coordinates set to the midpoint of the object. # Since this is a projection on the XY plane, we set the Z cooridnate to 0. mScreenCoordinates = cbind( mean(range(df['x'])), mean(range(df['y'])), mean(range(df['z'])) ) mScreenCoordinates[3] = 0 # The light source is far away so we set Z cooridnate to a large value. # Not that the x and y coordinates are the same as the screen mOriginCoordinates = mScreenCoordinates mOriginCoordinates[3] = nInfinitySubstitute # calculate the equation of the plane for the screen # ( which, we know, should be the xy plane so 0x + 0y + z = 0 ) nScreenPlaneCoefficients = fGetPlaneAt( mOriginCoordinates = mScreenCoordinates, mNormalVector = mScreenCoordinates - mOriginCoordinates ) # Getting the shadow coordinates mdfprojectionxy = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients )
kable(mdfprojectionxy, format = 'markdown')
You can see the z coordinate being zero, and the x and y coordinates being four points repeated twice each with a minor change in the coordinates. This difference between the almost duplicates is because our light sources is not actually at infinity and the change in the z coordinates causes a really small change in the shadow's position as well. You can also think of it as you seeing a cube from very far away from the same point as where the light source is. You'd almost see only a square but in actuality there is just a little bit of the rest of the cube that also sticks out from begind the face that you can see. Try reducing the value of the nInfinitySubstitute variable and see the difference.
I'd plot it but it just looks like a square which you can sort of make out from the data so not bothering.
We similarly also calculate shadows on the other two planes
mScreenCoordinates = cbind( mean(range(df['x']))/2, mean(range(df['y']))/2, mean(range(df['z']))/2 ) mOriginCoordinates = mScreenCoordinates mScreenCoordinates[2] = 0 mOriginCoordinates[2] = nInfinitySubstitute nScreenPlaneCoefficients = fGetPlaneAt( mOriginCoordinates = mScreenCoordinates, mNormalVector = mScreenCoordinates - mOriginCoordinates ) mdfprojectionxz = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients )
mScreenCoordinates = cbind( mean(range(df['x']))/2, mean(range(df['y']))/2, mean(range(df['z']))/2 ) mOriginCoordinates = mScreenCoordinates mScreenCoordinates[1] = 0 mOriginCoordinates[1] = nInfinitySubstitute nScreenPlaneCoefficients = fGetPlaneAt( mOriginCoordinates = mScreenCoordinates, mNormalVector = mScreenCoordinates - mOriginCoordinates ) mdfprojectionyz = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients )
Now that we have the 3D coordinates of the shadows, we can calculate what it would look like when viewed from a particular point.
mdfprojectionxy = fGetTransformedCoordinates( mCoordinates = mdfprojectionxy, # data to project on the screen mOriginCoordinates = mFinalVieWCoordinates, # origin coordinates mScreenCoordinates = mFinalScreenCoordinates, # screen coordinates mVectorPointingUpOnScreen = c(0,0,1), # which way is up for the viewer iTreatAs = 1 # treat each point as an independent point, not part of a path or a polygon ) mdfprojectionyz = fGetTransformedCoordinates( mdfprojectionyz, mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mdfprojectionxz = fGetTransformedCoordinates( mdfprojectionxz, mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 )
We don't need to do the first step for the data, since its coordinates are what they are. So we directly calculate their project on the screen.
mdfprojection = fGetTransformedCoordinates( as.matrix(df), mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 )
Let's see what this looks like now
# building the plot p1 = ggplot() + # shadows geom_point( data = data.frame(mdfprojectionxy), aes( x = x, y = y ), color = '#FFFF00' ) + geom_point( data = data.frame(mdfprojectionyz), aes( x = x, y = y ), color = '#00FFFF' ) + geom_point( data = data.frame(mdfprojectionxz), aes( x = x, y = y ), color = '#FF00FF' ) + # data geom_point( data = data.frame(mdfprojection), aes( x = x, y = y ) ) + # cosmetics coord_fixed() + theme( panel.background = element_rect(fill = '#666666'), panel.grid = element_blank(), axis.title = element_blank(), axis.text = element_blank(), axis.ticks = element_blank(), axis.line = element_blank() ) print(p1)
Looks sort of correct but it's a little hard to comprehend it without some visual guides. Let's add some.
Let us first add some line segments parallel to the X axis
# This draws five line segments between x = 0 to x = 5, with the z coordinates # varying between 0 to 5 for each of those line segments and y set to 0 # All these line segments therefore lie in the 0x + y + 0z = 0 plane mdfprojectionx = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(0,0,i), c(5,0,i) )), mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectionx = do.call(rbind,mdfprojectionx)
Sort of looks like one of the planes, right?
ggplot() + geom_path(data = data.frame(mdfprojectionx), aes(x = x, y = y, group = group)) + coord_fixed()
Let's repeat it for the other two planes as well
mdfprojectiony = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(i,0,0), c(i,5,0) )), mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectiony = do.call(rbind,mdfprojectiony) mdfprojectionz = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(0,i,0), c(0,i,5) )), mFinalVieWCoordinates, mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectionz = do.call(rbind,mdfprojectionz)
# building the plot p2 = ggplot() + # axis / planes geom_path( data = data.frame(mdfprojectionx), aes( x = x, y = y, group = group ), color = '#FF0000' ) + geom_path( data = data.frame(mdfprojectiony), aes( x = x, y = y, group = group ), color = '#00FF00' ) + geom_path( data = data.frame(mdfprojectionz), aes( x = x, y = y, group = group ), color = '#0000FF' ) + # shadows geom_point( data = data.frame(mdfprojectionxy), aes( x = x, y = y ), color = '#FFFF00' ) + geom_point( data = data.frame(mdfprojectionyz), aes( x = x, y = y ), color = '#00FFFF' ) + geom_point( data = data.frame(mdfprojectionxz), aes( x = x, y = y ), color = '#FF00FF' ) + # data geom_point( data = data.frame(mdfprojection), aes( x = x, y = y ) ) + # cosmetics coord_fixed() + theme( panel.background = element_rect(fill = '#666666'), panel.grid = element_blank(), axis.title = element_blank(), axis.text = element_blank(), axis.ticks = element_blank(), axis.line = element_blank() ) print(p2)
Better. The shadows make sense, the planes make sense, the data makes sense.
I've wrapped up this code in a function,f3DScatterPlot
and demonstrate how this
same plot comes out if you move the origin coordinate. This function is available
form the library.
The GIF is made from stitching together the outputs of plots from different coordinates. You can get the individual plots from the functionality in this library but stitching them together will need you to have another program, like image magick.
f3DScatterPlot = function ( mScreenCoordinates, mOriginCoordinates, df ) { mFinalScreenCoordinates = mScreenCoordinates mFinalOriginCoordinates = mOriginCoordinates # 3d coordinates of the data shadows { mScreenCoordinates = cbind( mean(range(df['x'])), mean(range(df['y'])), mean(range(df['z'])) ) mOriginCoordinates = mScreenCoordinates mScreenCoordinates[3] = 0 mOriginCoordinates[3] = nInfinitySubstitute nScreenPlaneCoefficients = c( mScreenCoordinates - mOriginCoordinates, sum( ( mScreenCoordinates - mOriginCoordinates ) * mScreenCoordinates ) ) mdfprojectionxy = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients ) mScreenCoordinates = cbind( mean(range(df['x'])), mean(range(df['y'])), mean(range(df['z'])) ) mOriginCoordinates = mScreenCoordinates mScreenCoordinates[2] = 0 mOriginCoordinates[2] = nInfinitySubstitute nScreenPlaneCoefficients = c( mScreenCoordinates - mOriginCoordinates, sum( ( mScreenCoordinates - mOriginCoordinates ) * mScreenCoordinates ) ) mdfprojectionxz = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients ) mScreenCoordinates = cbind( mean(range(df['x'])), mean(range(df['y'])), mean(range(df['z'])) ) mOriginCoordinates = mScreenCoordinates mScreenCoordinates[1] = 0 mOriginCoordinates[1] = nInfinitySubstitute nScreenPlaneCoefficients = c( mScreenCoordinates - mOriginCoordinates, sum( ( mScreenCoordinates - mOriginCoordinates ) * mScreenCoordinates ) ) mdfprojectionyz = fGetProjectionsOnPlane( as.matrix(df), mOriginCoordinates, nScreenPlaneCoefficients ) } # projecting the shadows on the screen { mdfprojectionxy = fGetTransformedCoordinates( mdfprojectionxy, mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mdfprojectionyz = fGetTransformedCoordinates( mdfprojectionyz, mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mdfprojectionxz = fGetTransformedCoordinates( mdfprojectionxz, mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) } # projectin the data on the screen { mdfprojection = fGetTransformedCoordinates( as.matrix(df), mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) } # axis / plan gridlines { mdfprojectionx = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(0,0,i), c(5,0,i) )), mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectionx = do.call(rbind,mdfprojectionx) mdfprojectiony = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(i,0,0), c(i,5,0) )), mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectiony = do.call(rbind,mdfprojectiony) mdfprojectionz = lapply( 0:5, function(i) { mtransformed = fGetTransformedCoordinates( as.matrix(rbind( c(0,i,0), c(0,i,5) )), mOriginCoordinates = mFinalOriginCoordinates, mScreenCoordinates = mFinalScreenCoordinates, mVectorPointingUpOnScreen = c(0,0,1), iTreatAs = 1 ) mtransformed = cbind(mtransformed, group = i) mtransformed } ) mdfprojectionz = do.call(rbind,mdfprojectionz) } # building the plot ggplot() + # axis / planes geom_path( data = data.frame(mdfprojectionx), aes( x = x, y = y, group = group ), color = '#FF0000' ) + geom_path( data = data.frame(mdfprojectiony), aes( x = x, y = y, group = group ), color = '#00FF00' ) + geom_path( data = data.frame(mdfprojectionz), aes( x = x, y = y, group = group ), color = '#0000FF' ) + # shadows geom_point( data = data.frame(mdfprojectionxy), aes( x = x, y = y ), color = '#FFFF00' ) + geom_point( data = data.frame(mdfprojectionyz), aes( x = x, y = y ), color = '#00FFFF' ) + geom_point( data = data.frame(mdfprojectionxz), aes( x = x, y = y ), color = '#FF00FF' ) + # data geom_point( data = data.frame(mdfprojection), aes( x = x, y = y ) ) + # cosmetics coord_fixed() + theme( panel.background = element_rect(fill = '#666666'), panel.grid = element_blank(), axis.title = element_blank(), axis.text = element_blank(), axis.ticks = element_blank(), axis.line = element_blank() ) }
ctmpfile = tempdir() for ( z in (mFinalVieWCoordinates[3]-5):(mFinalVieWCoordinates[3]+5) ) { mDynamicOriginCoordinates = mFinalVieWCoordinates mDynamicOriginCoordinates[3] = z p1 = f3DScatterPlot ( mFinalScreenCoordinates, mDynamicOriginCoordinates, df ) + ggtitle(paste0('View from: (', paste(mDynamicOriginCoordinates, collapse = ','), ')')) ggsave( p1, file = paste0(ctmpfile, '/example_', formatC(z, width = 2, flag = '0'), '.png'), height = 10, width = 10, units = 'cm' ) } system( paste0( 'convert -delay 20 ', ctmpfile, '/example_*', ' ', ctmpfile, '/example.gif' ) ) if ( !interactive() ) { file.copy( paste0(ctmpfile, '/example.gif'), './README_files/figure-markdown_strict/example.gif' ) }
I do a lot of fooball related work so a couple of things I've put together using this library in the football context -
cat('<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Sneak peek into a slow moving side project - 3D views and slices of football pitches.<a href="https://twitter.com/hashtag/TidyTuesday?src=hash&ref_src=twsrc%5Etfw">#TidyTuesday</a> <a href="https://twitter.com/lhashtag/ggplot2?src=hash&ref_src=twsrc%5Etfw">#ggplot2</a> <a href="https://twitter.com/hashtag/RStats?src=hash&ref_src=twsrc%5Etfw">#RStats</a> <a href="https://t.co/BSEba2zs9T">pic.twitter.com/BSEba2zs9T</a></p>— The Come On Man (@thecomeonman) <a href="https://twitter.com/thecomeonman/status/1288103535066746882?ref_src=twsrc%5Etfw">July 28, 2020</a></blockquote><p></p>')
This was generated using geom_pitch
from my other library, CodaBonito
Here's a simple example to demonstrate usage. Play around with the arguments to get a feel for how it works.
library(CodaBonito) p1 = ggplot() + geom_pitch( mOriginCoordinates = cbind(-100, 0, 50), mScreenCoordinates = cbind(60, 0, 0) ) print(p1)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.