BUGFIX: VFT;
change to src layout
This commit is contained in:
0
src/gui_qt/graphs/__init__.py
Normal file
0
src/gui_qt/graphs/__init__.py
Normal file
715
src/gui_qt/graphs/graphwindow.py
Normal file
715
src/gui_qt/graphs/graphwindow.py
Normal file
@ -0,0 +1,715 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from math import isnan
|
||||
|
||||
from numpy import errstate, floor, log10
|
||||
from pyqtgraph import GraphicsObject, getConfigOption, mkColor
|
||||
|
||||
from nmreval.utils.text import convert
|
||||
|
||||
from ..lib.pg_objects import LegendItemBlock, RegionItem
|
||||
from ..Qt import QtCore, QtWidgets, QtGui
|
||||
from .._py.graph import Ui_GraphWindow
|
||||
from ..lib import make_action_icons
|
||||
from ..lib.configurations import GraceMsgBox
|
||||
|
||||
|
||||
class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow):
|
||||
mousePositionChanged = QtCore.pyqtSignal(float, float)
|
||||
mouseDoubleClicked = QtCore.pyqtSignal()
|
||||
positionClicked = QtCore.pyqtSignal(tuple, bool)
|
||||
aboutToClose = QtCore.pyqtSignal(str)
|
||||
|
||||
counter = itertools.count()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._bgcolor = mkColor(getConfigOption('background'))
|
||||
self._fgcolor = mkColor(getConfigOption('foreground'))
|
||||
self._prev_colors = mkColor('k'), mkColor('w')
|
||||
|
||||
self._init_gui()
|
||||
|
||||
make_action_icons(self)
|
||||
|
||||
self.id = str(uuid.uuid4())
|
||||
|
||||
self.sets = []
|
||||
self.active = []
|
||||
|
||||
self.real_plots = {}
|
||||
self.imag_plots = {}
|
||||
self.error_plots = {}
|
||||
|
||||
self._special_needs = []
|
||||
self.closable = True
|
||||
|
||||
self.log = [False, False]
|
||||
|
||||
self.scene = self.plotItem.scene()
|
||||
self.scene.sigMouseMoved.connect(self.move_mouse)
|
||||
|
||||
self.checkBox.stateChanged.connect(lambda x: self.legend.setVisible(x == QtCore.Qt.Checked))
|
||||
self.label_button.toggled.connect(lambda x: self.label_widget.setVisible(x))
|
||||
self.limit_button.toggled.connect(lambda x: self.limit_widget.setVisible(x))
|
||||
self.gridbutton.toggled.connect(lambda x: self.graphic.showGrid(x=x, y=x))
|
||||
self.logx_button.toggled.connect(lambda x: self.set_logmode(xmode=x))
|
||||
self.logy_button.toggled.connect(lambda x: self.set_logmode(ymode=x))
|
||||
self.graphic.plotItem.vb.sigRangeChanged.connect(self.update_limits)
|
||||
self.listWidget.itemChanged.connect(self.show_legend)
|
||||
|
||||
# reconnect "Export..." in context menu to our function
|
||||
self.scene.contextMenu[0].disconnect()
|
||||
self.scene.contextMenu[0].triggered.connect(self.export_dialog)
|
||||
|
||||
def _init_gui(self):
|
||||
self.setWindowTitle('Graph ' + str(next(QGraphWindow.counter)))
|
||||
|
||||
self.label_widget.hide()
|
||||
self.limit_widget.hide()
|
||||
self.listWidget.hide()
|
||||
self.checkBox.hide()
|
||||
|
||||
self.plotItem = self.graphic.plotItem
|
||||
for orient in ['top', 'bottom', 'left', 'right']:
|
||||
self.plotItem.showAxis(orient)
|
||||
ax = self.plotItem.getAxis(orient)
|
||||
ax.enableAutoSIPrefix(False)
|
||||
if orient == 'top':
|
||||
ax.setStyle(showValues=False)
|
||||
ax.setHeight(10)
|
||||
elif orient == 'right':
|
||||
ax.setStyle(showValues=False)
|
||||
ax.setWidth(10)
|
||||
|
||||
self.legend = LegendItemBlock(offset=(20, 20))
|
||||
self.legend.setParentItem(self.plotItem.vb)
|
||||
self.plotItem.legend = self.legend
|
||||
self.legend.setVisible(True)
|
||||
|
||||
self.plotItem.setMenuEnabled(False, True)
|
||||
self.plotItem.ctrl.logXCheck.blockSignals(True)
|
||||
self.plotItem.ctrl.logYCheck.blockSignals(True)
|
||||
|
||||
for lineedit in [self.xmin_lineedit, self.xmax_lineedit, self.ymin_lineedit, self.ymax_lineedit]:
|
||||
lineedit.setValidator(QtGui.QDoubleValidator())
|
||||
|
||||
def __contains__(self, item: str):
|
||||
return item in self.sets
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.active)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.active)
|
||||
|
||||
def curves(self):
|
||||
for a in self.active:
|
||||
if self.real_button.isChecked():
|
||||
if self.error_plots[a] is not None:
|
||||
yield self.real_plots[a], self.error_plots[a]
|
||||
else:
|
||||
yield self.real_plots[a],
|
||||
|
||||
if self.imag_button.isChecked() and self.imag_plots[a] is not None:
|
||||
yield self.imag_plots[a],
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.windowTitle()
|
||||
|
||||
@title.setter
|
||||
def title(self, value):
|
||||
self.setWindowTitle(str(value))
|
||||
|
||||
@property
|
||||
def ranges(self) -> tuple:
|
||||
r = self.plotItem.getViewBox().viewRange()
|
||||
for i in [0, 1]:
|
||||
if self.log[i]:
|
||||
r[i] = tuple([10**x for x in r[i]])
|
||||
else:
|
||||
r[i] = tuple(r[i])
|
||||
|
||||
return tuple(r)
|
||||
|
||||
def add(self, name: str | list, plots: list):
|
||||
if isinstance(name, str):
|
||||
name = [name]
|
||||
plots = [plots]
|
||||
|
||||
for (real_plot, imag_plot, err_plot), n in zip(plots, name):
|
||||
toplevel = len(self.sets)
|
||||
self.sets.append(n)
|
||||
|
||||
if real_plot:
|
||||
real_plot.setZValue(2*toplevel+1)
|
||||
if imag_plot:
|
||||
imag_plot.setZValue(2*toplevel+1)
|
||||
if err_plot:
|
||||
err_plot.setZValue(2*toplevel)
|
||||
|
||||
self.real_plots[n] = real_plot
|
||||
self.imag_plots[n] = imag_plot
|
||||
self.error_plots[n] = err_plot
|
||||
|
||||
list_item = QtWidgets.QListWidgetItem(real_plot.opts.get('name', ''))
|
||||
list_item.setData(QtCore.Qt.UserRole, n)
|
||||
list_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable)
|
||||
list_item.setCheckState(QtCore.Qt.Checked)
|
||||
self.listWidget.addItem(list_item)
|
||||
|
||||
self.show_item(name)
|
||||
|
||||
def remove(self, name: str | list):
|
||||
if isinstance(name, str):
|
||||
name = [name]
|
||||
|
||||
for n in name:
|
||||
self.sets.remove(n)
|
||||
|
||||
for plot in [self.real_plots, self.imag_plots, self.error_plots]:
|
||||
self.graphic.removeItem(plot[n])
|
||||
|
||||
if n in self.active:
|
||||
self.active.remove(n)
|
||||
|
||||
# remove from label list
|
||||
self.listWidget.blockSignals(True)
|
||||
|
||||
for i in range(self.listWidget.count()-1, 0, -1):
|
||||
item = self.listWidget.item(i)
|
||||
if item.data(QtCore.Qt.UserRole) in name:
|
||||
self.listWidget.takeItem(i)
|
||||
|
||||
self.listWidget.blockSignals(False)
|
||||
|
||||
self._update_zorder()
|
||||
self.show_legend()
|
||||
|
||||
def move_sets(self, sets: list, position: int):
|
||||
move_plots = []
|
||||
move_items = []
|
||||
|
||||
self.listWidget.blockSignals(True)
|
||||
|
||||
for s in sets:
|
||||
idx = self.sets.index(s)
|
||||
move_plots.append(self.sets.pop(idx))
|
||||
move_items.append(self.listWidget.takeItem(idx))
|
||||
|
||||
if position == -1:
|
||||
self.sets.extend(move_plots)
|
||||
for it in move_items:
|
||||
self.listWidget.addItem(it)
|
||||
else:
|
||||
self.sets = self.sets[:position] + move_plots + self.sets[position:]
|
||||
for it in move_items[::-1]:
|
||||
self.listWidget.insertItem(position, it)
|
||||
|
||||
self.listWidget.blockSignals(False)
|
||||
self._update_zorder()
|
||||
|
||||
def show_item(self, idlist: list):
|
||||
if len(self.sets) == 0:
|
||||
return
|
||||
|
||||
for a in idlist:
|
||||
if a not in self.active:
|
||||
self.active.append(a)
|
||||
|
||||
for (bttn, plot_dic) in [
|
||||
(self.real_button, self.real_plots),
|
||||
(self.imag_button, self.imag_plots),
|
||||
(self.error_button, self.error_plots),
|
||||
]:
|
||||
if bttn.isChecked():
|
||||
item = plot_dic[a]
|
||||
if (item is not None) and (item not in self.graphic.items()):
|
||||
self.graphic.addItem(item)
|
||||
|
||||
self.show_legend()
|
||||
|
||||
def hide_item(self, idlist: list):
|
||||
if len(self.sets) == 0:
|
||||
return
|
||||
|
||||
for r in idlist:
|
||||
if r in self.active:
|
||||
self.active.remove(r)
|
||||
|
||||
for plt in [self.real_plots, self.imag_plots, self.error_plots]:
|
||||
item = plt[r]
|
||||
if item in self.graphic.items():
|
||||
self.graphic.removeItem(item)
|
||||
|
||||
@QtCore.pyqtSlot(bool, name='on_imag_button_toggled')
|
||||
@QtCore.pyqtSlot(bool, name='on_real_button_toggled')
|
||||
def set_imag_visible(self, visible: bool):
|
||||
if self.sender() == self.real_button:
|
||||
plots = self.real_plots
|
||||
if self.error_button.isChecked() and not visible:
|
||||
self.error_button.setChecked(False)
|
||||
else:
|
||||
plots = self.imag_plots
|
||||
|
||||
if visible:
|
||||
func = self.graphic.addItem
|
||||
else:
|
||||
func = self.graphic.removeItem
|
||||
|
||||
for a in self.active:
|
||||
item = plots[a]
|
||||
if item is not None:
|
||||
func(item)
|
||||
|
||||
self.show_legend()
|
||||
|
||||
@QtCore.pyqtSlot(bool, name='on_error_button_toggled')
|
||||
def show_errorbar(self, visible: bool):
|
||||
if visible and not self.real_button.isChecked():
|
||||
# no errorbars without points
|
||||
self.error_button.blockSignals(True)
|
||||
self.error_button.setChecked(False)
|
||||
self.error_button.blockSignals(False)
|
||||
return
|
||||
|
||||
if visible:
|
||||
for a in self.active:
|
||||
item = self.error_plots[a]
|
||||
if (item is not None) and (item not in self.graphic.items()):
|
||||
self.graphic.addItem(item)
|
||||
else:
|
||||
for a in self.active:
|
||||
item = self.error_plots[a]
|
||||
if (item is not None) and (item in self.graphic.items()):
|
||||
self.graphic.removeItem(item)
|
||||
|
||||
def add_external(self, item):
|
||||
if isinstance(item, RegionItem) and item.first:
|
||||
# Give regions nice values on first addition to a graph
|
||||
x, _ = self.ranges
|
||||
|
||||
if item.mode == 'mid':
|
||||
onset = item.getRegion()[0]
|
||||
if self.log[0]:
|
||||
delta = log10(x[1]/x[0])/20
|
||||
span = (onset / 10**delta , onset * 10**delta)
|
||||
else:
|
||||
delta = x[1]-x[0]
|
||||
span = (onset-delta/20, onset + delta/20)
|
||||
elif item.mode == 'half':
|
||||
span = (0.75*x[0]+0.25*x[1], 0.25*x[0]+0.75*x[1])
|
||||
else:
|
||||
span = item.getRegion()
|
||||
|
||||
item.setRegion(span)
|
||||
item.first = False
|
||||
|
||||
if item in self.graphic.items():
|
||||
return False
|
||||
|
||||
if not hasattr(item, 'setLogMode'):
|
||||
self._special_needs.append(item)
|
||||
|
||||
self.graphic.addItem(item)
|
||||
item.setZValue(1000)
|
||||
|
||||
return True
|
||||
|
||||
@QtCore.pyqtSlot(GraphicsObject)
|
||||
def remove_external(self, item):
|
||||
if item not in self.graphic.items():
|
||||
return False
|
||||
|
||||
if item in self._special_needs:
|
||||
self._special_needs.remove(item)
|
||||
|
||||
self.graphic.removeItem(item)
|
||||
|
||||
return True
|
||||
|
||||
def closeEvent(self, evt: QtGui.QCloseEvent):
|
||||
if not self.closable:
|
||||
evt.ignore()
|
||||
return
|
||||
|
||||
res = QtWidgets.QMessageBox.Yes
|
||||
if len(self.sets) != 0:
|
||||
res = QtWidgets.QMessageBox.question(self, 'Plot not empty', 'Graph is not empty. Deleting with all data?',
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
|
||||
if res == QtWidgets.QMessageBox.Yes:
|
||||
self.aboutToClose.emit(self.id)
|
||||
evt.accept()
|
||||
else:
|
||||
evt.ignore()
|
||||
|
||||
def move_mouse(self, evt):
|
||||
vb = self.plotItem.getViewBox()
|
||||
if self.plotItem.sceneBoundingRect().contains(evt):
|
||||
pos = vb.mapSceneToView(evt)
|
||||
if self.log[0]:
|
||||
try:
|
||||
_x = 10**(pos.x())
|
||||
except OverflowError:
|
||||
_x = pos.x()
|
||||
else:
|
||||
_x = pos.x()
|
||||
|
||||
if self.log[1]:
|
||||
try:
|
||||
_y = 10**(pos.y())
|
||||
except OverflowError:
|
||||
_y = pos.y()
|
||||
else:
|
||||
_y = pos.y()
|
||||
self.mousePositionChanged.emit(_x, _y)
|
||||
|
||||
@QtCore.pyqtSlot(name='on_title_lineedit_returnPressed')
|
||||
@QtCore.pyqtSlot(name='on_xaxis_linedit_returnPressed')
|
||||
@QtCore.pyqtSlot(name='on_yaxis_linedit_returnPressed')
|
||||
def labels_changed(self):
|
||||
label = {self.title_lineedit: 'title', self.xaxis_linedit: 'x', self.yaxis_linedit: 'y'}[self.sender()]
|
||||
self.set_label(**{label: self.sender().text()})
|
||||
|
||||
def set_label(self, x=None, y=None, title=None):
|
||||
if title is not None:
|
||||
self.plotItem.setTitle(convert(title, old='tex', new='html'), **{'size': '10pt', 'color': self._fgcolor})
|
||||
|
||||
if x is not None:
|
||||
self.plotItem.setLabel('bottom', convert(x, old='tex', new='html'),
|
||||
**{'font-size': '10pt', 'color': self._fgcolor.name()})
|
||||
|
||||
if y is not None:
|
||||
self.plotItem.setLabel('left', convert(y, old='tex', new='html'),
|
||||
**{'font-size': '10pt', 'color': self._fgcolor.name()})
|
||||
|
||||
def set_logmode(self, xmode: bool = None, ymode: bool = None):
|
||||
r = self.ranges
|
||||
|
||||
if xmode is None:
|
||||
xmode = self.plotItem.ctrl.logXCheck.isChecked()
|
||||
else:
|
||||
self.plotItem.ctrl.logXCheck.setCheckState(xmode)
|
||||
|
||||
if ymode is None:
|
||||
ymode = self.plotItem.ctrl.logYCheck.isChecked()
|
||||
else:
|
||||
self.plotItem.ctrl.logYCheck.setCheckState(ymode)
|
||||
|
||||
self.log = [xmode, ymode]
|
||||
|
||||
for item in self._special_needs:
|
||||
item.logmode[0] = self.log[:]
|
||||
|
||||
self.plotItem.updateLogMode()
|
||||
|
||||
self.set_range(x=r[0], y=r[1])
|
||||
|
||||
def enable_picking(self, enabled: bool):
|
||||
if enabled:
|
||||
self.scene.sigMouseClicked.connect(self.position_picked)
|
||||
else:
|
||||
try:
|
||||
self.scene.sigMouseClicked.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def position_picked(self, evt):
|
||||
vb = self.graphic.plotItem.vb
|
||||
|
||||
if self.graphic.plotItem.sceneBoundingRect().contains(evt.scenePos()) and evt.button() == 1:
|
||||
pos = vb.mapSceneToView(evt.scenePos())
|
||||
_x, _y = pos.x(), pos.y()
|
||||
|
||||
if self.log[0]:
|
||||
_x = 10**_x
|
||||
|
||||
if self.log[1]:
|
||||
_y = 10**_y
|
||||
|
||||
self.positionClicked.emit((_x, _y), evt.double())
|
||||
|
||||
@QtCore.pyqtSlot(name='on_apply_button_clicked')
|
||||
def set_range(self, x: tuple = None, y: tuple = None):
|
||||
if x is None:
|
||||
x = float(self.xmin_lineedit.text()), float(self.xmax_lineedit.text())
|
||||
x = min(x), max(x)
|
||||
|
||||
if y is None:
|
||||
y = float(self.ymin_lineedit.text()), float(self.ymax_lineedit.text())
|
||||
y = min(y), max(y)
|
||||
|
||||
for log, xy, func in zip(self.log, (x, y), (self.graphic.setXRange, self.graphic.setYRange)):
|
||||
if log:
|
||||
with errstate(all='ignore'):
|
||||
xy = [log10(val) for val in xy]
|
||||
|
||||
if isnan(xy[1]):
|
||||
xy = [-1, 1]
|
||||
elif isnan(xy[0]):
|
||||
xy[0] = xy[1]-4
|
||||
|
||||
func(xy[0], xy[1], padding=0)
|
||||
|
||||
@QtCore.pyqtSlot(object)
|
||||
def update_limits(self, _):
|
||||
r = self.ranges
|
||||
self.xmin_lineedit.setText('%.5g' % r[0][0])
|
||||
self.xmax_lineedit.setText('%.5g' % r[0][1])
|
||||
|
||||
self.ymin_lineedit.setText('%.5g' % r[1][0])
|
||||
self.ymax_lineedit.setText('%.5g' % r[1][1])
|
||||
|
||||
def _update_zorder(self):
|
||||
for i, sid in enumerate(self.sets):
|
||||
plt = self.real_plots[sid]
|
||||
if plt.zValue() != 2*i+1:
|
||||
plt.setZValue(2*i+1)
|
||||
if self.imag_plots[sid] is not None:
|
||||
self.imag_plots[sid].setZValue(2*i+1)
|
||||
if self.error_plots[sid] is not None:
|
||||
self.error_plots[sid].setZValue(2*i)
|
||||
|
||||
self.show_legend()
|
||||
|
||||
@QtCore.pyqtSlot(bool, name='on_legend_button_toggled')
|
||||
def show_legend_item_list(self, visible: bool):
|
||||
self.listWidget.setVisible(visible)
|
||||
self.checkBox.setVisible(visible)
|
||||
|
||||
def update_legend(self, sid, name):
|
||||
self.listWidget.blockSignals(True)
|
||||
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
if item.data(QtCore.Qt.UserRole) == sid:
|
||||
item.setText(convert(name, old='tex', new='html'))
|
||||
|
||||
self.listWidget.blockSignals(False)
|
||||
self.show_legend()
|
||||
|
||||
def show_legend(self):
|
||||
if not self.legend.isVisible():
|
||||
return
|
||||
|
||||
self.legend.clear()
|
||||
|
||||
for i, sid in enumerate(self.sets):
|
||||
item = self.real_plots[sid]
|
||||
other_item = self.imag_plots[sid]
|
||||
# should legend be visible? is either real part or imaginary part shown?
|
||||
if self.listWidget.item(i).checkState():
|
||||
if item in self.graphic.items():
|
||||
self.legend.addItem(item, convert(item.opts.get('name', ''), old='tex', new='html'))
|
||||
elif other_item in self.graphic.items():
|
||||
self.legend.addItem(other_item, convert(other_item.opts.get('name', ''), old='tex', new='html'))
|
||||
|
||||
def export_dialog(self):
|
||||
filters = 'All files (*.*);;AGR (*.agr);;SVG (*.svg);;PDF (*.pdf)'
|
||||
for imgformat in QtGui.QImageWriter.supportedImageFormats():
|
||||
str_format = imgformat.data().decode('utf-8')
|
||||
filters += ';;' + str_format.upper() + ' (*.' + str_format + ')'
|
||||
|
||||
outfile, _ = QtWidgets.QFileDialog.getSaveFileName(self, caption='Export graphic', filter=filters,
|
||||
options=QtWidgets.QFileDialog.DontConfirmOverwrite)
|
||||
if outfile:
|
||||
self.export(outfile)
|
||||
|
||||
def export(self, outfile: str):
|
||||
_, suffix = os.path.splitext(outfile)
|
||||
if suffix == '':
|
||||
QtWidgets.QMessageBox.warning(self, 'No file extension',
|
||||
'No file extension found, graphic was not saved.')
|
||||
return
|
||||
|
||||
if suffix == '.agr':
|
||||
res = 0
|
||||
if os.path.exists(outfile):
|
||||
res = GraceMsgBox(outfile, parent=self).exec()
|
||||
if res == -1:
|
||||
return
|
||||
|
||||
opts = self.export_graphics()
|
||||
|
||||
from ..io.exporters import GraceExporter
|
||||
if res == 0:
|
||||
mode = 'w'
|
||||
elif res == 1:
|
||||
mode = 'a'
|
||||
else:
|
||||
mode = res-2
|
||||
|
||||
GraceExporter(opts).export(outfile, mode=mode)
|
||||
|
||||
else:
|
||||
if os.path.exists(outfile):
|
||||
if QtWidgets.QMessageBox.warning(self, 'Export graphic',
|
||||
f'{os.path.split(outfile)[1]} already exists.\n'
|
||||
f'Do you REALLY want to replace it?',
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
||||
QtWidgets.QMessageBox.No) == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
bg_color = self._bgcolor
|
||||
fg_color = self._fgcolor
|
||||
self.set_color(foreground='k', background='w')
|
||||
|
||||
if suffix == '.pdf':
|
||||
from ..io.exporters import PDFPrintExporter
|
||||
PDFPrintExporter(self.graphic).export(outfile)
|
||||
|
||||
elif suffix == '.svg':
|
||||
from pyqtgraph.exporters import SVGExporter
|
||||
SVGExporter(self.scene).export(outfile)
|
||||
|
||||
else:
|
||||
from pyqtgraph.exporters import ImageExporter
|
||||
|
||||
ImageExporter(self.scene).export(outfile)
|
||||
|
||||
self.set_color(foreground=fg_color, background=bg_color)
|
||||
|
||||
def export_graphics(self) -> dict:
|
||||
dic = self.get_state()
|
||||
dic['items'] = []
|
||||
|
||||
in_legend = []
|
||||
|
||||
for item in self.curves():
|
||||
plot_item = item[0]
|
||||
legend_shown = False
|
||||
for sample, _ in self.legend.items:
|
||||
if sample.item is plot_item:
|
||||
legend_shown = True
|
||||
break
|
||||
in_legend.append(legend_shown)
|
||||
item_dic = plot_item.get_data_opts()
|
||||
if len(item) == 2:
|
||||
# plot can show errorbars
|
||||
item_dic['yerr'] = item[1].opts['topData']
|
||||
|
||||
if item_dic:
|
||||
dic['items'].append(item_dic)
|
||||
|
||||
dic['in_legend'] = in_legend
|
||||
|
||||
return dic
|
||||
|
||||
def get_state(self) -> dict:
|
||||
dic = {
|
||||
'id': self.id,
|
||||
'limits': (self.ranges[0], self.ranges[1]),
|
||||
'ticks': (),
|
||||
'labels': (self.plotItem.getAxis('bottom').labelText,
|
||||
self.plotItem.getAxis('left').labelText,
|
||||
self.plotItem.titleLabel.text,
|
||||
self.title),
|
||||
'log': self.log,
|
||||
'grid': self.gridbutton.isChecked(),
|
||||
'legend': self.legend.isVisible(),
|
||||
'plots': (self.real_button.isChecked(), self.imag_button.isChecked(), self.error_button.isChecked()),
|
||||
'children': self.sets,
|
||||
'active': self.active,
|
||||
}
|
||||
|
||||
in_legend = []
|
||||
for i in range(self.listWidget.count()):
|
||||
in_legend.append(bool(self.listWidget.item(i).checkState()))
|
||||
dic['in_legend'] = in_legend
|
||||
|
||||
# bottomLeft gives top left corner
|
||||
l_topleft = self.plotItem.vb.itemBoundingRect(self.legend).bottomLeft()
|
||||
legend_origin = [l_topleft.x(), l_topleft.y()]
|
||||
for i in [0, 1]:
|
||||
if self.log[i]:
|
||||
legend_origin[i] = 10**legend_origin[i]
|
||||
dic['legend_pos'] = legend_origin
|
||||
|
||||
for i, ax in enumerate(['bottom', 'left']):
|
||||
if self.log[i]:
|
||||
major = 10
|
||||
minor = 9
|
||||
else:
|
||||
vmin, vmax = dic['limits'][i][0], dic['limits'][i][1]
|
||||
dist = vmax - vmin
|
||||
scale = 10**floor(log10(abs(dist)))
|
||||
steps = [0.1, 0.2, 0.25, 0.5, 1., 2., 2.5, 5., 10., 20., 50., 100.]
|
||||
for step_i in steps:
|
||||
if dist / step_i / scale <= 10:
|
||||
break
|
||||
major = step_i * scale
|
||||
minor = 1
|
||||
|
||||
dic['ticks'] += (major, minor),
|
||||
|
||||
return dic
|
||||
|
||||
@staticmethod
|
||||
def set_state(state):
|
||||
graph = QGraphWindow()
|
||||
graph.id = state.get('id', graph.id)
|
||||
|
||||
graph.plotItem.setLabel('bottom', state['labels'][0], **{'font-size': '10pt', 'color': graph._fgcolor.name()})
|
||||
graph.plotItem.setLabel('left', state['labels'][1], **{'font-size': '10pt', 'color': graph._fgcolor.name()})
|
||||
graph.plotItem.setTitle(state['labels'][2], **{'size': '10pt', 'color': graph._fgcolor.name()})
|
||||
graph.setWindowTitle(state['labels'][3])
|
||||
|
||||
graph.graphic.showGrid(x=state['grid'], y=state['grid'])
|
||||
|
||||
graph.checkBox.setCheckState(QtCore.Qt.Checked if state['legend'] else QtCore.Qt.Unchecked)
|
||||
|
||||
graph.real_button.setChecked(state['plots'][0])
|
||||
graph.imag_button.setChecked(state['plots'][1])
|
||||
graph.error_button.setChecked(state['plots'][2])
|
||||
|
||||
graph.set_range(x=state['limits'][0], y=state['limits'][1])
|
||||
graph.logx_button.setChecked(state['log'][0])
|
||||
graph.logy_button.setChecked(state['log'][1])
|
||||
|
||||
return graph
|
||||
|
||||
def set_color(self, foreground=None, background=None):
|
||||
if background is not None:
|
||||
self._bgcolor = mkColor(background)
|
||||
self.graphic.setBackground(self._bgcolor)
|
||||
self.legend.setBrush(self._bgcolor)
|
||||
|
||||
if foreground is not None:
|
||||
self._fgcolor = mkColor(foreground)
|
||||
|
||||
for ax in ['left', 'bottom']:
|
||||
pen = self.plotItem.getAxis(ax).pen()
|
||||
pen.setColor(self._fgcolor)
|
||||
|
||||
self.plotItem.getAxis(ax).setPen(pen)
|
||||
self.plotItem.getAxis(ax).setTextPen(pen)
|
||||
|
||||
self.legend.setLabelTextColor(self._fgcolor)
|
||||
if self.legend.isVisible():
|
||||
self.show_legend()
|
||||
|
||||
title = self.plotItem.titleLabel.text
|
||||
if title is not None:
|
||||
self.plotItem.setTitle(title, **{'size': '10pt', 'color': self._fgcolor})
|
||||
|
||||
x = self.plotItem.getAxis('bottom').labelText
|
||||
if x is not None:
|
||||
self.plotItem.setLabel('bottom', x, **{'font-size': '10pt', 'color': self._fgcolor.name()})
|
||||
|
||||
y = self.plotItem.getAxis('left').labelText
|
||||
if y is not None:
|
||||
self.plotItem.setLabel('left', y, **{'font-size': '10pt', 'color': self._fgcolor.name()})
|
||||
|
||||
@QtCore.pyqtSlot(bool, name='on_bwbutton_toggled')
|
||||
def change_background(self, _):
|
||||
temp = self._fgcolor, self._bgcolor
|
||||
self.set_color(foreground=self._prev_colors[0], background=self._prev_colors[1])
|
||||
self._prev_colors = temp
|
147
src/gui_qt/graphs/guide_lines.py
Normal file
147
src/gui_qt/graphs/guide_lines.py
Normal file
@ -0,0 +1,147 @@
|
||||
from pyqtgraph import InfiniteLine
|
||||
|
||||
from ..Qt import QtWidgets, QtCore, QtGui
|
||||
from .._py.guidelinewidget import Ui_Form
|
||||
from ..lib.pg_objects import LogInfiniteLine
|
||||
|
||||
|
||||
class LineWidget(QtWidgets.QWidget, Ui_Form):
|
||||
line_created = QtCore.pyqtSignal(object, str)
|
||||
line_deleted = QtCore.pyqtSignal(object, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.lines = {}
|
||||
self.comments = {}
|
||||
|
||||
self.vh_pos_lineEdit.setValidator(QtGui.QDoubleValidator())
|
||||
|
||||
self.tableWidget.installEventFilter(self)
|
||||
|
||||
@QtCore.pyqtSlot(name='on_pushButton_clicked')
|
||||
def make_line(self):
|
||||
invalid = True
|
||||
|
||||
idx = self.mode_comboBox.currentIndex()
|
||||
try:
|
||||
pos = float(self.vh_pos_lineEdit.text())
|
||||
# Vertical: idx=0; horizontal: idx = 1
|
||||
angle = 90*abs(1-idx)
|
||||
invalid = False
|
||||
except ValueError:
|
||||
pos = None
|
||||
angle = None
|
||||
pass
|
||||
|
||||
if invalid:
|
||||
QtWidgets.QMessageBox().information(self, 'Invalid input', 'Input is not a valid number')
|
||||
return
|
||||
|
||||
qcolor = QtGui.QColor.fromRgb(*self.color_comboBox.value.rgb())
|
||||
comment = self.comment_lineEdit.text()
|
||||
line = LogInfiniteLine(pos=pos, angle=angle, movable=self.drag_checkBox.isChecked(), pen=qcolor)
|
||||
line.sigPositionChanged.connect(self.move_line)
|
||||
|
||||
self.make_table_row(pos, angle, qcolor, comment)
|
||||
|
||||
graph_id = self.graph_comboBox.currentData()
|
||||
try:
|
||||
self.lines[graph_id].append(line)
|
||||
self.comments[graph_id].append(comment)
|
||||
except KeyError:
|
||||
self.lines[graph_id] = [line]
|
||||
self.comments[graph_id] = [comment]
|
||||
|
||||
self.line_created.emit(line, graph_id)
|
||||
|
||||
def set_graphs(self, graphs: list):
|
||||
for graph_id, name in graphs:
|
||||
self.graph_comboBox.addItem(name, userData=graph_id)
|
||||
|
||||
def remove_graph(self, graph_id: str):
|
||||
idx = self.graph_comboBox.findData(graph_id)
|
||||
if idx != -1:
|
||||
self.graph_comboBox.removeItem(idx)
|
||||
|
||||
if graph_id in self.lines:
|
||||
del self.lines[graph_id]
|
||||
|
||||
@QtCore.pyqtSlot(int, name='on_graph_comboBox_currentIndexChanged')
|
||||
def change_graph(self, idx: int):
|
||||
self.tableWidget.clear()
|
||||
self.tableWidget.setRowCount(0)
|
||||
|
||||
graph_id = self.graph_comboBox.itemData(idx)
|
||||
if graph_id in self.lines:
|
||||
lines = self.lines[graph_id]
|
||||
comments = self.comments[graph_id]
|
||||
for i, line in enumerate(lines):
|
||||
self.make_table_row(line.pos(), line.angle, line.pen.color(), comments[i])
|
||||
|
||||
def make_table_row(self, position, angle, color, comment):
|
||||
if angle == 0:
|
||||
try:
|
||||
pos_label = 'x = ' + str(position.y())
|
||||
except AttributeError:
|
||||
pos_label = 'x = {position}'
|
||||
|
||||
elif angle == 90:
|
||||
try:
|
||||
pos_label = f'y = {position.x()}'
|
||||
except AttributeError:
|
||||
pos_label = f'y = {position}'
|
||||
|
||||
else:
|
||||
raise ValueError('Only horizontal or vertical lines are supported')
|
||||
|
||||
item = QtWidgets.QTableWidgetItem(pos_label)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable)
|
||||
item.setForeground(QtGui.QBrush(QtGui.QColor('black')))
|
||||
|
||||
row_count = self.tableWidget.rowCount()
|
||||
self.tableWidget.setRowCount(row_count+1)
|
||||
self.tableWidget.setItem(row_count, 0, item)
|
||||
|
||||
item2 = QtWidgets.QTableWidgetItem(comment)
|
||||
self.tableWidget.setItem(row_count, 1, item2)
|
||||
|
||||
colitem = QtWidgets.QTableWidgetItem(' ')
|
||||
colitem.setBackground(QtGui.QBrush(color))
|
||||
colitem.setFlags(QtCore.Qt.ItemIsSelectable)
|
||||
self.tableWidget.setVerticalHeaderItem(row_count, colitem)
|
||||
|
||||
def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent) -> bool:
|
||||
if evt.type() == QtCore.QEvent.KeyPress:
|
||||
if evt.key() == QtCore.Qt.Key_Delete:
|
||||
self.delete_line()
|
||||
return True
|
||||
|
||||
return super().eventFilter(src, evt)
|
||||
|
||||
def delete_line(self):
|
||||
remove_rows = sorted([item.row() for item in self.tableWidget.selectedItems()])
|
||||
graph_id = self.graph_comboBox.currentData()
|
||||
current_lines = self.lines[graph_id]
|
||||
|
||||
print(remove_rows)
|
||||
for i in reversed(remove_rows):
|
||||
print(i)
|
||||
self.tableWidget.removeRow(i)
|
||||
self.line_deleted.emit(current_lines[i], graph_id)
|
||||
|
||||
current_lines.pop(i)
|
||||
self.comments[graph_id].pop(i)
|
||||
|
||||
@QtCore.pyqtSlot(object)
|
||||
def move_line(self, line: InfiniteLine):
|
||||
current_idx = self.graph_comboBox.currentData()
|
||||
graphs = self.lines[current_idx]
|
||||
i = -1
|
||||
for i, line_i in enumerate(graphs):
|
||||
if line == line_i:
|
||||
break
|
||||
pos = line.value()
|
||||
text_item = self.tableWidget.item(i, 0)
|
||||
text_item.setText(text_item.text()[:4]+f'{pos:.4g}')
|
75
src/gui_qt/graphs/movedialog.py
Normal file
75
src/gui_qt/graphs/movedialog.py
Normal file
@ -0,0 +1,75 @@
|
||||
from ..Qt import QtCore, QtWidgets
|
||||
from .._py.move_dialog import Ui_MoveDialog
|
||||
|
||||
|
||||
class QMover(QtWidgets.QDialog, Ui_MoveDialog):
|
||||
moveData = QtCore.pyqtSignal(list, str, str)
|
||||
copyData = QtCore.pyqtSignal(list, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.entries = {}
|
||||
|
||||
self.fromcomboBox.currentIndexChanged.connect(self.change_graph)
|
||||
self.buttonBox.clicked.connect(self.button_clicked)
|
||||
|
||||
def setup(self, entries: dict):
|
||||
self.fromcomboBox.blockSignals(True)
|
||||
self.tocomboBox.blockSignals(True)
|
||||
for k, v in entries.items():
|
||||
self.entries[k[0]] = v
|
||||
self.fromcomboBox.addItem(k[1], userData=k[0])
|
||||
self.tocomboBox.addItem(k[1], userData=k[0])
|
||||
self.fromcomboBox.blockSignals(False)
|
||||
self.tocomboBox.blockSignals(False)
|
||||
self.change_graph(0)
|
||||
|
||||
@QtCore.pyqtSlot(int)
|
||||
def change_graph(self, idx: int):
|
||||
self.listWidget.clear()
|
||||
idd = self.fromcomboBox.itemData(idx)
|
||||
if idd is not None:
|
||||
for i, j in self.entries[idd]:
|
||||
it = QtWidgets.QListWidgetItem(j)
|
||||
it.setData(QtCore.Qt.UserRole, i)
|
||||
self.listWidget.addItem(it)
|
||||
|
||||
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
|
||||
def button_clicked(self, btn: QtWidgets.QAbstractButton):
|
||||
if self.buttonBox.buttonRole(btn) in [QtWidgets.QDialogButtonBox.ApplyRole,
|
||||
QtWidgets.QDialogButtonBox.AcceptRole]:
|
||||
from_graph = self.fromcomboBox.itemData(self.fromcomboBox.currentIndex())
|
||||
to_graph = self.tocomboBox.itemData(self.tocomboBox.currentIndex())
|
||||
if from_graph != to_graph:
|
||||
moving = []
|
||||
for idx in self.listWidget.selectedIndexes():
|
||||
it = self.listWidget.itemFromIndex(idx)
|
||||
moving.append(it.data(QtCore.Qt.UserRole))
|
||||
it_data = (it.data(QtCore.Qt.UserRole), it.text())
|
||||
if self.move_button.isChecked():
|
||||
self.entries[from_graph].remove(it_data)
|
||||
self.entries[to_graph].append(it_data)
|
||||
self.change_graph(self.fromcomboBox.currentIndex())
|
||||
|
||||
if self.move_button.isChecked():
|
||||
self.moveData.emit(moving, to_graph, from_graph)
|
||||
else:
|
||||
self.copyData.emit(moving, to_graph)
|
||||
|
||||
if self.buttonBox.buttonRole(btn) == QtWidgets.QDialogButtonBox.AcceptRole:
|
||||
self.close()
|
||||
|
||||
elif self.buttonBox.buttonRole(btn) == QtWidgets.QDialogButtonBox.RejectRole:
|
||||
self.close()
|
||||
else:
|
||||
return
|
||||
|
||||
def close(self):
|
||||
self.listWidget.clear()
|
||||
self.tocomboBox.clear()
|
||||
self.fromcomboBox.clear()
|
||||
self.entries = {}
|
||||
|
||||
super().close()
|
Reference in New Issue
Block a user