zhenxun_bot/zhenxun/utils/_build_mat.py
HibiKier 99f1388e23
首次启动时提供使用web ui方式完全配置 (#1870)
*  添加全局优先级hook

*  添加基础配置api

*  添加数据库连接测试

* 💬 提示重启

* 🩹 填充过配置时友好提示

* 🐛 首次生成简易配置后自动加载

*  添加配置后重启接口

*  添加重启标志文件

*  添加重启脚本命令

*  添加重启系统限制

*  首次配置判断是否为win系统

* 🔥 移除bat

*  添加关于菜单

*  支持整合包插件安装和添加整合包文档

* 🩹 检测数据库路径

* 🩹 修改数据库路径检测

* 🩹 修改数据库路径检测

* 🩹 修复路径注入

* 🎨 显示添加优先级

* 🐛 修改PriorityLifecycle字典类名称

*  修复路径问题

*  修复路径检测

*  新增路径验证功能,确保用户输入的路径安全并在项目根目录内

*  优化路径验证功能,增加对非法字符和路径长度的检查,确保用户输入的路径更加安全

* 🚨 auto fix by pre-commit hooks

*  优化获取文件列表的代码格式

* 📝 修改README中webui示例图

*  更新PriorityLifecycle.on_startup装饰器

*  简化安装依赖的命令构建逻辑

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-16 09:11:41 +08:00

570 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from io import BytesIO
from pathlib import Path
import random
import sys
from pydantic import BaseModel, Field
from ._build_image import BuildImage
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
class MatType(StrEnum):
LINE = "LINE"
"""折线图"""
BAR = "BAR"
"""柱状图"""
BARH = "BARH"
"""横向柱状图"""
class BuildMatData(BaseModel):
mat_type: MatType
"""类型"""
data: list[int | float] = Field(default_factory=list)
"""数据"""
x_name: str | None = None
"""X轴坐标名称"""
y_name: str | None = None
"""Y轴坐标名称"""
x_index: list[str] = Field(default_factory=list)
"""显示轴坐标值"""
y_index: list[int | float] = Field(default_factory=list)
"""数据轴坐标值"""
space: tuple[int, int] = (20, 20)
"""坐标值间隔(X, Y)"""
rotate: tuple[int, int] = (0, 0)
"""坐标值旋转(X, Y)"""
title: str | None = None
"""标题"""
font: str = "msyh.ttf"
"""字体"""
font_size: int = 15
"""字体大小"""
display_num: bool = True
"""是否在点与柱状图顶部显示数值"""
is_grid: bool = False
"""是否添加栅格"""
background_color: tuple[int, int, int] | str = (255, 255, 255)
"""背景颜色"""
background: Path | bytes | None = None
"""背景图片"""
bar_color: list[str] = Field(default_factory=lambda: ["*"])
"""柱状图柱子颜色, 多个时随机, 使用 * 时七色随机"""
padding: tuple[int, int] = (50, 50)
"""图表上下左右边距"""
class BuildMat:
"""
针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具
目前仅支持 正整数
"""
class InitGraph(BaseModel):
mark_image: BuildImage
"""BuildImage"""
x_height: int
"""横坐标开始高度"""
y_width: int
"""纵坐标开始宽度"""
x_point: list[int]
"""横坐标坐标"""
y_point: list[int]
"""纵坐标坐标"""
graph_height: int
"""坐标轴高度"""
class Config:
arbitrary_types_allowed = True
def __init__(self, mat_type: MatType) -> None:
self.line_length = 760
self._x_padding = 0
self._y_padding = 0
self.build_data = BuildMatData(mat_type=mat_type)
@property
def x_name(self) -> str | None:
return self.build_data.x_name
@x_name.setter
def x_name(self, data: str) -> str | None:
self.build_data.x_name = data
@property
def y_name(self) -> str | None:
return self.build_data.y_name
@y_name.setter
def y_name(self, data: str) -> str | None:
self.build_data.y_name = data
@property
def data(self) -> list[int | float]:
return self.build_data.data
@data.setter
def data(self, data: list[int | float]):
self._check_value(data, self.build_data.y_index)
self.build_data.data = data
@property
def x_index(self) -> list[str]:
return self.build_data.x_index
@x_index.setter
def x_index(self, data: list[str]):
self.build_data.x_index = data
@property
def y_index(self) -> list[int | float]:
return self.build_data.y_index
@y_index.setter
def y_index(self, data: list[int | float]):
# self._check_value(self.build_data.data, data)
data.sort()
self.build_data.y_index = data
@property
def space(self) -> tuple[int, int]:
return self.build_data.space
@space.setter
def space(self, data: tuple[int, int]):
self.build_data.space = data
@property
def rotate(self) -> tuple[int, int]:
return self.build_data.rotate
@rotate.setter
def rotate(self, data: tuple[int, int]):
self.build_data.rotate = data
@property
def title(self) -> str | None:
return self.build_data.title
@title.setter
def title(self, data: str):
self.build_data.title = data
@property
def font(self) -> str:
return self.build_data.font
@font.setter
def font(self, data: str):
self.build_data.font = data
# @property
# def font_size(self) -> int:
# return self.build_data.font_size
# @font_size.setter
# def font_size(self, data: int):
# self.build_data.font_size = data
@property
def display_num(self) -> bool:
return self.build_data.display_num
@display_num.setter
def display_num(self, data: bool):
self.build_data.display_num = data
@property
def is_grid(self) -> bool:
return self.build_data.is_grid
@is_grid.setter
def is_grid(self, data: bool):
self.build_data.is_grid = data
@property
def background_color(self) -> tuple[int, int, int] | str:
return self.build_data.background_color
@background_color.setter
def background_color(self, data: tuple[int, int, int] | str):
self.build_data.background_color = data
@property
def background(self) -> Path | bytes | None:
return self.build_data.background
@background.setter
def background(self, data: Path | bytes):
self.build_data.background = data
@property
def bar_color(self) -> list[str]:
return self.build_data.bar_color
@bar_color.setter
def bar_color(self, data: list[str]):
self.build_data.bar_color = data
def _check_value(
self,
y: list[int | float],
y_index: list[int | float] | None = None,
x_index: list[int | float] | None = None,
):
"""检查值合法性
参数:
y: 坐标值
y_index: y轴坐标值
x_index: x轴坐标值
"""
if y_index:
_value = x_index if self.build_data.mat_type == "barh" else y_index
if not isinstance(y[0], str):
__y = [float(t_y) for t_y in y]
_y_index = [float(t_y) for t_y in y_index]
if max(__y) > max(_y_index):
raise ValueError("坐标点的值必须小于y轴坐标的最大值...")
i = -9999999999
for _y in _y_index:
if _y > i:
i = _y
else:
raise ValueError("y轴坐标值必须有序...")
async def build(self) -> BuildImage:
"""构造图片"""
A = BuildImage(1, 1)
bar_color = self.build_data.bar_color
if "*" in bar_color:
bar_color = [
"#FF0000",
"#FF7F00",
"#FFFF00",
"#00FF00",
"#00FFFF",
"#0000FF",
"#8B00FF",
]
init_graph = await self._init_graph()
mark_image = None
if self.build_data.mat_type == MatType.LINE:
mark_image = await self._build_line_graph(init_graph, bar_color)
if self.build_data.mat_type == MatType.BAR:
mark_image = await self._build_bar_graph(init_graph, bar_color)
if self.build_data.mat_type == MatType.BARH:
mark_image = await self._build_barh_graph(init_graph, bar_color)
if mark_image:
padding_width, padding_height = self.build_data.padding
width = mark_image.width + padding_width
height = mark_image.height + padding_height * 2
if self.build_data.background:
if isinstance(self.build_data.background, bytes):
A = BuildImage(
width, height, background=BytesIO(self.build_data.background)
)
elif isinstance(self.build_data.background, Path):
A = BuildImage(width, height, background=self.build_data.background)
else:
A = BuildImage(width, height, self.build_data.background_color)
if A:
await A.paste(mark_image, (10, padding_height))
if self.build_data.title:
font = BuildImage.load_font(
self.build_data.font, self.build_data.font_size + 7
)
title_width, title_height = BuildImage.get_text_size(
self.build_data.title, font
)
pos = (
int(A.width / 2 - title_width / 2),
int(padding_height / 2 - title_height / 2),
)
await A.text(pos, self.build_data.title)
if self.build_data.x_name:
font = BuildImage.load_font(
self.build_data.font, self.build_data.font_size + 4
)
title_width, title_height = BuildImage.get_text_size(
self.build_data.x_name,
font, # type: ignore
)
pos = (
A.width - title_width - 20,
A.height - int(padding_height / 2 + title_height),
)
await A.text(pos, self.build_data.x_name)
return A
async def _init_graph(self) -> InitGraph:
"""构造初始化图表
返回:
InitGraph: InitGraph
"""
padding_width = 0
padding_height = 0
font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
x_width_list = []
y_height_list = []
for x in self.build_data.x_index:
text_size = BuildImage.get_text_size(x, font)
if text_size[1] > padding_height:
padding_height = text_size[1]
x_width_list.append(text_size)
if not self.build_data.y_index:
"""没有指定y_index时使用data自动生成"""
max_num = max(self.build_data.data)
if max_num < 5:
max_num = 5
s = int(max_num / 5)
_y_index = [max_num]
for _n in range(4):
max_num -= s
_y_index.append(max_num)
_y_index.sort()
# if len(_y_index) > 1:
# if _y_index[0] == _y_index[-1]:
# _tmp = ["_" for _ in range(len(_y_index) - 1)]
# _tmp.append(str(_y_index[0]))
# _y_index = _tmp
self.build_data.y_index = _y_index # type: ignore
for item in self.build_data.y_index:
text_size = BuildImage.get_text_size(str(item), font)
if text_size[0] > padding_width:
padding_width = text_size[0]
y_height_list.append(text_size)
if self.build_data.mat_type == MatType.BARH:
_tmp = x_width_list
x_width_list = y_height_list
y_height_list = _tmp
old_space = self.build_data.space
width = padding_width * 2 + self.build_data.space[0] * 2 + 20
height = (
sum([h[1] + self.build_data.space[1] for h in y_height_list])
+ self.build_data.space[1] * 2
+ 30
)
_x_index = self.build_data.x_index
_y_index = self.build_data.y_index
_barh_max_text_width = 0
if self.build_data.mat_type == MatType.BARH:
"""XY轴下标互换"""
_tmp = _y_index
_y_index = _x_index
_x_index = _tmp
"""额外增加字体宽度"""
for s in self.build_data.x_index:
s_w, s_h = BuildImage.get_text_size(s, font)
if s_w > _barh_max_text_width:
_barh_max_text_width = s_w
width += _barh_max_text_width
width += self.build_data.space[0] * 2 - old_space[0] * 2
"""X轴重新等均分配"""
x_length = width - padding_width * 2 - _barh_max_text_width
x_space = int((x_length - 20) / (len(_x_index) + 1))
if x_space < 50:
"""加大间距更加美观"""
x_space = 50
self.build_data.space = (x_space, self.build_data.space[1])
width += self.build_data.space[0] * (len(_x_index) - 1)
else:
"""非横向柱状图时加字体宽度"""
width += sum([w[0] + self.build_data.space[0] for w in x_width_list])
A = BuildImage(
width + 5,
(height + 10),
# color=(255, 255, 255),
color=(255, 255, 255, 0),
)
padding_height += 5
""""""
await A.line(
(
padding_width + 5 + _barh_max_text_width,
padding_height,
padding_width + 5 + _barh_max_text_width,
height - padding_height,
),
width=2,
)
""""""
await A.line(
(
padding_width + 5 + _barh_max_text_width,
height - padding_height,
width - padding_width + 5,
height - padding_height,
),
width=2,
)
x_cur_width = (
padding_width + _barh_max_text_width + self.build_data.space[0] + 5
)
if self.build_data.mat_type != MatType.BARH:
"""添加字体宽度"""
x_cur_width += x_width_list[0][0]
x_cur_height = height - y_height_list[0][1] - 5
# await A.point((x_cur_width, x_cur_height), (0, 0, 0))
x_point = []
for i, _x in enumerate(_x_index):
"""X轴数值"""
grid_height = x_cur_height
if self.build_data.is_grid:
grid_height = padding_height
await A.line(
(
x_cur_width,
x_cur_height - 1,
x_cur_width,
grid_height - 5,
)
)
x_point.append(x_cur_width - 1)
mid_point = x_cur_width - int(x_width_list[i][0] / 2)
await A.text((mid_point, x_cur_height), str(_x), font=font)
x_cur_width += self.build_data.space[0]
if self.build_data.mat_type != MatType.BARH:
"""添加字体宽度"""
x_cur_width += x_width_list[i][0]
y_cur_width = padding_width + _barh_max_text_width
y_cur_height = height - self.build_data.padding[1] - 9
start_height = y_cur_height
# await A.point((y_cur_width, y_cur_height), (0, 0, 0))
y_point = []
for i, _y in enumerate(_y_index):
"""Y轴数值"""
grid_width = y_cur_width
if self.build_data.is_grid:
grid_width = width - padding_width + 5
y_point.append(y_cur_height)
await A.line((y_cur_width + 5, y_cur_height, grid_width + 11, y_cur_height))
text_width = BuildImage.get_text_size(str(_y), font)[0]
await A.text(
(
y_cur_width - text_width,
y_cur_height - int(y_height_list[i][1] / 2) - 3,
),
str(_y),
font=font,
)
y_cur_height -= y_height_list[i][1] + self.build_data.space[1]
graph_height = 0
if self.build_data.mat_type == MatType.BARH:
graph_height = (
x_cur_width
- self.build_data.space[0]
- _barh_max_text_width
- padding_width
- 5
)
else:
graph_height = start_height - y_cur_height + 7
return self.InitGraph(
mark_image=A,
x_height=height - y_height_list[0][1] - 5,
y_width=padding_width + 5 + _barh_max_text_width,
graph_height=graph_height,
x_point=x_point,
y_point=y_point,
)
async def _build_line_graph(
self, init_graph: InitGraph, bar_color: list[str]
) -> BuildImage:
"""构建折线图
参数:
init_graph: InitGraph
bar_color: 颜色列表
返回:
BuildImage: 折线图
"""
font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
mark_image = init_graph.mark_image
x_height = init_graph.x_height
graph_height = init_graph.graph_height
random_color = random.choice(bar_color)
_black_point = BuildImage(11, 11, color=random_color)
await _black_point.circle()
max_num = max(self.y_index)
point_list = []
for x_p, y in zip(init_graph.x_point, self.build_data.data):
"""折线图标点"""
y_height = int(y / max_num * graph_height)
await mark_image.paste(_black_point, (x_p - 3, x_height - y_height))
point_list.append((x_p + 1, x_height - y_height + 1))
for i in range(len(point_list) - 1):
"""画线"""
a_x, a_y = point_list[i]
b_x, b_y = point_list[i + 1]
await mark_image.line((a_x, a_y, b_x, b_y), random_color)
if self.build_data.display_num:
"""显示数值"""
value = self.build_data.data[i]
text_size = BuildImage.get_text_size(str(value), font)
await mark_image.text(
(a_x - int(text_size[0] / 2), a_y - text_size[1] - 5),
str(value),
font=font,
)
"""最后一个数值显示"""
value = self.build_data.data[-1]
text_size = BuildImage.get_text_size(str(value), font)
await mark_image.text(
(
point_list[-1][0] - int(text_size[0] / 2),
point_list[-1][1] - text_size[1] - 5,
),
str(value),
font=font,
)
return mark_image
async def _build_bar_graph(self, init_graph: InitGraph, bar_color: list[str]):
"""构建折线图
参数:
init_graph: InitGraph
bar_color: 颜色列表
返回:
BuildImage: 折线图
"""
pass
async def _build_barh_graph(self, init_graph: InitGraph, bar_color: list[str]):
"""构建折线图
参数:
init_graph: InitGraph
bar_color: 颜色列表
返回:
BuildImage: 横向柱状图
"""
font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
mark_image = init_graph.mark_image
y_width = init_graph.y_width
graph_height = init_graph.graph_height
random_color = random.choice(bar_color)
max_num = max(self.y_index)
for y_p, y in zip(init_graph.y_point, self.build_data.data):
bar_width = int(y / max_num * graph_height) or 1
bar = BuildImage(bar_width, 18, random_color)
await mark_image.paste(bar, (y_width + 1, y_p - 9))
if self.build_data.display_num:
"""显示数值"""
await mark_image.text(
(y_width + bar_width + 5, y_p - 12), str(y), font=font
)
return mark_image