python多区间重叠可视化分析

实现目标:
多个重叠区间段的可视化分析。下面给出了一个用visio绘制的例子。

Drawing

图中用矩形来表示一个区间段,矩形的左边为区间起点,矩形的右边为区间终点。
带箭头的线段是特殊的区间,即其区间起点和终点重合。

visio画出来效果还不错,不过这样绘制速度太慢,很多工作需要手动调整,如果需要绘制的区间较多,或者需要多次绘制,那样是很容易出错的.
最好能用程序自动绘制,今天就用python来小试牛刀.

设计

我们首先分析出要通过程序设计自动绘制出例子中的图形需要遵循的一些原则。

  1. 原则一:通过矩形块的不同高度来避免重合,给出层次化的展示。
    这要求:
  • 区间的宽度越大,则其高度越小
  • 优先绘制高度高的矩形
  • 对于高度一致的矩形,若二者有区间有重合,则应使用更改为不同的高度
  1. 原则二:区间的名字标注应明确,不位于多个矩形上

上图写在矩形的左上角。显然当矩形最小高度差大于文字高度时,此条件自然满足,但严格地说,应该是每个矩形的左上角与其他矩形的高度差
大于文字高度。

Python实现

  1. 区间参数输入
    采用字典变量来表示区间。字典的key为需求编号,区间参数用list两个值表示,0序号值表示需求的名字(name),
    1序号值表示区间参数。

区间参数分以下几种情况:

  • 单个区间
  • 多个区间
  • 单个点
  • 以上混合

为适应上述所有可能的组合参数列表按如下方式表示

[(区间l xleft,区间1 xright),(区间2 xleft, 区间2 xright),点X,…]

对于单个点的区间X, 只有xleft。可通过type是否为tuple来判断是否为一个区间。

1
2
3
Requires={1:["猪肉",[(100,150),(200,400)]],
2:["羊肉",[(200,400)]],
3:["天山雪莲",[1011]], }

每个矩形块也有颜色的区别的,这有两种方案,然程序随机选择颜色,另外就是指定颜色了。采用指定颜色的方案,最简单的方法
就是把颜色信息加在参数列表中,用一个字符串表示。不过既然我们选择python,就有更加方便的方法:

1
2
3
Requires={1:["猪肉",[(100,150),(200,400)],{'color':'r'}],
2:["羊肉",[(200,400)],{'color':'g'}],
3:["天山雪莲",[1011],{'color':'b'}], }

这里用的字典的方式,这有方便扩展别的参数,比如线宽、透明度等等都可以指定。

  1. 从需求限制中提取出所有需要绘制的区间参数

rects=[[区间名称,区间坐标,区间宽度,plotPara],…]

1
2
3
4
5
6
7
rects=[]
for key, var in Requires.iteritems():
for regionPra in var[1]:
if type(regionPra) is tuple:
rects.append([var[0],regionPra,regionPra[1]-regionPra[0]],var[-1])
else:
rects.append([var[0],regionPra,0],var[-1])
  1. 依据区间参数确定绘制顺序及绘制高度,并进行绘制

    • 依据width对rects从大到小排序。
      从设计原则中我们知道,区间宽度越大,高度越小,所以我们首先来做一个排序,按区间宽度从小到大:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
       rectsSorted=[]
    minWidth=1e9
    heightMin=0.1
    heightMax=0.9
    d=(heightMax-heightMin)/NonZeroNum
    height=heightMax
    for sortedIndex in range(len(rects)):
    minWidth=1e9
    for rect in rects:
    if(rect[2]<minWidth):
    minWidth=rect[2]
    rectPoping=rect
    rects.remove(rectPoping)#从原序列中去掉矩形宽度最小的
    rectPoping[-1]['height']=height
    if rectPoping[2]!=0:
    height-=d
    rectsSorted.append(rectPoping)#放到排序序列中

    这里的思路是:遍历原序列,从中找到宽度最小的,将其放入新序列,并从序列中删除.不断重复此过程,直到原序列为空.

    • 其实在排序之前,我们可能遗漏了一种情况:多个区间完全重合.

    对于这种情况,我并不绘制多个不同高度的矩形,
    而是将多个矩形合并为一个,把它们的name拼接在一起(name是字符串),最后绘图时在矩形的文字标注上能看出有多个条目就够了.

    预处理—去除完全重合的区间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
       ###############排除重合####################
    #排除重合-步骤1. 找出相互重合的编号集
    IndexSameChecked=[] #相互重合的序号
    for i in range(len(rectsSorted)-1):
    for j in range(i+1, len(rectsSorted)):
    if (rectsSorted[j][2]>0 and rectsSorted[i][2]>0):#都是矩形
    xlefti=rectsSorted[i][1][0]
    xrighti=rectsSorted[i][1][1]
    xleftj=rectsSorted[j][1][0]
    xrightj=rectsSorted[j][1][1]
    if(xrightj==xrighti and xleftj==xlefti):#重合
    if(len(IndexSameChecked)>0):
    hasApended=False
    for setNum,sameIndSet in enumerate(IndexSameChecked):
    if (i in sameIndSet) or (j in sameIndSet):
    IndexSameChecked[setNum]=IndexSameChecked[setNum].union(set((i,j)))
    hasApended=True
    if not hasApended:
    IndexSameChecked.append(set((i,j)))
    elif (len(IndexSameChecked)==0):
    IndexSameChecked.append(set((i,j)))
    elif (rectsSorted[j][2]==0 and rectsSorted[i][2]==0):#都是线
    xi=rectsSorted[i][1]
    xj=rectsSorted[j][1]
    if(xi==xj):
    if(len(IndexSameChecked)>0):
    hasApended=False
    for setNum,sameIndSet in enumerate(IndexSameChecked):
    if (i in sameIndSet) or (j in sameIndSet):
    IndexSameChecked[setNum]=IndexSameChecked[setNum].union(set((i,j)))
    hasApended=True
    if not hasApended:
    IndexSameChecked.append(set((i,j)))
    else:
    IndexSameChecked.append(set((i,j)))

    #排除重合-步骤2. 剔除重合项
    for seti,sameIndSet in enumerate(IndexSameChecked):
    name=""
    for ind in sameIndSet:
    name=name+rectsSorted[ind][0]+";"
    rectsSorted[ind][0]=name
    IndexSameChecked[seti].remove(ind)

    readyDel=[]
    for sameIndSet in IndexSameChecked:
    for ind in sameIndSet:
    print "del",ind,rectsSorted[ind][0]
    readyDel.append(rectsSorted[ind])

    for item2del in readyDel:
    rectsSorted.remove(item2del)
    ###############排除重合end####################
    • 下面要来考虑区间交叠的文字标注问题

    过程应该是这样的,按高度从高到低的顺序考察每个矩形(不含线段),
    对每个考察对象和比它次高的对象,若二者完全不相交,它们可以使用相同的高度;
    若二者有区间相交,且较高者位于右方,则应确保较高者高于次高者”文字标注的高度”,以保证文字标注的清晰.
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    for recti in range(len(rectsSorted)-1):
    if (rectsSorted[recti][2]>0):
    xlefti=rectsSorted[recti][1][0]
    xrighti=rectsSorted[recti][1][1]
    xleftj=rectsSorted[recti+1][1][0]
    xrightj=rectsSorted[recti+1][1][1]
    if (xrighti<xleftj or xrightj<xlefti):#不相交
    print "equal"
    rectsSorted[recti][4]=rectsSorted[recti+1][4]
    elif(xrightj==xrighti and xleftj==xlefti):#重合
    print "overlap:",rectsSorted[recti][0]
    rectsSorted[recti][4]=rectsSorted[recti+1][4]
    else:
    if (xlefti>xleftj):
    rectsSorted[recti][4]=rectsSorted[recti+1][4]+0.03

    (此段代码尚待改进)

    • 现在各个矩形的绘制顺序、矩形高度都确定了,只需要逐个绘制出就行了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import matplotlib.pyplot as plt
    import matplotlib
    myfont=matplotlib.font_manager.FontProperties(fname=r'C:\WINDOWS\Fonts\STSONG.ttf',size=10)
    plt.close('all')
    fig=plt.figure(figsize=[ 17.7*6 , 3.7125*2])
    fig.subplots_adjust(left=0.03,right=0.97,top=0.97,bottom=0.11)
    for rect in rectsSorted:
    if type(rect[1]) is tuple:
    alpha=(rect[4]-heightMin+0.05)*0.4/(heightMax-heightMin)
    if alpha<0.15:
    alpha=0.15
    rectDraw=plt.Rectangle((rect[1][0],0),rect[2],rect[-1]['height'],facecolor=rect[-1]['color'],alpha=alpha)
    plt.gca().add_patch(rectDraw)
    plt.text(rect[1][0],rect[-1]['height'],rect[0],fontproperties=myfont,verticalalignment='top')
    else:
    plt.plot([rect[1],rect[1]],[0,1.11],color=rect[-1]['color'],alpha=0.4)
    plt.text(rect[1],0.9,rect[0],fontproperties=myfont,rotation=90,ha='center',va='bottom')
    plt.yticks([])
    ax=plt.gca()
    plt.grid(b='on',axis='x')
    ax.tick_params(axis='x',labelsize=8)
    for tick in ax.xaxis.get_major_ticks():
    tick.label.set_rotation('vertical')
    plt.axis('tight')

    这段代码中,我们设置了中文字体;调整了图片四周的留白;将横坐标改成了竖直显示;设置了x,y轴的ticks.
    这些都是使用matplotlib较常遇到的.

总结

最终的绘制效果还不错,这里就不展示了.可以看到程序把很多繁杂的工作自动化了.而visio可随时手动的更改也算是一种优势,如果visio也能通过程序设计自动的完成一些绘制那就最好了!如果有读者知道的话,请一定告诉我.

热评文章