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] 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() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() 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 = ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"] 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)
class ChartView(QChartView): def __init__(self, *args, **kwargs): super(ChartView, self).__init__(*args, **kwargs) self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 # 自定义x轴label self.category = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] self.initChart() # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) pen.setWidth(1) self.lineItem.setPen(pen) self.lineItem.setZValue(996) self.lineItem.hide() self.lineItemX = QGraphicsLineItem(self._chart) penx = QPen(Qt.gray) penx.setWidth(1) self.lineItemX.setPen(penx) self.lineItemX.setZValue(997) self.lineItemX.hide() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() def resizeEvent(self, event): super(ChartView, self).resizeEvent(event) # 当窗口大小改变时需要重新计算 # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标右上点 self.point_right_top = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) # 坐标右下点 self.point_right_bottom = self._chart.mapToPosition( QPointF(self.max_x, self.min_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / \ (self._chart.axisX().tickCount() - 1) 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 - self.min_x) / self.step_x) # 得到在坐标系中的所有正常显示的series的类型和点 points = [ (serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y ] if points: # 根据鼠标的点获取对应曲线的坐标点, # pos_x = self._chart.mapToPosition( # QPointF(index * self.step_x + self.min_x, self.min_y)) # 设置鼠标的垂直线 self.lineItem.setLine(pos.x(), self.point_top.y(), pos.x(), self.point_bottom.y()) self.lineItem.show() # 设置鼠标的水平线 self.lineItemX.setLine(self.point_top.x(), pos.y(), self.point_right_top.x(), pos.y()) self.lineItemX.show() try: title = self.category[index] except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show(title, points, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() self.lineItemX.hide() def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return visible = not marker.series().isVisible() # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 alpha = 1.0 if visible else 0.4 # 设置label的透明度 brush = marker.labelBrush() color = brush.color() 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) # 设置画笔透明度 pen = marker.pen() color = pen.color() color.setAlphaF(alpha) pen.setColor(color) marker.setPen(pen) def handleMarkerHovered(self, status): # 设置series的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return series = marker.series() if not series: return pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) series.setPen(pen) def handleSeriesHoverd(self, point, state): # 设置series的画笔宽度 series = self.sender() # 信号发送者 pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if state else -1)) series.setPen(pen) def initChart(self): self._chart = QChart(title="折线图堆叠") self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) dataTable = [["邮件营销", [120, 132, 101, 134, 90, 230, 210]], ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], ["视频广告", [150, 232, 201, 154, 190, 330, 410]], ["直接访问", [320, 332, 301, 334, 390, 330, 320]], ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]]] for series_name, data_list in dataTable: series = QLineSeries(self._chart) for j, v in enumerate(data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(7) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(7) # y轴设置7个刻度 axisY.setRange(0, 1500) # 设置y轴范围 # 自定义x轴 axis_x = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) axis_x.setTickCount(7) axis_x.setGridLineVisible(False) min_x = axisX.min() max_x = axisX.max() step = (max_x - min_x) / (7 - 1) # 7个tick for i in range(0, 7): axis_x.append(self.category[i], min_x + i * step) self._chart.setAxisX(axis_x, self._chart.series()[-1]) # self._chart.addAxis(axis_x, Qt.AlignBottom) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)
class ChartView(QChartView): def __init__(self, *args, **kwargs): super(ChartView, self).__init__(*args, **kwargs) self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 self.initChart() self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) self.lineItem.setZValue(998) self.lineItem.hide() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition(QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition(QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / (axisX.tickCount() - 1) # self.step_y = (self.max_y - self.min_y) / (axisY.tickCount() - 1) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(event.pos()).x() y = self._chart.mapToValue(event.pos()).y() index = round((x - self.min_x) / self.step_x) pos_x = self._chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) # print(x, pos_x, index, index * self.step_x + self.min_x) # 得到在坐标系中的所有series的类型和点 points = [(serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: # 永远在轴上的黑线条 self.lineItem.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.show() self.toolTipWidget.show("", points, event.pos() + QPoint(20, 20)) else: self.toolTipWidget.hide() self.lineItem.hide() def onSeriesHoverd(self, point, state): if state: try: name = self.sender().name() except: name = "" QToolTip.showText(QCursor.pos(), "%s\nx: %s\ny: %s" % (name, point.x(), point.y())) def initChart(self): self._chart = QChart(title="Line Chart") self._chart.setAcceptHoverEvents(True) dataTable = [ [120, 132, 101, 134, 90, 230, 210], [220, 182, 191, 234, 290, 330, 310], [150, 232, 201, 154, 190, 330, 410], [320, 332, 301, 334, 390, 330, 320], [820, 932, 901, 934, 1290, 1330, 1320] ] for i, data_list in enumerate(dataTable): series = QLineSeries(self._chart) for j, v in enumerate(data_list): series.append(j, v) series.setName("Series " + str(i)) series.setPointsVisible(True) # 显示原点 series.hovered.connect(self.onSeriesHoverd) self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 self._chart.axisX().setTickCount(7) # x轴设置7个刻度 self._chart.axisY().setTickCount(7) # y轴设置7个刻度 self._chart.axisY().setRange(0, 1500) # 设置y轴范围 self.setChart(self._chart)
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) pen.setWidth(1) self.lineItem.setPen(pen) self.lineItem.setZValue(998) self.lineItem.hide() ''' # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / (axisX.tickCount() - 1) 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 - self.min_x) / self.step_x) # 得到在坐标系中的所有series的类型和点 points = [(serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: pos_x = self._chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) self.lineItem.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.show() try: title = self.category[index] except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show( title, points, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() ''' def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return visible = not marker.series().isVisible() # # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 alpha = 1.0 if visible else 0.4 # 设置label的透明度 brush = marker.labelBrush() color = brush.color() 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) # 设置画笔透明度 pen = marker.pen() color = pen.color() color.setAlphaF(alpha) pen.setColor(color) marker.setPen(pen) def handleMarkerHovered(self, status): # 设置series的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return series = marker.series() if not series: return pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) series.setPen(pen) def handleSeriesHoverd(self, status, index, barset): return print(status, index, barset) # # 设置series的画笔宽度 # series = self.sender() # 信号发送者 # pen = series.pen() # if not pen: # return # pen.setWidth(pen.width() + (1 if state else -1)) # series.setPen(pen) def initChart(self): self._chart = QChart(title="柱状图堆叠") self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) 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) # series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 # x轴 axis_x = QBarCategoryAxis(self._chart) axis_x.append(categories) self._chart.setAxisX(axis_x, series) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)
class IMUChartView(QChartView, BaseGraph): def __init__(self, parent=None): super().__init__(parent=parent) # Render on OpenGL self.setViewport(QOpenGLWidget()) self.xvalues = {} self.chart = QChart() self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) # Cursor (with chart as parent) self.cursor = QGraphicsLineItem(self.chart) self.cursor.setZValue(100.0) # self.scene().addItem(self.cursor) # Selection features # self.setRubberBand(QChartView.HorizontalRubberBand) self.selectionBand = QRubberBand(QRubberBand.Rectangle, self) self.selecting = False self.initialClick = QPoint() # Track mouse self.setMouseTracking(True) self.labelValue = QLabel(self) self.labelValue.setStyleSheet( "background-color: rgba(255,255,255,75%); color: black;") self.labelValue.setAlignment(Qt.AlignCenter) self.labelValue.setMargin(5) self.labelValue.setVisible(False) self.build_style() self.selection_start_time = None self.selection_stop_time = None self.cursor_time = None def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.chart.setTheme(QChart.ChartThemeBlueCerulean) self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') # def closeEvent(self, event): # self.aboutToClose.emit(self) @staticmethod def decimate(xdata, ydata): # assert(len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 # decimate_factor = 1.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) # print('decimate factor', decimate_factor) x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) # for i in range(len(x)): for i, _ in enumerate(x): index = i * decimate_factor # assert(index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min_value, max_value): # print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X axisx = QDateTimeAxis() axisx.setTickCount(5) axisx.setFormat("dd MMM yyyy hh:mm:ss") axisx.setTitleText("Date") self.chart.addAxis(axisx, Qt.AlignBottom) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisx) series.attachAxis(axisY) vect = series.pointsVector() # for i in range(len(vect)): for i, _ in enumerate(vect): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable # axisx.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) # curve.setPointsVisible(True) # curve.setUseOpenGL(True) self.total_samples = max(self.total_samples, len(xdata)) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) # self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) # if len(xdecimated) > 0: # xdecimated = xdecimated - xdecimated[0] xdecimated *= 1000 # No decimal expected points = [] for i, _ in enumerate(xdecimated): # TODO hack # curve.append(QPointF(xdecimated[i], ydecimated[i])) points.append(QPointF(xdecimated[i], ydecimated[i])) curve.replace(points) points.clear() if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) self.xvalues[self.ncurves] = np.array(xdecimated) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def update_data(self, xdata, ydata, series_id): if series_id < len(self.chart.series()): # Find start time to replace data current_series = self.chart.series()[series_id] current_points = current_series.pointsVector() # Find start and end indexes start_index = -1 try: start_index = current_points.index( QPointF(xdata[0] * 1000, ydata[0])) # Right on the value! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x() == xdata[0] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[0] * 1000 < current_points[i].x()): start_index = i # print("update_data: start_index found approximative match.") break end_index = -1 try: end_index = current_points.index( QPointF(xdata[len(xdata) - 1] * 1000, ydata[len(ydata) - 1])) # Right on! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x( ) == xdata[len(xdata) - 1] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[len(xdata) - 1] * 1000 < current_points[i].x()): end_index = i # print("update_data: start_index found approximative match.") break if start_index < 0 or end_index < 0: return # Decimate, if needed xdata, ydata = self.decimate(xdata, ydata) # Check if we have the same number of points for that range. If not, remove and replace! target_points = current_points[start_index:end_index] if len(target_points) != len(xdata): points = [] for i, value in enumerate(xdata): # TODO improve points.append(QPointF(value * 1000, ydata[i])) new_points = current_points[0:start_index] + points[0:len(points)-1] + \ current_points[end_index:len(current_points)-1] current_series.replace(new_points) new_points.clear() # self.xvalues[series_id] = np.array(xdata) return @classmethod def set_title(cls, title): # print('Setting title: ', title) # self.chart.setTitle(title) return @staticmethod def series_to_polyline(xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) # for i in range(0, len(xdata)): # polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) for i, data in enumerate(xdata): polyline[i] = QPointF(data - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title( "Simple example with %d curves of %d points (OpenGL Accelerated Series)" % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): if self.selecting: clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) current_pos = QPoint(clicked_x, clicked_y) # e.pos() self.selection_stop_time = self.chart.mapToValue( QPointF(clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: if current_pos.x() < self.initialClick.x(): start_x = current_pos.x() width = self.initialClick.x() - start_x else: start_x = self.initialClick.x() width = current_pos.x() - self.initialClick.x() self.selectionBand.setGeometry( QRect(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.selection_rec: self.selection_rec.setRect( QRectF(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.interaction_mode == GraphInteractionMode.MOVE: new_pos = current_pos - self.initialClick self.chart.scroll(-new_pos.x(), new_pos.y()) self.initialClick = current_pos def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands # super().mousePressEvent(e) # Verify if click is inside plot area # if self.chart.plotArea().contains(e.pos()): self.selecting = True clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) self.initialClick = QPoint(clicked_x, clicked_y) # e.pos() self.selection_start_time = self.chart.mapToValue(QPointF( clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.setGeometry( QRect(self.initialClick.x(), self.chart.plotArea().y(), 1, self.chart.plotArea().height())) self.selectionBand.show() if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.setOverrideCursor(Qt.ClosedHandCursor) self.labelValue.setVisible(False) def mouseReleaseEvent(self, e: QMouseEvent): # Assure if click is inside plot area # Handling rubberbands (with min / max x) clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.hide() if clicked_x != self.mapToScene(self.initialClick).x(): mapped_x = self.mapToScene(self.initialClick).x() if self.initialClick.x() < clicked_x: self.setSelectionArea(mapped_x, clicked_x, True) else: self.setSelectionArea(clicked_x, mapped_x, True) if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.restoreOverrideCursor() self.selecting = False def clearSelectionArea(self, emit_signal=False): if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = None if emit_signal: self.clearedSelectionArea.emit() def setSelectionArea(self, start_pos, end_pos, emit_signal=False): selection_brush = QBrush(QColor(153, 204, 255, 128)) selection_pen = QPen(Qt.transparent) if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = self.scene().addRect( start_pos, self.chart.plotArea().y(), end_pos - start_pos, self.chart.plotArea().height(), selection_pen, selection_brush) if emit_signal: self.selectedAreaChanged.emit( self.chart.mapToValue(QPointF(start_pos, 0)).x(), self.chart.mapToValue(QPointF(end_pos, 0)).x()) def setSelectionAreaFromTime(self, start_time, end_time, emit_signal=False): # Convert times to x values if isinstance(start_time, datetime.datetime): start_time = start_time.timestamp() * 1000 if isinstance(end_time, datetime.datetime): end_time = end_time.timestamp() * 1000 start_pos = self.chart.mapToPosition(QPointF(start_time, 0)).x() end_pos = self.chart.mapToPosition(QPointF(end_time, 0)).x() self.setSelectionArea(start_pos, end_pos) def setCursorPosition(self, pos, emit_signal=False): self.cursor_time = self.chart.mapToValue(QPointF(pos, 0)).x() # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap_initial = self.chart.mapToValue(QPointF(pos, 0)).x() display = '' # '<i>' + (datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())).strftime('%d-%m-%Y %H:%M:%S') + # '</i><br />' ypos = 10 last_val = None for i in range(self.ncurves): # Find nearest point idx = (np.abs(self.xvalues[i] - xmap_initial)).argmin() ymap = self.chart.series()[i].at(idx).y() xmap = self.chart.series()[i].at(idx).x() if i == 0: display += "<i>" + (datetime.datetime.fromtimestamp(xmap_initial/1000)).strftime('%d-%m-%Y %H:%M:%S:%f') + \ "</i>" # Compute where to display label if last_val is None or ymap > last_val: last_val = ymap ypos = self.chart.mapToPosition(QPointF(xmap, ymap)).y() if display != '': display += '<br />' display += self.chart.series()[i].name( ) + ': <b>' + '%.3f' % ymap + '</b>' self.labelValue.setText(display) self.labelValue.setGeometry(pos, ypos, 100, 100) self.labelValue.adjustSize() self.labelValue.setVisible(True) if emit_signal: self.cursorMoved.emit(xmap_initial) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Find nearest point if isinstance(timestamp, datetime.datetime): timestamp = timestamp.timestamp() pos = self.get_pos_from_time(timestamp) self.setCursorPosition(pos, emit_signal) def get_pos_from_time(self, timestamp): px = timestamp idx1 = (np.abs(self.xvalues[0] - px)).argmin() x1 = self.chart.series()[0].at(idx1).x() pos1 = self.chart.mapToPosition(QPointF(x1, 0)).x() idx2 = idx1 + 1 if idx2 < len(self.chart.series()[0]): x2 = self.chart.series()[0].at(idx2).x() if x2 != x1: pos2 = self.chart.mapToPosition(QPointF(x2, 0)).x() x2 /= 1000 x1 /= 1000 pos = (((px - x1) / (x2 - x1)) * (pos2 - pos1)) + pos1 else: pos = pos1 else: pos = pos1 return pos def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # oldSize = e.oldSize() # newSize = e.size() # Update cursor from time if self.cursor_time: self.setCursorPositionFromTime(self.cursor_time / 1000.0) # Update selection if self.selection_rec: self.setSelectionAreaFromTime(self.selection_start_time, self.selection_stop_time) def zoom_in(self): self.chart.zoomIn() self.update_axes() def zoom_out(self): self.chart.zoomOut() self.update_axes() def zoom_area(self): if self.selection_rec: zoom_rec = self.selection_rec.rect() zoom_rec.setY(0) zoom_rec.setHeight(self.chart.plotArea().height()) self.chart.zoomIn(zoom_rec) self.clearSelectionArea(True) self.update_axes() def zoom_reset(self): self.chart.zoomReset() self.update_axes() def get_displayed_start_time(self): min_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() xmap = self.chart.mapToValue(QPointF(min_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) def get_displayed_end_time(self): max_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() max_x += self.chart.mapToScene( self.chart.plotArea()).boundingRect().width() xmap = self.chart.mapToValue(QPointF(max_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) @property def is_zoomed(self): return self.chart.isZoomed()
class IMUChartView(QChartView): aboutToClose = pyqtSignal(QObject) cursorMoved = pyqtSignal(datetime.datetime) def __init__(self, parent=None): super(QChartView, self).__init__(parent=parent) #self.setFixedHeight(400) #self.setMinimumHeight(500) """self.setMaximumHeight(700) self.setFixedHeight(700) self.setMinimumWidth(1500) self.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed)""" self.reftime = datetime.datetime.now() self.cursor = QGraphicsLineItem() self.scene().addItem(self.cursor) self.decim_factor = 1 # self.setScene(QGraphicsScene()) self.chart = QChart() # self.scene().addItem(self.chart) self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) self.setRubberBand(QChartView.HorizontalRubberBand) # X, Y label on bottom # self.xTextItem = QGraphicsSimpleTextItem(self.chart) # self.xTextItem.setText('X: ') # self.yTextItem = QGraphicsSimpleTextItem(self.chart) # self.yTextItem.setText('Y: ') # self.update_x_y_coords() # Track mouse self.setMouseTracking(True) # Top Widgets newWidget = QWidget(self) newLayout = QHBoxLayout() newLayout.setContentsMargins(0, 0, 0, 0) newWidget.setLayout(newLayout) #labelx = QLabel(self) #labelx.setText('X:') #self.labelXValue = QLabel(self) #labely = QLabel(self) #labely.setText('Y:') #self.labelYValue = QLabel(self) # Test buttons #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) # Spacer #newLayout.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) # Labels """newLayout.addWidget(labelx) newLayout.addWidget(self.labelXValue) self.labelXValue.setMinimumWidth(200) self.labelXValue.setMaximumWidth(200) newLayout.addWidget(labely) newLayout.addWidget(self.labelYValue) self.labelYValue.setMinimumWidth(200) self.labelYValue.setMaximumWidth(200) """ """if parent is not None: parent.layout().setMenuBar(newWidget) """ # self.layout() self.build_style() def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') def closeEvent(self, QCloseEvent): self.aboutToClose.emit(self) @pyqtSlot(QPointF) def lineseries_clicked(self, point): print('lineseries clicked', point) @pyqtSlot(QPointF) def lineseries_hovered(self, point): print('lineseries hovered', point) def update_x_y_coords(self): pass # self.xTextItem.setPos(self.chart.size().width() / 2 - 100, self.chart.size().height() - 40) # self.yTextItem.setPos(self.chart.size().width() / 2 + 100, self.chart.size().height() - 40) def decimate(self, xdata, ydata): assert (len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) #print('decimate factor', decimate_factor) # x = decimate(xdata, decimate_factor) # y = decimate(ydata, decimate_factor) self.decim_factor = decimate_factor x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) for i in range(len(x)): index = i * decimate_factor assert (index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) #print('return size', len(x), len(y), 'timestamp', x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min, max): #print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X # axisX = QDateTimeAxis() # axisX.setTickCount(5) # axisX.setFormat("dd MMM yyyy") # axisX.setTitleText("Date") # self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) axisX = QValueAxis() axisX.setTickCount(10) axisX.setLabelFormat("%li") axisX.setTitleText("Seconds") self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisX) series.attachAxis(axisY) vect = series.pointsVector() for i in range(len(vect)): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable axisX.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) #curve.setUseOpenGL(True) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) for i in range(len(xdecimated)): # TODO hack x = xdecimated[i] - xdecimated[0] curve.append(QPointF(x, ydecimated[i])) self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def set_title(self, title): # print('Setting title: ', title) #self.chart.setTitle(title) pass def series_to_polyline(self, xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) for i in range(0, len(xdata)): polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title("Simple example with %d curves of %d points " \ "(OpenGL Accelerated Series)" \ % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseMoveEvent(e) # Go back to seconds (instead of ms) """xmap = self.chart.mapToValue(e.pos()).x() ymap = self.chart.mapToValue(e.pos()).y() self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) self.labelYValue.setText(str(ymap))""" # self.xTextItem.setText('X: ' + str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) # self.yTextItem.setText('Y: ' + str(ymap)) def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands super().mousePressEvent(e) self.setCursorPosition(e.pos().x(), True) pass def setCursorPosition(self, pos, emit_signal=False): # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap = self.chart.mapToValue(QPointF(pos, 0)).x() ymap = self.chart.mapToValue(QPointF(pos, 0)).y() #self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) #self.labelYValue.setText(str(ymap)) if emit_signal: self.cursorMoved.emit( datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Converts timestamp to x value pos = self.chart.mapToPosition( QPointF((timestamp - self.reftime).total_seconds(), 0)).x() self.setCursorPosition(pos, emit_signal) def mouseReleaseEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseReleaseEvent(e) pass def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # Update cursor height area = self.chart.plotArea() line = self.cursor.line() self.cursor.setLine(line.x1(), area.y(), line.x2(), area.y() + area.height()) # self.scene().setSceneRect(0, 0, e.size().width(), e.size().height()) # Need to reposition X,Y labels self.update_x_y_coords()
class ChartView(QChartView): def __init__(self, file, parent=None): super(ChartView, self).__init__(parent) self._chart = QChart() self._chart.setAcceptHoverEvents(True) self.setChart(self._chart) self.initUi(file) def initUi(self, file): if isinstance(file, dict): return self.__analysis(file) if isinstance(file, str): if not os.path.isfile(file): return self.__analysis(json.loads(file)) with open(file, "rb") as fp: data = fp.read() encoding = chardet.detect(data) or {} data = data.decode(encoding.get("encoding") or "utf-8") self.__analysis(json.loads(data)) # def onSeriesHoverd(self, point, state): # print(point, state) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() min_x, max_x = axisX.min(), axisX.max() min_y, max_y = axisY.min(), axisY.max() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(event.pos()).x() y = self._chart.mapToValue(event.pos()).y() index = round(x) # 四舍五入 print(x, y, index) # 得到在坐标系中的所有series的类型和点 points = [(s.type(), s.at(index)) for s in self._chart.series() if min_x <= x <= max_x and min_y <= y <= max_y] print(points) def __getColor(self, color=None, default=Qt.white): ''' :param color: int|str|[r,g,b]|[r,g,b,a] ''' if not color: return QColor(default) if isinstance(color, QBrush): return color # 比如[r,g,b]或[r,g,b,a] if isinstance(color, list) and 3 <= len(color) <= 4: return QColor(*color) else: return QColor(color) def __getPen(self, pen=None, default=QPen(Qt.white, 1, Qt.SolidLine, Qt.SquareCap, Qt.BevelJoin)): ''' :param pen: pen json ''' if not pen or not isinstance(pen, dict): return default return QPen(self.__getColor(pen.get("color", None) or default.color()), pen.get("width", 1) or 1, pen.get("style", 0) or 0, pen.get("capStyle", 16) or 16, pen.get("joinStyle", 64) or 64) def __getAlignment(self, alignment): ''' :param alignment: left|top|right|bottom ''' try: return getattr(Qt, "Align" + alignment.capitalize()) except: return Qt.AlignTop # if alignment == "left": # return Qt.AlignLeft # if alignment == "right": # return Qt.AlignRight # if alignment == "bottom": # return Qt.AlignBottom # return Qt.AlignTop def __setTitle(self, title=None): ''' :param title: title json ''' if not title or not isinstance(title, dict): return # 设置标题 self._chart.setTitle(title.get("text", "") or "") # 设置标题颜色 self._chart.setTitleBrush( self.__getColor( title.get("color", self._chart.titleBrush()) or self._chart.titleBrush())) # 设置标题字体 font = QFont(title.get("font", "") or self._chart.titleFont()) pointSize = title.get("pointSize", -1) or -1 if pointSize > 0: font.setPointSize(pointSize) font.setWeight(title.get("weight", -1) or -1) font.setItalic(title.get("italic", False) or False) self._chart.setTitleFont(font) def __setAnimation(self, animation=None): ''' :param value: animation json ''' if not animation or not isinstance(animation, dict): return # 动画持续时间 self._chart.setAnimationDuration( animation.get("duration", 1000) or 1000) # 设置动画曲线 self._chart.setAnimationEasingCurve( EasingCurve.get(animation.get("curve", 10) or 10, None) or QEasingCurve.OutQuart) # 设置开启何种动画 self._chart.setAnimationOptions( AnimationOptions.get(animation.get("options", 0) or 0, None) or QChart.NoAnimation) def __setBackground(self, background=None): ''' :param background:background json ''' if not background or not isinstance(background, dict): return # 设置是否背景可用 self._chart.setBackgroundVisible( background.get("visible", True) or True) # 设置背景矩形的圆角 self._chart.setBackgroundRoundness(background.get("radius", 0) or 0) # 设置下拉阴影 self._chart.setDropShadowEnabled( background.get("dropShadow", True) or True) # 设置pen self._chart.setBackgroundPen( self.__getPen(background.get("pen", None), self._chart.backgroundPen())) # 设置背景 image = background.get("image", None) color = background.get("color", None) if image: self._chart.setBackgroundBrush(QBrush(QPixmap(image))) elif color: self._chart.setBackgroundBrush( self.__getColor(color, self._chart.backgroundBrush())) def __setMargins(self, margins=None): ''' :param margins: margins json ''' if not margins or not isinstance(margins, dict): return left = margins.get("left", 20) or 20 top = margins.get("top", 20) or 20 right = margins.get("right", 20) or 20 bottom = margins.get("bottom", 20) or 20 self._chart.setMargins(QMargins(left, top, right, bottom)) def __setLegend(self, legend=None): ''' :param legend: legend json ''' if not legend or not isinstance(legend, dict): return _legend = self._chart.legend() _legend.setAlignment(self.__getAlignment(legend.get("alignment", None))) _legend.setShowToolTips(legend.get("showToolTips", True) or True) def __getSerie(self, serie=None): if not serie or not isinstance(serie, dict): return None types = serie.get("type", "") or "" data = serie.get("data", []) or [] if not data or not isinstance(data, list): return None if types == "line": _series = QLineSeries(self._chart) else: return None # 设置series名字 _series.setName(serie.get("name", "") or "") # 添加数据到series中 for index, value in enumerate(data): # 保证vlaue必须是数字 _series.append(index, value if type(value) in (int, float) else 0) return _series def __setSeries(self, series=None): if not series or not isinstance(series, list): return for serie in series: _serie = self.__getSerie(serie) if _serie: # _serie.hovered.connect(self.onSeriesHoverd) self._chart.addSeries(_serie) # 创建默认的xy轴 self._chart.createDefaultAxes() def __setAxisX(self, axisx=None): if not axisx or not isinstance(axisx, dict): return series = self._chart.series() if not series: return types = axisx.get("type", None) data = axisx.get("data", []) or [] if not data or not isinstance(data, list): return None minx = self._chart.axisX().min() maxx = self._chart.axisX().max() if types == "category": xaxis = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) # 隐藏网格 xaxis.setGridLineVisible(False) # 刻度条数 tickc_d = len(data) tickc = tickc_d if tickc_d > 1 else self._chart.axisX().tickCount() xaxis.setTickCount(tickc) # 强制x轴刻度与新刻度条数一致 self._chart.axisX().setTickCount(tickc) step = (maxx - minx) / (tickc - 1) for i in range(min(tickc_d, tickc)): xaxis.append(data[i], minx + i * step) self._chart.setAxisX(xaxis, series[-1]) def __analysis(self, datas): ''' analysis json data :param datas: json data ''' # 标题 self.__setTitle(datas.get("title", None)) # 抗锯齿 if (datas.get("antialiasing", False) or False): self.setRenderHint(QPainter.Antialiasing) # 主题 self._chart.setTheme(datas.get("theme", 0) or 0) # 动画 self.__setAnimation(datas.get("animation", None)) # 背景设置 self.__setBackground(datas.get("background", None)) # 边距设置 self.__setMargins(datas.get("margins", None)) # 设置图例 self.__setLegend(datas.get("legend", None)) # 设置series self.__setSeries(datas.get("series", None)) # 自定义的x轴 self.__setAxisX(datas.get("axisx", None))
class MainWindow(QMainWindow): BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MAIN_UI_FILE = os.path.join(BASE_DIR, "main.ui") NEW_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_popup.ui") NEW_DISH_MULTI_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_multi_popup.ui") NEW_DISH_DATA_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_data_popup.ui") MODIFY_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "modify_dish_popup.ui") DB_FILE = os.path.join(BASE_DIR, "restaurant.db") def __init__(self): super(MainWindow, self).__init__() # Initialize variable self.db_connection = None self.new_dish_popup = QWidget() self.new_dish_multi_popup = QWidget() self.new_dish_data_popup = QWidget() self.modify_dish_popup = QWidget() self.dish_table_model = QStandardItemModel(0, 6) self.dish_table_proxy = TableFilter() self.dish_data_table_model = QStandardItemModel(0, 6) self.dish_data_table_proxy = TableFilter() self.graph_chart = None self.graph_series = {} # Load UI designs uic.loadUi(self.MAIN_UI_FILE, self) uic.loadUi(self.NEW_DISH_POPUP_UI_FILE, self.new_dish_popup) uic.loadUi(self.NEW_DISH_MULTI_POPUP_UI_FILE, self.new_dish_multi_popup) uic.loadUi(self.NEW_DISH_DATA_POPUP_UI_FILE, self.new_dish_data_popup) uic.loadUi(self.MODIFY_DISH_POPUP_UI_FILE, self.modify_dish_popup) self.init_dish_table() self.init_dish_data_table() self.init_graph() # Connect to database self.init_db_connection() # MainWindow Bind action triggers self.action_new_dish.triggered.connect(self.show_new_dish_popup) self.action_new_dish_multi.triggered.connect( self.show_new_dish_multi_popup) self.action_new_data_multi.triggered.connect( lambda: self.modify_new_dish_data_popup_table(show=True)) self.tabWidget.currentChanged.connect(self.update_graph) # Dish Table filter bind self.dish_lineEdit.textChanged.connect( lambda text, col_idx=1: self.dish_table_proxy.set_col_regex_filter( col_idx, text)) self.lower_price_doubleSpinBox.valueChanged.connect( lambda value, col_idx=2: self.dish_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_price_doubleSpinBox.valueChanged.connect( lambda value, col_idx=2: self.dish_table_proxy. set_col_number_filter(col_idx, -1, value)) self.lower_week_sell_spinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_week_sell_spinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_table_proxy. set_col_number_filter(col_idx, -1, value)) # Dish Data Table filter bind self.lower_data_dateEdit.dateChanged.connect( lambda date, col_idx=1: self.dish_data_table_proxy. set_col_date_filter(col_idx, date, -1)) self.higher_data_dateEdit.dateChanged.connect( lambda date, col_idx=1: self.dish_data_table_proxy. set_col_date_filter(col_idx, -1, date)) self.data_lineEdit.textChanged.connect( lambda text, col_idx=2: self.dish_data_table_proxy. set_col_regex_filter(col_idx, text)) self.lower_data_doubleSpinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_data_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_data_doubleSpinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_data_table_proxy. set_col_number_filter(col_idx, -1, value)) self.lower_data_spinBox.valueChanged.connect( lambda value, col_idx=4: self.dish_data_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_data_spinBox.valueChanged.connect( lambda value, col_idx=4: self.dish_data_table_proxy. set_col_number_filter(col_idx, -1, value)) self.data_all_check_checkBox.stateChanged.connect( lambda state, col_idx=5: self.data_table_check_state( state, col_idx)) self.dish_data_table_model.itemChanged.connect(self.update_series) # Popup bind action triggers self.new_dish_popup.create_new_dish_btn.clicked.connect( self.create_new_dish) self.new_dish_multi_popup.pushButton_ok.clicked.connect( self.create_new_dish_multi) self.new_dish_data_popup.dateEdit.dateChanged.connect( self.modify_new_dish_data_popup_table) self.new_dish_data_popup.pushButton_ok.clicked.connect( self.create_new_dish_data) # Get current dishes self.load_dish_table() self.load_dish_data_table() self.new_dish_data_popup.dateEdit.setDate(QtCore.QDate.currentDate()) def init_dish_table(self): self.dish_tableView.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) # Set Header data and stretch for col, col_name in enumerate( ["ID", "菜品", "价格", "近7天总售出", "操作", "备注"]): self.dish_table_model.setHeaderData(col, Qt.Horizontal, col_name, Qt.DisplayRole) self.dish_table_proxy.setSourceModel(self.dish_table_model) self.dish_tableView.setModel(self.dish_table_proxy) self.dish_tableView.setColumnHidden(0, True) for (col, method) in [(1, "Regex"), (2, "Number"), (3, "Number"), (5, "Regex")]: self.dish_table_proxy.filter_method[col] = method def init_dish_data_table(self): self.data_tableView.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) for col, col_name in enumerate( ["Dish_ID", "日期", "菜品", "价格", "售出", "选择"]): self.dish_data_table_model.setHeaderData(col, Qt.Horizontal, col_name, Qt.DisplayRole) self.dish_data_table_proxy.setSourceModel(self.dish_data_table_model) self.data_tableView.setModel(self.dish_data_table_proxy) self.data_tableView.setColumnHidden(0, True) for (col, method) in [(1, "Date"), (2, "Regex"), (3, "Number"), (4, "Number")]: self.dish_data_table_proxy.filter_method[col] = method def init_graph(self): self.graph_chart = QChart(title="售出图") self.graph_chart.legend().setVisible(True) self.graph_chart.setAcceptHoverEvents(True) graph_view = QChartView(self.graph_chart) graph_view.setRenderHint(QPainter.Antialiasing) self.gridLayout_5.addWidget(graph_view) def init_db_connection(self): self.db_connection = sqlite3.connect(self.DB_FILE) cursor = self.db_connection.cursor() # check create table if not exist sql_create_dish_table = """ CREATE TABLE IF NOT EXISTS dish ( id integer PRIMARY KEY, name text NOT NULL, price numeric Not NULL, remarks text, UNIQUE (name, price) ); """ sql_create_dish_data_table = """ CREATE TABLE IF NOT EXISTS dish_data ( dish_id integer NOT NULL REFERENCES dish(id) ON DELETE CASCADE, date date, sell_num integer DEFAULT 0, PRIMARY KEY (dish_id, date), CONSTRAINT dish_fk FOREIGN KEY (dish_id) REFERENCES dish (id) ON DELETE CASCADE ); """ sql_trigger = """ CREATE TRIGGER IF NOT EXISTS place_holder_data AFTER INSERT ON dish BEGIN INSERT INTO dish_data (dish_id, date, sell_num) VALUES(new.id, null, 0); END; """ cursor.execute(sql_create_dish_table) cursor.execute(sql_create_dish_data_table) cursor.execute("PRAGMA FOREIGN_KEYS = on") cursor.execute(sql_trigger) cursor.close() def load_dish_table(self): today = datetime.today() sql_select_query = """ SELECT dish.id, dish.name, dish.price, COALESCE(SUM(dish_data.sell_num), 0), dish.remarks FROM dish LEFT JOIN dish_data ON dish.id = dish_data.dish_id WHERE dish_data.date IS NULL OR dish_data.date BETWEEN date('{}') and date('{}') GROUP BY dish.id ORDER BY dish.name, dish.price;""".format( (today - timedelta(days=7)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")) cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() for row_idx, record in enumerate(records): self.dish_table_model.appendRow(create_dish_table_row(*record)) cursor.close() self.dish_tableView.setItemDelegateForColumn( 4, DishTableDelegateCell(self.show_modify_dish_popup, self.delete_dish, self.dish_tableView)) def load_dish_data_table(self): sql_select_query = """ SELECT dish_data.dish_id, dish_data.date, dish.name, dish.price, dish_data.sell_num FROM dish_data LEFT JOIN dish ON dish_data.dish_id = dish.id WHERE dish_data.date IS NOT NULL ORDER BY dish_data.date DESC, dish.name, dish.price, dish_data.sell_num;""" cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() for row_idx, record in enumerate(records): self.dish_data_table_model.appendRow( create_dish_data_table_row(*record)) cursor.close() self.lower_data_dateEdit.setDate(QDate.currentDate().addDays(-7)) self.higher_data_dateEdit.setDate(QDate.currentDate()) self.data_tableView.setItemDelegateForColumn( 5, DishDataTableDelegateCell(self.data_tableView)) def data_table_check_state(self, state, col): for row in range(self.dish_data_table_proxy.rowCount()): index = self.dish_data_table_proxy.mapToSource( self.dish_data_table_proxy.index(row, col)) if index.isValid(): self.dish_data_table_model.setData(index, str(state), Qt.DisplayRole) def show_new_dish_popup(self): # Move popup to center point = self.rect().center() global_point = self.mapToGlobal(point) self.new_dish_popup.move( global_point - QtCore.QPoint(self.new_dish_popup.width() // 2, self.new_dish_popup.height() // 2)) self.new_dish_popup.show() def show_new_dish_multi_popup(self): file_name = QFileDialog().getOpenFileName(None, "选择文件", "", self.tr("CSV文件 (*.csv)"))[0] self.new_dish_multi_popup.tableWidget.setRowCount(0) if file_name: with open(file_name, "r") as file: csv_reader = csv.reader(file, delimiter=",") for idx, row_data in enumerate(csv_reader): if len(row_data) == 2: name, price = row_data remark = "" elif len(row_data) == 3: name, price, remark = row_data else: QMessageBox.warning( self, "格式错误", self.tr('格式为"菜品 价格"或者"菜品 价格 备注"\n第{}行输入有误'.format( idx))) return self.new_dish_multi_popup.tableWidget.insertRow( self.new_dish_multi_popup.tableWidget.rowCount()) self.new_dish_multi_popup.tableWidget.setItem( idx, 0, QTableWidgetItem(name)) price_type = str_type(price) if price_type == str or (isinstance( price_type, (float, int)) and float(price) < 0): QMessageBox.warning( self, "格式错误", self.tr('第{}行价格输入有误'.format(idx + 1))) return self.new_dish_multi_popup.tableWidget.setItem( idx, 1, QTableWidgetItem("{:.2f}".format(float(price)))) self.new_dish_multi_popup.tableWidget.setItem( idx, 2, QTableWidgetItem(remark)) self.new_dish_multi_popup.show() def modify_new_dish_data_popup_table(self, *args, show=False): sql_select_query = """ SELECT id, name, price, dish_data.sell_num FROM dish LEFT JOIN dish_data ON dish.id=dish_data.dish_id WHERE dish_data.date IS NULL OR dish_data.date = date('{}') GROUP BY id, name, price ORDER BY dish.name, dish.price;""".format( self.new_dish_data_popup.dateEdit.date().toString("yyyy-MM-dd")) cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() self.new_dish_data_popup.tableWidget.setRowCount(len(records)) self.new_dish_data_popup.tableWidget.setColumnHidden(0, True) for row_idx, record in enumerate(records): dish_id, name, price, sell_num = record self.new_dish_data_popup.tableWidget.setItem( row_idx, 0, QTableWidgetItem(str(dish_id))) self.new_dish_data_popup.tableWidget.setItem( row_idx, 1, QTableWidgetItem(name)) self.new_dish_data_popup.tableWidget.setItem( row_idx, 2, QTableWidgetItem("{:.2f}".format(price))) spin_box = QSpinBox() spin_box.setMaximum(9999) spin_box.setValue(sell_num) self.new_dish_data_popup.tableWidget.setCellWidget( row_idx, 3, spin_box) cursor.close() if show: self.new_dish_data_popup.show() def create_new_dish(self): cursor = self.db_connection.cursor() sql_insert = """ INSERT INTO dish(name, price, remarks) VALUES(?,?,?)""" dish_name = self.new_dish_popup.dish_name.text() dish_price = self.new_dish_popup.dish_price.value() dish_remark = self.new_dish_popup.dish_remark.toPlainText() try: cursor.execute(sql_insert, (dish_name, dish_price, dish_remark)) new_dish_id = cursor.lastrowid cursor.close() self.db_connection.commit() # Update dish table and dish comboBox in UI self.dish_table_model.appendRow( create_dish_table_row(new_dish_id, dish_name, dish_price, 0, dish_remark)) self.new_dish_popup.hide() except sqlite3.Error: cursor.close() QMessageBox.warning(self, "菜品价格重复", self.tr('菜品价格组合重复,请检查')) def create_new_dish_multi(self): cursor = self.db_connection.cursor() sql_insert = """ INSERT INTO dish(name, price, remarks) VALUES (?, ?, ?)""" for row in range(self.new_dish_multi_popup.tableWidget.rowCount()): dish_name = self.new_dish_multi_popup.tableWidget.item(row, 0).text() dish_price = float( self.new_dish_multi_popup.tableWidget.item(row, 1).text()) dish_remark = self.new_dish_multi_popup.tableWidget.item(row, 2).text() try: cursor.execute(sql_insert, (dish_name, dish_price, dish_remark)) new_dish_id = cursor.lastrowid self.dish_table_model.appendRow( create_dish_table_row(new_dish_id, dish_name, dish_price, 0, dish_remark)) except sqlite3.Error: cursor.close() QMessageBox.warning( self, "菜品价格重复", self.tr('前{}行已插入。\n第{}行菜品价格组合重复,请检查'.format(row, row + 1))) return cursor.close() self.db_connection.commit() self.new_dish_multi_popup.hide() def create_new_dish_data(self): current_date = self.new_dish_data_popup.dateEdit.date().toString( "yyyy-MM-dd") table_filter = TableFilter() table_filter.setSourceModel(self.dish_data_table_model) table_filter.set_col_regex_filter(1, current_date) for row in range(table_filter.rowCount()): index = table_filter.mapToSource(table_filter.index(0, 1)) if index.isValid(): self.dish_data_table_model.removeRow(index.row()) del table_filter cursor = self.db_connection.cursor() sql_insert = """ INSERT OR REPLACE INTO dish_data(dish_id, date, sell_num) VALUES (?, ?, ?)""" for row in range(self.new_dish_data_popup.tableWidget.rowCount()): dish_id = int( self.new_dish_data_popup.tableWidget.item(row, 0).text()) name = self.new_dish_data_popup.tableWidget.item(row, 1).text() price = float( self.new_dish_data_popup.tableWidget.item(row, 2).text()) sell_num = self.new_dish_data_popup.tableWidget.cellWidget( row, 3).value() cursor.execute(sql_insert, (dish_id, current_date, sell_num)) self.dish_data_table_model.appendRow( create_dish_data_table_row(dish_id, current_date, name, price, sell_num)) cursor.close() self.db_connection.commit() self.new_dish_data_popup.hide() def delete_dish(self, dish_id): cursor = self.db_connection.cursor() sql_delete = """ DELETE FROM dish WHERE id=?""" cursor.execute(sql_delete, tuple([dish_id])) cursor.close() self.db_connection.commit() # Update dish table and dish comboBox in UI for row in self.dish_data_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_data_table_model.removeRow(index.row()) for row in self.dish_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_table_model.removeRow(index.row()) def show_modify_dish_popup(self, dish_id): point = self.rect().center() global_point = self.mapToGlobal(point) self.modify_dish_popup.move( global_point - QtCore.QPoint(self.modify_dish_popup.width() // 2, self.modify_dish_popup.height() // 2)) # Find the row and get necessary info index = self.dish_table_model.match(self.dish_table_model.index(0, 0), Qt.DisplayRole, str(dish_id)) if index: row_idx = index[0] dish_name = self.dish_table_model.data(row_idx.siblingAtColumn(1)) dish_price = self.dish_table_model.data(row_idx.siblingAtColumn(2)) dish_remark = self.dish_table_model.data( row_idx.siblingAtColumn(5)) self.modify_dish_popup.dish_name.setText(dish_name) self.modify_dish_popup.dish_price.setValue(float(dish_price)) self.modify_dish_popup.dish_remark.setText(dish_remark) try: self.modify_dish_popup.modify_dish_btn.clicked.disconnect() except TypeError: pass self.modify_dish_popup.modify_dish_btn.clicked.connect( lambda: self.modify_dish(row_idx, dish_id)) self.modify_dish_popup.show() def modify_dish(self, row, dish_id): cursor = self.db_connection.cursor() sql_update = """ UPDATE dish SET name = ?, price = ?, remarks = ? WHERE id=?""" dish_name = self.modify_dish_popup.dish_name.text() dish_price = self.modify_dish_popup.dish_price.value() dish_remark = self.modify_dish_popup.dish_remark.toPlainText() cursor.execute(sql_update, (dish_name, dish_price, dish_remark, dish_id)) cursor.close() self.db_connection.commit() self.modify_dish_popup.hide() # Update dish table and dish comboBox in UI old_name = self.dish_table_model.data(row.siblingAtColumn(1)) old_price = self.dish_table_model.data(row.siblingAtColumn(2)) sell_num = self.dish_table_model.data(row.siblingAtColumn(3)) row_idx = row.row() self.dish_table_model.removeRow(row_idx) self.dish_table_model.insertRow( row_idx, create_dish_table_row(dish_id, dish_name, dish_price, sell_num, dish_remark)) for row in self.dish_data_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_data_table_model.setData(index.siblingAtColumn(2), dish_name) self.dish_data_table_model.setData(index.siblingAtColumn(3), "{:.2f}".format(dish_price)) old_key = old_name + '(' + old_price + ')' if old_key in self.graph_line_series: self.graph_line_series[dish_name + '(' + str(dish_price) + ')'] = self.graph_line_series[old_key] del self.graph_line_series[old_key] def update_series(self, item: QStandardItem): if item.column() == 5: # check for checkbox column item_idx = item.index() date = self.dish_data_table_model.data(item_idx.siblingAtColumn(1)) dish_name = self.dish_data_table_model.data( item_idx.siblingAtColumn(2)) dish_price = self.dish_data_table_model.data( item_idx.siblingAtColumn(3)) sell_num = self.dish_data_table_model.data( item_idx.siblingAtColumn(4)) set_name = dish_name + "(" + dish_price + ")" key = str( QDateTime(QDate.fromString(date, "yyyy-MM-dd")).toSecsSinceEpoch()) if key not in self.graph_series: self.graph_series[key] = {} if int(item.text()) == 0: if set_name in self.graph_series[key]: del self.graph_series[key][set_name] if not self.graph_series[key]: del self.graph_series[key] else: self.graph_series[key][set_name] = int(sell_num) def update_graph(self, index): if index == 2: self.graph_chart.removeAllSeries() axis_x = QBarCategoryAxis() axis_x.setTitleText("日期") if self.graph_chart.axisX(): self.graph_chart.removeAxis(self.graph_chart.axisX()) self.graph_chart.addAxis(axis_x, Qt.AlignBottom) axis_y = QValueAxis() axis_y.setLabelFormat("%i") axis_y.setTitleText("售出量") if self.graph_chart.axisY(): self.graph_chart.removeAxis(self.graph_chart.axisY()) self.graph_chart.addAxis(axis_y, Qt.AlignLeft) max_num = 0 total_date = 0 set_dict = {} for key, data in sorted(self.graph_series.items(), key=lambda i: int(i[0])): axis_x.append( QDateTime.fromSecsSinceEpoch( int(key)).toString("yyyy年MM月dd日")) for set_name, value in data.items(): if set_name not in set_dict: set_dict[set_name] = QBarSet(set_name) for _ in range(total_date): set_dict[set_name].append(0) set_dict[set_name].append(value) max_num = max(max_num, value) total_date += 1 for _, bar_set in set_dict.items(): if bar_set.count() < total_date: bar_set.append(0) bar_series = QBarSeries() for _, bar_set in set_dict.items(): bar_series.append(bar_set) bar_series.hovered.connect(self.graph_tooltip) axis_y.setMax(max_num + 1) axis_y.setMin(0) self.graph_chart.addSeries(bar_series) bar_series.attachAxis(axis_x) bar_series.attachAxis(axis_y) def graph_tooltip(self, status, index, bar_set: QBarSet): if status: QToolTip.showText( QCursor.pos(), "{}\n日期: {}\n售出: {}".format(bar_set.label(), self.graph_chart.axisX().at(index), int(bar_set.at(index))))
class View(QGraphicsView): def __init__(self, parent=None): super().__init__(QGraphicsScene(), parent) self.m_tooltip = None self.m_callouts = [] self.setDragMode(QGraphicsView.NoDrag) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # chart self.m_chart = QChart() self.m_chart.setMinimumSize(640, 480) self.m_chart.setTitle( "Hover the line to show callout. Click the line to make it stay" ) self.m_chart.legend().hide() series = QLineSeries() series.append(1, 3) series.append(4, 5) series.append(5, 4.5) series.append(7, 1) series.append(11, 2) self.m_chart.addSeries(series) series2 = QSplineSeries() series2.append(1.6, 1.4) series2.append(2.4, 3.5) series2.append(3.7, 2.5) series2.append(7, 4) series2.append(10, 2) self.m_chart.addSeries(series2) self.m_chart.createDefaultAxes() self.m_chart.setAcceptHoverEvents(True) self.setRenderHint(QPainter.Antialiasing) self.scene().addItem(self.m_chart) self.m_coordX = QGraphicsSimpleTextItem(self.m_chart) self.m_coordX.setPos( self.m_chart.size().width() / 2 - 50, self.m_chart.size().height() ) self.m_coordX.setText("X: ") self.m_coordY = QGraphicsSimpleTextItem(self.m_chart) self.m_coordY.setPos( self.m_chart.size().width() / 2 + 50, self.m_chart.size().height() ) self.m_coordY.setText("Y: ") series.clicked.connect(self.keepCallout) series.hovered.connect(self.tooltip) series2.clicked.connect(self.keepCallout) series2.hovered.connect(self.tooltip) self.setMouseTracking(True) def resizeEvent(self, event): if self.scene() is not None: self.scene().setSceneRect(QRectF(QRect(QPoint(0, 0), event.size()))) self.m_chart.resize(QSizeF(event.size())) self.m_coordX.setPos( self.m_chart.size().width() / 2 - 50, self.m_chart.size().height() - 20 ) self.m_coordY.setPos( self.m_chart.size().width() / 2 + 50, self.m_chart.size().height() - 20 ) for callout in self.m_callouts: callout.updateGeometry() super().resizeEvent(event) def mouseMoveEvent(self, event): self.m_coordX.setText("X: %f" % self.m_chart.mapToValue(event.pos()).x()) self.m_coordY.setText("Y: %f" % self.m_chart.mapToValue(event.pos()).y()) super().mouseMoveEvent(event) def keepCallout(self): self.m_callouts.append(self.m_tooltip) self.m_tooltip = Callout(self.m_chart) def tooltip(self, point, state): if self.m_tooltip is None: self.m_tooltip = Callout(self.m_chart) if state: self.m_tooltip.setText("X: %f \nY: %f " % (point.x(), point.y())) self.m_tooltip.setAnchor(point) self.m_tooltip.setZValue(11) self.m_tooltip.updateGeometry() self.m_tooltip.show() else: self.m_tooltip.hide()
class LineChartView(QChartView): def __init__(self): super(LineChartView, self).__init__() self.resize(800, 600) self.setRenderHint(QPainter.Antialiasing) # 抗锯齿 # 自定义x轴label def CreatePlot(self, NumSection, Alltime, PlotData, type, name='列车调度'): if type == 1: self.initChart(NumSection, Alltime, PlotData, name) else: self.initProgressChart(NumSection, Alltime, PlotData, name) # 提示widget self.toolTipWidget = GraphicsProxyWidget(self._chart) # line self.lineItem = QGraphicsLineItem(self._chart) pen = QPen(Qt.gray) pen.setWidth(1) self.lineItem.setPen(pen) self.lineItem.setZValue(998) #self.lineItem.hide() self.lineItem.show() # 一些固定计算,减少mouseMoveEvent中的计算量 # 获取x和y轴的最小最大值 axisX, axisY = self._chart.axisX(), self._chart.axisY() self.min_x, self.max_x = axisX.min(), axisX.max() self.min_y, self.max_y = axisY.min(), axisY.max() def resizeEvent(self, event): super(LineChartView, self).resizeEvent(event) # 当窗口大小改变时需要重新计算 # 坐标系中左上角顶点 self.point_top = self._chart.mapToPosition( QPointF(self.min_x, self.max_y)) # 坐标原点坐标 self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / (self._chart.axisX().tickCount() - 1) def mouseMoveEvent(self, event): super(LineChartView, self).mouseMoveEvent(event) pos = event.pos() # 把鼠标位置所在点转换为对应的xy值 x = self._chart.mapToValue(pos).x() y = self._chart.mapToValue(pos).y() index = round(int(x / self.step_x) * self.step_x) + round( x % self.step_x) - 1 #index = round((x - self.min_x) / self.step_x) # 得到在坐标系中的所有正常显示的series的类型和点 points = [ (serie, serie.at(index)) for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y ] if points: pos_x = self._chart.mapToPosition( QPointF(index * self.step_x + self.min_x, self.min_y)) self.lineItem.setLine(pos_x.x(), self.point_top.y(), pos_x.x(), self.point_bottom.y()) self.lineItem.show() try: title = "" except: title = "" t_width = self.toolTipWidget.width() t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show(title, points, QPoint(x, y)) else: self.toolTipWidget.hide() self.lineItem.hide() def handleMarkerClicked(self): marker = self.sender() # 信号发送者 if not marker: return visible = not marker.series().isVisible() # # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 alpha = 1.0 if visible else 0.4 # 设置label的透明度 brush = marker.labelBrush() color = brush.color() 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) # 设置画笔透明度 pen = marker.pen() color = pen.color() color.setAlphaF(alpha) pen.setColor(color) marker.setPen(pen) def handleMarkerHovered(self, status): # 设置series的画笔宽度 marker = self.sender() # 信号发送者 if not marker: return series = marker.series() if not series: return pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if status else -1)) series.setPen(pen) def handleSeriesHoverd(self, point, state): # 设置series的画笔宽度 series = self.sender() # 信号发送者 pen = series.pen() if not pen: return pen.setWidth(pen.width() + (1 if state else -1)) series.setPen(pen) def initProgressChart(self, MaxTime, Allgen, PlotData, name): MinTime = 100000 for k in PlotData: if min(k[1]) < MinTime: MinTime = min(k[1]) self._chart = QChart(title=name) self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) # dataTable = [ # ["邮件营销", [120, 132, 101, 134, 90, 230]], # ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], # ["视频广告", [150, 232, 201, 154, 190, 330, 410]], # ["直接访问", [320, 332, 301, 334, 390, 330, 320]], # ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]] # ] for series_name, data_list, label_list in PlotData: series = QLineSeries(self._chart) for j, v in zip(label_list, data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(11) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(10) # y轴设置7个刻度 #axisY.setRange(0, MaxTime) # 设置y轴范围 # 自定义x轴 # axis_x = QCategoryAxis( # self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) # axis_x.setTickCount(Allgen) # axis_x.setGridLineVisible(False) # min_x = axisX.min() # for i in range(0, Allgen + 1): # axis_x.append(str(i), min_x + i) # self._chart.setAxisX(axis_x, self._chart.series()[-1]) # 自定义y轴 # axis_y = QCategoryAxis( # self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) # axis_y.setTickCount(round(MaxTime)-max(round(MinTime)-10,0)) # axis_y.setRange(max(round(MinTime)-10,0), round(MaxTime)) # for i in range(max(round(MinTime)-10,0), round(MaxTime) + 1): # axis_y.append('%i' % i, i) # self._chart.setAxisY(axis_y, self._chart.series()[-1]) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart) def initChart(self, NumSection, Alltime, PlotData, name): self._chart = QChart(title=name) self._chart.setAcceptHoverEvents(True) # Series动画 self._chart.setAnimationOptions(QChart.SeriesAnimations) # dataTable = [ # ["邮件营销", [120, 132, 101, 134, 90, 230]], # ["联盟广告", [220, 182, 191, 234, 290, 330, 310]], # ["视频广告", [150, 232, 201, 154, 190, 330, 410]], # ["直接访问", [320, 332, 301, 334, 390, 330, 320]], # ["搜索引擎", [820, 932, 901, 934, 1290, 1330, 1320]] # ] for series_name, data_list, label_list in PlotData: series = QLineSeries(self._chart) for j, v in zip(label_list, data_list): series.append(j, v) series.setName(series_name) series.setPointsVisible(True) # 显示圆点 series.hovered.connect(self.handleSeriesHoverd) # 鼠标悬停 self._chart.addSeries(series) self._chart.createDefaultAxes() # 创建默认的轴 axisX = self._chart.axisX() # x轴 axisX.setTickCount(Alltime) # x轴设置7个刻度 axisX.setGridLineVisible(False) # 隐藏从x轴往上的线条 axisY = self._chart.axisY() axisY.setTickCount(NumSection) # y轴设置7个刻度 axisY.setRange(0, NumSection) # 设置y轴范围 # 自定义x轴 axis_x = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue) axis_x.setTickCount(Alltime + 1) axis_x.setGridLineVisible(False) min_x = axisX.min() for i in range(0, Alltime + 1): axis_x.append(str(i), min_x + i) self._chart.setAxisX(axis_x, self._chart.series()[-1]) # 自定义y轴 axis_y = QCategoryAxis( self._chart, labelsPosition=QCategoryAxis.AxisLabelsPositionCenter) axis_y.setTickCount(NumSection) axis_y.setRange(0, NumSection) for i in range(0, NumSection + 1): axis_y.append('section%i' % (NumSection - i + 1), i) self._chart.setAxisY(axis_y, self._chart.series()[-1]) # chart的图例 legend = self._chart.legend() # 设置图例由Series来决定样式 legend.setMarkerShape(QLegend.MarkerShapeFromSeries) # 遍历图例上的标记并绑定信号 for marker in legend.markers(): # 点击事件 marker.clicked.connect(self.handleMarkerClicked) # 鼠标悬停事件 marker.hovered.connect(self.handleMarkerHovered) self.setChart(self._chart)