使用html table生成统一标签全景图

目录

使用html生成全景图

获取标签的统计数据

为每个标签指定格数和形状

标签填充到矩阵

合并td生成table

结尾


最近项目有一个需求,需要为统一标签生成一个全景图,类似于tree map chart,每个标签的大小由标签下面的博文数量决定,按照近一个月的博文数量排序并配色,在红和绿之间进行渐变,最红代表近一个月新增最多,最绿代表最近一个新增最少。

使用html生成全景图

一提到这种稍微有点儿技术含量的图,大部分人都会想到使用第三方体用的包,抱着不重复造轮子的想法,也找了一些直接可用的js包,例如基于d3.js实现的treemap-chart。但是这里要使用社区帖子的形式发表出来,帖子最多只能添加原生的html内容,js暂时还不支持,于是乎只能另辟蹊径了。

html的table可以合并格子,td的colspan和rowspan属性就能指定格子的行和列,再配合width和height属性就能实现指定大小的格子。

<td colspan="2" rowspan="4" width="80" height="160"></td>

当然,页面渲染的时候还是会根据页面的实际情况进行调整大小,这个时候固定住整个表格就行了,width和height一定要指定。

<table width="1600" height="1600" style="border-collapse: collapse;table-layout: fixed;">
<!-- 省略tr/td -->
</table>

到这里使用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生成table

再次遍历矩阵,对每个标签按照填充的形状合并表格,即指定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"""<table width="{len(tree_matrix)*unit_size}" height="{len(tree_matrix)*unit_size}" style="table-layout: fixed;">"""
    for i in range(len(tree_matrix)):
        table += f"""<tr style="border:0px;">"""
        for j in range(len(tree_matrix)):
            tag = tree_matrix[i][j]
            if tag is None:
                table += f"""<td width="{unit_size}" height="{unit_size}" style="border: 0.1px;border-color: white;overflow:hidden;text-overflow:ellipsis;font-weight: 300;font-size: 1px;background-color: rgb({red_value}, {green_value}, 0);"></td>"""
                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"""<td colspan="{s2}" rowspan="{s1}" width="{s2*unit_size}" height="{s1*unit_size}" style="border: 0.1px;border-color: white;text-align: center;overflow:hidden;text-overflow:ellipsis;font-weight: 300;font-size: 1px;background-color: rgb({red_value}, {green_value}, {blue_value});"><a target='_blank' href="{self.tag_area_dict[tag]['url']}" style="color:black;text-decoration:underline;"><div>{tag}</div></a></td>"""
            merged_tag_set.add(tag)
        table += f"</tr>"
    table += "</table>"
    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、社区帖子发布后经过了进一步的渲染,样式可能有一些出入,需要更进一步调整。

猜你喜欢

转载自blog.csdn.net/zxm2015/article/details/127449797