目录
写在前面:
本文开发的工具主要是在平时事务处理中需要查看多列数据差异很大的数据,需要横向对比纵向对比,并且要能及时感知数据的极值、平均值、中位数等数据的位置和数据的历史变迁。就想着开发一个工具可以快速展示数据,这样能加速剖析数据并得出结论,从而缩短从原始数据到最终决策的时间。
本工具的开发宗旨是讲究一个快和便捷,所以在功能的取舍上主要以实际使用为导向
结果显示
代码实现
导入包、字符串横坐标控件
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;"> ' + x_str + ' ' + 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 点击“确定”,折线图就会显示当前选定的区间