Plotly allows you to display text fields when hovering over a point on a scatterplot. Is it possible to instead display an image associated with each point when the user hovers over or clicks on it? I am mostly just using the web interface, but I could instead push my ggplot from R.
Unfortunately, there is no easy way to display images on hover on plotly graphs at the moment.
If you are willing to learn some javascript, plotly's embed API allows you to customize hover (as well as click) interactivity.
Here is an example of a custom hover interaction showing images on top of a plotly graph. The javascript source code can be found here.
Images on hover is now available by Plotly lib. Here is a sample:
from dash import Dash, dcc, html, Input, Output, no_update
import plotly.graph_objects as go
import pandas as pd
# Small molcule drugbank dataset
# Source: https://raw.githubusercontent.com/plotly/dash-sample-apps/main/apps/dash-drug-discovery/data/small_molecule_drugbank.csv'
data_path = 'datasets/small_molecule_drugbank.csv'
df = pd.read_csv(data_path, header=0, index_col=0)
fig = go.Figure(data=[
go.Scatter(
x=df["LOGP"],
y=df["PKA"],
mode="markers",
marker=dict(
colorscale='viridis',
color=df["MW"],
size=df["MW"],
colorbar={"title": "Molecular<br>Weight"},
line={"color": "#444"},
reversescale=True,
sizeref=45,
sizemode="diameter",
opacity=0.8,
)
)
])
# turn off native plotly.js hover effects - make sure to use
# hoverinfo="none" rather than "skip" which also halts events.
fig.update_traces(hoverinfo="none", hovertemplate=None)
fig.update_layout(
xaxis=dict(title='Log P'),
yaxis=dict(title='pkA'),
plot_bgcolor='rgba(255,255,255,0.1)'
)
app = Dash(__name__)
app.layout = html.Div([
dcc.Graph(id="graph-basic-2", figure=fig, clear_on_unhover=True),
dcc.Tooltip(id="graph-tooltip"),
])
#app.callback(
Output("graph-tooltip", "show"),
Output("graph-tooltip", "bbox"),
Output("graph-tooltip", "children"),
Input("graph-basic-2", "hoverData"),
)
def display_hover(hoverData):
if hoverData is None:
return False, no_update, no_update
# demo only shows the first point, but other points may also be available
pt = hoverData["points"][0]
bbox = pt["bbox"]
num = pt["pointNumber"]
df_row = df.iloc[num]
img_src = df_row['IMG_URL']
name = df_row['NAME']
form = df_row['FORM']
desc = df_row['DESC']
if len(desc) > 300:
desc = desc[:100] + '...'
children = [
html.Div([
html.Img(src=img_src, style={"width": "100%"}),
html.H2(f"{name}", style={"color": "darkblue"}),
html.P(f"{form}"),
html.P(f"{desc}"),
], style={'width': '200px', 'white-space': 'normal'})
]
return True, bbox, children
if __name__ == "__main__":
app.run_server(debug=True)
More info: https://dash.plotly.com/dash-core-components/tooltip
Related
I would like to work in Jupyter Notebook though the project with one plot. Update it with a new data or new layout, maybe add/delete additional plot nearby it and so on… In Bokeh I could use smth similar to:
target = show(layout, notebook_handle=True)
push_notebook(handle=target)
In Holoviews I found how to feed new data to the existing plot:
pipe = Pipe(data=[])
Image = hv.DynamicMap(hv.Image, streams=[pipe1])
pipe.send(np.random.rand(3,2)) #data change
But is there any solution to manage live layout update in Holoviews? Is it possible to update existed plot by .opts() construction? In this example I will get a new plot:
pipe = Pipe(data=[])
Image = hv.DynamicMap(hv.Image, streams=[pipe])
Image.opts(width=1000,height=1000)
#######new cell in jupyter notebook############
Image.opts(width=100,height=100)
Here is a brilliant answer I have got on my question:
import param
import panel as pn
import numpy as np
import holoviews as hv
from holoviews.streams import Pipe
pn.extension()
pipe = Pipe(data=[])
class Layout(param.Parameterized):
colormap = param.ObjectSelector(default='viridis', objects=['viridis', 'fire'])
width = param.Integer(default=500)
update_data = param.Action(lambda x: x.param.trigger('update_data'), label='Update data!')
#param.depends("update_data", watch=True)
def _update_data(self):
pipe.send(np.random.rand(3,2))
layout = Layout()
Image = hv.DynamicMap(hv.Image, streams=[pipe]).apply.opts(cmap=layout.param.colormap, width=layout.width)
pdmap = pn.panel(Image)
playout = pn.panel(layout)
def update_width(*events):
for event in events:
if event.what == "value":
pdmap.width = event.new
layout.param.watch(update_width, parameter_names=['width'], onlychanged=False)
pn.Row(playout, pdmap)
https://discourse.holoviz.org/t/how-to-update-holoviews-plots-width-height/1947
I am trying to use leaflet to show a smaller map than usual so I don't want to use the normal tiling system. I don't care about smooth zooming and loading higher resolution tiles when needed. Instead I am trying to add a raster image from an image file. Lets say this file that comes up when I google "hand drawn map"
So I try
download.file('https://external-preview.redd.it/7tYT__KHEh8FBKO6bsqPgC02OgLCHAFVPyjdVZI4bms.jpg?auto=webp&s=ff2fa2e448bb92c4ed6c049133f80370f306acb3',
destfile = 'map.jpg')
map = raster::raster('map.jpg')
# it seems like i need a projection to use a raster image.
# not sure what controls do I have over this, especially in
# absence of a proper map layer and it's likely
# part of the solution
crs(map) = CRS("+init=epsg:4326")
leaflet() %>%
leaflet::addRasterImage(map)
The resulting output is nothing like the input image
How do I take an arbitrary image and place in on a leaflet map?
I failed to find the exact reason why addRasterImage fails here but I found reports that it doesn't behave well on L.CRS.Simple projection, which is what you'll want to use to show a simple rectangle image.
Using htmlwidgets::onRender makes it possible to directly use the javascript function L.imageOverlay to add the image you want
library(leaflet)
# minimal custom image
imageURL = 'https://external-preview.redd.it/7tYT__KHEh8FBKO6bsqPgC02OgLCHAFVPyjdVZI4bms.jpg?auto=webp&s=ff2fa2e448bb92c4ed6c049133f80370f306acb3'
# get image data. we'll use this to set the image size
imageData =
magick::image_read(imageURL) %>% image_info()
leaflet(options = leafletOptions(crs = leafletCRS('L.CRS.Simple'))) %>%
htmlwidgets::onRender(glue::glue("
function(el, x) {
var myMap = this;
var imageUrl = '<imageURL>';
var imageBounds = [[<-imageData$height/2>,<-imageData$width/2>], [<imageData$height/2>,<imageData$width/2>]];
L.imageOverlay(imageUrl, imageBounds).addTo(myMap);
}
",.open = '<', .close = '>'))
For a large image like this if you want to make the image smaller you can either scale down using the imageBounds in javascript side or set minZoom to a negative value and use setView to start out zoomed out.
leaflet(options =
leafletOptions(crs = leafletCRS('L.CRS.Simple'),
minZoom = -1)) %>%
setView(0,0,zoom = -1) %>%
htmlwidgets::onRender(glue::glue("
function(el, x) {
var myMap = this;
var imageUrl = '<imageURL>';
var imageBounds = [[<-imageData$height/2>,<-imageData$width/2>], [<imageData$height/2>,<imageData$width/2>]];
L.imageOverlay(imageUrl, imageBounds).addTo(myMap);
}
",.open = '<', .close = '>'))
I want iterate over a list of string, output the string as plain text in jupyter lab then interactively highlight a substring to get easily the start index of the substring and the length. The goal is to do a quick annotation of text and get the coordinates of the substring.
Is it easy or even possible to do something like this with jupyter notebook (lab)? If then How?
I had a look at ipywidgets but couldn't find something for this use case.
Here's an example with the RangeSlider:
import ipywidgets
input_string = 'averylongstring'
widg = ipywidgets.IntRangeSlider(
value = [0, len(input_string)],
min=0, max=len(input_string)
)
output_widg = ipywidgets.Text()
display(widg)
display(output_widg)
def chomp_string(widg):
start,end = tuple(widg['new'])
output_widg.value = input_string[start: end]
widg.observe(chomp_string, names='value')
You can implement this using jp_proxy_widgets. See the following screenshot:
Note that there are warnings about compatibility for selection protocols -- I only tested this on Chrome on a Mac. Also I don't know why the indices are off by one
(select_callback(startOffset+1, endOffset+1);)
Please see https://github.com/AaronWatters/jp_proxy_widget for more information
Edit: Here is the pastable text as requested:
import jp_proxy_widget
select_widget = jp_proxy_widget.JSProxyWidget()
txt = """
Never gonna give you up.
Never gonna let you down.
Never gonna run around and
desert you.
"""
selected_text = None
def select_callback(startOffset, endOffset):
global selected_text
selected_text = txt[startOffset: endOffset]
print ("Selected", startOffset, endOffset, repr(selected_text))
select_widget.js_init("""
// (Javascript) Add a text area.
element.empty()
$("<h3>please select text:</h3>").appendTo(element);
var textarea = $('<textarea cols="50" rows="5">' + txt + "</textarea>").appendTo(element);
// Attach a select handler that calls back to select_callback.
var select_handler = function(event) {;
var target = event.target;
var startOffset = target.selectionStart;
var endOffset = target.selectionEnd;
select_callback(startOffset+1, endOffset+1);
};
textarea[0].addEventListener('select', select_handler);
""", txt=txt, select_callback=select_callback)
# display the widget
select_widget.debugging_display()
I'm having some troubles with the easybutton leaflet plugin in my Shiny App.
What I'm trying to do is to recenter the view on my points layer when the easybutton is clicked :
...
addCircleMarkers(lng = points$long,
lat = points$lat,
weight = 1, radius = 4,
group = "points",
...
addEasyButton(easyButton(
icon = 'ion-arrow-shrink',
title = 'Reset view',
onClick = JS("function(btn, map) {map.fitBounds(points.getBounds()); }")
))
But it doesn't work : "points is not defined" is printed in the JS console.
How can I get the real leaflet name (JS) of my points layer ?
Thank you.
Despite being a group name, points is not defined within the javascript - you need to use the layerManager to find layers - and passing the layer name from R to js is not quite as straightforward as one might hope.
This is not very clear in the documentation, but you should be able to set a group name for the markers, as you have, and then access it like so:
onClick = JS("function(btn, map) {
var groupLayer = map.layerManager.getLayerGroup('groupName');
}")
To get the bounds you should be able to use:
onClick = JS("function(btn, map) {
var groupLayer = map.layerManager.getLayerGroup('groupName');
map.fitBounds(groupLayer.getBounds());
}")
I'm currently trying to create a PyQtGraph gui to plot an image repeatedly as new data comes in using code similar to this:
self.app = QtGui.QApplication([])
self.win = QtGui.QMainWindow()
self.win.resize(800, 600)
self.imv = pg.ImageView()
self.win.setCentralWidget(self.imv)
self.win.setWindowTitle('My Title')
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.check_for_new_data_and_replot)
self.timer.start(100)
self.win.show()
then each time I get new data I draw the image:
self.imv.setImage(self.data_array)
One problem I'm running into is that my data array usually has a skewed aspect ratio, i.e. it is usually really "tall and skinny" or "short and fat", and the image that is plotted has the same proportions.
Is there a way to stretch the image to fit the window? I've looked through the documentation for ImageView and ImageItem, but can't find what I need. (Perhaps it is there, but I am having trouble identifying it.)
I figured it out-- using the lower-level ImageItem class displayed the image in a way that stretched to fit the window size:
self.app = QtGui.QApplication([])
self.win = pg.GraphicsLayoutWidget()
self.win.resize(800, 600)
self.img = pg.ImageItem()
self.plot = self.win.addPlot()
self.plot.addItem(self.img)
self.win.setWindowTitle('My Title')
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.check_for_new_data_and_replot)
self.timer.start(100)
self.win.show()
And to update the image data:
self.img.setImage(self.data_array)
This also lets me display axis scales on the sides, which was a feature I wanted as well.