|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | bullet-basic: Basic Bullet Chart |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: altair 6.0.0 | Python 3.14.3 |
| 4 | +Quality: /100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import altair as alt |
8 | 8 | import pandas as pd |
9 | 9 |
|
10 | 10 |
|
11 | 11 | # Data - KPI metrics with actual values, targets, and qualitative range thresholds |
12 | | -# Each metric shows: current performance, target goal, and bands for poor/satisfactory/good |
13 | 12 | metrics = [ |
14 | 13 | {"metric": "Revenue ($K)", "actual": 275, "target": 250, "poor": 150, "satisfactory": 200, "good": 300}, |
15 | 14 | {"metric": "Profit ($K)", "actual": 85, "target": 100, "poor": 50, "satisfactory": 75, "good": 125}, |
16 | 15 | {"metric": "New Customers", "actual": 320, "target": 300, "poor": 200, "satisfactory": 275, "good": 350}, |
17 | | - {"metric": "Satisfaction (1-5)", "actual": 4.2, "target": 4.5, "poor": 3.0, "satisfactory": 4.0, "good": 5.0}, |
| 16 | + {"metric": "Satisfaction", "actual": 4.2, "target": 4.5, "poor": 3.0, "satisfactory": 4.0, "good": 5.0}, |
18 | 17 | ] |
| 18 | +metric_order = [m["metric"] for m in metrics] |
19 | 19 |
|
20 | | -# Normalize all values to percentage of maximum (good value) for common scale |
| 20 | +# Normalize to percentage of maximum and build dataframes |
21 | 21 | range_data = [] |
22 | 22 | for m in metrics: |
23 | 23 | max_val = m["good"] |
24 | 24 | poor_pct = (m["poor"] / max_val) * 100 |
25 | 25 | sat_pct = (m["satisfactory"] / max_val) * 100 |
26 | | - # Build qualitative bands: Poor (0 to poor), Satisfactory (poor to sat), Good (sat to 100%) |
27 | | - range_data.append({"metric": m["metric"], "range_start": 0, "range_end": poor_pct, "band": "Poor"}) |
28 | | - range_data.append({"metric": m["metric"], "range_start": poor_pct, "range_end": sat_pct, "band": "Satisfactory"}) |
29 | | - range_data.append({"metric": m["metric"], "range_start": sat_pct, "range_end": 100, "band": "Good"}) |
| 26 | + range_data.append({"metric": m["metric"], "start": 0, "end": poor_pct, "band": "Poor"}) |
| 27 | + range_data.append({"metric": m["metric"], "start": poor_pct, "end": sat_pct, "band": "Satisfactory"}) |
| 28 | + range_data.append({"metric": m["metric"], "start": sat_pct, "end": 100, "band": "Good"}) |
30 | 29 |
|
31 | 30 | df_ranges = pd.DataFrame(range_data) |
32 | 31 |
|
33 | | -# Build dataframe for actual values (normalized to percentage) |
34 | | -df_actual = pd.DataFrame([{"metric": m["metric"], "actual": (m["actual"] / m["good"]) * 100} for m in metrics]) |
| 32 | +df_actual = pd.DataFrame( |
| 33 | + [{"metric": m["metric"], "actual_pct": (m["actual"] / m["good"]) * 100, "actual_raw": m["actual"]} for m in metrics] |
| 34 | +) |
35 | 35 |
|
36 | | -# Build dataframe for target markers (normalized to percentage) |
37 | | -df_target = pd.DataFrame([{"metric": m["metric"], "target": (m["target"] / m["good"]) * 100} for m in metrics]) |
| 36 | +df_target = pd.DataFrame( |
| 37 | + [{"metric": m["metric"], "target_pct": (m["target"] / m["good"]) * 100, "target_raw": m["target"]} for m in metrics] |
| 38 | +) |
38 | 39 |
|
39 | | -# Background qualitative ranges (grayscale bands as per specification) |
| 40 | +# Background qualitative ranges (grayscale bands) |
40 | 41 | ranges_chart = ( |
41 | 42 | alt.Chart(df_ranges) |
42 | | - .mark_bar(height=55) |
| 43 | + .mark_bar(height=50) |
43 | 44 | .encode( |
44 | | - y=alt.Y("metric:N", title=None, sort=[m["metric"] for m in metrics]), |
45 | | - x=alt.X("range_start:Q", title="% of Maximum", scale=alt.Scale(domain=[0, 110])), |
46 | | - x2=alt.X2("range_end:Q"), |
| 45 | + y=alt.Y("metric:N", title=None, sort=metric_order, axis=alt.Axis(labelFontSize=18)), |
| 46 | + x=alt.X( |
| 47 | + "start:Q", |
| 48 | + title="Performance (% of Goal)", |
| 49 | + scale=alt.Scale(domain=[0, 105]), |
| 50 | + axis=alt.Axis(titleFontSize=22, labelFontSize=16, tickCount=6), |
| 51 | + ), |
| 52 | + x2="end:Q", |
47 | 53 | color=alt.Color( |
48 | 54 | "band:N", |
49 | | - scale=alt.Scale(domain=["Poor", "Satisfactory", "Good"], range=["#d9d9d9", "#bdbdbd", "#969696"]), |
50 | | - legend=alt.Legend(title="Performance Band", orient="bottom", titleFontSize=18, labelFontSize=16), |
| 55 | + scale=alt.Scale(domain=["Poor", "Satisfactory", "Good"], range=["#e0e0e0", "#c0c0c0", "#9e9e9e"]), |
| 56 | + legend=alt.Legend( |
| 57 | + title="Performance Band", orient="bottom", titleFontSize=18, labelFontSize=16, direction="horizontal" |
| 58 | + ), |
51 | 59 | ), |
| 60 | + tooltip=[alt.Tooltip("metric:N", title="Metric"), alt.Tooltip("band:N", title="Band")], |
52 | 61 | ) |
53 | 62 | ) |
54 | 63 |
|
55 | | -# Actual value bar (Python Blue - primary measure) |
| 64 | +# Actual value bar (Python Blue) |
56 | 65 | actual_chart = ( |
57 | 66 | alt.Chart(df_actual) |
58 | | - .mark_bar(color="#306998", height=22) |
| 67 | + .mark_bar(color="#306998", height=20) |
59 | 68 | .encode( |
60 | | - y=alt.Y("metric:N", title=None, sort=[m["metric"] for m in metrics]), |
61 | | - x=alt.X("actual:Q", title="% of Maximum", scale=alt.Scale(domain=[0, 110])), |
| 69 | + y=alt.Y("metric:N", sort=metric_order), |
| 70 | + x=alt.X("actual_pct:Q"), |
| 71 | + tooltip=[ |
| 72 | + alt.Tooltip("metric:N", title="Metric"), |
| 73 | + alt.Tooltip("actual_raw:Q", title="Actual"), |
| 74 | + alt.Tooltip("actual_pct:Q", title="% of Goal", format=".1f"), |
| 75 | + ], |
62 | 76 | ) |
63 | 77 | ) |
64 | 78 |
|
65 | | -# Target marker (thin black vertical line perpendicular to bar) |
| 79 | +# Target marker (thin black vertical tick) |
66 | 80 | target_chart = ( |
67 | 81 | alt.Chart(df_target) |
68 | | - .mark_tick(color="black", thickness=4, size=55) |
| 82 | + .mark_tick(color="#222222", thickness=4, size=50) |
69 | 83 | .encode( |
70 | | - y=alt.Y("metric:N", title=None, sort=[m["metric"] for m in metrics]), |
71 | | - x=alt.X("target:Q", title="% of Maximum", scale=alt.Scale(domain=[0, 110])), |
| 84 | + y=alt.Y("metric:N", sort=metric_order), |
| 85 | + x=alt.X("target_pct:Q"), |
| 86 | + tooltip=[alt.Tooltip("metric:N", title="Metric"), alt.Tooltip("target_raw:Q", title="Target")], |
72 | 87 | ) |
73 | 88 | ) |
74 | 89 |
|
75 | | -# Layer all components: ranges (background), actual bar, target marker |
| 90 | +# Actual value text labels on bars |
| 91 | +value_labels = ( |
| 92 | + alt.Chart(df_actual) |
| 93 | + .mark_text(align="left", dx=6, fontSize=16, fontWeight="bold", color="#1a3a5c") |
| 94 | + .encode(y=alt.Y("metric:N", sort=metric_order), x=alt.X("actual_pct:Q"), text=alt.Text("actual_raw:Q")) |
| 95 | +) |
| 96 | + |
| 97 | +# Layer all components and configure |
76 | 98 | chart = ( |
77 | | - alt.layer(ranges_chart, actual_chart, target_chart) |
| 99 | + alt.layer(ranges_chart, actual_chart, target_chart, value_labels) |
78 | 100 | .properties( |
79 | | - width=1400, height=700, title=alt.Title("bullet-basic · altair · pyplots.ai", fontSize=28, anchor="middle") |
| 101 | + width=1400, height=400, title=alt.Title("bullet-basic · altair · pyplots.ai", fontSize=28, anchor="middle") |
80 | 102 | ) |
81 | 103 | .configure_axis(labelFontSize=18, titleFontSize=22) |
82 | 104 | .configure_view(strokeWidth=0) |
83 | 105 | ) |
84 | 106 |
|
85 | | -# Save outputs |
| 107 | +# Save |
86 | 108 | chart.save("plot.png", scale_factor=3.0) |
87 | 109 | chart.save("plot.html") |
0 commit comments