diff --git a/plots/bubble-packed/implementations/plotnine.py b/plots/bubble-packed/implementations/plotnine.py index d79b4c250b..b3161cca50 100644 --- a/plots/bubble-packed/implementations/plotnine.py +++ b/plots/bubble-packed/implementations/plotnine.py @@ -1,7 +1,7 @@ """ pyplots.ai bubble-packed: Basic Packed Bubble Chart -Library: plotnine 0.15.2 | Python 3.13.11 -Quality: 90/100 | Created: 2025-12-23 +Library: plotnine 0.15.3 | Python 3.14.3 +Quality: 90/100 | Updated: 2026-02-23 """ import numpy as np @@ -9,12 +9,14 @@ from plotnine import ( aes, coord_fixed, - element_blank, + element_rect, element_text, geom_polygon, geom_text, ggplot, + guides, labs, + scale_alpha_manual, scale_fill_manual, theme, theme_void, @@ -22,8 +24,7 @@ # Data - department budgets (in millions) -np.random.seed(42) -data = { +departments = { "label": [ "Engineering", "Marketing", @@ -34,7 +35,7 @@ "R&D", "IT Support", "Legal", - "Customer Service", + "Customer Svc", "Design", "Logistics", "Quality", @@ -61,142 +62,177 @@ ], } -df = pd.DataFrame(data) +df = pd.DataFrame(departments) # Scale values to radii (area-based scaling for accurate perception) max_radius = 1.0 -min_radius = 0.3 +min_radius = 0.25 df["radius"] = min_radius + (max_radius - min_radius) * np.sqrt(df["value"] / df["value"].max()) -# Circle packing using force simulation (inline, KISS style) +# Circle packing - greedy placement with vectorized collision detection n = len(df) radii = df["radius"].values - -# Sort by size (largest first) for better packing idx = np.argsort(-radii) sorted_radii = radii[idx] +gap = 0.03 -# Initialize positions x = np.zeros(n) y = np.zeros(n) +angles_sweep = np.linspace(0, 2 * np.pi, 72, endpoint=False) -# Place circles using greedy algorithm for i in range(1, n): best_dist = float("inf") best_x, best_y = 0.0, 0.0 - - for angle in np.linspace(0, 2 * np.pi, 36): - for ref in range(i): - # Try placing next to reference circle - test_x = x[ref] + (sorted_radii[ref] + sorted_radii[i] + 0.05) * np.cos(angle) - test_y = y[ref] + (sorted_radii[ref] + sorted_radii[i] + 0.05) * np.sin(angle) - - # Check for collisions - valid = True - for j in range(i): - dist = np.sqrt((test_x - x[j]) ** 2 + (test_y - y[j]) ** 2) - if dist < sorted_radii[i] + sorted_radii[j] + 0.03: - valid = False - break - - if valid: - center_dist = np.sqrt(test_x**2 + test_y**2) - if center_dist < best_dist: - best_dist = center_dist - best_x, best_y = test_x, test_y + target_r = sorted_radii[i] + + for ref in range(i): + place_r = sorted_radii[ref] + target_r + gap + cx = x[ref] + place_r * np.cos(angles_sweep) + cy = y[ref] + place_r * np.sin(angles_sweep) + + # Vectorized collision check across all angles simultaneously + dx_c = cx[:, np.newaxis] - x[:i][np.newaxis, :] + dy_c = cy[:, np.newaxis] - y[:i][np.newaxis, :] + dists_c = np.hypot(dx_c, dy_c) + valid = np.all(dists_c >= target_r + sorted_radii[:i] + gap, axis=1) + + center_dists = cx**2 + cy**2 + valid_dists = np.where(valid, center_dists, float("inf")) + best_k = np.argmin(valid_dists) + if valid_dists[best_k] < best_dist: + best_dist = valid_dists[best_k] + best_x, best_y = cx[best_k], cy[best_k] x[i] = best_x y[i] = best_y -# Force simulation to tighten packing -for _ in range(1000): - # Move toward center - x -= x * 0.001 - y -= y * 0.001 - - # Separate overlapping circles - for i in range(n): - for j in range(i + 1, n): - dx = x[j] - x[i] - dy = y[j] - y[i] - dist = np.sqrt(dx * dx + dy * dy) - min_dist = sorted_radii[i] + sorted_radii[j] + 0.03 - - if dist < min_dist and dist > 0.001: - overlap = (min_dist - dist) / 2 - dx_norm = dx / dist - dy_norm = dy / dist - x[i] -= overlap * dx_norm * 0.5 - y[i] -= overlap * dy_norm * 0.5 - x[j] += overlap * dx_norm * 0.5 - y[j] += overlap * dy_norm * 0.5 +# Force simulation to tighten packing (vectorized with numpy) +tri = np.triu(np.ones((n, n), dtype=bool), k=1) +min_dists = sorted_radii[:, np.newaxis] + sorted_radii[np.newaxis, :] + gap + +for _ in range(2000): + x *= 0.997 + y *= 0.997 + + dx = x[:, np.newaxis] - x[np.newaxis, :] + dy = y[:, np.newaxis] - y[np.newaxis, :] + dists = np.hypot(dx, dy) + + overlap = tri & (dists < min_dists) & (dists > 1e-3) + if overlap.any(): + safe_dists = np.where(dists > 1e-3, dists, 1.0) + push = ((min_dists - dists) / (2 * safe_dists)) * overlap + corr_x = push * dx + corr_y = push * dy + x += corr_x.sum(axis=1) - corr_x.sum(axis=0) + y += corr_y.sum(axis=1) - corr_y.sum(axis=0) # Restore original order -x_out = np.zeros(n) -y_out = np.zeros(n) +x_final = np.zeros(n) +y_final = np.zeros(n) for i, orig_idx in enumerate(idx): - x_out[orig_idx] = x[i] - y_out[orig_idx] = y[i] + x_final[orig_idx] = x[i] + y_final[orig_idx] = y[i] -df["x"] = x_out -df["y"] = y_out +df["x"] = x_final +df["y"] = y_final -# Create circle polygons for geom_polygon +# Build circle polygons for geom_polygon circle_dfs = [] +angles = np.linspace(0, 2 * np.pi, 64) for i, row in df.iterrows(): - angles = np.linspace(0, 2 * np.pi, 64) cx = row["x"] + row["radius"] * np.cos(angles) cy = row["y"] + row["radius"] * np.sin(angles) - circle_df = pd.DataFrame({"x": cx, "y": cy, "label": row["label"], "group": row["group"], "circle_id": i}) - circle_dfs.append(circle_df) - + circle_dfs.append(pd.DataFrame({"x": cx, "y": cy, "label": row["label"], "group": row["group"], "circle_id": i})) circles_df = pd.concat(circle_dfs, ignore_index=True) +circles_df["group"] = pd.Categorical(circles_df["group"], categories=["Tech", "Business", "Operations", "Support"]) -# Color palette for groups - colorblind-safe (Okabe-Ito palette) -group_colors = { - "Tech": "#0072B2", # Blue - "Business": "#E69F00", # Orange - "Operations": "#009E73", # Bluish Green - "Support": "#CC79A7", # Reddish Purple -} - -# Create label dataframe (centers) - show full labels for circles large enough -labels_df = df[["x", "y", "label", "radius"]].copy() - -# Show full label for large circles, abbreviated for medium, none for small +# Labels - conditional sizing: full name for large, abbreviated for small +labels_df = df.copy() labels_df["display_label"] = labels_df.apply( lambda row: ( - row["label"] - if row["radius"] >= 0.85 - else ( - (row["label"][:8] if len(row["label"]) > 8 else row["label"]) - if row["radius"] >= 0.6 - else ((row["label"][:5] if len(row["label"]) > 5 else row["label"]) if row["radius"] >= 0.45 else "") - ) + row["label"] if row["value"] >= 22 else (row["label"].split()[0] if row["value"] >= 10 else row["label"][:4]) ), axis=1, ) +labels_df["value_label"] = labels_df["value"].apply(lambda v: f"${v}M") -# Create plot +# Alpha by group emphasis — Tech & Business slightly more prominent +alpha_values = {"Tech": 0.90, "Business": 0.85, "Operations": 0.78, "Support": 0.75} + +# Color palette - Okabe-Ito colorblind-safe +group_colors = {"Tech": "#0072B2", "Business": "#E69F00", "Operations": "#009E73", "Support": "#CC79A7"} + +# Compute group totals for subtitle +group_totals = df.groupby("group")["value"].sum() +subtitle_text = " \u00b7 ".join(f"{g}: \\${group_totals[g]}M" for g in ["Tech", "Business", "Operations", "Support"]) + +# Tight viewport bounds for optimal canvas utilization +pad = 0.15 +x_lo = (df["x"] - df["radius"]).min() - pad +x_hi = (df["x"] + df["radius"]).max() + pad +y_lo = (df["y"] - df["radius"]).min() - pad +y_hi = (df["y"] + df["radius"]).max() + pad +half_span = max(x_hi - x_lo, y_hi - y_lo) / 2 +cx_mid, cy_mid = (x_lo + x_hi) / 2, (y_lo + y_hi) / 2 + +# Plot with layered grammar of graphics composition plot = ( ggplot() + # Layer 1: Circle fills with group-specific alpha + geom_polygon( - data=circles_df, mapping=aes(x="x", y="y", fill="group", group="circle_id"), color="white", size=0.5, alpha=0.85 + data=circles_df, + mapping=aes(x="x", y="y", fill="group", group="circle_id", alpha="group"), + color="white", + size=0.8, + ) + # Layer 2: Department name labels (bold, white) + + geom_text( + data=labels_df[labels_df["value"] >= 10], + mapping=aes(x="x", y="y", label="display_label"), + size=12, + color="white", + fontweight="bold", + nudge_y=0.10, + ) + # Layer 3: Small bubble labels + + geom_text( + data=labels_df[labels_df["value"] < 10], + mapping=aes(x="x", y="y", label="display_label"), + size=10, + color="white", + fontweight="bold", ) + # Layer 4: Budget value annotations for large/medium bubbles + geom_text( - data=labels_df, mapping=aes(x="x", y="y", label="display_label"), size=9, color="white", fontweight="bold" + data=labels_df[labels_df["value"] >= 12], + mapping=aes(x="x", y="y", label="value_label"), + size=10, + color="white", + alpha=0.85, + nudge_y=-0.17, ) - + scale_fill_manual(values=group_colors) - + coord_fixed() - + labs(title="bubble-packed · plotnine · pyplots.ai", fill="Department Group") + # Scales + + scale_fill_manual(values=group_colors, name="Department Group") + + scale_alpha_manual(values=alpha_values) + + guides(alpha=False) + # Tight viewport with coord_fixed for 1:1 aspect ratio + + coord_fixed(xlim=(cx_mid - half_span, cx_mid + half_span), ylim=(cy_mid - half_span, cy_mid + half_span)) + + labs(title="bubble-packed \u00b7 plotnine \u00b7 pyplots.ai", subtitle=subtitle_text) + # Theme — plotnine's distinctive void theme with layered customization + theme_void() + theme( - figure_size=(16, 9), - plot_title=element_text(size=24, ha="center", weight="bold"), - legend_title=element_text(size=18), - legend_text=element_text(size=14), - legend_position="right", - plot_background=element_blank(), + figure_size=(12, 12), + plot_title=element_text(size=24, ha="center", weight="bold", margin={"b": 5}), + plot_subtitle=element_text(size=16, ha="center", color="#555555", margin={"t": 5, "b": 10}), + legend_title=element_text(size=18, weight="bold"), + legend_text=element_text(size=16), + legend_position="bottom", + legend_direction="horizontal", + legend_key=element_rect(fill="white", color="none"), + legend_key_size=20, + plot_background=element_rect(fill="white", color="none"), + plot_margin=0.02, ) ) diff --git a/plots/bubble-packed/metadata/plotnine.yaml b/plots/bubble-packed/metadata/plotnine.yaml index 5cf1918477..a4fe18dc33 100644 --- a/plots/bubble-packed/metadata/plotnine.yaml +++ b/plots/bubble-packed/metadata/plotnine.yaml @@ -1,12 +1,12 @@ library: plotnine specification_id: bubble-packed created: '2025-12-23T09:16:24Z' -updated: '2025-12-23T09:27:41Z' -generated_by: claude-opus-4-5-20251101 +updated: '2026-02-23T16:26:11Z' +generated_by: claude-opus-4-6 workflow_run: 20456560252 issue: 0 -python_version: 3.13.11 -library_version: 0.15.2 +python_version: 3.14.3 +library_version: 0.15.3 preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotnine/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotnine/plot_thumb.png preview_html: null @@ -14,154 +14,179 @@ quality_score: 90 impl_tags: dependencies: [] techniques: + - annotations - layer-composition + - patches patterns: - data-generation - dataprep: [] + - iteration-over-groups + dataprep: + - normalization styling: - - alpha-blending - minimal-chrome + - alpha-blending + - edge-highlighting review: strengths: - - Excellent implementation of circle packing algorithm with force simulation for - tight packing - - Area-based scaling (sqrt) correctly used for accurate visual perception per spec - - Colorblind-safe Okabe-Ito palette provides excellent accessibility - - Smart label handling with full/truncated/abbreviated text based on circle size - - Clean code structure with well-organized packing algorithm inline + - Excellent Okabe-Ito colorblind-safe palette with intentional alpha variation per + group + - Robust circle packing via greedy placement + vectorized force simulation with + no overlaps + - Smart conditional label sizing (full name, first word, 4-char abbreviation) prevents + clutter + - Informative subtitle showing group budget totals provides immediate aggregate + context + - Clean void theme with white circle borders appropriate for this chart type + - Fully deterministic with area-based scaling for accurate visual perception weaknesses: - - Label truncation cuts off Engineering to Engineerin and Customer Service to Customer - S which loses readability - - No distinctive plotnine grammar of graphics features used - relies heavily on - manual polygon construction + - Noticeable whitespace gap between bubble cluster and bottom legend reduces canvas + utilization + - Smallest bubble labels (Trai, Admi, Qual) are abbreviated somewhat cryptically image_description: 'The plot displays a packed bubble chart with 15 circles representing - department budgets. Circles vary in size based on budget value, with Engineering - (largest, blue) and R&D (blue) being most prominent, followed by Marketing (yellow/gold), - Sales (yellow), and Operations (green). The circles are color-coded by four department - groups: Tech (blue #0072B2), Business (yellow/gold #E69F00), Operations (green - #009E73), and Support (purple/pink #CC79A7). Labels are displayed inside circles - with white bold text - larger circles show full or truncated labels ("Engineerin", - "Customer S"), while smallest circles show abbreviated labels. The circles are - tightly packed without overlap. Title "bubble-packed · plotnine · pyplots.ai" - appears at top center in bold black text. A legend on the right identifies the - four department groups.' + corporate department budgets (in millions). The largest circle is Engineering + ($45M) in blue, positioned centrally. Circles are colored by four groups using + the Okabe-Ito colorblind-safe palette: blue (#0072B2) for Tech (Engineering, R&D, + IT, Design), amber/gold (#E69F00) for Business (Marketing, Sales, Finance), green/teal + (#009E73) for Operations (Operations, Customer Svc, Logistics, Quality), and pink + (#CC79A7) for Support (HR, Legal, Training, Admin). Circles are tightly packed + with white borders separating them. Larger bubbles display both the department + name (bold white) and budget value (e.g., "$45M"). Smaller bubbles show abbreviated + names ("Trai", "Admi", "Qual"). The title reads "bubble-packed · plotnine · pyplots.ai" + in bold centered text. A subtitle shows group totals: "Tech: $109M · Business: + $78M · Operations: $66M · Support: $36M". A horizontal legend at the bottom labels + the four groups. The background is white with a void theme (no axes, grid, or + spines). Alpha variation subtly emphasizes Tech and Business groups over Operations + and Support.' criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 28 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 9 - max: 10 + score: 7 + max: 8 passed: true - comment: Title is 24pt bold, legend text clear. Some labels truncated but - intentional for small circles. White-on-color labels readable. + comment: 'All font sizes explicitly set (title 24pt, subtitle 16pt, legend + 18/16pt, in-bubble 10-12pt). All readable. Minor: smallest in-bubble text + could be slightly larger.' - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: Circles are well-packed without overlap, clear spacing between all - elements + comment: No overlapping text or elements. Circle packing maintains gaps, conditional + label sizing prevents clutter. - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Circle sizes well-differentiated showing value hierarchy, though - some smaller circles quite small + comment: All circles clearly visible with good size differentiation. White + borders and alpha variation ensure distinction. - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Uses Okabe-Ito colorblind-safe palette with good contrast between - groups + comment: Okabe-Ito colorblind-safe palette. All four colors distinguishable + under common color vision deficiencies. - id: VQ-05 - name: Layout Balance - score: 5 - max: 5 + name: Layout & Canvas + score: 3 + max: 4 passed: true - comment: Good use of 16:9 space, centered bubble cluster with legend positioned - well + comment: Bubbles occupy ~55-60% of canvas with good centering. Minor whitespace + gap between bubble cluster and bottom legend. - id: VQ-06 - name: Axis Labels - score: 0 - max: 2 - passed: true - comment: N/A for packed bubble chart (no axes), appropriate use of theme_void - - id: VQ-07 - name: Grid & Legend + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Clean void theme, legend well-positioned with clear title "Department - Group" + comment: Correct title format. Subtitle provides group totals with dollar + units. Void theme appropriately has no axes. + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Custom Okabe-Ito palette, intentional typography hierarchy, alpha + variation per group, white text on colored circles. Clearly above defaults. + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: theme_void() perfect for packed bubbles. White circle borders, clean + background, custom legend styling, plot_margin=0.02. Minor bottom whitespace + gap. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Visual hierarchy through size, color, and alpha. Subtitle with group + totals guides interpretation. Natural narrative about corporate budget allocation. 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 circles sized by value - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Size correctly represents budget value with area-based scaling - - id: SC-03 + comment: Correct packed bubble chart with area-based sizing and physics simulation + packing. + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Has labels, values for sizing, group coloring, and proper packing - algorithm - - id: SC-04 - name: Data Range + comment: 'All features present: size represents value, physics packing, labels + inside circles, color encodes group.' + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All 15 departments visible with appropriate size range - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Legend correctly maps colors to groups - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Values correctly mapped to circle area via sqrt scaling. Groups mapped + to colors. Labels correctly associated. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Correct format "bubble-packed · plotnine · pyplots.ai" + comment: Title in correct format. Legend with all four groups labeled correctly. data_quality: - score: 19 - 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 variety of sizes, group clustering by color, realistic budget - distribution + comment: 15 departments across 4 groups with wide value range ($5M-$45M). + Multiple sizes within each group. - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Department budgets are a perfect real-world use case for packed bubbles + comment: Corporate department budgets in millions. Neutral business scenario + with realistic department names. - id: DQ-03 name: Appropriate Scale - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Budget values in millions (5-45) are realistic for department spending + comment: Budget values $5M-$45M realistic for corporate departments. Proportions + sensible. code_quality: score: 10 max: 10 @@ -171,41 +196,51 @@ review: score: 3 max: 3 passed: true - comment: 'Clean linear flow: imports → data → packing algorithm → plot → save' + comment: 'Flat sequential structure: imports, data, packing, polygon construction, + plot, save. 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 sorting and packing + algorithm.' - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports are used + comment: All imports used. numpy, pandas, and all plotnine imports actively + utilized. - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current plotnine API + comment: Vectorized numpy for collision detection and force simulation. Appropriate + complexity. No fake UI. - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png - library_features: - score: 0 - max: 5 + comment: Saves as plot.png at 300 dpi using current plotnine API. + library_mastery: + score: 7 + max: 10 items: - - id: LF-01 + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: 'Good grammar of graphics composition: ggplot() + multiple geom layers + + scales + coords + themes. Significant numpy work inherent to chart type.' + - id: LM-02 name: Distinctive Features - score: 0 + score: 3 max: 5 - passed: false - comment: Uses geom_polygon for circles which is a workaround since plotnine - doesn't have native circle/bubble packing. This is creative but not a distinctive - plotnine feature. + passed: true + comment: Uses grammar of graphics layered composition, theme_void(), scale_alpha_manual, + guides(alpha=False), coord_fixed, element_rect/element_text theme components. verdict: APPROVED