Skip to content

Commit 2dfd2b4

Browse files
Merge branch 'main' into implementation/slope-basic/pygal
2 parents 0e8012c + a1da3a6 commit 2dfd2b4

8 files changed

Lines changed: 1005 additions & 715 deletions

File tree

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
slope-basic: Basic Slope Chart (Slopegraph)
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: bokeh 3.9.0 | Python 3.13.13
4+
Quality: 84/100 | Updated: 2026-04-30
55
"""
66

7+
import os
8+
79
from bokeh.io import export_png, save
8-
from bokeh.models import Label
10+
from bokeh.models import ColumnDataSource, HoverTool, Label
911
from bokeh.plotting import figure
1012
from bokeh.resources import CDN
1113

1214

13-
# Data - Product sales comparison Q1 vs Q4 (10 products)
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
21+
INCREASE_COLOR = "#009E73" # Okabe-Ito position 1 (brand green)
22+
DECREASE_COLOR = "#D55E00" # Okabe-Ito position 2 (vermillion)
23+
1424
products = [
1525
"Product A",
1626
"Product B",
@@ -26,66 +36,115 @@
2636
q1_sales = [85, 72, 91, 45, 68, 53, 78, 62, 40, 88]
2737
q4_sales = [92, 65, 88, 71, 74, 48, 95, 58, 67, 82]
2838

29-
# Determine direction for color coding
30-
colors = []
31-
for start, end in zip(q1_sales, q4_sales, strict=True):
32-
if end > start:
33-
colors.append("#306998") # Python Blue - increase
34-
elif end < start:
35-
colors.append("#FFD43B") # Python Yellow - decrease
36-
else:
37-
colors.append("#888888") # Gray - no change
38-
39-
# Create figure
39+
colors = [INCREASE_COLOR if end > start else DECREASE_COLOR for start, end in zip(q1_sales, q4_sales, strict=True)]
40+
41+
42+
def spread_labels(ys, min_gap=4.5):
43+
"""Shift label y-positions apart so dense clusters don't overlap."""
44+
n = len(ys)
45+
order = sorted(range(n), key=lambda i: ys[i])
46+
adjusted = [float(ys[i]) for i in order]
47+
for _ in range(30):
48+
changed = False
49+
for i in range(1, n):
50+
if adjusted[i] - adjusted[i - 1] < min_gap:
51+
mid = (adjusted[i] + adjusted[i - 1]) / 2
52+
adjusted[i - 1] = mid - min_gap / 2
53+
adjusted[i] = mid + min_gap / 2
54+
changed = True
55+
if not changed:
56+
break
57+
result = [0.0] * n
58+
for new_i, orig_i in enumerate(order):
59+
result[orig_i] = adjusted[new_i]
60+
return result
61+
62+
63+
left_y = spread_labels(q1_sales)
64+
right_y = spread_labels(q4_sales)
65+
4066
p = figure(
4167
width=4800,
4268
height=2700,
43-
title="slope-basic · bokeh · pyplots.ai",
69+
title="slope-basic · bokeh · anyplot.ai",
4470
x_range=(-0.5, 1.5),
45-
y_range=(25, 105),
71+
y_range=(25, 112),
4672
toolbar_location=None,
4773
)
4874

49-
# Style title and axes
75+
p.background_fill_color = PAGE_BG
76+
p.border_fill_color = PAGE_BG
77+
p.outline_line_color = None
78+
5079
p.title.text_font_size = "32pt"
5180
p.title.align = "center"
81+
p.title.text_color = INK
5282

53-
# Remove x axis tick labels and set custom labels for time points
5483
p.xaxis.visible = False
5584
p.yaxis.axis_label = "Sales (thousands)"
5685
p.yaxis.axis_label_text_font_size = "22pt"
86+
p.yaxis.axis_label_text_color = INK
5787
p.yaxis.major_label_text_font_size = "18pt"
88+
p.yaxis.major_label_text_color = INK_SOFT
89+
p.yaxis.axis_line_color = INK_SOFT
90+
p.yaxis.major_tick_line_color = INK_SOFT
91+
92+
p.xgrid.grid_line_color = None
93+
p.ygrid.grid_line_color = INK_SOFT
94+
p.ygrid.grid_line_alpha = 0.10
5895

59-
# Add time point labels at bottom
60-
p.add_layout(Label(x=0, y=32, text="Q1", text_font_size="28pt", text_align="center", text_baseline="top"))
61-
p.add_layout(Label(x=1, y=32, text="Q4", text_font_size="28pt", text_align="center", text_baseline="top"))
96+
# Time point labels
97+
for x_pos, label in [(0, "Q1"), (1, "Q4")]:
98+
p.add_layout(
99+
Label(
100+
x=x_pos, y=27, text=label, text_font_size="28pt", text_align="center", text_baseline="top", text_color=INK
101+
)
102+
)
103+
104+
# Direction legend in upper-center (above data range)
105+
for y_pos, legend_text, color in [(109, "— Increase", INCREASE_COLOR), (103, "— Decrease", DECREASE_COLOR)]:
106+
p.add_layout(
107+
Label(
108+
x=0.5,
109+
y=y_pos,
110+
text=legend_text,
111+
text_font_size="20pt",
112+
text_align="center",
113+
text_baseline="middle",
114+
text_color=color,
115+
)
116+
)
62117

63-
# Draw slope lines connecting Q1 to Q4 for each product
118+
# ColumnDataSource for scatter enables HoverTool
119+
scatter_data: dict[str, list] = {"x": [], "y": [], "color": [], "product": [], "period": [], "value": []}
64120
for product, start, end, color in zip(products, q1_sales, q4_sales, colors, strict=True):
65-
# Draw the connecting line
66-
p.line(x=[0, 1], y=[start, end], line_width=4, line_color=color, line_alpha=0.8)
121+
scatter_data["x"].extend([0, 1])
122+
scatter_data["y"].extend([start, end])
123+
scatter_data["color"].extend([color, color])
124+
scatter_data["product"].extend([product, product])
125+
scatter_data["period"].extend(["Q1", "Q4"])
126+
scatter_data["value"].extend([start, end])
67127

68-
# Add markers at both endpoints
69-
p.scatter(x=[0, 1], y=[start, end], size=18, color=color, alpha=0.9)
128+
source = ColumnDataSource(data=scatter_data)
70129

71-
# Add labels at start (Q1) - left aligned
130+
# Draw slope lines and endpoint labels
131+
for i, (product, start, end, color) in enumerate(zip(products, q1_sales, q4_sales, colors, strict=True)):
132+
p.line(x=[0, 1], y=[start, end], line_width=4, line_color=color, line_alpha=0.8)
72133
p.add_layout(
73134
Label(
74135
x=-0.05,
75-
y=start,
136+
y=left_y[i],
76137
text=f"{product}: {start}",
77138
text_font_size="18pt",
78139
text_align="right",
79140
text_baseline="middle",
80141
text_color=color,
81142
)
82143
)
83-
84-
# Add labels at end (Q4) - right aligned
85144
p.add_layout(
86145
Label(
87146
x=1.05,
88-
y=end,
147+
y=right_y[i],
89148
text=f"{end}: {product}",
90149
text_font_size="18pt",
91150
text_align="left",
@@ -94,16 +153,12 @@
94153
)
95154
)
96155

97-
# Style grid
98-
p.xgrid.grid_line_color = None
99-
p.ygrid.grid_line_alpha = 0.3
100-
p.ygrid.grid_line_dash = "dashed"
101-
102-
# Style outline
103-
p.outline_line_color = None
104-
105-
# Save as PNG
106-
export_png(p, filename="plot.png")
156+
dots = p.scatter(x="x", y="y", size=18, color="color", source=source, alpha=0.9)
157+
p.add_tools(
158+
HoverTool(
159+
renderers=[dots], tooltips=[("Product", "@product"), ("Period", "@period"), ("Sales", "@value{0} thousand")]
160+
)
161+
)
107162

108-
# Also save as HTML for interactive viewing
109-
save(p, filename="plot.html", resources=CDN, title="slope-basic · bokeh · pyplots.ai")
163+
export_png(p, filename=f"plot-{THEME}.png")
164+
save(p, filename=f"plot-{THEME}.html", resources=CDN, title="slope-basic · bokeh · anyplot.ai")
Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
slope-basic: Basic Slope Chart (Slopegraph)
3-
Library: matplotlib 3.10.8 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: matplotlib 3.10.9 | Python 3.13.13
4+
Quality: 90/100 | Updated: 2026-04-30
55
"""
66

7+
import os
8+
79
import matplotlib.pyplot as plt
10+
import matplotlib.ticker as mticker
811
from matplotlib.lines import Line2D
912

1013

11-
# Data: Sales figures (in millions $) for products comparing Q1 vs Q4
12-
# Deterministic data with well-spaced values to avoid label overlap
14+
# Theme
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
21+
COLOR_INC = "#009E73" # Okabe-Ito pos 1 — increase (brand green)
22+
COLOR_DEC = "#D55E00" # Okabe-Ito pos 2 — decrease (vermillion)
23+
24+
# Data
1325
products = ["Product A", "Product B", "Product C", "Product D", "Product E", "Product F", "Product G", "Product H"]
1426
q1_sales = [3.0, 6.5, 10.0, 13.5, 17.0, 20.5, 24.0, 27.5]
1527
q4_sales = [5.5, 4.0, 14.0, 11.0, 20.0, 17.5, 28.0, 25.0]
16-
17-
# Calculate changes to determine colors
1828
changes = [q4 - q1 for q1, q4 in zip(q1_sales, q4_sales, strict=True)]
1929

20-
# Create figure
21-
fig, ax = plt.subplots(figsize=(16, 9))
22-
23-
# Label collision avoidance: sort and adjust positions if too close
30+
# Label collision avoidance: sort by value and nudge positions if too close
2431
min_gap = 1.8
2532

26-
# Adjust Q1 labels (left side)
2733
q1_indexed = sorted(enumerate(q1_sales), key=lambda x: x[1])
2834
q1_label_pos = [0.0] * len(q1_sales)
2935
for i, (orig_idx, val) in enumerate(q1_indexed):
@@ -36,7 +42,6 @@
3642
else:
3743
q1_label_pos[orig_idx] = val
3844

39-
# Adjust Q4 labels (right side)
4045
q4_indexed = sorted(enumerate(q4_sales), key=lambda x: x[1])
4146
q4_label_pos = [0.0] * len(q4_sales)
4247
for i, (orig_idx, val) in enumerate(q4_indexed):
@@ -49,47 +54,76 @@
4954
else:
5055
q4_label_pos[orig_idx] = val
5156

52-
# Plot each slope line
57+
# Plot
58+
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
59+
ax.set_facecolor(PAGE_BG)
60+
5361
x_positions = [0, 1]
62+
63+
# Vertical column lines at axis positions — structural anchors for the slopegraph
64+
for x in x_positions:
65+
ax.axvline(x, color=INK_SOFT, linewidth=1.2, alpha=0.35, zorder=0)
66+
5467
for i, (product, q1, q4, change) in enumerate(zip(products, q1_sales, q4_sales, changes, strict=True)):
55-
# Color by direction: blue for increase, yellow/orange for decrease
56-
color = "#306998" if change >= 0 else "#FFD43B"
57-
ax.plot(x_positions, [q1, q4], marker="o", markersize=12, linewidth=3, color=color)
58-
59-
# Labels at both endpoints with adjusted positions to avoid overlap
60-
ax.text(
61-
-0.05,
62-
q1_label_pos[i],
63-
f"{product}: ${q1:.1f}M",
64-
ha="right",
65-
va="center",
66-
fontsize=14,
68+
color = COLOR_INC if change >= 0 else COLOR_DEC
69+
ax.plot(
70+
x_positions,
71+
[q1, q4],
72+
marker="o",
73+
markersize=12,
74+
linewidth=3,
6775
color=color,
68-
fontweight="bold",
76+
markeredgecolor=PAGE_BG,
77+
markeredgewidth=1.5,
6978
)
70-
ax.text(1.05, q4_label_pos[i], f"${q4:.1f}M", ha="left", va="center", fontsize=14, color=color, fontweight="bold")
7179

72-
# Style the axes
73-
ax.set_xlim(-0.6, 1.6)
80+
# Left label: product name + Q1 value; dotted connector if label was nudged
81+
lpos = q1_label_pos[i]
82+
if abs(lpos - q1) > 0.05:
83+
ax.plot([-0.03, -0.03], [q1, lpos], color=color, linewidth=0.8, alpha=0.35, linestyle=":")
84+
ax.text(-0.06, lpos, f"{product}: ${q1:.1f}M", ha="right", va="center", fontsize=16, color=color, fontweight="bold")
85+
86+
# Right label: product name + Q4 value; dotted connector if label was nudged
87+
rpos = q4_label_pos[i]
88+
if abs(rpos - q4) > 0.05:
89+
ax.plot([1.03, 1.03], [q4, rpos], color=color, linewidth=0.8, alpha=0.35, linestyle=":")
90+
ax.text(1.06, rpos, f"{product}: ${q4:.1f}M", ha="left", va="center", fontsize=16, color=color, fontweight="bold")
91+
92+
# Style
93+
ax.set_xlim(-0.75, 1.75)
7494
ax.set_xticks(x_positions)
75-
ax.set_xticklabels(["Q1 2024", "Q4 2024"], fontsize=20, fontweight="bold")
76-
ax.set_ylabel("Sales (Millions $)", fontsize=20)
77-
ax.set_title("slope-basic · matplotlib · pyplots.ai", fontsize=24)
95+
ax.set_xticklabels(["Q1 2024", "Q4 2024"], fontsize=20, fontweight="bold", color=INK)
96+
ax.set_ylabel("Sales", fontsize=20, color=INK)
97+
ax.set_title("slope-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK)
98+
99+
ax.tick_params(axis="x", length=0)
100+
ax.tick_params(axis="y", labelsize=16, labelcolor=INK_SOFT, colors=INK_SOFT)
101+
102+
# FuncFormatter for y-axis: show units inline as "$XM" for self-documenting tick labels
103+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda val, _: f"${val:.0f}M"))
78104

79-
# Remove spines and set grid
80105
ax.spines["top"].set_visible(False)
81106
ax.spines["right"].set_visible(False)
82107
ax.spines["bottom"].set_visible(False)
83-
ax.tick_params(axis="y", labelsize=16)
84-
ax.tick_params(axis="x", length=0)
85-
ax.grid(True, axis="y", alpha=0.3, linestyle="--")
108+
ax.spines["left"].set_color(INK_SOFT)
109+
110+
ax.grid(True, axis="y", alpha=0.10, linewidth=0.8, color=INK)
86111

87-
# Add legend for color coding
112+
# Legend centered below title
88113
legend_elements = [
89-
Line2D([0], [0], color="#306998", linewidth=3, label="Increase"),
90-
Line2D([0], [0], color="#FFD43B", linewidth=3, label="Decrease"),
114+
Line2D(
115+
[0], [0], color=COLOR_INC, linewidth=3, marker="o", markersize=10, markeredgecolor=PAGE_BG, label="Increase"
116+
),
117+
Line2D(
118+
[0], [0], color=COLOR_DEC, linewidth=3, marker="o", markersize=10, markeredgecolor=PAGE_BG, label="Decrease"
119+
),
91120
]
92-
ax.legend(handles=legend_elements, loc="upper right", fontsize=16)
121+
leg = ax.legend(
122+
handles=legend_elements, loc="upper center", bbox_to_anchor=(0.5, 0.98), fontsize=16, frameon=True, ncol=2
123+
)
124+
leg.get_frame().set_facecolor(ELEVATED_BG)
125+
leg.get_frame().set_edgecolor(INK_SOFT)
126+
plt.setp(leg.get_texts(), color=INK_SOFT)
93127

94128
plt.tight_layout()
95-
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
129+
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

0 commit comments

Comments
 (0)