Python Bokeh: Restart X axis to 0 on Zoom - python

I have code below that creates a simple line x-y plot.
When I zoom in, I want the x-axis ticker to start at 0 again instead of 3.9/whatever the x point of the zoom was as in the image.
No Zoom:
After Zooming:
How do I do that?
Code:
from bokeh.io import output_file, show, save
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
data = []
x = list(range(11))
y0 = x
y1 = [10 - xx for xx in x]
y2 = [abs(xx - 5) for xx in x]
source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1, y2=y2))
for i in range(3):
p = figure(title="Title " + str(i), plot_width=300, plot_height=300)
if len(data):
p.x_range = data[0].x_range
p.y_range = data[0].y_range
p.circle('x', 'y0', size=10, color="navy", alpha=0.5, legend_label='line1', source=source)
p.legend.location = 'top_right'
p.legend.click_policy = "hide"
data.append(p)
plot_col = column(data)
# show the results
show(plot_col)

This is an unusual requirement, and none of the built-in things behave this way. If you zoom in to the interval [4,7], the the range will be updated [4, 7], and so then the axis will display labels for [4, 7]. If it will suffice to simply display different tick labels, even while the underlying range start/end remain their usual values, then you could use a Custom Extension to generate whatever customized labels you want. There is an example in the User's Guide that already does almost exactly what you want already:
https://docs.bokeh.org/en/latest/docs/user_guide/extensions_gallery/ticking.html#userguide-extensions-examples-ticking
You might also be able to do something even more simply with a FuncTickFormatter, e.g. (untested)
p.xaxis.formatter = FuncTickFormatter(code="""
return tick - ticks[0]
""")

Related

Bokeh initial x axis zoom range and allow full zoom out on x axis with y_range bound

With the following I can set an initial x axis zoom range of the last 4 hours. Ideally I wish to use the wheel zoom to scroll out on the x axis only and view the whole plot but remain bound on the y axis by the range set. The following does not allow this and setting either of the following also does not:
y_range=DataRange1d(0, y_max, bounds="auto")
y_range=DataRange1d(0, y_max) # i.e. bounds=None as per docs
Can anyone assist in how I can allow an initial view on the x axis, allow zoom out on the x axis, but set bounds on the y axis such that the user cannot zoom out beyond the plot boundaries of min y=0 and (in the example) max y=8?
Example code with bounds statically set on y axis as per docs here:
import pandas as pandas
from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource, DataRange1d, Range1d
output_file('stackoverflowq.html')
list1 = [['2020-12-03 09:20:03.175453','5'],['2020-12-04 09:20:03.175453','7'],['2020-12-05 09:20:03.175453','3'],['2020-12-05 09:30:03.175453','4'],['2020-12-05 09:40:03.175453','5'],['2020-12-05 09:50:03.175453','6'],['2020-12-05 10:00:03.175453','4'],['2020-12-05 10:10:03.175453','1'],['2020-12-05 10:20:03.175453','2'],['2020-12-05 10:30:03.175453','8'],['2020-12-05 10:40:03.175453','2'],['2021-01-03 09:20:03.175453','5'],['2021-01-04 09:20:03.175453','7'],['2021-01-05 09:20:03.175453','3'],['2021-01-20 09:30:03.175453','4'],['2021-01-21 01:40:03.175453','5'],['2021-01-21 02:50:03.175453','6'],['2021-01-21 06:00:03.175453','4'],['2021-01-21 07:10:03.175453','1'],['2021-01-21 08:20:03.175453','2'],['2021-01-21 09:30:03.175453','8'],['2021-01-21 10:40:03.175453','2']]
cols = ['DateTime','vals']
df = pandas.DataFrame(list1,columns=cols)
df['DateTime'] = pandas.to_datetime(df['DateTime'])
df = df.set_index('DateTime')
y_max = int(df.vals.unique().max())*1.2
x_max = df.index.unique().max()
x_min = x_max - pandas.Timedelta('4h')
p = figure(x_axis_type="datetime", y_range=DataRange1d(0, y_max, bounds=(0, y_max)), x_range=(x_min, x_max))
p.line(x='DateTime', y='vals', color='blue', source=df)
p.xaxis.axis_label = 'Date Time'
p.yaxis.axis_label = 'vals'
show(p)
You have simply to change the wheel zoom tool. Use tools='xwheel_zoom'.
Example
Please use my minimal example and give me some feedback if you have still this issue.
import pandas as pd # version 1.1.4
from bokeh.plotting import figure, output_notebook, show # version 2.2.3
from bokeh.models import ColumnDataSource, DataRange1d, Range1d
output_notebook()
date_times = pd.Timestamp.now()
x_max = date_times
x_min = date_times - pd.Timedelta('4h')
x = [x_max - pd.Timedelta(f'{t}h') for t in range(5,0,-1)]
y = [2, 5, 8, 2, 7]
other_tools = "pan, box_zoom, save, reset, help,"
p = figure(x_axis_type="datetime", tools="xwheel_zoom,"+other_tools, x_range=(x_min,x_max))
p.circle(x, y, size=10)
show(p)
Inital zoom
Zoomed Out with wheel_zoom
Zoomed Out with xwheel_zoom
You have to select the wheel zoom tool by mouse click.

Heatmap with circles indicating size of population

I would like to produce a heatmap in Python, similar to the one shown, where the size of the circle indicates the size of the sample in that cell. I looked in seaborn's gallery and couldn't find anything, and I don't think I can do this with matplotlib.
It's the inverse. While matplotlib can do pretty much everything, seaborn only provides a small subset of options.
So using matplotlib, you can plot a PatchCollection of circles as shown below.
Note: You could equally use a scatter plot, but since scatter dot sizes are in absolute units it would be rather hard to scale them into the grid.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
N = 10
M = 11
ylabels = ["".join(np.random.choice(list("PQRSTUVXYZ"), size=7)) for _ in range(N)]
xlabels = ["".join(np.random.choice(list("ABCDE"), size=3)) for _ in range(M)]
x, y = np.meshgrid(np.arange(M), np.arange(N))
s = np.random.randint(0, 180, size=(N,M))
c = np.random.rand(N, M)-0.5
fig, ax = plt.subplots()
R = s/s.max()/2
circles = [plt.Circle((j,i), radius=r) for r, j, i in zip(R.flat, x.flat, y.flat)]
col = PatchCollection(circles, array=c.flatten(), cmap="RdYlGn")
ax.add_collection(col)
ax.set(xticks=np.arange(M), yticks=np.arange(N),
xticklabels=xlabels, yticklabels=ylabels)
ax.set_xticks(np.arange(M+1)-0.5, minor=True)
ax.set_yticks(np.arange(N+1)-0.5, minor=True)
ax.grid(which='minor')
fig.colorbar(col)
plt.show()
Here's a possible solution using Bokeh Plots:
import pandas as pd
from bokeh.palettes import RdBu
from bokeh.models import LinearColorMapper, ColumnDataSource, ColorBar
from bokeh.models.ranges import FactorRange
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
import numpy as np
output_notebook()
d = dict(x = ['A','A','A', 'B','B','B','C','C','C','D','D','D'],
y = ['B','C','D', 'A','C','D','B','D','A','A','B','C'],
corr = np.random.uniform(low=-1, high=1, size=(12,)).tolist())
df = pd.DataFrame(d)
df['size'] = np.where(df['corr']<0, np.abs(df['corr']), df['corr'])*50
#added a new column to make the plot size
colors = list(reversed(RdBu[9]))
exp_cmap = LinearColorMapper(palette=colors,
low = -1,
high = 1)
p = figure(x_range = FactorRange(), y_range = FactorRange(), plot_width=700,
plot_height=450, title="Correlation",
toolbar_location=None, tools="hover")
p.scatter("x","y",source=df, fill_alpha=1, line_width=0, size="size",
fill_color={"field":"corr", "transform":exp_cmap})
p.x_range.factors = sorted(df['x'].unique().tolist())
p.y_range.factors = sorted(df['y'].unique().tolist(), reverse = True)
p.xaxis.axis_label = 'Values'
p.yaxis.axis_label = 'Values'
bar = ColorBar(color_mapper=exp_cmap, location=(0,0))
p.add_layout(bar, "right")
show(p)
One option is to use matplotlib's scatter plots with legends and grid. You can specify size of those circles with specifying the scales. You can also change the color of each circle. You should somehow specify X,Y values so that the circles sit straight on lines. This is an example I got from here:
volume = np.random.rayleigh(27, size=40)
amount = np.random.poisson(10, size=40)
ranking = np.random.normal(size=40)
price = np.random.uniform(1, 10, size=40)
fig, ax = plt.subplots()
# Because the price is much too small when being provided as size for ``s``,
# we normalize it to some useful point sizes, s=0.3*(price*3)**2
scatter = ax.scatter(volume, amount, c=ranking, s=0.3*(price*3)**2,
vmin=-3, vmax=3, cmap="Spectral")
# Produce a legend for the ranking (colors). Even though there are 40 different
# rankings, we only want to show 5 of them in the legend.
legend1 = ax.legend(*scatter.legend_elements(num=5),
loc="upper left", title="Ranking")
ax.add_artist(legend1)
# Produce a legend for the price (sizes). Because we want to show the prices
# in dollars, we use the *func* argument to supply the inverse of the function
# used to calculate the sizes from above. The *fmt* ensures to show the price
# in dollars. Note how we target at 5 elements here, but obtain only 4 in the
# created legend due to the automatic round prices that are chosen for us.
kw = dict(prop="sizes", num=5, color=scatter.cmap(0.7), fmt="$ {x:.2f}",
func=lambda s: np.sqrt(s/.3)/3)
legend2 = ax.legend(*scatter.legend_elements(**kw),
loc="lower right", title="Price")
plt.show()
Output:
I don't have enough reputation to comment on Delenges' excellent answer, so I'll leave my comment as an answer instead:
R.flat doesn't order the way we need it to, so the circles assignment should be:
circles = [plt.Circle((j,i), radius=R[j][i]) for j, i in zip(x.flat, y.flat)]
Here is an easy example to plot circle_heatmap.
from matplotlib import pyplot as plt
import pandas as pd
from sklearn.datasets import load_wine as load_data
from psynlig import plot_correlation_heatmap
plt.style.use('seaborn-talk')
data_set = load_data()
data = pd.DataFrame(data_set['data'], columns=data_set['feature_names'])
#data = df_corr_selected
kwargs = {
'heatmap': {
'vmin': -1,
'vmax': 1,
'cmap': 'viridis',
},
'figure': {
'figsize': (14, 10),
},
}
plot_correlation_heatmap(data, bubble=True, annotate=False, **kwargs)
plt.show()

show the labels of Bokeh datetime axis in exact position of points

I'm using the datetime axis of Bokeh. In the Bokeh data source, I have my x in numpy datetime format and others are y numbers. I'm looking for a way to show the label of the x datetimx axis right below the point. I want Bokeh to show the exact datetime that I provided via my data source, not some approximation! For instance, I provide 5:15:00 and it shows 5:00:00 somewhere before the related point.I plan to stream data to the chart every 1 hour, and I want to show 5 points each time. Therefore, I need 5 date-time labels. How can I do that? I tried p.yaxis[0].ticker.desired_num_ticks = 5 but it didn't help. Bokeh still shows as many number of ticks as it wants! Here is my code and result:
import numpy as np
from bokeh.models.sources import ColumnDataSource
from bokeh.plotting import figure
from bokeh.io import show
from bokeh.palettes import Category10
p = figure(x_axis_type="datetime", plot_width=800, plot_height=500)
data = {'x':
[np.datetime64('2019-01-26T03:15:10'),
np.datetime64('2019-01-26T04:15:10'),
np.datetime64('2019-01-26T05:15:10'),
np.datetime64('2019-01-26T06:15:10'),
np.datetime64('2019-01-26T07:15:10')],
'A': [10,25,15,55,40],
'B': [60,50,80,65,120],}
source = ColumnDataSource(data=data)
cl = Category10[3][1:]
r11 = p.line(source=source, x='x', y='A', color=cl[0], line_width=3)
r12 = p.line(source=source, x='x', y='B', color=cl[1], line_width=3)
p.xaxis.formatter=DatetimeTickFormatter(
seconds=["%H:%M:%S"],
minsec=["%H:%M:%S"],
minutes=["%H:%M:%S"],
hourmin=["%H:%M:%S"],
hours=["%H:%M:%S"],
days=["%H:%M:%S"],
months=["%H:%M:%S"],
years=["%H:%M:%S"],
)
p.y_range.start = -100
p.x_range.range_padding = 0.1
p.yaxis[0].ticker.desired_num_ticks = 5
p.xaxis.major_label_orientation = math.pi/2
show(p)
and here is the result:
As stated in the docs, num_desired_ticks is only a suggestion. If you want a ticks at specific locations that do not change, then you can use a FixedTicker, which can be set by plain list as convenience:
p.xaxis.ticker = [2, 3.5, 4]
For datetimes, you would pass the values as milliseconds since epoch.
If you want a fixed number of ticks, but the locations may change (i.e. because the range may change), then there is nothing built in to do that. You could make a custom ticker extension.

Changing bokeh grid lines position

I am trying to plot a few points on a graph, similarly to a heat map.
Sample code (adapted from the heat map section here):
import pandas as pd
from bokeh.io import output_notebook, show
from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LinearColorMapper, PrintfTickFormatter
from bokeh.plotting import figure
from bokeh.transform import transform
import numpy as np
# change this if you don't run it on a Jupyter Notebook
output_notebook()
testx = np.random.randint(0,10,10)
testy = np.random.randint(0,10,10)
npdata = np.stack((testx,testy), axis = 1)
hist, bins = np.histogramdd(npdata, normed = False, bins = (10,10), range=((0,10),(0,10)))
data = pd.DataFrame(hist, columns = [str(x) for x in range(10)])
data.columns.name = 'y'
data['x'] = [str(x) for x in range(10)]
data = data.set_index('x')
df = pd.DataFrame(data.stack(), columns=['present']).reset_index()
source = ColumnDataSource(df)
colors = ['lightblue', "yellow"]
mapper = LinearColorMapper(palette=colors, low=df.present.min(), high=df.present.max())
p = figure(plot_width=400, plot_height=400, title="test circle map",
x_range=list(data.index), y_range=list((data.columns)),
toolbar_location=None, tools="", x_axis_location="below")
p.circle(x="x", y="y", size=20, source=source,
line_color=None, fill_color=transform('present', mapper))
p.axis.axis_line_color = None
p.axis.major_tick_line_color = None
p.axis.major_label_text_font_size = "10pt"
p.axis.major_label_standoff = 10
p.xaxis.major_label_orientation = 0
show(p)
That returns:
Now, as you can see, the grid lines are centered on the points(circles), and I would like, instead to have the circles enclosed in a square created by the lines.
I went through this to see if I could find information on how to offset the grid lines by 0.5 (that would have worked), but I was not able to.
There's nothing built into Bokeh to accomplish this kind of offsetting of categorical ticks, but you can write a custom extension to do it:
CS_CODE = """
import {CategoricalTicker} from "models/tickers/categorical_ticker"
export class MyTicker extends CategoricalTicker
type: "MyTicker"
get_ticks: (start, end, range, cross_loc) ->
ticks = super(start, end, range, cross_loc)
# shift the default tick locations by half a categorical bin width
ticks.major = ([x, 0.5] for x in ticks.major)
return ticks
"""
class MyTicker(CategoricalTicker):
__implementation__ = CS_CODE
p.xgrid.ticker = MyTicker()
p.ygrid.ticker = MyTicker()
Note that Bokeh assumes CoffeeScript by default when the code is just a string, but it's possible to use pure JS or TypeScript as well. Adding this to your code yields:
Please note the comment about output_notebook you must call it (possibly again, if you have called it previously) after the custom model is defined, due to #6107

How can I accomplish `set_xlim` or `set_ylim` in Bokeh?

I create a figure in a function, e.g.
import numpy
from bokeh.plotting import figure, show, output_notebook
output_notebook()
def make_fig():
rows = cols = 16
img = numpy.ones((rows, cols), dtype=numpy.uint32)
view = img.view(dtype=numpy.uint8).reshape((rows, cols, 4))
view[:, :, 0] = numpy.arange(256)
view[:, :, 1] = 265 - numpy.arange(256)
fig = figure(x_range=[0, c], y_range=[0, rows])
fig.image_rgba(image=[img], x=[0], y=[0], dw=[cols], dh=[rows])
return fig
Later I want to zoom in on the figure:
fig = make_fig()
# <- zoom in on plot, like `set_xlim` from matplotlib
show(fig)
How can I do programmatic zoom in bokeh?
One way is to can things with a simple tuple when creating a figure:
figure(..., x_range=(left, right), y_range=(bottom, top))
But you can also set the x_range and y_range properties of a created figure directly. (I had been looking for something like set_xlim or set_ylim from matplotlib.)
from bokeh.models import Range1d
fig = make_fig()
left, right, bottom, top = 3, 9, 4, 10
fig.x_range=Range1d(left, right)
fig.y_range=Range1d(bottom, top)
show(fig)
As of Bokeh 2.X, it seems it is not possible to replace figure.{x,y}_range with a new instance of Range1d from DataRange1d or vice versa.
Instead one has to set figure.x_range.start and figure.x_range.end for a dynamic update.
See https://github.com/bokeh/bokeh/issues/8421 for further details on this issue.
Maybe a naive solution, but why not passing the lim axis as argument of your function?
import numpy
from bokeh.plotting import figure, show, output_notebook
output_notebook()
def make_fig(rows=16, cols=16,x_range=[0, 16], y_range=[0, 16], plot_width=500, plot_height=500):
img = numpy.ones((rows, cols), dtype=numpy.uint32)
view = img.view(dtype=numpy.uint8).reshape((rows, cols, 4))
view[:, :, 0] = numpy.arange(256)
view[:, :, 1] = 265 - numpy.arange(256)
fig = figure(x_range=x_range, y_range=y_range, plot_width=plot_width, plot_height=plot_height)
fig.image_rgba(image=[img], x=[0], y=[0], dw=[cols], dh=[rows])
return fig
you can also use it directly
p = Histogram(wind , xlabel= 'meters/sec', ylabel = 'Density',bins=12,x_range=Range1d(2, 16))
show(p)

Categories

Resources