Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

r

ggplot2

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")) 
like image 546
alex_stephenson Avatar asked Mar 15 '26 00:03

alex_stephenson


2 Answers

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))
like image 55
tjebo Avatar answered Mar 17 '26 14:03

tjebo


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

like image 29
chemdork123 Avatar answered Mar 17 '26 13:03

chemdork123