Python Bokeh: How to set focus back to TextInput after layout change? - python

I have a Bokeh app in which I change the layout during runtime. This app contains a TextInput object which should always be focused, but this focus is lost when I change the layout. Is there a way to automatically set the focus back periodically or after each layout change?
I only found this related discussion, but was unable to apply it to my particular case.
Here is a minimal example where the focus is lost each time you type in the TextInput, triggering a layout change:
from bokeh.plotting import curdoc
from bokeh.models import TextInput
from bokeh.models.widgets import Div
from bokeh.layouts import column
def text_input_change(attr, old, new):
layout.children[1] = column(text, text_input)
doc = curdoc()
text = Div(text='Test', width=100)
text_input = TextInput(value='', title='', width=100)
text_input.on_change('value_input', text_input_change)
layout = column(text, text_input)
doc.add_root(layout)

Related

curdoc().add_root() causes bokeh plot rendering to fail silently

The bokeh application below is intended to generate a random dataset when the button is pushed. I am trying to serve the app using the bokeh.client style, where there is one session that may be shared between simultaneous viewers.
If I include the line: curdoc().add_root(column(p,button)) the plot will not be in the browser. I get a blank page with happy messages in JS console. If I remove it, I get a static plot, with no button. Can anyone explain what's wrong with my approach here?
I should add that the app works in the other server style with multiple distinct sessions. There I call bokeh serve myapp.py and don't make calls to the session object.
import numpy as np
from bokeh.plotting import figure, curdoc
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.client import push_session, pull_session
points = 100*np.random.rand(3,100)
points_x = points[0].tolist()
points_y = points[1].tolist()
p = figure(x_range=(0,100), y_range=(0,100))
circle_p = p.circle(x = points_x,
y = points_y,
size = 20,
color = "navy",
alpha = 0.5)
ds = circle_p.data_source
#callback function to update circles
def button_callback():
new_data = dict()
new_points = 100*np.random.rand(3,100)
new_data['x'] = new_points[0].tolist()
new_data['y'] = new_points[1].tolist()
new_data['z'] = new_points[2].tolist()
ds.data = new_data
#Add the button widget thingie to trigger the update
button = Button(label="Update")
button.on_click(button_callback)
# Put the button and plot in a layout in document
curdoc().add_root(column(p,button))
#create a session
session = push_session(curdoc())
session.show(p)
session.loop_until_closed()
You just want
session.show()
not
session.show(p)
Because you want to show the whole document, not just the plot. The first version works for me with Bokeh 0.12.6 (the latter also kind of works, but the plot is duplicated twice. My guess is you are using an older version that also had some layout bugs)

Adding objects dynamically in bokeh server application

I would like to add objects dynamically on the bokeh server. The example I am trying to run is the following bokeh server app:
from bokeh.layouts import column
from bokeh.plotting import curdoc
from bokeh.models import Button
def add_button():
print("adding button")
curdoc().add_root(column(button, button2))
button = Button(label="Start", button_type="success")
button.on_click(add_button)
button2 = Button(label="Next", button_type="success")
curdoc().add_root(column(button))
Many thanks for any help.
Did you want to keep adding a new button each time?
if so try this :
from bokeh.layouts import column, layout
from bokeh.plotting import curdoc
from bokeh.models import Button
from bokeh.models.widgets import Div
def add_button():
print("adding button")
layout.children.append(Button(label="Hi I am another button", button_type="success"))
button = Button(label="Click to add a button", button_type="success")
button.on_click(add_button)
layout = layout([[button]])
curdoc().add_root(layout)
If you only wanted to add a new button once, then just append Button2.

Python Bokeh send additional parameters to widget event handler

I want to send additional data to a bokeh event handler (e.g. an on_change or on_click method). A minimal example that increments or decrements an integer is below (I run this app with 'bokeh serve --show app.py). I had to write separate event handlers that do almost identical things in this example. To write this app with just one event handler function, I need to pass additional data or the event handler must know the calling object. How do I do that?
from bokeh.plotting import curdoc
from bokeh.models.widgets import Button, Paragraph
from bokeh.layouts import widgetbox
minus = Button(label='-')
plus = Button(label='+')
text = Paragraph(text='0')
def minus_callback():
text.text = str(int(text.text) - 1)
def plus_callback():
text.text = str(int(text.text) + 1)
minus.on_click(minus_callback)
plus.on_click(plus_callback)
# I would prefer to just use one callback and pass additional data to it:
# minus.on_click(callback, action='decrement')
# plus.on_click(callback, action='increment')
layout = widgetbox(minus, plus, text)
curdoc().add_root(layout)
The standard functools.partial facility that is built into python works fine with Bokeh callbacks.
from functools import partial
from bokeh.plotting import curdoc
from bokeh.models.widgets import Button, Paragraph
from bokeh.layouts import widgetbox
minus = Button(label='-')
plus = Button(label='+')
text = Paragraph(text='0')
def callback(foo):
print(foo)
minus.on_click(partial(callback, foo="minus"))
plus.on_click(partial(callback, foo="plus"))
layout = widgetbox(minus, plus, text)
curdoc().add_root(layout)
Extending on #bigreddot's answer, this code outputs the updated value to the text attribute of the Paragraph object.
from functools import partial
from bokeh.plotting import curdoc
from bokeh.models.widgets import Button, Paragraph
from bokeh.layouts import column
def callback(update):
text.text = str(int(text.text) + update)
minus = Button(label='-')
plus = Button(label='+')
text = Paragraph(text='0')
minus.on_click(partial(callback, update=-1))
plus.on_click(partial(callback, update=+1))
layout = column(minus, plus, text)
curdoc().add_root(layout)

How to embed a plot directly into a Window (python; QT;)

I wanna embed a Matplotlib plot directly into a window, QMainWindow.
It should be part of my program with a more complex GUI. ;)
The only way I found was to add the figure as widget into a QTabWidget.
See sample code below.
I lost the link to the webpage what inspired me.
Is there any way to embed the figure directly into the windows like other elements (buttons, textfield, textarea, ...)?
import sys
from PyQt4.QtGui import QApplication, QMainWindow, QDockWidget, QVBoxLayout,QTabWidget, QWidget
from matplotlib import pyplot
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
a = QApplication(sys.argv)
w = QMainWindow()
t = QTabWidget(w)
Tab1 = QWidget()
t.addTab(Tab1, '1st Plot')
t.resize(1280, 300)
x = [1, 2, 3]
Fig1 = pyplot.Figure();
Plot = Fig1.add_subplot(111);
Plot.plot(x)
Plot.grid();
layout = QVBoxLayout();
layout.addWidget(FigureCanvasQTAgg(Fig1));
Tab1.setLayout(layout);
w.showMaximized()
sys.exit(a.exec_())
Thank you very much.
FigureCanvasQtAgg is just a QWidget like any of the other controls you mentioned. The main difference being it doesn't allow you to pass a parent in the constructor like when you write
t = QTabWidget(w)
You can achieve the same with FigureCanvasQtAgg by calling setParent
canvas = FigureCanvasQtAgg(Fig1)
canvas.setParent(w)
You can also use QMainWindow's setCentralWidget method to add the matplotlib FigureCanvas directly to your main window. However, if you want a more complex gui with other controls I don't see any real problems with your current approach.
Lastly, you shouldn't really be using pyplot when embedding matplotlib. Stick with the object oriented API. Take a look at this example.
This is what I use for PySide. FigureCanvasQTAgg is a qt widget, so you don't need the PlotWidget class. I just find it useful, because it creates the figure for me. It also makes it useful for other projects, because you don't have to deal with importing the right matplotlib objects. From the PlotWidget just call the axes for all of your plotting needs.
from PySide import QtGui
import matplotlib
matplotlib.use("Qt4Agg")
matplotlib.rcParams["backend.qt4"] = "PySide"
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class PlotWidget(FigureCanvas):
'''Plotting widget that can be embedded in a PySide GUI.'''
def __init__(self, figure=None):
if figure is None:
figure = Figure(tight_layout=True)
super().__init__(figure)
self.axes = self.figure.add_subplot(111)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
# end class
if __name__ == "__main__":
import sys
a = QtGui.QApplication(sys.argv)
w = QtGui.QMainWindow()
w.show()
p = PlotWidget()
p.axes.plot([])
nav = NavigationToolbar(p, w)
w.addToolBar(nav)
w.setCentralWidget(p)
# or
# container = QtGui.QWidget()
# layout = QtGui.QHBoxLayout()
# container.setLayout(layout)
# w.setCentralWidget(container)
# layout.addWidget(p)
sys.exit(a.exec_())

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

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.

Categories

Resources