Horizontal barplot for comparison two data - based on ratio - r

I would like to create a horizontal barplot to compare two of my tables. I already did the comparison and created a table with ratio.
That's how the data looks like:
> dput(data)
structure(list(Name=c('Mazda RX4','Mazda RX4 Wag','Datsun 710','Hornet 4 Drive',
'Hornet Sportabout','Valiant','Duster 360','Merc 240D','Merc 230','Merc 280','Merc 280C',
'Merc 450SE','Merc 450SL','Merc 450SLC','Cadillac Fleetwood','Lincoln Continental',
'Chrysler Imperial','Fiat 128','Honda Civic','Toyota Corolla'),ratio=c(1.393319198903125,
0.374762569687951,0.258112791829808,0.250298480396529,1.272180366473129,0.318000456484454,
0.264074483447591,0.350798965144559,2.310541690719624,1.314300844213157,1.18061486696761,
0.281581177092538,0.270164442687919,2.335578882236703,2.362339701969396,1.307731925943769,
0.347550384302281,0.232276047899868,0.125643566969327,0.281209747680576),Freq=c(2L,9L,2L,2L,
4L,2L,2L,3L,3L,5L,2L,2L,2L,7L,2L,4L,4L,2L,2L,4L)),.Names=c('Name','ratio','Freq'),class=
'data.frame',row.names=c(NA,20L))
I would like to achieve something like that:
In the middle I would put 1. Based on the calculated ratio I would like to put the proper scale which goes up to 3 to the right for example and to 0 to the left (can be different of course).
Each of the cars should have a separate bar. It will give 20 bars on this plot.
Additional thing would be to put the numbers from column Freq on the plots. It's not obligatory but would help.

I don't really see how that plot makes much sense with your data, as there is no quantity that adds up to 1 (or a common total). It could make sense with proportions, not so much with ratio's. I might be missing something... Perhaps you're looking for something like this?
library(ggplot2)
r <- range(d$ratio)
br <- seq(floor(r[1]), ceiling(r[2]), 0.5)
ggplot(d, aes(x = Name, y = ratio - 1)) +
geom_bar(stat = 'identity', position = 'identity') +
coord_flip() +
ylab('ratio') + xlab('car') +
scale_y_continuous(breaks = br - 1, labels = br) +
theme_bw()
Add geom_text(aes(label = Freq), y = r[2] - 0.95) for the labels on the right side.
Or if you want to center the value of 1 (a bit more tricky):
r <- range(d$ratio)
m <- ceiling(max(abs(range(d$ratio))))
br <- seq(-m + 1, m - 1, 0.25)
ggplot(d, aes(x = Name, y = ratio - 1)) +
geom_bar(stat = 'identity', position = 'identity') +
geom_text(aes(label = Freq), y = m - 1.1) +
coord_flip() +
ylab('ratio') + xlab('car') +
scale_y_continuous(breaks = br, labels = br + 1, limits = c(-m + 1, m - 1),
expand = c(0, 0)) +
theme_bw()

## plot precomputations
yexpand <- 0.2;
barheight <- 0.8;
xlim <- c(0,3);
xticks <- seq(xlim[1L],xlim[2L],0.25);
ylim <- c(1-barheight/2-yexpand,nrow(data)+barheight/2+yexpand);
yticks <- seq_len(nrow(data));
cols <- c('#6F7EB3','#D05B5B');
## draw plot
par(mar=c(5,4,4,2)+0.1+c(0,3,0,0));
plot(NA,xlim=xlim,ylim=ylim,xaxs='i',yaxs='i',axes=F,ann=F);
segments(xlim[1L],ylim[1L],xlim[1L],ylim[2L],xpd=NA);
axis(1L,xticks,cex.axis=0.7);
axis(2L,yticks,data$Name,las=2L,cex.axis=0.7);
mtext(expression(italic(Ratio)),1L,3);
mtext(expression(italic(Car)),2L,5.5);
mtext(data$Freq,4L,0.75,at=yticks,las=2L,cex=0.7);
y1 <- seq_len(nrow(data))-barheight/2;
y2 <- seq_len(nrow(data))+barheight/2;
rect(xlim[1L],y1,data$ratio,y2,col=cols[1L],lwd=0.5);
rect(data$ratio,y1,xlim[2L],y2,col=cols[2L],lwd=0.5);
abline(v=1);

Related

Is it possible to align x axis title to a value of the axis?

Having a tibble and a simple scatterplot:
p <- tibble(
x = rnorm(50, 1),
y = rnorm(50, 10)
)
ggplot(p, aes(x, y)) + geom_point()
I get something like this:
I would like to align (center, left, right, as the case may be) the title of the x-axis - here rather blandly x - with a specific value on the axis, say the off-center 0 in this case. Is there a way to do that declaratively, without having to resort to the dumb (as in "free of context") trial-and-error element_text(hjust=??). The ?? are rather appropriate here because every value is a result of experimentation (my screen and PDF export in RStudio never agree on quite some plot elements). Any change in the data or the dimensions of the rendering may (or may not) invalidate the hjust value and I am looking for a solution that graciously repositions itself, much like the axes do.
Following the suggestions in the comments by #tjebo I dug a little deeper into the coordinate spaces. hjust = 0.0 and hjust = 1.0 clearly align the label with the Cartesian coordinate system extent (but magically left-aligned and right-aligned, respectively) so when I set specific limits, calculation of the exact value of hjust is straightforward (aiming for 0 and hjust = (0 - -1.5) / (3.5 - -1.5) = 0.3):
ggplot(p, aes(x, y)) +
geom_point() +
coord_cartesian(ylim = c(8, 12.5), xlim = c(-1.5, 3.5), expand=FALSE) +
theme(axis.title.x = element_text(hjust = 0.3))
This gives an acceptable result for a label like x, but for longer labels the alignment is off again:
ggplot(p %>% mutate(`Longer X label` = x), aes(x = `Longer X label`, y = y)) +
geom_point() +
coord_cartesian(ylim = c(8, 12.5), xlim = c(-1.5, 3.5), expand=FALSE) +
theme(axis.title.x = element_text(hjust = 0.3))
Any further suggestions much appreciated.
Another option (different enough hopefully to justify the second answer) is as already mentioned to create the annotation as a separate plot. This removes the range problem. I like {patchwork} for this.
library(tidyverse)
library(patchwork)
p <- tibble( x = rnorm(50, 1), y = rnorm(50, 10))
p1 <- tibble( x = rnorm(50, 1), y = 100*rnorm(50, 10))
## I like to define constants outside my ggplot call
mylab <- "longer_label"
x_demo <- c(-1, 2)
demo_fct <- function(p){
p1 <- ggplot(p, aes(x, y)) +
geom_point() +
labs(x = NULL) +
theme(plot.margin = margin())
p2 <- ggplot(p, aes(x, y)) +
## you need that for your correct alignment with the first plot
geom_blank() +
annotate(geom = "text", x = x_demo, y = 1,
label = mylab, hjust = 0) +
theme_void() +
# you need that for those annoying margin reasons
coord_cartesian(clip = "off")
p1 / p2 + plot_layout(heights = c(1, .05))
}
demo_fct(p) + plot_annotation(title = "demo1 with x at -1 and 2")
demo_fct(p1) + plot_annotation(title = "demo2 with larger data range")
Created on 2021-12-04 by the reprex package (v2.0.1)
I still think you will fair better and easier with custom annotation. There are typically two ways to do that. Either direct labelling with a text layer (for single labels I prefer annotate(geom = "text"), or you create a separate plot and stitch both together, e.g. with patchwork.
The biggest challenge is the positioning in y dimension. For this I typically take a semi-automatic approach where I only need to define one constant, and set the coordinates relative to the data range, so changes in range should in theory not matter much. (they still do a bit, because the panel dimensions also change). Below showing examples of exact label positioning for two different data ranges (using the same constant for both)
library(tidyverse)
# I only need patchwork for demo purpose, it is not required for the answer
library(patchwork)
p <- tibble( x = rnorm(50, 1), y = rnorm(50, 10))
p1 <- tibble( x = rnorm(50, 1), y = 100*rnorm(50, 10))
## I like to define constants outside my ggplot call
y_fac <- .1
mylab <- "longer_label"
x_demo <- c(-1, 2)
demo_fct <- function(df, x) {map(x_demo,~{
## I like to define constants outside my ggplot call
ylims <- range(df$y)
ggplot(df, aes(x, y)) +
geom_point() +
## set hjust = 0 for full positioning control
annotate(geom = "text", x = ., y = min(ylims) - y_fac*mean(ylims),
label = mylab, hjust = 0) +
coord_cartesian(ylim = ylims, clip = "off") +
theme(plot.margin = margin(b = .5, unit = "in")) +
labs(x = NULL)
})
}
demo_fct(p, x_demo) %>% wrap_plots() + plot_annotation(title = "demo 1, label at x = -1 and x = 2")
demo_fct(p1, x_demo) %>% wrap_plots() + plot_annotation(title = "demo 2 - different data range")
Created on 2021-12-04 by the reprex package (v2.0.1)

Transforming the y-axis without changing raw data in ggplot2

I have a question about how to transform the y-axis in ggplot2. My plot now has two lines and a scatter plot. For the scatter plot, I am very interested in the area around zero. Is there a possible way to enlarge the space between 0% and 5% and narrow the space between 20% and 30%?
I have tried to use coord_trans(y = "log10") to transform into a log form. But in this case, I have a lot of negative values, so if I want to use sqrt or log, the negative values will be removed. Do you have any suggestions?
Example of data points:
df1 = data.frame(y = runif(200,min = -1, max = 1))
df1 = data.frame( x= seq(1:200), y = df1[order(abs(df1$y)),])
ggplot(df1) +
geom_point(colour = "black",aes(x,y) ,size = 0.1)
I want to have more space between 0% and 5 % and less space between 5% and 30%.
I have tried to use trans_new() to transform the axes.
eps <- 1e-8
tn <- trans_new("logpeps",
function(x) (x+eps)^(3),
function(y) ((y)^(1/3) ),
domain=c(- Inf, Inf)
)
ggplot(df1)+ geom_point(colour = "black",aes(x,y) ,size = 0.1) +
# xlab("Observations sorted by PD in v3.1") + ylab("Absolute PD difference ") +
# ggtitle("Absolute PD for RiskCalc v4.0 relative to v3.1") +
scale_x_continuous(breaks = seq(0, round(rownum/1000)*1000, by = round(rownum/100)*10)) +
scale_y_continuous(limits = c(-yrange,yrange),breaks = c(-breaksY,breaksY),
sec.axis = sec_axis(~.,breaks = c(-breaksY[2:length(breaksY)],breaksY), labels = scales:: percent
)) +
# geom_line(data = df, aes(x,y[,3], colour = "blue"),size = 1) +
# geom_line(data = ds,aes(xval, yval,colour = "red"),size = 1) +
coord_trans(y = tn) +
scale_color_discrete(name = element_blank())
But it compresses the plot to the center, which is opposite to what I want. Then I try to use y = y^3, but it shows an
ERROR: zero_range(range)
Try a cube root transform on the y values:
aes(y=yVariable^(1/3))
or use trans_new() to define a new transformation (such as cube root, with pleasing breaks and labels).
A couple thoughts:
You can remove the empty edges of the plot like so:
scale_y_continuous(expand = c(0,0))
If you want to try the log transformation, just do:
scale_y_log10()
If you want to focus the window:
scale_y_continuous(limits=c(-.15,.15), expand=c(0,0))
Also consider adding theme_bw() for a cleaner look

Know proportions of ggplot2 plot

I usually save the plots from ggplot2 using the the png device. The width and the height of the output are set by the arguments of the function. Blank zones are drawn when the "natural proportions" of the graph dont't suit the proportions of the device. In order to avoid this and use the whole defined canvas, the proportions of the plot must be known. ¿Is there a way to find out this value without trial and error?
This code can be used as an example:
x <- seq(from = 0, to = 1, by = 0.1)
y <- seq(from = 1, to = 2, by = 0.1)
df <- expand.grid(x = x, y = y)
df <- cbind(df, z = rnorm(ncol(df), 0, 1))
p <- ggplot(df, aes(x,y, fill = z)) + geom_raster() + coord_fixed()
ppi <- 300
#Value 0.4 is used to change inches into milimeters
png("plot.png", width = 16*0.4*ppi, height = 20*0.4*ppi, res = ppi)
print(p)
dev.off()
It can be seen that some blank space is added at the top and at the bottom to fill the png file. This could be easily corrected by using a proportion different from 20/16, which is not optimal.
You can modify the ratio arg inside coord_fixed():
p <- ggplot(df, aes(x,y, fill = z)) +
geom_raster() +
coord_fixed(ratio = 20/16)
Alteratively you can specify the aspect.ratio inside the theme():
p <- ggplot(df, aes(x,y, fill = z)) +
geom_raster() +
theme(aspect.ratio = 20/16)
The result is the same:

ggplot, facet, piechart: placing text in the middle of pie chart slices

I'm trying to produce a facetted pie-chart with ggplot and facing problems with placing text in the middle of each slice:
dat = read.table(text = "Channel Volume Cnt
AGENT high 8344
AGENT medium 5448
AGENT low 23823
KIOSK high 19275
KIOSK medium 13554
KIOSK low 38293", header=TRUE)
vis = ggplot(data=dat, aes(x=factor(1), y=Cnt, fill=Volume)) +
geom_bar(stat="identity", position="fill") +
coord_polar(theta="y") +
facet_grid(Channel~.) +
geom_text(aes(x=factor(1), y=Cnt, label=Cnt, ymax=Cnt),
position=position_fill(width=1))
The output:
What parameters of geom_text should be adjusted in order to place numerical labels in the middle of piechart slices?
Related question is Pie plot getting its text on top of each other but it doesn't handle case with facet.
UPDATE: following Paul Hiemstra advice and approach in the question above I changed code as follows:
---> pie_text = dat$Cnt/2 + c(0,cumsum(dat$Cnt)[-length(dat$Cnt)])
vis = ggplot(data=dat, aes(x=factor(1), y=Cnt, fill=Volume)) +
geom_bar(stat="identity", position="fill") +
coord_polar(theta="y") +
facet_grid(Channel~.) +
geom_text(aes(x=factor(1),
---> y=pie_text,
label=Cnt, ymax=Cnt), position=position_fill(width=1))
As I expected tweaking text coordiantes is absolute but it needs be within facet data:
NEW ANSWER: With the introduction of ggplot2 v2.2.0, position_stack() can be used to position the labels without the need to calculate a position variable first. The following code will give you the same result as the old answer:
ggplot(data = dat, aes(x = "", y = Cnt, fill = Volume)) +
geom_bar(stat = "identity") +
geom_text(aes(label = Cnt), position = position_stack(vjust = 0.5)) +
coord_polar(theta = "y") +
facet_grid(Channel ~ ., scales = "free")
To remove "hollow" center, adapt the code to:
ggplot(data = dat, aes(x = 0, y = Cnt, fill = Volume)) +
geom_bar(stat = "identity") +
geom_text(aes(label = Cnt), position = position_stack(vjust = 0.5)) +
scale_x_continuous(expand = c(0,0)) +
coord_polar(theta = "y") +
facet_grid(Channel ~ ., scales = "free")
OLD ANSWER: The solution to this problem is creating a position variable, which can be done quite easily with base R or with the data.table, plyr or dplyr packages:
Step 1: Creating the position variable for each Channel
# with base R
dat$pos <- with(dat, ave(Cnt, Channel, FUN = function(x) cumsum(x) - 0.5*x))
# with the data.table package
library(data.table)
setDT(dat)
dat <- dat[, pos:=cumsum(Cnt)-0.5*Cnt, by="Channel"]
# with the plyr package
library(plyr)
dat <- ddply(dat, .(Channel), transform, pos=cumsum(Cnt)-0.5*Cnt)
# with the dplyr package
library(dplyr)
dat <- dat %>% group_by(Channel) %>% mutate(pos=cumsum(Cnt)-0.5*Cnt)
Step 2: Creating the facetted plot
library(ggplot2)
ggplot(data = dat) +
geom_bar(aes(x = "", y = Cnt, fill = Volume), stat = "identity") +
geom_text(aes(x = "", y = pos, label = Cnt)) +
coord_polar(theta = "y") +
facet_grid(Channel ~ ., scales = "free")
The result:
I would like to speak out against the conventional way of making pies in ggplot2, which is to draw a stacked barplot in polar coordinates. While I appreciate the mathematical elegance of that approach, it does cause all sorts of headaches when the plot doesn't look quite the way it's supposed to. In particular, precisely adjusting the size of the pie can be difficult. (If you don't know what I mean, try to make a pie chart that extends all the way to the edge of the plot panel.)
I prefer drawing pies in a normal cartesian coordinate system, using geom_arc_bar() from ggforce. It requires a little bit of extra work on the front end, because we have to calculate angles ourselves, but that's easy and the level of control we get as a result is more than worth it.
I've used this approach in previous answers here and here.
The data (from the question):
dat = read.table(text = "Channel Volume Cnt
AGENT high 8344
AGENT medium 5448
AGENT low 23823
KIOSK high 19275
KIOSK medium 13554
KIOSK low 38293", header=TRUE)
The pie-drawing code:
library(ggplot2)
library(ggforce)
library(dplyr)
# calculate the start and end angles for each pie
dat_pies <- left_join(dat,
dat %>%
group_by(Channel) %>%
summarize(Cnt_total = sum(Cnt))) %>%
group_by(Channel) %>%
mutate(end_angle = 2*pi*cumsum(Cnt)/Cnt_total, # ending angle for each pie slice
start_angle = lag(end_angle, default = 0), # starting angle for each pie slice
mid_angle = 0.5*(start_angle + end_angle)) # middle of each pie slice, for the text label
rpie = 1 # pie radius
rlabel = 0.6 * rpie # radius of the labels; a number slightly larger than 0.5 seems to work better,
# but 0.5 would place it exactly in the middle as the question asks for.
# draw the pies
ggplot(dat_pies) +
geom_arc_bar(aes(x0 = 0, y0 = 0, r0 = 0, r = rpie,
start = start_angle, end = end_angle, fill = Volume)) +
geom_text(aes(x = rlabel*sin(mid_angle), y = rlabel*cos(mid_angle), label = Cnt),
hjust = 0.5, vjust = 0.5) +
coord_fixed() +
scale_x_continuous(limits = c(-1, 1), name = "", breaks = NULL, labels = NULL) +
scale_y_continuous(limits = c(-1, 1), name = "", breaks = NULL, labels = NULL) +
facet_grid(Channel~.)
To show why I think this this approach is so much more powerful than the conventional (coord_polar()) approach, let's say we want the labels on the outside of the pie rather than inside. This creates a couple of problems, such as we will have to adjust hjust and vjust depending on the side of the pie a label falls, and also we will have to make the
plot panel wider than high to make space for the labels on the side without generating excessive space above and below. Solving these problems in the polar coordinate approach is not fun, but it's trivial in the cartesian coordinates:
# generate hjust and vjust settings depending on the quadrant into which each
# label falls
dat_pies <- mutate(dat_pies,
hjust = ifelse(mid_angle>pi, 1, 0),
vjust = ifelse(mid_angle<pi/2 | mid_angle>3*pi/2, 0, 1))
rlabel = 1.05 * rpie # now we place labels outside of the pies
ggplot(dat_pies) +
geom_arc_bar(aes(x0 = 0, y0 = 0, r0 = 0, r = rpie,
start = start_angle, end = end_angle, fill = Volume)) +
geom_text(aes(x = rlabel*sin(mid_angle), y = rlabel*cos(mid_angle), label = Cnt,
hjust = hjust, vjust = vjust)) +
coord_fixed() +
scale_x_continuous(limits = c(-1.5, 1.4), name = "", breaks = NULL, labels = NULL) +
scale_y_continuous(limits = c(-1, 1), name = "", breaks = NULL, labels = NULL) +
facet_grid(Channel~.)
To tweak the position of the label text relative to the coordinate, you can use the vjust and hjust arguments of geom_text. This will determine the position of all labels simultaneously, so this might not be what you need.
Alternatively, you could tweak the coordinate of the label. Define a new data.frame where you average the Cnt coordinate (label_x[i] = Cnt[i+1] + Cnt[i]) to position the label in the center of that particular pie. Just pass this new data.frame to geom_text in replacement of the original data.frame.
In addition, piecharts have some visual interpretation flaws. In general I would not use them, especially where good alternatives exist, e.g. a dotplot:
ggplot(dat, aes(x = Cnt, y = Volume)) +
geom_point() +
facet_wrap(~ Channel, ncol = 1)
For example, from this plot it is obvious that Cnt is higher for Kiosk than for Agent, this information is lost in the piechart.
Following answer is partial, clunky and I won't accept it.
The hope is that it will solicit better solution.
text_KIOSK = dat$Cnt
text_AGENT = dat$Cnt
text_KIOSK[dat$Channel=='AGENT'] = 0
text_AGENT[dat$Channel=='KIOSK'] = 0
text_KIOSK = text_KIOSK/1.7 + c(0,cumsum(text_KIOSK)[-length(dat$Cnt)])
text_AGENT = text_AGENT/1.7 + c(0,cumsum(text_AGENT)[-length(dat$Cnt)])
text_KIOSK[dat$Channel=='AGENT'] = 0
text_AGENT[dat$Channel=='KIOSK'] = 0
pie_text = text_KIOSK + text_AGENT
vis = ggplot(data=dat, aes(x=factor(1), y=Cnt, fill=Volume)) +
geom_bar(stat="identity", position=position_fill(width=1)) +
coord_polar(theta="y") +
facet_grid(Channel~.) +
geom_text(aes(y=pie_text, label=format(Cnt,format="d",big.mark=','), ymax=Inf), position=position_fill(width=1))
It produces following chart:
As you noticed I can't move labels for green (low).

Plotting overlapping positions in R

I have a dataframe in R like this:
dat = data.frame(Sample = c(1,1,2,2,3), Start = c(100,300,150,200,160), Stop = c(180,320,190,220,170))
And I would like to plot it such that the x-axis is the position and the y-axis is the number of samples at that position, with each sample in a different colour. So in the above example you would have some positions with height 1, some with height 2 and one area with height 3. The aim being to find regions where there are a large number of samples and what samples are in that region.
i.e. something like:
&
---
********- -- **
where * = Sample 1, - = Sample 2 and & = Sample 3
My first try:
dat$Sample = factor(dat$Sample)
ggplot(aes(x = Start, y = Sample, xend = Stop, yend = Sample, color = Sample), data = dat) +
geom_segment(size = 2) +
geom_segment(aes(x = Start, y = 0, xend = Stop, yend = 0), size = 2, alpha = 0.2, color = "black")
I combine two segment geometries here. One draws the colored vertical bars. These show where Samples have been measured. The second geometry draws the grey bar below where the density of the samples is shown. Any comments to improve on this quick hack?
This hack may be what you're looking for, however I've greatly increased the size of the dataframe in order to take advantage of stacking by geom_histogram.
library(ggplot2)
dat = data.frame(Sample = c(1,1,2,2,3),
Start = c(100,300,150,200,160),
Stop = c(180,320,190,220,170))
# Reformat the data for plotting with geom_histogram.
dat2 = matrix(ncol=2, nrow=0, dimnames=list(NULL, c("Sample", "Position")))
for (i in seq(nrow(dat))) {
Position = seq(dat[i, "Start"], dat[i, "Stop"])
Sample = rep(dat[i, "Sample"], length(Position))
dat2 = rbind(dat2, cbind(Sample, Position))
}
dat2 = as.data.frame(dat2)
dat2$Sample = factor(dat2$Sample)
plot_1 = ggplot(dat2, aes(x=Position, fill=Sample)) +
theme_bw() +
opts(panel.grid.minor=theme_blank(), panel.grid.major=theme_blank()) +
geom_hline(yintercept=seq(0, 20), colour="grey80", size=0.15) +
geom_hline(yintercept=3, linetype=2) +
geom_histogram(binwidth=1) +
ylim(c(0, 20)) +
ylab("Count") +
opts(axis.title.x=theme_text(size=11, vjust=0.5)) +
opts(axis.title.y=theme_text(size=11, angle=90)) +
opts(title="Segment Plot")
png("plot_1.png", height=200, width=650)
print(plot_1)
dev.off()
Note that the way I've reformatted the dataframe is a bit ugly, and will not scale well (e.g. if you have millions of segments and/or large start and stop positions).

Resources