|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | bullet-basic: Basic Bullet 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: /100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import plotly.graph_objects as go |
8 | | -from plotly.subplots import make_subplots |
9 | 8 |
|
10 | 9 |
|
11 | 10 | # Data - Multiple KPIs with different performance levels |
|
17 | 16 | ] |
18 | 17 |
|
19 | 18 | # Grayscale colors for qualitative ranges (poor -> satisfactory -> good) |
20 | | -range_colors = ["#D9D9D9", "#BFBFBF", "#A6A6A6"] |
| 19 | +range_colors = ["#A6A6A6", "#C0C0C0", "#DCDCDC"] |
21 | 20 |
|
22 | | -# Create subplots - one row per metric for proper scaling |
23 | | -fig = make_subplots( |
24 | | - rows=len(metrics), cols=1, shared_xaxes=False, vertical_spacing=0.12, subplot_titles=[m["label"] for m in metrics] |
25 | | -) |
| 21 | +# Create figure with native Indicator traces (bullet mode) |
| 22 | +fig = go.Figure() |
| 23 | +n = len(metrics) |
| 24 | +spacing = 0.04 |
| 25 | +row_height = (1.0 - spacing * (n - 1)) / n |
26 | 26 |
|
27 | | -# Create each bullet chart in its own subplot |
28 | 27 | for i, m in enumerate(metrics): |
29 | | - row = i + 1 |
30 | | - |
31 | | - # Add qualitative range bands (background, plotted in reverse order) |
32 | | - for j, r in enumerate(reversed(m["ranges"])): |
33 | | - fig.add_trace( |
34 | | - go.Bar( |
35 | | - x=[r], |
36 | | - y=[""], |
37 | | - orientation="h", |
38 | | - marker=dict(color=range_colors[len(m["ranges"]) - 1 - j]), |
39 | | - width=0.6, |
40 | | - showlegend=False, |
41 | | - hoverinfo="skip", |
42 | | - ), |
43 | | - row=row, |
44 | | - col=1, |
45 | | - ) |
| 28 | + y_start = 1.0 - (i + 1) * row_height - i * spacing |
| 29 | + y_end = y_start + row_height |
46 | 30 |
|
47 | | - # Add actual value bar (primary measure) using Python Blue |
48 | 31 | fig.add_trace( |
49 | | - go.Bar( |
50 | | - x=[m["actual"]], |
51 | | - y=[""], |
52 | | - orientation="h", |
53 | | - marker=dict(color="#306998"), |
54 | | - width=0.25, |
55 | | - showlegend=False, |
56 | | - name=m["label"], |
57 | | - hovertemplate=f"{m['label']}: {m['actual']}<extra></extra>", |
58 | | - ), |
59 | | - row=row, |
60 | | - col=1, |
61 | | - ) |
62 | | - |
63 | | - # Add target marker line (thin black vertical line) |
64 | | - fig.add_shape( |
65 | | - type="line", |
66 | | - x0=m["target"], |
67 | | - x1=m["target"], |
68 | | - y0=-0.4, |
69 | | - y1=0.4, |
70 | | - line=dict(color="#1A1A1A", width=5), |
71 | | - row=row, |
72 | | - col=1, |
73 | | - ) |
74 | | - |
75 | | - # Add actual value annotation for precise reading |
76 | | - max_range = m["ranges"][-1] |
77 | | - fig.add_annotation( |
78 | | - x=max_range * 1.02, |
79 | | - y=0, |
80 | | - text=f"<b>{m['actual']}</b>", |
81 | | - showarrow=False, |
82 | | - font=dict(size=20, color="#306998"), |
83 | | - xanchor="left", |
84 | | - row=row, |
85 | | - col=1, |
86 | | - ) |
87 | | - |
88 | | - # Update x-axis range for each subplot |
89 | | - fig.update_xaxes( |
90 | | - range=[0, max_range * 1.15], tickfont=dict(size=16), showgrid=True, gridcolor="rgba(0,0,0,0.1)", row=row, col=1 |
| 32 | + go.Indicator( |
| 33 | + mode="number+gauge", |
| 34 | + value=m["actual"], |
| 35 | + number={"font": {"size": 26, "color": "#306998"}}, |
| 36 | + domain={"x": [0.18, 0.95], "y": [y_start, y_end]}, |
| 37 | + title={"text": m["label"], "font": {"size": 22}, "align": "left"}, |
| 38 | + gauge={ |
| 39 | + "shape": "bullet", |
| 40 | + "axis": {"range": [0, m["ranges"][-1]], "tickfont": {"size": 16}}, |
| 41 | + "bar": {"color": "#306998"}, |
| 42 | + "bgcolor": "white", |
| 43 | + "threshold": {"line": {"color": "#1A1A1A", "width": 4}, "thickness": 0.75, "value": m["target"]}, |
| 44 | + "steps": [ |
| 45 | + {"range": [0, m["ranges"][0]], "color": range_colors[0]}, |
| 46 | + {"range": [m["ranges"][0], m["ranges"][1]], "color": range_colors[1]}, |
| 47 | + {"range": [m["ranges"][1], m["ranges"][2]], "color": range_colors[2]}, |
| 48 | + ], |
| 49 | + }, |
| 50 | + ) |
91 | 51 | ) |
92 | 52 |
|
93 | | - fig.update_yaxes(showticklabels=False, row=row, col=1) |
94 | | - |
95 | 53 | # Layout |
96 | 54 | fig.update_layout( |
97 | | - title=dict(text="bullet-basic · plotly · pyplots.ai", font=dict(size=32), x=0.5, xanchor="center"), |
98 | | - barmode="overlay", |
| 55 | + title={"text": "bullet-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"}, |
99 | 56 | template="plotly_white", |
100 | | - margin=dict(l=80, r=100, t=120, b=60), |
101 | | - showlegend=False, |
| 57 | + margin={"l": 40, "r": 40, "t": 100, "b": 40}, |
102 | 58 | height=900, |
103 | 59 | width=1600, |
104 | 60 | ) |
105 | 61 |
|
106 | | -# Update subplot titles font size |
107 | | -for annotation in fig["layout"]["annotations"]: |
108 | | - if "text" in annotation and annotation["text"] in [m["label"] for m in metrics]: |
109 | | - annotation["font"] = dict(size=22) |
110 | | - |
111 | 62 | # Save |
112 | 63 | fig.write_image("plot.png", width=1600, height=900, scale=3) |
113 | 64 | fig.write_html("plot.html", include_plotlyjs="cdn") |
0 commit comments