PyQt5 Combining QVBoxLayout and QStackedLayout - pyqt5

Can QVBoxLayout contain a QStackedLayout in a widget? I am trying to create a custom widget that looks like the following:
import sys
from PyQt5.QtWidgets import *
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg
class MyCustomWidget(QWidget):
def __init__(self):
super().__init__()
self.my_combo_box = QComboBox(self)
self.vlayout = QVBoxLayout(self)
self.slayout = QStackedLayout(self)
self.vlayout.addWidget(self.my_combo_box)
self.graphWidget = {}
self.widget = QWidget()
def add_new_graphics(self, name, x, y):
self.my_combo_box.addItem(name)
self.graphWidget[name] = pg.PlotWidget()
self.graphWidget[name].plot(x, y)
self.slayout.addWidget(self.graphWidget[name])
self.vlayout.addWidget(self.slayout)
self.widget.setLayout(self.vlayout)
app = QApplication([])
a = MyCustomWidget()
x1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y1 = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
a.add_new_graphics('data1', x1, y1)
x2 = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
y2 = [0, 2, 4, 2, 3, 1, 9, 2, 5, 5]
a.add_new_graphics('data2', x2, y2)
a.show()
sys.exit(app.exec_())
I get the following errors:
TypeError: addWidget(self, QWidget, stretch: int = 0, alignment: Union[Qt.Alignment, Qt.AlignmentFlag] = Qt.Alignment()): argument 1 has unexpected type 'QStackedLayout'
sys:1: RuntimeWarning: Visible window deleted. To prevent this, store a reference to the window object.

You can add any layout to a QVBoxLayout including a QStackedLayout, but you need to use layout.addLayout for this, not layout.addWidget. There are a few other things off in the definition of MyCustomWidget. Here is a version that should work:
class MyCustomWidget(QWidget):
def __init__(self):
super().__init__()
self.my_combo_box = QComboBox(self)
self.vlayout = QVBoxLayout(self)
self.slayout = QStackedLayout()
self.vlayout.addWidget(self.my_combo_box)
# add stacked layout to self.vlayout (only need to be done once).
self.vlayout.addLayout(self.slayout)
self.graphWidget = {}
# self.widget isn't doing anything in your code so I commented out this line
# self.widget = QWidget()
# Add signal-slot connection to show correct graph when current index in combo box is changed.
self.my_combo_box.currentIndexChanged.connect(self.slayout.setCurrentIndex)
def add_new_graphics(self, name, x, y):
self.my_combo_box.addItem(name)
self.graphWidget[name] = pg.PlotWidget()
self.graphWidget[name].plot(x, y)
self.slayout.addWidget(self.graphWidget[name])

Related

Remove default numbers from QHeaderView when using a custom label in the header

I am attempting to customize a QHeaderView in order to create custom labels that span two or more columns. In the code below, I have done this by placing a QLabel over the appropriate sections of the overall header. However, the default header numbering of the individual columns still appears as shown below. How can this be removed? I would prefer to do this without adding a background colour to each label.
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
class CustomHeader(QHeaderView):
def __init__(self, orientation, labels, parent=None):
QHeaderView.__init__(self, orientation, parent=None)
self.sectionResized.connect(self.handleSectionResized)
self.label_text = labels
self.labels = list()
self.setFixedHeight(40)
def handleSectionResized(self, i):
start = self.visualIndex(i)
stop = self.count()
for i in range(start, stop):
self.labels[i].setGeometry(self.sectionViewportPosition(i), 0, self.sectionSize(i) - 5, self.height())
self.labels[i].show()
def showEvent(self, event):
for i in range(len(self.label_text)):
label = QLabel(self.label_text[i], self)
label.setAlignment(Qt.AlignCenter)
widget = QWidget(self)
vbox = QVBoxLayout()
vbox.setSpacing(0)
vbox.setContentsMargins(0,0,0,0)
vbox.addWidget(label)
widget.setLayout(vbox)
widget.show()
self.labels.append(widget)
self.adjustPositions()
return super().showEvent(event)
def adjustPositions(self):
for index, label in enumerate(self.labels):
if index == 1 or index == 3:
size = self.sectionSize(index)
print(size)
geom = QRect(
self.sectionViewportPosition(index),
0,
200,
self.height(),
)
else:
size = self.sectionSize(index)
print(size)
geom = QRect(
self.sectionViewportPosition(index),
0,
self.sectionSize(index),
self.height(),
)
geom.adjust(2, 0, -2, 0)
label.setGeometry(geom)
class MyTable(QTableWidget):
def __init__(self, nrow, ncol, parent=None):
super().__init__(nrow, ncol, parent=parent)
self.verticalHeader().hide()
self.setHorizontalHeaderLabels([])
labels = ['One', 'Two', '', 'Three'] #, 'Four', 'Five']
self.header = CustomHeader(Qt.Horizontal, labels, self)
self.setHorizontalHeader(self.header)
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
data = [
[3, 20, 40, 25, 45],
[5, 22, 42, 27, 47],
]
for i in range(2):
for j in range(5):
item = QTableWidgetItem(str(data[i-2][j]))
self.setItem(i, j, item)
item.setBackground(Qt.white)
if __name__ == '__main__':
app = QApplication(sys.argv)
table = MyTable(7, 5)
table.show()
sys.exit(app.exec())
You could change them the same way you would with the standard header.
In your QTableWidget just call:
self.setHorizontalHeaderLabels([""] * self.columnCount())
For example:
class MyTable(QTableWidget):
def __init__(self, nrow, ncol, parent=None):
super().__init__(nrow, ncol, parent=parent)
self.verticalHeader().hide()
self.setHorizontalHeaderLabels([])
labels = ['One', 'Two', '', 'Three'] #, 'Four', 'Five']
self.header = CustomHeader(Qt.Horizontal, labels, self)
self.setHorizontalHeaderLabels([""] * self.columnCount()) # added this
self.setHorizontalHeader(self.header)
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)

How to limit scrolling outside bounds in pyqtgraph?

I've set up a chart for candlestick display using PyQtGraph.
I've made a simplified example below.
I'm trying to figure out how to limit the viewable/scrollable range on the chart for the y1 and y2 axis.
I want to limit them to equal the ymin and ymax settings in the boundingRect() function.
If I run the chart it starts off with the bounds set correctly but it allows you to manually scroll around the chart outside of the bounds that are set within the boundingRect()
I want to prevent the ability to scroll beyond what the boundingRect() allows.
I want to be able to scroll along the X axis without issue but I want the Y axis to dynamically limit the bounds to the Y axis of the candlesticks that are currently viewable.
For starters I can't figure out how to force limits on scrollable bounds in a way that is compatible with what I have written below.
QPainterPath or QRectF doesn't seem to have a function to limit the scrollable view that I can find. Or at the very least I can't figure out the proper syntax.
Then I need to figure out how to return the axis range of the currently viewable candles in order to dynamically set the scrollable/viewable limits. Haven't gotten that far yet.
Any help is appreciated.
import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui, QtWidgets
import numpy as np
data = np.array([ ## fields are (time, open, close, min, max).
(1., 10, 13),
(2., 13, 17),
(3., 17, 14),
(4., 14, 15),
(5., 15, 9),
(6., 9, 15),
(7., 15, 5),
(8., 5, 7),
(9., 7, 3),
(10., 3, 10),
(11., 10, 15),
(12., 15, 25),
(13., 25, 20),
(14., 20, 17),
(15., 17, 30),
(16., 30, 32),
(17., 32, 35),
(18., 35, 28),
(19., 28, 27),
(20., 27, 25),
(21., 25, 29),
(22., 29, 35),
(23., 35, 40),
(24., 40, 45),
])
class CandlestickItem(pg.GraphicsObject):
global data
_boundingRect = QtCore.QRectF()
# ...
def __init__(self):
pg.GraphicsObject.__init__(self)
self.flagHasData = False
def set_data(self, data):
self.data = data
self.flagHasData = True
self.generatePicture()
self.informViewBoundsChanged()
def generatePicture(self):
self.picture = QtGui.QPicture()
path = QtGui.QPainterPath()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]) / 3.
for (t, open, close) in self.data:
rect = QtCore.QRectF(t-w, open, w*2, close-open)
path.addRect(rect)
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(rect)
p.end()
self._boundingRect = path.boundingRect()
def paint(self, p, *args):
if self.flagHasData:
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
# data =data
# xmin = np.nanmin(data[:,0])
xmax = np.nanmax(data[:,0])
xmin = xmax - 5
ymin = np.nanmin(data[:,2])
ymax = np.nanmax(data[:,2])
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
item = CandlestickItem()
plt = pg.plot()
plt.addItem(item)
plt.setWindowTitle('pyqtgraph example: customGraphicsItem')
item.set_data(data)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtWidgets.QApplication.instance().exec_()
If I understood you correctly, you want to prevent user to zoom or scroll outside boundictRect of your candlesitck plot. That can be achieved with setting limit to viewbox.
EDIT: y view is now limited to min and max value within current view specified by x_start and x_end.
Here is modified code, that does that:
import math
import numpy as np
import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui, QtWidgets
data = np.array([ ## fields are (time, open, close, min, max).
(1., 10, 13),
(2., 13, 17),
(3., 17, 14),
(4., 14, 15),
(5., 15, 9),
(6., 9, 15),
(7., 15, 5),
(8., 5, 7),
(9., 7, 3),
(10., 3, 10),
(11., 10, 15),
(12., 15, 25),
(13., 25, 20),
(14., 20, 17),
(15., 17, 30),
(16., 30, 32),
(17., 32, 35),
(18., 35, 28),
(19., 28, 27),
(20., 27, 25),
(21., 25, 29),
(22., 29, 35),
(23., 35, 40),
(24., 40, 45),
])
class CandlestickItem(pg.GraphicsObject):
global data
_boundingRect = QtCore.QRectF()
# ...
def __init__(self):
pg.GraphicsObject.__init__(self)
self.picture = QtGui.QPicture()
self.flagHasData = False
def set_data(self, data):
self.data = data
self.flagHasData = True
self.generatePicture()
self.informViewBoundsChanged()
def generatePicture(self):
self.picture = QtGui.QPicture()
path = QtGui.QPainterPath()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]) / 3.
for (t, open, close) in self.data:
rect = QtCore.QRectF(t - w, open, w * 2, close - open)
path.addRect(rect)
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(rect)
p.end()
self._boundingRect = path.boundingRect()
def paint(self, p, *args):
if self.flagHasData:
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())
def viewTransformChanged(self):
super(CandlestickItem, self).viewTransformChanged()
br = self.boundingRect()
# Get coords of view mapped to data
mapped_view = self.mapRectToView(self.viewRect())
# Get start and end of x slice
x_slice_start = int(mapped_view.x()) - 1
x_slice_end = x_slice_start + (math.ceil(mapped_view.width()) + 1)
if x_slice_start < 0:
x_slice_start = 0
if x_slice_end > data.shape[0]:
x_slice_end = data.shape[0]
# Get data in x interval
y_slice = data[x_slice_start:x_slice_end]
try:
ymin = np.nanmin(y_slice[:, 2])
ymax = np.nanmax(y_slice[:, 2])
if ymin != ymax:
self.getViewBox().setLimits(xMin=br.x(), xMax=br.width(), yMin=ymin, yMax=ymax)
except ValueError:
pass
return br
item = CandlestickItem()
plt = pg.plot()
plt.addItem(item)
plt.setWindowTitle('pyqtgraph example: customGraphicsItem')
item.set_data(data)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtWidgets.QApplication.instance().exec_()

Vispy scene embedded into PyQt5 Application becomes black after being removed from layout then add back

The problem occurs after I do this series of operations:
Put two vispy.scene.SceneCanvas objects into two distinct QWidgets, call them scenewidget1, scenewidget2
Show a QWidget, add scenewidget1 and scenewidget2 to its layout
Remove scenewidget1 and scenewidget2 from layout, then add them back to layout
One vispy SceneCanvas becomes black and shows nothing, it will recover after the user clicks the scene, is there any way to fix this problem?
Before:
After remove from layout then add back:
Minimum Reproducible Example:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import vispy.scene as scene
from vispy.scene import visuals
import numpy as np
class SceneWidget(QWidget):
def __init__(self):
super().__init__()
self.layout = QHBoxLayout()
self.scene = scene.SceneCanvas(parent=self, bgcolor='w')
self.setLayout(self.layout)
self.layout.addWidget(self.scene.native)
# add something to the scene
self.grid = self.scene.central_widget.add_grid(spacing=0)
self.vb = self.grid.add_view(
row=1, col=1, camera='panzoom', bgcolor=(1, 1, 1, 1), )
self.scatter = visuals.Markers()
self.vb.add(self.scatter)
self.scatter.set_data(np.array([[0, 0], [1, 1], [2, 2]]))
self.vb.camera.set_range(x=(-1, 3), y=(-1, 3))
class CentralWin(QWidget):
def __init__(self):
super().__init__()
self.layout = QGridLayout()
self.scenewidget1 = SceneWidget() # Scene Widget is a QWidget with a SceneCanvas at center
self.scenewidget2 = SceneWidget()
self.layout.addWidget(self.scenewidget1, 0, 0, 1, 1)
self.layout.addWidget(self.scenewidget2, 0, 1, 1, 1)
self.btn = QPushButton('Remove From Layout Then Add Back')
self.btn.clicked.connect(self.removeFromLayout)
self.layout.addWidget(self.btn, 1, 0, 1, 2)
self.setLayout(self.layout)
def removeFromLayout(self):
self.layout.removeWidget(self.scenewidget1)
self.layout.removeWidget(self.scenewidget2)
self.scenewidget1.setParent(None)
self.scenewidget2.setParent(None)
self.layout.addWidget(self.scenewidget1, 0, 0, 1, 1)
self.layout.addWidget(self.scenewidget2, 0, 1, 1, 1)
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
win.setCentralWidget(CentralWin())
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, 'PYQT_VERSION'):
QApplication.instance().exec_()

Setting alpha in matplotlib.axes.Axes.table?

I'm trying to understand how I can set the alpha level in a matplotlib table. I tried setting it with a global rcParams, but not quite sure how to do that? (I want to change the transparency in the header color). In general I'm not sure this can be done globally, if not, how do i pass the parameter to table? Thx in advance.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from cycler import cycler
import six
# Set universal font and transparency
plt.rcParams["font.family"] = "DejaVu Sans"
plt.rcParams['axes.prop_cycle'] = cycler(alpha=[0.5])
raw_data = dict(Simulation=[42, 39, 86, 15, 23, 57],
SP500=[52, 41, 79, 80, 34, 47],
NASDAQ=[62, 37, 84, 51, 67, 32],
Benchmark=[72, 43, 36, 26, 53, 88])
df = pd.DataFrame(raw_data, index=pd.Index(
['Sharpe Ratio', 'Sortino Ratio', 'Calmars Ratio', 'VaR', 'CVaR', 'Max DD'], name='Metric'),
columns=pd.Index(['Simulation', 'SP500', 'NASDAQ', 'Benchmark'], name='Series'))
def create_table(data, col_width=None, row_height=None, font_size=None,
header_color='#000080', row_colors=None, edge_color='w',
header_columns=0, ax=None, bbox=None):
if row_colors is None:
row_colors = ['#D8D8D8', 'w']
if bbox is None:
bbox = [0, 0, 1, 1]
if ax is None:
size = (np.array(data.shape[::-1]) + np.array([0, 1])) * np.array([col_width, row_height])
fig, ax = plt.subplots(figsize=size)
ax.axis('off')
ax.axis([0, 1, data.shape[0], -1])
data_table = ax.table(cellText=data.values, colLabels=data.columns, rowLabels=data.index,
bbox=bbox, cellLoc='center', rowLoc='left', colLoc='center',
colWidths=([col_width] * len(data.columns)))
cell_map = data_table.get_celld()
for i in range(0, len(data.columns)):
cell_map[(0, i)].set_height(row_height * 0.20)
data_table.auto_set_font_size(False)
data_table.set_fontsize(font_size)
for k, cell in six.iteritems(data_table._cells):
cell.set_edgecolor(edge_color)
if k[0] == 0 or k[1] < header_columns:
cell.set_text_props(weight='bold', color='w')
cell.set_facecolor(header_color)
else:
cell.set_facecolor(row_colors[k[0] % len(row_colors)])
return ax
ax = create_table(df, col_width=1.5, row_height=0.5, font_size=8)
ax.set_title("Risk Measures", fontweight='bold')
ax.axis('off')
plt.tight_layout()
plt.savefig('risk_parameter_table[1].pdf')
plt.show()
You can set_alpha() manually on the table's _cells.
If you only want to change the headers, check if row == 0 or col == -1:
def create_table(...):
...
for (row, col), cell in data_table._cells.items():
if (row == 0) or (col == -1):
cell.set_alpha(0.5)
return ax

numpy vectorization instead of loop

I have the following equation:
where v, mu are |R^3, where Sigma is |R^(3x3) and where the result is a scalar value. Implementing this in numpy is no problem:
result = np.transpose(v - mu) # Sigma_inv # (v - mu)
Now I have a bunch of v-vectors (lets call them V \in |R^3xn) and I would
like to execute the above equation in a vectorized manner so that, as
a result I get a new vector Result \in |R^1xn.
# pseudocode
Result = np.zeros((n, 1))
for i,v in V:
Result[i,:] = np.transpose(v - mu) # Sigma_inv # (v - mu)
I looked at np.vectorize but the documentation suggests that its just the same as looping over all entries which I would prefer not to do. What would be an elegant vectorized solution?
As a side node: n might be quite large and a |R^nxn matrix will certainly not fit into my memory!
edit: working code sample
import numpy as np
S = np.array([[1, 2], [3,4]])
V = np.array([[10, 11, 12, 13, 14, 15],[20, 21, 22, 23, 24, 25]])
Res = np.zeros((V.shape[1], 1))
for i in range(V.shape[1]):
v = np.transpose(np.atleast_2d(V[:,i]))
Res[i,:] = (np.transpose(v) # S # v)[0][0]
print(Res)
Using a combination of matrix-multiplication and np.einsum -
np.einsum('ij,ij->j',V,S.dot(V))
Does this work for you?
res = np.diag(V.T # S # V).reshape(-1, 1)
It seems to provide the same result as you want.
import numpy as np
S = np.array([[1, 2], [3,4]])
V = np.array([[10, 11, 12, 13, 14, 15],[20, 21, 22, 23, 24, 25]])
Res = np.zeros((V.shape[1], 1))
for i in range(V.shape[1]):
v = np.transpose(np.atleast_2d(V[:,i]))
Res[i,:] = (np.transpose(v) # S # v)[0][0]
res = np.diag(V.T # S # V).reshape(-1, 1)
print(np.all(np.isclose(Res, res)))
# output: True
Although there is probably a more memory efficient solution using np.einsum.
Here is a simple solution:
import numpy as np
S = np.array([[1, 2], [3,4]])
V = np.array([[10, 11, 12, 13, 14, 15],[20, 21, 22, 23, 24, 25]])
Res = np.sum((V.T # S) * V.T, axis=1)
This are multiplications of matrix/vector stacks. numpy.matmul can do that after bringing S and V into the correct shape:
S = S[np.newaxis, :, :]
VT = V.T[:, np.newaxis, :]
V = VT.transpose(0, 2, 1)
tmp = np.matmul(S, V)
Res = np.matmul(VT, tmp)
print(Res)
#[[[2700]]
# [[3040]]
# [[3400]]
# [[3780]]
# [[4180]]
# [[4600]]]