Merge commit '47296980495f8bbfc9493e93de85dd62de6fa6b9' as 'paste-framework'

This commit is contained in:
zwf
2026-06-02 19:09:22 +08:00
107 changed files with 21484 additions and 0 deletions
+704
View File
@@ -0,0 +1,704 @@
import re
from typing import Optional
import svgwrite
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.shapes import Rect
from svgwrite.text import Text
from paste.util import ustr
class TextRect(Group):
"""
可显示文本的矩形。
"""
def __init__(self, text, insert, text_extra: dict = None, rect_extra: dict = None, **extra):
# 父类初始化
super().__init__(**extra)
self.text = text
"""
要显示的文本内容。
"""
self.extra = extra
"""
组合扩展信息。
"""
self.rectExtra = rect_extra if rect_extra is not None else {}
"""
外框扩展信息。
"""
self.textExtra = text_extra if text_extra is not None else {}
"""
文本扩展信息。
"""
self.rectInsert = insert
"""
整体位置参数,即外框的位置参数。
"""
# 初始化文本尺寸
_fs = self.font_size
self.textInsert = self.text_pos
"""
文本位置参数。
"""
# 文本初始化
self.textElement = Text(self.text, insert=self.textInsert, **self.textExtra)
# 矩形初始化
self.rectElement = Rect(insert=self.rectInsert, size=self.rect_size, **self.rectExtra)
# 加入元素
self.add(self.rectElement)
self.add(self.textElement)
@property
def font_size(self):
"""
从样式中识别字体大小,单位用像素,缺省 14px。
:return: 字体大小
"""
_font_size = self.textExtra.get('font-size', self.extra.get('font-size', f"{14}px"))
self.textExtra['font-size'] = _font_size
_size = re.sub(r'\D', '', _font_size.strip())
return int(_size)
@property
def text_width(self):
"""
文本宽度(近似)。
"""
total = len(self.text)
q_count = ustr.str_q_count(self.text)
return q_count * self.font_size + (total - q_count) * self.font_size * 0.5
@property
def text_height(self):
"""
文本高度(近似)。
"""
return self.font_size * 1.2
@property
def rect_width(self):
"""
外框宽度。
"""
return self.text_width + self.font_size * 1.5
@property
def rect_height(self):
"""
外框高度。
"""
return self.text_height * 1.9
@property
def rect_size(self):
"""
外框尺寸。
"""
return self.rect_width, self.rect_height
@property
def text_pos(self):
"""
文本位置。
"""
return \
self.rectInsert[0] + (self.rect_width - self.text_width) * 0.5, \
self.rectInsert[1] + self.text_height * 1.25
def reposition(self, position: tuple):
"""
重新定位。
:param position: 位置坐标
"""
self.rectInsert = position
self.rectElement.attribs['x'] = self.rectInsert[0]
self.rectElement.attribs['y'] = self.rectInsert[1]
self.textInsert = self.text_pos
self.textElement.attribs['x'] = self.textInsert[0]
self.textElement.attribs['y'] = self.textInsert[1]
def point_bottom(self):
"""
底部点。
"""
return self.rectInsert[0] + self.rect_width / 2, self.rectInsert[1] + self.rect_size[1]
def point_top(self):
"""
顶部点。
"""
return self.rectInsert[0] + self.rect_width / 2, self.rectInsert[1]
def point_left(self):
"""
左侧点。
"""
return self.rectInsert[0], self.rectInsert[1] + self.rect_height / 2
def point_right(self):
"""
右侧点。
"""
return self.rectInsert[0] + self.rect_width, self.rectInsert[1] + self.rect_height / 2
@classmethod
def horizontal_path(cls, start: tuple, end: tuple, **extra):
"""
生成水平方向连接线。
:param start: 起点坐标
:param end: 终点坐标
:param extra: 扩展参数
:return: 路径对象
"""
_p_control = [
(start[0] + end[0]) * 0.5,
start[1]
]
_p_center = [
(start[0] + end[0]) * 0.5,
(start[1] + end[1]) * 0.5
]
_path = Path(**extra)
_path.push(['M', start])
_path.push(['Q', _p_control + _p_center])
_path.push(['T', end])
return _path
@classmethod
def vertical_path(cls, start: tuple, end: tuple, **extra):
"""
生成垂直方向连接线。
:param start: 起点坐标
:param end: 终点坐标
:param extra: 扩展参数
:return: 路径对象
"""
_p_control = [
start[0],
(start[1] + end[1]) * 0.5
]
_p_center = [
(start[0] + end[0]) * 0.5,
(start[1] + end[1]) * 0.5
]
_path = Path(**extra)
_path.push(['M', start])
_path.push(['Q', _p_control + _p_center])
_path.push(['T', end])
return _path
def choose_point(self, sibling: 'TextRect'):
"""
选择与目标文本框的连线点。
返回起点(tuple)在自生文本框上,终点(tuple)在目标文本框上。
:param sibling: 目标文本框
:return: 起点、终点、是否水平连线
"""
_start = self.point_bottom()
_end = sibling.point_top()
_is_horizontal = True
if self.point_bottom()[1] > sibling.point_top()[1]:
if self.point_right()[0] < sibling.point_left()[0]:
_start = self.point_right()
_end = sibling.point_left()
_is_horizontal = False
elif self.point_left()[0] > sibling.point_right()[0]:
_start = self.point_left()
_end = sibling.point_right()
_is_horizontal = False
else:
_start = self.point_top()
_end = sibling.point_bottom()
_is_horizontal = True
elif self.point_top()[1] < sibling.point_bottom()[1]:
if self.point_right()[0] < sibling.point_left()[0]:
_start = self.point_right()
_end = sibling.point_left()
_is_horizontal = False
elif self.point_left()[0] > sibling.point_right()[0]:
_start = self.point_left()
_end = sibling.point_right()
_is_horizontal = False
else:
_start = self.point_bottom()
_end = sibling.point_top()
_is_horizontal = True
else:
if self.point_right()[0] < sibling.point_left()[0]:
_start = self.point_right()
_end = sibling.point_left()
_is_horizontal = False
elif self.point_left()[0] > sibling.point_right()[0]:
_start = self.point_left()
_end = sibling.point_right()
_is_horizontal = False
else:
_start = self.point_bottom()
_end = sibling.point_top()
_is_horizontal = True
return _start, _end, _is_horizontal
def connect(self, sibling: 'TextRect', **extra):
"""
取得连接路径。
:param sibling: 目标文本框
:param extra: 连线扩展参数
:return: 连接路径
"""
_start, _end, _is_horizontal = self.choose_point(sibling)
if _is_horizontal:
return self.vertical_path(_start, _end, **extra)
else:
return self.horizontal_path(_start, _end, **extra)
class RelationGraph:
"""
SVG 关系图。
根据 title 名称和 row_list 列表数据输出 svg 格式的关系图谱。
"""
def __init__(self, filename: str = 'noname.svg'):
self.filename = filename
self.width = 800
"""
画布宽度。
"""
self.height = 600
"""
画布高度。
"""
self.vhSpace = 170
"""
内容垂直浮动空间。
"""
self.lrSpace = 100
"""
内容左右留白空间。
"""
self.titleTextExtra = {
'font-size': '16px', 'fill': 'rgb(255, 255, 255)'
}
"""
标题文本样式。
"""
self.titleRectExtra = {
'rx': 10, 'ry': 10, 'fill': 'rgb(233, 72, 41)', 'fill-opacity': 1, 'stroke': 'rgb(233, 72, 41)'
}
"""
标题外框样式
"""
self.textExtra = {
'font-size': '14px', 'fill': 'rgb(255, 255, 255)'
}
"""
普通文本样式。
"""
self.rectExtra = {
'rx': 10, 'ry': 10, 'fill': 'rgb(65, 130, 164)', 'fill-opacity': 1, 'stroke': 'rgb(65, 130, 164)'
}
"""
普通文本外框样式。
"""
self.pathExtra = {
'fill': 'none', 'stroke': 'rgb(65, 130, 164)'
}
"""
连线样式。
"""
self.drawing = svgwrite.Drawing(filename=self.filename)
"""
主绘图对象。
"""
self.attribs = self.drawing.attribs
"""
图像样式。
"""
self.save = self.drawing.save
"""
保存文件方法。
"""
self.attribs.update({
'width': self.width, 'height': self.height
})
self.titleTr: Optional[TextRect] = None
"""
标题文本框对象。
"""
def draw(self, title: str, row_list: list[dict]):
"""
绘制图形。
:param row_list: 数据对象列表,必须包含 unit_name, unit_uscid, enterprise_id 三个字段
:param title: 标题文本
:return: 自身对象
"""
# 重定设图像参数
self.attribs.update(self.attribs)
# 创建标题文本框
self.titleTr = TextRect(
text=title, insert=(0, 0), text_extra=self.titleTextExtra, rect_extra=self.titleRectExtra, **{
'debug': False
}
)
self.titleTr.reposition((
(self.width - self.titleTr.rect_width) * 0.5, (self.height - self.titleTr.rect_height) * 0.5 - 20
))
self.drawing.add(self.titleTr)
_tr_list: list[TextRect] = []
for _i, _row in enumerate(row_list):
# 遍历数据,初始创建所有的文本框,得到文本框尺寸信息
# 同时保留所有需要输出的数据
_text = f"{_row['short_name']} ({_row['count']})"
_tr = TextRect(
text=_text, insert=(0, 0), rect_extra=self.rectExtra, **self.textExtra, **{
'debug': False,
'data-name': _row['unit_name'],
'data-uscid': _row['unit_uscid'],
'data-enterprise-id': _row['enterprise_id'],
}
)
_tr_list.append(_tr)
_harf = int(len(_tr_list) / 2) if int(len(_tr_list) % 2) == 0 else int(len(_tr_list) / 2) + 1
_top_list = []
_lft_list = _tr_list[:_harf]
_rit_list = _tr_list[_harf:]
_btm_list = []
if len(_tr_list) >= 12:
_top_list = _lft_list[:2]
_lft_list = _lft_list[2:]
if len(_tr_list) >= 14:
_btm_list = _rit_list[-2:]
_rit_list = _rit_list[:-2]
# 遍历所有顶部文本框,重新定位
for _i, _tr in enumerate(_top_list):
if _i == 0:
_position = (
self.titleTr.point_top()[0] - _tr.rect_width - 15,
self.titleTr.point_top()[1] - self.vhSpace - _tr.rect_height - 15
)
else:
_position = (
self.titleTr.point_top()[0] + 15,
self.titleTr.point_top()[1] - self.vhSpace - _tr.rect_height - 15
)
_tr.reposition(_position)
# 遍历所有底部文本框,重新定位
for _i, _tr in enumerate(_btm_list):
if _i == 0:
_position = (
self.titleTr.point_bottom()[0] - _tr.rect_width - 15,
self.titleTr.point_bottom()[1] + self.vhSpace + _tr.rect_height + 15
)
else:
_position = (
self.titleTr.point_bottom()[0] + 15,
self.titleTr.point_bottom()[1] + self.vhSpace + _tr.rect_height + 15
)
_tr.reposition(_position)
_top = self.titleTr.point_top()[1] - self.vhSpace
# 遍历所有左则文本框,重新定位
for _tr in _lft_list:
_w = _tr.rect_width
_h = _tr.rect_height
_space = self.titleTr.point_bottom()[1] - self.titleTr.point_top()[1] + self.vhSpace * 2 + _h
_margin = 0
if len(_lft_list) > 1:
_margin = (_space - len(_lft_list) * _h) / (len(_lft_list) - 1)
_left = self.titleTr.point_left()[0] - _w - self.lrSpace
_position = (_left, _top)
_tr.reposition(_position)
if _tr.point_left()[0] < 20:
_left = 20
_position = (_left, _top)
_tr.reposition(_position)
_top += _h + _margin
_top = self.titleTr.point_top()[1] - self.vhSpace
# 遍历所有右侧文本框,重新定位
for _tr in _rit_list:
_w = _tr.rect_width
_h = _tr.rect_height
_space = self.titleTr.point_bottom()[1] - self.titleTr.point_top()[1] + self.vhSpace * 2 + _h
_margin = 0
if len(_rit_list) > 1:
_margin = (_space - len(_rit_list) * _h) / (len(_rit_list) - 1)
_left = self.titleTr.point_right()[0] + self.lrSpace
_position = (_left, _top)
_tr.reposition(_position)
if _tr.point_right()[0] > self.width - 20:
_left = self.width - _tr.rect_width - 20
_position = (_left, _top)
_tr.reposition(_position)
_top += _h + _margin
for _tr in _tr_list:
self.drawing.add(self.titleTr.connect(_tr, **self.pathExtra))
for _tr in _tr_list:
self.drawing.add(_tr)
class EnterpriseGraph:
"""
SVG 企业汇总信息图。
根据 title 名称和 row_list 列表数据输出 svg 格式的关系图谱。
"""
def __init__(self, filename: str = 'noname.svg'):
self.filename = filename
self.width = 800
"""
画布宽度。
"""
self.height = 300
"""
画布高度。
"""
self.vhSpace = 50
"""
内容垂直浮动空间。
"""
self.lrSpace = 100
"""
内容左右留白空间。
"""
self.titleTextExtra = {
'font-size': '16px', 'fill': 'rgb(255, 255, 255)'
}
"""
标题文本样式。
"""
self.titleRectExtra = {
'rx': 10, 'ry': 10, 'fill': 'rgb(233, 72, 41)', 'fill-opacity': 1, 'stroke': 'rgb(233, 72, 41)'
}
"""
标题外框样式
"""
self.textExtra = {
'font-size': '14px', 'fill': 'rgb(255, 255, 255)'
}
"""
普通文本样式。
"""
self.rectExtra = {
'rx': 10, 'ry': 10, 'fill': 'rgb(65, 130, 164)', 'fill-opacity': 1, 'stroke': 'rgb(65, 130, 164)'
}
"""
普通文本外框样式。
"""
self.pathExtra = {
'fill': 'none', 'stroke': 'rgb(65, 130, 164)'
}
"""
连线样式。
"""
self.drawing = svgwrite.Drawing(filename=self.filename)
"""
主绘图对象。
"""
self.attribs = self.drawing.attribs
"""
图像样式。
"""
self.save = self.drawing.save
"""
保存文件方法。
"""
self.attribs.update({
'width': self.width, 'height': self.height
})
self.titleTr: Optional[TextRect] = None
"""
标题文本框对象。
"""
def draw(self, title: str, data_item: dict):
"""
绘制图形。
:param data_item: 数据项字典,中文名称:数据值
:param title: 标题文本
:return: 自身对象
"""
# 重定设图像参数
self.attribs.update(self.attribs)
# 创建标题文本框
self.titleTr = TextRect(
text=title, insert=(0, 0), text_extra=self.titleTextExtra, rect_extra=self.titleRectExtra, **{
'debug': False
}
)
self.titleTr.reposition((
(self.width - self.titleTr.rect_width) * 0.5, (self.height - self.titleTr.rect_height) * 0.5 - 20
))
self.drawing.add(self.titleTr)
_tr_list: list[TextRect] = []
for _key, _val in data_item.items():
# 遍历数据,初始创建所有的文本框,得到文本框尺寸信息
# 同时保留所有需要输出的数据
_text = f"{_key}{_val}"
_tr = TextRect(
text=_text, insert=(0, 0), rect_extra=self.rectExtra, **self.textExtra, **{
'debug': False,
}
)
_tr_list.append(_tr)
_harf = int(len(_tr_list) / 2) if int(len(_tr_list) % 2) == 0 else int(len(_tr_list) / 2) + 1
_top_list = []
_lft_list = _tr_list[:_harf]
_rit_list = _tr_list[_harf:]
_btm_list = []
if len(_tr_list) >= 12:
_top_list = _lft_list[:2]
_lft_list = _lft_list[2:]
if len(_tr_list) >= 14:
_btm_list = _rit_list[-2:]
_rit_list = _rit_list[:-2]
# 遍历所有顶部文本框,重新定位
for _key, _tr in enumerate(_top_list):
if _key == 0:
_position = (
self.titleTr.point_top()[0] - _tr.rect_width - 15,
self.titleTr.point_top()[1] - self.vhSpace - _tr.rect_height - 15
)
else:
_position = (
self.titleTr.point_top()[0] + 15,
self.titleTr.point_top()[1] - self.vhSpace - _tr.rect_height - 15
)
_tr.reposition(_position)
# 遍历所有底部文本框,重新定位
for _key, _tr in enumerate(_btm_list):
if _key == 0:
_position = (
self.titleTr.point_bottom()[0] - _tr.rect_width - 15,
self.titleTr.point_bottom()[1] + self.vhSpace + _tr.rect_height + 15
)
else:
_position = (
self.titleTr.point_bottom()[0] + 15,
self.titleTr.point_bottom()[1] + self.vhSpace + _tr.rect_height + 15
)
_tr.reposition(_position)
_top = self.titleTr.point_top()[1] - self.vhSpace
# 遍历所有左则文本框,重新定位
for _tr in _lft_list:
_w = _tr.rect_width
_h = _tr.rect_height
_space = self.titleTr.point_bottom()[1] - self.titleTr.point_top()[1] + self.vhSpace * 2 + _h
_margin = 0
if len(_lft_list) > 1:
_margin = (_space - len(_lft_list) * _h) / (len(_lft_list) - 1)
_left = self.titleTr.point_left()[0] - _w - self.lrSpace
_position = (_left, _top)
_tr.reposition(_position)
if _tr.point_left()[0] < 20:
_left = 20
_position = (_left, _top)
_tr.reposition(_position)
_top += _h + _margin
_top = self.titleTr.point_top()[1] - self.vhSpace
# 遍历所有右侧文本框,重新定位
for _tr in _rit_list:
_w = _tr.rect_width
_h = _tr.rect_height
_space = self.titleTr.point_bottom()[1] - self.titleTr.point_top()[1] + self.vhSpace * 2 + _h
_margin = 0
if len(_rit_list) > 1:
_margin = (_space - len(_rit_list) * _h) / (len(_rit_list) - 1)
_left = self.titleTr.point_right()[0] + self.lrSpace
_position = (_left, _top)
_tr.reposition(_position)
if _tr.point_right()[0] > self.width - 20:
_left = self.width - _tr.rect_width - 20
_position = (_left, _top)
_tr.reposition(_position)
_top += _h + _margin
for _tr in _tr_list:
self.drawing.add(self.titleTr.connect(_tr, **self.pathExtra))
for _tr in _tr_list:
self.drawing.add(_tr)