2018-12-28 10:48:24 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2021-07-13 14:52:26 +08:00
|
|
|
|
"""
|
2018-12-28 10:48:24 +08:00
|
|
|
|
Created on 2017年12月28日
|
2021-07-13 14:52:26 +08:00
|
|
|
|
@author: Irony
|
|
|
|
|
@site: https://pyqt.site , https://github.com/PyQt5
|
2018-12-28 10:48:24 +08:00
|
|
|
|
@email: 892768447@qq.com
|
|
|
|
|
@file: charts.bar.BarStack
|
|
|
|
|
@description: like http://echarts.baidu.com/demo.html#bar-stack
|
2021-07-13 14:52:26 +08:00
|
|
|
|
"""
|
2018-12-28 10:48:24 +08:00
|
|
|
|
|
|
|
|
|
import sys
|
2019-10-04 10:18:34 +08:00
|
|
|
|
from random import randint
|
2018-12-28 10:48:24 +08:00
|
|
|
|
|
2021-07-13 14:52:26 +08:00
|
|
|
|
try:
|
|
|
|
|
from PyQt5.QtChart import QChartView, QChart, QBarSeries, QBarSet, QBarCategoryAxis
|
|
|
|
|
from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint
|
|
|
|
|
from PyQt5.QtGui import QPainter, QPen
|
|
|
|
|
from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \
|
|
|
|
|
QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget
|
|
|
|
|
except ImportError:
|
|
|
|
|
from PySide2.QtCore import Qt, QPointF, QRectF, QPoint
|
|
|
|
|
from PySide2.QtGui import QPainter, QPen
|
|
|
|
|
from PySide2.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \
|
|
|
|
|
QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget
|
|
|
|
|
from PySide2.QtCharts import QtCharts
|
|
|
|
|
|
|
|
|
|
QChartView = QtCharts.QChartView
|
|
|
|
|
QChart = QtCharts.QChart
|
|
|
|
|
QBarSeries = QtCharts.QBarSeries
|
|
|
|
|
QBarSet = QtCharts.QBarSet
|
|
|
|
|
QBarCategoryAxis = QtCharts.QBarCategoryAxis
|
2018-12-28 10:48:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToolTipItem(QWidget):
|
|
|
|
|
|
|
|
|
|
def __init__(self, color, text, parent=None):
|
|
|
|
|
super(ToolTipItem, self).__init__(parent)
|
|
|
|
|
layout = QHBoxLayout(self)
|
|
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
clabel = QLabel(self)
|
|
|
|
|
clabel.setMinimumSize(12, 12)
|
|
|
|
|
clabel.setMaximumSize(12, 12)
|
|
|
|
|
clabel.setStyleSheet("border-radius:6px;background: rgba(%s,%s,%s,%s);" % (
|
|
|
|
|
color.red(), color.green(), color.blue(), color.alpha()))
|
|
|
|
|
layout.addWidget(clabel)
|
|
|
|
|
self.textLabel = QLabel(text, self, styleSheet="color:white;")
|
|
|
|
|
layout.addWidget(self.textLabel)
|
|
|
|
|
|
|
|
|
|
def setText(self, text):
|
|
|
|
|
self.textLabel.setText(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToolTipWidget(QWidget):
|
|
|
|
|
Cache = {}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super(ToolTipWidget, self).__init__(*args, **kwargs)
|
|
|
|
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
|
|
|
self.setStyleSheet(
|
|
|
|
|
"ToolTipWidget{background: rgba(50, 50, 50, 100);}")
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
self.titleLabel = QLabel(self, styleSheet="color:white;")
|
|
|
|
|
layout.addWidget(self.titleLabel)
|
|
|
|
|
|
|
|
|
|
def updateUi(self, title, bars):
|
|
|
|
|
self.titleLabel.setText(title)
|
|
|
|
|
for bar, value in bars:
|
|
|
|
|
if bar not in self.Cache:
|
|
|
|
|
item = ToolTipItem(
|
|
|
|
|
bar.color(),
|
|
|
|
|
(bar.label() or "-") + ":" + str(value), self)
|
|
|
|
|
self.layout().addWidget(item)
|
|
|
|
|
self.Cache[bar] = item
|
|
|
|
|
else:
|
|
|
|
|
self.Cache[bar].setText(
|
|
|
|
|
(bar.label() or "-") + ":" + str(value))
|
|
|
|
|
brush = bar.brush()
|
|
|
|
|
color = brush.color()
|
|
|
|
|
self.Cache[bar].setVisible(color.alphaF() == 1.0) # 隐藏那些不可用的项
|
|
|
|
|
self.adjustSize() # 调整大小
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GraphicsProxyWidget(QGraphicsProxyWidget):
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super(GraphicsProxyWidget, self).__init__(*args, **kwargs)
|
|
|
|
|
self.setZValue(999)
|
|
|
|
|
self.tipWidget = ToolTipWidget()
|
|
|
|
|
self.setWidget(self.tipWidget)
|
|
|
|
|
self.hide()
|
|
|
|
|
|
|
|
|
|
def width(self):
|
|
|
|
|
return self.size().width()
|
|
|
|
|
|
|
|
|
|
def height(self):
|
|
|
|
|
return self.size().height()
|
|
|
|
|
|
|
|
|
|
def show(self, title, bars, pos):
|
|
|
|
|
self.setGeometry(QRectF(pos, self.size()))
|
|
|
|
|
self.tipWidget.updateUi(title, bars)
|
|
|
|
|
super(GraphicsProxyWidget, self).show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChartView(QChartView):
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super(ChartView, self).__init__(*args, **kwargs)
|
|
|
|
|
self.resize(800, 600)
|
|
|
|
|
self.setRenderHint(QPainter.Antialiasing) # 抗锯齿
|
|
|
|
|
self.initChart()
|
|
|
|
|
|
|
|
|
|
# 提示widget
|
|
|
|
|
self.toolTipWidget = GraphicsProxyWidget(self._chart)
|
|
|
|
|
|
|
|
|
|
# line 宽度需要调整
|
|
|
|
|
self.lineItem = QGraphicsLineItem(self._chart)
|
|
|
|
|
pen = QPen(Qt.gray)
|
|
|
|
|
self.lineItem.setPen(pen)
|
|
|
|
|
self.lineItem.setZValue(998)
|
|
|
|
|
self.lineItem.hide()
|
|
|
|
|
|
|
|
|
|
# 一些固定计算,减少mouseMoveEvent中的计算量
|
|
|
|
|
# 获取x和y轴的最小最大值
|
|
|
|
|
axisX, axisY = self._chart.axisX(), self._chart.axisY()
|
|
|
|
|
self.category_len = len(axisX.categories())
|
|
|
|
|
self.min_x, self.max_x = -0.5, self.category_len - 0.5
|
|
|
|
|
self.min_y, self.max_y = axisY.min(), axisY.max()
|
|
|
|
|
# 坐标系中左上角顶点
|
|
|
|
|
self.point_top = self._chart.mapToPosition(
|
|
|
|
|
QPointF(self.min_x, self.max_y))
|
|
|
|
|
|
|
|
|
|
def mouseMoveEvent(self, event):
|
|
|
|
|
super(ChartView, self).mouseMoveEvent(event)
|
|
|
|
|
pos = event.pos()
|
|
|
|
|
# 把鼠标位置所在点转换为对应的xy值
|
|
|
|
|
x = self._chart.mapToValue(pos).x()
|
|
|
|
|
y = self._chart.mapToValue(pos).y()
|
|
|
|
|
index = round(x)
|
|
|
|
|
# 得到在坐标系中的所有bar的类型和点
|
|
|
|
|
serie = self._chart.series()[0]
|
|
|
|
|
bars = [(bar, bar.at(index))
|
|
|
|
|
for bar in serie.barSets() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y]
|
2021-07-13 14:52:26 +08:00
|
|
|
|
# print(bars)
|
2018-12-28 10:48:24 +08:00
|
|
|
|
if bars:
|
|
|
|
|
right_top = self._chart.mapToPosition(
|
|
|
|
|
QPointF(self.max_x, self.max_y))
|
|
|
|
|
# 等分距离比例
|
|
|
|
|
step_x = round(
|
|
|
|
|
(right_top.x() - self.point_top.x()) / self.category_len)
|
|
|
|
|
posx = self._chart.mapToPosition(QPointF(x, self.min_y))
|
|
|
|
|
self.lineItem.setLine(posx.x(), self.point_top.y(),
|
|
|
|
|
posx.x(), posx.y())
|
|
|
|
|
self.lineItem.show()
|
|
|
|
|
try:
|
|
|
|
|
title = self.categories[index]
|
|
|
|
|
except:
|
|
|
|
|
title = ""
|
|
|
|
|
t_width = self.toolTipWidget.width()
|
|
|
|
|
t_height = self.toolTipWidget.height()
|
|
|
|
|
# 如果鼠标位置离右侧的距离小于tip宽度
|
|
|
|
|
x = pos.x() - t_width if self.width() - \
|
2021-07-13 14:52:26 +08:00
|
|
|
|
pos.x() - 20 < t_width else pos.x()
|
2018-12-28 10:48:24 +08:00
|
|
|
|
# 如果鼠标位置离底部的高度小于tip高度
|
|
|
|
|
y = pos.y() - t_height if self.height() - \
|
2021-07-13 14:52:26 +08:00
|
|
|
|
pos.y() - 20 < t_height else pos.y()
|
2018-12-28 10:48:24 +08:00
|
|
|
|
self.toolTipWidget.show(
|
|
|
|
|
title, bars, QPoint(x, y))
|
|
|
|
|
else:
|
|
|
|
|
self.toolTipWidget.hide()
|
|
|
|
|
self.lineItem.hide()
|
|
|
|
|
|
|
|
|
|
def handleMarkerClicked(self):
|
|
|
|
|
marker = self.sender() # 信号发送者
|
|
|
|
|
if not marker:
|
|
|
|
|
return
|
|
|
|
|
bar = marker.barset()
|
|
|
|
|
if not bar:
|
|
|
|
|
return
|
|
|
|
|
# bar透明度
|
|
|
|
|
brush = bar.brush()
|
|
|
|
|
color = brush.color()
|
|
|
|
|
alpha = 0.0 if color.alphaF() == 1.0 else 1.0
|
|
|
|
|
color.setAlphaF(alpha)
|
|
|
|
|
brush.setColor(color)
|
|
|
|
|
bar.setBrush(brush)
|
|
|
|
|
# marker
|
|
|
|
|
brush = marker.labelBrush()
|
|
|
|
|
color = brush.color()
|
|
|
|
|
alpha = 0.4 if color.alphaF() == 1.0 else 1.0
|
|
|
|
|
# 设置label的透明度
|
|
|
|
|
color.setAlphaF(alpha)
|
|
|
|
|
brush.setColor(color)
|
|
|
|
|
marker.setLabelBrush(brush)
|
|
|
|
|
# 设置marker的透明度
|
|
|
|
|
brush = marker.brush()
|
|
|
|
|
color = brush.color()
|
|
|
|
|
color.setAlphaF(alpha)
|
|
|
|
|
brush.setColor(color)
|
|
|
|
|
marker.setBrush(brush)
|
|
|
|
|
|
|
|
|
|
def handleMarkerHovered(self, status):
|
|
|
|
|
# 设置bar的画笔宽度
|
|
|
|
|
marker = self.sender() # 信号发送者
|
|
|
|
|
if not marker:
|
|
|
|
|
return
|
|
|
|
|
bar = marker.barset()
|
|
|
|
|
if not bar:
|
|
|
|
|
return
|
|
|
|
|
pen = bar.pen()
|
|
|
|
|
if not pen:
|
|
|
|
|
return
|
|
|
|
|
pen.setWidth(pen.width() + (1 if status else -1))
|
|
|
|
|
bar.setPen(pen)
|
|
|
|
|
|
|
|
|
|
def handleBarHoverd(self, status, index):
|
|
|
|
|
# 设置bar的画笔宽度
|
|
|
|
|
bar = self.sender() # 信号发送者
|
|
|
|
|
pen = bar.pen()
|
|
|
|
|
if not pen:
|
|
|
|
|
return
|
|
|
|
|
pen.setWidth(pen.width() + (1 if status else -1))
|
|
|
|
|
bar.setPen(pen)
|
|
|
|
|
|
|
|
|
|
def initChart(self):
|
|
|
|
|
self._chart = QChart(title="柱状图堆叠")
|
|
|
|
|
self._chart.setAcceptHoverEvents(True)
|
|
|
|
|
# Series动画
|
|
|
|
|
self._chart.setAnimationOptions(QChart.SeriesAnimations)
|
|
|
|
|
self.categories = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
|
|
|
|
names = ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"]
|
|
|
|
|
series = QBarSeries(self._chart)
|
|
|
|
|
for name in names:
|
|
|
|
|
bar = QBarSet(name)
|
|
|
|
|
# 随机数据
|
|
|
|
|
for _ in range(7):
|
|
|
|
|
bar.append(randint(0, 10))
|
|
|
|
|
series.append(bar)
|
|
|
|
|
bar.hovered.connect(self.handleBarHoverd) # 鼠标悬停
|
|
|
|
|
self._chart.addSeries(series)
|
|
|
|
|
self._chart.createDefaultAxes() # 创建默认的轴
|
|
|
|
|
# x轴
|
|
|
|
|
axis_x = QBarCategoryAxis(self._chart)
|
|
|
|
|
axis_x.append(self.categories)
|
|
|
|
|
self._chart.setAxisX(axis_x, series)
|
|
|
|
|
# chart的图例
|
|
|
|
|
legend = self._chart.legend()
|
|
|
|
|
legend.setVisible(True)
|
|
|
|
|
# 遍历图例上的标记并绑定信号
|
|
|
|
|
for marker in legend.markers():
|
|
|
|
|
# 点击事件
|
|
|
|
|
marker.clicked.connect(self.handleMarkerClicked)
|
|
|
|
|
# 鼠标悬停事件
|
|
|
|
|
marker.hovered.connect(self.handleMarkerHovered)
|
|
|
|
|
self.setChart(self._chart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
view = ChartView()
|
|
|
|
|
view.show()
|
|
|
|
|
sys.exit(app.exec_())
|