用Tableau画嵌套气泡图

最近研究CJ Mayes博客里的一篇文章《Don’t Burst My Bubble》,里面介绍了circular packing的做法。

最近研究CJ Mayes博客里的一篇文章《Don’t Burst My Bubble》,里面介绍了circular packing的做法。

__BLOCK_figure__ __BLOCK_div__ https://cj-mayes.com/2021/09/21/dont-burst-my-bubble

经过一段时间的研究,源代码只包含生成图形的部分,我对源代码进行了一些修改,增加了数据处理过程,用Tableau画圆的部分也采用目前自己最喜欢的并集+表计算的方式。

由于涉及代码和tableau画圆的知识,属于高级图形的范畴,所以本文不做过多原理的讲解。

数据处理

原文直接使用的是json格式的数据。

这种数据我们不好获取,正常情况下,我们excel里的数据是这样存储的。

所以需要通过python转成有层次的json格式,代码如下:

import pandas as pd
import json
df=pd.read_csv('test.csv',encoding='gb2312')

Level1_sum=df.groupby('Level1').sum('value').reset_index()
Level1_sum.rename(columns={'Level1':'Lebel'},inplace=True)
concat_sum=pd.concat([Level1_sum])
dic_Level1=[]
for x in range(Level1_sum.shape[0]):
    Level2_sum=df[df.Level1==Level1_sum.Lebel[x]].groupby('Level2').sum('value').reset_index()
    Level2_sum.rename(columns={'Level2':'Lebel'},inplace=True)
    concat_sum=pd.concat([concat_sum,Level2_sum])
    dic_Level2=[]
    for y in range(Level2_sum.shape[0]):
        Level3_sum=df[df.Level2==Level2_sum.Lebel[y]].groupby('Level3').sum('value').reset_index()
        Level3_sum.rename(columns={'Level3':'Lebel'},inplace=True)
        concat_sum=pd.concat([concat_sum,Level3_sum])
        dic_Level3=[]
        for z in range(Level3_sum.shape[0]):
            dic_Level3.append(dict(id=Level3_sum.Lebel[z] ,datum=Level3_sum.value[z]))
        dic_Level2.append(dict(id=Level2_sum.Lebel[y] ,datum=Level2_sum.value[y],children=dic_Level3))
    dic_Level1.append(dict(id=Level1_sum.Lebel[x] ,datum=Level1_sum.value[x],children=dic_Level2))

#生成json
data=json.dumps(eval(str(dic_Level1))) 
#第二部分输出没有value值,增加一个输出文件,方便tableau里添加标签
concat_sum.reset_index(drop=True).to_csv('test_label.csv',encoding='utf-8-sig')

生成图形

import csv
import circlify
import matplotlib.pyplot as plt

# 将第一部分的json数据赋值给data
data=json.loads(data)

# Compute circle positions thanks to the circlify() function
# The maximum radius is set to 1.
circles = circlify.circlify(
    data,
    show_enclosure=False,
    target_enclosure=circlify.Circle(x=0, y=0, r=1)
)

# To ensure in Tableau the sizing works the fig size is the same x*y.
fig, ax = plt.subplots(figsize=(10,10))

# Add axes
ax.axis('on')

# Find axis boundaries, lim will be 1.0 with current settings
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# Amend Radius zone, Make sure Inner pad always > than Outer pad
padding_outer = 1
padding_inner = 1

header = ['ID', 'X co-ord', 'Y-cord', 'Radius', 'Rank',"Level"]
rank = 1

with open('test_result.csv', 'w',newline="") as f:
    writer = csv.writer(f)
    writer.writerow(header)

    # World Level
    for circle in circles:
        if circle.level != 1:
            continue
        x, y, r = circle
        level =1
        label = circle.ex["id"]
        #print(label,x,y,r,level)
        printdata = [label, x, y, r, rank,level]
        rank = rank + 1
        writer.writerow(printdata)

        # Not needed for Tableau
        ax.add_patch(plt.Circle((x, y), r*padding_outer, alpha=0.5, linewidth=2, color="lightblue"))

        # Continent Level
        for circle in circles:
            if circle.level != 2:
                continue
            x, y, r = circle
            level =2
            label = circle.ex["id"]
            #print(label,x,y,r,level)
            printdata = [label, x, y, r, rank,level]
            rank = rank + 1
            writer.writerow(printdata)

            # Not needed for Tableau
            ax.add_patch(plt.Circle((x, y), r*padding_outer, alpha=0.5, linewidth=2, color="lightblue"))

        # Country Level 
        for circle in circles:
            if circle.level != 3:
                continue
            x, y, r = circle
            level =3
            label = circle.ex["id"]
            #print(label,x,y,r)
            printdata = [label, x, y, r, rank,level]
            rank = rank + 1
            writer.writerow(printdata)

            # Not needed for Tableau
            ax.add_patch(plt.Circle((x, y), r*padding_inner, alpha=0.5, linewidth=2, color="#69b3a2"))
            plt.annotate(label, (x,y), ha='center', color="white")

# Show The Example Of The Graph We Will Be Recreating!
plt.show()

同时生成了一个test_result.csv的文件。文件记录了圆形的ID,原点的x、y坐标和半径r。

制作图形

导入test_result.csv文件,做一次并集。

创建计算字段

如果得到了上面的图形,就证明没有任何问题,我在利用其他数据源的时候,出现过ID字段重名的情况,用Rank字段作为区分也没有问题,不过建议还是处理一下数据,保证ID值唯一。

最终,我们使用多边形,并修改好颜色就是我们想要的结果了。如果需要增加value值的提示,可以用生成的test_label.csv文件做关联。

至于图形的其他玩法,比如嵌套更多层次的圆形,那么就需要你自己去探索了,可能需要修改代码。

另外画圆的原理,我推荐我自己在cctalk上的课程《Tableau高级图表进阶》,第三部分第一课(免费)。

📖 相关文章
用Tableau画嵌套饼图(偷懒版旭日图)
用Tableau画漏斗图的4种方法
用Tableau画延展条形图(Extended Bar Chart)
用Tableau画小提琴图
用Tableau画Voronoi-Treemap
——————————————————————————————

No comments yet