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

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.

Related

Bokeh plot line

I tried to write these codes to display the dataseries plot, but no data was not displayed.
I dont know where is the problem exactly.
data=pd.read_csv('weather.csv')[['STA','Date','Precip','MaxTemp','MinTemp','MeanTemp','Snowfall']].dropna()
data = data[data['Precip'] != 'T']
data['Precip'].astype(float)
data['STA']=data['STA'].astype("string")
data['Date']=pd.to_datetime(data['Date'])
stations=list(set(data['STA']))
stations.sort()
select_inital=select.value
colors = list(Category20_16)
colors.sort()
subset=data[data['STA']==select_inital]
initial_values= list(set(subset['STA']))
for i, j in enumerate(initial_values):
subset=data[data['STA']==j]
d=subset[['Date','Precip']]
d.sort_values('Date')
x=d['Date']
y=d['Precip']
d = ColumnDataSource(d)
p = figure(plot_width=700, plot_height=700, x_range=(0,200), title='Weather Evolution',x_axis_label='Date', y_axis_label='Precip',x_axis_type='datetime')
p.line(x,y, legend_label="Evolution", line_width=2)
show(p)
This is just guessing but I believe the problem is, that you are trying to set limits to the x_range. Bokeh is evaluating dates as milliseconds from 1970-01-01 00:00 and your x_range=(0,200) is also interpreted as millisecond. This means the visible area is very small and starts at January 1st 1970. You could use the defaults by bokeh instead.
Minimal example
This is your code for the figure except I removed the x_range.
import pandas as pd
from bokeh.plotting import figure, show, output_notebook
output_notebook()
x = pd.date_range('2022-12-01', '2022-12-24', freq='D')
y = list(range(1,25))
p = figure(
plot_width=700,
plot_height=700,
# x_range=(0,200),
title='Weather Evolution',
x_axis_label='Date',
y_axis_label='Precip',
x_axis_type='datetime'
)
p.line(x,y, legend_label="Evolution", line_width=2)
show(p)
Bokeh default x_range
x_range by user
Comment
If you want to set the x_range for a axis with type "datetime" you can pass timestamp objects to it.
Valid are among other things (e.g. float)
# datetime
from datetime import datetime
x_range=(datetime(2022,12, 7),datetime(2022,12, 10))
# pandas
import pandas as pd
x_range=(pd.Timestamp('2022-12-07'),pd.Timestamp('2022-12-10'))

How to set initial zoom of bokeh box chart of pandas group with a large number of categories

I'm plotting covid-19 data for countries grouped by World Bank regions using pandas and Bokeh.
from bokeh.io import output_file, show
from bokeh.palettes import Spectral5
from bokeh.plotting import figure
from bokeh.transform import factor_cmap
group = data.groupby(["region", "CountryName"])
index_cmap = factor_cmap(
'region_CountryName',
palette=Spectral5,
factors=sorted(data.region.unique()),
end=1
)
p = figure(plot_width=800, plot_height=600, title="Confirmed cases per 100k people by country",
x_range=group, toolbar_location="left")
p.vbar(x='region_CountryName', top='ConfirmedPer100k_max', width=1, source=group,
line_color="white", fill_color=index_cmap, )
p.y_range.start = 0
p.xgrid.grid_line_color = None
p.xaxis.major_label_orientation = 3.14159/2
p.xaxis.group_label_orientation = 3.14159/2
p.outline_line_color = None
show(p)
And I get a
I would like to set some sort of initial zoom into the x-axis to get a more manageable image
, which I got by manually zooming in.
Any suggestions?
You should be able to accomplish this with the x_range parameter. In this example, the plot's x range would be the first 20 countries. You can adjust as needed. You might also have to mess around a bit to get the group_cn_list correct. It's hard to say without seeing your data. If you can post a df example for reproducibility, it would help.
group_cn_list = group["CountryName"].tolist()
p = figure(plot_width=800, plot_height=600, title="Confirmed cases per 100k people by country",
x_range=group_cn_list[0:20], toolbar_location="left")

Duplicated legend labels on Bokeh

I'm trying to display qualitative data using a donut plot with the bokeh library. I have 2 datasets sharing some data labels, and I want to have a unified legend that gathers both labels.
I have managed to either show the legend for only one plot, or have it for both but with repeated items. However, I did not find a way to have unique entries. Here is a sample code to show my issue:
from math import pi
import pandas as pd
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.transform import cumsum
from bokeh.palettes import Set3
# Create fake data
df = pd.DataFrame(
{'label': ['X{}'.format(i) for i in range(0, 4)] + ['X{}'.format(i) for i in range(2, 8)],
'angle': [2*pi / 4] * 4 + [2*pi / 6] * 6,
'group': [1]*4 + [2]*6})
# Set up colors
unique_labels = df.label.unique()
color_mapping = pd.Series(dict(zip(unique_labels, Set3[len(unique_labels)])))
df['color'] = color_mapping.loc[df.label].values
# Plot two concentric donuts
p = figure(title='Test', tools="hover", tooltips="#label")
p.annular_wedge(source=df[df.group==1], x=0, y=1, inner_radius=0.5, outer_radius=0.6,
start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
line_color="white", fill_color='color', legend_group='label')
p.annular_wedge(source=df[df.group==2], x=0, y=1, inner_radius=0.3, outer_radius=0.4,
start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
line_color="white", fill_color='color', legend_group='label')
show(p)
In the end, I get the following result:
Any idea to solve it?
I found other related issues (i.e. matplotlib), but not for bokeh.
I think this will work:
legend_tmp = {x.label['value']: x for x in p.legend.items}
p.legend.items.clear()
p.legend.items.extend(legend_tmp.values())
When it creates the legend for the plot, it is adding all of the items for both angular_wedge's but it doesn't get deduplicated the way you might expect, since the legend members are fairly complex objects themselves (meaning they are identified by more than just the value of the label).

Python Bokeh: Restart X axis to 0 on Zoom

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]
""")

How can I add label to a Bokeh barchart?

I have a dataframe as
df = pd.DataFrame(data = {'Country':'Spain','Japan','Brazil'],'Number':[10,20,30]})
I wanted to plot a bar chart with labels (that is value of 'Number') annotated on the top for each bar and proceeded accordingly.
from bokeh.charts import Bar, output_file,output_notebook, show
from bokeh.models import Label
p = Bar(df,'Country', values='Number',title="Analysis", color = "navy")
label = Label(x='Country', y='Number', text='Number', level='glyph',x_offset=5, y_offset=-5)
p.add_annotation(label)
output_notebook()
show(p)
But I got an error as ValueError: expected a value of type Real, got COuntry of type str.
How do I solve this issue ?
Label produces a single label at position x and y. In you example, you are trying to add multiple labels using the data from your DataFrame as coordinates. Which is why you are getting your error message x and y need to be real coordinate values that map to the figure's x_range and y_range. You should look into using LabelSet (link) which can take a Bokeh ColumnDataSource as an argument and build multiple labels.
Unforutnately, you are also using a Bokeh Bar chart which is a high level chart which creates a categorical y_range. Bokeh cannot put labels on categorical y_ranges for now. You can circumvent this problem by creating a lower level vbar chart using placeholder x values and then styling it to give it the same look as your original chart. Here it is in action.
import pandas as pd
from bokeh.plotting import output_file, show, figure
from bokeh.models import LabelSet, ColumnDataSource, FixedTicker
# arbitrary placeholders which depends on the length and number of labels
x = [1,2,3]
# This is offset is based on the length of the string and the placeholder size
offset = -0.05
x_label = [x + offset for x in x]
df = pd.DataFrame(data={'Country': ['Spain', 'Japan', 'Brazil'],
'Number': [10, 20, 30],
'x': x,
'y_label': [-1.25, -1.25, -1.25],
'x_label': x_label})
source = ColumnDataSource(df)
p = figure(title="Analysis", x_axis_label='Country', y_axis_label='Number')
p.vbar(x='x', width=0.5, top='Number', color="navy", source=source)
p.xaxis.ticker = FixedTicker(ticks=x) # Create custom ticks for each country
p.xaxis.major_label_text_font_size = '0pt' # turn off x-axis tick labels
p.xaxis.minor_tick_line_color = None # turn off x-axis minor ticks
label = LabelSet(x='x_label', y='y_label', text='Number',
level='glyph', source=source)
p.add_layout(label)
show(p)

Categories

Resources