diff --git a/plots/bubble-packed/implementations/plotly.py b/plots/bubble-packed/implementations/plotly.py index 8e413b79f5..4e59524280 100644 --- a/plots/bubble-packed/implementations/plotly.py +++ b/plots/bubble-packed/implementations/plotly.py @@ -1,60 +1,58 @@ """ pyplots.ai bubble-packed: Basic Packed Bubble Chart -Library: plotly 6.5.0 | Python 3.13.11 -Quality: 93/100 | Created: 2025-12-23 +Library: plotly 6.5.2 | Python 3.14.3 +Quality: 91/100 | Updated: 2026-02-23 """ import numpy as np import plotly.graph_objects as go -# Data - department budget allocation -np.random.seed(42) -data = { - "Marketing": 2800000, - "Engineering": 4500000, - "Sales": 3200000, - "Operations": 1800000, - "HR": 950000, - "Finance": 1200000, - "R&D": 3800000, - "Support": 1100000, - "Legal": 650000, - "IT": 2100000, - "Product": 1500000, - "QA": 880000, - "Data Science": 1650000, - "Design": 720000, - "Admin": 450000, -} - -labels = list(data.keys()) -values = list(data.values()) - -# Circle packing simulation using force-directed approach +# Data — department budgets with functional groupings +departments = [ + ("Engineering", 4500000, "Technology"), + ("R&D", 3800000, "Technology"), + ("IT", 2100000, "Technology"), + ("Data Science", 1650000, "Technology"), + ("QA", 880000, "Technology"), + ("Sales", 3200000, "Revenue"), + ("Marketing", 2800000, "Revenue"), + ("Operations", 1800000, "Operations"), + ("Finance", 1200000, "Operations"), + ("Support", 1100000, "Operations"), + ("Admin", 450000, "Operations"), + ("HR", 950000, "Corporate"), + ("Legal", 650000, "Corporate"), + ("Product", 1500000, "Corporate"), + ("Design", 720000, "Corporate"), +] + +labels = [d[0] for d in departments] +values = np.array([d[1] for d in departments]) +groups = [d[2] for d in departments] n = len(labels) + +# Group colors — colorblind-safe palette starting with Python Blue +group_colors = {"Technology": "#306998", "Revenue": "#E69F00", "Operations": "#009E73", "Corporate": "#CC79A7"} + # Scale radii by area (sqrt) for accurate visual perception -radii_scale = np.sqrt(np.array(values)) / np.sqrt(max(values)) * 100 +radii = np.sqrt(values / values.max()) * 110 -# Initial positions - spread in a circle +# Circle packing via force simulation +np.random.seed(42) angles = np.linspace(0, 2 * np.pi, n, endpoint=False) -x_pos = np.cos(angles) * 200 + np.random.randn(n) * 50 -y_pos = np.sin(angles) * 200 + np.random.randn(n) * 50 +x_pos = np.cos(angles) * 150 + np.random.randn(n) * 30 +y_pos = np.sin(angles) * 150 + np.random.randn(n) * 30 -# Force simulation for circle packing -for _ in range(500): +for _ in range(600): for i in range(n): - fx, fy = 0, 0 - # Centering force - fx -= x_pos[i] * 0.01 - fy -= y_pos[i] * 0.01 - # Repulsion between circles + fx, fy = -x_pos[i] * 0.01, -y_pos[i] * 0.01 for j in range(n): if i != j: dx = x_pos[i] - x_pos[j] dy = y_pos[i] - y_pos[j] dist = np.sqrt(dx**2 + dy**2) + 0.1 - min_dist = radii_scale[i] + radii_scale[j] + 5 + min_dist = radii[i] + radii[j] + 4 if dist < min_dist: force = (min_dist - dist) * 0.3 fx += (dx / dist) * force @@ -62,82 +60,119 @@ x_pos[i] += fx y_pos[i] += fy -# Color palette - Python colors first, then colorblind-safe -colors = [ - "#306998", # Python Blue - "#FFD43B", # Python Yellow - "#4E79A7", - "#F28E2B", - "#E15759", - "#76B7B2", - "#59A14F", - "#EDC948", - "#B07AA1", - "#FF9DA7", - "#9C755F", - "#BAB0AC", - "#5778A4", - "#E49444", - "#85B6B2", -] +# Weight-based centering for better visual balance (larger bubbles pull center) +area_weights = radii**2 +x_pos -= np.average(x_pos, weights=area_weights) +y_pos -= np.average(y_pos, weights=area_weights) -# Format values for display (inline) -formatted_values = [f"${v / 1000000:.1f}M" if v >= 1000000 else f"${v / 1000:.0f}K" for v in values] +# Format values for display +formatted = [f"${v / 1e6:.1f}M" if v >= 1e6 else f"${v / 1e3:.0f}K" for v in values] +shares = [f"{v / values.sum() * 100:.1f}" for v in values] +total = f"${values.sum() / 1e6:.1f}M" -# Create bubble chart +# Tight axis ranges for better canvas utilization +pad = 15 +x_lo = (x_pos - radii).min() - pad +x_hi = (x_pos + radii).max() + pad +y_lo = (y_pos - radii).min() - pad +y_hi = (y_pos + radii).max() + pad + +# Convert data-coordinate radii to pixel marker diameters +fig_w, fig_h = 1600, 900 +m_l, m_r, m_t, m_b = 35, 35, 85, 85 +plot_w, plot_h = fig_w - m_l - m_r, fig_h - m_t - m_b +px_per_unit = min(plot_w / (x_hi - x_lo), plot_h / (y_hi - y_lo)) +marker_diameters = 2 * radii * px_per_unit + +# Text colors for contrast against group backgrounds +text_colors = [] +for g in groups: + c = group_colors[g] + lum = 0.299 * int(c[1:3], 16) + 0.587 * int(c[3:5], 16) + 0.114 * int(c[5:7], 16) + text_colors.append("white" if lum < 160 else "#333") + +# Build figure — one trace per group for idiomatic Plotly legend fig = go.Figure() -# Add markers -fig.add_trace( - go.Scatter( - x=x_pos, - y=y_pos, - mode="markers", - marker=dict(size=radii_scale * 2, color=colors[:n], line=dict(color="white", width=2), opacity=0.85), - hovertemplate=[ - f"{lbl}
{fval}" for lbl, fval in zip(labels, formatted_values, strict=True) - ], +for group_name, group_color in group_colors.items(): + idx = [i for i in range(n) if groups[i] == group_name] + fig.add_trace( + go.Scatter( + x=x_pos[idx], + y=y_pos[idx], + mode="markers", + name=group_name, + marker={ + "size": marker_diameters[idx], + "sizemode": "diameter", + "color": group_color, + "opacity": 0.9, + "line": {"color": "white", "width": 2.5}, + }, + text=[labels[i] for i in idx], + customdata=np.column_stack( + [[formatted[i] for i in idx], [shares[i] for i in idx], [groups[i] for i in idx]] + ), + hovertemplate="%{text} (%{customdata[2]})
Budget: %{customdata[0]}
Share: %{customdata[1]}%", + ) ) -) -# Add text annotations with size based on bubble radius +# Text labels inside bubbles — minimum 14pt for readability for i in range(n): - font_size = max(10, min(18, int(radii_scale[i] * 0.2))) + font_size = max(14, min(20, int(radii[i] * 0.22))) + label_text = f"{labels[i]}
{formatted[i]}" if radii[i] > 35 else f"{labels[i]}" fig.add_annotation( x=x_pos[i], y=y_pos[i], - text=f"{labels[i]}
{formatted_values[i]}", + text=label_text, showarrow=False, - font=dict(size=font_size, color="white", family="Arial"), + font={"size": font_size, "color": text_colors[i], "family": "Arial"}, ) # Layout fig.update_layout( - title=dict( - text="Department Budget Allocation · bubble-packed · plotly · pyplots.ai", - font=dict(size=32, color="#333"), - x=0.5, - xanchor="center", - ), - xaxis=dict( - showgrid=False, zeroline=False, showticklabels=False, title="", range=[min(x_pos) - 150, max(x_pos) + 150] - ), - yaxis=dict( - showgrid=False, - zeroline=False, - showticklabels=False, - title="", - scaleanchor="x", - scaleratio=1, - range=[min(y_pos) - 150, max(y_pos) + 150], - ), + title={ + "text": "Department Budget Allocation · bubble-packed · plotly · pyplots.ai", + "font": {"size": 32, "color": "#333"}, + "x": 0.5, + "xanchor": "center", + }, + xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": "", "range": [x_lo, x_hi]}, + yaxis={ + "showgrid": False, + "zeroline": False, + "showticklabels": False, + "title": "", + "scaleanchor": "x", + "scaleratio": 1, + "range": [y_lo, y_hi], + }, template="plotly_white", - showlegend=False, - margin=dict(l=50, r=50, t=100, b=50), + legend={ + "font": {"size": 16, "family": "Arial"}, + "orientation": "h", + "yanchor": "top", + "y": -0.04, + "xanchor": "center", + "x": 0.5, + "itemsizing": "constant", + }, + margin={"l": m_l, "r": m_r, "t": m_t, "b": m_b}, paper_bgcolor="white", plot_bgcolor="white", ) -# Save outputs -fig.write_image("plot.png", width=1600, height=900, scale=3) +# Total budget annotation below the cluster +fig.add_annotation( + text=f"Total: {total}", + xref="paper", + yref="paper", + x=0.5, + y=-0.01, + showarrow=False, + font={"size": 18, "color": "#666", "family": "Arial"}, +) + +# Save +fig.write_image("plot.png", width=fig_w, height=fig_h, scale=3) fig.write_html("plot.html", include_plotlyjs=True, full_html=True) diff --git a/plots/bubble-packed/metadata/plotly.yaml b/plots/bubble-packed/metadata/plotly.yaml index 1d33520167..8c86dd2f05 100644 --- a/plots/bubble-packed/metadata/plotly.yaml +++ b/plots/bubble-packed/metadata/plotly.yaml @@ -1,16 +1,16 @@ library: plotly specification_id: bubble-packed created: '2025-12-23T09:16:37Z' -updated: '2025-12-23T09:19:10Z' -generated_by: claude-opus-4-5-20251101 +updated: '2026-02-23T16:50:07Z' +generated_by: claude-opus-4-6 workflow_run: 20456557994 issue: 0 -python_version: 3.13.11 -library_version: 6.5.0 +python_version: 3.14.3 +library_version: 6.5.2 preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotly/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotly/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotly/plot.html -quality_score: 93 +quality_score: 91 impl_tags: dependencies: [] techniques: @@ -19,157 +19,171 @@ impl_tags: - html-export patterns: - data-generation + - iteration-over-groups dataprep: [] styling: + - minimal-chrome + - edge-highlighting - alpha-blending review: strengths: - - Excellent force-directed circle packing algorithm implementation that correctly - avoids overlap - - Proper area scaling (sqrt) for accurate visual perception as specified - - Clean, readable labels positioned inside bubbles with dynamic font sizing - - Rich hover templates with formatted budget values - - Good color palette starting with Python colors - - Both PNG and interactive HTML outputs provided - - Realistic department budget data with appropriate value range + - Excellent force simulation with weighted centering for tight non-overlapping packing + - Colorblind-safe palette with luminance-based text contrast calculation + - Rich hover tooltips with budget amounts and share percentages via hovertemplate + and customdata + - Pixel-to-data coordinate conversion for accurate bubble marker sizing + - Size-adaptive labels prevent cramped text in smaller bubbles + - Both PNG and HTML output maximize utility weaknesses: - - No legend showing total budget or providing a size reference scale - - Text in smallest bubbles (Admin, Legal, Design) appears slightly cramped - - Optional grouping feature from spec not demonstrated (though marked optional) - image_description: The plot displays a packed bubble chart with 15 circles representing - department budget allocations. Each circle is labeled with the department name - (bold) and budget value (in $M or $K format). The circles are packed tightly together - without overlap, with the largest circles being Engineering ($4.5M), R&D ($3.8M), - Sales ($3.2M), and Marketing ($2.8M). Smaller departments like Admin ($450K), - Legal ($650K), and Design ($720K) appear as smaller circles. The color palette - is diverse - Python blue for R&D, yellow for Engineering, various other colors - including pink, orange, teal, brown, and purple. The title "Department Budget - Allocation · bubble-packed · plotly · pyplots.ai" appears at the top center in - dark gray text. The background is clean white with no grid lines or axes shown. + - Force simulation packing could be slightly tighter for a more compact overall + shape + - Minor asymmetry in the bubble cluster positioning due to force-directed approach + image_description: 'The plot shows a packed bubble chart titled "Department Budget + Allocation · bubble-packed · plotly · pyplots.ai" centered at the top in large + dark text. Fifteen circles of varying sizes represent department budgets, packed + tightly together without overlap in the center of a white canvas. Four color groups + are used: steel blue (#306998) for Technology departments (Engineering $4.5M, + R&D $3.8M, IT $2.1M, Data Science $1.6M, QA $880K), gold/amber (#E69F00) for Revenue + (Sales $3.2M, Marketing $2.8M), teal (#009E73) for Operations (Operations $1.8M, + Finance $1.2M, Support $1.1M, Admin $450K), and pink/mauve (#CC79A7) for Corporate + (Product $1.5M, HR $950K, Design $720K, Legal $650K). Each bubble contains the + department name in bold white or dark text (contrast-adjusted), with budget amounts + shown for larger bubbles. White borders (2.5px) separate the circles visually. + A "Total: $27.3M" annotation appears below the cluster, and a horizontal legend + at the bottom identifies the four groups. The bubble cluster fills roughly 60-65% + of the canvas, with balanced margins.' criteria_checklist: visual_quality: - score: 37 - max: 40 + score: 28 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 7 + max: 8 passed: true - comment: All text is clearly readable. Title is large and prominent. Department - names and values are legible inside each bubble with white text that contrasts - well against the colored backgrounds. + comment: 'All font sizes explicitly set: title 32pt, bubble labels 14-20pt + adaptive, legend 16pt, total annotation 18pt. All readable at full resolution. + Minor deduction for tight fit in smallest bubbles.' - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: Circles are properly packed without overlapping. Text is contained - within each bubble. + comment: Force simulation with padding ensures no bubble overlap. All labels + contained within circles. No text collisions. - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: 'Bubble sizes are well-proportioned to show the data differences. - The force simulation effectively packed the circles. Minor: some smaller - bubbles (Admin, Legal) have slightly cramped text.' + comment: All 15 bubbles clearly visible with 10x size range. White borders + provide clean separation. Opacity 0.9 gives appropriate visual weight. - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Uses a colorblind-safe palette with sufficient variation. Python - blue and yellow featured prominently, other colors well-differentiated. + comment: 'Colorblind-safe palette: steel blue, gold, teal, pink. All 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 proportions with circles centered in the plot area. White space - around the packed bubbles is balanced. + comment: Bubble cluster fills ~60-65% of canvas. Minor deduction for slight + asymmetry and uneven corner spacing inherent to force-directed packing. - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: N/A for packed bubble chart - correctly hidden as position has no - meaning. - - id: VQ-07 - name: Grid & Legend - score: 0 - max: 2 + comment: Axes appropriately hidden for packed bubble chart. Title is descriptive + with context. Budget values inside bubbles provide quantitative context. + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 passed: true - comment: No legend shown. While a legend isn't strictly necessary since labels - are on bubbles, a small legend or group indicator could enhance understanding. + comment: Custom colorblind-safe palette, contrast-aware text colors via luminance + calculation, white marker borders, consistent Arial typography. Clearly + above defaults. + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Axes and gridlines hidden, clean white background, white marker borders, + opacity 0.9, horizontal legend, generous whitespace. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Size hierarchy communicates budget dominance. Color grouping reveals + Technology as largest spending category. Budget labels and total annotation + provide context. 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 where position has no meaning, only size - matters. - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Size correctly represents budget value, scaled by area (sqrt) as - spec requires. - - id: SC-03 + comment: Correct packed bubble chart with force simulation, size-mapped circles, + hidden axes. + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Labels inside circles, force simulation for packing, color encoding - categories. - - id: SC-04 - name: Data Range + comment: Area-scaled circles, force packing, adaptive labels, color-coded + groups, hover tooltips all present. + - 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: Labels directly on bubbles, no separate legend needed. - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Values correctly mapped to circle areas via sqrt. Groups mapped to + colors. 15 items within recommended range. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Correctly uses "{description} · bubble-packed · plotly · pyplots.ai" - format. + comment: Title includes spec-id, library, pyplots.ai format. Legend labels + match group data exactly. data_quality: - score: 18 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage score: 6 - max: 8 + max: 6 passed: true - comment: Shows good variation in circle sizes from $450K to $4.5M (10x range). - However, the spec mentions optional grouping which isn't demonstrated. + comment: Wide size variation (10x range), four distinct groups with varying + membership, size-adaptive labels. - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Department budget allocation is a perfect, real-world scenario for - packed bubble charts. + comment: Department budgets at a tech company — real, comprehensible, neutral + business scenario with recognizable department names. - id: DQ-03 name: Appropriate Scale - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Budget values are realistic for a medium-to-large company. + comment: Budget values $450K-$4.5M and total $27.3M are realistic for a mid-to-large + company. code_quality: score: 10 max: 10 @@ -179,42 +193,50 @@ review: score: 3 max: 3 passed: true - comment: 'Simple linear flow: imports → data → simulation → plot → save.' + comment: 'Clean linear flow: imports, data, packing, formatting, figure, layout, + 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: np.random.seed(42) set before random operations. Deterministic simulation. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only numpy and plotly.graph_objects used. + comment: Only numpy and plotly.graph_objects imported, both fully utilized. - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current plotly API. + comment: Well-structured force simulation, weighted centering, luminance-based + text contrast. Appropriate complexity. - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png and plot.html. - library_features: - score: 3 - max: 5 + comment: Saves plot.png via write_image at 4800x2700 and plot.html. Current + Plotly API. + library_mastery: + score: 8 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features - score: 3 + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Idiomatic go.Figure with per-group traces, hovertemplate with customdata, + add_annotation, update_layout, sizemode=diameter. + - id: LM-02 + name: Distinctive Features + score: 4 max: 5 passed: true - comment: Uses go.Scatter with annotations for labels and hover templates for - interactivity. The HTML export enables Plotly's interactive features. Could - leverage Plotly's animation capabilities for the force simulation or use - custom shapes. + comment: Rich hover tooltips via hovertemplate+customdata, HTML export, scaleanchor/scaleratio, + itemsizing=constant in legend. verdict: APPROVED