|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | donut-basic: Basic Donut Chart |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.9.0 | Python 3.14.4 |
| 4 | +Quality: 88/100 | Updated: 2026-04-24 |
5 | 5 | """ |
6 | 6 |
|
7 | | -from math import pi |
| 7 | +import os |
| 8 | +from math import cos, pi, sin |
8 | 9 |
|
9 | 10 | import numpy as np |
10 | 11 | from bokeh.io import export_png, output_file, save |
|
13 | 14 | from bokeh.transform import cumsum |
14 | 15 |
|
15 | 16 |
|
16 | | -# Data - Portfolio allocation by asset class |
17 | | -categories = ["Technology", "Healthcare", "Finance", "Energy", "Retail"] |
18 | | -values = [35, 25, 20, 12, 8] |
19 | | -total = sum(values) |
| 17 | +# Theme tokens |
| 18 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 19 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 20 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 21 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 22 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
20 | 23 |
|
21 | | -# Calculate angles for donut segments |
22 | | -data = { |
23 | | - "category": categories, |
24 | | - "value": values, |
25 | | - "angle": [v / total * 2 * pi for v in values], |
26 | | - "percentage": [f"{v / total * 100:.1f}%" for v in values], |
27 | | -} |
| 24 | +# Okabe-Ito categorical palette (first segment is always brand green) |
| 25 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"] |
28 | 26 |
|
29 | | -# Cumulative angles for label positioning |
30 | | -cumulative = np.cumsum([0] + data["angle"][:-1]).tolist() |
31 | | -data["start_angle"] = cumulative |
32 | | -data["end_angle"] = np.cumsum(data["angle"]).tolist() |
| 27 | +# Data — Annual budget allocation by department (USD thousands) |
| 28 | +categories = ["Engineering", "Operations", "Marketing", "Sales", "Support"] |
| 29 | +values = [480, 210, 155, 125, 55] |
| 30 | +total = sum(values) |
33 | 31 |
|
34 | | -# Colors - Python Blue as primary, then colorblind-safe palette |
35 | | -colors = ["#306998", "#FFD43B", "#4ECDC4", "#FF6B6B", "#95E1D3"] |
36 | | -data["color"] = colors |
| 32 | +angles = [v / total * 2 * pi for v in values] |
| 33 | +percentages = [f"{v / total * 100:.1f}%" for v in values] |
37 | 34 |
|
38 | | -source = ColumnDataSource(data=data) |
| 35 | +source = ColumnDataSource( |
| 36 | + data={"category": categories, "value": values, "angle": angles, "color": OKABE_ITO[: len(categories)]} |
| 37 | +) |
39 | 38 |
|
40 | | -# Create figure (square format for circular chart) |
| 39 | +# Plot — square canvas for circular shapes |
41 | 40 | p = figure( |
42 | 41 | width=3600, |
43 | 42 | height=3600, |
44 | | - title="donut-basic · bokeh · pyplots.ai", |
| 43 | + title="Budget by Department · donut-basic · bokeh · anyplot.ai", |
45 | 44 | toolbar_location=None, |
46 | 45 | tools="", |
47 | | - x_range=(-1.4, 1.8), |
48 | | - y_range=(-1.3, 1.3), |
| 46 | + x_range=(-1.4, 1.4), |
| 47 | + y_range=(-1.4, 1.4), |
49 | 48 | ) |
50 | 49 |
|
51 | | -# Draw donut using annular wedge |
52 | 50 | p.annular_wedge( |
53 | 51 | x=0, |
54 | 52 | y=0, |
55 | | - inner_radius=0.45, |
56 | | - outer_radius=0.95, |
| 53 | + inner_radius=0.55, |
| 54 | + outer_radius=1.0, |
57 | 55 | start_angle=cumsum("angle", include_zero=True), |
58 | 56 | end_angle=cumsum("angle"), |
59 | | - line_color="white", |
60 | | - line_width=4, |
| 57 | + line_color=PAGE_BG, |
| 58 | + line_width=6, |
61 | 59 | fill_color="color", |
| 60 | + legend_field="category", |
62 | 61 | source=source, |
63 | 62 | ) |
64 | 63 |
|
65 | | -# Add percentage labels on segments |
66 | | -for pct, start, end, color in zip(data["percentage"], data["start_angle"], data["end_angle"], colors, strict=True): |
67 | | - mid_angle = (start + end) / 2 |
68 | | - # Position labels at middle of the ring |
69 | | - label_radius = 0.70 |
70 | | - # Adjust angle to start from top and go clockwise |
71 | | - x = label_radius * np.cos(mid_angle - pi / 2 + pi) |
72 | | - y = label_radius * np.sin(mid_angle - pi / 2 + pi) |
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 | | - |
77 | | - label = Label( |
78 | | - x=x, |
79 | | - y=y, |
80 | | - text=pct, |
81 | | - text_font_size="26pt", |
82 | | - text_color=text_color, |
83 | | - text_font_style="bold", |
| 64 | +# Percentage labels on each segment (Bokeh: angle 0 = 3 o'clock, CCW) |
| 65 | +cumulative_starts = np.cumsum([0.0] + angles[:-1]) |
| 66 | +for pct, start, ang in zip(percentages, cumulative_starts, angles, strict=True): |
| 67 | + mid = start + ang / 2 |
| 68 | + label_radius = 0.78 |
| 69 | + x = label_radius * cos(mid) |
| 70 | + y = label_radius * sin(mid) |
| 71 | + p.add_layout( |
| 72 | + Label( |
| 73 | + x=x, |
| 74 | + y=y, |
| 75 | + text=pct, |
| 76 | + text_font_size="44pt", |
| 77 | + text_color="#F0EFE8", |
| 78 | + text_font_style="bold", |
| 79 | + text_align="center", |
| 80 | + text_baseline="middle", |
| 81 | + ) |
| 82 | + ) |
| 83 | + |
| 84 | +# Center metric |
| 85 | +p.add_layout( |
| 86 | + Label( |
| 87 | + x=0, |
| 88 | + y=0.13, |
| 89 | + text="Total budget", |
| 90 | + text_font_size="40pt", |
| 91 | + text_color=INK_SOFT, |
84 | 92 | text_align="center", |
85 | 93 | text_baseline="middle", |
86 | 94 | ) |
87 | | - p.add_layout(label) |
88 | | - |
89 | | -# Add center text showing total |
90 | | -center_label = Label( |
91 | | - x=0, y=0.08, text="Total", text_font_size="32pt", text_color="#555555", text_align="center", text_baseline="middle" |
92 | | -) |
93 | | -p.add_layout(center_label) |
94 | | - |
95 | | -center_value = Label( |
96 | | - x=0, |
97 | | - y=-0.12, |
98 | | - text=str(total), |
99 | | - text_font_size="48pt", |
100 | | - text_color="#306998", |
101 | | - text_font_style="bold", |
102 | | - text_align="center", |
103 | | - text_baseline="middle", |
104 | 95 | ) |
105 | | -p.add_layout(center_value) |
106 | | - |
107 | | -# Add legend entries on the right |
108 | | -legend_x = 1.20 |
109 | | -legend_y_start = 0.6 |
110 | | -legend_spacing = 0.24 |
111 | | - |
112 | | -for i, (cat, val, color) in enumerate(zip(categories, values, colors, strict=True)): |
113 | | - y_pos = legend_y_start - i * legend_spacing |
114 | | - # Color box |
115 | | - p.rect(x=legend_x, y=y_pos, width=0.10, height=0.10, fill_color=color, line_color=None) |
116 | | - # Label text |
117 | | - legend_label = Label( |
118 | | - x=legend_x + 0.10, |
119 | | - y=y_pos, |
120 | | - text=f"{cat} ({val}%)", |
121 | | - text_font_size="22pt", |
122 | | - text_color="#333333", |
123 | | - text_align="left", |
| 96 | +p.add_layout( |
| 97 | + Label( |
| 98 | + x=0, |
| 99 | + y=-0.10, |
| 100 | + text=f"${total:,}K", |
| 101 | + text_font_size="96pt", |
| 102 | + text_color=INK, |
| 103 | + text_font_style="bold", |
| 104 | + text_align="center", |
124 | 105 | text_baseline="middle", |
125 | 106 | ) |
126 | | - p.add_layout(legend_label) |
| 107 | +) |
| 108 | + |
| 109 | +# Style — theme-adaptive chrome |
| 110 | +p.background_fill_color = PAGE_BG |
| 111 | +p.border_fill_color = PAGE_BG |
| 112 | +p.outline_line_color = None |
| 113 | + |
| 114 | +p.title.text_font_size = "56pt" |
| 115 | +p.title.text_color = INK |
| 116 | +p.title.align = "center" |
| 117 | +p.title.text_font_style = "normal" |
127 | 118 |
|
128 | | -# Style |
129 | | -p.title.text_font_size = "36pt" |
130 | | -p.title.text_color = "#333333" |
131 | 119 | p.axis.visible = False |
132 | 120 | p.grid.visible = False |
133 | | -p.outline_line_color = None |
134 | | -p.background_fill_color = None |
135 | | -p.border_fill_color = None |
| 121 | + |
| 122 | +p.legend.background_fill_color = ELEVATED_BG |
| 123 | +p.legend.border_line_color = None |
| 124 | +p.legend.label_text_color = INK_SOFT |
| 125 | +p.legend.label_text_font_size = "36pt" |
| 126 | +p.legend.location = "top_right" |
| 127 | +p.legend.spacing = 18 |
| 128 | +p.legend.padding = 24 |
| 129 | +p.legend.glyph_height = 60 |
| 130 | +p.legend.glyph_width = 60 |
136 | 131 |
|
137 | 132 | # Save |
138 | | -export_png(p, filename="plot.png") |
139 | | -output_file("plot.html") |
| 133 | +export_png(p, filename=f"plot-{THEME}.png") |
| 134 | +output_file(f"plot-{THEME}.html") |
140 | 135 | save(p) |
0 commit comments