Open URL from bokeh, not using the taptool - bokeh

I am looking to open a URL from bokeh using OpenURL, but from within a callback for a button, not using taptool. The code below reproduces the issue I'm running into i.e. I can open new tabs when using taptool with OpenURL, but nothing happens when I use OpenURL in a button callback.
(Much of the example is from the bokeh docs: http://docs.bokeh.org/en/0.12.5/docs/user_guide/examples/interaction_open_url.html)
from bokeh.models import ColumnDataSource, OpenURL, TapTool
from bokeh.models.widgets import Button
from bokeh.plotting import figure, output_file, show
from bokeh.layouts import column, widgetbox
from bokeh.io import curdoc
p = figure(plot_width=400, plot_height=400,
tools="tap", title="Click the Dots")
source = ColumnDataSource(data=dict(
x=[1, 2, 3, 4, 5],
y=[2, 5, 8, 2, 7],
color=["navy", "orange", "olive", "firebrick", "gold"]
))
p.circle('x', 'y', color='color', size=20, source=source)
url = "http://www.colors.commutercreative.com/#color/"
taptool = p.select(type=TapTool)
taptool.callback = OpenURL(url=url)
button = Button(label="Generate", button_type="success")
def button_callback():
print('button callback')
OpenURL(url="http://www.google.com")
button.on_click(button_callback)
doc = curdoc()
doc.add_root(column([p,widgetbox(button)]))
doc.title = "Hello World"
Thank you!

OpenURL is a custom version of CustomJS, so it runs only in JavaScript. It's also a class and not a function - you can't just construct an object of class OpenURL, you also have to use its other methods to make it work.
With that being said, you cannot use it with Button since OpenURL expects a data source to replace all the placeholders in the URL. And Button can't have a data source.
Instead, what you need is a regular CustomJS:
b.js_on_click(CustomJS(args=dict(urls=['https://www.google.com',
'https://stackoverflow.com/']),
code="urls.forEach(url => window.open(url))"))
Note that how the solution above will work depends on your browser. E.g. in my case Google Chrome was opening only the first URL, and I had to explicitly allow pop-ups for the generated web page before it started to also open the second URL.
And you don't need bokeh serve to make it work - it will work even on a static web page generated by a call to save or show.

Related

Insert folium map html code inside a bokeh app

I posted this as a followup question on Include folium in bokeh tabs, and now as a new question as well.
I´m trying to render the raw HTML-code from my folium map, but it´s not working.. Any ideas? :)
div = Div(
text=map.get_root().render(),
width=x,
height=y
)
I would much rather be able to render my folium map directly into a bokeh Div object instead of running an Flask app on the side.. I have looked into the possibilities of using an iframe, but there seems to be something off with my code here as well:
div.text = """<iframe srcdoc= """ + map.get_root().render() + """ height=""" + y + """ width=""" + x +"""></iframe>"""
I managed to use a Flask app on the side for the folium map and then use the url as src to my iframe, but then I was having trouble updating the content of that map from my bokeh tool.
Feel free to comment on anything of the above, cheers! :)
Update - Testscript:
from bokeh.models.widgets import Div
from bokeh.layouts import row
from bokeh.plotting import curdoc
import folium
def run():
folium_map = folium.Map(location=(60., 20.))
div = Div(
text=folium_map._repr_html_(),
width=500,
height=500,
)
return row(div)
bokeh_layout = run()
doc = curdoc()
doc.add_root(bokeh_layout)
With map.get_root().render(), you have all the HTML page. If you just want an iframe, you can use the method _repr_html_() of the folium map :
div = Div(
text=map._repr_html_(),
width=x,
height=y
)

With Bokeh, how to hide the icon of a tool while keeping it enabled?

With Bokeh, how to hide the icon of a tool while keeping it enabled?
Some context, I have multiple p.line() plots in a single figure. Each line plot has its own hover tool as per this question.
I don't find it appealing though, to have each hover tool its own icon:
.
So I thought about keeping the multiple Hover tools but hiding them from the user.
What are my options?
Thanks in advance.
First, I would ask myself if a user would like a hidden hover that can't be disabled?
So first option I would suggest is adding a tooltips not to each renderer but to the figure which creates a single HoverTool icon that applies to all lines in the plot like this: (Bokeh v1.3.0)
from bokeh.plotting import figure, show
from bokeh.models import HoverTool, Column, Button, CustomJS
import numpy as np
p = figure(toolbar_location='above',
tooltips=[("x", "#x")]
)
lines = [p.line(np.arange(10), np.random.random(10)) for i in range(3)]
for i in range(len(lines)):
p.add_tools(HoverTool(tooltips=[("x", "#x")], renderers=[lines[i]]))
# button = Button(label='Hide Hover Icon')
# code = ''' hover_btns = document.getElementsByClassName('bk-tool-icon-hover')
# for(i=0; i<hover_btns.length; i++)
# hover_btns[i].style.display = 'none' '''
#
# button.callback = CustomJS(code=code)
show(Column(p,
# button,
))
Then if you really don't want to see any hover icon in your toolbar you could add a JS callback executed on button click that hides all hover icons like this:
from bokeh.plotting import figure, show
from bokeh.models import HoverTool, Column, Button, CustomJS
import numpy as np
p = figure(toolbar_location='above',
# tooltips=[("x", "#x")]
)
lines = [p.line(np.arange(10), np.random.random(10)) for i in range(3)]
for i in range(len(lines)):
p.add_tools(HoverTool(tooltips=[("x", "#x")], renderers=[lines[i]]))
button = Button(label='Hide Hover Icon')
code = ''' hover_btns = document.getElementsByClassName('bk-tool-icon-hover')
for(i=0; i<hover_btns.length; i++)
hover_btns[i].style.display = 'none' '''
button.callback = CustomJS(code=code)
show(Column(p,
button,
))
But doing so requires a user to click on the button first, so maybe a more elegant way to do this would be to call the JS code already at page load using an approach described here.

Adding widgets dynamically in bokeh

I want to add filters dynamically in bokeh, i.e. every time a button is pressed, a new filter is appended. However, the layout gets broken after a new widgets are added: new ones get written over old ones instead of the layout being recomputed. Code example
from bokeh.layouts import row, column
from bokeh.models.widgets import Button, Select
from bokeh.io import curdoc
def add_select():
feature = Select(value='feat', options=["a"])
dynamic_col.children.append(feature)
b1 = Button(label="Add condition", button_type="success")
b1.on_click(add_select)
b2 = Button(label="Apply", button_type="success")
dynamic_col = column()
curdoc().add_root(column(b1, dynamic_col, b2))
Layout before clicking "Add" button
Layout after Select widget gets added
Why don't you use a single list to handle all your widgets ?
from bokeh.layouts import column
from bokeh.models.widgets import Button, Select
from bokeh.io import curdoc
def add_select():
feature = Select(value='feat', options=["a"])
dynamic_col.children.insert(-1, feature)
b1 = Button(label="Add condition", button_type="success")
b1.on_click(add_select)
b2 = Button(label="Apply", button_type="success")
dynamic_col = column(b1, b2)
curdoc().add_root(dynamic_col)
I "insert" instead of "append" the widget to let the 2nd button at the end of the list
I got this result :

How to align and place ipywidgets

Is there a way to control the placement and alignment of ipywidgets (inside jupyter notebook)?
from ipywidgets import widgets
from IPython.display import Javascript, display
event_type_ui = widgets.Text(value='open', description='Test Event')
platform_ui = widgets.Text(value='Android', description='Control Event')
display(event_type_ui)
display(platform_ui)
I would like to specify an offset (in pixels?) to allow the label to fit and to have two controls aligned vertically.
After some fiddling, I was able to get this:
Before:
After:
Here is a copy-paste snippet, if interested:
from ipywidgets import widgets
from IPython.display import Javascript, display
align_kw = dict(
_css = (('.widget-label', 'min-width', '20ex'),),
margin = '0px 0px 5px 12px'
)
platform_ui = widgets.Dropdown(description = 'platform',options=['iPhone','iPad','Android'], **align_kw)
event_type_ui = widgets.Text(value='open', description='Test Event', **align_kw)
os_version_ui = widgets.Text(value='iOS9', description='Operating System', **align_kw)
refresh_ui = widgets.Checkbox(description='Force Refresh', **align_kw)
display(platform_ui,event_type_ui,os_version_ui,refresh_ui)
The layout and styling documentation for ipywidgets says:
Every Jupyter interactive widget has a layout attribute exposing a number of css properties that impact how widgets are laid out.
I was able to coax it into aligning the labels:
from ipywidgets import Text, HBox, VBox, Box
from IPython.display import display
widget1 = Text(value='Cats', description='Test Event', layout='margin-right:5px')
widget2 = Text(value='Oranges', description='Another Test Event')
widget1.width = '200px'
widget2.width = '150px'
display(VBox([widget1, widget2]))
to produced this:
But in general, I doesn't seem like we can directly target layout properties of the description label, just the entire widget itself.
The ipywidgets also have an add_class that can be used for getting into the weeds when you really need it:
import ipywidgets as widgets
from IPython.display import display, HTML
widget_list={}
for label in ("longish description","brief"):
widget_list[label]=widgets.Text(description=label)
widget_list[label].add_class("balanced-text")
display(HTML("<style>div.balanced-text .widget-label {max-width:200px; width:200px}</style>"))
display(widgets.VBox(list(widget_list.values())))

Autodesk Maya model panel resize event

I'm writing a simple tool menu for Maya, and I'd like to stick it to the border of model panel (perspective).
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class TestWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent = self.getMayaWindow())
self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint)
self.setFixedSize(100, 100)
panelPtr = omui.MQtUtil.findControl('modelPanel4')
panel = wrapInstance(long(panelPtr), QtGui.QWidget)
position = panel.mapToGlobal(panel.pos())
self.move(position.x(), position.y() + panel.geometry().height() / 2 - self.geometry().height() / 2)
mainLayout = QtGui.QVBoxLayout(self)
button = QtGui.QPushButton('CLOSE')
button.setFixedSize(80, 80)
button.clicked.connect(self.deleteLater)
mainLayout.addWidget(button)
def getMayaWindow(self):
omui.MQtUtil.mainWindow()
ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(ptr), QtGui.QWidget)
w = TestWidget()
w.show()
The main widget is positioned exactly where I want when it is created (horizontally on the left side of model panel, vertically - in the middle of model panel).
I need to reposition it accordingly when the model panel is resized, but model panel does not emit resized() signal. I'd appreciate any advise.
I've been trying many things to get this working yesterday. I did some additionnal researches today and came to this topic: cgsociety: Creating a floating button inside the viewport
In case of broken link, this is one of the answer:
You can use geometry but there are some issues with triggering
commands based on selection and the undo queue. If you want to go that
route, I would suggest looking into zooHud and zooTriggers (Part of
the zooToolbox)
If you are wanting actual GUI control parented to the viewport, mel
only offers hudslider, hudbutton, and headsUpMessage.
You can also use PyQt and parent in your own custom widgets/layouts or
whatever you want using something like this:
import maya.OpenMayaUI as apiUI import sip
from PyQt4 import QtGui
view = apiUI.M3dView()
apiUI.M3dView.getM3dViewFromModelPanel('modelPanel4', view)
viewWidget = sip.wrapinstance(long(view.widget()), QtCore.QObject)
global myBtn
myBtn = QtGui.QPushButton(viewWidget)
myBtn.setText('testing!')
myBtn.move(100, 100) #Relative to top-left corner of viewport myBtn.show()
You can do anything a full qt widget can do with that, so it's
extremely flexible. but it would require having PyQt installed, which
can be a barrier depending on your tools distribution.
I did a mix of this answer and your code:
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class CustomQWidget(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
mainLayout = QtGui.QVBoxLayout(self)
closeButton = QtGui.QPushButton('CLOSE')
closeButton.setFixedSize(80, 40)
closeButton.clicked.connect(self.deleteLater)
helloButton = QtGui.QPushButton('HELLO')
helloButton.setFixedSize(80, 40)
helloButton.clicked.connect(self.printHello)
#Trying to fix glitchy background / Doesn't work, why?
#Is it because layouts don't have background?
p = self.palette()
p.setColor(self.backgroundRole(), QtCore.Qt.red)
self.setPalette(p)
self.setAttribute(QtCore.Qt.WA_StyledBackground, True)
##############################
mainLayout.addWidget(closeButton)
mainLayout.addWidget(helloButton)
def printHello(self):
print "Hello"
view = omui.M3dView()
omui.M3dView.getM3dViewFromModelPanel('modelPanel4', view) #Given the name of a model panel,
#get the M3dView used by that panel. If this fails, then a panel with the given name could not be located.
viewWidget = wrapInstance(long(view.widget()), QtGui.QWidget)
position = viewWidget.mapToGlobal(viewWidget.pos())
w = CustomQWidget(viewWidget)
w.move(0, viewWidget.geometry().height() / 2 - 100 / 2) #Relative to middle-left corner of viewport
w.show()
One of the issue I have it that the background of the widget is glitched:
If anyone knows why and how to fix it, I'll edit my answer with pleasure.
Else, when running this script from Maya's script editor, the widget follows the panel when it is resized.
I did fix such a problem, but not using Python/PyQt.
The problem itself is, that your Qt Widget is there. I have not found a way to make it not paint its background.
My solution was different: I derived from a Qt Layout, pushed all my widgets into that layout and used MQtUtil to get the QWidget of that modelPanel's modelEditor to attach the "real Qt layout" to it.
Heavy caveat that may make Python not suited: Maya doesn't expect "non-Maya" Layouts to be bound to "real-Maya" Widgets like modelEditors. So you need to listen to QEvents and find out when to destroy your layout, so Maya doesn't crash trying.
set autofillbackground True to fix your background painting issue

Resources