Adding labels to bokeh pie chart wedge - python-3.x

I am new to bokeh, and want to render a pie chart using bokeh figure.
I used the reference from https://docs.bokeh.org/en/latest/docs/gallery/pie_chart.html in order to create my pie chart figure.
Now, I need to add on each part of the pie chart a label which represent the percentage of this part, and the label position should be align to the center.
I could not find a simple way to do it via the documentation, and try to find ways to do it manually, like this example: Adding labels in pie chart wedge in bokeh
I tried to create a label set and add the layout to the plot but i could not figure out if there is a way to control the label position, size, and font. text_align (right, left, center) does not do the job for me.
Here is my code - this function create and return an html of the pie chart
The chart argument contains the relevant data for the chart. in this case its a tuple (size 1), and series[0] contains the name of the series (series.title), list of x values (series.x), and list of y values (series.y)
def render_piechart(self, chart):
"""
Renders PieChart object using Bokeh
:param chart: Pie chart
:return:
"""
series = chart.series[0]
data_dict = dict(zip(series.x, series.y))
data = pd.Series(data_dict).reset_index(name='value').rename(columns={'index': 'Category'})
data['angle'] = data['value'] / data['value'].sum() * 2 * pi
data['color'] = palette[:len(series.x)]
data['percentage'] = data['value'] / data['value'].sum() * 100
data['percentage'] = data['percentage'].apply(lambda x: str(round(x, 2)) + '%')
TOOLTIPS = [('Category', '#Category'), ('Value', '#value'), ('Percentage', '#percentage')]
fig = figure(title=series.title,
plot_width=400 if chart.sizehint == 'medium' else 600,
plot_height=350 if chart.sizehint == 'medium' else 450,
tools='hover', tooltips=TOOLTIPS, x_range=(-0.5, 1.0))
fig.wedge(x=0, y=1, radius=0.45, start_angle=cumsum('angle', include_zero=True),
end_angle=cumsum('angle'), line_color='white', fill_color='color',
legend='Category', source=data)
fig.title.text_font_size = '20pt'
source = ColumnDataSource(data)
labels = LabelSet(x=0, y=1, text='percentage', level='glyph', angle=cumsum('angle', include_zero=True),
source=source, render_mode='canvas')
fig.add_layout(labels)
fig.axis.axis_label = None
fig.axis.visible = False
fig.grid.grid_line_color = None
return bokeh.embed.file_html(fig, bokeh.resources.CDN)
And this is the results:
pie chart consist of 3 parts
pie chart consist of 10 parts
in the 2 examples - the series title is 'kuku'
x and y values for the first example:
x=["A", "B", "C"]
y=[10, 20, 30]
and for the second example:
x=["A", "B", "C", "D", "E", "F", "G", "H", "I"]
y=[10, 20, 30, 100, 90, 80, 70, 60, 30 , 40 ,50]
I know that in the past i could do it easily with Donut but it is deprecated.
I want to be able to get something like this one:
example1
or this: example2

The problem, as you understand, is here:
labels = LabelSet(x=0, y=1, text='percentage', level='glyph', angle=cumsum('angle', include_zero=True), source=source, render_mode='canvas')
It's a bit confusing to create labels in Bokeh, but still:
you should add columns like 'text_pos_x' and 'text_pos_y' for every row you draw and fill it in with coordinates where you would like to place the text. And then apply it in LabelSet function, giving x='text_pos_x' and y='text_pos_y' so that every single part of plot have its own coordinates where to place a label:
labels = LabelSet(x='text_pos_x', y='text_pos_y', text='percentage', level='glyph', angle=0, source=source, render_mode='canvas')
and yes, it's necessary to set angle = 0 to avoid text being rotated.

To complete #Higem 's answer I would suggest you some formula to centre your labels correctly on your pie chart. I modified your code as follows:
def render_piechart(self, chart):
"""
Renders PieChart object using Bokeh
:param chart: Pie chart
:return:
"""
radius = 0.45 # Radius of your pie chart
series = chart.series[0]
data_dict = dict(zip(series.x, series.y))
data = pd.Series(data_dict).reset_index(name='value').rename(columns={'index': 'Category'})
data['angle'] = data['value'] / data['value'].sum() * 2 * pi
data['color'] = palette[:len(series.x)]
data['percentage'] = data['value'] / data['value'].sum() * 100
data['percentage'] = data['percentage'].apply(lambda x: str(round(x, 2)) + '%')
# Projection on X and Y axis for label positioning
data['label_x_pos'] = np.cos(data['angle'].cumsum()-data['angle'].div(2))*3*radius/4
data['label_y_pos'] = np.sin(data['angle'].cumsum()-data['angle'].div(2))*3*radius/4
TOOLTIPS = [('Category', '#Category'), ('Value', '#value'), ('Percentage', '#percentage')]
fig = figure(title=series.title,
plot_width=400 if chart.sizehint == 'medium' else 600,
plot_height=350 if chart.sizehint == 'medium' else 450,
tools='hover', tooltips=TOOLTIPS, x_range=(-0.5, 1.0))
fig.wedge(x=0, y=0, radius=radius, start_angle=cumsum('angle', include_zero=True),
end_angle=cumsum('angle'), line_color='white', fill_color='color',
legend='Category', source=data) # Change center of the pie chart to (0, 0)
fig.title.text_font_size = '20pt'
source = ColumnDataSource(data)
labels = LabelSet(x='label_x_pos', y='label_y_pos', text='percentage', level='glyph', text_align='center', source=source, render_mode='canvas')
fig.add_layout(labels)
fig.axis.axis_label = None
fig.axis.visible = False
fig.grid.grid_line_color = None
return bokeh.embed.file_html(fig, bokeh.resources.CDN)
The result is the following:
I used the basic formula to convert polar coordinates to cartesian coordinates, see Wikipedia.

Related

Remove margins around subplots in Plotly

I have a plot made up of 3 choropleth subplots next to each other. I set the overall height and width to my desired dimensions (800 x 400 pixels). I want each subplot to go from top to bottom, but as it stands, the subplots retain the aspect ratio of 2:1, meaning I have wide margins at top and bottom. Those I want to remove.
As a minimum example, I am attaching the data and plot code:
The toy dataset:
import geopandas as gpd
from shapely.geometry.polygon import Polygon
minidf = gpd.GeoDataFrame(dict(
krs_code = ["08111", "08118"],
m_rugged = [42.795776, 37.324421],
bip = [83747, 43122],
cm3_over_1999 = [47.454688, 47.545940],
geometry = [Polygon(((9.0397, 48.6873),
(9.0397, 48.8557),
(9.3152, 48.8557),
(9.3152, 48.6873),
(9.0397, 48.6873))),
Polygon(((8.8757, 48.7536),
(8.8757, 49.0643),
(9.4167, 49.0643),
(9.4167, 48.7536),
(8.8757, 48.7536)))]
)).set_index("krs_code")
The plotting code:
import json
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(rows = 1, cols = 3,
specs = [[{"type": "choropleth"}, {"type": "choropleth"}, {"type": "choropleth"}]],
horizontal_spacing = 0.0025 )
fig.update_layout(height = 400, width = 800,
margin = dict(t=0, r=0, b=0, l=0),
coloraxis_showscale=False )
for i, column in enumerate(["m_rugged", "cm3_over_1999", "bip"]):
fig.add_trace(
go.Choropleth(
locations = minidf.index,
z = minidf[column].astype(float), # Data to be color-coded
geojson = json.loads(minidf[["geometry"]].to_json()),
showscale = False
),
col = i+1, row = 1)
fig.update_geos(fitbounds="locations", visible=True)
fig.show()
Notice the margins at top and bottom, which retain the aspect ratio of each subplot, while they are supposed to stretch from top to bottom:
I tried several parameters within go.Choropleth() and .update_layout(), but to no avail.

Make one y-axis label bold in matplotlib

Goodmorning,
Question, I've got this script that creates a horizontal bar chart (see image)
I would like to have one label in the y-axis bold "Nederland".
I've searched an tried a lot, but I really have no idea how I can do this.
I found this solution:
Matplotlib - Changing the color of a single x-axis tick label
But I could not get it to work.
Any hint to a solution would be great.
def AVG_BarChart(self, data:dict=None, graph_file:str = None, datum:str=None, countries:dict=None, watermarktext:str="energieprijzenbot.nl", prijsper:str="kWh")->bool:
plt.figure(figsize=(9, 6))
plt.xlabel(f"Prijs per {prijsper}")
plt.title(f"Gemiddelde {prijsper} inkoopprijs per land {datum}")
colors = ["#FE8000", "#EFBD76", "#FFA52B", "#FF9D3C", "#FFF858", "#FCFFCB", "#07EEB2", "#FF4179","#E05B4B", "#E09336", "#DAB552", "#DBD9A6", "#87B49C", "#4B8A7E", "#A5DD96", "#E1F3C9", "#0095AD", "#00D5E5", "#82E9F0", "#C0ED42", "#FFE301", "#FFF352", "#FF85DA", "#FF69B3","#A15AC4", "#3F7539", "#B8CBAD", "#E1E2C2", "#F84040", "#9D1E29"]
random.shuffle(colors)
values = 2 ** np.random.randint(2, 10, len(data))
max_value = values.max()
labels = list(data.keys())
values = list(data.values())
height = 0.9
plt.barh(y=labels, width=values, height=height, color=colors, align='center', alpha=0.8)
ax = plt.gca()
ax.xaxis.set_major_formatter('€ {x:n}')
plt.bar_label(ax.containers[0], labels=[f'€ {x:n}' for x in ax.containers[0].datavalues], label_type="edge", padding=-50)
ax.text(0.5, 0.5, watermarktext, transform=ax.transAxes,
fontsize=40, color='gray', alpha=0.3,
ha='center', va='center', rotation='30')
for i, (label, value) in enumerate(zip(labels, values)):
country_iso = self.get_key(val=label, my_dict=countries).lower()
self.offset_image(x=value, y=i, flag=country_iso, bar_is_too_short=value < max_value / 10, ax=ax)
plt.subplots_adjust(left=0.15)
plt.savefig(graph_file, bbox_inches='tight', width = 0.4)
return True
I tried looping thru the labels like this
i = 0
for w in ax.get_yticklabels():
country = ax.get_yticklabels()[i].get_text()
if country == "Nederland":
ax.get_yticklabels()[i].set_color('red')
ax.get_yticklabels()[i].set_fontweight('bold')
i += 1
When debugging I actually get a country name back, but when running the script normal, all country labels are empty...
So, I was close to the answer. But somehow I got back empty .get_text() string.
# ... some code
labels = list(data.keys())
# ... more code
ax.set_yticklabels(labels)
for lab in ax.get_yticklabels():
if lab.get_text() == "Nederland":
lab.set_fontweight('bold')
I just hope by setting the labels again, It does not mix up anything :-)

Bokeh colorbar, assign a tick to each color

I'm trying to plot an heatmap of a matrix containing some counts (called mat in my code, then df after change the structure to use it with Bokeh). The structure is like this:
X
element 1
element 2
element 3
category 1
0
6
4
category 2
1
7
3
category 3
5
2
10
category 4
0
1
4
Now with my code I'm using df.value.unique() both for the color mapper and the ticks, but in the heatmap the colorbar's ticks doesn't correspond to the colors:
How can I make the ticks coincide each one to one color? I'm quite sure I have to use the CategoricalColorMapper but with that I get only a white screen. Thank you for the help.
Here's my code:
mat = pd.read_csv("tests/count_50.dat", sep="\t", index_col=0)
mat.index.name = 'MGI_id'
mat.columns.name = 'phen_sys'
#set data as float numbers
mat=mat.astype(float)
#Create a custom palette and add a specific mapper to map color with values
df = mat.stack(dropna=False).rename("value").reset_index()
pal=bokeh.palettes.brewer['YlGnBu'][len(df.value.unique())]
mapper = LinearColorMapper(palette=pal, low=df.value.min(), high=df.value.max(), nan_color = 'gray')
#Define a figure
p = figure(
plot_width=1280,
plot_height=800,
title="Heatmap",
x_range=list(df.MGI_id.drop_duplicates()),
y_range=list(df.phen_sys.drop_duplicates()[::-1]),
tooltips=[('Phenotype system','#phen_sys'),('Gene','#MGI_id'),('Phenotypes','#value')],
x_axis_location="above",
output_backend="webgl")
#Create rectangles for heatmap
p.rect(
x="MGI_id",
y="phen_sys",
width=1,
height=1,
source=ColumnDataSource(df),
fill_color=transform('value', mapper))
p.xaxis.major_label_orientation = 45
#Add legend
t = df.value.unique()
t.sort()
color_bar = ColorBar(
color_mapper=mapper,
ticker=FixedTicker(ticks=t, desired_num_ticks=len(df.value.unique())),
label_standoff=6,
border_line_color=None)
p.add_layout(color_bar, 'right')
show(p)
I found a solution:
I create a factor list by ordering the values and then converting both the dataframe values and the factors. At that point I created a CategoricalColorMapper instead of the linear one and the plot now is correct:
Your list of values goes from 0 to 10, so ColorBar will go up to 10. You can change mapper 'high' value to '9':
mapper = LinearColorMapper(palette=colors, low=0, high=9, nan_color = 'gray')
Or a ColorBar that goes from 1 to 10:
mapper = LinearColorMapper(palette=colors, low=1, high=10, nan_color = 'gray')

Selecting data for a specific coordinate, openpyxl

Firstly - piece of my data:
I need to plot a line chart with years on the X axis and people's height on the Y axis:
But I'm getting it:
For plotting chart I using code below
from openpyxl.chart import LineChart, Reference
chart = LineChart()
chart.title='Average height'
chart.y_axis.title = 'Height'
chart.x_axis.title = 'Year'
values = Reference(working_sheet, min_col = 7, min_row = 1, max_col=9, max_row = 102)
chart.add_data(values, titles_from_data=True)
working_sheet.add_chart(chart, 'K2')
So I need to know how I can select data for a specific coordinate using openpyxl

matplotlib set stacked bar chart labels

I am trying to create a stacked bar chart that groups data by operating system. I'm having trouble creating an individual label for each component in each bar.
What I'm trying to do is different from the example in the docs because in my data each category appears in only one bar, whereas in the example each bar contains one member of each category.
Currently I have this code
plt.cla()
plt.clf()
plt.close()
def get_cmap(n, name='hsv'):
'''Returns a function that maps each index in 0, 1, ..., n-1 to a distinct
RGB color; the keyword argument name must be a standard mpl colormap name.'''
return plt.cm.get_cmap(name, n)
fig = plt.figure(figsize=(18, 10), dpi=80)
# group by the prefixes for now
prefixes = []
indices = []
bars = []
legend = {}
cmap = get_cmap(len(os_counts.index) + 1)
k = 0
for i, prefix in enumerate(d):
indices.append(i)
if len(d[prefix]["names"]) == 1:
prefixes.append(d[prefix]["names"][0])
else:
prefixes.append(prefix)
#colors = [next(cycol) for j in range(len(d[prefix]["names"]))]
colors = [cmap(k + j) for j in range(len(d[prefix]["names"]))]
k += len(colors)
bar = plt.bar([i] * len(d[prefix]["names"]), d[prefix]["values"], color=colors, label=d[prefix]["names"])
bars.append(bar)
plt.xticks(rotation=90)
plt.ylabel("Frequency")
plt.xlabel("Operating System")
plt.xticks(indices, prefixes)
plt.legend()
plt.show()
Which produces this result. As you can see, the legend is created for the first colour within the bar and shows an array.
I think that each call to plt.bar gets one label. So, you are giving it a list as a label for each plt.bar call. If you want a label for every color, representing every operating system then I think the solution is to call plt.bar once for each color or os.

Resources