ggplot2双坐标轴的解决方案

ggplot2双坐标轴的解决方案

本来没有打算写这一篇的,因为在一幅图表中使用双坐标轴确实不是一个很好地习惯,无论是信息传递的效率还是数据表达的准确性而言。

但是最近有好几个小伙伴儿跟我咨询关于ggplot2的次坐标轴问题,平时的一些业务分析中,有些场景出于数据呈现的需要,或者阅读习惯等,往往需要在一幅图中呈现两个量级不等的坐标。

所以我觉得这一篇推送很有必要,确实在最新版的ggplot2(ggplot 2.2.0以上版本)中,已经加入了次坐标轴参数,通过这个次坐标轴的转换,我们可以模拟出不同数量级的次坐标轴效果。

因为其中用到了英文月份简写,这里对系统日期显示格式做了特殊设置:

lct <- Sys.getlocale("LC_TIME")  
#备份本地默认日期显示格式

Sys.setlocale("LC_TIME", "C")    
#指定标准日期显示格式

Sys.setlocale("LC_TIME",lct)     
#这一句是恢复默认系统日期显示格式
#(记得要在使用完下面的month函数之后再运行这一句,否则月份返回的是中文)

加载包:
library("lubridate")
library("ggplot2")
library("scales")
library("magrittr")
library("tidyr")

生成作图数据

作图数据1——单序列柱形图

data1 <- data.frame(
      Month = seq(from = as.Date('2017-01-01'),to=as.Date('2017-06-01'),by='1 month') %>% month(label=TRUE),
      Value = runif(6,10,50) %>% round()
     )
  Month Value
1   Jan    39
2   Feb    38
3   Mar    50
4   Apr    33
5   May    18
6   Jun    49

作图数据2——二分类折线图(带散点)

data2 <- data.frame(
      Month = seq(from = as.Date('2017-01-01'),to=as.Date('2017-06-01'),by='1 month') %>% month(label=TRUE),
      Categroy1 = runif(6,0.1,0.5) %>% round(2),
      Categroy2 = runif(6,0.1,0.5) %>% round(2)
     ) %>% gather(Category,Value,-1)
   Month  Category Value
1    Jan Categroy1  0.49
2    Feb Categroy1  0.23
3    Mar Categroy1  0.10
4    Apr Categroy1  0.38
5    May Categroy1  0.34
6    Jun Categroy1  0.13
7    Jan Categroy2  0.48
8    Feb Categroy2  0.38
9    Mar Categroy2  0.48
10   Apr Categroy2  0.15
11   May Categroy2  0.40
12   Jun Categroy2  0.16

以下是整个过程代码,基本是司空见惯的内容,这里不做过多解释,仅提示其中两处重点,注意第二行geom_line内的y参数赋值以及第四行的scale_y_continuous语句:

ggplot() +
  geom_col( data = data1,aes(x = Month,y = Value),fill="#6794a7") +
  geom_line(data = data2,aes(x = Month,y = rescale(Value,c(0,55)),colour=Category,group=Category),size=1.5) +
  geom_point(data = data2,aes(x = Month,y = rescale(Value,c(0,55)),colour=Category),shape=21,fill="white",size=4)+
  scale_y_continuous(breaks=pretty_breaks(5),sec.axis = sec_axis( ~rescale(.,c(0,0.5)),name = "Categroy",labels=sprintf("%d%%",(0:5)*10)))+
  scale_color_manual(label = c("Categroy1", "Categroy2"),values = c("#ee8f71","#C10534")) +
  labs(
       title="This is a Title!",
       subtitle="This is a Subtitle",
       caption="This is a Caption"
     )+
  theme_minimal(base_size=16) %+replace% 
  theme(
  plot.caption = element_text(hjust=0),
  plot.margin = unit(c(1,0.5,1,0.5), "lines")
  )





这段代码与我们经常用的有两点不同:

第一次自定义映射——折线度量数据的映射转换:

geom_line(geom_point,因为点图是附属于折线图,仅做修饰之用,这里只重点说折线图层)中的y参数指定的对象使用了一个统计变换函数,rescale函数其实很好理解,就是将一个数值向量按照给定的另一个数值向量的极差(range),等比例标准化。

如果你知道如何将一组向量按照0~1标准化的话,那么这个函数就不难理解 ,其实就是将标准化的尺度给了一个自定义的范围。

因为在ggplot2标度系统中,不容许在一个图形中出现两个量级不等的标度(一山不容二虎),但是想要提供度量不等的次坐标轴,折中的方法就是,将次坐标轴的所有量级按照主坐标轴的量级进行缩放(如果次坐标轴量级大于主坐标轴,那么就是等比例放大,如果比主坐标轴量级大则缩小)。

针对本例而言,就是将折线图的数据源量级(0.0~0.5)放大到0~35的区间上,所有的单个指标的缩放比例都是相同的,这样你在图上就不会感受到太大的视角误差。

value1<-data1$Value
value21 <- data2[data2$Category == 'Categroy1',"Value"]
value22 <- data2[data2$Category == 'Categroy2',"Value"]

mydata <- data.frame(value1,value21,value22)

mydata$value31 <- rescale(mydata$value21,c(0,50))
mydata$value32 <- rescale(mydata$value22,c(0,50))

  value1 value21 value22   value31   value32
1     39    0.49    0.48 50.000000 50.000000
2     38    0.23    0.38 16.666667 34.848485
3     50    0.10    0.48  0.000000 50.000000
4     33    0.38    0.15 35.897436  0.000000
5     18    0.34    0.40 30.769231 37.878788
6     49    0.13    0.16  3.846154  1.515152

这是最终的折现结果,在geom_line中使用rescale函数实际上就是做的这种度量重新自定义映射的过程。

第二次自定义映射——次坐标轴刻度标签转换:

仅仅做以上步骤还不够,因为这只能保障次坐标轴的数据点位置相对于整个坐标系统而言,不会出现太大的视觉误差,但是现在的问题是这个图形对象中有两套不同的度量,所以必须声明不同的y轴度量标准,也就是y轴的刻度线及刻度标签,刻度标签的定义就是本案例的第二个重点,它仍然是通过rescale函数进行了一次度量的重新映射。

不过这次映射的过程刚好是相反的操作,即将之前已经被标准化到0~50区间内的原始度量标签通过rescale函数再次标准化到0~0.5区间内,这样保障显示在次坐标轴上的度量是符合原始数据极差范围呢。

说的有些拗口了,实际上以上过程思路很简单,就是先将数据映射到正确的位置,然后将次坐标轴刻度线度量标签再按照真实极差进行分布,一虚一实,正好达到了模拟效果。

scale_y_continuous(
          breaks=pretty_breaks(5),                   #创建主坐标轴的刻度区间(这里是5个区间6个刻度点)         
          sec.axis = sec_axis( ~rescale(.,c(0,0.5)), #对次坐标轴刻度标签的二次映射(极差范围指定真实极差即可)  
          name = "Categroy",                         #次坐标轴名称
          labels=sprintf("%d%%",(0:5)*10))           #刻度标签显示格式(这里是百分号)
          )

思路大体上就是这样子,希望这一篇文章可以帮到大家!


在线课程请点击文末原文链接:

Hellobi Live | R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Dat

编辑于 2017-12-10 10:24