Skip to content

Commit 21970d3

Browse files
authored
Merge pull request #9 from opensource-observer/carl/oso-2047-migrate-insights-notebooks-from-inline-styles-to-css-classes
carl/oso 2047 migrate insights notebooks from inline styles to css classes
2 parents 1e49343 + c425e58 commit 21970d3

10 files changed

Lines changed: 594 additions & 559 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ app/public/data/
3434
# --- Claude Code (local settings) ---
3535
.claude/
3636
.mcp.json
37+
.playwright-mcp/
3738

3839
# --- Editor / IDE ---
3940
.vscode/

notebooks/insights/defi-builder-journeys.py

Lines changed: 71 additions & 74 deletions
Large diffs are not rendered by default.

notebooks/insights/developer-lifecycle.py

Lines changed: 67 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,10 @@
66

77
@app.cell(hide_code=True)
88
def header_title(mo):
9-
mo.md("""
10-
# Lifecycle Analysis
11-
<small>Owner: <span style="background-color: #f0f0f0; padding: 2px 4px; border-radius: 3px;">OSO Team</span> · Last Updated: <span style="background-color: #f0f0f0; padding: 2px 4px; border-radius: 3px;">2026-02-17</span></small>
12-
13-
Visualize the full lifecycle of a developer joining, contributing, and leaving an ecosystem.
14-
""")
9+
mo.Html('<div class="ddp-header"><h1>Lifecycle Analysis</h1><p>Tracking how developers move through lifecycle states across crypto ecosystems.</p><div class="ddp-header-meta"><span>Created: <span class="ddp-badge">2026-03-16</span></span></div></div>')
1510
return
1611

1712

18-
@app.cell(hide_code=True)
19-
def header_accordion(mo):
20-
mo.accordion({
21-
"Overview": mo.md("""
22-
- This notebook tracks developer lifecycle states — the month-by-month progression of developers joining, contributing, and eventually churning from an ecosystem
23-
- It reveals how the balance between newcomers, established contributors, and churned developers shifts over time and across ecosystems
24-
- Key metrics: monthly active developers by lifecycle state, churn ratio, dormant developer count
25-
"""),
26-
"Context": mo.md("""
27-
**Lifecycle labels** classify each developer's monthly activity into one of 16 granular states. These roll up into 4 categories used in the summary chart:
28-
29-
| Category | Label | Description |
30-
|:---------|:------|:------------|
31-
| **First Time** | `first time` | First-ever contribution to the ecosystem |
32-
| **Full Time** | `full time` | 10+ active days, continuing from prior month |
33-
| | `new full time` | First month reaching 10+ active days |
34-
| | `part time to full time` | Transitioned from part-time level |
35-
| | `dormant to full time` | Returned from dormancy at full-time level |
36-
| **Part Time** | `part time` | 1-9 active days, continuing from prior month |
37-
| | `new part time` | First month at part-time level |
38-
| | `full time to part time` | Stepped down from full-time level |
39-
| | `dormant to part time` | Returned from dormancy at part-time level |
40-
| **Churned / Dormant** | `dormant` | No activity this month (previously active) |
41-
| | `first time to dormant` | Dormant after first contribution |
42-
| | `part time to dormant` | Dormant after part-time activity |
43-
| | `full time to dormant` | Dormant after full-time activity |
44-
| | `churned (after first time)` | Extended inactivity after first contribution |
45-
| | `churned (after reaching part time)` | Extended inactivity after reaching part time |
46-
| | `churned (after reaching full time)` | Extended inactivity after reaching full time |
47-
48-
**Active** = First Time + Full Time + Part Time (all 9 labels above the Churned/Dormant group)
49-
50-
**Churn Ratio** = sum(churned + dormant) / sum(active) over the trailing window (12mo or all-time)
51-
52-
Data is bucketed monthly; private repos excluded; contributions include commits, issues, pull requests, and code reviews.
53-
54-
**Metric Definitions**
55-
- Lifecycle — Developer stage definitions
56-
- Activity — Monthly Active Developer (MAD) methodology
57-
"""),
58-
"Data Sources": mo.md("""
59-
- **Open Dev Data (Electric Capital)** — Ecosystem and developer taxonomy, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems)
60-
- **Key Models** — `oso.int_crypto_ecosystems_developer_lifecycle_monthly_aggregated`
61-
"""),
62-
})
63-
return
6413

6514

6615
@app.cell(hide_code=True)
@@ -100,10 +49,10 @@ def ecosystem_overview_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_LA
10049

10150
def _stat(value, label, caption=''):
10251
return (
103-
f'<div style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;flex:1;min-width:140px">'
104-
f'<div style="font-size:20px;font-weight:700;color:#111">{value}</div>'
105-
f'<div style="font-size:12px;font-weight:600;color:#374151;margin-top:2px">{label}</div>'
106-
+ (f'<div style="font-size:11px;color:#9ca3af;margin-top:2px">{caption}</div>' if caption else '')
52+
f'<div class="ddp-stat-box">'
53+
f'<div class="ddp-stat-value">{value}</div>'
54+
f'<div class="ddp-stat-label">{label}</div>'
55+
+ (f'<div class="ddp-stat-caption">{caption}</div>' if caption else '')
10756
+ '</div>'
10857
)
10958

@@ -128,7 +77,7 @@ def _stat(value, label, caption=''):
12877
_churn_ratio_12 = (_churn_12_sum / _active_12_sum * 100) if _active_12_sum > 0 else 0
12978

13079
_stats_html = (
131-
'<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px">'
80+
'<div class="ddp-stat-row">'
13281
+ _stat(f'{_active_count:,}', 'Active Developers', f'Latest month ({str(_latest_month)[:7]})')
13382
+ _stat(f'{_ft_count:,}', 'Full-Time', '10+ active days/month')
13483
+ _stat(f'{_pt_count:,}', 'Part-Time', '1-9 active days/month')
@@ -193,28 +142,30 @@ def _categorize(label):
193142
_opts = [o for o in _ECOSYSTEMS if o in _states]
194143
_djs_safe = _json.dumps(_states).replace('</', '<\\/')
195144
_opts_js = _json.dumps(_opts)
196-
_sel_html = '<div style="margin-bottom:8px"><span style="font-size:11px;color:#6b7280;display:block;margin-bottom:2px">Ecosystem</span><select id="sel" style="padding:4px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;color:#374151;background:#fff;cursor:pointer">' + ''.join(f'<option value="{i}">{o}</option>' for i, o in enumerate(_opts)) + '</select></div>'
145+
_sel_html = '<div style="margin-bottom:8px"><span class="ddp-select-label">Ecosystem</span><select id="sel" class="ddp-select">' + ''.join(f'<option value="{i}">{o}</option>' for i, o in enumerate(_opts)) + '</select></div>'
197146

198147
_inner = (
199148
'<!DOCTYPE html><html><head><meta charset="utf-8">'
200149
'<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
201150
'<style>'
202-
'*{box-sizing:border-box;margin:0;padding:0}'
203-
'body{font-family:Arial,sans-serif;font-size:13px;padding:4px}'
151+
'*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif!important}'
152+
'body{font-size:14px;color:#0f172a;padding:4px}'
153+
'.ddp-select{padding:4px 8px;border:1px solid #e2e8f0;border-radius:4px;font-size:0.8125em;color:#0f172a;background:#fff;cursor:pointer;outline:none}'
154+
'.ddp-select-label{font-size:0.6875em;color:#64748b;display:block;margin-bottom:2px}'
204155
'</style></head><body>'
205156
f'{_sel_html}'
206157
'<div id="stats" style="margin-bottom:12px"></div>'
207158
'<div id="chart"></div>'
208159
f'<script>var D={_djs_safe};var O={_opts_js};'
209160
'var sel=document.getElementById("sel");'
210161
'function show(i){document.getElementById("stats").innerHTML=D[O[i]].stats||"";'
211-
'Plotly.react("chart",D[O[i]].chart.data,D[O[i]].chart.layout,{responsive:true});}'
162+
'Plotly.react("chart",D[O[i]].chart.data,D[O[i]].chart.layout,{responsive:true,displayModeBar:false});}'
212163
'sel.addEventListener("change",function(){show(parseInt(this.value))});'
213164
'show(0);'
214165
'</script></body></html>'
215166
)
216167
_src = _html_mod.escape(_inner, quote=True)
217-
mo.Html(f'<iframe srcdoc="{_src}" style="width:100%;height:580px;border:none;display:block" scrolling="no"></iframe>')
168+
mo.Html(f'<iframe srcdoc="{_src}" class="ddp-chart-frame-tall" scrolling="no"></iframe>')
218169
return
219170

220171

@@ -290,26 +241,28 @@ def ecosystem_comparison_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_
290241
_opts = [m for m in _METRICS if m in _states]
291242
_djs_safe = _json.dumps(_states).replace('</', '<\\/')
292243
_opts_js = _json.dumps(_opts)
293-
_sel_html = '<div style="margin-bottom:8px"><span style="font-size:11px;color:#6b7280;display:block;margin-bottom:2px">Metric</span><select id="sel" style="padding:4px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;color:#374151;background:#fff;cursor:pointer">' + ''.join(f'<option value="{i}">{o}</option>' for i, o in enumerate(_opts)) + '</select></div>'
244+
_sel_html = '<div style="margin-bottom:8px"><span class="ddp-select-label">Metric</span><select id="sel" class="ddp-select">' + ''.join(f'<option value="{i}">{o}</option>' for i, o in enumerate(_opts)) + '</select></div>'
294245

295246
_inner = (
296247
'<!DOCTYPE html><html><head><meta charset="utf-8">'
297248
'<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
298249
'<style>'
299-
'*{box-sizing:border-box;margin:0;padding:0}'
300-
'body{font-family:Arial,sans-serif;font-size:13px;padding:4px}'
250+
'*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif!important}'
251+
'body{font-size:14px;color:#0f172a;padding:4px}'
252+
'.ddp-select{padding:4px 8px;border:1px solid #e2e8f0;border-radius:4px;font-size:0.8125em;color:#0f172a;background:#fff;cursor:pointer;outline:none}'
253+
'.ddp-select-label{font-size:0.6875em;color:#64748b;display:block;margin-bottom:2px}'
301254
'</style></head><body>'
302255
f'{_sel_html}'
303256
'<div id="chart"></div>'
304257
f'<script>var D={_djs_safe};var O={_opts_js};'
305258
'var sel=document.getElementById("sel");'
306-
'function show(i){Plotly.react("chart",D[O[i]].chart.data,D[O[i]].chart.layout,{responsive:true});}'
259+
'function show(i){Plotly.react("chart",D[O[i]].chart.data,D[O[i]].chart.layout,{responsive:true,displayModeBar:false});}'
307260
'sel.addEventListener("change",function(){show(parseInt(this.value))});'
308261
'show(0);'
309262
'</script></body></html>'
310263
)
311264
_src = _html_mod.escape(_inner, quote=True)
312-
mo.Html(f'<iframe srcdoc="{_src}" style="width:100%;height:520px;border:none;display:block" scrolling="no"></iframe>')
265+
mo.Html(f'<iframe srcdoc="{_src}" class="ddp-chart-frame-tall" scrolling="no"></iframe>')
313266
return
314267

315268

@@ -414,6 +367,53 @@ def apply_ec_style(fig, title=None, subtitle=None, y_title=None, show_legend=Tru
414367
return (apply_ec_style,)
415368

416369

370+
@app.cell(hide_code=True)
371+
def header_accordion(mo):
372+
mo.accordion({
373+
"Metrics & Definitions": mo.md("""
374+
**Lifecycle labels** classify each developer's monthly activity into one of 16 granular states. These roll up into 4 categories used in the summary chart:
375+
376+
| Category | Label | Description |
377+
|:---------|:------|:------------|
378+
| **First Time** | `first time` | First-ever contribution to the ecosystem |
379+
| **Full Time** | `full time` | 10+ active days, continuing from prior month |
380+
| | `new full time` | First month reaching 10+ active days |
381+
| | `part time to full time` | Transitioned from part-time level |
382+
| | `dormant to full time` | Returned from dormancy at full-time level |
383+
| **Part Time** | `part time` | 1-9 active days, continuing from prior month |
384+
| | `new part time` | First month at part-time level |
385+
| | `full time to part time` | Stepped down from full-time level |
386+
| | `dormant to part time` | Returned from dormancy at part-time level |
387+
| **Churned / Dormant** | `dormant` | No activity this month (previously active) |
388+
| | `first time to dormant` | Dormant after first contribution |
389+
| | `part time to dormant` | Dormant after part-time activity |
390+
| | `full time to dormant` | Dormant after full-time activity |
391+
| | `churned (after first time)` | Extended inactivity after first contribution |
392+
| | `churned (after reaching part time)` | Extended inactivity after reaching part time |
393+
| | `churned (after reaching full time)` | Extended inactivity after reaching full time |
394+
395+
**Active** = First Time + Full Time + Part Time (all 9 labels above the Churned/Dormant group)
396+
397+
**Churn Ratio** = sum(churned + dormant) / sum(active) over the trailing window (12mo or all-time)
398+
399+
**Metric Definitions**
400+
- Lifecycle — Developer stage definitions
401+
- Activity — Monthly Active Developer (MAD) methodology
402+
"""),
403+
"Assumptions & Limitations": mo.md("""
404+
- **Activity windows**: Developer activity is measured using 28-day rolling windows; a developer is considered active if they have at least 1 active day in the window
405+
- **Ecosystem assignment**: Repos are mapped to ecosystems via recursive repo mapping from Open Dev Data — a repo may belong to multiple ecosystems through the parent-child hierarchy
406+
- **Identity resolution**: Developer identities are resolved by Electric Capital's fingerprinting; the same person using different accounts may be counted multiple times
407+
- **Public GitHub only**: Only public GitHub commits and activity are tracked; private repos and non-GitHub platforms are excluded
408+
"""),
409+
"Data Sources": mo.md("""
410+
- **Open Dev Data (Electric Capital)** — Ecosystem and developer taxonomy, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems)
411+
- **Key Models** — `oso.int_crypto_ecosystems_developer_lifecycle_monthly_aggregated`
412+
"""),
413+
})
414+
return
415+
416+
417417
@app.cell(hide_code=True)
418418
def test_connection(mo, pyoso_db_conn):
419419
_test_df = mo.sql("""SELECT 1 AS test""", engine=pyoso_db_conn, output=False)

0 commit comments

Comments
 (0)