How to link axes of all plots in a Bokeh layout? - python-3.x

I am designing a Bokeh layout using the Bokeh server. I am defining two main columns (see attached image), and I am attempting to link the x-axis of all plots on the right column. The problems are that:
I am trying to make this app as dynamic as possible, which mean that depending on the case-study, not all the plots will be available, and each individual plot is set from a separate function
Each plot object is stored in a list, and I don't know how to access their properties
The reference plot is not known a priori so I don't see how I can implement the example in the Bokeh doc - in other words, I need to first plot all the subplots to then get the relevant x_range
So I was wondering if it is possible to set the linking behaviour a posteriori once all plots in the column are defined (i.e. the output of plotDataset below). My intuition is to loop through the objects, get the children and set the x_range to the first plot, but I don't know how to do that.
Below is a simplified version of what I am trying to achieve. Ideally, I would get the x_range of the first plot of fCol and apply it to all other plots just before return column(fCol)
Any idea is greatly appreciated! And also, I am fairly beginner with Python so please shout if you see anything else horrible!
Thank you
def plotTS(data, col):
tTmp = []
# A loop that defines each tab of the plot
for i in range(len(col)):
fTmp = figure()
fTmp.circle(data[:]['time'], data[:][col[i]], color=color)
# Append tab
tTmp.append(Panel(child=fTmp))
# Return the tabs
return Tabs(tabs=tTmp)
def plotDataset(data):
col = ['NDVI', 'EVI'] # Name of the tabs
fCol = []
fCol.append(plotTS(data, col))
# NOTE: I use an append approach because in reality plotTS is called more than once
return column(fCol)
# General layout - I did not include the code for the left column
layout = row(leftColumn, plotDataset(data))
Link to image

See code below (Bokeh v1.1.0).
from bokeh.models import Panel, Tabs, Column, Row
from bokeh.plotting import figure
from tornado.ioloop import IOLoop
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
def modify_doc(doc):
leftColumn = Column(figure())
def plotTS(data, col):
tTmp = []
for i in col:
fTmp = figure()
fTmp.circle(data['x'], data['y'], color='black')
tTmp.append(Panel(child=fTmp, title = i))
return Tabs(tabs=tTmp)
def plotDataset(data):
col = ['NDVI', 'EVI']
fCol = plotTS(data, col)
shared_range = None
for panel in fCol.tabs:
fig = panel.child
if shared_range is None:
shared_range = fig.x_range
else:
fig.x_range = shared_range
return Column(fCol)
layout = Row(leftColumn, plotDataset(data = dict(x = [1, 2, 3], y = [1, 2, 3])))
doc.add_root(layout)
io_loop = IOLoop.current()
server = Server(applications = {'/app': Application(FunctionHandler(modify_doc))}, io_loop = io_loop, port = 5002)
server.start()
server.show('/app')
io_loop.start()

Related

Disable constant position reset of Matplotlib chart

I have a python code that by using Matplotlib displays a candlestick chart real time, so the last bar updates each second, the problem Is that the program doesn't let me scroll back/zoom in ... (interacting with the chart) because It keeps reset the position. How can I solve this?
Thanks, have a good day.
import pandas as pd
import mplfinance as mpf
import matplotlib.animation as animation
# Class to simulate getting more data from API:
class RealTimeAPI():
def __init__(self):
self.data_pointer = 0
self.data_frame = pd.read_csv('SP500_NOV2019_IDay.csv',
index_col=0, parse_dates=True)
# self.data_frame = self.data_frame.iloc[0:120,:]
self.df_len = len(self.data_frame)
def fetch_next(self):
r1 = self.data_pointer
self.data_pointer += 1
if self.data_pointer >= self.df_len:
return None
return self.data_frame.iloc[r1:self.data_pointer, :]
def initial_fetch(self):
if self.data_pointer > 0:
return
r1 = self.data_pointer
self.data_pointer += int(0.2*self.df_len)
return self.data_frame.iloc[r1:self.data_pointer, :]
rtapi = RealTimeAPI()
resample_map = {'Open': 'first',
'High': 'max',
'Low': 'min',
'Close': 'last'}
resample_period = '15T'
df = rtapi.initial_fetch()
rs = df.resample(resample_period).agg(resample_map).dropna()
fig, axes = mpf.plot(rs, returnfig=True, figsize=(11, 8),
type='candle', title='\n\nGrowing Candle')
ax = axes[0]
def animate(ival):
global df
global rs
nxt = rtapi.fetch_next()
if nxt is None:
print('no more data to plot')
ani.event_source.interval *= 3
if ani.event_source.interval > 12000:
exit()
return
df = df.append(nxt)
rs = df.resample(resample_period).agg(resample_map).dropna()
ax.clear()
mpf.plot(rs, ax=ax, type='candle')
ani = animation.FuncAnimation(fig, animate, interval=250)
mpf.show()
The code you have posted is the "growing candle animation" example from the mplfinance repository. Mplfinance does not use Matlab but rather MatPlotLib.
(It is temping to edit your question and change all references of "Matlab" to "Mplfinance/Matplotlib" but I will leave that to you to do, just in case I'm missing something or you are actually doing something with Matlab. Furthermore, just to be clear, the code is plotting a "candlestick chart," not a "bar chart." By the way, MatPlotLib was originally written to somewhat resemble the Matlab plotting interface, so some confusion is understandable, but matplotlib has since grown in many ways).
The comment by #CrisLuengo is correct in that the mplfinance code is re-drawing the plot more-or-less from scratch with each animation frame, so any interactive changes (such as zooming in) will be re-set with each animation frame. The mplfinance animation examples do this to keep the code simpler.
I believe it is possible to accomplish what you want (animation with interactivity) however I've never done it myself, so without being explicit as to how to do it, if I were going to do so, I would probably do something along the lines of the following posts:
https://stackoverflow.com/a/46327978/1639359
https://stackoverflow.com/a/46328223/1639359
https://github.com/matplotlib/mplfinance/issues/262#issuecomment-692316347
Full disclosure: I am the maintainer of the mplfinance package. Perhaps someday we will enhance mplfinance to make this easier; but for now such an enhancement is not a priority; therefore one of the above more complex solutions is necessary.

How to animate multiple figures at the same time

I made an animation for sorting algorithms and it it works great for animating one sorting algorithm, but when I try to animate multiple at the same time both windows come up but none of them are moving. I was wondering how I could go around to fix this.
When I run the code the first figure is stuck on the first frame and the second figure jumps to the last frame
import matplotlib.pyplot as plt
from matplotlib import animation
import random
# my class for getting data from sorting algorithms
from animationSorters import *
def sort_anim(samp_size=100, types=['bubblesort', 'quicksort']):
rndList = random.sample(range(1, samp_size+1), samp_size)
anim = []
for k in range(0, len(types)):
sort_type = types[k]
animation_speed = 1
def barlist(x):
if sort_type == 'bubblesort':
l = bubblesort_swaps(x)#returns bubble sort data
elif sort_type == 'quicksort':
l = quicksort_swaps(x)#returns quick sort data
final = splitSwaps(l, len(x))
return final
fin = barlist(rndList)
fig = plt.figure(k+1)
plt.rcParams['axes.facecolor'] = 'black'
n= len(fin)#Number of frames
x=range(1,len(rndList)+1)
barcollection = plt.bar(x,fin[0], color='w')
anim_title = sort_type.title() + '\nSize: ' + str(samp_size)
plt.title(anim_title)
def animate(i):
y=fin[i]
for i, b in enumerate(barcollection):
b.set_height(y[i])
anim.append(animation.FuncAnimation(fig,animate, repeat=False,
blit=False, frames=n, interval=animation_speed))
plt.show()
sort_anim()
As explained in the documentation for the animation module:
it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
Therefore you need to return the references to your animations from your function, otherwise those objects are destroyed when exiting the function.
Consider the following simplification of your code:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
def my_func(nfigs=2):
anims = []
for i in range(nfigs):
fig = plt.figure(num=i)
ax = fig.add_subplot(111)
col = ax.bar(x=range(10), height=np.zeros((10,)))
ax.set_ylim([0, 1])
def animate(k, bars):
new_data = np.random.random(size=(10,))
for j, b in enumerate(bars):
b.set_height(new_data[j])
return bars,
ani = animation.FuncAnimation(fig, animate, fargs=(col, ), frames=100)
anims.append(ani)
return anims
my_anims = my_func(3)
# calling simply my_func() here would not work, you need to keep the returned
# array in memory for the animations to stay alive
plt.show()

Interaction with multiple widgets Bokeh

EDIT: Some users have mentioned that the question is unclear. My objective is to keep track of previous states.
I am trying to create a plot that can be modified using 3 widgets. However, every new widget change does not take into consideration previous widget selections (for example, if a selection is made using widget 1 and then widget 2 is modified, the modification of widget 2 considers the original graph and not the changes made with widget 1).
I am trying to avoid using Custom_JS as I have no experience with Javascript. Is there any way to combine the functions so that any change in the widgets takes into consideration previous widget interactions?
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib
from datetime import date
from ipywidgets import interact
from bokeh.io import push_notebook, show, output_notebook, curdoc
from bokeh.plotting import figure
from bokeh.layouts import column, layout, widgetbox, row
from bokeh.models import ColumnDataSource, HoverTool, CustomJS, Panel
from bokeh.layouts import widgetbox
from bokeh.models.widgets import RangeSlider, Slider, Select,
DateRangeSlider, Tabs
df = pd.read_csv("/Users/danielmontillanavas/Desktop/Tiller/00_Data/SF_may_correct_decimal.csv", sep =',',decimal=',')
df = df.drop(['Unnamed: 0'], axis=1)
df.rename(columns = {'Stamped phone from HS':'phone','Contact Email':'Email','Account Name':'Account_name',
'Opportunity ID':'ID', 'Close Date':'Close_date','Stamped-date of closed (DO NOT USE)':'Stamped_date',
'Quote Amount':'Quote_Amount', 'Lead Source':'Source','Desired activation date':'Activ_date'},
inplace=True)
df.Close_date = pd.to_datetime(df.Close_date, format='%Y-%m-%d')
cols_num = ['Quote_Amount','DISCOUNT']
df[cols_num] = df[cols_num].apply(pd.to_numeric)
df_closed = df[df['Stage']=='Closed']
df_closed.fillna("Unknown", inplace=True)
start_point = min(df['Quote_Amount'])
end_point = max(df['Quote_Amount'])
TOOLS = 'pan,wheel_zoom,box_zoom,reset,tap,save,box_select,lasso_select'
source = ColumnDataSource(df_closed)
hover = HoverTool(
tooltips=[
("Quote", "$x"),
("Discount", "$y")
]
)
p = figure(title='Quotes per Source - Closed deals',tools=[hover,TOOLS],
plot_height=800, plot_width=800)
p.circle('Quote_Amount','DISCOUNT',source=source, size = 8, color = 'CornflowerBlue', alpha = 0.6)
N = 20000
slider = Slider(start=start_point, end=end_point, step=10, value=N,
title='Select Quote Amount Cutoff')
dfList = df_closed.Source.unique().tolist()
All_view = ['All']
source_options = All_view + dfList
menu = Select(title = "Select Lead Source",options=source_options, value = 'All')
first_date = min(df['Close_date'])
last_date = max(df['Close_date'])
date_range_slider = DateRangeSlider(title="Select Date Range ", start=first_date, end=date.today(), value=(date(2017, 9, 7), date(2017, 10, 15)), step=1)
def slider_callback(attr, old, new):
N = new # this works also with slider.value but new is more explicit
new1 = ColumnDataSource(df_closed.loc[(df_closed.Quote_Amount < N)])
source.data = new1.data
slider.on_change('value',slider_callback)
def menu_callback(attr, old, new):
if menu.value == 'All': new2 = ColumnDataSource(df_closed)
else: new2 = ColumnDataSource(df_closed.loc[(df_closed.Source == menu.value)])
source.data = new2.data
menu.on_change('value',menu_callback)
def date_callback(attr, old, new):
start = date_range_slider.value_as_datetime[0].strftime("%Y-%m-%d")
end = date_range_slider.value_as_datetime[1].strftime("%Y-%m-%d")
df_closed_new = df_closed[df_closed['Close_date'] >= start]
df_closed_new = df_closed[df_closed['Close_date'] <= end]
new3 = ColumnDataSource(df_closed_new)
source.data = new3.data
date_range_slider.on_change('value',date_callback)
# Put controls in a single element
controls = widgetbox(menu, slider, date_range_slider)
# Create a row layout
layout = row(controls, p)
curdoc().add_root(layout)
You question is not very clear to me. Are you talking about the previous state of widgets? The callbacks have access to the current state of all the other widgets so if you want to maintain a history of their previous states you'll have to explicitly keep track of that.
However I immediately notice on thing that should not be done, so I am going to post an answer just to draw attention to it. Don't create new CDS objects just to use their .data attribute and throw them away:
new1 = ColumnDataSource(df_closed.loc[(df_closed.Quote_Amount < N)])
source.data = new1.data
There is alot of machinery under the covers that affords all the automatic synchronization other features of Bokeh. CDS in particular are extremely heavyweight, complicated objects, and doing this above is a known anti-pattern that can break things. Instead, if you just need a new suitable .data dict, then use from_df:
new_data = ColumnDataSource.from_df(df_closed.loc[(df_closed.Quote_Amount < N)])
source.data = new_data

bokeh server app returns blank screen

I am trying to render a simple app on bokeh server. I took the script straight from the bokeh website.
# myapp.py
from random import random
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.palettes import RdYlBu3
from bokeh.plotting import figure, curdoc
# create a plot and style its properties
p = figure(x_range=(0, 100), y_range=(0, 100), toolbar_location=None)
p.border_fill_color = 'black'
p.background_fill_color = 'black'
p.outline_line_color = None
p.grid.grid_line_color = None
# add a text renderer to our plot (no data yet)
r = p.text(x=[], y=[], text=[], text_color=[], text_font_size="20pt",
text_baseline="middle", text_align="center")
i = 0
ds = r.data_source
# create a callback that will add a number in a random location
def callback():
global i
# BEST PRACTICE --- update .data in one step with a new dict
new_data = dict()
new_data['x'] = ds.data['x'] + [random()*70 + 15]
new_data['y'] = ds.data['y'] + [random()*70 + 15]
new_data['text_color'] = ds.data['text_color'] + [RdYlBu3[i%3]]
new_data['text'] = ds.data['text'] + [str(i)]
ds.data = new_data
i = i + 1
# add a button widget and configure with the call back
button = Button(label="Press Me")
button.on_click(callback)
# put the button and plot in a layout and add to the document
curdoc().add_root(column(button, p))
I run this in my Win Python Command Prompt.exe using the below:
python -m bokeh serve --show main.py
This runs the app, but I am left with a blank webpage. See the output in the command prompt below:
Can anyone help with how to get this to show the doc?

PyQtGraph SpotItem returns 'NoneType' when calling user data

I am trying to get the associated user data of a point (which is a SpotItem instance) in a scatter plot when clicked on it. While methods listed in the documentation (like pos() or size()) seem to work fine, I recieve a NoneType object when I apply the data() method. I actually expected it to return my user data, but it doesn't.
So, how can I retrieve my associated original data that I put in?
What I actually need is something like an index i of the original input lists for a clicked point that would allow me to track back the corresponding x[i] y[i] set.
Here is my code sample:
import pyqtgraph as pg
#some dummy data
x=[0,1,2,3,4,5,3.5,3.4]
y=[5,4,3,2,1,0,3.4,3.5]
win=pg.GraphicsWindow()
p1=win.addPlot(row=1, col=1)
my_data=pg.ScatterPlotItem(x,y,symbol='o',size=30)
p1.addItem(my_data)
def clicked(items,points):
print("point data: ",points[0].data())
my_data.sigClicked.connect(clicked)
I am using Python 3.6 (with Spyder 3.1.4), Qt 5.6 and PyQt 5
sigClicked gave us the item (ScatterPlotItem) that has been pressed and the points (SpotItem) where they have been pressed, with the seconds we can obtain the element Point() that gives us the position, and this has the methods x() y y() that return the coordinates. From item we can get all the x and y that you have initially placed through data['x'] and data['y'], respectively, then we have the points pressed and all the points possible so to find the indexes we use np.argwhere() and then we intersect the values with np.intersect1d(), at the end we eliminate the repeated points with set.
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
app = QtGui.QApplication([])
x=[0,1,2,3,4,5,3.5,3.4, 3.4]
y=[5,4,3,2,1,0,3.4,3.5, 3.5]
win=pg.GraphicsWindow()
p1=win.addPlot(row=1, col=1)
my_data=pg.ScatterPlotItem(x,y,symbol='o',size=30)
p1.addItem(my_data)
def clicked(item, points):
indexes = []
for p in points:
p = p.pos()
x, y = p.x(), p.y()
lx = np.argwhere(item.data['x'] == x)
ly = np.argwhere(item.data['y'] == y)
i = np.intersect1d(lx, ly).tolist()
indexes += i
indexes = list(set(indexes))
print(indexes)
my_data.sigClicked.connect(clicked)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
like this:
def clicked(items,obj, points):
now_point = point[0].pos()
x = now_point[0]
y = now_point[1]
Then you can get what you want.

Resources