Skip to content

Commit f32ca35

Browse files
feat(altair): implement dumbbell-basic (#9568)
## Implementation: `dumbbell-basic` - python/altair Implements the **python/altair** version of `dumbbell-basic`. **File:** `plots/dumbbell-basic/implementations/python/altair.py` **Parent Issue:** #945 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28480646536)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 69940be commit f32ca35

2 files changed

Lines changed: 131 additions & 103 deletions

File tree

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
""" anyplot.ai
22
dumbbell-basic: Basic Dumbbell Chart
3-
Library: altair 6.1.0 | Python 3.14.4
4-
Quality: 89/100 | Updated: 2026-04-26
3+
Library: altair 6.2.2 | Python 3.13.14
4+
Quality: 92/100 | Updated: 2026-06-30
55
"""
66

77
import os
88

99
import altair as alt
1010
import pandas as pd
11+
from PIL import Image
1112

1213

1314
# Theme-adaptive chrome
@@ -17,7 +18,7 @@
1718
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1819
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
1920

20-
# Okabe-Ito palette positions 1 and 2
21+
# Imprint palette positions 1 and 2
2122
COLOR_BEFORE = "#009E73"
2223
COLOR_AFTER = "#C475FD"
2324

@@ -41,67 +42,94 @@
4142
}
4243
)
4344
data["difference"] = data["After"] - data["Before"]
44-
data = data.sort_values("difference", ascending=True)
45+
data = data.sort_values("difference", ascending=True).reset_index(drop=True)
4546

4647
# Long-form data for the two dot series
4748
dots_data = pd.melt(
4849
data, id_vars=["category", "difference"], value_vars=["Before", "After"], var_name="period", value_name="score"
4950
)
5051

51-
x_scale = alt.Scale(domain=[45, 90])
52+
title = "Employee Satisfaction · dumbbell-basic · python · altair · anyplot.ai"
53+
n = len(title)
54+
title_fontsize = round(16 * (67 / n)) if n > 67 else 16
55+
56+
x_scale = alt.Scale(domain=[45, 92])
5257
y_sort = alt.EncodingSortField(field="difference", order="ascending")
5358

5459
# Connecting lines (theme-adaptive subtle ink)
5560
lines = (
5661
alt.Chart(data)
57-
.mark_rule(strokeWidth=3, color=INK_SOFT, opacity=0.55)
62+
.mark_rule(strokeWidth=3, color=INK_SOFT, opacity=0.45)
5863
.encode(y=alt.Y("category:N", sort=y_sort, title=None), x=alt.X("Before:Q", scale=x_scale), x2=alt.X2("After:Q"))
5964
)
6065

6166
# Dots for Before / After values
6267
dots = (
6368
alt.Chart(dots_data)
64-
.mark_circle(size=420, opacity=1.0, stroke=PAGE_BG, strokeWidth=2)
69+
.mark_circle(size=350, opacity=1.0, stroke=PAGE_BG, strokeWidth=2)
6570
.encode(
6671
y=alt.Y("category:N", sort=y_sort, title=None),
6772
x=alt.X("score:Q", scale=x_scale, title="Employee Satisfaction Score (%)"),
6873
color=alt.Color(
6974
"period:N",
7075
scale=alt.Scale(domain=["Before", "After"], range=[COLOR_BEFORE, COLOR_AFTER]),
71-
legend=alt.Legend(title="Policy Change", labelFontSize=16, titleFontSize=18),
76+
legend=alt.Legend(title="Policy Change", labelFontSize=10, titleFontSize=12),
7277
),
7378
tooltip=["category:N", "period:N", "score:Q"],
7479
)
7580
)
7681

82+
# Difference labels via transform_calculate — shows the gain at a glance
83+
diff_labels = (
84+
alt.Chart(data)
85+
.transform_calculate(label="'+' + toString(datum.difference) + ' pts'")
86+
.mark_text(align="left", dx=8, fontSize=11, fontWeight="bold", clip=False)
87+
.encode(
88+
y=alt.Y("category:N", sort=y_sort, title=None),
89+
x=alt.X("After:Q", scale=x_scale),
90+
text=alt.Text("label:N"),
91+
color=alt.value(INK_SOFT),
92+
)
93+
)
94+
7795
chart = (
78-
(lines + dots)
96+
(lines + dots + diff_labels)
7997
.properties(
80-
width=1600,
81-
height=900,
82-
title=alt.Title(
83-
"Employee Satisfaction · dumbbell-basic · altair · anyplot.ai",
84-
fontSize=28,
85-
color=INK,
86-
anchor="start",
87-
offset=20,
88-
),
98+
width=576,
99+
height=374,
100+
title=alt.Title(title, fontSize=title_fontsize, color=INK, anchor="start", offset=16),
89101
background=PAGE_BG,
90102
)
91103
.configure_view(fill=PAGE_BG, stroke=None)
92104
.configure_axis(
93-
labelFontSize=18,
94-
titleFontSize=22,
105+
labelFontSize=10,
106+
titleFontSize=12,
95107
domainColor=INK_SOFT,
108+
domainOpacity=0,
96109
tickColor=INK_SOFT,
97110
gridColor=INK,
98111
gridOpacity=0.10,
99112
labelColor=INK_SOFT,
100113
titleColor=INK,
101114
)
102115
.configure_title(color=INK)
103-
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, padding=12)
116+
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, padding=10)
104117
)
105118

106-
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
119+
chart.save(f"plot-{THEME}.png", scale_factor=4.0)
120+
121+
# Pad to exact 3200×1800 canvas (vl-convert pads outside width/height)
122+
TW, TH = 3200, 1800
123+
_img = Image.open(f"plot-{THEME}.png").convert("RGB")
124+
_w, _h = _img.size
125+
if _w > TW or _h > TH:
126+
raise SystemExit(
127+
f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
128+
f"Shrink chart .properties(width=, height=) values and re-render."
129+
)
130+
if _w < TW or _h < TH:
131+
_canvas = Image.new("RGB", (TW, TH), PAGE_BG)
132+
_canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
133+
_canvas.save(f"plot-{THEME}.png")
134+
107135
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)