zhenxun_bot/zhenxun/utils/_build_mat.py
2024-05-18 20:56:23 +08:00

470 lines
15 KiB
Python
Raw 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.

import random
from io import BytesIO
from pathlib import Path
from re import S
from pydantic import BaseModel
from strenum import StrEnum
from ._build_image import BuildImage
class MatType(StrEnum):
LINE = "LINE"
"""折线图"""
BAR = "BAR"
"""柱状图"""
BARH = "BARH"
"""横向柱状图"""
class BuildMatData(BaseModel):
mat_type: MatType
"""类型"""
data: list[int | float] = []
"""数据"""
x_name: str | None = None
"""X轴坐标名称"""
y_name: str | None = None
"""Y轴坐标名称"""
x_index: list[str] = []
"""显示轴坐标值"""
y_index: list[int | float] = []
"""数据轴坐标值"""
space: tuple[int, int] = (15, 15)
"""坐标值间隔(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] = ["*"]
"""柱状图柱子颜色, 多个时随机, 使用 * 时七色随机"""
padding: tuple[int, int] = (50, 50)
"""图表上下左右边距"""
class BuildMat:
"""
针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具
目前仅支持 正整数
"""
class InitGraph(BaseModel):
mark_image: BuildImage
"""BuildImage"""
x_height: int
"""横坐标高度"""
x_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):
"""构造图片"""
A = None
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:
pass
if self.build_data.mat_type == MatType.BARH:
pass
if mark_image:
padding_width, padding_height = self.build_data.padding
width = mark_image.width + padding_width * 2
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, (padding_width, 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)
width_list = []
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]
width_list.append(text_size[0])
if not self.build_data.y_index:
"""没有指定y_index时使用data自动生成"""
max_num = max(self.build_data.data)
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()
self.build_data.y_index = _y_index
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]
height_list.append(text_size[1])
width = (
sum([w + self.build_data.space[0] for w in width_list])
+ height_list[0]
+ self.build_data.space[0] * 2
+ 20
)
height = (
sum([h + self.build_data.space[1] for h in height_list])
+ self.build_data.space[1] * 2
+ 30
)
if self.build_data.mat_type == MatType.BARH:
"""横向柱状图时xy轴长度调换"""
_tmp = height
height = width
width = _tmp
A = BuildImage(
width,
(height + 10),
color=(255, 255, 255, 0),
)
padding_height += 5
await A.line(
(
padding_width + 5,
padding_height,
padding_width + 5,
height - padding_height,
),
width=2,
)
await A.line(
(
padding_width + 5,
height - padding_height,
width - padding_width + 5,
height - padding_height,
),
width=2,
)
_x_index = self.build_data.x_index
_y_index = self.build_data.y_index
if self.build_data.mat_type == MatType.BARH:
_tmp = _y_index
_y_index = _x_index
_x_index = _tmp
cur_width = padding_width + self.build_data.space[0] * 2
cur_height = height - height_list[0] - 5
x_point = []
for i, _x in enumerate(_x_index):
"""X轴数值"""
grid_height = cur_height
if self.build_data.is_grid:
grid_height = padding_height
await A.line((cur_width, cur_height - 1, cur_width, grid_height - 5))
x_point.append(cur_width - 1)
mid_point = cur_width - int(width_list[i] / 2)
await A.text((mid_point, cur_height), str(_x), font=font)
cur_width += width_list[i] + self.build_data.space[0]
cur_width = padding_width
cur_height = height - self.build_data.padding[1]
for i, _y in enumerate(_y_index):
"""Y轴数值"""
grid_width = cur_width
if self.build_data.is_grid:
grid_width = width - padding_width + 5
await A.line((cur_width + 5, cur_height, grid_width + 11, cur_height))
text_width = BuildImage.get_text_size(str(_y), font)[0]
await A.text(
(cur_width - text_width, cur_height - int(height_list[i] / 2) - 3),
str(_y),
font=font,
)
cur_height -= height_list[i] + self.build_data.space[1]
graph_height = height - self.build_data.padding[1] - cur_height + 5
return self.InitGraph(
mark_image=A,
x_height=height - height_list[0] - 5,
graph_height=graph_height,
x_point=x_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 * init_graph.graph_height)
await mark_image.paste(_black_point, (x_p, x_height - y_height))
point_list.append((x_p + 4, x_height - y_height + 4))
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):
pass
async def _build_barh_graph(self):
pass