|
1 | 1 | """ anyplot.ai |
2 | 2 | waffle-basic: Basic Waffle Chart |
3 | | -Library: plotly 6.7.0 | Python 3.13.13 |
4 | | -Quality: 90/100 | Updated: 2026-05-05 |
| 3 | +Library: plotly 6.9.0 | Python 3.13.14 |
| 4 | +Quality: 91/100 | Updated: 2026-07-26 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
|
10 | 10 | import plotly.graph_objects as go |
11 | 11 |
|
12 | 12 |
|
13 | | -# Theme tokens |
| 13 | +# Theme tokens (see prompts/default-style-guide.md "Background" + "Theme-adaptive Chrome") |
14 | 14 | THEME = os.getenv("ANYPLOT_THEME", "light") |
15 | 15 | PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
16 | 16 | ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
17 | 17 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
18 | 18 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
19 | | -GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
20 | 19 |
|
21 | | -# Okabe-Ito palette - first series ALWAYS #009E73 |
22 | | -IMPRINT = [ |
23 | | - "#009E73", # bluish green (brand) |
24 | | - "#C475FD", # vermillion |
25 | | - "#4467A3", # blue |
26 | | - "#BD8233", # reddish purple |
27 | | - "#AE3030", # orange |
28 | | -] |
| 20 | +# Imprint palette - first series ALWAYS #009E73 |
| 21 | +IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030"] |
29 | 22 |
|
30 | | -# Data - Budget allocation across spending categories |
| 23 | +# Data - budget allocation across spending categories |
31 | 24 | categories = ["Operations", "Marketing", "R&D", "HR", "Other"] |
32 | 25 | values = [35, 25, 22, 12, 6] # Percentages (sum to 100) |
| 26 | +n_categories = len(categories) |
33 | 27 |
|
34 | | -# Create 10x10 waffle grid (100 squares, each = 1%) |
| 28 | +# Build a 10x10 waffle grid (100 squares, each = 1%), filled bottom-up, left-to-right |
35 | 29 | grid_size = 10 |
36 | 30 | total_squares = grid_size * grid_size |
37 | | -waffle_data = np.zeros(total_squares, dtype=int) |
| 31 | +category_idx = np.repeat(np.arange(n_categories), values) |
| 32 | +waffle_matrix = category_idx.reshape(grid_size, grid_size)[::-1] |
38 | 33 |
|
39 | | -# Fill grid with category indices (row by row from bottom-left) |
40 | | -square_idx = 0 |
41 | | -for cat_idx, count in enumerate(values): |
42 | | - for _ in range(count): |
43 | | - if square_idx < total_squares: |
44 | | - waffle_data[square_idx] = cat_idx |
45 | | - square_idx += 1 |
| 34 | +# Hover metadata mirrors the grid so each square reports its own category and share |
| 35 | +category_names = np.array(categories)[waffle_matrix] |
| 36 | +category_values = np.array(values)[waffle_matrix] |
46 | 37 |
|
47 | | -# Reshape to 10x10 grid |
48 | | -waffle_matrix = waffle_data.reshape(grid_size, grid_size) |
| 38 | +# Single Heatmap trace (not 100+ scatter traces) with a hand-built discrete |
| 39 | +# colorscale - each category occupies an equal band so cell colors stay flat, |
| 40 | +# and the vertical colorbar doubles as a labeled legend (unrotated tick text, |
| 41 | +# one row per category, so nothing crowds or clips at the canvas edge). |
| 42 | +discrete_colorscale = [] |
| 43 | +for idx, color in enumerate(IMPRINT[:n_categories]): |
| 44 | + discrete_colorscale.append([idx / n_categories, color]) |
| 45 | + discrete_colorscale.append([(idx + 1) / n_categories, color]) |
49 | 46 |
|
50 | | -# Create figure |
51 | | -fig = go.Figure() |
52 | | - |
53 | | -# Add squares using scatter plot with larger markers for visibility |
54 | | -for row in range(grid_size): |
55 | | - for col in range(grid_size): |
56 | | - cat_idx = waffle_matrix[row, col] |
57 | | - fig.add_trace( |
58 | | - go.Scatter( |
59 | | - x=[col], |
60 | | - y=[row], |
61 | | - mode="markers", |
62 | | - marker={ |
63 | | - "size": 55, |
64 | | - "symbol": "square", |
65 | | - "color": IMPRINT[cat_idx], |
66 | | - "line": {"color": PAGE_BG, "width": 2}, |
67 | | - }, |
68 | | - showlegend=False, |
69 | | - hoverinfo="skip", |
70 | | - ) |
71 | | - ) |
72 | | - |
73 | | -# Add legend traces (one per category) |
74 | | -for cat_idx, (cat, val) in enumerate(zip(categories, values, strict=True)): |
75 | | - fig.add_trace( |
76 | | - go.Scatter( |
77 | | - x=[None], |
78 | | - y=[None], |
79 | | - mode="markers", |
80 | | - marker={"size": 22, "symbol": "square", "color": IMPRINT[cat_idx]}, |
81 | | - name=f"{cat}: {val}%", |
82 | | - showlegend=True, |
83 | | - ) |
| 47 | +fig = go.Figure( |
| 48 | + go.Heatmap( |
| 49 | + z=waffle_matrix, |
| 50 | + zmin=-0.5, |
| 51 | + zmax=n_categories - 0.5, |
| 52 | + colorscale=discrete_colorscale, |
| 53 | + xgap=6, |
| 54 | + ygap=6, |
| 55 | + text=category_names, |
| 56 | + customdata=category_values, |
| 57 | + hovertemplate="<b>%{text}</b><br>%{customdata}% of total<extra></extra>", |
| 58 | + showscale=True, |
| 59 | + colorbar={ |
| 60 | + "thickness": 26, |
| 61 | + "len": 0.7, |
| 62 | + "x": 1.06, |
| 63 | + "xanchor": "left", |
| 64 | + "y": 0.5, |
| 65 | + "yanchor": "middle", |
| 66 | + "tickmode": "array", |
| 67 | + "tickvals": list(range(n_categories)), |
| 68 | + "ticktext": [f"{cat} — {val}%" for cat, val in zip(categories, values, strict=True)], |
| 69 | + "ticks": "", |
| 70 | + "outlinewidth": 1, |
| 71 | + "outlinecolor": INK_SOFT, |
| 72 | + "tickfont": {"size": 13, "color": INK_SOFT}, |
| 73 | + "bgcolor": ELEVATED_BG, |
| 74 | + }, |
84 | 75 | ) |
| 76 | +) |
85 | 77 |
|
86 | 78 | # Layout |
87 | 79 | fig.update_layout( |
| 80 | + autosize=False, |
88 | 81 | title={ |
89 | | - "text": "waffle-basic · plotly · anyplot.ai", |
90 | | - "font": {"size": 28, "color": INK}, |
| 82 | + "text": "waffle-basic · python · plotly · anyplot.ai", |
| 83 | + "font": {"size": 16, "color": INK}, |
91 | 84 | "x": 0.5, |
92 | 85 | "xanchor": "center", |
93 | 86 | }, |
94 | | - xaxis={ |
95 | | - "showgrid": False, |
96 | | - "zeroline": False, |
97 | | - "showticklabels": False, |
98 | | - "range": [-0.8, 9.8], |
99 | | - "scaleanchor": "y", |
100 | | - "scaleratio": 1, |
101 | | - }, |
102 | | - yaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "range": [-0.8, 9.8]}, |
103 | | - legend={ |
104 | | - "font": {"size": 18, "color": INK_SOFT}, |
105 | | - "bgcolor": ELEVATED_BG, |
106 | | - "bordercolor": INK_SOFT, |
107 | | - "borderwidth": 1, |
108 | | - "orientation": "h", |
109 | | - "yanchor": "top", |
110 | | - "y": -0.08, |
111 | | - "xanchor": "center", |
112 | | - "x": 0.5, |
113 | | - "itemsizing": "constant", |
114 | | - }, |
| 87 | + xaxis={"visible": False, "range": [-0.5, grid_size - 0.5], "scaleanchor": "y", "scaleratio": 1}, |
| 88 | + yaxis={"visible": False, "range": [-0.5, grid_size - 0.5]}, |
115 | 89 | paper_bgcolor=PAGE_BG, |
116 | 90 | plot_bgcolor=PAGE_BG, |
117 | | - margin={"l": 60, "r": 60, "t": 120, "b": 120}, |
| 91 | + margin={"l": 60, "r": 220, "t": 90, "b": 60}, |
118 | 92 | ) |
119 | 93 |
|
120 | 94 | # Save outputs |
121 | | -fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3) |
| 95 | +fig.write_image(f"plot-{THEME}.png", width=600, height=600, scale=4) |
122 | 96 | fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") |
0 commit comments