Skip to content

Commit 80354c5

Browse files
feat(matplotlib): implement line-retention-cohort (#4927)
## Implementation: `line-retention-cohort` - matplotlib Implements the **matplotlib** version of `line-retention-cohort`. **File:** `plots/line-retention-cohort/implementations/matplotlib.py` **Parent Issue:** #4572 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23164943198)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 0dd291f commit 80354c5

2 files changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
""" pyplots.ai
2+
line-retention-cohort: User Retention Curve by Cohort
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: 91/100 | Created: 2026-03-16
5+
"""
6+
7+
import matplotlib.patheffects as pe
8+
import matplotlib.pyplot as plt
9+
import matplotlib.ticker as mticker
10+
import numpy as np
11+
12+
13+
# Data
14+
np.random.seed(42)
15+
16+
cohorts = {
17+
"Jan 2025": {"size": 1245, "base_rate": 0.82, "plateau": 8},
18+
"Feb 2025": {"size": 1102, "base_rate": 0.80, "plateau": 12},
19+
"Mar 2025": {"size": 1380, "base_rate": 0.78, "plateau": 15},
20+
"Apr 2025": {"size": 1510, "base_rate": 0.85, "plateau": 22},
21+
"May 2025": {"size": 1423, "base_rate": 0.88, "plateau": 30},
22+
}
23+
24+
weeks = np.arange(0, 13)
25+
26+
retention_data = {}
27+
for cohort, info in cohorts.items():
28+
retention = [100.0]
29+
for week in weeks[1:]:
30+
decay = info["base_rate"] ** week * 100
31+
plateau = info["plateau"]
32+
value = max(decay, plateau) + np.random.normal(0, 1.0)
33+
value = max(value, plateau - 2)
34+
retention.append(round(value, 1))
35+
retention_data[cohort] = retention
36+
37+
# Plot
38+
fig, ax = plt.subplots(figsize=(16, 9))
39+
40+
colors = ["#4e79a7", "#a0cbe8", "#306998", "#59a14f", "#1a6b4a"]
41+
linewidths = [2.0, 2.2, 2.5, 2.8, 3.2]
42+
alphas = [0.6, 0.65, 0.75, 0.88, 1.0]
43+
marker_sizes = [5, 5.5, 6, 6.5, 7]
44+
45+
for i, (cohort, retention) in enumerate(retention_data.items()):
46+
size = cohorts[cohort]["size"]
47+
label = f"{cohort} (n={size:,})"
48+
ax.plot(
49+
weeks,
50+
retention,
51+
color=colors[i],
52+
linewidth=linewidths[i],
53+
alpha=alphas[i],
54+
marker="o",
55+
markersize=marker_sizes[i],
56+
markeredgecolor="white",
57+
markeredgewidth=0.8,
58+
label=label,
59+
zorder=2 + i,
60+
path_effects=[pe.Stroke(linewidth=linewidths[i] + 1.5, foreground="white"), pe.Normal()],
61+
)
62+
63+
# Reference line
64+
ax.axhline(y=20, color="#aaaaaa", linestyle="--", linewidth=1.2, alpha=0.7, zorder=1)
65+
ax.annotate(
66+
"20% retention target",
67+
xy=(12, 20),
68+
xytext=(10.5, 24),
69+
fontsize=13,
70+
color="#888888",
71+
fontstyle="italic",
72+
arrowprops={"arrowstyle": "-", "color": "#aaaaaa", "lw": 0.8},
73+
)
74+
75+
# Y-axis percentage formatter
76+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{int(x)}%"))
77+
78+
# Style
79+
ax.set_xlabel("Weeks Since Signup", fontsize=20)
80+
ax.set_ylabel("Retained Users", fontsize=20)
81+
ax.set_title("line-retention-cohort · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=16)
82+
ax.tick_params(axis="both", labelsize=16)
83+
84+
ax.set_xlim(-0.3, 12.5)
85+
ax.set_ylim(0, 105)
86+
ax.set_xticks(weeks)
87+
ax.set_yticks([0, 20, 40, 60, 80, 100])
88+
89+
ax.spines["top"].set_visible(False)
90+
ax.spines["right"].set_visible(False)
91+
ax.spines["left"].set_color("#cccccc")
92+
ax.spines["bottom"].set_color("#cccccc")
93+
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color="#888888")
94+
95+
ax.legend(fontsize=16, loc="upper right", framealpha=0.92, edgecolor="#dddddd", fancybox=False)
96+
97+
plt.tight_layout()
98+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
library: matplotlib
2+
specification_id: line-retention-cohort
3+
created: '2026-03-16T20:43:57Z'
4+
updated: '2026-03-16T20:54:59Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23164943198
7+
issue: 4572
8+
python_version: 3.14.3
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-retention-cohort/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-retention-cohort/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent visual hierarchy with graduated line width, opacity, and color from
17+
older to newer cohorts
18+
- Path effects (white stroke) elegantly separate overlapping curves — distinctive
19+
matplotlib technique
20+
- Complete spec compliance with all required features including reference line,
21+
cohort sizes, opacity variation
22+
- Realistic, well-calibrated data with meaningful variation across cohorts
23+
weaknesses:
24+
- The three blue shades (Jan/Feb/Mar) are somewhat close in hue; wider separation
25+
would improve distinguishability
26+
image_description: The plot displays 5 user retention curves (Jan–May 2025 cohorts)
27+
on a 16:9 canvas. Colors transition from light blue (Jan 2025) through medium
28+
blue (Feb, Mar) to green shades (Apr, May), with line width and opacity increasing
29+
for newer cohorts. All curves start at 100% at week 0 and exhibit exponential
30+
decay. A dashed gray horizontal reference line at 20% is labeled "20% retention
31+
target" in italic. The Y-axis shows percentages from 0% to 100% with gridlines;
32+
the X-axis shows weeks 0–12. The legend in the upper right lists each cohort with
33+
sample size (e.g., "Jan 2025 (n=1,245)"). Top and right spines are removed; left/bottom
34+
spines are subtle gray. White stroke path effects separate overlapping lines.
35+
Newer cohorts (Apr, May) clearly retain better, plateauing around 20–30%, while
36+
older cohorts (Jan, Feb) drop below 15%.
37+
criteria_checklist:
38+
visual_quality:
39+
score: 29
40+
max: 30
41+
items:
42+
- id: VQ-01
43+
name: Text Legibility
44+
score: 8
45+
max: 8
46+
passed: true
47+
comment: 'All font sizes explicitly set: title=24, labels=20, ticks=16, legend=16,
48+
annotation=13'
49+
- id: VQ-02
50+
name: No Overlap
51+
score: 6
52+
max: 6
53+
passed: true
54+
comment: No text collisions; legend positioned clear of data
55+
- id: VQ-03
56+
name: Element Visibility
57+
score: 6
58+
max: 6
59+
passed: true
60+
comment: Lines well-sized with markers and path effects for separation
61+
- id: VQ-04
62+
name: Color Accessibility
63+
score: 3
64+
max: 4
65+
passed: true
66+
comment: Blue-to-green gradient is colorblind-safe but three blue shades are
67+
somewhat close
68+
- id: VQ-05
69+
name: Layout & Canvas
70+
score: 4
71+
max: 4
72+
passed: true
73+
comment: 16:9 figsize with tight_layout, good canvas utilization
74+
- id: VQ-06
75+
name: Axis Labels & Title
76+
score: 2
77+
max: 2
78+
passed: true
79+
comment: Descriptive labels with % formatter on Y-axis
80+
design_excellence:
81+
score: 15
82+
max: 20
83+
items:
84+
- id: DE-01
85+
name: Aesthetic Sophistication
86+
score: 6
87+
max: 8
88+
passed: true
89+
comment: Cohesive blue-to-green palette, path effects, graduated opacity/width
90+
— above defaults
91+
- id: DE-02
92+
name: Visual Refinement
93+
score: 5
94+
max: 6
95+
passed: true
96+
comment: Spines removed, subtle grid, generous whitespace, polished details
97+
- id: DE-03
98+
name: Data Storytelling
99+
score: 4
100+
max: 6
101+
passed: true
102+
comment: Visual hierarchy through line weight/opacity emphasizes newer cohorts;
103+
20% target provides context
104+
spec_compliance:
105+
score: 15
106+
max: 15
107+
items:
108+
- id: SC-01
109+
name: Plot Type
110+
score: 5
111+
max: 5
112+
passed: true
113+
comment: Correct line chart with multiple retention curves
114+
- id: SC-02
115+
name: Required Features
116+
score: 4
117+
max: 4
118+
passed: true
119+
comment: 'All spec features present: 100% start, distinct colors, legend with
120+
sizes, reference line, opacity variation'
121+
- id: SC-03
122+
name: Data Mapping
123+
score: 3
124+
max: 3
125+
passed: true
126+
comment: X=weeks since signup, Y=retention percentage, correctly mapped
127+
- id: SC-04
128+
name: Title & Legend
129+
score: 3
130+
max: 3
131+
passed: true
132+
comment: Correct title format and legend labels with cohort sizes
133+
data_quality:
134+
score: 15
135+
max: 15
136+
items:
137+
- id: DQ-01
138+
name: Feature Coverage
139+
score: 6
140+
max: 6
141+
passed: true
142+
comment: 5 cohorts with distinct decay rates and plateau levels showing improvement
143+
trend
144+
- id: DQ-02
145+
name: Realistic Context
146+
score: 5
147+
max: 5
148+
passed: true
149+
comment: Monthly SaaS signup cohorts — realistic, neutral business analytics
150+
scenario
151+
- id: DQ-03
152+
name: Appropriate Scale
153+
score: 4
154+
max: 4
155+
passed: true
156+
comment: Cohort sizes 1100-1510, retention 8-100% over 12 weeks — plausible
157+
values
158+
code_quality:
159+
score: 10
160+
max: 10
161+
items:
162+
- id: CQ-01
163+
name: KISS Structure
164+
score: 3
165+
max: 3
166+
passed: true
167+
comment: 'Linear flow: imports, data, plot, style, save'
168+
- id: CQ-02
169+
name: Reproducibility
170+
score: 2
171+
max: 2
172+
passed: true
173+
comment: np.random.seed(42) set
174+
- id: CQ-03
175+
name: Clean Imports
176+
score: 2
177+
max: 2
178+
passed: true
179+
comment: All imports used
180+
- id: CQ-04
181+
name: Code Elegance
182+
score: 2
183+
max: 2
184+
passed: true
185+
comment: Clean parallel lists for per-cohort styling, appropriate complexity
186+
- id: CQ-05
187+
name: Output & API
188+
score: 1
189+
max: 1
190+
passed: true
191+
comment: Saves plot.png with dpi=300
192+
library_mastery:
193+
score: 7
194+
max: 10
195+
items:
196+
- id: LM-01
197+
name: Idiomatic Usage
198+
score: 4
199+
max: 5
200+
passed: true
201+
comment: Proper Axes methods, FuncFormatter, path_effects, spine control
202+
- id: LM-02
203+
name: Distinctive Features
204+
score: 3
205+
max: 5
206+
passed: true
207+
comment: path_effects with Stroke+Normal for white halo is matplotlib-specific
208+
verdict: APPROVED
209+
impl_tags:
210+
dependencies: []
211+
techniques:
212+
- annotations
213+
- manual-ticks
214+
patterns:
215+
- data-generation
216+
- iteration-over-groups
217+
dataprep: []
218+
styling:
219+
- alpha-blending
220+
- edge-highlighting
221+
- grid-styling

0 commit comments

Comments
 (0)