diff --git a/plots/bubble-packed/implementations/matplotlib.py b/plots/bubble-packed/implementations/matplotlib.py index 71e2c2a196..372b099446 100644 --- a/plots/bubble-packed/implementations/matplotlib.py +++ b/plots/bubble-packed/implementations/matplotlib.py @@ -1,16 +1,17 @@ """ pyplots.ai bubble-packed: Basic Packed Bubble Chart -Library: matplotlib 3.10.8 | Python 3.13.11 -Quality: 92/100 | Created: 2025-12-23 +Library: matplotlib 3.10.8 | Python 3.14.3 +Quality: 87/100 | Updated: 2026-02-23 """ +import matplotlib.collections as mcoll import matplotlib.patches as mpatches +import matplotlib.patheffects as pe import matplotlib.pyplot as plt import numpy as np # Data - Department budget allocation (in thousands) -np.random.seed(42) labels = [ "Engineering", "Marketing", @@ -28,136 +29,235 @@ "Security", "QA", ] -values = [850, 420, 680, 320, 180, 290, 750, 210, 150, 380, 240, 550, 460, 170, 195] - -# Colors by group (Python Blue primary, Yellow secondary, others colorblind-safe) -colors = [ - "#306998", # Engineering - Blue (Tech) - "#FFD43B", # Marketing - Yellow (Business) - "#306998", # Sales - Blue (Revenue) - "#4A90A4", # Operations - Teal (Support) - "#4A90A4", # HR - Teal (Support) - "#4A90A4", # Finance - Teal (Support) - "#FFD43B", # R&D - Yellow (Innovation) - "#4A90A4", # Customer Support - Teal (Support) - "#7B9E89", # Legal - Sage (Compliance) - "#306998", # IT - Blue (Tech) - "#FFD43B", # Design - Yellow (Creative) - "#306998", # Product - Blue (Tech) - "#FFD43B", # Data Science - Yellow (Analytics) - "#7B9E89", # Security - Sage (Compliance) - "#7B9E89", # QA - Sage (Quality) -] +values = [950, 420, 680, 310, 160, 280, 820, 200, 130, 370, 230, 580, 470, 145, 175] + +# Group assignments - realistic organizational structure +group_map = { + "Engineering": "Engineering", + "IT": "Engineering", + "Data Science": "Engineering", + "R&D": "Engineering", + "Marketing": "Business", + "Sales": "Business", + "Product": "Business", + "Design": "Business", + "Operations": "Operations", + "HR": "Operations", + "Finance": "Operations", + "Customer Support": "Operations", + "Legal": "Compliance", + "Security": "Compliance", + "QA": "Compliance", +} + +# Colorblind-safe palette with high hue separation +group_colors = {"Engineering": "#306998", "Business": "#E8C33A", "Operations": "#D4654A", "Compliance": "#8B6DB0"} +colors = [group_colors[group_map[label]] for label in labels] # Scale values to radius (sqrt for area-proportional sizing) -min_radius = 0.35 -max_radius = 1.9 -values_array = np.array(values) +min_radius = 0.30 +max_radius = 2.0 +values_array = np.array(values, dtype=float) radii = min_radius + (max_radius - min_radius) * np.sqrt( (values_array - values_array.min()) / (values_array.max() - values_array.min()) ) -# Circle packing using physics simulation -n = len(labels) - -# Initial positions in grid -grid_size = int(np.ceil(np.sqrt(n))) -positions = np.zeros((n, 2)) -for i in range(n): - positions[i] = [(i % grid_size) * 4 - grid_size * 2, (i // grid_size) * 4 - grid_size * 2] - # Sort by size (largest first) for better packing +n = len(labels) order = np.argsort(-radii) -positions = positions[order] radii_sorted = radii[order] labels_sorted = [labels[i] for i in order] values_sorted = [values[i] for i in order] colors_sorted = [colors[i] for i in order] +groups_sorted = [group_map[labels[i]] for i in order] -# Physics simulation for packing -for iteration in range(350): - # Pull toward center with decreasing strength - pull_strength = 0.06 * (1 - iteration / 400) +# Assign group IDs for clustering +unique_groups = list(group_colors.keys()) +group_ids = np.array([unique_groups.index(g) for g in groups_sorted]) + +# Initial positions in spiral pattern for tighter convergence +angles = np.linspace(0, 4 * np.pi, n) +spiral_r = np.linspace(0, 3, n) +positions = np.column_stack([spiral_r * np.cos(angles), spiral_r * np.sin(angles)]) + +# Physics simulation with group-aware clustering +for iteration in range(500): + progress = iteration / 500 + pull_strength = 0.06 * (1 - progress * 0.8) + group_pull = 0.04 * (1 - progress * 0.5) + + # Compute group centers of mass + group_centers = {} + for gid in range(len(unique_groups)): + mask = group_ids == gid + if np.any(mask): + group_centers[gid] = positions[mask].mean(axis=0) + + # Pull toward center + pull toward own group center for i in range(n): - dist = np.sqrt(positions[i, 0] ** 2 + positions[i, 1] ** 2) + dist = np.linalg.norm(positions[i]) if dist > 0.01: positions[i] -= pull_strength * positions[i] / dist + gc = group_centers[group_ids[i]] + to_group = gc - positions[i] + gd = np.linalg.norm(to_group) + if gd > 0.01: + positions[i] += group_pull * to_group / gd + # Push apart overlapping circles for i in range(n): for j in range(i + 1, n): - dx = positions[j, 0] - positions[i, 0] - dy = positions[j, 1] - positions[i, 1] - dist = np.sqrt(dx**2 + dy**2) - min_dist = radii_sorted[i] + radii_sorted[j] + 0.05 # Small gap between circles + delta = positions[j] - positions[i] + dist = np.linalg.norm(delta) + same_group = group_ids[i] == group_ids[j] + gap = 0.04 if same_group else 0.15 + min_dist = radii_sorted[i] + radii_sorted[j] + gap if dist < min_dist and dist > 0.001: overlap = (min_dist - dist) / 2 - dx_norm = dx / dist - dy_norm = dy / dist - positions[i, 0] -= overlap * dx_norm - positions[i, 1] -= overlap * dy_norm - positions[j, 0] += overlap * dx_norm - positions[j, 1] += overlap * dy_norm - -# Create plot (4800x2700 px at 300 dpi) + direction = delta / dist + positions[i] -= overlap * direction + positions[j] += overlap * direction + +# Center the layout: shift all positions so the bounding box is centered at origin +bbox_min = positions.min(axis=0) - radii_sorted.max() +bbox_max = positions.max(axis=0) + radii_sorted.max() +positions -= (bbox_min + bbox_max) / 2 + +# Plot (4800x2700 px at 300 dpi) fig, ax = plt.subplots(figsize=(16, 9)) -# Draw circles +# Draw circles using PatchCollection for efficient rendering +circles = [] +face_colors = [] for i in range(n): - circle = mpatches.Circle( - (positions[i, 0], positions[i, 1]), - radii_sorted[i], - facecolor=colors_sorted[i], - edgecolor="white", - linewidth=2.5, - alpha=0.88, - ) - ax.add_patch(circle) + circle = mpatches.Circle((positions[i, 0], positions[i, 1]), radii_sorted[i]) + circles.append(circle) + face_colors.append(colors_sorted[i]) - # Add labels inside larger circles - label_len = len(labels_sorted[i]) - min_radius_for_label = 0.55 + label_len * 0.025 - if radii_sorted[i] > min_radius_for_label: +collection = mcoll.PatchCollection( + circles, facecolors=face_colors, edgecolors="white", linewidths=2.5, alpha=0.90, zorder=2 +) +ax.add_collection(collection) + +# Add labels inside circles that are large enough, external labels for small ones +small_circles = [] +for i in range(n): + label_chars = len(labels_sorted[i]) + min_r_for_label = 0.48 + label_chars * 0.018 + if radii_sorted[i] > min_r_for_label: font_scale = min(1.0, radii_sorted[i] / 1.4) - label_fontsize = max(9, int(15 * font_scale)) - value_fontsize = max(8, int(13 * font_scale)) + label_fontsize = max(12, int(15 * font_scale)) + value_fontsize = max(12, int(13 * font_scale)) + + # Determine text color based on background luminance (WCAG relative luminance) + bg_color = colors_sorted[i] + rgb = [int(bg_color[j : j + 2], 16) / 255 for j in (1, 3, 5)] + luminance = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] + text_color = "#1a1a2e" if luminance > 0.45 else "white" + text_outline = ( + pe.withStroke(linewidth=3, foreground="#00000033") + if luminance <= 0.45 + else pe.withStroke(linewidth=3, foreground="#ffffff33") + ) + + # Wrap long labels for smaller circles + display_label = labels_sorted[i] + is_wrapped = False + if " " in display_label and radii_sorted[i] < 1.0: + display_label = display_label.replace(" ", "\n") + is_wrapped = True + + # Adjust vertical offsets for wrapped vs single-line labels + label_y_offset = 0.05 if is_wrapped else 0.12 + value_y_offset = -0.35 if is_wrapped else -0.22 + ax.text( positions[i, 0], - positions[i, 1] + radii_sorted[i] * 0.1, - labels_sorted[i], + positions[i, 1] + radii_sorted[i] * label_y_offset, + display_label, ha="center", va="center", fontsize=label_fontsize, fontweight="bold", - color="white", + color=text_color, + path_effects=[text_outline], + zorder=3, ) ax.text( positions[i, 0], - positions[i, 1] - radii_sorted[i] * 0.22, + positions[i, 1] + radii_sorted[i] * value_y_offset, f"${values_sorted[i]}K", ha="center", va="center", fontsize=value_fontsize, - color="white", - alpha=0.95, + color=text_color, + alpha=0.85, + path_effects=[text_outline], + zorder=3, ) + else: + small_circles.append(i) -# Set axis limits with padding +# External labels with leader lines for small circles +for i in small_circles: + cx, cy = positions[i, 0], positions[i, 1] + r = radii_sorted[i] + + # Find direction away from center for label placement + angle = np.arctan2(cy, cx) + offset_dist = r + 0.6 + lx = cx + offset_dist * np.cos(angle) + ly = cy + offset_dist * np.sin(angle) + + ax.annotate( + f"{labels_sorted[i]}\n${values_sorted[i]}K", + xy=(cx, cy), + xytext=(lx, ly), + fontsize=12, + fontweight="bold", + color="#333333", + ha="center", + va="center", + arrowprops={"arrowstyle": "-", "color": "#666666", "lw": 1.2, "shrinkA": 0, "shrinkB": 2}, + zorder=4, + ) + +# Axis limits with padding all_x = positions[:, 0] all_y = positions[:, 1] max_r = radii_sorted.max() -padding = 0.6 +padding = 0.9 ax.set_xlim(all_x.min() - max_r - padding, all_x.max() + max_r + padding) ax.set_ylim(all_y.min() - max_r - padding, all_y.max() + max_r + padding) ax.set_aspect("equal") - -# Remove axes for clean visualization ax.axis("off") -# Title +# Title with total budget subtitle for context +total_budget = sum(values) ax.set_title( - "Department Budget Allocation · bubble-packed · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20 + f"Department Budget Allocation (${total_budget / 1000:.1f}M Total)\nbubble-packed · matplotlib · pyplots.ai", + fontsize=24, + fontweight="bold", + pad=20, +) + +# Legend for group colors +legend_handles = [ + mpatches.Patch(facecolor=color, edgecolor="white", linewidth=1.5, label=group) + for group, color in group_colors.items() +] +ax.legend( + handles=legend_handles, + loc="lower right", + fontsize=16, + framealpha=0.9, + edgecolor="#cccccc", + fancybox=True, + borderpad=0.8, + handlelength=1.5, + handleheight=1.2, ) plt.tight_layout() diff --git a/plots/bubble-packed/metadata/matplotlib.yaml b/plots/bubble-packed/metadata/matplotlib.yaml index 78a38059c7..6c222e6801 100644 --- a/plots/bubble-packed/metadata/matplotlib.yaml +++ b/plots/bubble-packed/metadata/matplotlib.yaml @@ -1,158 +1,191 @@ library: matplotlib specification_id: bubble-packed created: '2025-12-23T09:16:10Z' -updated: '2025-12-23T09:18:25Z' -generated_by: claude-opus-4-5-20251101 +updated: '2026-02-23T16:23:51Z' +generated_by: claude-opus-4-6 workflow_run: 20456558183 issue: 0 -python_version: 3.13.11 +python_version: 3.14.3 library_version: 3.10.8 preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/matplotlib/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/matplotlib/plot_thumb.png preview_html: null -quality_score: 92 +quality_score: 87 impl_tags: dependencies: [] techniques: + - annotations - patches + - custom-legend patterns: - data-generation - - matrix-construction - dataprep: [] + - iteration-over-groups + dataprep: + - normalization styling: - - alpha-blending - minimal-chrome + - alpha-blending + - edge-highlighting review: strengths: - - Excellent physics-based circle packing algorithm with 350 iterations ensuring - no overlap - - Area-proportional sizing using sqrt scaling for accurate visual perception - - Clean, colorblind-safe palette with logical grouping by department function - - Smart label placement - only shows labels in circles large enough to fit them - - Professional title format following pyplots.ai convention - - Realistic department budget data scenario + - Excellent data storytelling with realistic department budget theme and organizational + group clustering + - WCAG-aware text contrast switching based on background luminance + - PatchCollection for efficient batch circle rendering is idiomatic matplotlib + - Physics simulation with group-aware clustering produces well-organized layout + - Path effects on text ensure readability against varied background colors weaknesses: - - Missing a color legend explaining the groupings (Tech, Support, Business, Compliance) - - Some smaller bubbles (Legal, Security) are unlabeled and their departments are - unclear without a legend - - Customer Support bubble is not visible in the visualization - image_description: 'The plot displays a packed bubble chart representing "Department - Budget Allocation" with 15 circular bubbles of varying sizes. The largest bubbles - are Engineering ($850K), R&D ($750K), Sales ($680K), and Product ($550K). The - bubbles are color-coded into four groups: blue (Tech: Engineering, Sales, IT, - Product), yellow (Business/Creative: Marketing, R&D, Design, Data Science), teal - (Support: Operations, HR, Finance, Customer Support), and sage green (Compliance/Quality: - Legal, Security, QA). Larger bubbles display department names in bold white text - with values below. The bubbles are tightly packed without overlapping, demonstrating - the physics simulation. The title reads "Department Budget Allocation · bubble-packed - · matplotlib · pyplots.ai" in bold at the top. The background is white with axes - turned off for a clean visualization.' + - Center area crowding where smaller circles pack tightly making labels harder to + read + - Layout is somewhat bottom-heavy due to Engineering circle dominating the lower + portion + - Smallest circle labels at 12pt are below ideal size for this canvas resolution + image_description: 'The plot shows a packed bubble chart titled "Department Budget + Allocation ($5.9M Total)" with subtitle "bubble-packed · matplotlib · pyplots.ai". + Fifteen circles of varying sizes represent department budgets ranging from $130K + (Legal) to $950K (Engineering). Circles are colored by organizational group: steel + blue (#306998) for Engineering group (Engineering, R&D, Data Science, IT), golden + yellow (#E8C33A) for Business group (Sales, Product, Marketing, Design), coral + red (#D4654A) for Operations group (Operations, Finance, Customer Support, HR), + and muted purple (#8B6DB0) for Compliance group (Legal, Security, QA). Circles + are packed together with visible group clustering — Engineering-related departments + cluster at the bottom, Business at the upper-left, Operations in the center, and + Compliance scattered among them. White edges separate circles cleanly. Labels + with department names and dollar values appear inside larger circles with bold + text and subtle path-effect outlines. Smaller circles (Legal, Security) have external + labels with leader lines. A legend in the lower right identifies the four groups. + The overall layout is clean with axes turned off and a white background.' criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 25 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 9 - max: 10 + score: 7 + max: 8 passed: true - comment: Title is 24pt and clear; labels inside bubbles are readable but some - smaller bubbles (HR, QA) have slightly cramped text + comment: Font sizes explicitly set throughout (title 24pt, labels 12-15pt, + legend 16pt). Smallest labels at 12pt below ideal 16pt guideline but acceptable + for packed bubble constraints. - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 4 + max: 6 passed: true - comment: Excellent bubble packing with no overlapping elements + comment: Some crowding in center cluster where smaller circles pack tightly. + Customer Support label cramped in small wrapped text. No severe unreadable + overlap. - id: VQ-03 name: Element Visibility - score: 8 - max: 8 + score: 5 + max: 6 passed: true - comment: Circle sizes well-adapted, proper area-based scaling using sqrt + comment: All 15 circles visible with area-proportional sizing. Smallest circles + are tiny but distinguishable. White edges and alpha=0.90 provide clean separation. - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Four distinct, colorblind-safe colors (blue, yellow, teal, sage) + comment: Four-color palette with strong hue separation. No red-green reliance. + WCAG-aware text contrast switching. - id: VQ-05 - name: Layout Balance - score: 4 - max: 5 + name: Layout & Canvas + score: 3 + max: 4 passed: true - comment: Good proportions, slight asymmetry but natural for packed bubbles - - id: VQ-07 - name: Grid & Legend + comment: Good canvas utilization (~60-70%). Layout somewhat bottom-heavy due + to largest circle placement. Content not cut off. + - id: VQ-06 + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: No grid needed; color grouping is implicit and clear + comment: Descriptive title with aggregate budget value. No axes needed for + packed bubble chart. + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: 'Strong design: custom four-color palette, WCAG text contrast, path + effect outlines, white circle edges. Clearly above library defaults.' + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Axes removed, white circle edges, alpha transparency, group spacing + differentiation, path effects on text. Minor center crowding. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Group clustering creates organizational narrative. Size variation + guides the eye. Title adds total budget context. Color grouping reveals + structure. spec_compliance: - score: 25 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct packed bubble chart with physics simulation - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Size correctly represents budget values - - id: SC-03 + comment: Correct packed bubble chart with physics simulation packing. + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Labels, values, color grouping all present - - id: SC-04 - name: Data Range + comment: 'All spec features: area-proportional sizing, physics simulation, + labels inside/outside, color encoding, group clustering.' + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All 15 departments visible and readable - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Color grouping is intuitive (no explicit legend needed) - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Values correctly mapped to circle area via sqrt scaling. All 15 items + represented. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: 'Correctly formatted: "{description} · bubble-packed · matplotlib - · pyplots.ai"' + comment: Title includes bubble-packed · matplotlib · pyplots.ai. Legend shows + all four groups with matching colors. data_quality: - score: 18 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows varying sizes, groupings, and labels; could show more extreme - size variation + comment: Wide range of sizes, four distinct groups, internal and external + labels, group clustering with spacing. - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Department budgets is a real, comprehensible business scenario + comment: Department budget allocation is a concrete neutral business scenario + with recognizable department names. - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Budget values ($150K-$850K) are realistic for departments + comment: Budget values $130K-$950K are realistic. Total $5.9M plausible for + mid-size tech company. code_quality: score: 10 max: 10 @@ -162,41 +195,51 @@ review: score: 3 max: 3 passed: true - comment: 'Clean sequential structure: imports → data → packing simulation - → plot → save' + comment: 'Linear structure: imports, data, scaling, physics simulation, plotting, + saving. No functions or classes.' - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Uses np.random.seed(42) + comment: 'Fully deterministic: hardcoded data, deterministic spiral positions, + no randomness in simulation.' - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only matplotlib.pyplot, matplotlib.patches, numpy used + comment: 'All five imports used: collections, patches, patheffects, pyplot, + numpy.' - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: All APIs current + comment: Complexity appropriate for packed bubble in matplotlib. Well-commented, + no over-engineering. - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png - library_features: - score: 3 - max: 5 + comment: Saves as plot.png at 300 DPI. No deprecated API usage. + library_mastery: + score: 7 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Good use of PatchCollection, mpatches.Circle, ax.annotate with arrowprops, + path_effects. OO API throughout. + - id: LM-02 + name: Distinctive Features score: 3 max: 5 passed: true - comment: Uses matplotlib patches (Circle) and custom physics simulation; could - leverage matplotlib.collections for efficiency - verdict: APPROVED + comment: PatchCollection for batch rendering, patheffects for text outlines, + annotate with arrowprops. Distinctive matplotlib features. + verdict: REJECTED diff --git a/plots/bubble-packed/specification.yaml b/plots/bubble-packed/specification.yaml index 64027537f4..48e8112bee 100644 --- a/plots/bubble-packed/specification.yaml +++ b/plots/bubble-packed/specification.yaml @@ -6,7 +6,7 @@ title: Basic Packed Bubble Chart # Specification tracking created: 2025-12-15T20:43:45Z -updated: 2025-12-15T20:43:45Z +updated: 2026-02-23T12:00:00Z issue: 992 suggested: MarkusNeusinger @@ -15,13 +15,15 @@ tags: plot_type: - bubble - packed + - circlepacking data_type: - categorical - numeric + - proportional domain: - general - business features: - basic - comparison - - hierarchical + - proportional