class MainWindow(ChartViewToolTips):
    def __init__(self):
        super().__init__()

        series = QLineSeries()
        series.setPointsVisible(True)
        series.setPointLabelsVisible(True)
        series.setPointLabelsFormat("(@xPoint, @yPoint)")
        series.hovered.connect(self.show_series_tooltip)

        series.append(0, 6)
        series.append(2, 4)
        series.append(3, 8)
        series.append(7, 4)
        series.append(10, 5)

        self._chart = QChart()
        self._chart.setMinimumSize(640, 480)
        self._chart.setTitle("Line Chart Example")
        self._chart.setAnimationOptions(QChart.SeriesAnimations)
        self._chart.legend().hide()
        self._chart.addSeries(series)
        self._chart.createDefaultAxes()

        self.setChart(self._chart)

    def show_series_tooltip(self, point, state: bool):
        # value -> pos
        point = self._chart.mapToPosition(point)

        if not self._tooltip:
            self._tooltip = Callout(self._chart)

        if state:
            distance = 20
            for series in self._chart.series():
                for p_value in series.pointsVector():
                    p = self._chart.mapToPosition(p_value)

                    current_distance = math.sqrt(
                        (p.x() - point.x()) * (p.x() - point.x())
                        + (p.y() - point.y()) * (p.y() - point.y())
                    )
                    if current_distance < distance:
                        self._tooltip.setText("X: {}\nY: {}".format(p.x(), p.y()))
                        self._tooltip.setAnchor(p_value)
                        self._tooltip.setZValue(11)
                        self._tooltip.updateGeometry()
                        self._tooltip.show()
        else:
            self._tooltip.hide()
Exemple #2
0
class barChartView(QChartView):
    def __init__(self, xAxis=[], *args, **kwargs):
        super(barChartView, self).__init__(*args, **kwargs)
        self.initChart(xAxis)

        # line 宽度需要调整
        self.lineItem = QGraphicsLineItem(self._chart)
        pen = QPen(Qt.gray)
        self.lineItem.setPen(pen)
        self.lineItem.setZValue(998)
        self.lineItem.hide()
        self.cal()

    # 一些固定计算,减少mouseMoveEvent中的计算量
    def cal(self):
        # 提示widget
        self.toolTipWidget = GraphicsProxyWidget(self._chart)
        # 获取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 setCat(self, data):
        self.categories = data

    #初始化
    def initChart(self, xAxis):
        self._chart = QChart()
        # 调整边距
        self._chart.layout().setContentsMargins(0, 0, 0, 0)  # 外界
        self._chart.setMargins(QMargins(3, 0, 3, 0))  # 内界
        self._chart.setBackgroundRoundness(0)
        self._chart.setBackgroundVisible(False)
        # 设置主题
        self._chart.setTheme(QChart.ChartThemeBlueIcy)
        # 抗锯齿
        self.setRenderHint(QPainter.Antialiasing)
        # 开启动画效果
        self._chart.setAnimationOptions(QChart.SeriesAnimations)
        self.categories = xAxis
        self._series = QBarSeries(self._chart)
        self._chart.addSeries(self._series)
        self._chart.createDefaultAxes()  # 创建默认的轴
        self._axis_x = QBarCategoryAxis(self._chart)
        self._axis_x.append(self.categories)
        self._axis_y = QValueAxis(self._chart)
        self._axis_y.setTitleText("任务数")
        self._axis_y.setRange(0, 10)
        self._chart.setAxisX(self._axis_x, self._series)
        self._chart.setAxisY(self._axis_y, self._series)
        # chart的图例
        legend = self._chart.legend()
        legend.setVisible(True)

        self.setChart(self._chart)

    def mouseMoveEvent(self, event):
        super(barChartView, 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
        ]
        # print(bars)
        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()
Exemple #3
0
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()
Exemple #4
0
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()
Exemple #5
0
class KLineChartView(QChartView):

    # QCandlestickSeries的hovered的信号响应后传递日期出去
    candles_hovered = pyqtSignal(bool, str)

    def __init__(self, data: pd.DataFrame):
        super(KLineChartView, self).__init__()
        self.setRenderHint(QPainter.Antialiasing)  # 抗锯齿
        self._chart = QChart()
        self._series = QCandlestickSeries()
        self._stocks = data
        self._category = list()
        self._count = None

        self.init_chart()
        self._zero_value = (0, self._chart.axisY().min())
        self._max_value = (len(self._chart.axisX().categories()),
                           self._chart.axisY().max())
        self._zero_point = self._chart.mapToPosition(
            QPointF(self._zero_value[0], self._zero_value[1]))
        self._max_point = self._chart.mapToPosition(
            QPointF(self._max_value[0], self._max_value[1]))
        # 计算x轴单个cate的宽度,用来处理横线不能画到边界
        self._cate_width = (self._max_point.x() - self._zero_point.x()) / len(
            self._category)

        self._series.hovered.connect(self.on_series_hovered)

    def on_series_hovered(self, status, candles_set):
        trade_date = time.strftime('%Y%m%d',
                                   time.localtime(candles_set.timestamp()))
        self.candles_hovered.emit(status, trade_date)

    def set_name(self, name):
        self._series.setName(name)

    def clear_series_values(self):
        self._series.clear()
        self._chart.axisY().setRange(0, 10)
        self._chart.axisX().setCategories(list())
        self._stocks = None

    def add_series_values(self, data: pd.DataFrame, is_init=False):
        self._stocks = data
        self._category = self._stocks['trade_date']
        self._count = len(self._category)
        for _, stock in self._stocks.iterrows():
            time_p = datetime.datetime.strptime(stock['trade_date'], '%Y%m%d')
            time_p = float(time.mktime(time_p.timetuple()))
            _set = QCandlestickSet(float(stock['open']), float(stock['high']),
                                   float(stock['low']), float(stock['close']),
                                   time_p, self._series)
            self._series.append(_set)

        if not is_init:
            self._stocks = data
            self._category = self._stocks['trade_date']
            axis_x = self._chart.axisX()
            axis_y = self._chart.axisY()
            axis_x.setCategories(self._category)
            max_p = self._stocks[['high', 'low']].stack().max()
            min_p = self._stocks[['high', 'low']].stack().min()
            axis_y.setRange(min_p * 0.99, max_p * 1.01)
            self._zero_value = (0, self._chart.axisY().min())
            self._max_value = (len(self._chart.axisX().categories()),
                               self._chart.axisY().max())
            # 计算x轴单个cate的宽度,用来处理横线不能画到边界
            self._cate_width = (self._max_point.x() -
                                self._zero_point.x()) / len(self._category)

    def resizeEvent(self, event):
        super(KLineChartView, self).resizeEvent(event)
        self._zero_point = self._chart.mapToPosition(
            QPointF(self._zero_value[0], self._zero_value[1]))
        self._max_point = self._chart.mapToPosition(
            QPointF(self._max_value[0], self._max_value[1]))
        self._cate_width = (self._max_point.x() - self._zero_point.x()) / len(
            self._category)

    def max_point(self):
        return QPointF(self._max_point.x() + self._cate_width / 2,
                       self._max_point.y())

    def min_point(self):
        return QPointF(self._zero_point.x() - self._cate_width / 2,
                       self._zero_point.y())

    def init_chart(self):
        self._chart.setAnimationOptions(QChart.SeriesAnimations)
        self._series.setIncreasingColor(QColor(Qt.red))
        self._series.setDecreasingColor(QColor(Qt.green))
        self._series.setName(self._stocks['name'].iloc[0])
        self.add_series_values(self._stocks, True)
        self._chart.addSeries(self._series)

        self._chart.createDefaultAxes()
        self._chart.setLocalizeNumbers(True)
        axis_x = self._chart.axisX()
        axis_y = self._chart.axisY()
        axis_x.setGridLineVisible(False)
        axis_y.setGridLineVisible(False)
        axis_x.setCategories(self._category)
        axis_x.setLabelsVisible(False)
        axis_x.setVisible(False)
        max_p = self._stocks[['high', 'low']].stack().max()
        min_p = self._stocks[['high', 'low']].stack().min()
        axis_y.setRange(min_p * 0.99, max_p * 1.01)

        # chart的图例
        legend = self._chart.legend()
        # 设置图例由Series来决定样式
        legend.setMarkerShape(QLegend.MarkerShapeFromSeries)

        self.setChart(self._chart)
        # 设置外边界全部为0
        self._chart.layout().setContentsMargins(0, 0, 0, 0)
        # 设置内边界的bottom为0
        margins = self._chart.margins()
        self._chart.setMargins(QMargins(margins.left(), 0, margins.right(), 0))
        # 设置背景区域无圆角
        self._chart.setBackgroundRoundness(0)
Exemple #6
0
class VLineChartView(QChartView):

    bar_hovered = pyqtSignal(bool, str)

    def __init__(self, data: pd.DataFrame):
        super(VLineChartView, self).__init__()
        self._stocks = data
        self._category = self._stocks['trade_date']
        self._chart = QChart()
        self._chart.setAnimationOptions(QChart.SeriesAnimations)
        self._series = QStackedBarSeries()
        # 成交量以万股为单位
        self._vol_multiple = 10000
        self.init_chart()
        self._zero_value = (0, self._chart.axisY().min())
        self._max_value = (len(self._chart.axisX().categories()),
                           self._chart.axisY().max())
        self._zero_point = self._chart.mapToPosition(
            QPointF(self._zero_value[0], self._zero_value[1]))
        self._max_point = self._chart.mapToPosition(
            QPointF(self._max_value[0], self._max_value[1]))
        # 计算x轴单个cate的宽度,用来处理横线不能画到边界
        self._cate_width = (self._max_point.x() - self._zero_point.x()) / len(
            self._category)
        self._series.hovered.connect(self.on_series_hovered)

        x_index_list = np.percentile(range(len(self._category)),
                                     [0, 25, 50, 75, 100])
        self._x_axis_list = [
            QGraphicsSimpleTextItem(self._category[int(index)], self._chart)
            for index in x_index_list
        ]
        [axis.setText(axis.text()[4:]) for axis in self._x_axis_list[1:]]
        self._v_b = QGraphicsSimpleTextItem('B', self._chart)
        self._v_b.setZValue(100)

    def on_series_hovered(self, status, index):
        self.bar_hovered.emit(status, self._category[index])

    def clear_series_value(self):
        self._series.clear()
        self._stocks = None
        self._chart.axisY().setRange(0, 10)
        self._chart.axisX().setCategories(list())

    def add_series_values(self, data: pd.DataFrame, is_init=False):
        self._stocks = data
        bar_red = QBarSet('red')
        bar_red.setColor(Qt.red)
        bar_green = QBarSet('green')
        bar_green.setColor(Qt.green)
        for _, stock in self._stocks.iterrows():
            if stock['open'] < stock['close']:
                bar_red.append(stock['vol'] / self._vol_multiple)
                bar_green.append(0)
            else:
                bar_red.append(0)
                bar_green.append(stock['vol'] / self._vol_multiple)

        self._series.append(bar_red)
        self._series.append(bar_green)

        if not is_init:
            self._stocks = data
            self._category = self._stocks['trade_date']
            axis_x = self._chart.axisX()
            axis_y = self._chart.axisY()
            axis_x.setCategories(self._category)
            max_p = self._stocks[[
                'vol',
            ]].stack().max()
            min_p = self._stocks[[
                'vol',
            ]].stack().min()
            axis_y.setRange(min_p / self._vol_multiple * 0.9,
                            max_p / self._vol_multiple * 1.1)
            self._zero_value = (0, self._chart.axisY().min())
            self._max_value = (len(self._chart.axisX().categories()),
                               self._chart.axisY().max())
            # 计算x轴单个cate的宽度,用来处理横线不能画到边界
            self._cate_width = (self._max_point.x() -
                                self._zero_point.x()) / len(self._category)

    def resizeEvent(self, event):
        super(VLineChartView, self).resizeEvent(event)
        self._zero_point = self._chart.mapToPosition(
            QPointF(self._zero_value[0], self._zero_value[1]))
        self._max_point = self._chart.mapToPosition(
            QPointF(self._max_value[0], self._max_value[1]))
        self._cate_width = (self._max_point.x() - self._zero_point.x()) / len(
            self._category)
        # 绘制自定义X轴
        self._x_axis_list[0].setPos(self._zero_point.x() - self._cate_width,
                                    self._zero_point.y() + 10)
        self._x_axis_list[1].setPos(self._max_point.x() * 0.25,
                                    self._zero_point.y() + 10)
        self._x_axis_list[2].setPos(self._max_point.x() * 0.5,
                                    self._zero_point.y() + 10)
        self._x_axis_list[3].setPos(self._max_point.x() * 0.75,
                                    self._zero_point.y() + 10)
        self._x_axis_list[4].setPos(
            self._max_point.x() - self._x_axis_list[-1].boundingRect().width(),
            self._zero_point.y() + 10)
        # 20180207 这个日期的柱形图上面画一个字母b
        vol = self._stocks[self._stocks['trade_date'] ==
                           '20180207']['vol'] / self._vol_multiple
        print('vol:', vol, ' trade_date:', '20180207')
        pos = self._chart.mapToPosition(
            QPointF(list(self._category).index('20180207'), vol))
        pos = QPointF(pos.x() - self._cate_width / 2,
                      pos.y() - self._v_b.boundingRect().height())
        self._v_b.setPos(pos)

    def max_point(self):
        return QPointF(self._max_point.x() + self._cate_width / 2,
                       self._max_point.y())

    def min_point(self):
        return QPointF(self._zero_point.x() - self._cate_width / 2,
                       self._zero_point.y())

    def init_chart(self):
        self.add_series_values(self._stocks, True)
        self._chart.addSeries(self._series)
        self._chart.createDefaultAxes()
        self._chart.setLocalizeNumbers(True)
        axis_x = self._chart.axisX()
        axis_y = self._chart.axisY()
        axis_x.setGridLineVisible(False)
        axis_y.setGridLineVisible(False)
        axis_y.setLabelFormat("%.2f")
        axis_x.setCategories(self._category)
        axis_x.setLabelsVisible(False)
        max_p = self._stocks[[
            'vol',
        ]].stack().max()
        min_p = self._stocks[[
            'vol',
        ]].stack().min()
        axis_y.setRange(min_p / self._vol_multiple * 0.9,
                        max_p / self._vol_multiple * 1.1)

        # chart的图例
        legend = self._chart.legend()
        legend.hide()
        # 设置图例由Series来决定样式
        # legend.setMarkerShape(QLegend.MarkerShapeFromSeries)

        self.setChart(self._chart)
        self._chart.layout().setContentsMargins(0, 0, 0, 0)
        # 设置内边界的bottom为0
        # margins = self._chart.margins()
        # self._chart.setMargins(QMargins(margins.left(), 0, margins.right(), 0))
        self._chart.setBackgroundRoundness(0)
Exemple #7
0
class KLineChartView(QChartView):
    def __init__(self):
        super(KLineChartView, self).__init__()
        self.setRenderHint(QPainter.Antialiasing)  # 抗锯齿
        self._chart = QChart(title='蜡烛图悬浮提示')
        self.stocks = read_tick_data()
        self.category = [
            trade_date[4:] for trade_date in self.stocks['trade_date']
        ]
        self._count = len(self.category)
        self.resize(800, 300)

        self.init_chart()

        self.toolTipWidget = GraphicsProxyWidget(self._chart)

        # 鼠标跟踪的十字线
        self.lineItem_h = QGraphicsLineItem(self._chart)
        self.lineItem_v = QGraphicsLineItem(self._chart)
        pen = QPen()
        pen.setStyle(Qt.DotLine)
        pen.setColor(QColor(Qt.gray))
        pen.setWidth(2)
        self.lineItem_h.setPen(pen)
        self.lineItem_v.setPen(pen)
        self.lineItem_h.setZValue(100)
        self.lineItem_v.setZValue(100)
        self.lineItem_h.hide()
        self.lineItem_v.hide()

        # 坐标轴上最大最小的值
        # x 轴是
        self.min_x, self.max_x = 0, len(self._chart.axisX().categories())
        self.min_y, self.max_y = self._chart.axisY().min(), self._chart.axisY(
        ).max()
        # y 轴最高点坐标
        self.point_y_max = self._chart.mapToPosition(
            QPointF(self.min_x, self.max_y))
        # x 轴最高点坐标
        self.point_x_max = self._chart.mapToPosition(
            QPointF(self.max_x, self.min_y))
        # self.point_x_min = self._chart.mapToPosition(QPointF(self.min_x, self.min_y))

        # 计算x轴单个cate的宽度,用来处理横线不能画到边界
        self.x_width = (self.point_x_max.x() - self.point_y_max.x()) / len(
            self.category)
        self.x_x_min = self.point_y_max.x() - self.x_width / 2
        self.x_x_max = self.point_x_max.x() - self.x_width / 2

        # 中间位置,用来判断TipWidget放在哪里
        mid_date = self.stocks['trade_date'].iloc[len(
            self.stocks['trade_date']) // 2]
        self.mid_x = float(
            time.mktime(
                datetime.datetime.strptime(str(mid_date),
                                           '%Y%m%d').timetuple()))
        self.left_pos = self.point_y_max
        self.right_pos = self._chart.mapToPosition(
            QPointF(self.max_x, self.max_y))

    def mouseMoveEvent(self, event):
        super(KLineChartView, self).mouseMoveEvent(event)
        pos = event.pos()
        if self.x_x_min < pos.x() < self.x_x_max \
                and self.point_x_max.y() > pos.y() > self.point_y_max.y():
            self.lineItem_h.setLine(self.x_x_min, pos.y(), self.x_x_max,
                                    pos.y())
            self.lineItem_v.setLine(pos.x(), self.point_y_max.y(), pos.x(),
                                    self.point_x_max.y())
            self.lineItem_h.show()
            self.lineItem_v.show()
        else:
            self.lineItem_h.hide()
            self.lineItem_v.hide()

    def resizeEvent(self, event):
        super(KLineChartView, self).resizeEvent(event)
        # y 轴最高点坐标
        self.point_y_max = self._chart.mapToPosition(
            QPointF(self.min_x, self.max_y))
        # x 轴最高点坐标
        self.point_x_max = self._chart.mapToPosition(
            QPointF(self.max_x, self.min_y))
        # 计算x轴单个cate的宽度,用来处理横线不能画到边界
        self.x_width = (self.point_x_max.x() - self.point_y_max.x()) / len(
            self.category)
        self.x_x_min = self.point_y_max.x() - self.x_width / 2
        self.x_x_max = self.point_x_max.x() - self.x_width / 2
        self.left_pos = self.point_y_max
        self.right_pos = self._chart.mapToPosition(
            QPointF(self.max_x, self.max_y))

    def init_chart(self):
        self._chart.setAnimationOptions(QChart.SeriesAnimations)
        series = QCandlestickSeries()
        series.setIncreasingColor(QColor(Qt.red))
        series.setDecreasingColor(QColor(Qt.green))
        series.setName(self.stocks['name'].iloc[0])
        for _, stock in self.stocks.iterrows():
            time_p = datetime.datetime.strptime(stock['trade_date'], '%Y%m%d')
            time_p = float(time.mktime(time_p.timetuple()))
            _set = QCandlestickSet(float(stock['open']), float(stock['high']),
                                   float(stock['low']), float(stock['close']),
                                   time_p, series)
            _set.hovered.connect(self.handleBarHoverd)  # 鼠标悬停
            series.append(_set)
        self._chart.addSeries(series)

        self._chart.createDefaultAxes()
        self._chart.setLocalizeNumbers(True)
        axis_x = self._chart.axisX()
        axis_y = self._chart.axisY()
        axis_x.setGridLineVisible(False)
        axis_y.setGridLineVisible(False)
        axis_x.setCategories(self.category)
        max_p = self.stocks[['high', 'low']].stack().max() + 10
        min_p = self.stocks[['high', 'low']].stack().min() - 10
        axis_y.setRange(min_p, max_p)

        # chart的图例
        legend = self._chart.legend()
        # 设置图例由Series来决定样式
        legend.setMarkerShape(QLegend.MarkerShapeFromSeries)

        self.setChart(self._chart)
        # 设置外边界全部为0
        self._chart.layout().setContentsMargins(0, 0, 0, 0)
        # 设置内边界都为0
        # self._chart.setMargins(QMargins(0, 0, 0, 0))
        # 设置背景区域无圆角
        self._chart.setBackgroundRoundness(0)

    def handleBarHoverd(self, status):
        """ 改变画笔的风格 """
        bar = self.sender()  # 信号发送者
        pen = bar.pen()
        if not pen:
            return
        pen.setStyle(Qt.DotLine if status else Qt.SolidLine)
        bar.setPen(pen)
        if status:
            # 通过 bar 可以获取横轴坐标(timestamp)和纵轴坐标(high)
            # 然后将坐标值转换为位置,显示 TipWidget 的位置
            right_pos = QPointF(
                self.right_pos.x() - self.toolTipWidget.width() - self.x_width,
                self.right_pos.y())
            pos = self.left_pos if bar.timestamp() > self.mid_x else right_pos
            trade_date = time.strftime('%Y%m%d',
                                       time.localtime(bar.timestamp()))
            self.toolTipWidget.show(str(trade_date), str(bar.open()),
                                    str(bar.close()), str(bar.high()),
                                    str(bar.low()), pos)
        else:
            self.toolTipWidget.hide()
Exemple #8
0
class ChartView(QChartView):
    def __init__(self, *args, **kwargs):
        super(ChartView, self).__init__(*args, **kwargs)
        self.setRenderHint(QPainter.Antialiasing)  # 抗锯齿
        self._chart = QChart(title="词频图")
        # 提示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()

        self._chart.setAcceptHoverEvents(True)
        # Series动画
        self._chart.setAnimationOptions(QChart.SeriesAnimations)

    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的类型和点
        try:
            serie = self._chart.series()[0]
        except:
            return
        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, common):
        self.categories = [item[0] for item in common]
        series = QBarSeries(self._chart)
        bar = QBarSet("")
        # 随机数据
        for item in common:
            bar.append(item[1])
        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)
        # 一些固定计算,减少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))
        self.setChart(self._chart)