How to modify the navigation toolbar easily in a matplotlib figure window? - python

Is it possible to do something like the following to modify the navigation toolbar in matplotlib?
Generate a figure window, with: fig = figure()
Get a reference of the navigation tool-bar, with: tbar = fig.get_navigation_toolbar(),
or better yet, just by: tbar = fig.navtbar
Modify the tool-bar through the reference tbar, such as delete/add/edit a button with something like this:
tbar.add_button(<a Button object>);
tbar.remove_button(a reference to a button);
tbar.edit_button(a reference to a button);
Update the figure with: fig.canvas.draw()
Thank you very much.

The way I found to remove unwanted toolbar items is making a subclass, which is instantiated and used in a GTK application. As I manually create Figure, FigureCanvas and NavigationToolbar objects anyway, this was the easiest way.
class NavigationToolbar(NavigationToolbar2GTKAgg):
# only display the buttons we need
toolitems = [t for t in NavigationToolbar2GTKAgg.toolitems if
t[0] in ('Home', 'Pan', 'Zoom', 'Save')]
If you want to create custom buttons, you should take a look on the definition of NavigationToolbar2 in backend_bases. You can easily add your own entries to the toolitems list and define appropriate callback functions in your toolbar subclass.

With MPL 1.2.1 it is possible to get an handler of the navigation toolbar of a standard MPL figure through figure.canvas.toolbar. I'm not sure about previous versions.
At least with the QT backend it is possible to add arbitrary widgets to the navigation toolbar using the QT method .addWidget(). I suppose other backends will work using similar methods, but I haven't tested them.
Here it is a working example (using the QT backend) that adds a QLineEdit() to the navigation toolbar to change the title of a MPL figure (run from IPython (pylab) with run -i ..., then launch test()):
from PySide import QtGui, QtCore
def test():
plot([1,2,3], lw=2)
q = qt4_interface(gcf())
return q # WARNING: it's paramount to return the object otherwise, with
# no references, python deletes it and the GUI doesn't respond!
class qt4_interface:
def __init__(self,fig):
self.fig = fig
toolbar = fig.canvas.toolbar
self.line_edit = QtGui.QLineEdit()
toolbar.addWidget(self.line_edit)
self.line_edit.editingFinished.connect(self.do_something)
def do_something(self, *args):
self.fig.axes[0].set_title(self.line_edit.text())
self.fig.canvas.draw()
#f = open('l','a'); f.write('yes\n'); f.flush(); f.close()

The previous answers work but are very backend-specific. A slighly more elegant solution is to subclass NavigationToolbar2, as done in this other answer: Matplotlib/Tkinter - customizing toolbar tooltips
There the aim was to change the tooltips, but adding or removing a button is equally trivial.

In addition to torfbotl's solution above, you might have an extra button hanging at the end (the one with the green check mark).
That can be mitigated in the subclass constructor:
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class PanOnlyToolbar(NavigationToolbar):
# only display the buttons we need
toolitems = [t for t in NavigationToolbar2GTKAgg.toolitems if
t[0] in ("Pan", )]
def __init__(self, *args, **kwargs):
super(PanOnlyToolbar, self).__init__(*args, **kwargs)
self.layout().takeAt(1) #or more than 1 if you have more buttons

Using PyQt5 and matplotlib version '3.0.2'
If you want to add some buttons just follow the doc given by the class NavigationToolbar2() that is initialised in NavigationToolbar2QT() wich is imported from matplotlib.backends.backend_qt5agg :
# list of toolitems to add to the toolbar, format is:
# (
# text, # the text of the button (often not visible to users)
# tooltip_text, # the tooltip shown on hover (where possible)
# image_file, # name of the image for the button (without the extension)
# name_of_method, # name of the method in NavigationToolbar2 to call
# )
So you need to redefine your class as previously said (you can also see under, the pre-defined buttons available atm). In my case I wanted to remove 2 buttons ('Save' and 'Subplots' that I commented) so that gave me :
class NavigationToolbar2QT(NavigationToolbar2QT):
# only display the buttons we need
NavigationToolbar2QT.toolitems = (
('Home', 'Reset original view', 'home', 'home'),
('Back', 'Back to previous view', 'back', 'back'),
('Forward', 'Forward to next view', 'forward', 'forward'),
(None, None, None, None),
('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'),
('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'),
# ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'),
(None, None, None, None),
# ('Save', 'Save the figure', 'filesave', 'save_figure'),
)
And calling the NavigationToolbar2QT (still in my case) :
figure = plt.figure()
canvas = FigureCanvas(figure)
toolbar = NavigationToolbar2QT(canvas, self)

I found that just that
fig = plt.figure()
toolbar = fig.canvas.manager.toolbar
tb=toolbar.toolitems
while len(tb)>0:
tb.pop(0)
worked to remove all tools, and popping individual tools worked too. That said,
toolbar.toolitems=[]
didnt work, so the code must have another reference to this array somewhere.

Related

PyQt dynamically switch between QSpinBox to QTimeEdit widgets depending on QComboBox data

I have a combo box with 2 options 'time' or 'interval' when 'time' is selected I would like to show a QTimeEdit and When 'interval is selected I would like to show a QSpinBox.
I can hide the interval widget and show the time widget but I do not know how to re-position it so that it is displayed where the interval widget was.
Here is what I have so far:
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc
class MainWindow(qtw.QMainWindow):
def __init__(self):
super().__init__()
form = qtw.QWidget()
self.setCentralWidget(form)
layout = qtw.QFormLayout()
form.setLayout(layout)
self.when_list = qtw.QComboBox(currentIndexChanged=self.on_change)
self.when_list.addItem('Every X Minutes', 'interval')
self.when_list.addItem('At a specific time', 'time')
self.interval_box = qtw.QSpinBox()
self.time_edit = qtw.QTimeEdit()
self.event_list = qtw.QComboBox()
self.event_list.addItem('Event 1')
self.event_list.addItem('Event 2')
self.event_msg = qtw.QLineEdit()
self.add_button = qtw.QPushButton('Add Event', clicked=self.add_event)
layout.addRow(self.when_list, self.interval_box)
layout.addRow(self.event_list)
layout.addRow(self.event_msg)
layout.addRow(self.add_button)
self.show()
def on_change(self):
if self.when_list.currentData() == 'time':
# Hide interval
self.interval_box.hide()
# Show time - how do I put this where interval_box was?
self.time_edit.show()
elif self.when_list.currentData() == 'interval':
# Hide time - ERROR object has no attribute time_edit
self.time_edit.hide()
# show interval - ERROR object has no attribute interval_box
self.interval_box.show()
def add_event(self):
pass
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec())
How can I fix the errors and dynamically switch between the widgets?
Instead of hiding and showing widgets, you can use a QStackedWidget (which is similar to a tab widget, but without tabs) and use the combo box signal to select which one show.
Note that you should not connect to a *changed signal in the constructor if you're going to set properties that could call that signal and the slot uses objects that don't exist yet: in your case you connected the currentIndexChanged signal in the constructor, but that signal is always called when an item is added to a previously empty combobox, and since at that point the time_edit object has not been created, you'll get an AttributeError as soon as you add the first item.
While using signal connections in the constructor can be useful, it must always be used with care.
class MainWindow(qtw.QMainWindow):
def __init__(self):
# ...
self.when_list = qtw.QComboBox()
self.when_list.addItem('Every X Minutes', 'interval')
self.when_list.addItem('At a specific time', 'time')
self.when_list.currentIndexChanged.connect(self.on_change)
# ...
self.time_stack = qtw.QStackedWidget()
self.time_stack.addWidget(self.interval_box)
self.time_stack.addWidget(self.time_edit)
layout.addRow(self.when_list, self.time_stack)
# ...
def on_change(self):
if self.when_list.currentData() == 'time':
self.time_stack.setCurrentWidget(self.time_edit)
elif self.when_list.currentData() == 'interval':
self.time_stack.setCurrentWidget(self.interval_box)
Another solution could be to remove the widget that is to be hidden and insert a row in the same place using QFormLayout functions, but while that layout is useful in many situations, it's mostly intended for pretty "static" interfaces. The alternative would be to use a QGridLayout, which allows setting more widgets on the same "cell": in that case, you can easily toggle visibility of items, but it could create some issues as the layout would also try to adjust its contents everytime (which can be solved by using setRetainSizeWhenHidden() for the widget's size policy.

How can I add a pane to nuke and use it inside the GUI?

I am trying to create a nuke pane that I can call the same way the embedded panes are called.
So far, I am able to see it in the 'custom windows' menu, but when I press on the button, the panel shows up empty.
I know the code is working because when I copy/paste it in the script editor, it works as desired, but I am probably missing some function that calls for my pane to be populated.
I am using QtWidgets, and this is my reduced code:
class NukePanel_AssetManager(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.setLayout(QtWidgets.QVBoxLayout())
myTable = QtWidgets.QTableWidget()
# all my functions and the things I am creating #
self.layout().addWidget(myTable)
pane = nuke.getPaneFor('Properties.1')
panels.registerWidgetAsPanel('NukePanel_AssetManager', 'Asset Manager', 'uk.co.thefoundry.NukePanel_AssetManager', True).addToPane(pane)
What am I missing?
It probably has to do with where the panel is being defined. Since the first argument in registerWidgetAsPanel is a string-representation of the name of the panel class (so weird), it works in the script panel (because it is defined right there), but not when you're loading it via init (because it is actually being defined in your module). If your module is named MyPanels:
from nukescripts.panels import registerWidgetAsPanel
pathToClass='MyPanels.NukePanel_AssetManager' #the full python path to your panel
HRName='Asset Manager' #the Human-readable name you want for your panel
regName='NukePanel.AssetManager' #the internal registered name for this panel (used for saving and loading layouts)
registerWidgetAsPanel(pathToClass, HRName, regName)
Nuke has some some strange and inconsistent module importing; if you still have trouble, try adding your panel/module as a variable under the nuke module, which is available everywhere:
import nuke
from MyPanels import NukePanel_AssetManager
nuke.NukePanel_AssetManager=NukePanel_AssetManager
Hacky, but has helped me out a lot in the past!
Modal pane
The simplest way to have a regular modal pane in Nuke GUI is the following one:
import nuke
pane = nuke.Panel('Set number of Blur nodes')
pane.addSingleLineInput('Number of Blur nodes', '')
pane.show()
someInput = pane.value('Number of Blur nodes')
if someInput <= "0":
nuke.message('ERROR\nSet a number between 1 and 3')
elif someInput >= "4":
nuke.message('ERROR\nSet a number between 1 and 3')
elif "1" <= someInput <= "3":
nuke.message('number of Blur nodes: ' + someInput)
for _ in list(range(1, int(someInput[0]) + 1)):
nuke.createNode('Blur', inpanel = False)
Non-modal pane
Of course, you often want a non-modal panes, like depicted on a picture:
import nuke
import nukescripts
class Panes:
def createPane(self):
pythonPanel = nukescripts.PythonPanel('Keying Panel')
knob = nuke.Keyer_Knob('keyer', 'range')
knob.setValue( [0.0, 0.075, 0.5, 1.0] )
pythonPanel.addKnob(knob)
pythonPanel.addToPane()
return self
Panes().createPane()
This solved it!!
import nukescripts
import myPanelScript
pathToClass='myPanelScript.NukePanel_AssetManager' #the full python path to your panel
HRName='Asset Manager' #the Human-readable name you want for your panel
regName='NukePanel.AssetManager' #the internal registered name for this panel (used for saving and loading layouts)
nukescripts.panels.registerWidgetAsPanel(pathToClass, HRName, regName, True).addToPane(nuke.getPaneFor('Properties.1'))
Thank you all for your answers, as this would have not been possible for me without your inputs!!

Can we call the button using select(A single selection widget) in bokeh?

https://docs.bokeh.org/en/latest/docs/user_guide/interaction/widgets.html (You can see select)
I want to call a button, text input and slider in using select.
plot_button = Button(label="plot")
color_button = Button(label="Color", disabled=True)
axis_start_value_text = TextInput(title=" Start value=", value="270")
axis_slider = Slider(value=0, start=0, end=100, step=1, title="Title")
button_map = {
"plot": plot_button,
"Color": color_button,
"Start value": axis_start_value_text,
"Slider": axis_slider
}
button_call = Select(title="Button call", options=sorted(button_map.keys()),
value="plot")
In the Select button, when I click on a button, text_input and slider the button appears on the screen that I clicked. İs it possible?
I put up an example app on bitbucket.
You can give css_classes to your widgets, or widgetbox, and then access the div elements contaning the widgets in a CustomJS callback.
I have not found a way to set the display to None from the start, couldn't do it with customm css. So I use a timeout function, you'd need to tweak the timing of the timeout function if you don't want the widgets to appear before they are made invisible.
EDIT:
I updated the code, in fact there is a clean way to hide the widgets initially without using a timeout callback. I use a custom css for that with display:none for all objects with class "hidden". Then in the CustomJS you just need to add or remove the "hidden" to the class name of the widgetboxes.

Qt menu artifact when calling input dialog

I am building a PyQt application which is supposed to receive mouse right-click drags on a QGraphicsView, draw a "lasso" (a line stretching from the drag origin to the mouse position, and a circle at the mouse position), and then, on mouse release, erase the lasso graphics and display an input dialog for the next part of the app.
For some reason, when I use the mouse to click "Ok" on the input dialog, a menu artifact appears on the QGraphicsView which contained the lasso. The menu artifact is a drop-down menu line that says "(check mark) Exit". Occasionally it may be the context menu for one of my custom QGraphicsObjects as well - but for whatever reason, calling the dialog and then clicking "Ok" results in an unintended right-click-like event on the QGraphicsView.
This only seems to happen when the last step before method return is the QInputDialog - replacing it with a pass or a call to some other method does not result in the artifact. I'd be very grateful to anyone with a clue to what is causing this problem!
Here's the minimal code:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QMainWindow):
# The app main window.
def __init__(self):
super(Window, self).__init__()
# Initialize window UI
self.initUI()
def initUI(self, labelText=None):
# Set user-interface attributes.
# Set up menu-, tool-, status-bars and add associated actions.
self.toolbar = self.addToolBar('Exit')
# Create a menu item to exit the app.
exitAction = QtGui.QAction(QtGui.QIcon('icons/exit.png'), '&Exit', self)
exitAction.triggered.connect(QtGui.qApp.quit)
self.toolbar.addAction(exitAction)
# Create the main view.
self.viewNetwork = NetworkPortal()
self.viewNetwork.setMinimumWidth(800)
self.viewNetwork.setMinimumHeight(800)
self.setCentralWidget(self.viewNetwork)
self.show()
class NetworkPortal(QtGui.QGraphicsView):
# A view which allows you to see and manipulate a network of nodes.
def __init__(self):
super(NetworkPortal, self).__init__(QtGui.QGraphicsScene())
# Add the CircleThing graphic to the scene.
circleThing = CircleThing()
self.scene().addItem(circleThing)
class CircleThing(QtGui.QGraphicsEllipseItem):
# Defines the graphical object.
def __init__(self):
super(CircleThing, self).__init__(-10, -10, 20, 20)
# Set flags for the graphical object.
self.setFlags(
QtGui.QGraphicsItem.ItemIsSelectable |
QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges
)
self.dragLine = None
self.dragCircle = None
def mouseMoveEvent(self, event):
# Reimplements mouseMoveEvent to drag out a line which can, on
# mouseReleaseEvent, form a new Relationship or create a new Thing.
# If just beginning a drag,
if self.dragLine == None:
# Create a new lasso line.
self.startPosX = event.scenePos().x()
self.startPosY = event.scenePos().y()
self.dragLine = self.scene().addLine(
self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y(),
QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine)
)
# Create a new lasso circle at the location of the drag position.
self.dragCircle = QtGui.QGraphicsEllipseItem(-5, -5, 10, 10)
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
self.scene().addItem(self.dragCircle)
# If a drag is already in progress,
else:
# Move the lasso line and circle to the drag position.
self.dragLine.setLine(QtCore.QLineF(self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y()))
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
def mouseReleaseEvent(self, event):
# If the line already exists,
if self.dragLine != None:
# If the released button was the right mouse button,
if event.button() == QtCore.Qt.RightButton:
# Clean up the link-drag graphics.
self.scene().removeItem(self.dragLine)
self.dragLine = None
self.scene().removeItem(self.dragCircle)
self.dragCircle = None
# Create the related Thing.
# Display input box querying for name value.
entry, ok = QtGui.QInputDialog.getText(None, 'Enter some info: ',
'Input:', QtGui.QLineEdit.Normal, '')
return
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
newWindow = Window()
sys.exit(app.exec_())
This is a guess on my part, but I've seen this kind of weird thing before.
Some Qt widgets have default behavior on certain types of events. I've never used QGraphicsView, but often the default for a right click is to open a context-sensitive pop-up menu (usually useless, in my experience). That may be happening in your case, which would explain why you only see this when there's a right click.
You can suppress the default Qt behavior by calling event.ignore() before returning from mouseReleaseEvent.
There was not a direct answer to the cause of this bug, but using a QTimer.singleShot() to call the dialog (in combination with a QSignalMapper in order to enter parameters) is a functional workaround that separates the dialog from the method in which the bug was occurring. Thanks to #Avaris for this one.

matplotlib show figure again

When using matplotlib:
from matplotlib import pyplot as plt
figure = plt.figure()
ax = figure.add_subplot(111)
ax.plot(x,y)
figure.show() # figure is shown in GUI
# How can I view the figure again after I closed the GUI window?
figure.show() # Exception in Tkinter callback... TclError: this isn't a Tk application
figure.show() # nothing happened
So my questions are:
How can I get the previous plot back if I have called figure.show()?
Is there a more convenient alternative to figure.add_suplot(111) if I have multiple figures and thus from pylab import *; plot(..); show() seems not a solution I'm looking for.
And what I eagerly want is
showfunc(stuff) # or
stuff.showfunc()
where stuff is an object containing all the plots arranged in one picture, and showfunc is STATELESS(I mean, each time I call it, I behaves as if it's first time called). Is this possible when working with matplotlib?
I can't find a satisfactory answer, so I handle this problem by writing a custom Figure class extending matplotlib.figure.Figure and providing a new show() method, which create a gtk.Window object each time called.
import gtk
import sys
import os
import threading
from matplotlib.figure import Figure as MPLFigure
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NaviToolbar
class ThreadFigure(threading.Thread):
def __init__(self, figure, count):
threading.Thread.__init__(self)
self.figure = figure
self.count = count
def run(self):
window = gtk.Window()
# window.connect('destroy', gtk.main_quit)
window.set_default_size(640, 480)
window.set_icon_from_file(...) # provide an icon if you care about the looks
window.set_title('MPL Figure #{}'.format(self.count))
window.set_wmclass('MPL Figure', 'MPL Figure')
vbox = gtk.VBox()
window.add(vbox)
canvas = FigureCanvas(self.figure)
vbox.pack_start(canvas)
toolbar = NaviToolbar(canvas, window)
vbox.pack_start(toolbar, expand = False, fill = False)
window.show_all()
# gtk.main() ... should not be called, otherwise BLOCKING
class Figure(MPLFigure):
display_count = 0
def show(self):
Figure.display_count += 1
thrfig = ThreadFigure(self, Figure.display_count)
thrfig.start()
Make this file as start file of IPython. And
figure = Figure()
ax = figure.add_subplot(211)
... (same story as using standard `matplotlib.pyplot` )
figure.show()
# window closed accidentally or intentionally...
figure.show()
# as if `.show()` is never called
Works! I never touched GUI programming, and don't know if this would have any side-effect. Comment freely if you think something should be done that way.

Categories

Resources