|
1 | 1 | """ pyplots.ai |
2 | 2 | qrcode-basic: Basic QR Code Generator |
3 | | -Library: pygal 3.1.0 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-07 |
| 3 | +Library: pygal 3.1.0 | Python 3.14.3 |
| 4 | +Quality: 85/100 | Updated: 2026-04-07 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import sys |
8 | 8 |
|
9 | 9 | import qrcode |
10 | 10 |
|
11 | 11 |
|
12 | | -# Temporarily remove current directory from path to avoid name collision |
| 12 | +# Avoid name collision: this file is named pygal.py, which shadows the package |
13 | 13 | _cwd = sys.path[0] if sys.path[0] else "." |
14 | 14 | if _cwd in sys.path: |
15 | 15 | sys.path.remove(_cwd) |
16 | 16 |
|
17 | | -from pygal.graph.graph import Graph # noqa: E402 |
| 17 | +import pygal # noqa: E402 |
18 | 18 | from pygal.style import Style # noqa: E402 |
19 | 19 |
|
20 | 20 |
|
21 | | -# Restore path |
22 | 21 | sys.path.insert(0, _cwd) |
23 | 22 |
|
| 23 | +# --- Data --- |
| 24 | +qr_content = "https://pyplots.ai" |
24 | 25 |
|
25 | | -class QRCodeChart(Graph): |
26 | | - """Custom QR Code visualization for pygal - renders QR matrix as SVG squares.""" |
27 | | - |
28 | | - def __init__(self, *args, **kwargs): |
29 | | - self.qr_data = kwargs.pop("qr_data", "https://pyplots.ai") |
30 | | - self.error_correction = kwargs.pop("error_correction", qrcode.constants.ERROR_CORRECT_M) |
31 | | - self.box_size = kwargs.pop("box_size", 10) |
32 | | - self.border = kwargs.pop("border", 4) |
33 | | - self.fill_color = kwargs.pop("fill_color", "#000000") |
34 | | - self.back_color = kwargs.pop("back_color", "#FFFFFF") |
35 | | - super().__init__(*args, **kwargs) |
36 | | - self._qr_matrix = None |
37 | | - |
38 | | - def _generate_qr_matrix(self): |
39 | | - """Generate QR code matrix using qrcode library.""" |
40 | | - qr = qrcode.QRCode(version=None, error_correction=self.error_correction, box_size=1, border=0) |
41 | | - qr.add_data(self.qr_data) |
42 | | - qr.make(fit=True) |
43 | | - return qr.get_matrix() |
44 | | - |
45 | | - def _plot(self): |
46 | | - """Draw the QR code as SVG rectangles.""" |
47 | | - # Generate QR matrix |
48 | | - self._qr_matrix = self._generate_qr_matrix() |
49 | | - if not self._qr_matrix: |
50 | | - return |
51 | | - |
52 | | - matrix_size = len(self._qr_matrix) |
53 | | - total_size = matrix_size + 2 * self.border # Add quiet zone |
54 | | - |
55 | | - # Get plot dimensions |
56 | | - plot_width = self.view.width |
57 | | - plot_height = self.view.height |
58 | | - |
59 | | - # Calculate cell size to fit QR code in view with margins |
60 | | - margin = 150 # Space for title |
61 | | - available_size = min(plot_width, plot_height) - 2 * margin |
62 | | - cell_size = available_size / total_size |
63 | | - |
64 | | - # Calculate offset to center the QR code |
65 | | - qr_total_size = total_size * cell_size |
66 | | - x_offset = self.view.x(0) + (plot_width - qr_total_size) / 2 |
67 | | - y_offset = self.view.y(total_size) + (plot_height - qr_total_size) / 2 + 50 |
68 | | - |
69 | | - # Create group for the QR code |
70 | | - plot_node = self.nodes["plot"] |
71 | | - qr_group = self.svg.node(plot_node, class_="qr-code") |
72 | | - |
73 | | - # Draw background (quiet zone included) |
74 | | - bg_rect = self.svg.node(qr_group, "rect", x=x_offset, y=y_offset, width=qr_total_size, height=qr_total_size) |
75 | | - bg_rect.set("fill", self.back_color) |
76 | | - bg_rect.set("stroke", "#CCCCCC") |
77 | | - bg_rect.set("stroke-width", "2") |
78 | | - |
79 | | - # Draw QR code modules (black squares) |
80 | | - for row_idx, row in enumerate(self._qr_matrix): |
81 | | - for col_idx, cell in enumerate(row): |
82 | | - if cell: # Black module |
83 | | - x = x_offset + (col_idx + self.border) * cell_size |
84 | | - y = y_offset + (row_idx + self.border) * cell_size |
85 | | - |
86 | | - rect = self.svg.node(qr_group, "rect", x=x, y=y, width=cell_size, height=cell_size) |
87 | | - rect.set("fill", self.fill_color) |
88 | | - |
89 | | - # Add encoded data label below the QR code |
90 | | - label_y = y_offset + qr_total_size + 80 |
91 | | - label_x = x_offset + qr_total_size / 2 |
92 | | - font_size = 42 |
93 | | - |
94 | | - # URL label |
95 | | - text_node = self.svg.node(qr_group, "text", x=label_x, y=label_y) |
96 | | - text_node.set("text-anchor", "middle") |
97 | | - text_node.set("fill", "#306998") |
98 | | - text_node.set("style", f"font-size:{font_size}px;font-weight:bold;font-family:sans-serif") |
99 | | - text_node.text = self.qr_data |
100 | | - |
101 | | - # Error correction level label |
102 | | - ec_labels = { |
103 | | - qrcode.constants.ERROR_CORRECT_L: "Error Correction: L (7%)", |
104 | | - qrcode.constants.ERROR_CORRECT_M: "Error Correction: M (15%)", |
105 | | - qrcode.constants.ERROR_CORRECT_Q: "Error Correction: Q (25%)", |
106 | | - qrcode.constants.ERROR_CORRECT_H: "Error Correction: H (30%)", |
107 | | - } |
108 | | - ec_label = ec_labels.get(self.error_correction, "") |
109 | | - |
110 | | - ec_node = self.svg.node(qr_group, "text", x=label_x, y=label_y + 60) |
111 | | - ec_node.set("text-anchor", "middle") |
112 | | - ec_node.set("fill", "#666666") |
113 | | - ec_node.set("style", f"font-size:{int(font_size * 0.7)}px;font-family:sans-serif") |
114 | | - ec_node.text = ec_label |
115 | | - |
116 | | - # Matrix size info |
117 | | - size_node = self.svg.node(qr_group, "text", x=label_x, y=label_y + 110) |
118 | | - size_node.set("text-anchor", "middle") |
119 | | - size_node.set("fill", "#666666") |
120 | | - size_node.set("style", f"font-size:{int(font_size * 0.7)}px;font-family:sans-serif") |
121 | | - size_node.text = f"Matrix: {matrix_size} x {matrix_size} modules" |
122 | | - |
123 | | - def _compute(self): |
124 | | - """Compute the box for rendering.""" |
125 | | - # Generate matrix to get size |
126 | | - if self._qr_matrix is None: |
127 | | - self._qr_matrix = self._generate_qr_matrix() |
128 | | - |
129 | | - matrix_size = len(self._qr_matrix) if self._qr_matrix else 21 |
130 | | - total_size = matrix_size + 2 * self.border |
131 | | - |
132 | | - self._box.xmin = 0 |
133 | | - self._box.xmax = total_size |
134 | | - self._box.ymin = 0 |
135 | | - self._box.ymax = total_size |
136 | | - |
137 | | - |
138 | | -# Custom style for 3600x3600 square canvas (best for QR code) |
| 26 | +qr = qrcode.QRCode(version=None, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=1, border=0) |
| 27 | +qr.add_data(qr_content) |
| 28 | +qr.make(fit=True) |
| 29 | +qr_matrix = qr.get_matrix() |
| 30 | +matrix_size = len(qr_matrix) |
| 31 | +quiet_zone = 4 |
| 32 | +total_cols = matrix_size + 2 * quiet_zone |
| 33 | + |
| 34 | +# --- Color palette for QR structural elements --- |
| 35 | +FINDER_DARK = "#1a237e" # Deep indigo — finder patterns |
| 36 | +TIMING_DARK = "#6a1b9a" # Purple — timing strips |
| 37 | +ALIGN_DARK = "#00695c" # Teal — alignment pattern |
| 38 | +DATA_DARK = "#212121" # Near-black — data modules |
| 39 | +WHITE = "#FFFFFF" |
| 40 | + |
| 41 | +# --- Identify structural elements inline (KISS: no helper functions) --- |
| 42 | +finder_cells = set() |
| 43 | +for r in range(7): |
| 44 | + for c in range(7): |
| 45 | + finder_cells.add((r, c)) # Top-left |
| 46 | + finder_cells.add((r, matrix_size - 7 + c)) # Top-right |
| 47 | + finder_cells.add((matrix_size - 7 + r, c)) # Bottom-left |
| 48 | + |
| 49 | +timing_cells = set() |
| 50 | +for i in range(7, matrix_size - 7): |
| 51 | + timing_cells.add((6, i)) # Horizontal timing strip |
| 52 | + timing_cells.add((i, 6)) # Vertical timing strip |
| 53 | + |
| 54 | +align_cells = set() |
| 55 | +if matrix_size >= 25: |
| 56 | + ax, ay = matrix_size - 7, matrix_size - 7 |
| 57 | + for dr in range(-2, 3): |
| 58 | + for dc in range(-2, 3): |
| 59 | + align_cells.add((ax + dr, ay + dc)) |
| 60 | + |
| 61 | +# --- Style --- |
139 | 62 | custom_style = Style( |
140 | 63 | background="white", |
141 | | - plot_background="white", |
| 64 | + plot_background="#fafafa", |
142 | 65 | foreground="#333333", |
143 | | - foreground_strong="#333333", |
| 66 | + foreground_strong="#222222", |
144 | 67 | foreground_subtle="#666666", |
145 | | - colors=("#306998", "#FFD43B"), |
146 | | - title_font_size=72, |
147 | | - legend_font_size=48, |
148 | | - label_font_size=42, |
149 | | - value_font_size=36, |
150 | | - font_family="sans-serif", |
| 68 | + colors=("#000000",), |
| 69 | + title_font_size=64, |
| 70 | + label_font_size=36, |
| 71 | + major_label_font_size=36, |
| 72 | + legend_font_size=0, |
| 73 | + value_font_size=0, |
| 74 | + font_family="'Helvetica Neue', Helvetica, Arial, sans-serif", |
| 75 | + opacity=1.0, |
| 76 | + opacity_hover=1.0, |
| 77 | + transition="0s", |
151 | 78 | ) |
152 | 79 |
|
153 | | -# Data - URL to encode in QR code |
154 | | -qr_content = "https://pyplots.ai" |
| 80 | +# --- Chart --- |
| 81 | +# CSS: remove bar strokes for seamless pixel grid; add subtle border to plot area |
| 82 | +custom_css = ( |
| 83 | + "inline:" |
| 84 | + "rect { stroke-width: 0 !important; stroke: none !important;" |
| 85 | + " shape-rendering: crispEdges !important; }" |
| 86 | + " .plot_background { stroke: #bdbdbd !important; stroke-width: 2 !important;" |
| 87 | + " rx: 6 !important; ry: 6 !important; }" |
| 88 | + " .title { font-weight: 600 !important; letter-spacing: 1px !important; }" |
| 89 | +) |
155 | 90 |
|
156 | | -# Create QR code chart |
157 | | -chart = QRCodeChart( |
| 91 | +chart = pygal.StackedBar( |
158 | 92 | width=3600, |
159 | 93 | height=3600, |
160 | 94 | style=custom_style, |
161 | | - title="qrcode-basic \u00b7 pygal \u00b7 pyplots.ai", |
162 | | - qr_data=qr_content, |
163 | | - error_correction=qrcode.constants.ERROR_CORRECT_M, |
164 | | - border=4, |
165 | | - fill_color="#000000", |
166 | | - back_color="#FFFFFF", |
| 95 | + title="qrcode-basic · pygal · pyplots.ai", |
167 | 96 | show_legend=False, |
168 | | - margin=120, |
169 | | - margin_top=200, |
170 | | - margin_bottom=200, |
171 | 97 | show_x_labels=False, |
172 | 98 | show_y_labels=False, |
| 99 | + show_x_guides=False, |
| 100 | + show_y_guides=False, |
| 101 | + spacing=0, |
| 102 | + margin=100, |
| 103 | + margin_top=160, |
| 104 | + margin_bottom=300, |
| 105 | + print_values=False, |
| 106 | + range=(0, total_cols), |
| 107 | + x_title=( |
| 108 | + f"{qr_content} · Error Correction: M (15%)" |
| 109 | + f" · {matrix_size}×{matrix_size} modules\n" |
| 110 | + f"■ Finder (indigo) ■ Timing (purple)" |
| 111 | + f" ■ Alignment (teal) ■ Data (black)" |
| 112 | + ), |
| 113 | + css=["file://style.css", "file://graph.css", custom_css], |
173 | 114 | ) |
174 | 115 |
|
175 | | -# Add a dummy series to trigger _plot (pygal requires at least one series) |
176 | | -chart.add("", [0]) |
177 | | - |
178 | | -# Save output |
179 | | -chart.render_to_file("plot.svg") |
| 116 | +# --- Build rows --- |
| 117 | +# Slight oversize (1.03) ensures rows overlap, eliminating SVG rendering seams |
| 118 | +CELL = 1.03 |
| 119 | +white_row = [{"value": CELL, "color": WHITE} for _ in range(total_cols)] |
| 120 | + |
| 121 | +# Bottom quiet zone |
| 122 | +for _ in range(quiet_zone): |
| 123 | + chart.add("", white_row) |
| 124 | + |
| 125 | +# QR matrix rows (bottom to top for StackedBar stacking) |
| 126 | +for row_idx in reversed(range(matrix_size)): |
| 127 | + row_data = [] |
| 128 | + for col_idx in range(-quiet_zone, matrix_size + quiet_zone): |
| 129 | + if col_idx < 0 or col_idx >= matrix_size: |
| 130 | + row_data.append({"value": CELL, "color": WHITE}) |
| 131 | + elif qr_matrix[row_idx][col_idx]: |
| 132 | + pos = (row_idx, col_idx) |
| 133 | + if pos in finder_cells: |
| 134 | + color = FINDER_DARK |
| 135 | + elif pos in align_cells: |
| 136 | + color = ALIGN_DARK |
| 137 | + elif pos in timing_cells: |
| 138 | + color = TIMING_DARK |
| 139 | + else: |
| 140 | + color = DATA_DARK |
| 141 | + row_data.append({"value": CELL, "color": color}) |
| 142 | + else: |
| 143 | + row_data.append({"value": CELL, "color": WHITE}) |
| 144 | + chart.add("", row_data) |
| 145 | + |
| 146 | +# Top quiet zone |
| 147 | +for _ in range(quiet_zone): |
| 148 | + chart.add("", white_row) |
| 149 | + |
| 150 | +# --- Save --- |
180 | 151 | chart.render_to_png("plot.png") |
181 | | - |
182 | | -# Also save HTML for interactivity |
183 | | -html_content = f"""<!DOCTYPE html> |
184 | | -<html> |
185 | | -<head> |
186 | | - <meta charset="utf-8"> |
187 | | - <title>qrcode-basic - pygal</title> |
188 | | - <style> |
189 | | - body {{ margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f5f5f5; }} |
190 | | - .chart {{ max-width: 100%; height: auto; }} |
191 | | - </style> |
192 | | -</head> |
193 | | -<body> |
194 | | - <figure class="chart"> |
195 | | - {chart.render(is_unicode=True)} |
196 | | - </figure> |
197 | | -</body> |
198 | | -</html> |
199 | | -""" |
200 | | - |
201 | | -with open("plot.html", "w", encoding="utf-8") as f: |
202 | | - f.write(html_content) |
| 152 | +chart.render_to_file("plot.html") |
0 commit comments