Plot 0

So, for my project, I want to show how we smooth racetrack bounds as part of the autonomous racing team. For some context, the gps coordinates of the edges of a racetrack are usually programmatically calculated from satellite imagery, but this can have a lot of small errors and be really rough in general, so the track won’t be as smooth as we want it. We need to smooth the race track bounds so that when we generate racelines, the racelines themselves won’t be shaky, as shaky racelines means our racecar will lose traction and crash. So we apply smoothing algorithms to correct this. These smoothing algorithms are compared by their ability to smooth out the bounds but also keep the smoothed bounds from veering off where they actually are. This plot focuses mainly on the smoothing aspect.

p0 <- ggplot(data = bounds, aes(x = Index)) + 
  geom_line(aes(y = Change.in.Heading.Inner, color = "Inner")) + 
  geom_line(aes(y = Change.in.Heading.Outer, color = "Outer")) + 
  facet_wrap(~ Smoothing.Type) +
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       color = "Path") + 
  theme_minimal()
p0

This is my first, basic plot. Honestly, I think it looks pretty solid. We can clearly see the difference in smoothness between each of the smoothing techniques.

Plot 1: Use an Area Plot

p1 <- ggplot(data = bounds, aes(x = Index)) + 
  geom_area(aes(y = Change.in.Heading.Inner, fill = "Inner"), alpha = 0.5) + 
  geom_area(aes(y = Change.in.Heading.Outer, fill = "Outer"), alpha = 0.5) + 
  facet_wrap(~ Smoothing.Type) +
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       fill = "Path") + 
  theme_minimal()
p1

This looks fine, but kind of messes with the viewer’s ability to recognize that we are using SMOOTHING techniques, as smoothing shapes isn’t as intuitive as smoothing lines.

Plot 2

bounds_long <- bounds %>%
  gather(key = "Measure", value = "Value", -Index, -Smoothing.Type) %>%
  mutate(Bound.Type = ifelse(grepl("Inner", Measure), "Inner", "Outer"),
         Measure = gsub("Change.in.Heading.", "", Measure),
         Color = paste(Bound.Type, Smoothing.Type, sep = " - "))

p2 <- ggplot(data = bounds_long, aes(x = Index, y = Value, 
                                     group = Color, color = Color)) + 
  geom_line() + 
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       color = "Path Type") + 
  theme_minimal() +
  scale_color_manual(values = c("Inner - Sma" = "blue", "Outer - Sma" = "red",
                                "Inner - Sg" = "green", "Outer - Sg" = "orange",
                                "Inner - Gaussian" = "purple", 
                                "Outer - Gaussian" = "brown"))

p2

So now all the bounds are on the same graph, but it’s really difficult to actually compare the lines. There’s way too much going on. I’m going to try matching the colors of the pairs of inner and outer bounds so we can cut through the mess.

Plot 3: Change the colors to pairs

bounds_long <- bounds %>% 
  pivot_longer(cols = -c(Index, Smoothing.Type), 
               names_to = "Bound.Type", 
               values_to = "Value") %>%
  mutate(
    Bound.Type = case_when(
      grepl("Inner", Bound.Type) ~ "Inner",
      TRUE ~ "Outer"
    ),
    ID = interaction(Smoothing.Type, Bound.Type, sep = " - ")
  )

colors <- c("Sma - Inner" = "#1b9e77", "Sma - Outer" = "#66c2a5",
            "Sg - Inner" = "#d95f02", "Sg - Outer" = "#fc8d62",
            "Gaussian - Inner" = "#7570b3", "Gaussian - Outer" = "#9e9ac8")

p3 <- ggplot(data = bounds_long, aes(x = Index, y = Value, group = ID, color = ID)) + 
  geom_line() + 
  scale_color_manual(values = colors) +
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       color = "Path Type & Smoothing") +
  theme_minimal()

p3

This really isn’t much better.

Plot 4: Use interactive filtering dropdown

bounds_long <- bounds %>%
  gather(key = "Measure", value = "Value", -Index, -Smoothing.Type) %>%
  mutate(Bound.Type = ifelse(grepl("Inner", Measure), "Inner", "Outer"),
         Measure = gsub("Change.in.Heading.", "", Measure),
         Color = paste(Bound.Type, Smoothing.Type, sep = " - "))

p4 <- ggplot(data = bounds_long, aes(x = Index, y = Value, color = Color)) +
  geom_line(size=0.8) +
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       color = "Path Type") +
  theme_minimal() +
  scale_color_manual(values = c("Inner - Sma" = "blue", "Outer - Sma" = "lightblue",
                                "Inner - Sg" = "green", "Outer - Sg" = "lightgreen",
                                "Inner - Gaussian" = "purple", 
                                "Outer - Gaussian" = "darkorchid1"))

p4 <- ggplotly(p3)

buttons <- list(
  list(method = "restyle",
       args = list("visible", list(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE)),
       label = "All"),
  list(method = "restyle",
       args = list("visible", list(FALSE, TRUE, FALSE, TRUE, FALSE, TRUE, FALSE, TRUE)),
       label = "Sma"),
  list(method = "restyle",
       args = list("visible", list(FALSE, TRUE, TRUE, FALSE, FALSE, TRUE, TRUE, FALSE)),
       label = "Sg"),
  list(method = "restyle",
       args = list("visible", list(TRUE, TRUE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE)),
       label = "Gaussian")
)

p4 <- layout(p4,
        updatemenus = list(
          list(
            x = 1.0,
               y = 1.0,
               buttons = buttons)
        ))

p4

Here I tried using a dropdown filter to cutdown on the amount of infomration, but it doesn’t work quite as well as imagined, because we still can’t directly compare filtering techniques against each other.

Plot 5: Use animation

p5 <- ggplot(data = bounds, aes(x = Index)) + 
  geom_line(aes(y = Change.in.Heading.Inner, color = "Inner")) + 
  geom_line(aes(y = Change.in.Heading.Outer, color = "Outer")) + 
  labs(title = "Change in Heading for Inner and Outer Paths",
       y = "Change in Heading (radians)",
       color = "Path") + 
  theme_minimal()

animation <- p5 +
  transition_states(Smoothing.Type, transition_length = 2, state_length = 1) +
  enter_fade() + exit_fade() +
  ease_aes('linear') +
  labs(subtitle = 'Smoothing Type: {closest_state}')

# NOTE: I rendered the animation outside of Rmd knit because knitting was slow
#anim <- animate(animation, nframes = 50, duration = 5, width = 800, height = 600, renderer=gifski_renderer())
#output_file <- "smooth.gif"
#anim_save(filename = output_file, animation = anim)

I tried to do some animation here, and it looks really good aesthetically. But, my concern is that the viewer kind of has to wait to directly compare the smoothing algorithms, and it’s hard to compare them visually when the animation is constantly shifting. I want to explore a way where I can show all smoothing types simultaneously.

Plot 6

Okay, let’s switch things up. I think this idea of plotting everything on the same plot isn’t working, so let’s plot these graphs individually side by side.

bounds_long <- bounds %>%
  pivot_longer(cols = -c(Index, Smoothing.Type), 
               names_to = "Bound.Type", 
               values_to = "Value") %>%
  mutate(Bound.Type = case_when(
    grepl("Inner", .data$`Bound.Type`) ~ "Inner",
    TRUE ~ "Outer"
  ))

plots <- list()

for (bound_type in c("Inner", "Outer")) {
  bounds_filtered <- bounds_long %>% 
    filter(Bound.Type == bound_type)
  p6 <- ggplot(bounds_filtered, aes(x = Index, y = Value, 
                                    color = Smoothing.Type)) +
    geom_line() +
    facet_wrap(~ Smoothing.Type, ncol = 1, scales = "free_y") +
    labs(title = paste("Change in Heading for", bound_type, "Bounds"),
         y = "Change in Heading (radians)",
         x = "Index",
         color = "Smoothing Technique") +
    theme_minimal() +
    theme(legend.position = "bottom")
  
  plots[[bound_type]] <- p6
}

plots$Inner + plots$Outer

This is good, kind of similar to the original idea. We’ve come full circle. However, I still think this plot struggles to be able to compare how well the smoothing did more directly.

Plot 7: Faceting/Use of patchwork

bounds_long <- bounds %>%
  pivot_longer(cols = starts_with("Change.in.Heading"),
               names_to = "Bound.Type",
               names_prefix = "Change.in.Heading.",
               values_to = "Change.in.Heading") %>%
  mutate(Bound.Type = factor(Bound.Type, levels = c("Inner", "Outer")))

create_plot <- function(data, bound_type, smoothing_type) {
  data %>%
    filter(`Bound.Type` == bound_type, Smoothing.Type %in% c("Original", smoothing_type)) %>%
    ggplot(aes(x = Index, y = `Change.in.Heading`, color = Smoothing.Type)) +
    geom_line() +
    labs(title = paste(smoothing_type, bound_type, "with Original"),
         x = "Index", y = "Change in Heading") +
    scale_color_manual(values = c("Original" = "gray", smoothing_type = "blue")) +
    theme_minimal() +
    theme(legend.position = "bottom")
}

plots <- list()
for (smoothing_type in c("Sma", "Sg", "Gaussian")) {
  for (bound_type in c("Inner", "Outer")) {
    plots[[paste(smoothing_type, bound_type)]] <- create_plot(bounds_long, bound_type, smoothing_type)
  }
}

p7 <- plots[["Sma Inner"]] / plots[["Sg Inner"]] / plots[["Gaussian Inner"]] | 
               plots[["Sma Outer"]] / plots[["Sg Outer"]] / plots[["Gaussian Outer"]]

p7

I think this is a big improvement, with the original bounds behind our smoothed bounds, it becomes a lot easier to compare how well the smoothing did. It also helps because I’m available to make sure these smoothing isn’t TOO strong and messing up the bounds. This plot was really difficult to make for some reason, so I had to get rid of a lot of the previous plot’s aesthetics, though. Let’s add the color and aggregated titles back. It’s WAY too small as well.

Plot 8: Themes/axis/legend titles

bounds_long <- bounds %>%
  mutate(Smoothing.Type = factor(Smoothing.Type, levels = c("Original", "Sma", "Sg", "Gaussian")))

bounds_long <- bounds_long %>%
  pivot_longer(cols = starts_with("Change.in.Heading"),
               names_to = "Bound.Type",
               names_prefix = "Change.in.Heading.",
               values_to = "Change.in.Heading") %>%
  mutate(Bound.Type = factor(Bound.Type, levels = c("Inner", "Outer")))

create_plot <- function(data, bound_type, smoothing_type, hide_x_axis_text, hide_y_axis_text) {
  full_names <- c(Sma = "Simple Moving Average", 
                  Sg = "Savitzky-Golay", 
                  Gaussian = "Gaussian")
  
  plot_title <- full_names[[smoothing_type]]
  
  colors <- c("Original" = "gray", 
              "Sma" = "#377eb8", 
              "Sg" = "#ff7f00", 
              "Gaussian" = "#4daf4a") # Adjust colors as needed
  
  p8 <- data %>%
    filter(`Bound.Type` == bound_type, Smoothing.Type %in% c("Original", smoothing_type)) %>%
    ggplot(aes(x = Index, y = `Change.in.Heading`, color = Smoothing.Type)) +
    geom_line() +
    labs(title = plot_title,
         x = ifelse(hide_x_axis_text, "", "Reference Index"), 
         y = ifelse(hide_y_axis_text, "", "Change in Heading (radians)")) +
    scale_color_manual(values = colors) + 
    theme_minimal() +
    theme(legend.position = "none",
          axis.text.x = element_text(size = 4), 
          axis.text.y = element_text(size = 4), 
          axis.ticks = element_blank(),
          plot.margin = margin(t = 5, b = 5, unit = "pt"),
          plot.title = element_text(hjust = 0.5, size = 8, color = colors[smoothing_type]))
  
  if (hide_x_axis_text) {
    p8 <- p8 + theme(axis.title.x = element_blank())
  }
  
  if (hide_y_axis_text) {
    p8 <- p8 + theme(axis.title.y = element_blank())
  }
  
  return(p8)
}

plots <- list()

for (smoothing_type in c("Sma", "Sg", "Gaussian")) {
  for (bound_type in c("Inner", "Outer")) {
    plot_key <- paste(smoothing_type, bound_type)
    plots[[plot_key]] <- create_plot(bounds_long, bound_type, smoothing_type, 
                                     hide_x_axis_text = smoothing_type != "Gaussian", 
                                     hide_y_axis_text = smoothing_type != "Sg")
  }
}

plot_layout <- (plots[["Sma Inner"]] / plots[["Sg Inner"]] / plots[["Gaussian Inner"]] | 
                plots[["Sma Outer"]] / plots[["Sg Outer"]] / plots[["Gaussian Outer"]])

plot_layout <- plot_layout + 
  theme(legend.position = "none") +
  plot_annotation(title = "Inner Bound Change in Heading     Outer Bound Change in Heading",
                  theme = theme(plot.title = element_text(hjust = 0.5, 
                                               size = 14, 
                                               face = "bold")))

print(plot_layout)

Added back the aesthetics from an earlier version, reformatted titles to be more clear and non-redundant, and made the plots bigger. I think this looks A LOT better, and also has the same comparison ability as the graph above. Added labels directly on the line instead of in the legend. This is more readable because they viewer doesn’t have to switch back and forth between the legend and the plots. It’s also better than the animation because we can compare all the smoothing techniques simultaneously.

Aesthetic changes between plots:

Appendix

Convos with ChatGPT