|
| 1 | +""" |
| 2 | +waffle-basic: Basic Waffle Chart |
| 3 | +Library: pygal |
| 4 | +""" |
| 5 | + |
| 6 | +import sys |
| 7 | + |
| 8 | + |
| 9 | +# Temporarily remove current directory from path to avoid name collision |
| 10 | +_cwd = sys.path[0] if sys.path[0] else "." |
| 11 | +if _cwd in sys.path: |
| 12 | + sys.path.remove(_cwd) |
| 13 | + |
| 14 | +from pygal.graph.graph import Graph # noqa: E402 |
| 15 | +from pygal.style import Style # noqa: E402 |
| 16 | + |
| 17 | + |
| 18 | +# Restore path |
| 19 | +sys.path.insert(0, _cwd) |
| 20 | + |
| 21 | + |
| 22 | +class Waffle(Graph): |
| 23 | + """Custom Waffle chart for pygal - displays proportions as colored squares in a grid.""" |
| 24 | + |
| 25 | + _serie_margin = 0 |
| 26 | + |
| 27 | + def __init__(self, *args, **kwargs): |
| 28 | + self.rows = kwargs.pop("rows", 10) |
| 29 | + self.cols = kwargs.pop("cols", 10) |
| 30 | + super().__init__(*args, **kwargs) |
| 31 | + |
| 32 | + def _plot(self): |
| 33 | + """Draw the waffle grid.""" |
| 34 | + # Calculate total values and percentages |
| 35 | + total = sum(sum(v for v in serie.values if v is not None) for serie in self.series) |
| 36 | + if total == 0: |
| 37 | + return |
| 38 | + |
| 39 | + total_squares = self.rows * self.cols |
| 40 | + |
| 41 | + # Get plot area dimensions from the view |
| 42 | + plot_width = self.view.width |
| 43 | + plot_height = self.view.height |
| 44 | + |
| 45 | + # Calculate square size with gap |
| 46 | + square_size = min(plot_width / self.cols, plot_height / self.rows) * 0.85 |
| 47 | + gap = square_size * 0.12 |
| 48 | + |
| 49 | + # Center the grid |
| 50 | + grid_width = self.cols * (square_size + gap) - gap |
| 51 | + grid_height = self.rows * (square_size + gap) - gap |
| 52 | + |
| 53 | + # Calculate the actual plot area boundaries |
| 54 | + # view.y(rows) gives the top of the plot area in SVG coordinates |
| 55 | + x_start = self.view.x(0) |
| 56 | + y_start = self.view.y(self.rows) # Top of the plot area in SVG coordinates |
| 57 | + |
| 58 | + # Center the grid within the plot area |
| 59 | + x_offset = x_start + (plot_width - grid_width) / 2 |
| 60 | + y_offset = y_start + (plot_height - grid_height) / 2 |
| 61 | + |
| 62 | + # Create a group for waffle squares |
| 63 | + plot_node = self.nodes["plot"] |
| 64 | + waffle_group = self.svg.node(plot_node, class_="waffle-chart") |
| 65 | + |
| 66 | + # Assign squares to each series |
| 67 | + square_index = 0 |
| 68 | + for serie_index, serie in enumerate(self.series): |
| 69 | + serie_value = sum(v for v in serie.values if v is not None) |
| 70 | + num_squares = round(serie_value / total * total_squares) |
| 71 | + |
| 72 | + color = self.style.colors[serie_index % len(self.style.colors)] |
| 73 | + |
| 74 | + # Create a group for this series |
| 75 | + serie_group = self.svg.node(waffle_group, class_="series serie-%d color-%d" % (serie_index, serie_index)) |
| 76 | + |
| 77 | + for _ in range(num_squares): |
| 78 | + if square_index >= total_squares: |
| 79 | + break |
| 80 | + |
| 81 | + row = square_index // self.cols |
| 82 | + col = square_index % self.cols |
| 83 | + |
| 84 | + x = x_offset + col * (square_size + gap) |
| 85 | + y = y_offset + row * (square_size + gap) |
| 86 | + |
| 87 | + # Draw square with rounded corners |
| 88 | + self.svg.node( |
| 89 | + serie_group, |
| 90 | + "rect", |
| 91 | + x=x, |
| 92 | + y=y, |
| 93 | + width=square_size, |
| 94 | + height=square_size, |
| 95 | + fill=color, |
| 96 | + rx=square_size * 0.1, |
| 97 | + ry=square_size * 0.1, |
| 98 | + class_="waffle-square reactive", |
| 99 | + ) |
| 100 | + |
| 101 | + square_index += 1 |
| 102 | + |
| 103 | + def _compute(self): |
| 104 | + """Compute the box for rendering.""" |
| 105 | + # Set basic box dimensions |
| 106 | + self._box.xmin = 0 |
| 107 | + self._box.xmax = self.cols |
| 108 | + self._box.ymin = 0 |
| 109 | + self._box.ymax = self.rows |
| 110 | + |
| 111 | + |
| 112 | +# Custom style for 4800x2700 canvas |
| 113 | +custom_style = Style( |
| 114 | + background="white", |
| 115 | + plot_background="white", |
| 116 | + foreground="#333333", |
| 117 | + foreground_strong="#333333", |
| 118 | + foreground_subtle="#666666", |
| 119 | + colors=("#306998", "#FFD43B", "#4ECDC4", "#FF6B6B"), |
| 120 | + title_font_size=72, |
| 121 | + legend_font_size=48, |
| 122 | + label_font_size=36, |
| 123 | + value_font_size=32, |
| 124 | + font_family="sans-serif", |
| 125 | +) |
| 126 | + |
| 127 | +# Data - Budget allocation example (values should sum to 100) |
| 128 | +categories = {"Operations": 42, "Marketing": 28, "R&D": 18, "Admin": 12} |
| 129 | + |
| 130 | +# Create waffle chart |
| 131 | +chart = Waffle( |
| 132 | + width=4800, |
| 133 | + height=2700, |
| 134 | + rows=10, |
| 135 | + cols=10, |
| 136 | + style=custom_style, |
| 137 | + title="Budget Allocation · waffle-basic · pygal · pyplots.ai", |
| 138 | + show_legend=True, |
| 139 | + legend_at_bottom=True, |
| 140 | + legend_at_bottom_columns=4, |
| 141 | + margin=80, |
| 142 | + margin_bottom=250, |
| 143 | + show_x_labels=False, |
| 144 | + show_y_labels=False, |
| 145 | +) |
| 146 | + |
| 147 | +# Add data series with percentages in labels |
| 148 | +for category, value in categories.items(): |
| 149 | + chart.add(f"{category} ({value}%)", [value]) |
| 150 | + |
| 151 | +# Save outputs |
| 152 | +chart.render_to_file("plot.svg") |
| 153 | +chart.render_to_png("plot.png") |
| 154 | + |
| 155 | +# Also save HTML for interactive viewing |
| 156 | +html_content = f"""<!DOCTYPE html> |
| 157 | +<html> |
| 158 | +<head> |
| 159 | + <title>waffle-basic · pygal · pyplots.ai</title> |
| 160 | + <style> |
| 161 | + body {{ margin: 0; padding: 20px; background: #f5f5f5; }} |
| 162 | + .container {{ max-width: 100%; margin: 0 auto; }} |
| 163 | + svg {{ max-width: 100%; height: auto; }} |
| 164 | + </style> |
| 165 | +</head> |
| 166 | +<body> |
| 167 | + <div class="container"> |
| 168 | + {chart.render(is_unicode=True)} |
| 169 | + </div> |
| 170 | +</body> |
| 171 | +</html>""" |
| 172 | + |
| 173 | +with open("plot.html", "w") as f: |
| 174 | + f.write(html_content) |
0 commit comments