pyqtgraph: How to link two PlotWidget windows to show the same plot? - pyqtgraph

I am developing an orbital analysis tool using PyQT5 and pyqtgraph!
See: https://github.com/3fon3fonov/trifon
My tool has a plotting area with ~15 plots shown in different tab windows, which show different aspects of the data analysis.
The tool it self is assembled with the Designer-qt5, while the QGraphicView widgets are promoted to pyqtgraphs's PlotWidgets
For example in the gui.py I initialize the plots like this:
def initialize_plots(self):
global p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,pe
p1 = self.graphicsView_timeseries_RV
p2 = self.graphicsView_timeseries_RV_o_c
p3 = self.graphicsView_timeseries_phot
p4 = self.graphicsView_timeseries_phot_o_c
p5 = self.graphicsView_timeseries_activity
p6 = self.graphicsView_timeseries_correlations
# ...
pe = self.graphicsView_extra_plot
# ...
so p1-p6 in this case are different PlotWidget objects on which I add Items/Plot data, i.e. p1.plot(x,y), p1.addItem(), etc.
What I want is to link pe to any of p1-p6!. pe is an extra plot so the user can choose from those already available/created.
Thus the user can select which plot he/she wants to see next to the main plot.
Lets imagine that the ComboBox dropdown menu selects between p1-p6 objects, so
pe = p1, or later: pe = p4
for example.
Is there any way this to be done with PyQtgraph?
I really tried all kind things in the last two weeks and nothing seems to work.
I am aware of the
pe.setXLink(p1)
pe.setYLink(p2)
but this only links the axes not the plot object itself. It doesn't work for me.

I implemented something like that using Docks and a DockArea. I simply added several docks stacked below each other.
They can be shown using either by clicking on the labels or by using the raiseDock() method of each dock.
You can simply add the PlotWidget (or any other Widget) to the dock using the addWidget() method of each dock.
The labels can be hidden or locked if you don't want the user to be able to move the docks at runtime.
import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
from pyqtgraph.dockarea import DockArea, Dock
class Accel_GUI():
def __init__(self, window, dock_area):
self.testing = 0
self.pen = pg.mkPen(color='y')
"""Setup the UI"""
self.window = window
self.dock_area = dock_area
self.window.setCentralWidget(self.dock_area)
self.spec_dock = []
self.spec_dock.append(Dock("Spectro 1",
size=(1200, 600),
autoOrientation=False))
self.spec_dock.append(Dock("Spectro 2",
size=(1200, 600),
autoOrientation=False))
self.dock_area.addDock(self.spec_dock[0], "top")
self.dock_area.addDock(self.spec_dock[1], "below", self.spec_dock[0])
if __name__ == "__main__":
app = QtGui.QApplication.instance()
if app is None:
app = QtGui.QApplication(argv)
win = QtGui.QMainWindow()
area = DockArea()
pyqtplot = Accel_GUI(win, area)
win.show()
app.exec_()
There is also an example in the pyqtgraph library that shows how to work with docks.

Related

Add/Remove numerous plots on a PlotWidget in Pyqtgraph

I have a plotWidget (self.widget0) from pyqtgraph on the pyqt GUI. I want to add 200 plots at this widget. What I can do is add each plot one by one:
self.plot0 = self.widget0.plot(xx0, yy0)
self.plot1 = self.widget0.plot(xx1, yy1)
...
self.plot199 = self.widget0.plot(xx199, yy199)
Where xx0, xx1,... and yyo, yy1... are all 1D numpy arrays of the plot.
For this case, I can update the specified plots later but keep all others, for example if I want to update the 100th plot:
self.widget0.removeItem(self.plot99)
self.plot99 = self.widget0.plot(xx99_new, yy99_new)
My question is adding those 200 lines in to self.plot0, self.plot1, self.plot2, self.plot3, ... are so inefficient and difficult. Can anyone advise a way to code this using loop or dictionary?
A dict of plots would suffice, you want a dict so that when an element is removed, the order isn't lost:
self.plots = {}
for plot_num in range(200):
self.plots[plot_num] = self.widget0.plot(xx[plot_num], yy[plot_num])
self.widget0.removeItem(self.plots[99])
self.plots[99] = self.widget0.plot(xx99_new, yy99_new)

How to add edge labels (interactive or permanent ones) for networkx graph in bokeh?

I would like to add label for edges in networkx graph using bokeh. How can I do this?
This question is similar to How to add permanent name labels (not interactive ones) on nodes for a networkx graph in bokeh? but different enough to warrant its own reply. As discussed in the other issue, this is currently a task that is probably harder than it should be to accomplish. I'd really encourage you to open a GitHub Issue to start a discussion of how this can be improved for users.
Here is complete example.
import networkx as nx
from bokeh.io import output_file, show
from bokeh.models import CustomJSTransform, LabelSet
from bokeh.models.graphs import from_networkx
from bokeh.plotting import figure
G=nx.barbell_graph(3,2)
p = figure(x_range=(-3,3), y_range=(-3,3))
p.grid.grid_line_color = None
r = from_networkx(G, nx.spring_layout, scale=3, center=(0,0))
r.node_renderer.glyph.size=15
r.edge_renderer.glyph.line_alpha=0.2
p.renderers.append(r)
This part is all fairly standard. To put labels on edges we must define transforms to extract the start and end coordinates from the layout provider. This code just averages the coordinates to put a label in the center of each edge (labelled by the start-end node numbers):
from bokeh.transform import transform
# add the labels to the edge renderer data source
source = r.edge_renderer.data_source
source.data['names'] = ["%d-%d" % (x, y) for (x,y) in zip(source.data['start'], source.data['end'])]
# create a transform that can extract and average the actual x,y positions
code = """
const result = new Float64Array(xs.length)
const coords = provider.get_edge_coordinates(source)[%s]
for (let i = 0; i < xs.length; i++) {
result[i] = (coords[i][0] + coords[i][1])/2
}
return result
"""
xcoord = CustomJSTransform(v_func=code % "0", args=dict(provider=r.layout_provider, source=source))
ycoord = CustomJSTransform(v_func=code % "1", args=dict(provider=r.layout_provider, source=source))
# Use the transforms to supply coords to a LabelSet
labels = LabelSet(x=transform('start', xcoord),
y=transform('start', ycoord),
text='names', text_font_size="12px",
x_offset=5, y_offset=5,
source=source, render_mode='canvas')
p.add_layout(labels)
show(p)
Edit 07/2022: Added missing var keyword to JavaScript part, would not show labels otherwise in current bokeh version.
I faced the same problem, I did check https://docs.bokeh.org/en/latest/docs/user_guide/styling.html and found it seems bokeh does not support well for the knowledge graph, including edge labels.

Is there a way to make an IPython Notebook output interactivly create an input and execute it?

I was wondering if I can make an output interactively run a piece of code. So if for example I had a class (parts in pseudo-code):
import numpy as np
class test(object):
def __init__():
self.a = np.random.randn(10)
print ## Interactive Output: Click me to view data array##
def show():
print a
So when I create a class instance it should output some interactive link (maybe in html) or something like that and when I click it, the show() method should be called. However, I have no idea how to achieve that.
You could use the widgets shipped with the notebook (for jupyter they are an independent package).
Something like this could do what you want (Python 3):
from IPython.html import widgets
from IPython.display import display
import numpy as np
class Test(object):
def __init__(self, arraylen):
self.a = np.random.randn(arraylen)
self.button = widgets.Button(description = 'Show')
self.button.on_click(self.show)
display(self.button)
def show(self, ev = None):
display(self.a)
self.button.disabled = True
test = Test(10)
You create a button widget when you initialise the class widgets.Button(description = 'Show')
Attach an event to it button.on_click(self.show)
And display the button display(self.button)
In the show method I included a way to disable the button functionality once the array is showed self.button.disabled = True. You can comment this line if you want to show more times the array.

How to deselect all items in QTreeWidget?

I have tried every suggestion I can come across online, from setting flags to using selectionModel
def DatabaseLoadWrapper(self,database, init):
self.treeWidget.currentItemChanged.disconnect(self.updateStackedWidget)
self.DatabaseLoad(database, init)
self.treeWidget.clearSelection()
self.treeWidget.setCurrentItem(self.treeWidget.findItems(self.selectedDatabase,Qt.MatchExactly|Qt.MatchRecursive)[0])
self.treeWidget.currentItemChanged.connect(self.updateStackedWidget)
This is where my code needs to force a selection on the QTreeWidget, none of the code I use throws up any errors but also has no effect on the selection. And I end up with this where the user has selected Database 1 but I need to revert back to having only Database 2 selected:
Edit: The Tree Widget is built using this code:
def setupMenu(self):
self.DatabaseParent = QTreeWidgetItem(['Databases'])
for item in NamesInDatabase():
self.DatabaseParent.addChild(QTreeWidgetItem([item]))
self.AverageParent = QTreeWidgetItem(['Averaged Database'])
self.SortingParent = QTreeWidgetItem(['Waste Composition'])
self.ResultParent = QTreeWidgetItem(['Results'])
self.treeWidget.addTopLevelItem(self.DatabaseParent)
self.treeWidget.addTopLevelItem(self.AverageParent)
self.treeWidget.addTopLevelItem(self.SortingParent)
self.treeWidget.addTopLevelItem(self.ResultParent)
It basically is adding databases, averaged database, waste compisition & results, as fixed parts of the navigation menu and then populating children of databases with the names of the databases in the save file.
Your question fails to expose the part of the code that is causing the problem. By default, setting the current item, as you do, also sets the selection. So this code, for example, correctly sets the selection to item "b":
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setCurrentItem(items[1])
treeWidget.show()
sys.exit(qApp.exec_())
However, I suspect there is code elsewhere in your project that is affecting this. For example, if you had set the selection mode for the QTableWidget selection model to MultiSelection then selections become cumulative:
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
treeWidget.setCurrentItem(items[0])
treeWidget.setCurrentItem(items[2])
treeWidget.show()
sys.exit(qApp.exec_())
However, that still doesn't explain your issue because the clearSelection call should have cleared the preceding selection in any case. Further debugging of your code is needed, for example to check that the wrapper function and the setCurrentItem are being called as you claim. Also check what else is being called subsequent to the DatabaseLoadWrapper.
In Pyside2, This works for me:
If you click on treewidget the selection will be clear.
self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)

link to view changes axis range pyqtgraph

Assume that I have two views\plots created in pyqtgraph and then they are linked.
using lines
p2.setYLink('Plot1')
p2.setXLink('Plot1')
Question is that when we link the views, the ranges of both the views are made equal, which raises issue as one plot appears to be too much zoomed out or zoomed in.
We just want to link the views to zoom together but don't want the ranges to change as the plot looks changed.
Below is sample code that explains the issue visually
import sys
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
#QtGui.QApplication.setGraphicsSystem('raster')
try:
app = QtGui.QApplication(sys.argv)
except RuntimeError:
app = QtCore.QCoreApplication.instance()
x1 = [1,2,3,4,5]
y1 = x1
x2 = [10,20,30,40,50]
y2 = x2
win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views")
win.resize(800,600)
win.addLabel("Linked Views", colspan=2)
win.nextRow()
p1 = win.addPlot(x=x1, y=y1, name="Plot1", title="Plot1")
p2 = win.addPlot(x=x2, y=y2, name="Plot2", title="Plot2: Y linked with Plot1")
p2_state = p2.vb.getState()
p1_state = p1.vb.getState()
p2.setLabel('bottom', "Label to test offset")
p2.setYLink('Plot1') ## test linking by name
p2.setXLink('Plot1')
app.exec_()
To restate the question: you want to have two views that can have different ranges and scales, but when you zoom with the mouse in one view, the other view will zoom by the same amount.
This is not the intended function of the setXLink/setYLink methods, so you will need to implement this by subclassing or monkey-patching the viewboxes. For example:
import pyqtgraph as pg
p1 = pg.plot([1,6,2,4,3])
p2 = pg.plot([30,50,10,70,20])
def scaleBy(*args, **kwds):
pg.ViewBox.scaleBy(p1.plotItem.vb, *args, **kwds)
pg.ViewBox.scaleBy(p2.plotItem.vb, *args, **kwds)
p1.plotItem.vb.scaleBy = scaleBy
p2.plotItem.vb.scaleBy = scaleBy
There is a problem, however, that you need know two things when scaling: how much to scale by (this is the same for both views, so not a problem), and around what point to scale (this is different between the views, so a bit more difficult to determine). The solution to this depends on your desired behavior.

Resources