'Changing the shape of bars in ggplot2 bar_plot (windmill plot)

I have some data around wind energy production in China. I'm visualising the data as a (circular) barplot, with the ultimate intention to make it I want to look like a windmill (yup I know this isn't great 'data analysis', just a bit of fun). How can I change the shape of the bars from their square form to look more like the blades on a turbine (ideally just through changing the shape of the bars, although I guess using a grob may be possible) Code and data for the circular bar plot:

data_clean <- structure(list(Type = c("Wind", "Wind", "Wind", "Wind", "Wind", 
"Wind", "Wind", "Wind", "Wind", "Wind"), Year = c(2010, 2011, 
2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019), Value_TWh = c(49.4, 
74.1, 103, 138.3, 159.8, 185.6, 240.9, 303.4, 366, 405.7), id = 1:10, 
    film_year = c("Year: 2010 . Energy Output: 49.4", "Year: 2011 . Energy Output: 74.1", 
    "Year: 2012 . Energy Output: 103", "Year: 2013 . Energy Output: 138.3", 
    "Year: 2014 . Energy Output: 159.8", "Year: 2015 . Energy Output: 185.6", 
    "Year: 2016 . Energy Output: 240.9", "Year: 2017 . Energy Output: 303.4", 
    "Year: 2018 . Energy Output: 366", "Year: 2019 . Energy Output: 405.7"
    )), class = c("spec_tbl_df", "tbl_df", "tbl", "data.frame"
), row.names = c(NA, -10L))


label_data <- data_clean
number_of_bar <- nrow(label_data)
angle <-  90 - 360 * (label_data$id-0.5) /number_of_bar 
label_data$hjust<-ifelse( angle < -90, 1, 0)
label_data$angle<-ifelse(angle < -90, angle+180, angle)
  
  
data_clean %>% 
  ggplot(aes(x = Year, y = Value_TWh)) + 
  geom_bar(stat = "identity", fill = "grey", alpha = 0.7) +
  ylim(-400,1200)  + 
        theme_minimal() +
        geom_text(aes(label=film_year),
                  hjust = label_data$hjust,
                  color = "black",
                  fontface = "bold",
                  alpha = 0.6,
                  size = 4,
                  angle = label_data$angle) +
                  coord_polar(start = 0) + 
        theme( axis.text = element_blank(),
               axis.title = element_blank(),
               panel.grid = element_blank(),
               plot.margin = unit(rep(-1,4), "cm")) 


Solution 1:[1]

I like this too much. Here a quick geom_windmill, based on GeomPolygon. The idea is to use a custom Stat, which is based on user chemdork's drawing and maths.

library(ggplot2)
library(grid)

ggplot(data_clean, aes(x=Year, y=Value_TWh, color=id, group = id)) +
  geom_windmill(color='black', aes(fill=Value_TWh))


ggplot(data_clean, aes(x=Year, y=Value_TWh, color=id, group = id)) +
  geom_windmill(color='black', aes(fill=Value_TWh)) +
  coord_polar()


ggplot(data_clean, aes(x=Year, y=Value_TWh, color=id, group = id)) +
  geom_windmill(color='black', aes(fill=Value_TWh), span_x = 2) 


ggplot(data_clean, aes(x=Year, y=Value_TWh, color=id, group = id)) +
  geom_windmill(color='black', aes(fill=Value_TWh), span_x = 2) +
  coord_polar()

geom_windmill with stat_windmill

stat_windmill <- function(mapping = NULL, data = NULL, geom = "polygon",
                          position = "identity", na.rm = FALSE, show.legend = NA, 
                          inherit.aes = TRUE, span_x = 1, ...) {
  layer(
    stat = StatWindmill, data = data, mapping = mapping, geom = geom, 
    position = position, show.legend = show.legend, inherit.aes = inherit.aes,
    params = list(na.rm = na.rm, span_x = span_x, ...)
  )
}

StatWindmill <- ggproto("StatWindmill", Stat,
                     compute_group = function(data, scales, span_x = 1) {
                       blade_frame <- data.frame(
                         x_map=c(0.15,0.85,0.95,0.95,0.5,0.05,0.05),
                         y = c(0,0,0.45,0.8,1,0.8,0.45)
                       )
                       new_x <- (data$x - span_x/2) + (span_x * blade_frame$x_map)
                       new_y <- data$y * blade_frame$y
                       new_blade <- data.frame(x=new_x, y=new_y)
                       new_blade
                      
                     },
                     
                     required_aes = c("x", "y")
)

geom_windmill <- function(mapping = NULL, data = NULL,
                         stat = "identity", position = "identity",
                         rule = "evenodd",
                         ...,
                         na.rm = FALSE,
                         show.legend = NA,
                         inherit.aes = TRUE) {
  layer(
    data = data,
    mapping = mapping,
    stat = StatWindmill,
    geom = GeomPolygon,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(
      na.rm = na.rm,
      rule = rule,
      ...
    )
  )
}


data

data_clean <- structure(list(Type = c("Wind", "Wind", "Wind", "Wind", "Wind", 
                                      "Wind", "Wind", "Wind", "Wind", "Wind"), Year = c(2010, 2011, 
                                                                                        2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019), Value_TWh = c(49.4, 
                                                                                                                                                       74.1, 103, 138.3, 159.8, 185.6, 240.9, 303.4, 366, 405.7), id = 1:10, 
                             film_year = c("Year: 2010 . Energy Output: 49.4", "Year: 2011 . Energy Output: 74.1", 
                                           "Year: 2012 . Energy Output: 103", "Year: 2013 . Energy Output: 138.3", 
                                           "Year: 2014 . Energy Output: 159.8", "Year: 2015 . Energy Output: 185.6", 
                                           "Year: 2016 . Energy Output: 240.9", "Year: 2017 . Energy Output: 303.4", 
                                           "Year: 2018 . Energy Output: 366", "Year: 2019 . Energy Output: 405.7"
                             )), class = c("spec_tbl_df", "tbl_df", "tbl", "data.frame"
                             ), row.names = c(NA, -10L))

Solution 2:[2]

Here's an alternative to @ssp3nc3r 's response. DISCLAIMER: Please excuse my complete inability to draw a turbine blade adequately. Hopefully, you get the idea, but like @ssp3nc3r's answer, you need to define the blade first. Here's a blade defined by 7 points:

raw_blade <- data.frame(x=c(0.1,0.9,1,1,0.5,0,0),
                    y=c(0,0,4.5,8,10,8,4.5))

ggplot(raw_blade, aes(x,y)) + geom_polygon() + xlim(-4,4)

enter image description here

The idea is that we will map the points on this dataframe according to your dataframe. This means I need to define the blade shape differently. In the dataframe, the "center" along x needs to be the x value of your data, and the tip of the blade at the top should be the y value of your data. I'm going to start with redefining the shape along an x and y range of 0 to 1 for both. This is easily mapped to y (multiply the y of the blade by the y value of your dataset), but a bit more tricky for x, where we need to know the span of the x value (distance between the points) and do some math.

In order to draw multiple polygons and separate blades, each one needs to be defined according to some group variable, so the function below iterates through a set of x and y values and for each one:

  1. Creates the blade by doing math stuff
  2. Assigns a group number
  3. Appends this blade to the new data frame

Here's the code below:

make_blades <- function(data_x, data_y, span_x=1) {
  # span_x is the distance between discrete x values
  
  blade_frame <- data.frame(
    x_map=c(0.15,0.85,0.95,0.95,0.5,0.05,0.05),
    y=c(0,0,0.45,0.8,1,0.8,0.45)
  )
   
  if (length(data_x)!=length(data_y))
    return(NULL)
  
  num_items <- length(data_x)
  new_df <- data.frame()
  
  for (i in 1:num_items) {
    new_x <- (data_x[i] - span_x/2) + (span_x * blade_frame$x_map)
    new_y <- blade_frame$y * data_y[i]
    new_blade <- data.frame(x=new_x, y=new_y, group=rep(i,nrow(blade_frame)))
    new_df <- rbind(new_df, new_blade)
  }
  return(new_df)
}

It's then a matter of creating a data frame by passing your x and y values to make_blades(), then plotting. For the plot, we will be sure to assign the group= aesthetic to our "group" column in the dataset so that the individual blades can be made:

my_blades <- make_blades(data_x=data_clean$Year, data_y=data_clean$Value_TWh)

p <- ggplot(my_blades, aes(x=x, y=y, group=group)) +
  geom_polygon(color='black', fill='skyblue')
p

enter image description here

Looks surprisingly adequate! Here's the polar coordinate version:

p + coord_polar()

enter image description here

Solution 3:[3]

Create an SVG path that looks like the shape you want (e.g., windmill blades), and then plot it wherever you like on the graph using geom_polygon. I created a tutorial a while back on my blog: https://ssp3nc3r.github.io/post/2020-04-08-create-and-encode-custom-glyphs-from-an-svg-path/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 ssp3nc3r