Load ggplot

library(ggplot2)
library(dplyr)

First generate some very simple fake data, two classes, with percentages between 0 and 100.

y=c(rnorm(10,5,20),runif(10,50,100))
y[y<0]=0
cl=c(rep("ClassA",10),rep("ClassB",10))
data=data.frame(y,cl)

Now this is what a naive ggplot of this data would look like using the normal error bars, with means and standard deviations:

agd1 = group_by(data,cl) %>% 
  summarise(mn = mean(y),sd = sd(y)) %>%
  mutate(up = mn+sd, lw = mn-sd)

p = ggplot(agd1,aes(x=cl,y=mn)) + geom_errorbar(aes(ymin = lw, ymax = up), width = 0.2)
p

The best way to transform percentage data (I now see you have an actual measured percentage, not just a number of sucesses / failures in trials) is with an arcsin-squareroot transform

data = mutate(data,trans_y = asin(sqrt(0.01*y)))

Now aggregate to calculate those standard deviations again:

agd2 = group_by(data,cl) %>% 
  summarise(mn = mean(trans_y),sd = sd(trans_y)) %>%
  mutate(up = mn+sd, lw = mn-sd)

Now we have to apply the inverse of arcsin-squareroot (here as a function) to turn them back into percentages. We have to back-transform the mean as well.

iasr = function(x){(sin(x)**2)*100}
agd2 = mutate(agd2, rup = iasr(up),rlw = iasr(lw), rmn = iasr(mn))

And make a ggplot using these new upper and lower bounds. Which using my fake data doesn’t look greatly different, but you will notice it is slightly. This should constrain the bounds to within 0,1.

p = ggplot(agd2,aes(x=cl,y=mn)) + geom_errorbar(aes(ymin = rlw, ymax = rup), width = 0.2)
p