How do I create a pie chart using Bokeh? - python

All I'd like to do is create a pie chart. The Bokeh documentation covers a number of sophisticated charts, including a donut chart, but it doesn't seem to cover pie chart.
Is there any example of this?
Ultimately, the chart will need to be to be embedded in a webpage, so I'll need to take advantage of Bokeh's html embed capabilities.

The answer below is very outdated. The Donut function was part of the old bokeh.charts API that was deprecated and removed long ago. For any modern version of Bokeh (e.g. 0.13 or newer) you can create a pie chart using the wedge glyphs, as follows:
from math import pi
import pandas as pd
from bokeh.io import output_file, show
from bokeh.palettes import Category20c
from bokeh.plotting import figure
from bokeh.transform import cumsum
x = { 'United States': 157, 'United Kingdom': 93, 'Japan': 89, 'China': 63,
'Germany': 44, 'India': 42, 'Italy': 40, 'Australia': 35,
'Brazil': 32, 'France': 31, 'Taiwan': 31, 'Spain': 29 }
data = pd.Series(x).reset_index(name='value').rename(columns={'index':'country'})
data['angle'] = data['value']/data['value'].sum() * 2*pi
data['color'] = Category20c[len(x)]
p = figure(plot_height=350, title="Pie Chart", toolbar_location=None,
tools="hover", tooltips="#country: #value")
p.wedge(x=0, y=1, radius=0.4,
start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
line_color="white", fill_color='color', legend='country', source=data)
show(p)
OUTDATED BELOW
An example for Bokeh 0.8.1 using the bokeh.plotting interface:
from bokeh.plotting import *
from numpy import pi
# define starts/ends for wedges from percentages of a circle
percents = [0, 0.3, 0.4, 0.6, 0.9, 1]
starts = [p*2*pi for p in percents[:-1]]
ends = [p*2*pi for p in percents[1:]]
# a color for each pie piece
colors = ["red", "green", "blue", "orange", "yellow"]
p = figure(x_range=(-1,1), y_range=(-1,1))
p.wedge(x=0, y=0, radius=1, start_angle=starts, end_angle=ends, color=colors)
# display/save everythin
output_file("pie.html")
show(p)
Bokeh >0.9 will correctly compute the bounding area of all glyphs, not just "pointlike" marker glyphs, and explicitly setting the ranges like this will not be required.

NOTE from project maintainers: This answer refers to an old bokeh.charts API that was remove from bokeh a long time ago
A Donut chart will return a simple pie chart if you input a pandas series rather than a dataframe. And it will display labels too!
from bokeh.charts import Donut, show
import pandas as pd
data = pd.Series([0.15,0.4,0.7,1.0], index = list('abcd'))
pie_chart = Donut(data)
show(pie_chart)

Thanks to the answers above for helping me as well. I want to add how to add a legend to your pie-chart as I had some trouble with that. Below is just a snippet. My piechart just had 2 sections. Thus, I just made a pie chart figure and called wedge on it twice:
import numpy as np
percentAchieved = .6
pieFigure = figure(x_range=(-1, 1), y_range=(-1, 1))
starts = [np.pi / 2, np.pi * 2 * percentAchieved + np.pi / 2]
ends = [np.pi / 2+ np.pi * 2 * percentAchieved, np.pi / 2 + 2*np.pi]
pieColors = ['blue', 'red']
#therefore, this first wedge will add a legend entry for the first color 'blue' and label it 'hello'
pieFigure.wedge(x=0, y=0, radius=.7, start_angle=starts, end_angle=ends, color=pieColors, legend="hello")
#this will add a legend entry for the 'red' color and label it 'bye'. Made radius zero to not make
#another piechart overlapping the original one
pieFigure.wedge(x=0, y=0, radius=0, start_angle=starts, end_angle=ends, color=pieColors[1], legend="bye")

Related

Label to holoviews HLine

I have a bar plot with two static HLine, and would like to add a label to them (or a legend) so that they are defined on the plot. I tried something like:
eq = (
sr2.hvplot(
kind="bar",
groupby ="rl",
dynamic = False,)
* hv.HLine(0.35, name="IA1").opts(color='red')
* hv.HLine(0.2, label="IA2").opts(color='green')
)
but no label comes with the chart.
This answer to a similar question explains that it is not easy, and you need a workaround:
How do I get a full-height vertical line with a legend label in holoviews + bokeh?
You can also use parts of this solution:
https://discourse.holoviz.org/t/horizontal-spikes/117
Maybe the easiest is just to not use hv.HLine() when you would like a legend with your horizontal line, but create a manual line yourself with hv.Curve() instead:
# import libraries
import pandas as pd
import seaborn as sns
import holoviews as hv
import hvplot.pandas
hv.extension('bokeh')
# create sample dataset
df = sns.load_dataset('anscombe')
# create some horizontal lines manually defining start and end point
manual_horizontal_line = hv.Curve([[0, 10], [15, 10]], label='my_own_line')
another_horizontal_line = hv.Curve([[0, 5], [15, 5]], label='another_line')
# create scatterplot
scatter_plot = df.hvplot.scatter(x='x', y='y', groupby='dataset', dynamic=False)
# overlay manual horizontal lines on scatterplot
scatter_plot * manual_horizontal_line * another_horizontal_line
Resulting plot:
If you want text labels instead of a legend, you can use hv.Text:
import holoviews as hv
hv.extension('bokeh')
hv.Scatter(([4,6,8,10],[0.1,0.5,0.01,0.7])) * \
hv.HLine(0.35).opts(color='red') * hv.Text(9, 0.38, "IA1").opts(color='red') * \
hv.HLine(0.20).opts(color='green') * hv.Text(9, 0.23, "IA2").opts(color='green')

Adding labels in pie/donut chart using annual_wedge in bokeh 2.x

I have a problem applying correctly aligned labelsets to a donut style chart created with annual_wedge glyphs in bokeh.
I have trouble aligning labels belonging to the categories of the "rings" of the donut chart "horizontaly", where horizontaly is the alignment of the label from the inner_radius of the annual_wedge to the outer_radius.
Currently labelsets doesn´t seem to have an inner_radius setting corresponding to an annual_wedge which makes applying and alignment of labelsets to annual wedges extremely difficult.
I was following the example in Adding labels in pie chart wedge in bokeh using string padding but this seems to be a very dirty hack to move the labels horizontally.
How can I apply labels to an annual_wedge in a aligned way corresponding to the inner_radius of the annual_wedge glyph?
Here is my code example:
# > gics_sector_data
gics_sector_data["gics_name"] = gics_sector_data["gics_name"].astype(str)
gics_sector_data["gics_name"] = gics_sector_data["gics_name"].str.pad(47, side = "left")
# Sector Ring
p.annular_wedge(x=9, y=9, inner_radius=0.8,outer_radius=2.5,
start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
line_color="white", fill_color='color', source=gics_sector_data)
sourceSector = ColumnDataSource(gics_sector_data)
labelsSector = LabelSet(x=9, y=9, text='gics_name',
angle=cumsum('angle', include_zero=True), source=sourceSector, render_mode='canvas',
text_font_size="8pt", text_align='left',background_fill_color='white')
p.add_layout(labelsSector)
In the image i set the background color of the labels to white so the alignmend and spacing of the lables can be better seen.
]
Just for your info: this is the data to be displayed Global Industry Classification Standard
You will have to compute the coordinates yourself.
Expanding on the linked example:
from math import pi
import numpy as np
import pandas as pd
from bokeh.io import show
from bokeh.models import LabelSet, ColumnDataSource
from bokeh.palettes import Category20c
from bokeh.plotting import figure
x = {'United States': 157,
'United Kingdom': 93,
'Japan': 89,
'China': 63,
'Germany': 44,
'India': 42,
'Italy': 40,
'Australia': 35,
'Brazil': 32,
'France': 31,
'Taiwan': 31,
'Spain': 29}
R = 0.4
data = (pd.Series(x)
.reset_index(name='value')
.rename(columns={'index': 'country'})
.assign(end_angle=lambda d: np.cumsum(d['value'] / d['value'].sum() * 2 * pi),
start_angle=lambda d: np.pad(d['end_angle'], (1, 0))[:-1],
color=Category20c[len(x)],
label_x=lambda d: R * 0.95 * np.cos(d['start_angle']),
label_y=lambda d: R * 0.95 * np.sin(d['start_angle'])))
source = ColumnDataSource(data)
p_range = (-R * 1.2, R * 1.2)
p_size = 500
p = figure(plot_height=p_size, plot_width=p_size, toolbar_location=None,
x_range=p_range, y_range=p_range,
tools="hover", tooltips="#country: #value")
for r in [p.xaxis, p.yaxis, p.grid]:
r.visible = False
p.wedge(x=0, y=0, radius=R,
start_angle='start_angle', end_angle='end_angle',
line_color="white", fill_color='color', legend_label='country', source=source)
p.add_layout(LabelSet(x='label_x', y='label_y', text='value', text_align='right',
angle='start_angle', source=source, render_mode='canvas'))
show(p)
Thanks to Eugene I was lead to the path of enlightment and was able to get the chart as I wanted.
I had to modify the calculation of the label positions by calculating the middle between the start angle and the end angle of a sector of the "donut" because I wanted to have the labels in the middle of each wedge.
I modified Eugenes example to this where "num" is just the value I use to calculate the relative width of each annual_wedge (Eugene uses 'value' in his example, I use num for occupied number of slots of each wedge of a total of similiar width slots on the 'donut', I hope this is clear enough):
R = 3
gics_sector_data= gics_sector_data.assign(
end_angle=lambda d: np.cumsum(d['num'] / d['num'].sum() * 2 * pi),
start_angle=lambda d: np.pad(d['end_angle'], (1, 0))[:-1],
label_x=lambda d: R* 1.00 * np.cos(((d['end_angle']-d['start_angle'])/2)+d['start_angle']),
label_y=lambda d: R* 1.00 * np.sin(((d['end_angle']-d['start_angle'])/2)+d['start_angle']),
label_angle=lambda d: (((d['end_angle']-d['start_angle'])/2)+d['start_angle']))
Something about the 1.00 multiplication, you can move the labels away from the outer edge of the wedge by f.e. using 0.95.
Something aside: I shortened the label names by adding a label_shortname column because bokeh can not line wrap labels. The shortname will be used for the labels, the long name for the tooltip:
gics_sector_data['gics_name_short'] = gics_sector_data['gics_name'].str.slice(stop=20)
and then draw the annual_wedge like with the start_angles and end_anglescalculated:
# Sector Ring
p.annular_wedge(x=0, y=0, inner_radius=gics_sector_radius-ringWidth, outer_radius=gics_sector_radius,
start_angle='start_angle', end_angle='end_angle',
line_color="white", fill_color='color', source=gics_sector_data)
and finally draw the LabelSet with the label_x, label_y and label_angle calculated which will draw the label in the middle of the wedge, text_baseline='middle' is mandatory to achive a nice result:
sourceSector = ColumnDataSource(gics_sector_data)
labelsSector = LabelSet(x='label_x', y='label_y', text='gics_name_short',
angle='label_angle', source=sourceSector, render_mode='canvas',
text_font_size="7pt", text_align='right', text_baseline='middle')
p.add_layout(labelsSector)
and here the 99.999% perfect result GICS result bokeh chart , I hope this will help to create similar charts

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).

Using semi-transparent palettes in bokeh

I would like to create a color palette in bokeh that has varying alpha values, so some colors are semi-transparent.
I tried to pass some RGBA value as hex (#33eedd777) or CSS colors (rgba(129, 23, 43, 90)) as a palette but bokeh raised a ValueError.
Here is a code example:
import numpy as np
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.transform import linear_cmap
from bokeh.util.hex import hexbin
n = 50000
x = np.random.standard_normal(n)
y = np.random.standard_normal(n)
bins = hexbin(x, y, 0.1)
p = figure(tools="wheel_zoom,reset", match_aspect=True, background_fill_color='#440154')
p.grid.visible = False
cm = ['#08459400', '#f7fbffff']
cm = ['rgba(80, 80, 80, 10)', 'rgba(8, 8, 8, 255)', ]
p.hex_tile(q="q", r="r", size=0.1, line_color=None, source=bins,
fill_color=linear_cmap('counts', cm, 0, max(bins.counts)))
show(p)
As of Bokeh 0.13.0, this appears to be a bug. The Seq(Color) property of LinearColorMap is supposed to accept lists of RGBA tuples, e.g. [(100, 80, 200, 0.2), ...] and it does accept them without a validation error, but does not generate correct output. That is the answer, there is no other information to add, or workarounds to give at this time (CSS color strings cannot be used, and hex color codes cannot have an alpha).
I would encourage you to file a bug report on GitHub with this information so that it may be fixed for all users:
https://github.com/bokeh/bokeh/issues

Bokeh Area Chart unable to plot

I am unable to plot the area chart in bokeh for some reason..
Below is the code used for the same..
from bokeh.charts import Area, show, output_file
Areadict = dict(
I = df['IEXT'],
Date=df['Month'],
O = df['OT']
)
area = Area(Areadict, x='Date', y=['I','O'], title="Area Chart",
legend="top_left",
xlabel='time', ylabel='memory')
output_file('area.html')
show(area)
All i see if the date axis getting plotted, but no signs of the two areacharts that I am interested in.
Please advise
I would recommend looking at Holoviews which is a very high level API built on top of Bokeh, and is endorsed by the Bokeh team. You can see an Area chart example in their documentation. Basically it looks like:
# create holoviews objects
dims = dict(kdims='time', vdims='memory')
python = hv.Area(python_array, label='python', **dims)
pypy = hv.Area(pypy_array, label='pypy', **dims)
jython = hv.Area(jython_array, label='jython', **dims)
# plot
overlay.relabel("Area Chart") + hv.Area.stack(overlay).relabel("Stacked Area Chart")
Which results in
Otherwise, as of Bokeh 0.13 to create a stacked area chart with the stable bokeh.plotting API, you will need to stack the data yourself, as shown in this example:
import numpy as np
import pandas as pd
from bokeh.plotting import figure, show, output_file
from bokeh.palettes import brewer
N = 20
cats = 10
df = pd.DataFrame(np.random.randint(10, 100, size=(N, cats))).add_prefix('y')
def stacked(df):
df_top = df.cumsum(axis=1)
df_bottom = df_top.shift(axis=1).fillna({'y0': 0})[::-1]
df_stack = pd.concat([df_bottom, df_top], ignore_index=True)
return df_stack
areas = stacked(df)
colors = brewer['Spectral'][areas.shape[1]]
x2 = np.hstack((df.index[::-1], df.index))
p = figure(x_range=(0, N-1), y_range=(0, 800))
p.grid.minor_grid_line_color = '#eeeeee'
p.patches([x2] * areas.shape[1], [areas[c].values for c in areas],
color=colors, alpha=0.8, line_color=None)
show(p)
which results in

Categories

Resources