|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | line-retention-cohort: User Retention Curve by Cohort |
3 | 3 | Library: pygal 3.1.0 | Python 3.14.3 |
4 | 4 | Quality: 80/100 | Created: 2026-03-16 |
|
31 | 31 | retention.append(max(round(prev - max(drop, 0.5), 1), 5.0)) |
32 | 32 | retention_data[cohort] = retention |
33 | 33 |
|
34 | | -# Style |
| 34 | +# Style - reference line color first, then cohort colors fading from muted to vivid |
| 35 | +colors_with_opacity = ( |
| 36 | + "rgba(180, 60, 60, 0.6)", |
| 37 | + "rgba(48, 105, 152, 0.45)", |
| 38 | + "rgba(232, 119, 93, 0.55)", |
| 39 | + "rgba(80, 168, 110, 0.70)", |
| 40 | + "rgba(212, 168, 67, 0.85)", |
| 41 | + "rgba(139, 107, 174, 1.0)", |
| 42 | +) |
| 43 | + |
35 | 44 | custom_style = Style( |
36 | 45 | background="white", |
37 | 46 | plot_background="white", |
38 | 47 | foreground="#333", |
39 | 48 | foreground_strong="#333", |
40 | 49 | foreground_subtle="#cccccc", |
41 | | - colors=("#306998", "#E8775D", "#50A86E", "#D4A843", "#8B6BAE"), |
| 50 | + colors=colors_with_opacity, |
42 | 51 | title_font_size=72, |
43 | 52 | label_font_size=48, |
44 | 53 | major_label_font_size=42, |
|
50 | 59 | chart = pygal.Line( |
51 | 60 | width=4800, |
52 | 61 | height=2700, |
53 | | - title="line-retention-cohort · pygal · pyplots.ai", |
| 62 | + title="line-retention-cohort \u00b7 pygal \u00b7 pyplots.ai", |
54 | 63 | x_title="Weeks Since Signup", |
55 | 64 | y_title="Retained Users (%)", |
56 | 65 | style=custom_style, |
57 | 66 | show_dots=True, |
58 | 67 | dots_size=6, |
59 | | - stroke_style={"width": 5}, |
| 68 | + stroke_style={"width": 3}, |
60 | 69 | show_y_guides=True, |
61 | 70 | show_x_guides=False, |
62 | 71 | legend_at_bottom=True, |
63 | 72 | truncate_legend=-1, |
64 | | - range=(0, 100), |
| 73 | + range=(0, 105), |
65 | 74 | x_label_rotation=0, |
| 75 | + value_formatter=lambda x: f"{x:.0f}%" if x is not None else "", |
| 76 | + tooltip_fancy_mode=True, |
| 77 | + interpolate="cubic", |
66 | 78 | ) |
67 | 79 |
|
68 | 80 | chart.x_labels = [str(w) for w in weeks] |
69 | 81 |
|
70 | | -for cohort, params in cohorts.items(): |
| 82 | +# Add reference threshold line at 20% retention |
| 83 | +chart.add( |
| 84 | + "20% Retention Threshold", |
| 85 | + [20.0] * len(weeks), |
| 86 | + stroke_style={"width": 2, "dasharray": "12, 8"}, |
| 87 | + show_dots=False, |
| 88 | + dots_size=0, |
| 89 | +) |
| 90 | + |
| 91 | +# Add cohorts with increasing stroke width for newer cohorts |
| 92 | +stroke_widths = [2, 3, 4, 5, 7] |
| 93 | +dot_sizes = [4, 5, 5, 6, 8] |
| 94 | +cohort_list = list(cohorts.items()) |
| 95 | + |
| 96 | +for i, (cohort, params) in enumerate(cohort_list): |
71 | 97 | label = f"{cohort} (n={params['size']:,})" |
72 | | - chart.add(label, retention_data[cohort]) |
| 98 | + chart.add( |
| 99 | + label, |
| 100 | + [{"value": v, "label": f"Week {w}: {v:.1f}% retained"} for w, v in zip(weeks, retention_data[cohort], strict=True)], |
| 101 | + stroke_style={"width": stroke_widths[i]}, |
| 102 | + dots_size=dot_sizes[i], |
| 103 | + ) |
73 | 104 |
|
74 | 105 | # Save |
75 | 106 | chart.render_to_file("plot.html") |
|
0 commit comments