'update'
This commit is contained in:
282
charts.py
Normal file
282
charts.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from PyQt6.QtWidgets import QWidget, QLabel, QSizePolicy
|
||||
from PyQt6.QtGui import QPainter, QColor, QPen, QPainterPath, QLinearGradient
|
||||
from PyQt6.QtCore import Qt, QRectF, QPointF
|
||||
import math
|
||||
|
||||
# 统一底色与圆角
|
||||
BACKGROUND_COLOR = QColor(245, 246, 250)
|
||||
PANEL_BORDER_COLOR = QColor(225, 228, 236)
|
||||
CORNER_RADIUS = 10
|
||||
|
||||
def map_ratio_to_color(r: float) -> QColor:
|
||||
hue_stops = [(0.0,0.58),(0.25,0.64),(0.50,0.70),(0.75,0.78),(1.0,0.85)]
|
||||
def lerp(a,b,t): return a + (b-a)*t
|
||||
if r <= 0: h = hue_stops[0][1]
|
||||
elif r >= 1: h = hue_stops[-1][1]
|
||||
else:
|
||||
for i in range(1,len(hue_stops)):
|
||||
if r <= hue_stops[i][0]:
|
||||
lpos, lh = hue_stops[i-1]; rpos, rh = hue_stops[i]
|
||||
local_t = 0 if rpos==lpos else (r-lpos)/(rpos-lpos)
|
||||
h = lerp(lh,rh,local_t); break
|
||||
sat = 0.35 + 0.20 * r
|
||||
valb = 0.75 + 0.15 * (1 - abs(r-0.5)*2)
|
||||
return QColor.fromHsvF(h, sat, valb)
|
||||
|
||||
class BarChartWidget(QWidget):
|
||||
def __init__(self, data, title='', unit='分钟', labels=None, value_format=None, label_rotation: int = 0, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data or []
|
||||
self.title = title
|
||||
self.unit = unit
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.value_format = value_format
|
||||
self.label_rotation = label_rotation # 0=不旋转, 45=倾斜
|
||||
self.setMinimumHeight(180)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 260)
|
||||
|
||||
def setData(self, data, labels=None):
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1))
|
||||
p.setBrush(BACKGROUND_COLOR)
|
||||
# 背景圆角
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
if not self.data:
|
||||
p.setPen(QColor('#666666'))
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据')
|
||||
p.end(); return
|
||||
max_val = max(self.data) or 1
|
||||
title_left, title_top = 12, 6
|
||||
margin_top = 28
|
||||
margin_bottom = 28 if self.label_rotation == 0 else 50
|
||||
margin_left, margin_right = 40, 10
|
||||
title_height = 18
|
||||
w, h = rect.width(), rect.height()
|
||||
chart_rect = QRectF(margin_left, margin_top, w - margin_left - margin_right, h - margin_top - margin_bottom)
|
||||
bar_count = len(self.data)
|
||||
bar_space = chart_rect.width() / bar_count
|
||||
bar_width = bar_space * 0.55
|
||||
# 标题
|
||||
p.setPen(Qt.GlobalColor.black)
|
||||
font = p.font(); font.setPointSize(10); font.setBold(True); p.setFont(font)
|
||||
p.drawText(QRectF(title_left, title_top, chart_rect.width(), title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.title)
|
||||
# 网格
|
||||
font.setPointSize(8); font.setBold(False); p.setFont(font)
|
||||
pen_grid = QPen(QColor('#dddddd')); pen_grid.setStyle(Qt.PenStyle.DotLine)
|
||||
p.setPen(pen_grid)
|
||||
grid_lines = 4
|
||||
for i in range(grid_lines+1):
|
||||
y = chart_rect.top() + chart_rect.height() * (1 - i / grid_lines)
|
||||
p.drawLine(QPointF(margin_left, y), QPointF(margin_left + chart_rect.width(), y))
|
||||
val = max_val * i / grid_lines
|
||||
p.setPen(QColor('#555555'))
|
||||
label = f'{val:.0f}' if max_val >= 10 else f'{val:.1f}'
|
||||
p.drawText(QRectF(2, y-8, margin_left-4, 14), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, label)
|
||||
p.setPen(pen_grid)
|
||||
# 单位
|
||||
p.setPen(QColor('#333333'))
|
||||
p.drawText(2, margin_top-4, margin_left-4, 14, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom, self.unit)
|
||||
# 柱子
|
||||
for idx, v in enumerate(self.data):
|
||||
x_center = chart_rect.left() + idx * bar_space + bar_space/2
|
||||
bar_h = 0 if max_val == 0 else chart_rect.height() * (v / max_val)
|
||||
bar_rect = QRectF(x_center - bar_width/2, chart_rect.bottom()-bar_h, bar_width, bar_h)
|
||||
ratio = 0.0 if max_val == 0 else max(0.0, min(1.0, v / max_val))
|
||||
color = map_ratio_to_color(ratio)
|
||||
p.setPen(Qt.PenStyle.NoPen); p.setBrush(color)
|
||||
p.drawRoundedRect(bar_rect, 2, 2)
|
||||
if v > 0:
|
||||
p.setPen(QColor('#222222'))
|
||||
sf = p.font(); sf.setPointSize(7); p.setFont(sf)
|
||||
val_str = self.value_format(v) if self.value_format else (f'{v:.0f}' if v >= 10 else f'{v:.1f}')
|
||||
p.drawText(QRectF(bar_rect.left()-2, bar_rect.top()-14, bar_rect.width()+4, 12), Qt.AlignmentFlag.AlignCenter, val_str)
|
||||
# x 标签
|
||||
p.setPen(QColor('#333333'))
|
||||
lf = p.font(); lf.setPointSize(8); p.setFont(lf)
|
||||
for idx, text in enumerate(self.labels):
|
||||
x_center = chart_rect.left() + idx * bar_space + bar_space/2
|
||||
if self.label_rotation:
|
||||
p.save()
|
||||
p.translate(x_center, chart_rect.bottom()+4)
|
||||
p.rotate(-45)
|
||||
# 旋转后使用固定宽度避免重叠 (40px)
|
||||
p.drawText(QRectF(-20, 0, 40, 14), Qt.AlignmentFlag.AlignCenter, text)
|
||||
p.restore()
|
||||
else:
|
||||
p.drawText(QRectF(x_center - bar_space/2, chart_rect.bottom()+2, bar_space, 14), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, text)
|
||||
p.end()
|
||||
|
||||
class BubbleMetricsWidget(QWidget):
|
||||
def __init__(self, metrics, parent=None):
|
||||
super().__init__(parent)
|
||||
self.metrics = metrics
|
||||
self.setMinimumHeight(240)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 300)
|
||||
|
||||
def setMetrics(self, metrics):
|
||||
self.metrics = metrics; self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1)); p.setBrush(BACKGROUND_COLOR)
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
title_left, title_top, title_height = 12, 6, 20
|
||||
margin = 2
|
||||
content_rect = QRectF(rect.left()+margin, rect.top()+title_height+margin, rect.width()-2*margin, rect.height()-title_height-2*margin)
|
||||
if not self.metrics:
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据'); p.end(); return
|
||||
# 归一化
|
||||
minute_values = [(v*60 if u=='h' else v) for (lbl,v,u,c) in self.metrics]
|
||||
max_minutes = max(minute_values) if minute_values else 1
|
||||
radii_norm = [0.3 + 0.7 * math.sqrt((mv/max_minutes) if max_minutes>0 else 0) for mv in minute_values]
|
||||
default_pos = { '全年':(0.20,0.50), '月均':(0.50,0.18), '近7天':(0.80,0.50), '日均':(0.50,0.82) }
|
||||
if any(lbl not in default_pos for (lbl,*_) in self.metrics):
|
||||
step = 1.0/(len(self.metrics)+1)
|
||||
default_pos = {m[0]:(step*(i+1),0.5) for i,m in enumerate(self.metrics)}
|
||||
W,H = content_rect.width(), content_rect.height(); base_len = min(W,H)
|
||||
centers = [(content_rect.left()+default_pos.get(lbl,(0.5,0.5))[0]*W,
|
||||
content_rect.top()+default_pos.get(lbl,(0.5,0.5))[1]*H) for (lbl,*_) in self.metrics]
|
||||
constraints=[]
|
||||
for (cx,cy),r in zip(centers,radii_norm):
|
||||
dist=min(cx-content_rect.left(), content_rect.right()-cx, cy-content_rect.top(), content_rect.bottom()-cy)
|
||||
if r>0: constraints.append(dist/(r*base_len))
|
||||
for i in range(len(centers)):
|
||||
for j in range(i+1,len(centers)):
|
||||
dx = centers[i][0]-centers[j][0]; dy = centers[i][1]-centers[j][1]
|
||||
d = math.hypot(dx,dy)
|
||||
if d>0:
|
||||
gap=6; constraints.append((d-gap)/((radii_norm[i]+radii_norm[j])*base_len))
|
||||
S = max(0.05, min(constraints) if constraints else 0.5)
|
||||
for (label,val,unit,color), r_norm, (cx,cy) in zip(self.metrics, radii_norm, centers):
|
||||
R = r_norm * S * base_len
|
||||
circle_rect = QRectF(cx-R, cy-R, 2*R, 2*R)
|
||||
p.setPen(QPen(QColor('#ffffff'),2)); p.setBrush(QColor(color)); p.drawEllipse(circle_rect)
|
||||
p.setPen(QColor('#ffffff'))
|
||||
f = p.font(); f.setBold(True); f.setPointSize(max(9,int(R/5))); p.setFont(f)
|
||||
txt = f"{val:.0f} 小时" if unit=='h' and val>=10 else (f"{val:.1f} 小时" if unit=='h' else f"{val:.0f} 分钟")
|
||||
p.drawText(circle_rect, Qt.AlignmentFlag.AlignCenter, f"{txt}\n{label}")
|
||||
# 标题(为空保留对齐占位)
|
||||
p.setPen(QColor('#222222'))
|
||||
f = p.font(); f.setBold(True); f.setPointSize(10); p.setFont(f)
|
||||
p.drawText(QRectF(title_left, title_top, rect.width()-title_left-8, title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, '')
|
||||
p.end()
|
||||
|
||||
class ScatterChartWidget(QWidget):
|
||||
def __init__(self, data, title='', unit='小时', labels=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.title = title; self.unit = unit
|
||||
self.setMinimumHeight(180)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 260)
|
||||
|
||||
def setData(self, data, labels=None):
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1)); p.setBrush(BACKGROUND_COLOR)
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
if not self.data:
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据'); p.end(); return
|
||||
title_left,title_top = 12,6
|
||||
margin_top,margin_bottom = 28,32
|
||||
margin_left,margin_right = 40,10
|
||||
title_height = 18
|
||||
w,h = rect.width(), rect.height()
|
||||
chart_rect = QRectF(margin_left, margin_top, w - margin_left - margin_right, h - margin_top - margin_bottom)
|
||||
# 标题
|
||||
p.setPen(Qt.GlobalColor.black)
|
||||
font = p.font(); font.setPointSize(10); font.setBold(True); p.setFont(font)
|
||||
p.drawText(QRectF(title_left, title_top, chart_rect.width(), title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.title)
|
||||
# 网格
|
||||
max_val = max(self.data) if self.data else 1
|
||||
if max_val <= 0: max_val = 1
|
||||
grid_lines=4
|
||||
font.setPointSize(8); font.setBold(False); p.setFont(font)
|
||||
pen_grid = QPen(QColor('#dddddd')); pen_grid.setStyle(Qt.PenStyle.DotLine)
|
||||
for i in range(grid_lines+1):
|
||||
y = chart_rect.top() + chart_rect.height()*(1 - i/grid_lines)
|
||||
p.setPen(pen_grid); p.drawLine(QPointF(chart_rect.left(), y), QPointF(chart_rect.right(), y))
|
||||
val = max_val * i / grid_lines
|
||||
p.setPen(QColor('#555555'))
|
||||
label = f'{val:.0f}' if max_val>=10 else f'{val:.1f}'
|
||||
p.drawText(QRectF(2, y-8, margin_left-4, 14), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, label)
|
||||
# 单位
|
||||
p.setPen(QColor('#333333'))
|
||||
p.drawText(2, margin_top-4, margin_left-4, 14, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom, self.unit)
|
||||
# 点
|
||||
count = len(self.data)
|
||||
x_step = chart_rect.width()/(count-1) if count>1 else chart_rect.width()/2
|
||||
points=[]
|
||||
for i,v in enumerate(self.data):
|
||||
x = chart_rect.left()+i*x_step
|
||||
y = chart_rect.bottom() - (v/max_val)*chart_rect.height()
|
||||
ratio = max(0.0,min(1.0,v/max_val))
|
||||
color = map_ratio_to_color(ratio)
|
||||
points.append((x,y,v,color,ratio))
|
||||
# 均值线
|
||||
avg_val = sum(self.data)/len(self.data) if self.data else 0
|
||||
avg_y = chart_rect.bottom() - (avg_val/max_val)*chart_rect.height()
|
||||
pen_avg = QPen(QColor('#aaaadd')); pen_avg.setStyle(Qt.PenStyle.DashLine); pen_avg.setWidth(1)
|
||||
p.setPen(pen_avg); p.drawLine(QPointF(chart_rect.left(), avg_y), QPointF(chart_rect.right(), avg_y))
|
||||
p.setPen(QColor('#666688'))
|
||||
f2 = p.font(); f2.setPointSize(7); p.setFont(f2)
|
||||
p.drawText(QRectF(chart_rect.right()-60, avg_y-10, 58, 12), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, f'均值 {avg_val:.1f}')
|
||||
# 折线+面积
|
||||
if len(points) >=2:
|
||||
pen_line = QPen(QColor('#8890c0')); pen_line.setWidth(1); p.setPen(pen_line)
|
||||
for i in range(len(points)-1):
|
||||
p.drawLine(QPointF(points[i][0], points[i][1]), QPointF(points[i+1][0], points[i+1][1]))
|
||||
grad = QLinearGradient(chart_rect.left(), chart_rect.top(), chart_rect.left(), chart_rect.bottom())
|
||||
grad.setColorAt(0.0, QColor(200,205,235,90)); grad.setColorAt(1.0, QColor(200,205,235,10))
|
||||
path = QPainterPath(QPointF(points[0][0], chart_rect.bottom()))
|
||||
for (x,y, *_ ) in points: path.lineTo(x,y)
|
||||
path.lineTo(points[-1][0], chart_rect.bottom()); path.closeSubpath(); p.fillPath(path, grad)
|
||||
# 极值
|
||||
max_point = max(points, key=lambda t:t[2]) if points else None
|
||||
min_point = min(points, key=lambda t:t[2]) if points else None
|
||||
for (x,y,v,color,ratio) in points:
|
||||
r = 5 + 7*ratio
|
||||
pen = QPen(QColor('#ffffff'),1.2)
|
||||
if max_point and (x,y,v)==max_point[:3]: pen = QPen(QColor('#ff8888'),1.5)
|
||||
elif min_point and (x,y,v)==min_point[:3]: pen = QPen(QColor('#66aac6'),1.5)
|
||||
p.setPen(pen); p.setBrush(color); p.drawEllipse(QPointF(x,y), r, r)
|
||||
label_y_top = y - r - 10; place_above = label_y_top > chart_rect.top()+4
|
||||
txt_rect = QRectF(x-24, (label_y_top if place_above else y + r + 2), 48, 12)
|
||||
p.setPen(QColor('#222222'))
|
||||
sf = p.font(); sf.setPointSize(7); p.setFont(sf)
|
||||
val_str = f'{v:.1f}' if v < 10 else f'{v:.0f}'
|
||||
p.drawText(txt_rect, Qt.AlignmentFlag.AlignCenter, val_str)
|
||||
# x 标签
|
||||
p.setPen(QColor('#333333'))
|
||||
lf = p.font(); lf.setPointSize(8); p.setFont(lf)
|
||||
for i, lab in enumerate(self.labels):
|
||||
x = chart_rect.left() + (i * x_step if count>1 else chart_rect.width()/2)
|
||||
p.drawText(QRectF(x-20, chart_rect.bottom()+2, 40, 14), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, lab)
|
||||
p.end()
|
||||
Reference in New Issue
Block a user