Skip to content

Commit d45c640

Browse files
update(qrcode-basic): altair — scannable QR codes (#5224)
## Summary Updated **altair** implementation for **qrcode-basic**. **Changes:** Use `qrcode` library for real scannable QR code generation instead of manual matrix construction (fixes #3413) ### Changes - Replaced manual QR matrix with `qrcode` library for proper encoding - QR code now encodes "https://pyplots.ai" and is scannable by standard readers - Maintained library-idiomatic rendering approach - Spec updated to require scannable output ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent abe5517 commit d45c640

File tree

2 files changed

+205
-202
lines changed

2 files changed

+205
-202
lines changed
Lines changed: 74 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,100 @@
11
""" pyplots.ai
22
qrcode-basic: Basic QR Code Generator
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2026-01-07
3+
Library: altair 6.0.0 | Python 3.14.3
4+
Quality: 91/100 | Updated: 2026-04-07
55
"""
66

77
import altair as alt
88
import numpy as np
99
import pandas as pd
10+
import qrcode
1011

1112

12-
# Data - Generate QR code pattern (Version 1, 21x21 modules)
13-
# Creates a proper QR code structure with finder patterns
14-
np.random.seed(42)
15-
16-
size = 21 # Version 1 QR code size
17-
quiet_zone = 4 # Standard quiet zone (white border)
18-
total_size = size + 2 * quiet_zone
19-
20-
# Initialize matrix with white (0)
21-
matrix = np.zeros((total_size, total_size), dtype=int)
22-
23-
# Add finder pattern at top-left
24-
for i in range(7):
25-
matrix[quiet_zone + i, quiet_zone] = 1
26-
matrix[quiet_zone + i, quiet_zone + 6] = 1
27-
matrix[quiet_zone, quiet_zone + i] = 1
28-
matrix[quiet_zone + 6, quiet_zone + i] = 1
29-
for i in range(2, 5):
30-
for j in range(2, 5):
31-
matrix[quiet_zone + i, quiet_zone + j] = 1
32-
33-
# Add finder pattern at top-right
34-
tr_col = quiet_zone + size - 7
35-
for i in range(7):
36-
matrix[quiet_zone + i, tr_col] = 1
37-
matrix[quiet_zone + i, tr_col + 6] = 1
38-
matrix[quiet_zone, tr_col + i] = 1
39-
matrix[quiet_zone + 6, tr_col + i] = 1
40-
for i in range(2, 5):
41-
for j in range(2, 5):
42-
matrix[quiet_zone + i, tr_col + j] = 1
43-
44-
# Add finder pattern at bottom-left
45-
bl_row = quiet_zone + size - 7
46-
for i in range(7):
47-
matrix[bl_row + i, quiet_zone] = 1
48-
matrix[bl_row + i, quiet_zone + 6] = 1
49-
matrix[bl_row, quiet_zone + i] = 1
50-
matrix[bl_row + 6, quiet_zone + i] = 1
51-
for i in range(2, 5):
52-
for j in range(2, 5):
53-
matrix[bl_row + i, quiet_zone + j] = 1
54-
55-
# Add timing patterns (alternating black/white lines between finder patterns)
56-
for i in range(8, size - 8):
57-
matrix[quiet_zone + 6, quiet_zone + i] = (i + 1) % 2
58-
matrix[quiet_zone + i, quiet_zone + 6] = (i + 1) % 2
59-
60-
# Add dark module (always black, required in QR codes)
61-
matrix[quiet_zone + size - 8, quiet_zone + 8] = 1
62-
63-
# Fill data area with deterministic pattern representing encoded data
13+
# Data - Generate a real, scannable QR code encoding "https://pyplots.ai"
6414
content = "https://pyplots.ai"
65-
hash_val = hash(content)
66-
for row in range(total_size):
67-
for col in range(total_size):
68-
qr_row = row - quiet_zone
69-
qr_col = col - quiet_zone
70-
71-
# Skip quiet zone
72-
if row < quiet_zone or row >= total_size - quiet_zone:
73-
continue
74-
if col < quiet_zone or col >= total_size - quiet_zone:
75-
continue
76-
77-
# Skip finder patterns and separators
78-
if qr_row < 9 and qr_col < 9:
79-
continue
80-
if qr_row < 9 and qr_col >= size - 8:
81-
continue
82-
if qr_row >= size - 8 and qr_col < 9:
83-
continue
84-
85-
# Skip timing patterns
86-
if qr_row == 6 or qr_col == 6:
87-
continue
8815

89-
# Create deterministic pattern from position and content hash
90-
idx = row * total_size + col
91-
matrix[row, col] = ((hash_val >> (idx % 32)) ^ (idx * 7)) % 2
92-
93-
# Convert matrix to DataFrame for Altair
94-
data = []
95-
for row in range(total_size):
96-
for col in range(total_size):
97-
data.append({"x": col, "y": total_size - 1 - row, "value": matrix[row, col]})
98-
df = pd.DataFrame(data)
99-
100-
# Create QR code visualization using mark_rect
16+
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=1, border=0)
17+
qr.add_data(content)
18+
qr.make(fit=True)
19+
20+
# Convert QR code to numpy matrix and add quiet zone
21+
qr_matrix = np.array(qr.get_matrix(), dtype=int)
22+
quiet_zone = 4
23+
padded = np.zeros((qr_matrix.shape[0] + 2 * quiet_zone, qr_matrix.shape[1] + 2 * quiet_zone), dtype=int)
24+
padded[quiet_zone : quiet_zone + qr_matrix.shape[0], quiet_zone : quiet_zone + qr_matrix.shape[1]] = qr_matrix
25+
total_size = padded.shape[0]
26+
27+
# Build DataFrame with region classification and color encoding
28+
rows, cols = np.where(np.ones_like(padded, dtype=bool))
29+
region_colors = {"Finder Pattern": "#1B3A5C", "Timing Pattern": "#2A7F62", "Data": "#306998", "Quiet Zone": "#FFFFFF"}
30+
records = []
31+
for r, c in zip(rows, cols, strict=True):
32+
qr_r, qr_c = r - quiet_zone, c - quiet_zone
33+
if qr_r < 0 or qr_r >= qr_matrix.shape[0] or qr_c < 0 or qr_c >= qr_matrix.shape[1]:
34+
region = "Quiet Zone"
35+
elif (
36+
(qr_r < 7 and qr_c < 7)
37+
or (qr_r < 7 and qr_c >= qr_matrix.shape[1] - 7)
38+
or (qr_r >= qr_matrix.shape[0] - 7 and qr_c < 7)
39+
):
40+
region = "Finder Pattern"
41+
elif qr_r == 6 or qr_c == 6:
42+
region = "Timing Pattern"
43+
else:
44+
region = "Data"
45+
val = padded[r, c]
46+
display_region = region if val == 1 else "Background"
47+
records.append({"x": c, "y": total_size - 1 - r, "value": val, "region": region, "display": display_region})
48+
49+
data = pd.DataFrame(records)
50+
51+
# Color mapping: dark modules get region-specific colors, background is white
52+
display_domain = ["Finder Pattern", "Timing Pattern", "Data", "Background"]
53+
display_colors = ["#1B3A5C", "#2A7F62", "#306998", "#FFFFFF"]
54+
55+
# Plot - QR code with color-encoded structural regions
10156
chart = (
102-
alt.Chart(df)
103-
.mark_rect(stroke=None)
57+
alt.Chart(data)
58+
.mark_rect(stroke=None, strokeWidth=0)
10459
.encode(
10560
x=alt.X("x:O", axis=None),
10661
y=alt.Y("y:O", axis=None),
107-
color=alt.Color("value:N", scale=alt.Scale(domain=[0, 1], range=["#FFFFFF", "#000000"]), legend=None),
62+
color=alt.Color(
63+
"display:N",
64+
scale=alt.Scale(domain=display_domain, range=display_colors),
65+
legend=alt.Legend(
66+
title="QR Code Regions",
67+
titleFontSize=16,
68+
labelFontSize=14,
69+
orient="bottom",
70+
direction="horizontal",
71+
values=["Finder Pattern", "Timing Pattern", "Data"],
72+
),
73+
),
74+
tooltip=[
75+
alt.Tooltip("region:N", title="Region"),
76+
alt.Tooltip("x:O", title="Col"),
77+
alt.Tooltip("y:O", title="Row"),
78+
],
10879
)
10980
.properties(
11081
width=800,
11182
height=800,
112-
title=alt.Title("qrcode-basic · altair · pyplots.ai", fontSize=28, anchor="middle", dy=-10),
83+
title=alt.Title(
84+
"qrcode-basic · altair · pyplots.ai",
85+
subtitle="Structural regions: Finder Patterns (navy) for orientation · Timing Patterns (green) for alignment · Data modules (blue)",
86+
fontSize=28,
87+
subtitleFontSize=16,
88+
subtitleColor="#555555",
89+
anchor="middle",
90+
dy=-10,
91+
),
11392
)
11493
.configure_view(strokeWidth=0)
11594
.configure_axis(grid=False)
95+
.configure_legend(symbolStrokeWidth=0, padding=20)
11696
)
11797

118-
# Save as PNG (square format: 3600 x 3600 px with scale_factor)
98+
# Save
11999
chart.save("plot.png", scale_factor=4.5)
120-
121-
# Save interactive HTML version
122100
chart.save("plot.html")

0 commit comments

Comments
 (0)