A glimpse of where we’re going:
# For some nice easy plotting
library(ggplot2)
# For some "perlin noise" to rotate our eggs.... huh?
library(ambient)
head(diamonds, 3)
## # A tibble: 3 × 10
## carat cut color clarity depth table price x y z
## <dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
## 1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
## 2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
## 3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
ggplot()
makes your canvas and
geom_<shape>()
puts your art on the canvas. The
aes()
(aesthetic) is where you tell ggplot()
your x
and y
.
ggplot(diamonds, aes(x = carat, y = price)) +
geom_point()
You can customize the plots p easily after they’re made by
+
-ing things on. Us as beautimus artists might not want our
canvas to have all the data looking things. We can use
theme_void()
to clean everything out.
ggplot(diamonds, aes(x = carat, y = price)) +
geom_point() +
theme_void()
Last 2 ggplot2
tactics to know about know.
aes()
in
the geom_<shape>()
.# my diamond is big but cheap
my_diamond <- data.frame(carat = 5, price = 5000)
ggplot() +
geom_point(data = diamonds, aes(x = carat, y = price)) +
geom_point(data = my_diamond, aes(x = carat, y = price), color = "#ff8200") +
theme_void()
Okay, using all of this let’s make some eggs!
Hardcore research*:
*i’m oldschool so i used google instead of chatgpt… for this…
Result of research:
You can develop the shape of a hen egg, if you change the equation of a oval a little. You multiply y or y² by a suitable term t(x), so that y becomes smaller on the right side of the y-axis and larger on the left side. y(x=0) must not be changed. The equation of the ellipse e.g. \(\frac{x^2}{9} + \frac{y^2}{4} = 1\) change to \(\frac{x^2}{9} + \frac{y^2}{4} \dot{} t(x) = 1\)
…[example t(x)] \(t(x) = \frac{1}{1 - 0.2x}\)
Okay… we can definitely write a function for \(t(x)\)
# shoutout baker skateboards: https://www.youtube.com/watch?v=cdbfSfnRDX4
t_func <- function(x) {
1 / (1 - 0.2 * x)
}
How to make a function from this: \(\frac{x^2}{9} + \frac{y^2}{4} \dot{} t(x) = 1\)… that umm gets trickier. Maybe we go new school on this. I’ve ChatGPT’d:
this is a formula for an egg based on an ellipse formula: \(\frac{x^2}{9} + \frac{y^2}{4} \dot{} t(x) = 1\). write an R function that takes in a center point (x and y), an x radius, a y radius, and a function (named t) as parameters to draw an ellipse egg like this
How did I know what to ask? ….
I modified the returned code to suit our needs better.
egg_pts <- function(cx, cy, xr = 0.9, yr = 0.6) {
theta <- seq(0, 2 * pi, length.out = 100)
# Calculate x and y coordinates of points on the ellipse
x <- xr * cos(theta)
y <- yr * sin(theta)
# Adjust y coordinates using the t function
y <- y * t_func(x)
pts <- data.frame(x = x + cx, y = y + cy)
return(pts)
}
To use the function we’ll pass in a location for our egg to live.
pts <- egg_pts(0, 0)
head(pts)
## x y
## 1 0.9000000 0.00000000
## 2 0.8981880 0.04638725
## 3 0.8927593 0.09246533
## 4 0.8837358 0.13792937
## 5 0.8711538 0.18248293
## 6 0.8550640 0.22584193
Did it work….?
ggplot(pts, aes(x = x, y = y)) +
geom_point() +
labs(title = "it's... it's... beautiful!")
Holy moly we created egg.
But… we prolly don’t want dot eggs… instead of
geom_point()
we can use geom_polygon()
to
nicely connect them.
ggplot(pts, aes(x = x, y = y)) +
geom_polygon() +
theme_void() +
labs(title = "BOOM! egg :)")
You know what’s cooler than 1 egg….
pts1 <- egg_pts(0, 0)
pts2 <- egg_pts(1, 2)
ggplot() +
geom_polygon(data = pts1, aes(x = x, y = y)) +
geom_polygon(data = pts2, aes(x = x, y = y)) +
theme_void() +
labs(title = "oh nooo.... we squished em")
We can use coord_fixed(ratio = 1)
to tell ggplot we want
an “aspect ratio” of 1 to unsquish.
pts1 <- egg_pts(0, 0)
pts2 <- egg_pts(1, 2)
# some changes have to be made here...
ggplot() +
geom_polygon(data = pts1, aes(x = x, y = y)) +
geom_polygon(data = pts2, aes(x = x, y = y)) +
theme_void() +
coord_fixed(ratio = 1)
Let’s get forward thinking. I want to have as many eggs as I want plotted at once. Let’s rewrite our 2 egg code to be more flexible and handle any number of eggs!
# some changes have to be made here...
eggs <- list(pts1, pts2)
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas +
geom_polygon(data = egg, aes(x = x, y = y))
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
Got dang we’re cooking with gas.
Next! I don’t want my eggs all laying down! We should rotate these puppies.
Rotating things can look pretty scary at first, but it’s a super powerful idea and comes up in some useful stats techniques like PCA (animation & more info)!.
Rotating math is of scope for this doc, but this is a phenomenal video for more on matrix transformations including rotation.
rotated_egg_pts <- function(cx, cy, xr = 0.9, yr = 0.6, rotate_degrees = 0) {
theta <- seq(0, 2 * pi, length.out = 100)
# Calculate x and y coordinates of points on the ellipse
x <- xr * cos(theta)
y <- yr * sin(theta)
# Adjust y coordinates using the t function
y <- y * t_func(x)
# Rotate code!
theta <- rotate_degrees * (pi / 180) # convert degress to radians
rotation_matrix <- matrix(
c(
cos(theta), -sin(theta), # say where we want
sin(theta), cos(theta) # our unit vectors to be
),
ncol = 2
)
rotated <- cbind(x, y) %*% rotation_matrix # apply!
x <- rotated[, 1] # pull back out x
y <- rotated[, 2] # pull back out y
# End of Rotate code!
pts <- data.frame(x = x + cx, y = y + cy)
return(pts)
}
So… does it… does it… work?
pts1 <- rotated_egg_pts(0, 0, rotate_degrees = -90)
pts2 <- rotated_egg_pts(0, 1, rotate_degrees = 180)
eggs <- list(pts1, pts2)
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas + geom_polygon(data = egg, aes(x = x, y = y))
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
We need more eggs!!
Let’s create a for loop that can make a row of eggs. The for loop just needs to update the x location of each egg.
xs <- -5:5
n_xs <- length(xs)
eggs <- list()
for (i in 1:n_xs) {
x <- xs[i]
egg <- rotated_egg_pts(x, 0, rotate_degrees = -90)
eggs[[i]] <- egg
}
Something happened, that’s for sure. Let’s plot.
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas +
geom_polygon(data = egg, aes(x = x, y = y))
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
MORE! Double for loop for rows and columns!
# Multiplication is spacing things out here
xs <- -5:5 * 1.5
ys <- -5:5 * 2.2
n_xs <- length(xs)
n_ys <- length(ys)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
egg <- rotated_egg_pts(x, y, rotate_degrees = -90)
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
length(eggs)
## [1] 121
Show me them eggs!
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas +
geom_polygon(data = egg, aes(x = x, y = y))
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
Okay… that’s something!! It’s missing an easter vibe though. We need pastels.
palette <- c("#69ffb9", "#76ecfb", "#c1fda0", "#9386e6", "#f298f4")
xs <- -5:5 * 1.5
ys <- -5:5 * 2.2
n_xs <- length(xs)
n_ys <- length(ys)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
egg <- rotated_egg_pts(x, y, rotate_degrees = -90)
# Pick a random color and save
color <- sample(palette, 1)
egg$color <- color
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
Modify our plot code to change the fill based on the new color column
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas + geom_polygon(data = egg, aes(x = x, y = y), fill = egg$color)
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
This is the end of our plotting code changes. It’d clean things up to hide it in a function.
plot_eggs <- function(eggs) {
canvas <- ggplot()
for (egg in eggs) {
canvas <- canvas + geom_polygon(data = egg, aes(x = x, y = y), fill = egg$color)
}
canvas +
theme_void() +
coord_fixed(ratio = 1)
}
plot_eggs(eggs)
:) - cute! Where do we go from here? tbh it could be over already but here are some ideas.
Personally, I think being too neat is boring… what about wobbly eggs?
xs <- -5:5 * 1.5
ys <- -5:5 * 2.2
n_xs <- length(xs)
n_ys <- length(ys)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
# Randomize rotation
rotate <- rnorm(1, mean = -90, sd = 10)
egg <- rotated_egg_pts(x, y, rotate_degrees = rotate)
# Pick a random color and save
color <- sample(palette, 1)
egg$color <- color
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
plot_eggs(eggs)
What if we got wobblier or un-wobblier based on y??
xs <- -5:5 * 1.5
ys <- 0:-10 * 2
n_xs <- length(xs)
n_ys <- length(ys)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
# Randomize rotation
rotate_sd <- abs(y) * 2
rotate <- rnorm(1, mean = -90, sd = rotate_sd)
egg <- rotated_egg_pts(x, y, rotate_degrees = rotate)
# Pick a random color and save
color <- sample(palette, 1)
egg$color <- color
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
plot_eggs(eggs)
You could have this randomness be a little less random…
One way to end up with some nice organic feeling randomness is to use “noise”. In particular, “Perlin noise” is used often. This noise has been used to generate terrain in video game, simulate trees moving in the wind, or we can use it to rotate our eggs!
xs <- -10:15 * 1.5
ys <- -10:15 * 2
n_xs <- length(xs)
n_ys <- length(ys)
# Generate the values that will rotate the eggs
noise <- noise_perlin(c(n_xs, n_ys), frequency = 0.1)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
# Randomize rotation
# (1) pulling noise value, (2) scaling it up, &(3) centering at 90,
rotate <- -90 + noise[i, j] * 120
egg <- rotated_egg_pts(x, y, rotate_degrees = rotate)
# Pick a random color and save
color <- sample(palette, 1)
egg$color <- color
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
plot_eggs(eggs)
Go vols!
palette <- c("#ff8200", "#58595b", "#58595b", "#58595b", "#58595b")
xs <- -10:15 * 1.5
ys <- -10:15 * 2
n_xs <- length(xs)
n_ys <- length(ys)
# Generate the values that will rotate the eggs
noise <- noise_perlin(c(n_xs, n_ys), frequency = 0.1)
eggs <- list()
egg_num <- 1 # keep track of how many eggs we've made
for (i in 1:n_xs) {
for (j in 1:n_ys) {
x <- xs[i]
y <- ys[j]
# Randomize rotation
# (1) pulling noise value, (2) scaling it up, &(3) centering at 90,
rotate <- -90 + noise[i, j] * 120
egg <- rotated_egg_pts(xs[i], ys[j], rotate_degrees = rotate)
# Pick a random color and save
color <- sample(palette, 1)
egg$color <- color
eggs[[egg_num]] <- egg
egg_num <- egg_num + 1
}
}
plot_eggs(eggs)