|
1 | 1 | """ pyplots.ai |
2 | 2 | bubble-basic: Basic Bubble Chart |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: plotly 6.5.2 | Python 3.14.3 |
| 4 | +Quality: 94/100 | Updated: 2026-02-16 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
|
16 | 16 | revenue = np.random.randn(n_companies) * 15 + 50 |
17 | 17 | revenue = np.clip(revenue, 10, 100) |
18 | 18 |
|
19 | | -# Growth rate (%) - y axis |
| 19 | +# Growth rate (%) - y axis, correlated with revenue |
20 | 20 | growth = revenue * 0.3 + np.random.randn(n_companies) * 8 + 5 |
21 | 21 | growth = np.clip(growth, -5, 45) |
22 | 22 |
|
23 | | -# Market share (%) - bubble size |
| 23 | +# Market share (%) - bubble size and color |
24 | 24 | market_share = np.abs(np.random.randn(n_companies) * 8 + 12) |
25 | 25 | market_share = np.clip(market_share, 2, 35) |
26 | 26 |
|
27 | | -# Normalize size for bubble scaling (area-based perception) |
28 | | -size_min, size_max = 20, 90 |
29 | | -size_normalized = (market_share - market_share.min()) / (market_share.max() - market_share.min()) |
30 | | -bubble_sizes = size_min + size_normalized * (size_max - size_min) |
| 27 | +# Add a few distinct outliers/clusters for storytelling |
| 28 | +# High-revenue, high-growth market leaders |
| 29 | +revenue[0], growth[0], market_share[0] = 92, 40, 33 |
| 30 | +revenue[1], growth[1], market_share[1] = 85, 38, 28 |
| 31 | +revenue[2], growth[2], market_share[2] = 88, 42, 31 |
| 32 | + |
| 33 | +# Low-revenue emerging players with moderate growth |
| 34 | +revenue[3], growth[3], market_share[3] = 15, 32, 5 |
| 35 | +revenue[4], growth[4], market_share[4] = 18, 28, 4 |
| 36 | + |
| 37 | +# Bubble sizing via sizeref (Plotly's idiomatic area-based scaling) |
| 38 | +sizeref = 2.0 * max(market_share) / (55**2) |
| 39 | + |
| 40 | +# Custom colorscale: ensure minimum value has visible contrast against white bg |
| 41 | +colorscale = [[0, "#6a9ec0"], [0.25, "#4a82a8"], [0.5, "#306998"], [0.75, "#1f4f78"], [1, "#0d2e4d"]] |
31 | 42 |
|
32 | 43 | # Plot |
33 | 44 | fig = go.Figure() |
34 | 45 |
|
35 | | -# Main bubble scatter |
36 | 46 | fig.add_trace( |
37 | 47 | go.Scatter( |
38 | 48 | x=revenue, |
39 | 49 | y=growth, |
40 | 50 | mode="markers", |
41 | 51 | marker={ |
42 | | - "size": bubble_sizes, |
43 | | - "color": "#306998", |
44 | | - "opacity": 0.6, |
45 | | - "line": {"width": 1.5, "color": "#1a3d5c"}, |
46 | | - "sizemode": "diameter", |
| 52 | + "size": market_share, |
| 53 | + "sizemode": "area", |
| 54 | + "sizeref": sizeref, |
| 55 | + "sizemin": 6, |
| 56 | + "color": market_share, |
| 57 | + "colorscale": colorscale, |
| 58 | + "colorbar": { |
| 59 | + "title": {"text": "Market<br>Share (%)", "font": {"size": 18}}, |
| 60 | + "tickfont": {"size": 16}, |
| 61 | + "thickness": 18, |
| 62 | + "len": 0.55, |
| 63 | + "y": 0.5, |
| 64 | + }, |
| 65 | + "opacity": 0.78, |
| 66 | + "line": {"width": 1.5, "color": "white"}, |
47 | 67 | }, |
48 | 68 | text=[f"Market Share: {s:.1f}%" for s in market_share], |
49 | 69 | hovertemplate="<b>Revenue:</b> $%{x:.1f}M<br><b>Growth:</b> %{y:.1f}%<br>%{text}<extra></extra>", |
50 | 70 | showlegend=False, |
51 | 71 | ) |
52 | 72 | ) |
53 | 73 |
|
54 | | -# Size legend - representative bubbles |
| 74 | +# Size legend - representative bubbles positioned below colorbar |
55 | 75 | legend_sizes = [5, 15, 30] |
56 | | -legend_bubble_sizes = [ |
57 | | - size_min + ((s - market_share.min()) / (market_share.max() - market_share.min())) * (size_max - size_min) |
58 | | - for s in legend_sizes |
59 | | -] |
60 | | - |
61 | | -for label_size, bubble_size in zip(legend_sizes, legend_bubble_sizes, strict=True): |
| 76 | +for label_size in legend_sizes: |
62 | 77 | fig.add_trace( |
63 | 78 | go.Scatter( |
64 | 79 | x=[None], |
65 | 80 | y=[None], |
66 | 81 | mode="markers", |
67 | 82 | marker={ |
68 | | - "size": bubble_size, |
| 83 | + "size": label_size, |
| 84 | + "sizemode": "area", |
| 85 | + "sizeref": sizeref, |
| 86 | + "sizemin": 4, |
69 | 87 | "color": "#306998", |
70 | | - "opacity": 0.6, |
71 | | - "line": {"width": 1.5, "color": "#1a3d5c"}, |
| 88 | + "opacity": 0.78, |
| 89 | + "line": {"width": 1.5, "color": "white"}, |
72 | 90 | }, |
73 | 91 | name=f"{label_size}%", |
74 | 92 | showlegend=True, |
75 | 93 | ) |
76 | 94 | ) |
77 | 95 |
|
78 | | -# Layout |
| 96 | +# Layout - balanced margins, legend below colorbar |
79 | 97 | fig.update_layout( |
80 | 98 | title={"text": "bubble-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"}, |
81 | 99 | xaxis={ |
82 | 100 | "title": {"text": "Revenue ($ millions)", "font": {"size": 24}}, |
83 | 101 | "tickfont": {"size": 18}, |
84 | | - "gridcolor": "rgba(0,0,0,0.1)", |
| 102 | + "gridcolor": "rgba(0,0,0,0.08)", |
85 | 103 | "gridwidth": 1, |
| 104 | + "zeroline": False, |
86 | 105 | }, |
87 | 106 | yaxis={ |
88 | 107 | "title": {"text": "Growth Rate (%)", "font": {"size": 24}}, |
89 | 108 | "tickfont": {"size": 18}, |
90 | | - "gridcolor": "rgba(0,0,0,0.1)", |
| 109 | + "gridcolor": "rgba(0,0,0,0.08)", |
91 | 110 | "gridwidth": 1, |
| 111 | + "zeroline": False, |
92 | 112 | }, |
93 | 113 | template="plotly_white", |
94 | 114 | legend={ |
95 | 115 | "title": {"text": "Market Share", "font": {"size": 18}}, |
96 | 116 | "font": {"size": 16}, |
97 | | - "x": 1.02, |
98 | | - "y": 0.98, |
| 117 | + "x": 1.12, |
| 118 | + "y": 0.02, |
99 | 119 | "xanchor": "left", |
100 | | - "yanchor": "top", |
101 | | - "bgcolor": "rgba(255,255,255,0.8)", |
102 | | - "bordercolor": "rgba(0,0,0,0.2)", |
| 120 | + "yanchor": "bottom", |
| 121 | + "bgcolor": "rgba(255,255,255,0.85)", |
| 122 | + "bordercolor": "rgba(0,0,0,0.12)", |
103 | 123 | "borderwidth": 1, |
104 | 124 | }, |
105 | 125 | margin={"l": 100, "r": 180, "t": 120, "b": 100}, |
| 126 | + plot_bgcolor="white", |
| 127 | + paper_bgcolor="white", |
106 | 128 | ) |
107 | 129 |
|
108 | 130 | # Save |
|
0 commit comments