Skip to content

Commit fd94db3

Browse files
Merge branch 'main' into implementation/slope-basic/plotly
2 parents 09a1c4a + 755bfd0 commit fd94db3

4 files changed

Lines changed: 524 additions & 374 deletions

File tree

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)
Lines changed: 92 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,141 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
slope-basic: Basic Slope Chart (Slopegraph)
3-
Library: seaborn 0.13.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: seaborn 0.13.2 | Python 3.13.13
4+
Quality: 83/100 | Updated: 2026-04-30
55
"""
66

7+
import os
8+
79
import matplotlib.pyplot as plt
810
import pandas as pd
911
import seaborn as sns
1012
from matplotlib.lines import Line2D
1113

1214

13-
# Data - Tech company revenue growth comparison Q1 vs Q4
14-
# Values spaced to avoid label overlap
15+
# Theme tokens
16+
THEME = os.getenv("ANYPLOT_THEME", "light")
17+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
21+
22+
COLOR_INCREASE = "#009E73" # Okabe-Ito position 1
23+
COLOR_DECREASE = "#D55E00" # Okabe-Ito position 2
24+
25+
sns.set_theme(
26+
style="ticks",
27+
rc={
28+
"figure.facecolor": PAGE_BG,
29+
"axes.facecolor": PAGE_BG,
30+
"axes.edgecolor": INK_SOFT,
31+
"axes.labelcolor": INK,
32+
"text.color": INK,
33+
"xtick.color": INK_SOFT,
34+
"ytick.color": INK_SOFT,
35+
"grid.color": INK,
36+
"grid.alpha": 0.10,
37+
"legend.facecolor": ELEVATED_BG,
38+
"legend.edgecolor": INK_SOFT,
39+
},
40+
)
41+
42+
# Data — tech company revenue comparison Q1 vs Q4 (four rank crossings)
1543
data = {
16-
"entity": ["TechCorp", "DataFlow", "CloudNine", "NetWorks", "CodeBase", "ByteSize", "LogicPro", "SoftEdge"],
17-
"Q1 Revenue ($M)": [145, 72, 215, 52, 178, 98, 125, 162],
18-
"Q4 Revenue ($M)": [192, 108, 178, 72, 225, 82, 155, 138],
44+
"entity": ["StreamPeak", "DataCore", "CloudSync", "NetPulse", "CodeBase", "ByteFlow", "LogicGrid", "TechVault"],
45+
"Q1 ($M)": [50, 110, 165, 220, 275, 325, 378, 430],
46+
"Q4 ($M)": [95, 60, 230, 178, 335, 268, 415, 368],
1947
}
2048

2149
df = pd.DataFrame(data)
22-
23-
# Calculate change for color coding
24-
df["change"] = df["Q4 Revenue ($M)"] - df["Q1 Revenue ($M)"]
50+
df["change"] = df["Q4 ($M)"] - df["Q1 ($M)"]
2551
df["direction"] = df["change"].apply(lambda x: "Increase" if x > 0 else "Decrease")
52+
df = df.sort_values("Q1 ($M)").reset_index(drop=True)
2653

27-
# Sort by Q1 value for better visual layout
28-
df = df.sort_values("Q1 Revenue ($M)").reset_index(drop=True)
29-
30-
# Create figure
31-
fig, ax = plt.subplots(figsize=(16, 9))
32-
sns.set_style("whitegrid")
33-
34-
# Define colors - Python palette
35-
colors = {"Increase": "#306998", "Decrease": "#FFD43B"}
36-
37-
# Reshape data for seaborn
3854
df_melted = df.melt(
39-
id_vars=["entity", "direction"],
40-
value_vars=["Q1 Revenue ($M)", "Q4 Revenue ($M)"],
41-
var_name="Period",
42-
value_name="Revenue",
55+
id_vars=["entity", "direction"], value_vars=["Q1 ($M)", "Q4 ($M)"], var_name="Period", value_name="Revenue ($M)"
56+
)
57+
df_melted["period_num"] = df_melted["Period"].map({"Q1 ($M)": 0, "Q4 ($M)": 1})
58+
59+
# Plot
60+
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
61+
ax.set_facecolor(PAGE_BG)
62+
63+
palette = {"Increase": COLOR_INCREASE, "Decrease": COLOR_DECREASE}
64+
65+
sns.lineplot(
66+
data=df_melted,
67+
x="period_num",
68+
y="Revenue ($M)",
69+
hue="direction",
70+
units="entity",
71+
estimator=None,
72+
palette=palette,
73+
linewidth=3.5,
74+
marker="o",
75+
markersize=12,
76+
alpha=0.85,
77+
legend=False,
78+
ax=ax,
4379
)
4480

45-
# Plot lines for each entity using seaborn
46-
for entity in df["entity"]:
47-
entity_data = df_melted[df_melted["entity"] == entity]
48-
direction = entity_data["direction"].iloc[0]
49-
sns.lineplot(
50-
data=entity_data,
51-
x="Period",
52-
y="Revenue",
53-
color=colors[direction],
54-
linewidth=3.5,
55-
alpha=0.85,
56-
marker="o",
57-
markersize=14,
58-
ax=ax,
59-
)
60-
61-
# Add entity labels at both ends
62-
for _idx, row in df.iterrows():
63-
color = colors[row["direction"]]
64-
q1_val = row["Q1 Revenue ($M)"]
65-
q4_val = row["Q4 Revenue ($M)"]
81+
# Endpoint labels at both axes
82+
for _, row in df.iterrows():
83+
color = palette[row["direction"]]
84+
q1_val = int(row["Q1 ($M)"])
85+
q4_val = int(row["Q4 ($M)"])
6686

67-
# Left labels (Q1)
6887
ax.annotate(
6988
f"{row['entity']} ({q1_val})",
7089
xy=(0, q1_val),
71-
xytext=(-12, 0),
90+
xytext=(-14, 0),
7291
textcoords="offset points",
73-
fontsize=15,
92+
fontsize=14,
7493
color=color,
7594
ha="right",
7695
va="center",
7796
fontweight="bold",
7897
)
79-
# Right labels (Q4)
8098
ax.annotate(
8199
f"({q4_val}) {row['entity']}",
82100
xy=(1, q4_val),
83-
xytext=(12, 0),
101+
xytext=(14, 0),
84102
textcoords="offset points",
85-
fontsize=15,
103+
fontsize=14,
86104
color=color,
87105
ha="left",
88106
va="center",
89107
fontweight="bold",
90108
)
91109

92-
# Style adjustments
110+
# Style
111+
ax.set_xticks([0, 1])
112+
ax.set_xticklabels(["Q1 Revenue ($M)", "Q4 Revenue ($M)"], fontsize=16, color=INK_SOFT)
93113
ax.set_xlabel("")
94-
ax.set_ylabel("Revenue ($M)", fontsize=20)
95-
ax.set_title("slope-basic · seaborn · pyplots.ai", fontsize=24, fontweight="bold", pad=20)
96-
ax.tick_params(axis="both", labelsize=16)
114+
ax.set_ylabel("Revenue ($M)", fontsize=20, color=INK)
115+
ax.set_title("slope-basic · seaborn · anyplot.ai", fontsize=24, fontweight="bold", pad=20, color=INK)
116+
ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT)
97117
ax.set_xlim(-0.15, 1.15)
98118

99-
# Add padding to y-axis for labels
100-
y_min = min(df["Q1 Revenue ($M)"].min(), df["Q4 Revenue ($M)"].min())
101-
y_max = max(df["Q1 Revenue ($M)"].max(), df["Q4 Revenue ($M)"].max())
102-
y_padding = (y_max - y_min) * 0.08
119+
y_min = min(df["Q1 ($M)"].min(), df["Q4 ($M)"].min())
120+
y_max = max(df["Q1 ($M)"].max(), df["Q4 ($M)"].max())
121+
y_padding = (y_max - y_min) * 0.10
103122
ax.set_ylim(y_min - y_padding, y_max + y_padding)
104123

105-
ax.grid(True, alpha=0.3, linestyle="--")
124+
ax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)
125+
ax.spines["top"].set_visible(False)
126+
ax.spines["right"].set_visible(False)
127+
for spine in ("left", "bottom"):
128+
ax.spines[spine].set_color(INK_SOFT)
106129

107-
# Add legend for direction
130+
# Legend
108131
legend_elements = [
109-
Line2D([0], [0], color="#306998", linewidth=3.5, marker="o", markersize=10, label="Increase"),
110-
Line2D([0], [0], color="#FFD43B", linewidth=3.5, marker="o", markersize=10, label="Decrease"),
132+
Line2D([0], [0], color=COLOR_INCREASE, linewidth=3.5, marker="o", markersize=10, label="Increase"),
133+
Line2D([0], [0], color=COLOR_DECREASE, linewidth=3.5, marker="o", markersize=10, label="Decrease"),
111134
]
112-
ax.legend(handles=legend_elements, loc="lower right", fontsize=16, framealpha=0.9)
135+
ax.legend(
136+
handles=legend_elements, loc="lower right", fontsize=16, framealpha=0.9, facecolor=ELEVATED_BG, edgecolor=INK_SOFT
137+
)
113138

114139
plt.tight_layout()
115140
plt.subplots_adjust(left=0.22, right=0.78)
116-
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
141+
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

0 commit comments

Comments
 (0)