python_pyqtgraph折线图工具协助分析数据

目录

写在前面:

结果显示

 代码实现

导入包、字符串横坐标控件

单边折线图控件

主界面

使用过程


写在前面:

本文开发的工具主要是在平时事务处理中需要查看多列数据差异很大的数据,需要横向对比纵向对比,并且要能及时感知数据的极值、平均值、中位数等数据的位置和数据的历史变迁。就想着开发一个工具可以快速展示数据,这样能加速剖析数据并得出结论,从而缩短从原始数据到最终决策的时间。

本工具的开发宗旨是讲究一个快和便捷,所以在功能的取舍上主要以实际使用为导向

结果显示

 代码实现

导入包、字符串横坐标控件

import os,sys
import pandas as pd
from typing import Dict
from PyQt5 import QtCore,QtWidgets
from PyQt5.QtCore import Qt
import pyqtgraph as pg
pg.setConfigOption('background','w')
pg.setConfigOption('foreground','k')

class RotateAxisItem(pg.AxisItem):
    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
        p.setRenderHint(p.Antialiasing,False)
        p.setRenderHint(p.TextAntialiasing,True)

        ## draw long line along axis
        pen,p1,p2 = axisSpec
        p.setPen(pen)
        p.drawLine(p1,p2)
        p.translate(0.5,0)  ## resolves some damn pixel ambiguity

        ## draw ticks
        for pen,p1,p2 in tickSpecs:
            p.setPen(pen)
            p.drawLine(p1,p2)

        ## draw all text
        # if self.tickFont is not None:
        #     p.setFont(self.tickFont)
        p.setPen(self.pen())
        for rect,flags,text in textSpecs:
            # this is the important part
            p.save()
            p.translate(rect.x(),rect.y())
            p.rotate(-30)
            p.drawText(int(-rect.width()),int(rect.height()),int(rect.width()),int(rect.height()),flags,text)
            # restoring the painter is *required*!!!
            p.restore()

单边折线图控件

class GraphWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.init_data()
        self.init_ui()
    def init_data(self):
        self.whole_data: Dict = None
        self.whole_xtick: list = []
        self.whole_x: list = []
        self.color_line = (30,144,255)
        self.cur_len = 20
        # 最多20条
        self.color_map = {
            '道奇蓝':(30,144,255),
            '橙色':(255,165,0),
            '深紫罗兰色':(148,0,211),
            '春天的绿色':(60,179,113),
            '热情的粉红':(255,105,180),
            '暗淡的灰色':(105,105,105),
            '番茄':(255,99,71)
        }
        self.color_16bit_map = {
            '道奇蓝': '#1E90FF',
            '橙色': '#FFA500',
            '深紫罗兰色': '#9400D3',
            '春天的绿色': '#3CB371',
            '热情的粉红': '#FF69B4',
            '暗淡的灰色': '#696969',
            '番茄': '#FF6347'
        }
        pass
    def init_ui(self):
        self.duration_label = QtWidgets.QLabel('左边界~右边界')

        self.left_label = QtWidgets.QLabel('左边:')
        self.left_slider = QtWidgets.QSlider(Qt.Horizontal)
        self.left_slider.valueChanged.connect(self.left_slider_valueChanged)
        self.right_slider = QtWidgets.QSlider(Qt.Horizontal)
        self.right_slider.valueChanged.connect(self.right_slider_valueChanged)
        self.right_label = QtWidgets.QLabel(':右边')

        check_btn = QtWidgets.QPushButton('确定')
        check_btn.clicked.connect(self.check_btn_clicked)

        layout_top = QtWidgets.QHBoxLayout()
        layout_top.addWidget(self.duration_label)
        layout_top.addWidget(self.left_label)
        layout_top.addWidget(self.left_slider)
        layout_top.addWidget(self.right_slider)
        layout_top.addWidget(self.right_label)
        layout_top.addWidget(check_btn)
        # layout_top.addStretch(1)

        xax = RotateAxisItem(orientation='bottom')
        xax.setHeight(h=50)
        self.pw = pg.PlotWidget(axisItems={'bottom': xax})
        self.pw.setMouseEnabled(x=True, y=False)
        # self.pw.enableAutoRange(x=False,y=True)
        self.pw.setAutoVisible(x=False, y=True)

        layout = QtWidgets.QVBoxLayout()
        layout.addLayout(layout_top)
        layout.addWidget(self.pw)
        self.setLayout(layout)
        pass

    def first_setData(self,data:Dict):
        self.whole_data = data
        self.whole_x = data['x']
        self.whole_xtick = data['xTick']
        self.left_slider.setMinimum(0)
        self.left_slider.setMaximum(self.whole_x[-1])
        self.right_slider.setMinimum(0)
        self.right_slider.setMaximum(self.whole_x[-1])
        self.left_slider.setValue(0)
        self.right_slider.setValue(self.whole_x[-1])
        self.left_label.setText(f"左边:{self.whole_xtick[0]}")
        self.right_label.setText(f"{self.whole_xtick[-1]}:右边")

        self.set_data(data)
        pass

    def set_data(self,data:Dict):
        '''
        {
        x:[],
        y_list:[[],[]],
        y_names:[str,str],
        xTick00:[],
        xTick:[]
        }
        :param data:
        :return:
        '''
        self.pw.clear()
        self.pw.addLegend()

        xTick = [data['xTick00']]
        x = data['x']
        y_list = data['y_list']
        y_names = data['y_names']

        self.x_Tick = data['xTick']
        self.y_data = y_list
        self.y_names = y_names

        self.duration_label.setText(f"{self.x_Tick[0]}~{self.x_Tick[-1]}")

        xax = self.pw.getAxis('bottom')
        xax.setTicks(xTick)

        self.target_color_list = []
        color_keys_list = list(self.color_map.keys())
        for i in range(len(y_names)):
            t_i = i//len(color_keys_list)
            t_key = color_keys_list[t_i]
            self.target_color_list.append(t_key)
            self.pw.plot(x,y_list[i],connect='finite', pen=pg.mkPen({'color': self.color_map[t_key], 'width': 2}),name=y_names[i])

        self.vLine = pg.InfiniteLine(angle=90, movable=False)
        self.hLine = pg.InfiniteLine(angle=0, movable=False)
        self.label = pg.TextItem()

        self.pw.addItem(self.vLine, ignoreBounds=True)
        self.pw.addItem(self.hLine, ignoreBounds=True)
        self.pw.addItem(self.label, ignoreBounds=True)
        self.vb = self.pw.getViewBox()
        self.proxy = pg.SignalProxy(self.pw.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved)
        # 显示整条折线图
        self.pw.enableAutoRange()
        pass

    def set_empty(self):
        self.pw.clear()
        pass

    def mouseMoved(self, evt):
        pos = evt[0]
        if self.pw.sceneBoundingRect().contains(pos):
            mousePoint = self.vb.mapSceneToView(pos)
            index = int(mousePoint.x())
            if index >= 0 and index < len(self.x_Tick):
                x_str = self.x_Tick[index]

                y_str_html = ''
                for i in range(len(self.target_color_list)):
                    y_str = f"<br><font color='{self.color_16bit_map[self.target_color_list[i]]}'>{self.y_names[i]}:{self.y_data[i][index]}</font>"
                    y_str_html += y_str

                html_str = '<p style="color:black;font-size:18px;font-weight:bold;">&nbsp;' + x_str + '&nbsp;' + y_str_html + '</p>'
                self.label.setHtml(html_str)
                self.label.setPos(mousePoint.x(), mousePoint.y())
            self.vLine.setPos(mousePoint.x())
            self.hLine.setPos(mousePoint.y())
        pass

    def left_slider_valueChanged(self):
        left_value = self.left_slider.value()
        self.left_label.setText(f"左边:{self.whole_xtick[left_value]}")
        pass
    def right_slider_valueChanged(self):
        right_value = self.right_slider.value()
        self.right_label.setText(f"{self.whole_xtick[right_value]}:右边")

    def check_btn_clicked(self):
        left_value = self.left_slider.value()
        right_value = self.right_slider.value()

        if right_value<=left_value:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '左边界不能大于有边界',
                QtWidgets.QMessageBox.Yes
            )
            return
        xTick = self.whole_data['xTick'][left_value:right_value]
        xTick00 = []
        dur_num = int(len(xTick) / float(self.cur_len))
        if dur_num >= 2:
            for i in range(0, len(xTick), dur_num):
                xTick00.append((i, xTick[i]))
        else:
            for i in range(0, len(xTick)):
                xTick00.append((i, xTick[i]))
        y_list00 = []
        y_list = self.whole_data['y_list']
        for item in y_list:
            item00 = item[left_value:right_value]
            y_list00.append(item00)
        x = [i for i in range(len(xTick))]
        line_data = {
            'xTick00': xTick00,
            'xTick': xTick,
            'x': x,
            'y_list': y_list00,
            'y_names': self.whole_data['y_names']
        }
        self.set_data(line_data)
        pass
    pass

主界面

class LineMainWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.init_data()
        self.init_ui()
        pass
    def init_data(self):
        self.please_selected_str:str = '-- 请选择 --'
        self.field_list: list = []
        self.x_field: str = ''
        self.current_filename:str = ''
        self.whole_df: pd.DataFrame = None
        self.cur_len: int = 20
        pass
    def init_ui(self):
        self.setWindowTitle('数据折线图展示')

        tip_label1 = QtWidgets.QLabel('横坐标字段:')
        self.x_lineedit = QtWidgets.QLineEdit()

        self.file_name_label = QtWidgets.QLabel('文件名')
        self.file_name_label.setWordWrap(True)
        open_file_btn = QtWidgets.QPushButton('打开excel或csv文件')
        open_file_btn.clicked.connect(self.open_file_btn_clicked)

        tip_label = QtWidgets.QLabel('表头下拉列表:')
        self.head_combox = QtWidgets.QComboBox()
        self.head_combox.addItem(self.please_selected_str)
        self.head_combox.currentTextChanged.connect(self.head_combox_currentTextChanged)

        self.list_widget = QtWidgets.QListWidget()
        check_btn = QtWidgets.QPushButton('确定')
        check_btn.clicked.connect(self.check_btn_clicked)
        clear_btn = QtWidgets.QPushButton('清空')
        clear_btn.clicked.connect(self.clear_btn_clicked)

        layout_left = QtWidgets.QVBoxLayout()
        layout_left.addWidget(tip_label1)
        layout_left.addWidget(self.x_lineedit)
        layout_left.addWidget(self.file_name_label)
        layout_left.addWidget(open_file_btn)
        layout_left.addWidget(tip_label)
        layout_left.addWidget(self.head_combox)
        layout_left.addWidget(self.list_widget)
        layout_left.addWidget(check_btn)
        layout_left.addWidget(clear_btn)
        layout_left.addStretch(1)

        self.title_label = QtWidgets.QLabel('折线图标题')
        self.title_label.setAlignment(QtCore.Qt.AlignCenter)
        self.title_label.setStyleSheet('QLabel{font-size:16px;font-weight:bold}')

        self.line_widget = GraphWidget()

        layout_right = QtWidgets.QVBoxLayout()
        layout_right.addWidget(self.title_label)
        layout_right.addWidget(self.line_widget)

        layout = QtWidgets.QHBoxLayout()
        layout.addLayout(layout_left,1)
        layout.addLayout(layout_right,9)

        self.setLayout(layout)
        pass
    def open_file_btn_clicked(self):
        x_str = self.x_lineedit.text()
        x_str = x_str.strip()
        if len(x_str)<=0:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '请先输入横坐标字段名',
                QtWidgets.QMessageBox.Yes
            )
            return
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            '打开Excel文件或csv文件',
            '.',
            'Excel或CSV(*.xlsx *.csv)'
        )
        if not path:
            return
        if path.endswith('.xlsx'):
            df = pd.read_excel(path,engine='openpyxl')
            pass
        elif path.endswith('.csv'):
            df = pd.read_csv(path,encoding='utf-8')
            pass
        else:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '只能上传Excel文件或CSV文件',
                QtWidgets.QMessageBox.Yes
            )
            return
        self.file_name_label.setText(path)
        self.field_list.clear()
        cols = df.columns
        if x_str not in cols:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '横坐标字段不在文件中',
                QtWidgets.QMessageBox.Yes
            )
            return
        for col in cols:
            if str(df[col].dtype)=='object':
                continue
            self.field_list.append(col)
        self.x_field = x_str
        self.current_filename = os.path.basename(path)
        self.whole_df = df.copy()

        self.head_combox.clear()
        self.head_combox.addItem(self.please_selected_str)
        self.head_combox.addItems(self.field_list)
        pass
    def head_combox_currentTextChanged(self,txt):
        cur_txt = self.head_combox.currentText()
        if len(cur_txt.strip())<=0:
            return
        if cur_txt == self.please_selected_str:
            return
        self.list_widget.addItem(cur_txt)
        pass
    def check_btn_clicked(self):
        total_count = self.list_widget.count()
        if total_count > 20 or total_count<=0:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '选择的字段在1个到20个之间',
                QtWidgets.QMessageBox.Yes
            )
            return

        selected_list = []
        for i in range(total_count):
            item = self.list_widget.item(i)
            selected_list.append(item.text())

        df = self.whole_df.copy()
        xTick = df[self.x_field].values.tolist()
        xTick00 = []
        dur_num = int(len(xTick) / float(self.cur_len))
        if dur_num >= 2:
            for i in range(0, len(xTick), dur_num):
                xTick00.append((i, xTick[i]))
        else:
            for i in range(0, len(xTick)):
                xTick00.append((i, xTick[i]))

        y_list = []
        for item in selected_list:
            y_one = df[item].values.tolist()
            y_list.append(y_one)

        if total_count<=1:
            title_str = f"{self.current_filename}_{selected_list[0]}"
        else:
            title_str = f"{self.current_filename}_多列"
        line_data = {
            'xTick00': xTick00,
            'xTick': xTick,
            'x': [i for i in range(0, len(df))],
            'y_list': y_list,
            'y_names': selected_list
        }
        self.title_label.setText(title_str)
        self.line_widget.first_setData(line_data)

        pass
    def clear_btn_clicked(self):
        self.list_widget.clear()

使用过程

if __name__ == '__main__':
    QtCore.QCoreApplication.setAttribute(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    app = QtWidgets.QApplication(sys.argv)
    main_window = LineMainWidget()
    main_window.showMaximized()
    app.exec()
    pass

1 输入作为横坐标的字段

2 选择要显示的文件

3 选择要显示的字段,可以选择多个

4 点击“确定”后,右侧就会画出折线图

5 移动滑块,可以改变横坐标区间

6 点击“确定”,折线图就会显示当前选定的区间 

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值