Place a chart in plotly popup - python

I'm using plotly for R, although I'm open to using the Python version, as well. When I hover over a datapoint, is there a way to make the popup contain another chart? Ideally the chart would be created from the data, although I can use a static image as a fallback.
I'm unsure where to start on this, and apologize in advance for not having an MWE.

Solution 1: Stick to R
Thanks to #MLavoie. The following example use pure R to create two plot, the "mainplot" and the "hover" which reacts to the hover event of the first one.
library(shiny)
library(plotly)
ui <- fluidPage(
plotlyOutput("mainplot"),
plotlyOutput("hover")
)
server <- function(input, output) {
output$mainplot <- renderPlotly({
# https://plot.ly/r/
d <- diamonds[sample(nrow(diamonds), 1000), ]
plot_ly(d, x = carat, y = price, text = paste("Clarity: ", clarity), mode = "markers", color = carat, size = carat, source="main")
})
output$hover <- renderPlotly({
eventdat <- event_data('plotly_hover', source="main") # get event data from source main
if(is.null(eventdat) == T) return(NULL) # If NULL dont do anything
point <- as.numeric(eventdat[['pointNumber']]) # Index of the data point being charted
# draw plot according to the point number on hover
plot_ly( x = c(1,2,3), y = c(point, point*2, point*3), mode = "scatter")
})
}
shinyApp(ui, server)
This example use the shiny binds for plotly. For every hover event, a POST request is sent to the server, then the server will update the popup-chart. It's very inefficient thus may not work well on slow connections.
The above code is just for demo, and not yet tested. See a working and much more complicated example here (with source).
Solution 2: Javascript
Yes, you can do it using the plotly Javascript API.
Short answer
Create your graph using R or Python or any other supported language.
Insert the graph into a new HTML page and add a callback function as shown in the example below. If you have good knowledge about DOM, you can also add the JS to the original HTML instead of creating a new one.
Draw the popup graph inside the callback function which accepts parameters containing the data of the datapoint on-hover.
Details
As #MLavoie mentioned, a good example is shown in plotly.hover-events
Let's dig into the code. In the JS file, there is a simple callback function attached to Plot:
Plot.onHover = function(message) {
var artist = message.points[0].x.toLowerCase().replace(/ /g, '-');
var imgSrc = blankImg;
if(artistToUrl[artist] !== undefined) imgSrc = artistToUrl[artist];
Plot.hoverImg.src = imgSrc;
};
Above, artistToUrl is a huge object filled with base64 string which I will not paste here to overflow the post. But you can see it under the JS tab of the example page. It has such structure:
var artistToUrl = { 'bob-dylan': 'data:image/jpeg;base64,/...',...}
Working example:
For demonstration, I prepare a simple example here (click to try):
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<iframe id="plot" style="width: 900px; height: 600px;" src="https://plot.ly/~jackp/10816.embed" seamless></iframe>
<div id="myDiv"></div>
<script>
(function main() {
var Plot = { id: 'plot', domain: 'https://plot.ly' };
Plot.onHover = function(message) {
var y = message.points[0].y; /*** y value of the data point(bar) under hover ***/
var line1 = {
x: [0.25,0.5,1], /*** dummy x array in popup-chart ***/
y: [1/y, 2, y], /*** dummy y array in popup-chart ***/
mode: 'lines+markers'
};
var layout = {
title:'Popup graph on hover',
height: 400,
width: 480
};
Plotly.newPlot('myDiv', [ line1 ], layout); // this finally draws your popup-chart
};
Plot.init = function init() {
var pinger = setInterval(function() {
Plot.post({task: 'ping'});
}, 500);
function messageListener(e) {
var message = e.data;
if(message.pong) {
console.log('Initial pong, frame is ready to receive');
clearInterval(pinger);
Plot.post({
'task': 'listen',
'events': ['hover']
});
}
else if(message.type === 'hover') {
Plot.onHover(message);
}
}
window.removeEventListener('message', messageListener);
window.addEventListener('message', messageListener);
};
Plot.post = function post(o) {
document.getElementById(Plot.id).contentWindow.postMessage(o, Plot.domain);
};
Plot.init();
})();
</script>
</body>
</html>
This is modified from the poltly.hover-events example for python. Instead of poping up an image, I change the onhover callback to plot a curve based on the y value of the each bar.
The main chart is generated by python and inserted here as iframe. You can make your own by any language including R. In this page we add a <div id="myDiv"></div> and use the plotly.js to draw the popup-chart whithin it.
Export R data frame to JS enviornment
Shiny uses jsonlite to convert R objects to json and send them to the client. We can use the same mechanism to pack and send our data frame so that the JS callback can use the data to render the popup chart.
server.r
output$json <- reactive({
paste('<script>data =', RJSONIO::toJSON(your_data_frame, byrow=T, colNames=T),'</script>')
ui.r
fluidPage(..., htmlOutput("json"), ...)
In the JS callback function, you can use data as any other JS objects.
More detail goes here and here.

If you want to stick with R you could use Shiny to get almost the result you want. When you hover each point an image will be render under the main plot. For the example below, I used the first three rows of the mtcars datasets. To run the code, you only need 3 logos/images corresponding to the name of the first three rows (under mtcars$name, Mazda RX4, Mazda RX4 Wag, Datsun 710 in this example).
library(shiny)
library(plotly)
datatest <- diamonds %>% count(cut)
datatest$ImageNumber <- c(0, 1, 2, 3, 4)
datatest$name <- c("Image0", "Image1", "Image2", "Image3", "Image4")
ui <- fluidPage(
plotlyOutput("plot"),
# verbatimTextOutput("hover2"),
#imageOutput("hover"),
plotlyOutput("hover3")
)
server <- function(input, output, session) {
output$plot <- renderPlotly({
plot_ly(datatest, x = cut, y = n, type = "bar", marker = list(color = toRGB("black")))
})
selected_image <- reactive({
eventdat <- event_data('plotly_hover', source = 'A')
ImagePick <- as.numeric(eventdat[['pointNumber']])
sub <- datatest[datatest$ImageNumber %in% ImagePick, ]
return(sub)
})
# output$hover2 <- renderPrint({
#d <- event_data("plotly_hover")
#if (is.null(d)) "Hover events appear here (unhover to clear)" else d
#})
# output$hover <- renderImage({
# datag <- selected_image()
#filename <- normalizePath(file.path('/Users/drisk/Desktop/temp',
# paste(datag$name, '.png', sep='')))
# Return a list containing the filename and alt text
# list(src = filename,
# alt = paste("Image number", datag$name))
# }, deleteFile = FALSE)
output$hover3 <- renderPlotly({
datag <- selected_image()
# draw plot according to the point number on hover
plot_ly(data=datag, x = ImageNumber, y = n, mode = "scatter")
})
}
shinyApp(ui, server)

Seems the answers posted aren't working for you #Adam_G. I have been exploring similar libraries for my own work and determined that Plot.ly is not always the right path when you want advanced features. Have you seen bokeh? It is basically designed for this type of task and much easier to implement (also a D3.js library like Plot.ly). Here is a copy of an example they posted where you can move a slider to change a graph of data (similar to the example posted by #gdlmx for Plot.ly but you can use it without hosting it on a website). I added the flexx package so you can use this writing pure Python (no JavaScript - it can translate Python functions to JavaScript (CustomJS.from_py_func(callback)) https://github.com/zoofIO/flexx-notebooks/blob/master/flexx_tutorial_pyscript.ipynb):
from bokeh.io import vform
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import figure, output_file, show
import flexx
output_file("callback.html")
x = [x*0.005 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
def callback(source=source):
data = source.get('data')
f = cb_obj.get('value') #this is the bokeh callback object, linked to the slider below
x, y = data['x'], data['y']
for i in range(len(x)):
y[i] = x[i]**f #the slider value passed to this function as f will alter chart as a function of x and y
source.trigger('change') #as the slider moves, the chart will change
slider = Slider(start=0.1, end=4, value=1, step=.1, title="power", callback=CustomJS.from_py_func(callback))
layout = vform(slider, plot)
show(layout)
See here for the actual example in action: http://docs.bokeh.org/en/0.10.0/docs/user_guide/interaction.html#customjs-for-widgets
To integrate with hover events see here ( from bokeh.models import HoverTool):
http://docs.bokeh.org/en/0.10.0/docs/user_guide/interaction.html#customjs-for-hover
Hover example:
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import HoverTool
output_file("toolbar.html")
source = ColumnDataSource(
data=dict(
x=[1, 2, 3, 4, 5],
y=[2, 5, 8, 2, 7],
desc=['A', 'b', 'C', 'd', 'E'],
)
)
hover = HoverTool(
tooltips=[
("index", "$index"),
("(x,y)", "($x, $y)"),
("desc", "#desc"),
]
)
p = figure(plot_width=400, plot_height=400, tools=[hover], title="Mouse over the dots")
p.circle('x', 'y', size=20, source=source)
show(p)
Looking at the 1st code you could put whatever formula you want under the def callback function - some playing around required. You can get the hover to alter a graph next to it (hform(leftchart, rightchart) or above / below it (vform(topchart, bottomchart)). This is passed as CustomJS which bokeh uses to allow extendability and flexx allows you to write it in Python.
The alternative is to put whatever you want customized on the hover tooltips using HTML (although this example is placing images in dictionaries instead of new plots from the underlying data): http://docs.bokeh.org/en/0.10.0/docs/user_guide/tools.html#custom-tooltip

Related

Python bokeh callback function slider

Can someone explain, why this doesn't do anything and how I can print values from inside the callback function? Problem must lay in the callback function. I want my plot to be updated by the date range slider in a standalone html file, but nothing happens, when I change the values of the slider.
import numpy as np
import pandas as pd
from datetime import datetime
from bokeh.models import ColumnDataSource, DatetimeTickFormatter, HoverTool
from bokeh.models.widgets import DateRangeSlider
from bokeh.layouts import layout, column
from bokeh.models.callbacks import CustomJS
from bokeh.plotting import figure, output_file, show, save
datesX = pd.date_range(start='2018-01-02', periods=100)
# valuesY = pd.DataFrame(np.random.randint(0,25,size=(100, 1)), columns=list('A'))
np.random.seed(0)
valuesY = np.random.rand(100)
source = ColumnDataSource(data={'x': datesX, 'y': valuesY})
# output to static HTML file
output_file('file.html')
hover = HoverTool(tooltips=[('Timestamp', '#x{%Y-%m-%d %H:%M:%S}'), ('Value', '#y')], formatters={'#x': 'datetime'})
date_range_slider = DateRangeSlider(title="Zeitrahmen", start=datesX[0], end=datesX[99], \
value=(datesX[0], datesX[99]), step=1, width=1000)
# create a new plot with a title and axis labels
p = figure(title='file1', x_axis_label='Date', y_axis_label='yValue', x_axis_type='datetime',
tools="pan, wheel_zoom, box_zoom, reset", plot_width=1000, plot_height=700)
# add a line renderer with legend and line thickness
p.line(x='x', y='y', source=source, line_width=2)
p.add_tools(hover)
callback = CustomJS(args=dict(source=source), code="""
var data = source.data;
var a = cb_obj.value;
var xmin = a[0];
var xmax = a[1];
var x = data['x'];
var y = data['y'];
for (var i = 0; i < x.length; i++) {
if (data['x'][i] == xmin){
startidx = i;
}
if (data['x'][i] == xmax){
endidx = i;
}
}
int j=0;
while(j <= endidx-startidx){
x[j] = data['x'][startidx+j];
y[j] = data['y'][startidx+j];
j++;
}
source.change.emit();
""")
date_range_slider.js_on_change('value', callback)
layout = column(p, date_range_slider)
# show the results
show(layout)
When developing JS callbacks, it is essential to learn how to view your browser's JavaScript console. This differs by browser so you will have to search how to access the console on whatever specific browser you are using.
Once you can see the console, you will see that your code has an error:
SyntaxError: Unexpected identifier 'j'
because you have int j instead og var j in your callback code. It that is fixed, there is then another error:
ReferenceError: Can't find variable: endidx
It's not really clear what you you actually want to happen, so it's not really possible to try to actually fix the code completely, but hopefully this gives you the information you need to properly do the debugging yourself. Some other tips for debugging:
to "print" in console, use console.log(...)
you can also add the command debugger in the callback code. If you run the Bokeh script with BOKEH_MINIFIED=no and the browser dev tools window open, then the exectution will stop at that point and you can single step through the code.

python bokeh dynamically update categorical x_range

I want to create a candlestick plot in bokeh and dynamically update the x-axis according to user input through a MultiSelect widget. Basically the user will choose a few items in the MultiSelect and then I would like those to become the values of the x axis. I already have set up the MultiSelect widget and confirmed that it is working by attaching a DataTable to the MultiSelect and having it update accordingly (which it does). I just need help retrieving the values from the MultiSelect widget and setting them as my plot.x_range. Based on a few github/issues posts (like this one: https://github.com/bokeh/bokeh/issues/4022) I tried using a FactorRange, but it isn't working. Currently the behavior is the x axis labels stay set to the values that are set during the initial configuration of the MultiSelect ('aaa' and 'bbb'), and don't change when I choose different values in the MultiSelect widget.
Here's a code sample:
### SET UP SOURCE DF INFO ###
tab2_source = ColumnDataSource(df)
tab2_original_source = ColumnDataSource(df)
columns_t2 = [TableColumn(field='Gene', title='Gene'), TableColumn(field='row_min', title='min'), TableColumn(field='row_max', title='max'),
TableColumn(field='quantile_25', title='25th quantile'), TableColumn(field='quantile_50', title='50th quantile'), TableColumn(field='quantile_75', title='75th quantile')]
data_table_t2 = DataTable(source=tab2_source, columns=columns_t2, reorderable=False)
### STUFF FOR THE WIDGETS ###
# customJS stuff
tab2_callback_code = """
var t2_data = tab2_source.data;
var t2_original_data = tab2_original_source.data;
var gene_t2 = gene_select_obj_t2.value;
console.log("gene: " + gene_t2);
for (var key in t2_original_data) {
t2_data[key] = [];
for (var i = 0; i < t2_original_data['Gene'].length; ++i) {
if (t2_original_data['Gene'][i] === gene_t2[0] || t2_original_data['Gene'][i] === gene_t2[1]) {
t2_data[key].push(t2_original_data[key][i]);
}
}
}
tab2_source.change.emit();
target_obj_t2.change.emit();
"""
# make drop-down selectors
select_gene_t2 = MultiSelect(title = "Select up to 2 Genes to View:", value=['aaa', 'bbb'], options=list(df.Gene.unique()))
# define the callback objects now that the select widgets exist
tab2_callback = CustomJS(
args=dict(tab2_source=tab2_source,
tab2_original_source=tab2_original_source,
gene_select_obj_t2=select_gene_t2,
target_obj_t2=data_table_t2),
code=tab2_callback_code)
# connect the callbacks to the filter widgets
select_gene_t2.js_on_change('value', tab2_callback)
## PLOTTING ##
p2 = figure(plot_width=500, plot_height=400, title='Avg Gene Exp', y_range=(-2,12), x_range=FactorRange())
p2.x_range.factors = list(select_gene_t2.value)
p2.vbar(x='Gene', top='quantile_75', bottom='quantile_25', source=tab2_source, width=0.5, fill_color="#D5E1DD", line_color="black")
tab2 = Panel(child=column(select_gene_t2, p2, data_table_t2), title="Exp by Gene")
tabs = Tabs(tabs=[tab2])
save(tabs)
For anyone who is interested, this is the best solution I found:
Add this line to the tab2_callback_code (plot_xr references my figure p2 - you will also need to add these references in the CustomJS args dict):
plot_xr.x_range.factors = gene_t2;
Also it is important to instantiate the figure before the tab2_callback_code is called. This worked for me and now my x_range values change as I select different options in the MultiSelect.

How do I access and change a column in ColumnDataSource in bokeh?

I wanted to create a plot with bokeh in python which runs quite well so far. But now I wanted to add a Slider and tell him to hide all bars in my vbar plot which are lower than the value of the slider.
current = df[(df['ID'] > num_tokens.value[0])].dropna()
source.data = {
'ID': current.ID
}
I tried to create a variable 'current' and assign it to the 'ID' column so that the plot can update the plot. But I always get a TypeError: Int is not subscriptable. How can I make my slider widget make work?
Thank you in advance
Don't know if we must close this issue or not but I would sugget using a customJS callback:
Create initially a source and a render_soruce from df
source = ColumnDataSource(df)
renderer_source = ColumnDataSource(df)
Then define your callback and your slider
code = """
var slider_value= cb_obj.value; //cb_obj is your slider widget
var data=source.data;
var data_id = data['ID'];
var data_x=data['x'];
var data_y=data['y'];
var render_x=render['x'];
var render_y=render['y'];
var x = [];
var y = [];
render_x=[];
render_y=[];
for (var i=0;i<data_id.length; i++){
if (data_id[i]== slider_valuer) {
x.push(data_x[i]);
y.push(data_y[i]);
}
renderer_source.data['x']=x;
renderer_source.data['y']=y;
renderer_source.change.emit();
"""
callback = CustomJS(args=dict(source=source, renderer_source=renderer_source), code=code)
slider = Slider(start=0, end=(max_value_o_slider), value=1, step=1, title="Frame")
slider.js_on_change('value', callback)
And identify source=renderer_source in your plot
You can achieve this with almost no JavaScript using a view:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CDSView, CustomJSFilter, Slider, CustomJS
from bokeh.plotting import figure
N = 5
x = list(range(1, N + 1))
top = list(range(1, N + 1))
# Specifying manual ranges to prevent range changes when some bars are hidden.
p = figure(x_range=(0, N + 1), y_range=(0, N + 1))
ds = ColumnDataSource(data=dict(x=x, top=top))
s = Slider(start=0, end=N, value=0)
# Making sure that everything that depends on the `ds`
# is notified when the slider value is changed.
s.js_on_change('value', CustomJS(args=dict(ds=ds),
code="ds.change.emit();"))
view = CDSView(source=ds,
filters=[CustomJSFilter(args=dict(ds=ds, slider=s),
code="return ds.data['top'].map((top) => top > slider.value);")])
p.vbar(x='x', width=0.7, top='top', bottom=0, source=ds, view=view)
curdoc().add_root(column(p, s))

bokeh, dynamically change plot width

I'm trying to change plot width when taptool selects something.
I invoke this using bokeh serve, and navigating to the localhost website
when I click on one of the rectangles, my console prints out "callback" and "callback2" but my plot doesn't change width.
What am I doing wrong?
counts = [1*10**7,2*10**7,3*10**7] #dummy data
l_edge = [x for x in range(len(counts))]
r_edge = [x + .85 for x in range(len(counts))]
data = {
'height': counts,
'leftEdges': l_edge,
'rightEdges': r_edge,
}
p = figure()
s = ColumnDataSource(data)
p.add_tools(TapTool())
def callbackfcn(attr,old,new):
global p
print('callback')
p.width = np.random.choice([100,200,300,1000,10000])
p.height = np.random.choice([100,200,300,1000,10000])
print('callback2')
r = p.quad(top='height',bottom=0,
left = 'leftEdges',right = 'rightEdges',
source = s)
r.data_source.on_change('selected',callbackfcn)
layout = column(p)
curdoc().add_root(layout)
Have you tried instead including sizing_mode in your layout? Take a look at the Bokeh docs for an example.

Bokeh Interactive legend hide multiple glyphs

I've enabled the hide legend option for my dataset. When I click it, only one bar goes off and others stay. I'm not quite sure what is causing the issue.
Here's the bar plot before and after:
.
Here's what my data looks like:
Here's the code:
p = Bar(output,'Programs',values="Averages", group="University",plot_width=600,plot_height=400, title="Comparison")
p.legend.click_policy="hide"
output_file("bar.html")
show(p)
It is not currently (Bokeh 0.12.6) possible to hide all the bars via legend.click_policy="hide", as stated in the documentation:
Interactive legend features currently work on “per-glyph” legends. Legends that are created by specifying a column to automatically group do no yet work with the features described below
It is, however, possible to hide the bars by adding CheckboxGroup() with CustomJS that hides the bars when the checkboxes are clicked. Below you can see an MCVE, which is also available online as a Jupyter Notebook:
import numpy as np
from bkcharts import Bar, show
from bokeh.layouts import column
from bokeh.models import CheckboxGroup, CustomJS
data = {'University': ['ENGT'] * 3 + ['UBC'] * 3,
'Averages': [76.5, 79.9, 72.2, 71, 72, 69],
'Programs': ['CHML', 'CIVL', 'CPEN', 'CHML', 'CIVL', 'CPEN']}
group = "University"
bars = Bar(data=data, label='Programs', values="Averages", group=group,
plot_width=600, plot_height=400, title="Comparison")
checkboxes = CheckboxGroup(labels=np.unique(data[group]).tolist(),
# Make all checkboxes checked by default
active=list(range(np.unique(data[group]).size)))
checkboxes.callback = CustomJS(args=dict(bars=bars), code="""
var group = '%s';
function change_bars_visibility(checkbox_name, visible) {
for (j = 0; j < bars.renderers.length; j++) {
// Go through rendered objects
if (bars.renderers[j].attributes.hasOwnProperty('data_source') &&
bars.renderers[j].data_source.data[group][0] === checkbox_name) {
// Change the visibility of this rendered object if it belongs to
// the group determined by the checkbox that was clicked
bars.renderers[j].visible = visible;
}
}
}
for (i = 0; i < cb_obj.labels.length; i++) {
// Go through checkbox labels
var checkbox_name = cb_obj.labels[i];
if (cb_obj.active.indexOf(i) >= 0) {
// alert(checkbox_name + " is activated");
change_bars_visibility(checkbox_name, true);
}
else {
// alert(checkbox_name + " is disabled");
change_bars_visibility(checkbox_name, false);
}
}
""" % group)
show(column(bars, checkboxes))

Categories

Resources