|
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: 81/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 - reference line color first, then cohort colors fading from muted to vivid |
| 34 | +# Style - threshold line first, then cohort colors with higher minimum opacity |
35 | 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)", |
| 36 | + "rgba(200, 50, 50, 0.85)", |
| 37 | + "rgba(48, 105, 152, 0.65)", |
| 38 | + "rgba(220, 100, 70, 0.75)", |
| 39 | + "rgba(60, 160, 100, 0.85)", |
| 40 | + "rgba(200, 155, 50, 0.92)", |
| 41 | + "rgba(130, 90, 170, 1.0)", |
42 | 42 | ) |
43 | 43 |
|
44 | 44 | custom_style = Style( |
45 | | - background="white", |
46 | | - plot_background="white", |
47 | | - foreground="#333", |
48 | | - foreground_strong="#333", |
49 | | - foreground_subtle="#cccccc", |
| 45 | + background="#fafafa", |
| 46 | + plot_background="#fafafa", |
| 47 | + foreground="#2a2a2a", |
| 48 | + foreground_strong="#1a1a1a", |
| 49 | + foreground_subtle="#d8d8d8", |
50 | 50 | colors=colors_with_opacity, |
51 | 51 | title_font_size=72, |
52 | 52 | label_font_size=48, |
53 | 53 | major_label_font_size=42, |
54 | 54 | legend_font_size=40, |
55 | 55 | value_font_size=36, |
| 56 | + opacity=".95", |
| 57 | + opacity_hover="1", |
| 58 | + transition="200ms ease-in", |
| 59 | + font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
| 60 | + title_font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
| 61 | + legend_font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
| 62 | + label_font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
| 63 | + major_label_font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
| 64 | + value_font_family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif", |
56 | 65 | ) |
57 | 66 |
|
58 | 67 | # Plot |
|
69 | 78 | show_y_guides=True, |
70 | 79 | show_x_guides=False, |
71 | 80 | legend_at_bottom=True, |
| 81 | + legend_at_bottom_columns=3, |
72 | 82 | truncate_legend=-1, |
73 | 83 | range=(0, 105), |
74 | 84 | x_label_rotation=0, |
75 | 85 | value_formatter=lambda x: f"{x:.0f}%" if x is not None else "", |
76 | 86 | tooltip_fancy_mode=True, |
| 87 | + tooltip_border_radius=6, |
77 | 88 | interpolate="cubic", |
| 89 | + show_minor_x_labels=False, |
| 90 | + y_labels=[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], |
| 91 | + margin_top=40, |
| 92 | + margin_bottom=60, |
| 93 | + spacing=30, |
78 | 94 | ) |
79 | 95 |
|
80 | 96 | chart.x_labels = [str(w) for w in weeks] |
| 97 | +chart.x_labels_major = ["0", "3", "6", "9", "12"] |
81 | 98 |
|
82 | | -# Add reference threshold line at 20% retention |
| 99 | +# Add reference threshold line at 20% retention - bold and visible |
83 | 100 | chart.add( |
84 | | - "20% Retention Threshold", |
| 101 | + "\u2500\u2500 20% Retention Threshold", |
85 | 102 | [20.0] * len(weeks), |
86 | | - stroke_style={"width": 2, "dasharray": "12, 8"}, |
| 103 | + stroke_style={"width": 3, "dasharray": "16, 8"}, |
87 | 104 | show_dots=False, |
88 | 105 | dots_size=0, |
89 | 106 | ) |
90 | 107 |
|
91 | 108 | # Add cohorts with increasing stroke width for newer cohorts |
92 | | -stroke_widths = [2, 3, 4, 5, 7] |
| 109 | +stroke_widths = [3, 3.5, 4, 5, 7] |
93 | 110 | dot_sizes = [4, 5, 5, 6, 8] |
94 | 111 | cohort_list = list(cohorts.items()) |
95 | 112 |
|
96 | 113 | for i, (cohort, params) in enumerate(cohort_list): |
97 | 114 | label = f"{cohort} (n={params['size']:,})" |
98 | 115 | chart.add( |
99 | 116 | label, |
100 | | - [{"value": v, "label": f"Week {w}: {v:.1f}% retained"} for w, v in zip(weeks, retention_data[cohort], strict=True)], |
| 117 | + [ |
| 118 | + {"value": v, "label": f"Week {w}: {v:.1f}% retained"} |
| 119 | + for w, v in zip(weeks, retention_data[cohort], strict=True) |
| 120 | + ], |
101 | 121 | stroke_style={"width": stroke_widths[i]}, |
102 | 122 | dots_size=dot_sizes[i], |
103 | 123 | ) |
|
0 commit comments