ggplot2双坐标轴的解决方案

浏览: 1781

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

但是最近有好几个小伙伴儿跟我咨询关于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")
 )

image.png

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

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

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))           #刻度标签显示格式(这里是百分号)
         )

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

在线课程请点击文末原文链接:
往期案例数据请移步本人GitHub:
https://github.com/ljtyduyu/DataWarehouse/tree/master/File

image.png

推荐 0
本文由 杜雨 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册