I am trying to visualise sensor output in relation to its path.
I plot path as scatter in one figure and some range of signal amplitude in the second figure. I need to visualise (highlight) a path point at which the particular reading was taken.
I started using bokeh as a backend and in general, got very good results with visualisations I need. But I am stuck on this particular interaction.
I would like to have some marker like a vertical line anchored in the middle of the figure. When I move/scroll the amplitude plot (the bottom one), I would like to highlight the point on the path plot where the reading closest to the marker line was taken.
The example code:
(I would like to anchor the marker line and add interaction between the red dot and the vertical line taking an index of the signal, which is not implemented.)
import numpy as np
import pandas as pd
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool, Span
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
output_file('interactive_path_sig.html', title="interactive path")
class InteractivePath():
def __init__(self):
x = np.arange(0, 1000, 0.5)
self.df = pd.DataFrame({"x": x,
"y": np.sin(x),
"z": np.cos(x)})
self.source = ColumnDataSource(self.df)
def plot_path(self):
plt = figure(title = "Sensor Path")
plt.scatter(x="x", y="y",source=self.source,
line_color=None, size = 6)
# TODO implement interaction instead of hard coded index
index=500 # this is where I think I need to create working callback
print("x={}, y={}".format(self.df['x'][index], self.df['y'][index]))
plt.circle(x=self.df['x'][index], y=self.df['y'][index],
fill_color="red", size=15)
hover = HoverTool()
hover.tooltips=[("index", "#index"), ("senosr","#z")]
plt.add_tools(hover)
return plt
def plot_signal(self):
plt = figure(x_range=(450, 550), title="Signal Amplitude")
plt.line(x="index", y="z", source=self.source, line_color="black", line_width=2)
# TODO implement interaction instead of hard coded index
index = 500 # I think this needs emit some singal to other plot
vline = Span(location=index, dimension='height', line_color='red', line_width=3)
plt.renderers.extend([vline])
return plt
def get_grid(self):
""" place visualisation in a grid and display"""
grid = gridplot([[self.plot_path()], [self.plot_signal()]],
sizing_mode='stretch_both',)
return grid
def vis_main(self):
""" use all visualisations"""
show(self.get_grid())
if __name__=="__main__":
vis = InteractivePath()
vis.vis_main()
So a few pointers:
I think you'll want both of those plots in the same method because the columndatasource is common between them, and you can set CustomJS behaviors between them if they're in the same scope.
The index that you're using already exists within your self.df which will be easier to interact with once it's on your plot, since you can handle it with JS plot behavior instead of going back to a python variable and reloading data.
Instead of drawing a new glyph for your 'highlighted' point, consider using the 'hover' or 'selected' functionality built in. hover_color='red' for example could replace drawing and moving another class of glyph. If you want to leave statically selected so you can generate a nice report without a mouse in a screenshot, defining a callback using the built-in selected property of ColumnDataSource
I can post some actual code blocks with more specific examples, but if any of these points is a hard stop for your actual use case, it'll drive solution.
Edit:
So I got pretty close using one class method - the issue is being able to edit the second plot from the first method, not the actual change to the ColumnDataSource itself.
def plot_it(self):
self.plot_signal = figure(x_range=(450, 550), y_range=(-1, 1), title='signal')
self.plot_signal.line(x='index', y='z', source=self.source)
self.plot_signal.segment(x0=500, y0=-2, x1=500, y1=2, source=self.source)
self.plot_path = figure(title='sensor')
self.plot_path.scatter(x='x', y='y', source=self.source, hover_color='red')
jscode='''
var data = source.data;
var plot_signal = plot_signal;
var index = cb_data.index['1d'].indices;
var xmin = 0;
var xmax = 0;
if (index > 0) {
xmin = index[0] - 50;
xmax = index[0] + 50;
plot_signal.x_range.end = xmax;
plot_signal.x_range.start = xmin;
plot_signal.change.emit();
}
hover_callback = CustomJS(args=dict(source=self.source, plot_signal=self.plot_signal), code=jscode)
hover.tooltips = [('index', '#index'), ('sensor', '#z')]
self.plot_path.add_tools(hover)
def get_grid(self):
self.plot_it()
grid = gridplot([[self.plot_path], [self.plot_signal]])
return grid
That should do everything but move the line segment. I couldn't find the segment naming convention to add plot_signal.SOMEOBJECT.x0 and .x1 but it would just get added to the if (index > 0) block just like using index[0]. I took some of the style options out because I'm transcribing from another computer.
This question on moving a line segment might give you the syntax on the segment JSON object.
Related
I'm trying to develop a relatively complex plotting application, which has a huge selection of data to plot. Using dropdowns, the user can select which lines they would like to plot. I've developed a largely simplified version of the code (shown below) to illustrate what my application is like.
import bokeh.plotting.figure as bk_figure
import random
import numpy as np
from bokeh.io import show
from bokeh.layouts import row, column, widgetbox
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook # enables plot interface in J notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
global x, ys
output_notebook()
plot = bk_figure(plot_width=950, plot_height=800, title="Legend Test Plot"\
, x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)
x = np.linspace(0,10,10)
ys = []
#generates three different lines
for i in range(len(lines)):
ys.append(x*i)
#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)
def change_line(attr,old,new):
#remove old lines
render_copy = list(plot.renderers)
for line in render_copy:
plot.renderers.remove(line)
legend_items = []
#add selected lines to plot
for i,line in enumerate(line_select.value):
y = ys[int(line)]
source = ColumnDataSource(data={'x':x,'y':y})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)
line_select.on_change('value',change_line)
layout = column(line_select,plot)
def modify_doc(doc):
doc.add_root(row(layout,width=800))
doc.title = "PlumeDataVis"
handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)
I've decided to dynamically add and remove line glyphs from the plot as they are selected in the MultiSelect. This is because if I simply hide the lines, the performance of the program suffers, given that there are so many line options in the real dataset.
Problem:
I want to add a legend to the plot which only contains entries for the Line glyphs that are currently in the plot (there are far too many line options in the real dataset to have all of them visible in the legend at all times.) I've been having issues finding any resources to help with this: for most applications, something like this is sufficient, but this doesn't work with the way I've defined the lines I'm plotting.
I've been adding legends manually, for example:
#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)
#create first legend
legend_item = [LegendItem(label=lines[0],\
renderers=[glyph])]
legend = Legend(items=legend_item)
plot.add_layout(legend,place='right')
but I can't figure out how to effectively remove the legend layouts from the plot once I've added them. After reading the source code for add_layout, I realized that you could get a list of layouts in a given location by using something like getattr(plot,'right'). Trying to use this, I replaced the change_line function with the following:
def change_line(attr,old,new):
#remove old lines
render_copy = list(plot.renderers)
for line in render_copy:
plot.renderers.remove(line)
#remove old legend
right_attrs_copy = list(getattr(plot,'right'))
for legend in right_attrs_copy:
getattr(plot,'right').remove(legend)
legend_items = []
#add selected lines to plot
for i,line in enumerate(line_select.value):
y = ys[int(line)]
source = ColumnDataSource(data={'x':x,'y':y})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)
legend_items.append(LegendItem(label='line '+str(line),\
renderers=[glyph]))
#create legend
legend = Legend(items=legend_items)
plot.add_layout(legend,place='right')
Checking the attributes of the plot, this appears to add and remove legends and lines correctly, but it causes the plot to completely stop visually updating.
Does anyone know how to accomplish this behavior? It's possible that I'm not even adding the legend in the correct way, but I couldn't figure out how else to add them when lines are defined as Glyph objects.
Basic glyphs provide much flexibility compared to chart/model classes. A basic line (not Line) glyph can be used here.
In the code below, I am adding basic glyphs to the chart. I am saving the glyphs in a dictionary which can be actioned later (as OP said its a complex application, I am sure this will be used later). I have commented the ColumnDataSource creation, as it will accessible through data_source.data of respective glyphs (now saved in dictionary).
Also, since now we are creating lines one by one, color needs to be provided for different lines. I have used a bokeh.palette function to generate a number of colors. More on this can be read here
import bokeh.plotting.figure as bk_figure
import random
import numpy as np
from bokeh.io import show
from bokeh.layouts import row, column, widgetbox
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook # enables plot interface in J notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
import bokeh.palettes
#change the number as per the max number of glyphs in system
palette = bokeh.palettes.inferno(5)
global x, ys
output_notebook()
plot = bk_figure(plot_width=950, plot_height=800, title="Legend Test Plot"\
, x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)
x = np.linspace(0,10,10)
ys = []
#generates three different lines
for i in range(len(lines)):
ys.append(x*i)
linedict = {}
#add line 0 to plot initially
#source = ColumnDataSource(data={'x':x,'y':ys[0]})
#glyph = Line(x='x',y='y')
#glyph = plot.add_glyph(source,glyph)
l1 = plot.line(x = x, y= ys[0], legend=str(0), color = palette[0])
linedict[str(0)] = l1
def change_line(attr,old,new):
#remove old lines
render_copy = list(plot.renderers)
for line in render_copy:
plot.renderers.remove(line)
legend_items = []
#add selected lines to plot
for i,line in enumerate(line_select.value):
y = ys[int(line)]
#source = ColumnDataSource(data={'x':x,'y':y})
l1 = plot.line(x = x, y= y, legend=line, color = palette[i])
#linedict[line] = l1
glyph = Line(x='x',y='y', legend=line, color = palette[i])
glyph = plot.add_glyph(source,glyph)
line_select.on_change('value',change_line)
layout = column(line_select,plot)
def modify_doc(doc):
doc.add_root(row(layout,width=800))
doc.title = "PlumeDataVis"
handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)
After much anguish, I finally figured it out (this link was helpful). #Eugene Pakhomov was correct in that the fact that I removed lines and legends in my initial code was a problem. Instead, the key was to initialize a new line only when the user requested to plot a new maximum number of lines. In all other cases, you can simply edit the data_source of existing lines. This allows the program to avoid having all the lines plotted and hidden when the user only wants to plot one or two of the total options.
Instead of deleting and remaking the legend, you can set it to be empty on every update, then add entries as needed.
The following code worked for me in a Jupyter Notebook running bokeh 1.4.0:
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
from bokeh.palettes import Category10 as palette
output_notebook()
plot = bk_figure(plot_width=750, plot_height=600, title="Legend Test Plot"\
, x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)
x = np.linspace(0,10,10)
ys = []
#generates three different lines with 0,1, and 2 slope
for i in range(len(lines)):
ys.append(x*i)
#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)
#intialize Legend
legend = Legend(items=[LegendItem(label=lines[0],renderers=[glyph])])
plot.add_layout(legend)
def change_line(attr,old,new):
plot.legend.items = [] #reset the legend
#add selected lines to plot
for i,line in enumerate(line_select.value):
line_num = int(line)
color = palette[10][i]
#if i lines have already been plotted in the past, just edit an existing line
if i < len(plot.renderers):
#edit the existing line's data source
plot.renderers[i]._property_values['data_source'].data = {'x':x, 'y':ys[line_num]}
#Add a new legend entry
plot.legend.items.append(LegendItem(label=line,renderers=[plot.renderers[i]]))
#otherwise, initialize an entirely new line
else:
#create a new glyph with a new data source
source = ColumnDataSource(data={'x':x,'y':ys[line_num]})
glyph = Line(x='x',y='y',line_color=color)
glyph = plot.add_glyph(source,glyph)
#Add a new legend entry
plot.legend.items.append(LegendItem(label=line,renderers=[plot.renderers[i]]))
#'Remove' all extra lines by making them contain no data
#instead of outright deleting them, which Bokeh dislikes
for extra_line_num in range(i+1,len(plot.renderers)):
plot.renderers[extra_line_num]._property_values['data_source'].data = {'x':[],'y':[]}
line_select.on_change('value',change_line)
layout = column(line_select,plot)
def modify_doc(doc):
doc.add_root(row(layout,width=800))
doc.title = "PlumeDataVis"
handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)
I'm trying to label a pandas-df (containing timeseries data) with the help of
a bokeh-lineplot, box_select tool and a TextInput widget in a jupyter-notebook. How can I access the by the box_select selected data points?
I tried to edit a similar problems code (Get selected data contained within box select tool in Bokeh) by changing the CustomJS to something like:
source.callback = CustomJS(args=dict(p=p), code="""
var inds = cb_obj.get('selected')['1d'].indices;
[source.data['xvals'][i] for i in inds] = 'b'
"""
)
but couldn't apply a change on the source of the selected points.
So the shortterm goal is to manipulate a specific column of source of the selected points.
Longterm I want to use a TextInput widget to label the selected points by the supplied Textinput. That would look like:
EDIT:
That's the current code I'm trying in the notebook, to reconstruct the issue:
from random import random
import bokeh as bk
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, HoverTool
from bokeh.plotting import figure, output_file, show, output_notebook
output_notebook()
x = [random() for x in range(20)]
y = [random() for y in range(20)]
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(data=dict(x=x, y=y, label=[i for i in "a"*20]))
p1 = figure(plot_width=400, plot_height=400, tools="box_select", title="Select Here")
p1.circle('x', 'y', source=source, alpha=0.6)
p1.add_tools(hovertool)
source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
var inds = cb_obj.indices;
for (var i = 0; i < inds.length; i++) {
source.data['label'][inds[i]] = 'b'
}
source.change.emit();
""")
)
layout = row(p1)
show(layout)
The main thing to note is that BokehJS can only automatically notice updates when actual assignments are made, e.g.
source.data = some_new_data
That would trigger an update. If you update the data "in place" then BokehJS is not able to notice that. You will have to be explicit and call source.change.emit() to let BokehJS know something has been updated.
However, you should also know that you are using three different things that are long-deprecated and will be removed in the release after next.
cb_obj.get('selected')
There is no need to ever use .get You can just access properties directly:
cb_obj.selected
The ['1d'] syntax. This dict approach was very clumsy and will be removed very soon. For most selections you want the indices property of the selection:
source.selected.indices
source.callback
This is an ancient ad-hoc callback. There is a newer general mechanism for callbacks on properties that should always be used instead
source.selected.js_on_change('indices', CustomJS(...))
Note that in this case, the cb_obj is the selection, not the data source.
With the help of this guide on how to embed a bokeh server in the notebook I figured out the following minimal example for my purpose:
from random import random
import pandas as pd
import numpy as np
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, BoxSelectTool
from bokeh.models.widgets import TextInput
output_notebook()
def modify_doc(doc):
# create a plot and style its properties
TOOLS="pan,wheel_zoom,reset"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
label = [i for i in "a"*10]
))
p.scatter("xvals", "yvals",source=source, color="white")
p.line("xvals", "yvals",source=source)
p.add_tools(BoxSelectTool(dimensions="width"))
p.add_tools(hovertool)
# create a callback that will add a number in a random location
def callback():
inds = source.selected.indices
for i in inds:
source.data['label'][i] = label_input.value.strip()
print(source.data)
new_data = pd.DataFrame(source.data)
new_data.to_csv("new_data.csv", index=False)
# TextInput to specify the label
label_input = TextInput(title="Label")
# add a button widget and configure with the call back
button = Button(label="Label Data")
button.on_click(callback)
# put the button and plot in a layout and add to the document
doc.add_root(column(button,label_input, p))
show(modify_doc, notebook_url="http://localhost:8888")
That generates the following UI:
BTW: Due to the non-existing box_select tool for the line glyph I use a workaround by combining it with invisible scatter points.
So far so good, is there a more elegant way to access the data.source/new_data df in the notebook outside modify_doc() than exporting it within the callback?
using Bokeh, I am trying to update the .selected dictionary of a ColumnDataSource programmatically, via the callback of a Slider, but cannot manage to get the selection reflected in the plot.
In the following snippet, the idea is that I want to be able to make a y-axis selection both via the ybox_select tool and/or by adjusting the sliders that control the position of a pair of min/max lines (NOTE: for brevity, in this example I only included the 'max' slider and line). If possible, I want to achieve this without using CustomJS callbacks.
I got as far as adjusting the horizontal line and the slider value (and of course the selection, which happens implicitly) when I operate the ybox_select tool (which triggers the selection_change function). Instead, when I operate the slider (triggering the slider_selection function), I manage to control the horizontal line but, apparently, not the source selection. In other words, the modification of source.data that occurs in slider_selection is reflected in the plot (i.e. the modified position of the horizontal line) but the modification of source.selected is NOT reflected in the plot (nor in a DataTable, as I verified separately).
Following the suggestion in this thread (where I've asked a shorter version of this question but didn't get any answers so far), I've worked on a copy of source.selected and then copied back to .selected (same for .data), but this didn't have any effects.
I must be missing something rather fundamental, but cannot figure out what. Any idea? Please avoid suggestions based on CustomJS, unless you're sure that there is no pure-Python alternative.
Thanks a lot for any feedback!
(Note: run this code as a script with bokeh serve --show script.py)
from bokeh.io import curdoc
from bokeh.models import BoxSelectTool, Slider
from bokeh.plotting import figure, ColumnDataSource
from bokeh.sampledata.glucose import data
from bokeh.layouts import column
import numpy as np
#===============================================================================
# Data and source
y = data.ix['2010-10-06']['glucose']
x = np.arange(len(y))
maxval=[max(y)]*len(x)
source = ColumnDataSource(dict(x=x, y=y, maxval=maxval))
#===============================================================================
# Basic plot setup
tools = 'wheel_zoom,ybox_select,reset'
p = figure(plot_width=800, plot_height=400, tools=tools, title='Min/max selection')
# Plot data
cr = p.circle('x', 'y', color="blue", source = source,
selection_color="blue", nonselection_color="gray",
size=6, alpha=0.8)
# Plot max horizontal line
p.line('x', 'maxval', line_color='blue', line_width=0.5, source=source,
nonselection_alpha=1.0, nonselection_color='blue')
#===============================================================================
# Callbacks
def selection_change(attrname, old, new):
ixs = new['1d']['indices']
if ixs:
arr = np.asarray(source.data['y'])[ixs]
max_slider.value = np.max(arr)
source.data['maxval'] = [np.max(arr)]*len(source.data['x'])
def slider_selection(attrname, old, new):
selected = source.selected.copy()
data = source.data.copy()
data['maxval'] = [max_slider.value]*len(data['x'])
yy = np.asarray(data['y'])
maxi = np.asarray(data['maxval'])
# Below is the new selection I would to visualize
selected['1d']['indices'] = np.where(yy <= maxi)[0].tolist()
# Updated data is reflected in the plot (horizontal line at 'maxval' moves)
source.data = data.copy()
# Updated selection is NOT reflected in the plot
# (nor in a DataTable, as tested separately)
source.selected = selected.copy()
#===============================================================================
# Slider
max_slider = Slider(start=min(y), end=max(y),
value=max(y), step=0.1, title="Maximum")
#===============================================================================
# Trigger callbacks
source.on_change('selected', selection_change)
max_slider.on_change('value', slider_selection)
#===============================================================================
# Layout
plot_layout = column(p, max_slider)
curdoc().add_root(plot_layout)
curdoc().title = "Demo"
Adding the following line to slider_selection seems to do what you want:
source.trigger("selected", old, selected)
the new function definition:
def slider_selection(attrname, old, new):
selected = source.selected.copy()
data = source.data.copy()
data['maxval'] = [max_slider.value]*len(data['x'])
yy = np.asarray(data['y'])
maxi = np.asarray(data['maxval'])
# Below is the new selection I would to visualize
selected['1d']['indices'] = np.where(yy <= maxi)[0].tolist()
# Updated data is reflected in the plot (horizontal line at 'maxval' moves)
source.data = data.copy()
# Updated selection is NOT reflected in the plot
# (nor in a DataTable, as tested separately)
source.selected = selected.copy()
source.trigger("selected", old, selected)
(Though it's a bit late, I found your question trying to find a similar answer, I figured this might be useful to others).
I'm trying to plot my 1D PDE in time using bokeh. I have a nparray U where each line is a time slice( dt ) and each column is a space slice (dx), therefore U[0] is my initial condition and U[n] is my n-th iteration.
Of course if I ask to plot(x,U[t]) for every t, I get the correct plot (as good as my approximations can be ;-), but I would like associate this with a time slider for interactivity (of course in future I would like to "play" it as animation ;-)
For now I would like to avoid to use bokeh server, since I want that this HTML be an stand alone "application"
The issue here is that callbacks just don't work, or at least I'm not being able to update the graph. I don't want to run a "bokeh" server since all my data is already inside a numpy data structure (U array). Every line is a iteration in time and every column is a dx.
#!/usr/bin/env python
from __future__ import division
from bokeh.models import ColumnDataSource, HBox, VBoxForm, HoverTool
from bokeh.models.widgets import Slider, TextInput
from bokeh.plotting import Figure, output_file, show
import numpy as np
def linearconv(nx,c=1,sigma=0.5,tmax=1,xmax=3):
nt=int((tmax/xmax)*((nx-1)/(c*sigma))+1) # Time Grid
x,dx=np.linspace(0,xmax,nx,retstep=True)
t,dt=np.linspace(0,tmax,nt,retstep=True)
# Initial conditions
#
# u=2 if 0.5 <= x <= 1
# u=1 everywhere else in the support
U = np.ones((nt,nx))
U[0][np.where((.5<=x) & (x<=1))]=2
# Calculate the wave over the time
for n in range(1,nt):
for i in range(1,nx):
U[n][i]= U[n-1][i] - c*dt/dx* ( U[n-1][i]-U[n-1][i-1] )
return U,x,t,dx,dt,nt
def prepareplot(height=400, width=400,title="Wave #"):
plot = Figure(plot_height=height, plot_width=width, title=title,
tools="crosshair,pan,reset,resize,save,wheel_zoom")
return plot
def update_ttime(attrname, old, new):
plot.title = "Wave #{}s".format(title.value)
def update_graph(attrname, old, new):
# Get the current slider values
t = time.value
source = ColumnDataSource(data=dict(x=x, t=U[t]))
plot.line('x','t', source=source)
plot.line('x','t', source=source)
nx=101
# Set up data
U,x,t,dx,dt,nt = linearconv(nx)
plot = prepareplot()
time = Slider(title="Time", value=t[-1], start=t[0], end=t[-1], step=dt)
ttime = TextInput(title="Time", value="{}".format(t[-1]))
source = ColumnDataSource(data=dict(x=x, t=U[-1]))
plot.line('x','t', source=source)
# Set up callbacks
ttime.on_change('value', update_ttime)
time.on_change('value', update_graph)
# Setup layouts
inputs = VBoxForm(children=[ttime,time])
layout = HBox(children=[inputs,plot], width=800)
# Plot the plot
output_file("sli.html")
show(layout)
I'm a very nooby programmer and this is my first Stack Overflow question. :)
So I'm trying to animate a car's trip on google maps using Python. I used matplotlib at first and could get a dot animated over the path line... then I tried using bokeh and successfully got the path to overlay on google maps...
My problem is that I haven't found a good way to do both (animate plot over google maps).
My data is in the form of Lat/Long coordinates.
Any advice? Thanks in advance!
EDIT: Here's my code that does the gmapplot... I'd rather have this and no animation than animation with no GMAP. My goal is to animate that "car" dot.
import numpy as np
from bokeh.io import output_file, show, vform
from bokeh.models.widgets import Dropdown
from bokeh.models import (GMapPlot, GMapOptions, ColumnDataSource, Line, Circle,
DataRange1d, PanTool, WheelZoomTool, BoxSelectTool, HoverTool)
data = np.genfromtxt('Desktop\Temp Data for Python\test data 3.csv', delimiter=',',
names=True)
map_options = GMapOptions(lat=np.average(data['Latitude']),
lng=np.average(data['Longitude']), map_type="roadmap", zoom=13)
plot = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=map_options,
title="My Drive")
source = ColumnDataSource(data=dict(lat=data['Latitude'], lon=data['Longitude'],
speed=data['GpsSpeed'],))
path = Line(x="lon", y="lat", line_width = 2, line_color='blue')
car = Circle(x=data['Longitude'][0], y=data['Latitude'][0], size=5, fill_color='red')
plot.add_glyph(source, path)
plot.add_glyph(source, car)
plot.add_tools(PanTool(), WheelZoomTool(), BoxSelectTool(),
HoverTool(tooltips=[("Speed", "#speed"),]))
output_file("gmap_plot.html")
show(plot)
This may not be exactly what you are looking for, but you could have a slider widget that controls the position of your car dot. The slider example found in the bokeh docs (or github repository, I can't remember) helped me when I started using sliders.
Just so you are aware, I was having problems with latlng points showing up in the correct locations. There is about a 10px offset. This is an open issue (github issue 2964).
The following code currently is just producing a generic bokeh Figure, but in theory, if you change it from a Figure to a GMapPlot it should work. I wasn't able to get this working with GMapPlots directly. I think this may be because of github issue 3737. I can't even run the Austin example from the bokeh docs.
Hopefully this is what you had in mind
from bokeh.plotting import Figure, ColumnDataSource, show, vplot
from bokeh.io import output_file
from bokeh.models import (Slider, CustomJS, GMapPlot,
GMapOptions, DataRange1d, Circle, Line)
import numpy as np
output_file("path.html")
# Create path around roundabout
r = 0.000192
x1 = np.linspace(-1,1,100)*r
x2 = np.linspace(1,-1,100)*r
x = np.hstack((x1,x2))
f = lambda x : np.sqrt(r**2 - x**2)
y1 = f(x1)
y2 = -f(x2)
y = np.hstack((y1,y2))
init_x = 40.233688
init_y = -111.646784
lon = init_x + x
lat = init_y + y
# Initialize data sources.
location = ColumnDataSource(data=dict(x=[lon[0]], y=[lat[0]]))
path = ColumnDataSource(data=dict(x=lon, y=lat))
# Initialize figure, path, and point
"""I haven't been able to verify that the GMapPlot code below works, but
this should be the right thing to do. The zoom may be totally wrong,
but my latlng points should be a path around a roundabout.
"""
##options = GMapOptions(lat=40.233681, lon=-111.646595, map_type="roadmap", zoom=15)
##fig = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=options)
fig = Figure(plot_height=600, plot_width=600)
c = Circle(x='x', y='y', size=10)
p = Line(x='x', y='y')
fig.add_glyph(location, c)
fig.add_glyph(path, p)
# Slider callback
callback = CustomJS(args=dict(location=location, path=path), code="""
var loc = location.get('data');
var p = path.get('data');
t = cb_obj.get('value');
/* set the point location to the path location that
corresponds to the slider position */
loc['x'][0] = p['x'][t];
loc['y'][0] = p['y'][t];
location.trigger('change');
""")
# The way I have written this, 'start' has to be 0 and
# 'end' has to be the length of the array of path points.
slider = Slider(start=0, end=200, step=1, callback=callback)
show(vplot(fig, slider))