Skip to content

Commit c07a105

Browse files
feat(plotly): implement heatmap-cohort-retention (#4942)
## Implementation: `heatmap-cohort-retention` - plotly Implements the **plotly** version of `heatmap-cohort-retention`. **File:** `plots/heatmap-cohort-retention/implementations/plotly.py` **Parent Issue:** #4570 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23165008247)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 99676b5 commit c07a105

2 files changed

Lines changed: 387 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
""" pyplots.ai
2+
heatmap-cohort-retention: Cohort Retention Heatmap
3+
Library: plotly 6.6.0 | Python 3.14.3
4+
Quality: 91/100 | Created: 2026-03-16
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data
12+
np.random.seed(42)
13+
14+
cohort_labels = [
15+
"Jan 2024",
16+
"Feb 2024",
17+
"Mar 2024",
18+
"Apr 2024",
19+
"May 2024",
20+
"Jun 2024",
21+
"Jul 2024",
22+
"Aug 2024",
23+
"Sep 2024",
24+
"Oct 2024",
25+
]
26+
num_cohorts = len(cohort_labels)
27+
num_periods = num_cohorts
28+
cohort_sizes = [1200, 1350, 980, 1100, 1450, 1280, 1050, 1380, 1150, 900]
29+
30+
retention = np.full((num_cohorts, num_periods), np.nan)
31+
for i in range(num_cohorts):
32+
max_period = num_periods - i
33+
retention[i, 0] = 100.0
34+
for j in range(1, max_period):
35+
base_drop = np.exp(-0.25 * j) * 100
36+
noise = np.random.normal(0, 3)
37+
trend_bonus = i * 0.8
38+
retention[i, j] = np.clip(base_drop + noise + trend_bonus, 5, 100)
39+
40+
# Build hovertext and cell annotations with conditional coloring
41+
hover_text = []
42+
cell_annotations = []
43+
for i in range(num_cohorts):
44+
row_hover = []
45+
for j in range(num_periods):
46+
if np.isnan(retention[i, j]):
47+
row_hover.append("")
48+
else:
49+
val = retention[i, j]
50+
row_hover.append(
51+
f"<b>{cohort_labels[i]}</b> · Month {j}<br>"
52+
f"Cohort size: {cohort_sizes[i]:,} users<br>"
53+
f"Retained: <b>{val:.1f}%</b>"
54+
)
55+
cell_annotations.append(
56+
{
57+
"x": f"Month {j}",
58+
"y": f"{cohort_labels[i]} (n={cohort_sizes[i]:,})",
59+
"text": f"<b>{val:.0f}%</b>",
60+
"showarrow": False,
61+
"font": {"size": 15, "color": "#1a2e1a" if val < 45 else "white"},
62+
}
63+
)
64+
hover_text.append(row_hover)
65+
66+
# Y-axis labels with cohort size
67+
y_labels = [f"{label} (n={size:,})" for label, size in zip(cohort_labels, cohort_sizes, strict=False)]
68+
x_labels = [f"Month {i}" for i in range(num_periods)]
69+
70+
# Custom colorscale — teal-green sequential with good perceptual uniformity
71+
colorscale = [
72+
[0.0, "#f0f9f4"],
73+
[0.15, "#c6e7d4"],
74+
[0.30, "#7cc5a3"],
75+
[0.50, "#3a9d6e"],
76+
[0.70, "#1e7a4e"],
77+
[0.85, "#135c39"],
78+
[1.0, "#0a3d26"],
79+
]
80+
81+
# Plot
82+
fig = go.Figure(
83+
data=go.Heatmap(
84+
z=retention,
85+
x=x_labels,
86+
y=y_labels,
87+
showscale=True,
88+
hovertext=hover_text,
89+
hoverinfo="text",
90+
colorscale=colorscale,
91+
zmin=0,
92+
zmax=100,
93+
colorbar={
94+
"title": {"text": "Retention Rate", "font": {"size": 18, "color": "#2d2d2d"}},
95+
"tickfont": {"size": 16, "color": "#2d2d2d"},
96+
"ticksuffix": "%",
97+
"tickvals": [0, 20, 40, 60, 80, 100],
98+
"len": 0.75,
99+
"thickness": 18,
100+
"outlinewidth": 0,
101+
"x": 1.02,
102+
},
103+
xgap=3,
104+
ygap=3,
105+
)
106+
)
107+
108+
# Add cell annotations with conditional text coloring (dark on light, white on dark)
109+
for ann in cell_annotations:
110+
fig.add_annotation(**ann)
111+
112+
# Add a subtle annotation highlighting the improving trend
113+
fig.add_annotation(
114+
text="↑ Later cohorts retain better",
115+
xref="paper",
116+
yref="paper",
117+
x=0.0,
118+
y=-0.10,
119+
showarrow=False,
120+
font={"size": 15, "color": "#3a9d6e", "family": "Arial"},
121+
xanchor="left",
122+
)
123+
124+
# Style
125+
fig.update_layout(
126+
title={
127+
"text": "heatmap-cohort-retention · plotly · pyplots.ai",
128+
"font": {"size": 28, "color": "#1a1a1a", "family": "Arial Black, Arial"},
129+
"x": 0.5,
130+
"xanchor": "center",
131+
"y": 0.96,
132+
},
133+
xaxis={
134+
"title": {"text": "Months Since Signup", "font": {"size": 22, "color": "#2d2d2d"}, "standoff": 15},
135+
"tickfont": {"size": 17, "color": "#3d3d3d"},
136+
"side": "bottom",
137+
"dtick": 1,
138+
},
139+
yaxis={
140+
"title": {"text": "Signup Cohort", "font": {"size": 22, "color": "#2d2d2d"}, "standoff": 20},
141+
"tickfont": {"size": 16, "color": "#3d3d3d"},
142+
"autorange": "reversed",
143+
},
144+
template="plotly_white",
145+
width=1600,
146+
height=900,
147+
margin={"l": 195, "r": 90, "t": 75, "b": 95},
148+
paper_bgcolor="#fafafa",
149+
plot_bgcolor="#fafafa",
150+
font={"family": "Arial, Helvetica, sans-serif"},
151+
)
152+
153+
# Save
154+
fig.write_image("plot.png", width=1600, height=900, scale=3)
155+
fig.write_html("plot.html", include_plotlyjs="cdn")
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
library: plotly
2+
specification_id: heatmap-cohort-retention
3+
created: '2026-03-16T20:47:33Z'
4+
updated: '2026-03-16T20:59:38Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23165008247
7+
issue: 4570
8+
python_version: 3.14.3
9+
library_version: 6.6.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/heatmap-cohort-retention/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/heatmap-cohort-retention/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/heatmap-cohort-retention/plotly/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent triangular shape implementation with NaN masking for unavailable periods
17+
- Custom 7-stop teal-green colorscale with good perceptual uniformity
18+
- Smart conditional text coloring (dark on light cells, white on dark cells) for
19+
universal readability
20+
- Rich HTML hover tooltips showing cohort name, period, size, and retention — leveraging
21+
Plotly interactivity
22+
- Storytelling annotation guides viewer interpretation with improving trend across
23+
cohorts
24+
- Data shows realistic retention decay with plausible SaaS cohort sizes
25+
weaknesses:
26+
- Cell annotation font size (15) could be slightly larger for improved readability
27+
at full resolution
28+
- LM-01 could reach 5 by using plotly.express or more advanced Plotly patterns
29+
image_description: 'The plot displays a triangular cohort retention heatmap with
30+
10 monthly cohorts (Jan 2024 – Oct 2024) on the y-axis and Months 0–9 on the x-axis.
31+
A custom teal-green sequential colorscale runs from very light green (low retention,
32+
~9%) to deep forest green (100% retention). Each cell contains a bold percentage
33+
annotation with conditional text coloring — white text on dark cells, dark green
34+
text on light cells. The y-axis labels include cohort sizes in parentheses (e.g.,
35+
"Jan 2024 (n=1,200)"). The triangular shape is clearly visible: Jan 2024 spans
36+
all 10 months while Oct 2024 has only Month 0. A colorbar on the right displays
37+
"Retention Rate" with percentage ticks from 0–100%. A subtle green annotation
38+
at the bottom reads "↑ Later cohorts retain better". The background is a soft
39+
off-white (#fafafa) with the plotly_white template providing clean minimal chrome.
40+
Cell gaps of 3px create clear visual separation between cells.'
41+
criteria_checklist:
42+
visual_quality:
43+
score: 29
44+
max: 30
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 7
49+
max: 8
50+
passed: true
51+
comment: All font sizes explicitly set (title 28, axes 22, ticks 16-17, cell
52+
text 15, colorbar 18/16). Cell annotation font slightly small.
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 6
56+
max: 6
57+
passed: true
58+
comment: No overlapping text. Cell gaps and margins prevent collisions.
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 6
62+
max: 6
63+
passed: true
64+
comment: Heatmap cells well-sized with 3px gaps for clear separation.
65+
- id: VQ-04
66+
name: Color Accessibility
67+
score: 4
68+
max: 4
69+
passed: true
70+
comment: Green sequential colorscale is colorblind-safe. Conditional text
71+
coloring ensures readability.
72+
- id: VQ-05
73+
name: Layout & Canvas
74+
score: 4
75+
max: 4
76+
passed: true
77+
comment: Plot fills canvas well with balanced margins. Triangular white space
78+
is inherent to data.
79+
- id: VQ-06
80+
name: Axis Labels & Title
81+
score: 2
82+
max: 2
83+
passed: true
84+
comment: 'Descriptive labels: Months Since Signup, Signup Cohort.'
85+
design_excellence:
86+
score: 14
87+
max: 20
88+
items:
89+
- id: DE-01
90+
name: Aesthetic Sophistication
91+
score: 6
92+
max: 8
93+
passed: true
94+
comment: Custom 7-stop teal-green colorscale, conditional text coloring, off-white
95+
background, cell gaps. Clearly above defaults.
96+
- id: DE-02
97+
name: Visual Refinement
98+
score: 4
99+
max: 6
100+
passed: true
101+
comment: plotly_white template, 3px cell gaps, generous whitespace, custom
102+
background color.
103+
- id: DE-03
104+
name: Data Storytelling
105+
score: 4
106+
max: 6
107+
passed: true
108+
comment: Storytelling annotation guides interpretation. Improving trend across
109+
cohorts creates a clear narrative.
110+
spec_compliance:
111+
score: 15
112+
max: 15
113+
items:
114+
- id: SC-01
115+
name: Plot Type
116+
score: 5
117+
max: 5
118+
passed: true
119+
comment: Correct triangular heatmap with NaN masking.
120+
- id: SC-02
121+
name: Required Features
122+
score: 4
123+
max: 4
124+
passed: true
125+
comment: 'All spec features present: triangular shape, 100% at period 0, cell
126+
text, cohort sizes, colorbar, green colorscale.'
127+
- id: SC-03
128+
name: Data Mapping
129+
score: 3
130+
max: 3
131+
passed: true
132+
comment: X=months since signup, Y=cohorts (reversed), Z=retention rate.
133+
- id: SC-04
134+
name: Title & Legend
135+
score: 3
136+
max: 3
137+
passed: true
138+
comment: Title matches required format. Colorbar serves as legend with Retention
139+
Rate label.
140+
data_quality:
141+
score: 15
142+
max: 15
143+
items:
144+
- id: DQ-01
145+
name: Feature Coverage
146+
score: 6
147+
max: 6
148+
passed: true
149+
comment: Shows retention decay, triangular shape, cohort size variation, improving
150+
trend, full range 9-100%.
151+
- id: DQ-02
152+
name: Realistic Context
153+
score: 5
154+
max: 5
155+
passed: true
156+
comment: SaaS monthly cohort retention is a classic neutral business analytics
157+
scenario.
158+
- id: DQ-03
159+
name: Appropriate Scale
160+
score: 4
161+
max: 4
162+
passed: true
163+
comment: Realistic exponential decay, plausible cohort sizes (900-1450).
164+
code_quality:
165+
score: 10
166+
max: 10
167+
items:
168+
- id: CQ-01
169+
name: KISS Structure
170+
score: 3
171+
max: 3
172+
passed: true
173+
comment: 'Clean linear flow: imports, data, hover/annotations, plot, style,
174+
save.'
175+
- id: CQ-02
176+
name: Reproducibility
177+
score: 2
178+
max: 2
179+
passed: true
180+
comment: np.random.seed(42) ensures deterministic output.
181+
- id: CQ-03
182+
name: Clean Imports
183+
score: 2
184+
max: 2
185+
passed: true
186+
comment: Only numpy and plotly.graph_objects, both fully utilized.
187+
- id: CQ-04
188+
name: Code Elegance
189+
score: 2
190+
max: 2
191+
passed: true
192+
comment: Appropriate complexity. No fake UI or over-engineering.
193+
- id: CQ-05
194+
name: Output & API
195+
score: 1
196+
max: 1
197+
passed: true
198+
comment: Saves plot.png at 4800x2700 (1600x900 scale=3). Also exports HTML.
199+
library_mastery:
200+
score: 8
201+
max: 10
202+
items:
203+
- id: LM-01
204+
name: Idiomatic Usage
205+
score: 4
206+
max: 5
207+
passed: true
208+
comment: Good use of go.Heatmap with proper parameters, fig.add_annotation,
209+
update_layout.
210+
- id: LM-02
211+
name: Distinctive Features
212+
score: 4
213+
max: 5
214+
passed: true
215+
comment: Rich HTML hover tooltips, HTML export, colorbar customization — distinctly
216+
Plotly capabilities.
217+
verdict: APPROVED
218+
impl_tags:
219+
dependencies: []
220+
techniques:
221+
- annotations
222+
- colorbar
223+
- hover-tooltips
224+
- html-export
225+
patterns:
226+
- data-generation
227+
- matrix-construction
228+
- iteration-over-groups
229+
dataprep: []
230+
styling:
231+
- custom-colormap
232+
- edge-highlighting

0 commit comments

Comments
 (0)