How can I add label to a Bokeh barchart? - python

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)

Related

How do I fix the error E-1019 (DUPLICATE_FACTORS) when using categorical y-axis in Bokeh?

I have a Pandas dataframe which has values for the y axis spread over 3 columns. Those I want to show in a categorical y-axis. Then I have a column for x and a column for the color. From those values I want to create a heatmap.
I created the following code, which returns the error E-1019 (DUPLICATE_FACTORS): FactorRange must specify a unique list of categorical factors for an axis
from bokeh.io import show
from bokeh.models import ColumnDataSource, FactorRange, LinearColorMapper
from bokeh.plotting import figure
from bokeh.palettes import Greys256
mapper = LinearColorMapper(palette=Greys256, low=0, high=5, high_color = 'red')
df_in = pd.DataFrame([['cat1', 'ccat1', 'cccat1', 4, 20],['cat1', 'ccat1', 'cccat1', 5, 15],['cat1', 'ccat1', 'cccat1', 6, 10]], columns=['key1','key2', 'key3', 'x', 'color'])
factors = list(df_in[['key1', 'key2', 'key3']].astype(str).itertuples(index=False, name=None))
data = dict(
y=factors,
x=list(df_in['x'].astype(int)),
color=list(df_in['color'].astype(int)),
)
source = ColumnDataSource(data=data)
p = figure(y_range=FactorRange(*factors))
p.rect(y='y', x='x', width=1, height=0.75, source = source, fill_color={'field': 'color', 'transform': mapper})
show(p)
When I construct the dictionary inside data manually by hardcoding it (including duplicated values in key1-3, I do not get this error.
Do I extract the dataframe wrong?
Figured it out by myself:
The FactorRange needs to be unique while the factors in the y axis should keep having an entry for each value in the heatmap.
A list can be made unique with list(set(factors)))
p = figure(y_range=FactorRange(*list(set(factors)))

Plotting Bokeh bar chart using sum of grouped Pandas column

I'm trying to create a bar chart to see which stores had the biggest revenue in my dataset. Using the default Pandas plot I can do that in one line:
df.groupby('store_name')['sale_value'].sum().sort_values(ascending=False).head(20).plot(kind='bar')
But this chart is not very interactive and I can't see the exact values, so I want to try and create it using Bokeh and be able to mouseover a bar and see the exact amout, for example.
I tried doing the following but just got a blank page:
source = ColumnDataSource(df.groupby('store_name')['sale_value'])
plot = Plot()
glyph = VBar(x='store_name', top='sale_value')
plot.add_glyph(source, glyph)
show(plot)
and if I change source to ColumnDataSource(df.groupby('store_name')['sale_value'].sum()) I get 'ValueError: expected a dict or pandas.DataFrame, got store_name'
How can I create this chart with mouseover using Bokeh?
Let's asume this is our DataFrame:
df = pd.DataFrame({'store_name':['a', 'b', 'a', 'c'], 'sale_value':[4, 5, 2, 4]})
df
>>>
store_name sale_value
0 a 4
1 b 5
2 a 2
3 c 4
Now it is possible to creat a bar chart with your approach.
First we have to do some imports and preprocessing:
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, VBar, Title
source = ColumnDataSource(df.groupby('store_name')['sale_value'].sum().to_frame().reset_index())
my_ticks = [i for i in range(len(source.data['store_name']))]
my_tick_labels = {i: source.data['store_name'][i] for i in range(len(source.data['store_name']))}
There are some changes in the section of the groupby. A .sum() is added and it is reset to a DataFrame with ascending index.
Then you can create a plot.
plot = Plot(title=Title(text='Plot'),
plot_width=300,
plot_height=300,
min_border=0,
toolbar_location=None
)
glyph = VBar(x='index',
top='sale_value',
bottom=0,
width=0.5,
fill_color="#b3de69"
)
plot.add_glyph(source, glyph)
xaxis = LinearAxis(ticker = my_ticks,
major_label_overrides= my_tick_labels
)
plot.add_layout(xaxis, 'below')
yaxis = LinearAxis()
plot.add_layout(yaxis, 'left')
plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker))
plot.add_layout(Grid(dimension=1, ticker=yaxis.ticker))
show(plot)
I also want to show your a second approach I prefere more.
from bokeh.plotting import figure, show
plot = figure(title='Plot',
plot_width=300,
plot_height=300,
min_border=0,
toolbar_location=None
)
plot.vbar(x='index',
top='sale_value',
source=source,
bottom=0,
width=0.5,
fill_color="#b3de69"
)
plot.xaxis.ticker = my_ticks
plot.xaxis.major_label_overrides = my_tick_labels
show(plot)
I like the second one more, because it is a bit shorter.
The created figure is in both cases the same. It looks like this.

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 to add data labels to a bar chart in Bokeh?

In the Bokeh guide there are examples of various bar charts that can be created. http://docs.bokeh.org/en/0.10.0/docs/user_guide/charts.html#id4
This code will create one:
from bokeh.charts import Bar, output_file, show
from bokeh.sampledata.autompg import autompg as df
p = Bar(df, 'cyl', values='mpg', title="Total MPG by CYL")
output_file("bar.html")
show(p)
My question is if it's possible to add data labels to each individual bar of the chart? I searched online but could not find a clear answer.
Use Labelset
Use Labelset to create a label over each individual bar
In my example I'm using vbar with the plotting interface, it is a little bit more low level then the Charts interface, but there might be a way to add it into the Bar chart.
from bokeh.palettes import PuBu
from bokeh.io import show, output_notebook
from bokeh.models import ColumnDataSource, ranges, LabelSet
from bokeh.plotting import figure
output_notebook()
source = ColumnDataSource(dict(x=['Áætlaðir','Unnir'],y=[576,608]))
x_label = ""
y_label = "Tímar (klst)"
title = "Tímar; núllti til þriðji sprettur."
plot = figure(plot_width=600, plot_height=300, tools="save",
x_axis_label = x_label,
y_axis_label = y_label,
title=title,
x_minor_ticks=2,
x_range = source.data["x"],
y_range= ranges.Range1d(start=0,end=700))
labels = LabelSet(x='x', y='y', text='y', level='glyph',
x_offset=-13.5, y_offset=0, source=source, render_mode='canvas')
plot.vbar(source=source,x='x',top='y',bottom=0,width=0.3,color=PuBu[7][2])
plot.add_layout(labels)
show(plot)
You can find more about labelset here: Bokeh annotations
NOTE FROM BOKEH MAINTAINERS The portions of the answer below that refer to the bokeh.charts are of historical interest only. The bokeh.charts API was deprecated and subsequently removed from Bokeh. See the answers here and above for information on the stable bokeh.plotting API
Yes, you can add labels to each bar of the chart. There are a few ways to do this. By default, your labels are tied to your data. But you can change what is displayed. Here are a few ways to do that using your example:
from bokeh.charts import Bar, output_file, show
from bokeh.sampledata.autompg import autompg as df
from bokeh.layouts import gridplot
from pandas import DataFrame
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Range1d, HoverTool
# output_file("bar.html")
""" Adding some sample labels a few different ways.
Play with the sample data and code to get an idea what does what.
See below for output.
"""
Sample data (new labels):
I used some logic to determine the new dataframe column. Of course you could use another column already in df (it all depends on what data you're working). All you really need here is to supply a new column to the dataframe.
# One method
labels = []
for number in df['cyl']:
if number == 3:
labels.append("three")
if number == 4:
labels.append("four")
if number == 5:
labels.append("five")
if number == 6:
labels.append("six")
if number == 8:
labels.append("eight")
df['labels'] = labels
Another way to get a new dataframe column. Again, we just need to supply df a new column to use on our bar plot.
# Another method
def new_labels(x):
if x % 2 != 0 or x == 6:
y = "Inline"
elif x % 2 == 0:
y = "V"
else:
y = "nan"
return y
df["more_labels"] = df["cyl"].map(new_labels)
Now the bar chart:
I've done it two ways. p1 just specifies the new labels. Note that because I used strings it put them in alphabetical order on the chart. p2 uses the original labels, plus adds my new labels on the same bar.
# Specifying your labels
p1 = Bar(df, label='labels', values='mpg',
title="Total MPG by CYL, remapped labels, p1",
width=400, height=400, legend="top_right")
p2 = Bar(df, label=['cyl', 'more_labels'], values='mpg',
title="Total MPG by CYL, multiple labels, p2", width=400, height=400,
legend="top_right")
Another way:
Bokeh has three main "interface levels". High level charts provides quick easy access but limited functionality; plotting which gives more options; models gives even more options.
Here I'm using the plotting interface and the Figure class that contains a rect method. This gives you more detailed control of your chart.
# Plot with "intermediate-level" bokeh.plotting interface
new_df = DataFrame(df.groupby(['cyl'])['mpg'].sum())
factors = ["three", "four", "five", "six", "eight"]
ordinate = new_df['mpg'].tolist()
mpg = [x * 0.5 for x in ordinate]
p3 = figure(x_range=factors, width=400, height=400,
title="Total MPG by CYL, using 'rect' instead of 'bar', p3")
p3.rect(factors, y=mpg, width=0.75, height=ordinate)
p3.y_range = Range1d(0, 6000)
p3.xaxis.axis_label = "x axis name"
p3.yaxis.axis_label = "Sum(Mpg)"
A fourth way to add specific labels:
Here I'm using the hover plot tool. Hover over each bar to display your specified label.
# With HoverTool, using 'quad' instead of 'rect'
top = [int(x) for x in ordinate]
bottom = [0] * len(top)
left = []
[left.append(x-0.2) for x in range(1, len(top)+1)]
right = []
[right.append(x+0.2) for x in range(1, len(top)+1)]
cyl = ["three", "four", "five", "six", "eight"]
source = ColumnDataSource(
data=dict(
top=[int(x) for x in ordinate],
bottom=[0] * len(top),
left=left,
right=right,
cyl=["three", "four", "five", "six", "eight"],
)
)
hover = HoverTool(
tooltips=[
("cyl", "#cyl"),
("sum", "#top")
]
)
p4 = figure(width=400, height=400,
title="Total MPG by CYL, with HoverTool and 'quad', p4")
p4.add_tools(hover)
p4.quad(top=[int(x) for x in ordinate], bottom=[0] * len(top),
left=left, right=right, color="green", source=source)
p4.xaxis.axis_label = "x axis name"
Show all four charts in a grid:
grid = gridplot([[p1, p2], [p3, p4]])
show(grid)
These are the ways I am aware of. There may be others. Change whatever you like to fit your needs. Here is what running all of this will output (you'll have to run it or serve it to get the hovertool):

Categories

Resources