目录
使用html生成全景图
获取标签的统计数据
为每个标签指定格数和形状
标签填充到矩阵
合并td生成table
结尾
最近项目有一个需求,需要为统一标签生成一个全景图,类似于tree map chart,每个标签的大小由标签下面的博文数量决定,按照近一个月的博文数量排序并配色,在红和绿之间进行渐变,最红代表近一个月新增最多,最绿代表最近一个新增最少。
一提到这种稍微有点儿技术含量的图,大部分人都会想到使用第三方体用的包,抱着不重复造轮子的想法,也找了一些直接可用的js包,例如基于d3.js实现的treemap-chart。但是这里要使用社区帖子的形式发表出来,帖子最多只能添加原生的html内容,js暂时还不支持,于是乎只能另辟蹊径了。
html的table可以合并格子,td的colspan和rowspan属性就能指定格子的行和列,再配合width和height属性就能实现指定大小的格子。
当然,页面渲染的时候还是会根据页面的实际情况进行调整大小,这个时候固定住整个表格就行了,width和height一定要指定。
到这里使用table来实现全景图的先决条件已经具备,剩下就是把标签转成形状,填进矩阵,再转换成table代码就行了。
数据在pg和数仓中都有,需要统计出来每个标签的全部博文数量和近一个月的博文数量。直接使用sql在pg中查询统计,由于数据较多,虽然有索引但是耗时还是比较久的,使用数仓的话建一个定时任务,定时执行sql统计任务,结果保存在表中,每次需要数据直接查询表就可以了,不影响线上资源,这就是OLAP和OLTP的区别所在了吧。
这里选择后者直接在数仓中操作了,虽然做了额外的操作,还是有一劳永逸的感觉。
博文数量多的标签占的格数就越多,最少也需要有一个格子,观察数据后发现,数据不是很规律,大量的数据都集成少数标签,这里决定采用分段函数进行映射。为了展示上的方便,标签格数限定在[1,2,3,4,6,8,9]里面,这样得到的形状都是矩形(1*5这种太长了,故没有考虑)。
每个标签又指定了其可能的形状,例如,2的可能形状为1*2和2*1,6的可能形状为2*3或者3*2。
def _get_tag_shape(self, count):
"""根据标签数量获取对应形状和形状块数"""
tag_matrix_count = 0
possible_size_list = []
if count > 100000000:
tag_matrix_count = 9
possible_size_list.append((3,3))
elif count > 10000000:
tag_matrix_count = 8
possible_size_list.append((2,4))
possible_size_list.append((4,2))
elif count > 1000000:
tag_matrix_count = 6
possible_size_list.append((2,3))
possible_size_list.append((3,2))
elif count > 100000:
tag_matrix_count = 4
possible_size_list.append((2,2))
elif count > 10000:
tag_matrix_count = 3
possible_size_list.append((1,3))
possible_size_list.append((3,1))
elif count > 1000:
tag_matrix_count = 2
possible_size_list.append((1,2))
possible_size_list.append((2,1))
else:
tag_matrix_count = 1
possible_size_list.append((1,1))
return tag_matrix_count, possible_size_list
接下来还要根据所有的标签占用的总格子数生成一个矩阵,每个格子赋初值None。
def _get_tree_matrix(self, total_count, max_len=300):
"""生成矩阵"""
side_len = 0
for i in range(max_len):
if i * i >= total_count:
side_len = i
break
if side_len == 0:
return
tree_matrix = list()
for i in range(side_len):
tree_matrix.append(list())
for j in range(side_len):
tree_matrix[i].append(None)
return tree_matrix
这一步就是将所有标签的形状填到矩阵中,标签填入的循序由近一个月博文数量决定,数量多先填,数量少后填。首先从矩阵中挑选一个没有填充的起始点,观察起始点能否将标签可能的形状填进去,如果都不能,则查找下一个起始点,直到能填进去为止。
同时按照对角线遍历,将标签填充到对角线附近,达到的效果为矩阵左上角是热门标签,右下角是冷门的标签。首先来看一下矩阵的对角线遍历,主要思想就是正向i+1,j-1,逆向j+1,j-1。
def find_diagonal_order(self, tree_matrix):
"""按照对角线遍历"""
order_list = []
side_len = len(tree_matrix)
reverse_flag = True
i = 0
j = 0
while i < (side_len-1) or j < (side_len-1):
order_list.append((i, j))
# 按照方向进行移动
if reverse_flag:
i += 1
j -= 1
else:
i -= 1
j += 1
# 如果位置超出边界,进行偏移纠正
if j < 0 and i < side_len:
j += 1
reverse_flag = False
elif i < 0 and j < side_len:
i += 1
reverse_flag = True
# 两个对角
elif i == side_len:
i -= 1
j += 2
reverse_flag = False
elif j == side_len:
j -= 1
i += 2
reverse_flag = True
order_list.append((i, j))
return order_list
接下来就是填充矩阵部分,将所有的标签填到矩阵中。
def fill_tag(self, tag, tag_shape_dict, tree_matrix, order_list):
"""填写标签"""
possible_size_list = tag_shape_dict["possible_size_list"]
tag_filled_shape = None
no_suit_positions = []
i, j = self._get_start_position(no_suit_positions, tree_matrix, order_list)
filled = False
while i is not None and j is not None:
for s1, s2 in possible_size_list:
continue_flag = True
for i1 in range(i, i + s1, 1):
if not continue_flag:
break
for j1 in range(j, j + s2, 1):
if (i + s1 > len(tree_matrix)) or (j + s2 > len(tree_matrix)) \
or tree_matrix[i1][j1] is not None:
no_suit_positions.append((i, j))
continue_flag = False
break
if continue_flag:
for i1 in range(i, i + s1, 1):
for j1 in range(j, j + s2, 1):
tree_matrix[i1][j1] = tag
tag_filled_shape = (s1, s2)
filled = True
break
if filled:
break
i, j = self._get_start_position(no_suit_positions, tree_matrix, order_list)
return tag_filled_shape
再次遍历矩阵,对每个标签按照填充的形状合并表格,即指定td的colspan和rowspan属性,同时生成合并后table的width和height,单元格的大小unit_size也需要指定。
def generate_table(self, tag_filled_shape_dict, tree_matrix, unit_size=40):
"""生成table"""
merged_tag_set = set()
table = f""""""
for i in range(len(tree_matrix)):
table += f""""""
for j in range(len(tree_matrix)):
tag = tree_matrix[i][j]
if tag is None:
table += f""" "
table += " """
continue
if tag in merged_tag_set or tag not in tag_filled_shape_dict:
continue
s1, s2 = tag_filled_shape_dict[tag]
red_value, green_value, blue_value = self._generate_color(self.tag_area_dict[tag]["rank"], len(self.tag_area_dict))
table += f""" """
merged_tag_set.add(tag)
table += f"
"
return table
为了实现整体上面红绿渐变,这里还需要使用css的rgb来指定背景色,这里实现了一个任意两个点之间的颜色渐变方法。
def _generate_color(self, rank, tag_num):
"""
从开始位置到结束位置渐变
"""
start_position = (255, 0, 0)
middle_position = (255, 255, 255)
end_position = (0, 255, 0)
value = (rank + 1) / tag_num
if value < 0.5:
red_value = start_position[0] + value * (middle_position[0] - start_position[0]) * 2
gree_value = start_position[1] + value * (middle_position[1] - start_position[1]) * 2
blue_value = start_position[2] + value * (middle_position[2] - start_position[2]) * 2
else:
red_value = middle_position[0] + (value - 0.5) * (end_position[0] - middle_position[0]) * 2
gree_value = middle_position[1] + (value - 0.5) * (end_position[1] - middle_position[1]) * 2
blue_value = middle_position[2] + (value - 0.5) * (end_position[2] - middle_position[2]) * 2
return int(red_value), int(gree_value), int(blue_value)
到了这里基本的landscape就已经完成了,但是由于标签的数量不能刚好能将矩阵填满,导致出来的图形不规则,这里对空的格子进行填充,填充附近标签相同的颜色,能保证出来的图是一个完整的矩阵。
社区发布的全站标签landscape
https://bbs.csdn.net/topics/608731536https://bbs.csdn.net/topics/608731536
社区发布的标签分组landscape
https://bbs.csdn.net/topics/608731909https://bbs.csdn.net/topics/608731909
本文使用html table实现了一个最基本的标签landscape,当然也还有很多改进的空间,例如:
1、填充标签时由于遍历算法和标签的形状限制,可能会导致前面一些孤立的单元格没有被填充,后面的占用单元格数量为1标签将会填进去,造成红色标签里面可能会有一个绿色标签,但是如果不填写这些格子就成了空。
2、最后面的填充空格子时采用了最简单的颜色填充方法,可能进一步的合并空格效果会好一些。
3、社区帖子发布后经过了进一步的渲染,样式可能有一些出入,需要更进一步调整。