import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool
def rounded_annular_wedge_patch(center, inner_radius, outer_radius, start_angle, end_angle,
corner_radius=0.05, n_points=80, gap_width=0):
cx, cy = center
if gap_width > 0:
inner_gap_angle = gap_width / inner_radius / 2.5
outer_gap_angle = gap_width / outer_radius / 2
start_angle_inner = start_angle + inner_gap_angle
end_angle_inner = end_angle - inner_gap_angle
start_angle_outer = start_angle + outer_gap_angle
end_angle_outer = end_angle - outer_gap_angle
else:
start_angle_inner = start_angle_outer = start_angle
end_angle_inner = end_angle_outer = end_angle
corner_points = 15
angular_corner_offset_inner = corner_radius / inner_radius
angular_corner_offset_outer = corner_radius / outer_radius
outer_start_adj = start_angle_outer + angular_corner_offset_outer
outer_end_adj = end_angle_outer - angular_corner_offset_outer
if outer_end_adj > outer_start_adj:
outer_angles = np.linspace(outer_start_adj, outer_end_adj, n_points)
x_outer = cx + outer_radius * np.cos(outer_angles)
y_outer = cy + outer_radius * np.sin(outer_angles)
else:
x_outer = np.array([])
y_outer = np.array([])
inner_start_adj = end_angle_inner - angular_corner_offset_inner
inner_end_adj = start_angle_inner + angular_corner_offset_inner
if inner_start_adj > inner_end_adj:
inner_angles = np.linspace(inner_start_adj, inner_end_adj, n_points)
x_inner = cx + inner_radius * np.cos(inner_angles)
y_inner = cy + inner_radius * np.sin(inner_angles)
else:
x_inner = np.array([])
y_inner = np.array([])
# Corners
corner1_center_x = cx + (outer_radius - corner_radius) * np.cos(start_angle_outer)
corner1_center_y = cy + (outer_radius - corner_radius) * np.sin(start_angle_outer)
c1_start = start_angle_outer - np.pi/2
c1_end = start_angle_outer
c1_angles = np.linspace(c1_start, c1_end, corner_points)
x_c1 = corner1_center_x + corner_radius * np.cos(c1_angles)
y_c1 = corner1_center_y + corner_radius * np.sin(c1_angles)
corner2_center_x = cx + (outer_radius - corner_radius) * np.cos(end_angle_outer)
corner2_center_y = cy + (outer_radius - corner_radius) * np.sin(end_angle_outer)
c2_start = end_angle_outer
c2_end = end_angle_outer + np.pi/2
c2_angles = np.linspace(c2_start, c2_end, corner_points)
x_c2 = corner2_center_x + corner_radius * np.cos(c2_angles)
y_c2 = corner2_center_y + corner_radius * np.sin(c2_angles)
corner3_center_x = cx + (inner_radius + corner_radius) * np.cos(end_angle_inner)
corner3_center_y = cy + (inner_radius + corner_radius) * np.sin(end_angle_inner)
c3_start = end_angle_inner + np.pi/2
c3_end = end_angle_inner + np.pi
c3_angles = np.linspace(c3_start, c3_end, corner_points)
x_c3 = corner3_center_x + corner_radius * np.cos(c3_angles)
y_c3 = corner3_center_y + corner_radius * np.sin(c3_angles)
corner4_center_x = cx + (inner_radius + corner_radius) * np.cos(start_angle_inner)
corner4_center_y = cy + (inner_radius + corner_radius) * np.sin(start_angle_inner)
c4_start = start_angle_inner + np.pi
c4_end = start_angle_inner + 3*np.pi/2
c4_angles = np.linspace(c4_start, c4_end, corner_points)
x_c4 = corner4_center_x + corner_radius * np.cos(c4_angles)
y_c4 = corner4_center_y + corner_radius * np.sin(c4_angles)
x_patch = np.concatenate([
x_c1, x_outer, x_c2, x_c3, x_inner, x_c4
])
y_patch = np.concatenate([
y_c1, y_outer, y_c2, y_c3, y_inner, y_c4
])
return x_patch, y_patch
def plot_rounded_annular_wedges(
data, labels=None, colors=None, center=(0,0),
inner_radius=0.5, outer_radius=1.0, corner_radius=0.08, gap_width=0.19, n_points=80,
title="Rounded Doughnut Chart"
):
total = sum(data)
N = len(data)
if not colors:
colors = ["gold", "lime", "dodgerblue", "purple", "orange", "cyan", "magenta"]
colors = (colors * ((N + len(colors) - 1) // len(colors)))[:N]
if not labels:
labels = [f"Piece {i+1}" for i in range(N)]
angles = [2*np.pi*v/total for v in data]
start_angle = np.deg2rad(30)
starts = [start_angle]
for a in angles[:-1]:
starts.append(starts[-1] + a)
ends = [s + a for s, a in zip(starts, angles)]
percents = [f"{int(round(100 * v / total))}%" for v in data]
xs, ys = [], []
for s, e in zip(starts, ends):
x, y = rounded_annular_wedge_patch(
center, inner_radius, outer_radius, s, e, corner_radius, n_points, gap_width=gap_width
)
xs.append(x.tolist())
ys.append(y.tolist())
source = ColumnDataSource(data=dict(
xs=xs, ys=ys, label=labels, percent=percents, color=colors
))
p = figure(width=550, height=420, x_range=(-1.3, 2.3), y_range=(-1.3, 1.3),
match_aspect=True, title=title)
patches_renderer = p.patches('xs', 'ys', source=source,
fill_color='color', fill_alpha=0.7,
line_color="white", line_width=2,
hover_line_color='black', hover_line_width=3)
hover = HoverTool(
tooltips=[("Label", "@label"), ("Percent", "@percent")],
renderers=[patches_renderer]
)
p.add_tools(hover)
# Percentage text labels
label_coords_x = []
label_coords_y = []
for s, e in zip(starts, ends):
mid_angle = (s + e) / 2
r_label = (inner_radius + outer_radius) / 2
lx = center[0] + r_label * np.cos(mid_angle)
ly = center[1] + r_label * np.sin(mid_angle)
label_coords_x.append(lx)
label_coords_y.append(ly)
p.text(
x=label_coords_x,
y=label_coords_y,
text=percents,
text_align="center",
text_baseline="middle",
text_font_size="14pt",
text_color="black",
text_font_style="bold"
)
# Custom legend (top right)
legend_x = 1.22
legend_y = 0.8
legend_spacing = 0.16
for i, (c, lbl) in enumerate(zip(colors, labels)):
y_pos = legend_y - i * legend_spacing
p.scatter([legend_x], [y_pos], size=18, color=c, alpha=0.7)
p.text([legend_x + 0.09], [y_pos], text=[lbl], text_align="left", text_baseline="middle", text_font_size="13pt")
p.grid.visible = False
p.axis.visible = False
p.title.text_font_size = "17pt"
p.title.align = "center"
p.background_fill_color="#fff9e5"
show(p)
# ----- Example Usage -----
data = [10, 15, 5, 12, 18]
labels = ["Apples", "Pears", "Bananas", "Plums", "Tomatoes"]
colors = ["gold", "lime", "dodgerblue", "purple", "tomato"]
plot_rounded_annular_wedges(
data, labels=labels, colors=colors,
inner_radius=0.5, outer_radius=1.0,
corner_radius=0.08, gap_width=0.19
)
data3 = [7, 13, 15, 5, 3, 9]
labels3 = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Weekend"]
colors3 = ["#f67280", "#ffafcc", "#a3cef1", "#b5ead7", "#f6ffe0", "#d2f6c5"]
plot_rounded_annular_wedges(
data3, labels=labels3, colors=colors3,
inner_radius=0.5, outer_radius=1,
corner_radius=0.08, gap_width=0.19,
title="Weekly Activity",
)
data2 = [25, 15, 35, 25]
labels2 = ["HR", "R&D", "Marketing", "Sales"]
colors2 = ["#5e60ce", "#00b4d8", "#ffd166", "#ff006e"]
plot_rounded_annular_wedges(
data2, labels=labels2, colors=colors2,
inner_radius=0.5, outer_radius=1.0,
corner_radius=0.08, gap_width=0.19,
title="Department Budgets",
)