|
1 | 1 | """ pyplots.ai |
2 | 2 | donut-basic: Basic Donut Chart |
3 | 3 | Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-14 |
| 4 | +Quality: 91/100 | Created: 2025-12-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | from math import pi |
|
13 | 13 | from bokeh.transform import cumsum |
14 | 14 |
|
15 | 15 |
|
16 | | -# Data |
| 16 | +# Data - Portfolio allocation by asset class |
17 | 17 | categories = ["Technology", "Healthcare", "Finance", "Energy", "Retail"] |
18 | 18 | values = [35, 25, 20, 12, 8] |
| 19 | +total = sum(values) |
19 | 20 |
|
20 | 21 | # Calculate angles for donut segments |
21 | 22 | data = { |
22 | 23 | "category": categories, |
23 | 24 | "value": values, |
24 | | - "angle": [v / sum(values) * 2 * pi for v in values], |
25 | | - "percentage": [f"{v / sum(values) * 100:.1f}%" for v in values], |
| 25 | + "angle": [v / total * 2 * pi for v in values], |
| 26 | + "percentage": [f"{v / total * 100:.1f}%" for v in values], |
26 | 27 | } |
27 | 28 |
|
28 | | -# Cumulative angles for start/end positions |
| 29 | +# Cumulative angles for label positioning |
29 | 30 | cumulative = np.cumsum([0] + data["angle"][:-1]).tolist() |
30 | 31 | data["start_angle"] = cumulative |
31 | 32 | data["end_angle"] = np.cumsum(data["angle"]).tolist() |
|
36 | 37 |
|
37 | 38 | source = ColumnDataSource(data=data) |
38 | 39 |
|
39 | | -# Create figure |
| 40 | +# Create figure (square format for circular chart) |
40 | 41 | p = figure( |
41 | | - width=4800, |
42 | | - height=2700, |
| 42 | + width=3600, |
| 43 | + height=3600, |
43 | 44 | title="donut-basic · bokeh · pyplots.ai", |
44 | 45 | toolbar_location=None, |
45 | 46 | tools="", |
46 | | - x_range=(-1.2, 1.8), |
47 | | - y_range=(-1.2, 1.2), |
| 47 | + x_range=(-1.4, 1.8), |
| 48 | + y_range=(-1.3, 1.3), |
48 | 49 | ) |
49 | 50 |
|
50 | 51 | # Draw donut using annular wedge |
51 | 52 | p.annular_wedge( |
52 | 53 | x=0, |
53 | 54 | y=0, |
54 | | - inner_radius=0.4, |
55 | | - outer_radius=0.9, |
| 55 | + inner_radius=0.45, |
| 56 | + outer_radius=0.95, |
56 | 57 | start_angle=cumsum("angle", include_zero=True), |
57 | 58 | end_angle=cumsum("angle"), |
58 | 59 | line_color="white", |
59 | | - line_width=3, |
| 60 | + line_width=4, |
60 | 61 | fill_color="color", |
61 | 62 | source=source, |
62 | 63 | ) |
63 | 64 |
|
64 | 65 | # Add percentage labels on segments |
65 | | -for pct, start, end in zip(data["percentage"], data["start_angle"], data["end_angle"], strict=True): |
| 66 | +for pct, start, end, color in zip(data["percentage"], data["start_angle"], data["end_angle"], colors, strict=True): |
66 | 67 | mid_angle = (start + end) / 2 |
67 | 68 | # Position labels at middle of the ring |
68 | | - label_radius = 0.65 |
| 69 | + label_radius = 0.70 |
| 70 | + # Adjust angle to start from top and go clockwise |
69 | 71 | x = label_radius * np.cos(mid_angle - pi / 2 + pi) |
70 | 72 | y = label_radius * np.sin(mid_angle - pi / 2 + pi) |
71 | 73 |
|
| 74 | + # Use white text for dark backgrounds, dark for light backgrounds |
| 75 | + text_color = "white" if color in ["#306998", "#FF6B6B", "#4ECDC4"] else "#333333" |
| 76 | + |
72 | 77 | label = Label( |
73 | 78 | x=x, |
74 | 79 | y=y, |
75 | 80 | text=pct, |
76 | | - text_font_size="24pt", |
77 | | - text_color="white", |
| 81 | + text_font_size="26pt", |
| 82 | + text_color=text_color, |
78 | 83 | text_font_style="bold", |
79 | 84 | text_align="center", |
80 | 85 | text_baseline="middle", |
81 | 86 | ) |
82 | 87 | p.add_layout(label) |
83 | 88 |
|
84 | 89 | # Add center text showing total |
85 | | -total = sum(values) |
86 | 90 | center_label = Label( |
87 | | - x=0, y=0.05, text="Total", text_font_size="28pt", text_color="#333333", text_align="center", text_baseline="middle" |
| 91 | + x=0, y=0.08, text="Total", text_font_size="32pt", text_color="#555555", text_align="center", text_baseline="middle" |
88 | 92 | ) |
89 | 93 | p.add_layout(center_label) |
90 | 94 |
|
91 | 95 | center_value = Label( |
92 | 96 | x=0, |
93 | | - y=-0.1, |
| 97 | + y=-0.12, |
94 | 98 | text=str(total), |
95 | | - text_font_size="40pt", |
| 99 | + text_font_size="48pt", |
96 | 100 | text_color="#306998", |
97 | 101 | text_font_style="bold", |
98 | 102 | text_align="center", |
|
101 | 105 | p.add_layout(center_value) |
102 | 106 |
|
103 | 107 | # Add legend entries on the right |
104 | | -legend_x = 1.15 |
105 | | -legend_y_start = 0.5 |
106 | | -legend_spacing = 0.2 |
| 108 | +legend_x = 1.20 |
| 109 | +legend_y_start = 0.6 |
| 110 | +legend_spacing = 0.24 |
107 | 111 |
|
108 | 112 | for i, (cat, val, color) in enumerate(zip(categories, values, colors, strict=True)): |
109 | 113 | y_pos = legend_y_start - i * legend_spacing |
110 | 114 | # Color box |
111 | | - p.rect(x=legend_x, y=y_pos, width=0.08, height=0.08, fill_color=color, line_color=None) |
| 115 | + p.rect(x=legend_x, y=y_pos, width=0.10, height=0.10, fill_color=color, line_color=None) |
112 | 116 | # Label text |
113 | 117 | legend_label = Label( |
114 | | - x=legend_x + 0.08, |
| 118 | + x=legend_x + 0.10, |
115 | 119 | y=y_pos, |
116 | | - text=f"{cat} ({val})", |
117 | | - text_font_size="20pt", |
| 120 | + text=f"{cat} ({val}%)", |
| 121 | + text_font_size="22pt", |
118 | 122 | text_color="#333333", |
119 | 123 | text_align="left", |
120 | 124 | text_baseline="middle", |
121 | 125 | ) |
122 | 126 | p.add_layout(legend_label) |
123 | 127 |
|
124 | 128 | # Style |
125 | | -p.title.text_font_size = "32pt" |
| 129 | +p.title.text_font_size = "36pt" |
126 | 130 | p.title.text_color = "#333333" |
127 | 131 | p.axis.visible = False |
128 | 132 | p.grid.visible = False |
|
0 commit comments