I'm trying to create a plot layout using PyQtGraph within a PyQt application.
I need a single row with two plots the first two columns wide and the second a single column wide.
Reading the docs I presumed something like this would work:
# Create the PyQtGraph Plot area
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=1, col=1, colspan=2, title = 'Data1')
self.w2 = self.view.addPlot(row=1, col=3, colspan=1, title = 'Data2')
But in this case I get two plot areas each taking 50% of the window width.
What am I doing wrong?
Best Regards,
Ben
colspan allows you to let a cell in the grid layout span over multiple columns. I a way it merges multiple grid cells. In your example you end up with a grid of 1 row by 3 columns. The first two columns apparently each have a width of 25% of the total (or one has 0% and the other 50%), and the third column takes the other 50%. In short: colspan does not allow you to control the width of the columns.
So, how to set the width of the columns or their contents? That was surprisingly hard to find. There seem to be no PyQtGraph methods that handle this directly, you must use the underlying Qt classes.
A pg.GraphicsLayoutWidget has as its central item a pg.GraphicsLayout. This in turn has a layout member that contains a Qt QGraphicsGridLayout. This allows you to manipulate the column widths with: setColumnFixedWidth, setColumnMaximimumWidth, setColumnStretchFactor, etc. Something like this may be what you need:
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=0, col=0, title = 'Data1')
self.w2 = self.view.addPlot(row=0, col=1, title = 'Data2')
qGraphicsGridLayout = self.view.ci.layout
qGraphicsGridLayout.setColumnStretchFactor(0, 2)
qGraphicsGridLayout.setColumnStretchFactor(1, 1)
Take a look in the documentation of QGraphicsGridLayout and experiment a bit.
Related
I am making an XY-scatter chart, where both axes show aggregated data.
For both variables I want to have an interval selection in two small charts below where I can brush along the x-axis to set a range.
The selection should then be used to filter what is taken into account for each aggregation operation individually.
On the example of the cars data set, let's say I what to look at Horsepower over Displacement. But not of every car: instead I aggregate (sum) by Origin. Additionally I create two plots of totally mean HP and displacement over time, where I add interval selections, as to be able to set two distinct time ranges.
Here is an example of what it should look like, although the selection functionality is not yet as intended.
And here below is the code to produce it. Note, that I left some commented sections in there which show what I already tried, but does not work. The idea for the transform_calculate came from this GitHub issue. But I don't know how I could use the extracted boundary values for changing what is included in the aggregations of x and y channels. Neither the double transform_window took me anywhere. Could a transform_bin be useful here? How?
Basically, what I want is: when brush1 reaches for example from 1972 to 1975, and brush2 from 1976 to 1979, I want the scatter chart to plot the summed HP of each country in the years 1972, 1973 and 1974 against each countries summed displacement from 1976, 1977 and 1978 (for my case I don't need the exact date format, the Year might as well be integers here).
import altair as alt
from vega_datasets import data
cars = data.cars.url
brush1 = alt.selection(type="interval", encodings=['x'])
brush2 = alt.selection(type="interval", encodings=['x'])
scatter = alt.Chart(cars).mark_point().encode(
x = 'HP_sum:Q',
y = 'Dis_sum:Q',
tooltip = 'Origin:N'
).transform_filter( # Ok, I can filter the whole data set, but that always acts on both variables (HP and displacement) together... -> not what I want.
brush1 | brush2
).transform_aggregate(
Dis_sum = 'sum(Displacement)',
HP_sum = 'sum(Horsepower)',
groupby = ['Origin']
# ).transform_calculate( # Can I extract the selection boundaries like that? And if yes: how can I use these extracts to calculate the aggregationsof HP and displacement?
# b1_lower='(isDefined(brush1.x) ? (brush1.x[0]) : 1)',
# b1_upper='(isDefined(brush1.x) ? (brush1.x[1]) : 1)',
# b2_lower='(isDefined(brush2.x) ? (brush2.x[0]) : 1)',
# b2_upper='(isDefined(brush2.x) ? (brush2.x[1]) : 1)',
# ).transform_window( # Maybe instead of calculate I can use two window transforms...??
# conc_sum = 'sum(conc)',
# frame = [brush1.x[0],brush1.x[1]], # This will not work, as it sets the frame relative (back- and foreward) to each datum (i.e. sliding window), I need it to correspond to the entire data set
# groupby=['sample']
# ).transform_window(
# freq_sum = 'sum(freq)',
# frame = [brush2.x[0],brush2.x[1]], # ...same problem here
# groupby=['sample']
)
range_sel1 = alt.Chart(cars).mark_line().encode(
x = 'Year:T',
y = 'mean(Horsepower):Q'
).add_selection(
brush1
).properties(
height = 100
)
range_sel2 = alt.Chart(cars).mark_line().encode(
x = 'Year:T',
y = 'mean(Displacement):Q'
).add_selection(
brush2
).properties(
height = 100
)
scatter & range_sel1 & range_sel2
Interval selection cannot be used for aggregate charts yet in Vega-Lite. The error behavior have been updated in a recent PR to Vega-Lite to show a helpful message.
Not sure if I understand your requirements correctly, does this look close to what you want? (Just added param selections on top of your vertically concatenated graphs)
Vega Editor
I'm trying to make a Qlabel that will only fill available space in Qt (really in PyQt but the same principle applies). It's meant to display a (sometimes really long) file path, and as I currently have it the length of the label makes the minimum size of that part of the window way too large.
I want to make the label's text be reduced to the maximum possible length without exceeding the minimum width due to the other widgets in the panel. I have found the QFontMetrics::elideText() method, which can effectively clip the text in the way I want, but I still can't figure out how to get the pixel width without the label affecting the size of the panel.
My hackjob thought process is to set the Qlabel's sizes to zero by overriding size/minimumsize/maximumsize, measure the remaining space allotted, and then set text to that. However, I don't know how to get that remaining space, and I feel like there should be a better way.
My layout for reference:
QLabel is a pretty neat widget: it seems very simple, but it's not.
The size and displaying aspects are very important: since it's able to display formatted text, it can even have some layout issues.
Since your requirement is to keep the label as small as possible (but while keeping its content displayed if possible), the most important requirement is to implement the sizeHint (and minimumSizeHint()) functions, since the layout of the parent widget will consider that when resizing its contents.
A possible solution is based on two aspects:
provide a basic [minimum] size hint that doesn't consider the whole contents
override the painting by eliding text whenever the available space is not enough
The following code obviously does NOT consider rich text formatting, including different paragraph alignment, word wrapping, etc.
This is an example showing a subclassed QLabel trying to display the following path:
'/tmp/test_dir/some_long_path/some_subdir/imagepath/'
Consider that you could even actually use a basic QWidget instead. In the following code I'm considering the QFrame subclassing abilities which also include adding proper margins and borders, depending on the style and the frameShape or frameShadow properties.
class ElideLabel(QtWidgets.QLabel):
_elideMode = QtCore.Qt.ElideMiddle
def elideMode(self):
return self._elideMode
def setElideMode(self, mode):
if self._elideMode != mode and mode != QtCore.Qt.ElideNone:
self._elideMode = mode
self.updateGeometry()
def minimumSizeHint(self):
return self.sizeHint()
def sizeHint(self):
hint = self.fontMetrics().boundingRect(self.text()).size()
l, t, r, b = self.getContentsMargins()
margin = self.margin() * 2
return QtCore.QSize(
min(100, hint.width()) + l + r + margin,
min(self.fontMetrics().height(), hint.height()) + t + b + margin
)
def paintEvent(self, event):
qp = QtGui.QPainter(self)
opt = QtWidgets.QStyleOptionFrame()
self.initStyleOption(opt)
self.style().drawControl(
QtWidgets.QStyle.CE_ShapedFrame, opt, qp, self)
l, t, r, b = self.getContentsMargins()
margin = self.margin()
try:
# since Qt >= 5.11
m = self.fontMetrics().horizontalAdvance('x') / 2 - margin
except:
m = self.fontMetrics().width('x') / 2 - margin
r = self.contentsRect().adjusted(
margin + m, margin, -(margin + m), -margin)
qp.drawText(r, self.alignment(),
self.fontMetrics().elidedText(
self.text(), self.elideMode(), r.width()))
You can override the paintEvent() and achieve it in the following way:
class QElidedLabel(QLabel):
def paintEvent(self, event):
painter = QPainter(self)
textDoc = QTextDocument()
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width() - 10)
textDoc.setPlainText(elided)
textDoc.drawContents(painter)
This will draw the Elided Label automatically and you won't have to change your code anywhere else. Also you can set the size policy of QLabel to MinimumExpanding to make sure your QLabel takes the maximum available space. This way the self.width() returns the current maximum width. You can take a look at documentation for the working of QTextDocument() and QFontMetrics(). Also, self.width() - 10 just makes sure that '...' in the elided label is not hidden, you can remove - 10 and just use self.width() if .. visible for you after removing it as well.
I am unsuccessfully trying to modify the size of the text of the different categories (text highlighted in yellow) in a BOKEH bar-chart.
I would be really grateful if somebody knows how to achieve this.
The trick is to modify the text size of the CategoricalAxis which is below the figure object (usually first item at position 0).
To modify the text size for groups, use this line.
p.below[0].group_text_font_size = '13px'
In the same way you can set multiple other styling options, like the axis_label_text_font_size or the major_label_text_font_size and many other. To see how it works, look at the example below.
Demonstration
Following the example from bokeh about Handling Categorical Data the output looks like this:
Now you can do some changes for the text below the x-axis.
p.below[0].group_text_font_size = '16px'
p.below[0].group_text_font_style = 'normal'
p.below[0].group_text_color = 'black'
p.below[0].major_label_text_font_size = '14px'
p.below[0].major_label_text_color = 'black'
This will set
a new font size for groups ('16px') and labels ('14px'),
a new font color for groups and labels (here 'black') and
a new font style to the groups (here 'normal').
The figure looks now like this:
I have a problem here that is quite obvious but I can't find a solution. I am new to PyQt and PyQtGraph.
Here are the relevant codes:
verticalGroupBox = QtGui.QGroupBox("Waveforms")
layoutV = QtGui.QVBoxLayout()
waveformPlot1 = pg.PlotWidget()
waveformPlot2 = pg.PlotWidget()
waveformPlot3 = pg.PlotWidget()
waveformPlot1.plotItem.plot(self.time, self.data_plot[0])
layoutV.addWidget(waveformPlot1)
waveformPlot2.setYRange(-30000, 30000, padding=0.01)
waveformPlot2.setXRange(0, self.timeDuration, padding=0.01)
layoutV.addWidget(waveformPlot2)
waveformPlot3.setYRange(-30000, 30000, padding=0.01)
layoutV.addWidget(waveformPlot3)
Here are the images to my problem:
https://imgur.com/a/uN25Ra6 |
https://imgur.com/a/daYvSCz
As you can see in the first image, I only placed a plot with data on waveformPlot1 and made waveformPlot2 and waveformPlot3 an empty plot widgets for testing. So now, my problem is, when I decided to attach another data that is expected to be plotted in waveformPlot2 widget (in order from plot1-plot3), hence, it is
plotted in waveformPlot3 widget. (see second image)
waveformPlot2.plotItem.plot(self.time, self.data_plot[0])
I notice that it is because of the layout that it follows where it ends, so my 2nd plot data was plotted on the 3rd widget
I need to Solve two problems With my widget above.
I'd like to be able to define the amount of space put between the post widgets shown in the image (they look fine as is, but I wanna know it's done).
I'd like to grow the text edits vertically based on the amount of text they contain without growing horizontally.
For 1 the code that populates the widgets is as follows :
self._body_frame = QWidget()
self._body_frame.setMinimumWidth(750)
self._body_layout = QVBoxLayout()
self._body_layout.setSpacing(0)
self._post_widgets = []
for i in range(self._posts_per_page):
pw = PostWidget()
self._post_widgets.append(pw)
self._body_layout.addWidget(pw)
self._body_frame.setLayout(self._body_layout)
SetSpacing(0) doesn't bring things any closer, however SetSpacing(100) does increase it.
edit
(for Question 2) I haven't mentioned this, but I want the parent widget to have a vertical scrollbar.
I have answered my own question, but its wordy, and cause and affect based. A proper well written tutorial style answer to address both points gets the bounty :D
edit 2
Using my own answer below I have solved the problem. I'll be accepting my own answer now.
1) Layouts
The other answer on here is very unclear and possibly off about how layout margins work. Its actually very straightforward.
Layouts have content margins
Widgets have content margins
Both of these define a padding around what they contain. A margin setting of 2 on a layout means 2 pixels of padding on all sides. If you have parent-child widgets and layouts, which is always the case when you compose your UI, each object can specific margins which take effect individually. That is... a parent layout specifying a margin of 2, with a child layout specifying a margin of 2, will effectively have 4 pixels of margin being displayed (obviously with some frame drawing in between if the widget has a frame.
A simple layout example illustrates this:
w = QtGui.QWidget()
w.resize(600,400)
layout = QtGui.QVBoxLayout(w)
layout.setMargin(10)
frame = QtGui.QFrame()
frame.setFrameShape(frame.Box)
layout.addWidget(frame)
layout2 = QtGui.QVBoxLayout(frame)
layout2.setMargin(20)
frame2 = QtGui.QFrame()
frame2.setFrameShape(frame2.Box)
layout2.addWidget(frame2)
You can see that the top level margin is 10 on each side, and the child layout is 20 on each side. Nothing really complicated in terms of math.
Margin can also be specified on a per-side basis:
# left: 20, top: 0, right: 20, bottom: 0
layout.setContentsMargins(20,0,20,0)
There is also the option of setting spacing on a layout. Spacing is the pixel amount that is placed between each child of the layout. Setting it to 0 means they are right up against each other. Spacing is a feature of the layout, while margin is a feature of the entire object. A layout can have margin around it, and also spacing between its children. And, the children of the widget can have their own margins which are part of their individual displays.
layout.setSpacing(10) # 10 pixels between each layout item
2) Auto-Resizing QTextEdit
Now for the second part of your question. There are a few ways to create a auto-resizing QTextEdit I am sure. But one way to approach it is to watch for content changes in the document, and then adjust the widget based on the document height:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
I subclassed QTextEdit -> GrowingTextEdit, and connected the signal emitted from its document to a slot sizeChange that checks the document height. I also included a heightMin and heightMax attribute to let you specify how large or small its allowed to autogrow. If you try this out, you will see that as you type into the box, the widget will start to resize itself, and also shrink back when you remove lines. You can also turn off the scrollbars if you want. Right now each text edit has its own bars, in addition to the parent scroll area. Also, I think you could add a small pad value to the docHeight so that it expands just enough to not show scrollbars for the content.
This approach is not really low level. It uses the commonly exposed signals and child members of the widget for you to receive notifications of state changes. Its pretty common to make use of the signals for extending functionality of existing widgets.
To Address Question 1:
Parent Widgets and Layouts both have margins, in addition to the spacing parameter of the layout itself. From some cause and affect testing It is apprent that margins apply both to the outer region of a parent as well as an internal region.
So, for example if a 2 pixel margin is specified to a parent the vertical border has <--2 pixel | 2 pixel --> margin in addition to the margins of the layout (A HBoxLayout in this case). If the layout has a 2 pixel margin as well the area around horizontal line would look like:
<-- 2 pixel | 2 pixel --> <-- 2 pixel (L) 2 pixel--> (W)
edit Perhaps its more like this: | 2 pixel --> (L) 2 pixel --> <-- 2 pixel (W)
Where | is the vertical line of the parent (L) is the vertical line of the Layout and (W) is the border of the embedded widget in the horizontal layout.
The spacing of the layout is an additional parameter that controls how much space is inserted between widgets of the layout in addition to any layout margins.
The description above might not be accurate( so feel free to edit it where it is inaccurate), but setting the margins of the parent and the layout to zero as well as the layouts spacing to zero produces the result you are after.
For point 2:
I do not think there is a straight forward way to address this issue (you probably have to resort to hooking in at a lower level, which requires a deeper understanding of the API). I think you should use the QLabel Widget instead of the QTextEdit widget. Labels do not have a view and thus expand as needed, at least thats how they work by default, as long as the parent isn't constrained in it's movement.
So, change the QTextEdit to Qlabel and add a scrolling view to the parent and everything should work as you want. I have a feeling you chose QTextEdit because of it's background. Research the way HTML works in QT widgets and you might be able to alter the background via HTML.
edit
This widget (excuse the size) is created by the following code on OS X with PyQT:
import sys
from PyQt4 import Qt
class PostMeta(Qt.QWidget):
posted_at_base_text = "<b> Posted At:</b>"
posted_by_base_text = "<b> Posted By:</b>"
def __init__(self):
Qt.QWidget.__init__(self)
self._posted_by_label = Qt.QLabel()
self._posted_at_label = Qt.QLabel()
layout = Qt.QVBoxLayout()
layout.setMargin(0)
layout.setSpacing(5)
layout.addWidget(self._posted_by_label)
layout.addWidget(self._posted_at_label)
layout.addStretch()
self.setLayout(layout)
self._posted_by_label.setText(PostMeta.posted_by_base_text)
self._posted_at_label.setText(PostMeta.posted_at_base_text)
class FramePost(Qt.QFrame):
def __init__(self):
Qt.QFrame.__init__(self)
layout = Qt.QHBoxLayout()
layout.setMargin(10)
self.te = Qt.QLabel()
self.te.setStyleSheet("QLabel { background : rgb(245,245,245) }")
self.te.setFrameStyle( Qt.QFrame.Panel | Qt.QFrame.Sunken)
self.te.setLineWidth(1)
self._post_meta = PostMeta()
layout.addWidget(self._post_meta)
vline = Qt.QFrame()
vline.setFrameShape(Qt.QFrame.VLine)
layout.addWidget(vline)
layout.addWidget(self.te)
self.te.setText(
""" line one
line two
line three
line four
line five
line six
line seven
line eight
line nine
line ten
line eleven
line twelve
line thirteen""")
self.setLayout(layout)
self.setFrameStyle(Qt.QFrame.Box)
self.setLineWidth(2)
app = Qt.QApplication(sys.argv)
w = Qt.QWidget()
layout = Qt.QHBoxLayout()
fp = FramePost()
layout.addWidget(fp)
w.setLayout(layout)
w.show()
app.exec_()
The labels in the left widget show the spacer and margin tweaking done, and I've used a QLabel for the post text. Notice I've tweaked the label to look a bit more like a default QTextEdit