From 94585f6067bc950e2a147806409cddc72e8295b8 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:40:49 -0400 Subject: [PATCH 01/23] feat(css): add ddp-card, ddp-chart-frame-tall, ddp-select-label classes --- notebooks/styles/_insights.css | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/notebooks/styles/_insights.css b/notebooks/styles/_insights.css index a38214b..973dbf8 100644 --- a/notebooks/styles/_insights.css +++ b/notebooks/styles/_insights.css @@ -5,7 +5,7 @@ * * CSS classes prefixed with .ddp- are ready for notebooks to adopt. * Currently most notebooks use inline styles for these patterns — - * see OSO-XXXX for the cleanup ticket to migrate to these classes. + * see OSO-2047 for the cleanup ticket to migrate to these classes. */ /* ========================================================================== @@ -260,6 +260,38 @@ table td code, table td .mono { margin-bottom: 0.5em; } +/* ========================================================================== + CARD WRAPPER (bordered container with shadow) + Used by: ethereum-repo-rank.py table wrapper + Replace: style="border:1px solid #e2e8f0;border-radius:12px;overflow:hidden; + background:white;box-shadow:0 1px 3px rgba(0,0,0,0.04);" + With: class="ddp-card" + ========================================================================== */ + +.ddp-card { + border: 1px solid var(--ddp-border); + border-radius: 8px; + overflow: hidden; + background: var(--ddp-bg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +/* Taller iframe variant for lifecycle/retention charts */ +.ddp-chart-frame-tall { + width: 100%; + height: 580px; + border: none; + display: block; +} + +/* Label above a select element */ +.ddp-select-label { + font-size: 0.6875em; + color: var(--ddp-text-muted); + display: block; + margin-bottom: 2px; +} + /* ========================================================================== MOBILE ========================================================================== */ From 18a9a98c7883c21a244dff2e94acf1467ba02715 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:41:51 -0400 Subject: [PATCH 02/23] chore(css): migrate developer-report-2025 inline styles to CSS classes Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/developer-report-2025.py | 60 ++++++++++----------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/notebooks/insights/developer-report-2025.py b/notebooks/insights/developer-report-2025.py index db536c3..64d7211 100644 --- a/notebooks/insights/developer-report-2025.py +++ b/notebooks/insights/developer-report-2025.py @@ -8,7 +8,7 @@ def header_title(mo): mo.md(""" # 2025 Developer Trends - Owner: OSO Team · Last Updated: 2026-02-17 + Owner: OSO Team · Last Updated: 2026-02-17 Explore an interactive reproduction of the [Electric Capital Developer Report](https://www.developerreport.com), updated with 2025 data. """) @@ -431,7 +431,7 @@ def chart1_total_mads(EC_LIGHT_BLUE, df_all, go, mo, pd): _opts = list(_states.keys()) _djs_safe = _json.dumps(_states).replace('Time Range' + _sel_html = '
Time Range
' _inner = ( '' @@ -450,7 +450,7 @@ def chart1_total_mads(EC_LIGHT_BLUE, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -513,7 +513,7 @@ def chart2_tenure_composition(TENURE_COLORS, df_all, go, mo, pd): _opts = list(_states.keys()) _djs_safe = _json.dumps(_states).replace('Time Range' + _sel_html = '
Time Range
' _inner = ( '' @@ -532,7 +532,7 @@ def chart2_tenure_composition(TENURE_COLORS, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -664,7 +664,7 @@ def chart3_experienced_devs(TENURE_COLORS, df_all, go, mo): _opts = list(_states.keys()) _djs_safe = _json.dumps(_states).replace('Comparison Period' + _sel_html = '
Comparison Period
' _inner = ( '' @@ -683,7 +683,7 @@ def chart3_experienced_devs(TENURE_COLORS, df_all, go, mo): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -814,7 +814,7 @@ def chart4_developer_changes(TENURE_COLORS, df_all, go, mo): _opts = list(_states.keys()) _djs_safe = _json.dumps(_states).replace('Comparison Period' + _sel_html = '
Comparison Period
' _inner = ( '' @@ -833,7 +833,7 @@ def chart4_developer_changes(TENURE_COLORS, df_all, go, mo): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -914,8 +914,8 @@ def chart5_newcomer_volatility(EC_LIGHT_BLUE, df_all, go, mo, pd): _states[_opt] = {'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('Time Granularity' - _range_sel = '
Date Range
' + _gran_sel = '
Time Granularity
' + _range_sel = '
Date Range
' _sel_html = f'
{_gran_sel}{_range_sel}
' _inner = ( @@ -935,7 +935,7 @@ def chart5_newcomer_volatility(EC_LIGHT_BLUE, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1072,8 +1072,8 @@ def chart6_btc_eth_share(df_all, go, mo, pd): _states[_opt] = {'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('View' - _time_sel = '
Time Range
' + _view_sel = '
View
' + _time_sel = '
Time Range
' _sel_html = f'
{_view_sel}{_time_sel}
' _inner = ( @@ -1093,7 +1093,7 @@ def chart6_btc_eth_share(df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1197,8 +1197,8 @@ def chart_ecosystem_total_devs(EC_LIGHT_BLUE, df_all, go, mo, pd): _states[_opt] = {'stats': _stats_html, 'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('Ecosystem' - _time_sel = '
Time Range
' + _eco_sel = '
Ecosystem
' + _time_sel = '
Time Range
' _sel_html = f'
{_eco_sel}{_time_sel}
' _inner = ( @@ -1219,7 +1219,7 @@ def chart_ecosystem_total_devs(EC_LIGHT_BLUE, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1289,8 +1289,8 @@ def chart_ecosystem_tenure(TENURE_COLORS, df_all, go, mo, pd): _states[_opt] = {'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('Ecosystem' - _time_sel = '
Time Range
' + _eco_sel = '
Ecosystem
' + _time_sel = '
Time Range
' _sel_html = f'
{_eco_sel}{_time_sel}
' _inner = ( @@ -1310,7 +1310,7 @@ def chart_ecosystem_tenure(TENURE_COLORS, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1374,8 +1374,8 @@ def chart_ecosystem_activity(ACTIVITY_COLORS, df_all, go, mo, pd): _states[_opt] = {'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('Ecosystem' - _time_sel = '
Time Range
' + _eco_sel = '
Ecosystem
' + _time_sel = '
Time Range
' _sel_html = f'
{_eco_sel}{_time_sel}
' _inner = ( @@ -1395,7 +1395,7 @@ def chart_ecosystem_activity(ACTIVITY_COLORS, df_all, go, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1491,8 +1491,8 @@ def chart_ecosystem_newcomers_by_year(df_all, go, mo): _states[_opt] = {'chart': _json.loads(_fig.to_json())} _djs_safe = _json.dumps(_states).replace('Ecosystem' - _time_sel = '
Time Range
' + _eco_sel = '
Ecosystem
' + _time_sel = '
Time Range
' _sel_html = f'
{_eco_sel}{_time_sel}
' _inner = ( @@ -1512,7 +1512,7 @@ def chart_ecosystem_newcomers_by_year(df_all, go, mo): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -1594,8 +1594,8 @@ def comparison_chart(df_all, mo, pd): ) + '' ) - _metric_sel = '
Metric
' - _time_sel = '
Time Range
' + _metric_sel = '
Metric
' + _time_sel = '
Time Range
' _sel_html = f'
{_cb_html}{_metric_sel}{_time_sel}
' _inner = ( @@ -1628,7 +1628,7 @@ def comparison_chart(df_all, mo, pd): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return From 63851fa6ef6560333c2861af1db23e2c51cf6134 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:41:56 -0400 Subject: [PATCH 03/23] chore(css): migrate speedrun-ethereum inline styles to CSS classes Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/speedrun-ethereum.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/notebooks/insights/speedrun-ethereum.py b/notebooks/insights/speedrun-ethereum.py index 9b3b36e..2af6f26 100644 --- a/notebooks/insights/speedrun-ethereum.py +++ b/notebooks/insights/speedrun-ethereum.py @@ -8,7 +8,7 @@ def header_title(mo): mo.md(""" # Case Study: Speedrun Ethereum - Owner: OSO Team · Last Updated: 2026-02-17 + Owner: OSO Team · Last Updated: 2026-02-17 An in-depth case study on the role Speedrun Ethereum has played in onboarding and retaining new Ethereum developers. """) @@ -524,7 +524,7 @@ def section_activity_by_ecosystem( _opts = list(_states.keys()) _djs_safe = _json.dumps(_states).replace('Analyze' + _sel_html = '
Analyze
' _inner = ( '' @@ -548,7 +548,7 @@ def section_activity_by_ecosystem( mo.md("---"), mo.md("## Speedrun Ethereum has contributed an incremental ~250 monthly active developers to Ethereum"), mo.md("_Measured as the increase in Ethereum-active developers attributable to SRE alumni relative to the pre-SRE baseline. Showing Ethereum ecosystem._"), - mo.Html(f''), + mo.Html(f''), ]) return @@ -943,7 +943,7 @@ def section_experience_funnel( _opts2 = list(_states2.keys()) _djs2_safe = _json2.dumps(_states2).replace('Ecosystem' + _sel2_html = '
Ecosystem
' _inner2 = ( '' @@ -967,7 +967,7 @@ def section_experience_funnel( mo.vstack([ mo.md("## Not surprisingly, less experienced developers have higher churn and less overall long-term impact on Ethereum"), - mo.Html(f''), + mo.Html(f''), mo.md("The table below provides additional detail on the developer funnel:"), show_table(_df_table2) ]) @@ -1179,7 +1179,7 @@ def section_experience_retention( _opts3 = list(_states3.keys()) _djs3_safe = _json3.dumps(_states3).replace('Ecosystem' + _sel3_html = '
Ecosystem
' _inner3 = ( '' @@ -1202,7 +1202,7 @@ def section_experience_retention( mo.vstack([ mo.md("---"), mo.md("## Developers with > 12 months prior experience remain active contributors to Ethereum at significantly higher rates"), - mo.Html(f''), + mo.Html(f''), ]) return @@ -1242,7 +1242,7 @@ def section_experienced_dev_activity( _opts4 = list(_states4.keys()) _djs4_safe = _json4.dumps(_states4).replace('Experience Level' + _sel4_html = '
Experience Level
' _inner4 = ( '' @@ -1266,7 +1266,7 @@ def section_experienced_dev_activity( mo.md("---"), mo.md("## For experienced developers, Speedrun Ethereum functions less as onboarding and more as activation and redirection toward Ethereum"), mo.md("_Showing Active Developers metric for Ethereum ecosystem. Select experience level:_"), - mo.Html(f''), + mo.Html(f''), ]) return @@ -1298,7 +1298,7 @@ def section_cohort_year_retention( _opts5 = list(_states5.keys()) _djs5_safe = _json5.dumps(_states5).replace('Experience Level' + _sel5_html = '
Experience Level
' _inner5 = ( '' @@ -1322,7 +1322,7 @@ def section_cohort_year_retention( mo.md("---"), mo.md("## Engagement past the 3–month mark is a good predictor of longer-term retention"), mo.md("_Showing Ethereum ecosystem by cohort year. Select experience level:_"), - mo.Html(f''), + mo.Html(f''), ]) return From 5beab26585ad2ae662a8ced431d78d35859124be Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:42:25 -0400 Subject: [PATCH 04/23] chore(css): migrate developer-lifecycle inline styles to CSS classes Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/developer-lifecycle.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/notebooks/insights/developer-lifecycle.py b/notebooks/insights/developer-lifecycle.py index 89874d1..2290b53 100644 --- a/notebooks/insights/developer-lifecycle.py +++ b/notebooks/insights/developer-lifecycle.py @@ -8,7 +8,7 @@ def header_title(mo): mo.md(""" # Lifecycle Analysis - Owner: OSO Team · Last Updated: 2026-02-17 + Owner: OSO Team · Last Updated: 2026-02-17 Visualize the full lifecycle of a developer joining, contributing, and leaving an ecosystem. """) @@ -100,10 +100,10 @@ def ecosystem_overview_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_LA def _stat(value, label, caption=''): return ( - f'
' - f'
{value}
' - f'
{label}
' - + (f'
{caption}
' if caption else '') + f'
' + f'
{value}
' + f'
{label}
' + + (f'
{caption}
' if caption else '') + '
' ) @@ -128,7 +128,7 @@ def _stat(value, label, caption=''): _churn_ratio_12 = (_churn_12_sum / _active_12_sum * 100) if _active_12_sum > 0 else 0 _stats_html = ( - '
' + '
' + _stat(f'{_active_count:,}', 'Active Developers', f'Latest month ({str(_latest_month)[:7]})') + _stat(f'{_ft_count:,}', 'Full-Time', '10+ active days/month') + _stat(f'{_pt_count:,}', 'Part-Time', '1-9 active days/month') @@ -193,7 +193,7 @@ def _categorize(label): _opts = [o for o in _ECOSYSTEMS if o in _states] _djs_safe = _json.dumps(_states).replace('Ecosystem
' + _sel_html = '
Ecosystem
' _inner = ( '' @@ -214,7 +214,7 @@ def _categorize(label): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return @@ -290,7 +290,7 @@ def ecosystem_comparison_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_ _opts = [m for m in _METRICS if m in _states] _djs_safe = _json.dumps(_states).replace('Metric
' + _sel_html = '
Metric
' _inner = ( '' @@ -309,7 +309,7 @@ def ecosystem_comparison_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_ '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return From 5c8953a106aa1d120cb1252478130aaa230671ce Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:43:37 -0400 Subject: [PATCH 05/23] chore(css): migrate developer-retention inline styles to CSS classes Co-Authored-By: Claude Opus 4.6 (1M context) --- notebooks/insights/developer-retention.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/notebooks/insights/developer-retention.py b/notebooks/insights/developer-retention.py index 6938d74..aa4bb96 100644 --- a/notebooks/insights/developer-retention.py +++ b/notebooks/insights/developer-retention.py @@ -8,7 +8,7 @@ def header_title(mo): mo.md(""" # Retention Analysis - Owner: OSO Team · Last Updated: 2026-02-17 + Owner: OSO Team · Last Updated: 2026-02-17 Analyze developer retention by ecosystem and cohort year — what percentage of developers who joined in Year X are still active after N years? """) @@ -159,10 +159,10 @@ def retention_overview_tabs(df_all_retention, mo, go): def _stat(value, label, caption=''): return ( - f'
' - f'
{value}
' - f'
{label}
' - + (f'
{caption}
' if caption else '') + f'
' + f'
{value}
' + f'
{label}
' + + (f'
{caption}
' if caption else '') + '
' ) @@ -189,7 +189,7 @@ def _stat(value, label, caption=''): _avg_2yr = _metrics_2yr['retention_rate'].mean() if len(_metrics_2yr) > 0 else 0 _stats_html = ( - '
' + '
' + _stat(f'{_avg_1yr:.1f}%', 'Avg 1-Year Retention', f'{_eco} across selected cohorts') + _stat(f'{_avg_2yr:.1f}%', 'Avg 2-Year Retention', f'{_eco} across selected cohorts') + _stat(str(_best_1yr['cohort_year']), 'Best Cohort', f"{_best_1yr['retention_rate']:.1f}% retention at 1 year") @@ -268,7 +268,7 @@ def _stat(value, label, caption=''): _opts = [o for o in _ECOSYSTEMS if o in _states] _djs_safe = _json.dumps(_states).replace('Ecosystem
' + _sel_html = '
Ecosystem
' _inner = ( '' @@ -289,7 +289,7 @@ def _stat(value, label, caption=''): '' ) _src = _html_mod.escape(_inner, quote=True) - mo.Html(f'') + mo.Html(f'') return From c6ee71fba9c72bd3b50630952132964eda4289ca Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:43:46 -0400 Subject: [PATCH 06/23] chore(css): migrate ethereum-repo-rank inline styles to CSS classes Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/ethereum-repo-rank.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/notebooks/insights/ethereum-repo-rank.py b/notebooks/insights/ethereum-repo-rank.py index ed2a398..f25a3e6 100644 --- a/notebooks/insights/ethereum-repo-rank.py +++ b/notebooks/insights/ethereum-repo-rank.py @@ -113,12 +113,12 @@ def _heat(m): def _rank_badge(i): if i == 0: - return '🥇' + return '🥇' if i == 1: - return '🥈' + return '🥈' if i == 2: - return '🥉' - return f'#{i+1}' + return '🥉' + return f'#{i+1}' # Sort indicators for column headers _eth_arrow = " ↓" if leaderboard_sort.value == "Eth Builder Attention" else "" @@ -152,7 +152,7 @@ def _community(pct): {_heat(_r["momentum"])} """) - _table_html = f"""
+ _table_html = f"""
@@ -180,7 +180,7 @@ def _community(pct):
""" - _footer_text = f'Showing {_start+1}–{_end} of {len(_df)} repos with Ethereum builder signal' + _footer_text = f'Showing {_start+1}–{_end} of {len(_df)} repos with Ethereum builder signal' mo.vstack([ mo.hstack([ From ffc2e12622ee32e5f1c20b8aa8577cafa04478d7 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:46:16 -0400 Subject: [PATCH 07/23] chore(css): migrate defi-builder-journeys inline styles to CSS classes Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/defi-builder-journeys.py | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/notebooks/insights/defi-builder-journeys.py b/notebooks/insights/defi-builder-journeys.py index 04944c3..b4378da 100644 --- a/notebooks/insights/defi-builder-journeys.py +++ b/notebooks/insights/defi-builder-journeys.py @@ -1479,7 +1479,7 @@ def _fmt_tvl(v): ), _table, mo.md( - '' + '' '**Note on TVL figures:** TVL values are sourced from DefiLlama and may differ from other sources ' 'due to double-counting across chains, stablecoin classification differences, or timing of snapshots. ' 'ETH TVL Share includes Ethereum L1 and major L2s.' @@ -1600,7 +1600,7 @@ def _state_color(state): _svg = [ f'', '', ] @@ -1711,14 +1711,14 @@ def _render_rows(rows, start_idx=0): def _render_cell(eng): _cell = _grid.get((_active_label, eng), {'Crypto': [], 'OSS': []}) return ( - f'
' + f'
' f'
Crypto Ecosystems
{_render_list(_cell.get("Crypto", []), _active_color)}
' f'
Other / Non-Crypto OSS
{_render_list(_cell.get("OSS", []), _active_color)}
' f'
' ) _html = ( - f'
' + f'
' f'
' f'
' f'Contributing' @@ -2095,7 +2095,7 @@ def _render_one(eco_label, active_color): ], justify='space-around', widths='equal'), ] if _chart is not None: - _elements.append(mo.md(f'Aggregate TVL and monthly active builders across {_eco_name} DeFi projects over time.
This is not a correlation chart --- TVL (background) and builder counts (foreground) are shown together to provide context on ecosystem-level trends, not to imply a causal relationship.')) + _elements.append(mo.md(f'Aggregate TVL and monthly active builders across {_eco_name} DeFi projects over time.
This is not a correlation chart --- TVL (background) and builder counts (foreground) are shown together to provide context on ecosystem-level trends, not to imply a causal relationship.')) _elements.append(_chart) return mo.vstack(_elements) @@ -2122,23 +2122,23 @@ def ethereum_content( ecosystem_overview_content['Ethereum'], mo.md("---"), mo.md("## Where does Ethereum DeFi builder talent flow over time?"), - mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), + mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), alluvial_content['Ethereum'], mo.md("---"), mo.md("## Is Ethereum DeFi retaining builder talent over time?"), - mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Other Crypto** = non-Ethereum chains, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), + mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Other Crypto** = non-Ethereum chains, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), balance_content['Ethereum'], mo.md("---"), mo.md("## How long do Ethereum DeFi builders stay active after onboarding?"), - mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), + mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), cohort_content['Ethereum'], mo.md("---"), mo.md("## Where do new Ethereum DeFi builders come from?"), - mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), + mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), inflow_content['Ethereum'], mo.md("---"), mo.md("## Which projects seed the Ethereum DeFi builder pipeline?"), - mo.md('Top feeder projects by number of builders who contributed before joining Ethereum DeFi. Includes both crypto and non-crypto open source projects.'), + mo.md('Top feeder projects by number of builders who contributed before joining Ethereum DeFi. Includes both crypto and non-crypto open source projects.'), feeder_content['Ethereum'], ]) ethereum_tab_content = _eth @@ -2159,23 +2159,23 @@ def solana_content( ecosystem_overview_content['Solana'], mo.md("---"), mo.md("## Where does Solana DeFi builder talent flow over time?"), - mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), + mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), alluvial_content['Solana'], mo.md("---"), mo.md("## Is Solana DeFi retaining builder talent over time?"), - mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Ethereum** = Ethereum L1+L2 projects, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), + mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Ethereum** = Ethereum L1+L2 projects, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), balance_content['Solana'], mo.md("---"), mo.md("## How long do Solana DeFi builders stay active after onboarding?"), - mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), + mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), cohort_content['Solana'], mo.md("---"), mo.md("## Where do new Solana DeFi builders come from?"), - mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), + mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), inflow_content['Solana'], mo.md("---"), mo.md("## Which projects seed the Solana DeFi builder pipeline?"), - mo.md('Top feeder projects by number of builders who contributed before joining Solana DeFi. Includes both crypto and non-crypto open source projects.'), + mo.md('Top feeder projects by number of builders who contributed before joining Solana DeFi. Includes both crypto and non-crypto open source projects.'), feeder_content['Solana'], ]) solana_tab_content = _sol @@ -2196,23 +2196,23 @@ def other_ecosystem_content( ecosystem_overview_content['Other'], mo.md("---"), mo.md("## Where does other DeFi builder talent flow over time?"), - mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), + mo.md('Each column is a year. Colored bands show where builders are active. Flows between columns trace how builders move between states year-over-year. Wider bands = more builders.'), alluvial_content['Other'], mo.md("---"), mo.md("## Is other DeFi retaining builder talent over time?"), - mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Ethereum** = Ethereum L1+L2 projects, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), + mo.md('Bars above zero = builders entering the ecosystem; below zero = leaving. The dark line tracks net flow. Categories: **Ethereum** = Ethereum L1+L2 projects, **Non-Crypto OSS** = traditional open source, **Inactive** = no observable activity for 12+ months. 2025 data is partial.'), balance_content['Other'], mo.md("---"), mo.md("## How long do other DeFi builders stay active after onboarding?"), - mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), + mo.md('Each line tracks a cohort of builders from their onboarding year. Retention = % still active on home project at each interval. Recent cohorts have shorter curves because less time has elapsed.'), cohort_content['Other'], mo.md("---"), mo.md("## Where do new other DeFi builders come from?"), - mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), + mo.md('Breakdown of builder onboarding pipeline by year. Newcomers had less than 6 months of prior OSS activity before joining DeFi.'), inflow_content['Other'], mo.md("---"), mo.md("## Which projects seed the other DeFi builder pipeline?"), - mo.md('Top feeder projects by number of builders who contributed before joining other DeFi ecosystems. Includes both crypto and non-crypto open source projects.'), + mo.md('Top feeder projects by number of builders who contributed before joining other DeFi ecosystems. Includes both crypto and non-crypto open source projects.'), feeder_content['Other'], ]) other_tab_content = _other @@ -2407,11 +2407,11 @@ def _grid_group(df, col_offset, border_color, label): static_plotly(_fig_grid), mo.md("---"), mo.md("### Monthly active builders over time"), - mo.md('Aggregate monthly active builder count across all projects in each group.'), + mo.md('Aggregate monthly active builder count across all projects in each group.'), static_plotly(_fig_devs), mo.md("---"), mo.md("### Cross-ecosystem flows: tug of war"), - mo.md('Direct builder movement between Ethereum and non-Ethereum DeFi. Counts builders whose primary ecosystem changed year-over-year.'), + mo.md('Direct builder movement between Ethereum and non-Ethereum DeFi. Counts builders whose primary ecosystem changed year-over-year.'), static_plotly(_fig_tow), mo.hstack([ mo.stat(value=f'{"+" if _eth_bs["net"] >= 0 else ""}{_eth_bs["net"]}', label='Ethereum total net', bordered=True, caption=f'{_eth_bs["count_before"]} \u2192 {_eth_bs["count_after"]} ({_eth_bs["first_year"]}\u2013{_eth_bs["last_year"]})'), @@ -2419,11 +2419,11 @@ def _grid_group(df, col_offset, border_color, label): ], widths='equal', gap=1), mo.md("---"), mo.md("### Retention: who sticks around longer?"), - mo.md('Average retention across all year cohorts (2020\u20132024). Each curve averages the % still active at each quarter interval.'), + mo.md('Average retention across all year cohorts (2020\u20132024). Each curve averages the % still active at each quarter interval.'), static_plotly(_fig_ret), mo.md("---"), mo.md("### Newcomer pipeline: who attracts more fresh talent?"), - mo.md('Newcomers = builders with less than 6 months of prior OSS activity before their DeFi onboarding.'), + mo.md('Newcomers = builders with less than 6 months of prior OSS activity before their DeFi onboarding.'), static_plotly(_fig_inflow), ]) return (comparison_tab_content,) @@ -2498,7 +2498,7 @@ def main_layout( # Build panel HTML: highlighted table (if any) + tab content # Wrap in max-width container so tables, SVGs, and charts all align - _wrap = '
' + _wrap = '
' _wrap_end = '
' _content_htmls = [] for _name, _table_html, _content in _tab_configs: From bd8fa7a4f619959eff26c45d1776226592443f28 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 11:50:29 -0400 Subject: [PATCH 08/23] feat: convert ethereum-repo-rank dropdowns to iframe+JS for static export Co-Authored-By: Claude Opus 4.6 (1M context) --- notebooks/insights/ethereum-repo-rank.py | 380 ++++++++++++----------- 1 file changed, 193 insertions(+), 187 deletions(-) diff --git a/notebooks/insights/ethereum-repo-rank.py b/notebooks/insights/ethereum-repo-rank.py index f25a3e6..4930e42 100644 --- a/notebooks/insights/ethereum-repo-rank.py +++ b/notebooks/insights/ethereum-repo-rank.py @@ -53,145 +53,132 @@ def _(df_trending, eth_dev_set, df_engagement_raw, mo): @app.cell(hide_code=True) -def _(mo): - leaderboard_window = mo.ui.dropdown( - options=["Past 30 Days", "Past 7 Days"], - value="Past 30 Days", - label="", - ) - leaderboard_sort = mo.ui.dropdown( - options=["All Builder Attention", "Eth Builder Attention"], - value="All Builder Attention", - label="Sort by", - ) - leaderboard_page = mo.ui.dropdown( - options=["Page 1", "Page 2", "Page 3", "Page 4"], - value="Page 1", - label="", - ) - return leaderboard_page, leaderboard_sort, leaderboard_window - +def _(df_trending, mo): + import json as _json + import html as _html_mod -@app.cell(hide_code=True) -def _(df_trending, leaderboard_window, leaderboard_sort, leaderboard_page, mo): - _window = leaderboard_window.value - _is_30d = _window == "Past 30 Days" - _all_col = "global_engagers_30d" if _is_30d else "global_engagers_7d" - _eth_col = "eth_devs_30d" if _is_30d else "eth_devs_7d" - _suffix = "30d" if _is_30d else "7d" - - # Sort by selected column, cap at 100 - _sort_col = _eth_col if leaderboard_sort.value == "Eth Builder Attention" else _all_col - _df = df_trending.sort_values(_sort_col, ascending=False).head(100).reset_index(drop=True) - - # Momentum: 7d daily rate / 30d daily rate + # Prepare top 100 rows with momentum + _df = df_trending.sort_values("global_engagers_30d", ascending=False).head(100).reset_index(drop=True) _df["rate_7d"] = _df["global_engagers_7d"] / 7 _df["rate_30d"] = _df["global_engagers_30d"] / 30 _df["momentum"] = _df["rate_7d"] / _df["rate_30d"].clip(lower=0.01) - # Pagination - _page_size = 25 - _total_pages = max(1, -(-len(_df) // _page_size)) - _page = min(int(leaderboard_page.value.split()[-1]), _total_pages) - _start = (_page - 1) * _page_size - _end = min(_start + _page_size, len(_df)) - _page_df = _df.iloc[_start:_end] - - def _fmt(n): - if n >= 10000: - return f"{n/1000:.1f}K" - if n >= 1000: - return f"{n/1000:.2f}K" - return str(int(n)) - - def _heat(m): - if m >= 1.5: - return "🌶🌶🌶" - if m >= 0.7: - return "🌶🌶" - return "🌶" - - def _rank_badge(i): - if i == 0: - return '🥇' - if i == 1: - return '🥈' - if i == 2: - return '🥉' - return f'#{i+1}' - - # Sort indicators for column headers - _eth_arrow = " ↓" if leaderboard_sort.value == "Eth Builder Attention" else "" - _all_arrow = " ↓" if leaderboard_sort.value == "All Builder Attention" else "" - - _th = "padding:6px 8px;font-size:0.68em;color:#64748b;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;white-space:nowrap;" - _td = "padding:4px 8px;vertical-align:middle;" - - def _community(pct): - if pct >= 0.01: - return 'Crypto' - return 'Mainstream' - - _rows = [] - for _idx in range(len(_page_df)): - _r = _page_df.iloc[_idx] - _rank = _start + _idx - _repo = _r["repo_name"] + _records = [] + for _i in range(len(_df)): + _r = _df.iloc[_i] _desc = str(_r.get("description", "")) if len(_desc) > 72: _desc = _desc[:69] + "..." - _row_bg = "background:#fafbfc;" if _idx % 2 == 1 else "" - - _rows.append(f""" - {_rank_badge(_rank)} - {_repo} - {_desc} - {_community(_r["eth_dev_pct"])} - {_fmt(int(_r[_eth_col]))} - {_fmt(int(_r[_all_col]))} - {_heat(_r["momentum"])} - """) - - _table_html = f"""
- - - - - - - - - - - - - - - - - - - - - - - {"".join(_rows)} - -
#RepositoryDescriptionCommunityEth Builders{_eth_arrow}All Builders{_all_arrow}Heat
-
""" - - _footer_text = f'Showing {_start+1}–{_end} of {len(_df)} repos with Ethereum builder signal' + _records.append({ + "repo_name": str(_r["repo_name"]), + "description": _desc, + "eth_dev_pct": float(_r["eth_dev_pct"]), + "eth_devs_30d": int(_r["eth_devs_30d"]), + "eth_devs_7d": int(_r["eth_devs_7d"]), + "global_engagers_30d": int(_r["global_engagers_30d"]), + "global_engagers_7d": int(_r["global_engagers_7d"]), + "momentum": float(_r["momentum"]), + }) + + _data_js = _json.dumps(_records).replace("' + '' + '
' + '
' + '' + '' + '' + '
' + '
' + '' + '' + ) + _src = _html_mod.escape(_inner, quote=True) mo.vstack([ - mo.hstack([ - mo.md("### Trending Repos"), - mo.hstack([leaderboard_sort, leaderboard_window], gap=1, justify="end"), - ], justify="space-between", align="end"), - mo.Html(_table_html), - mo.hstack([ - mo.md(_footer_text), - leaderboard_page, - ], justify="space-between", align="center"), + mo.md("### Trending Repos"), + mo.Html(f''), ]) return @@ -583,76 +570,95 @@ def _make_table(df_slice, show_repos=True): @app.cell(hide_code=True) -def _(df_trending, mo): +def _(df_trending, df_engagement_daily, mo): + import json as _json2 + import html as _html_mod2 + + # Build repo options (sorted by popularity) _repo_opts = ( df_trending[df_trending["global_engagers_30d"] > 0] .sort_values("global_engagers_30d", ascending=False)["repo_name"] .tolist() ) - # Default to zeroclaw, ironclaw, openclaw + + # Default selections _preferred = ["zeroclaw-labs/zeroclaw", "nearai/ironclaw", "openclaw/openclaw"] _defaults = [r for r in _preferred if r in _repo_opts] - compare_repos = mo.ui.multiselect( - options=_repo_opts, - value=_defaults, - label="**Compare repos**", - max_selections=6, - full_width=True, + # Serialize daily engagement data for all top repos + _daily_subset = df_engagement_daily[df_engagement_daily["repo_name"].isin(_repo_opts)].copy() + _daily_subset["day_str"] = _daily_subset["day"].dt.strftime("%Y-%m-%d") + _daily_records = {} + for _repo in _repo_opts: + _rd = _daily_subset[_daily_subset["repo_name"] == _repo].sort_values("day") + if len(_rd) > 0: + _daily_records[_repo] = { + "days": _rd["day_str"].tolist(), + "cum": _rd["cum_engagers"].tolist(), + } + + _opts_js = _json2.dumps(_repo_opts).replace("{r}' for r in _repo_opts ) - return (compare_repos,) + _inner2 = ( + '' + '' + ) + _src2 = _html_mod2.escape(_inner2, quote=True) -@app.cell(hide_code=True) -def _(compare_repos, df_engagement_daily, mo, go, PLOTLY_LAYOUT): - _selected = compare_repos.value if compare_repos.value else [] - - if not _selected: - _out = mo.vstack([ - mo.md("### Cumulative builder engagement\n\nSelect repos above to compare growth curves."), - compare_repos, - ]) - else: - _sel_lower = [r.lower() for r in _selected] - _eh = df_engagement_daily[df_engagement_daily["repo_name"].isin(_sel_lower)] - - _colors = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4"] - - _fig = go.Figure() - - for _i, _repo in enumerate(_sel_lower): - _short = _repo.split("/")[-1] - _color = _colors[_i % len(_colors)] - _r, _g, _b = int(_color[1:3], 16), int(_color[3:5], 16), int(_color[5:7], 16) - - _d = _eh[_eh["repo_name"] == _repo].sort_values("day") - if len(_d) > 0: - _fig.add_trace(go.Scatter( - x=_d["day"], y=_d["cum_engagers"], name=_short, - mode="lines", line=dict(color=_color, width=2, shape="hvh"), - fill="tozeroy", fillcolor=f"rgba({_r},{_g},{_b},0.05)", - hovertemplate="%{y:,.0f} builders" + _short + "", - )) - - _fig.update_layout( - **{ - **PLOTLY_LAYOUT, - "legend": {**PLOTLY_LAYOUT["legend"], "orientation": "h", "yanchor": "bottom", "y": 1.04, "xanchor": "left", "x": 0, "bgcolor": "rgba(255,255,255,0.8)"}, - "margin": dict(t=40, l=70, r=40, b=50), - "height": 450, - }, - ) - _fig.update_xaxes(tickformat="%b %d", showgrid=False) - _fig.update_yaxes(showgrid=True, tickformat=",") - - _out = mo.vstack([ - mo.md("""### Cumulative builder engagement + mo.vstack([ + mo.md("""### Cumulative builder engagement - Each line counts unique builders who starred or forked (first event only). A steep ramp means viral discovery; a flattening curve means the moment has passed."""), - compare_repos, - mo.ui.plotly(_fig, config={"displayModeBar": False}), - ]) - _out + Each line counts unique builders who starred or forked (first event only). A steep ramp means viral discovery; a flattening curve means the moment has passed."""), + mo.Html(f''), + ]) return From 812ac3cc7034111e404775b46af3ee0343815425 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:12:28 -0400 Subject: [PATCH 09/23] fix(css): use clamp() for stat card values to prevent overflow on long text --- notebooks/styles/base.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/notebooks/styles/base.css b/notebooks/styles/base.css index 2ca28a1..1754310 100644 --- a/notebooks/styles/base.css +++ b/notebooks/styles/base.css @@ -42,6 +42,13 @@ code, code *, pre, pre *, .cm-editor, .cm-editor * { font-family: var(--ddp-font-mono) !important; } +/* === Content width === + * Global max-width for all cell output so tables, charts, markdown, + * and stat cards align to the same column. Root variant narrows to 720px. */ +#root [data-cell-id] > div { + max-width: 1100px !important; +} + /* === Base === */ body { margin: 0; @@ -179,12 +186,13 @@ td { } [data-testid="marimo-stat"] [class*="value"], [data-testid="marimo-stat"] [class*="Value"] { - font-size: 1.5em !important; + font-size: clamp(1em, 4vw, 1.5em) !important; font-weight: 600 !important; letter-spacing: -0.02em !important; color: var(--ddp-text) !important; overflow-wrap: break-word !important; word-break: break-word !important; + line-height: 1.2 !important; } [data-testid="marimo-stat"] [class*="caption"], [data-testid="marimo-stat"] [class*="Caption"] { From 460161c99dd1f5e102fe109f9ccb9dfc0e31a868 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:15:09 -0400 Subject: [PATCH 10/23] fix(css): update iframe style blocks with DDP design system in developer-report-2025 --- notebooks/insights/developer-report-2025.py | 67 ++++++++++++++------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/notebooks/insights/developer-report-2025.py b/notebooks/insights/developer-report-2025.py index 64d7211..24cf415 100644 --- a/notebooks/insights/developer-report-2025.py +++ b/notebooks/insights/developer-report-2025.py @@ -437,8 +437,10 @@ def chart1_total_mads(EC_LIGHT_BLUE, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -519,8 +521,10 @@ def chart2_tenure_composition(TENURE_COLORS, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -670,8 +674,10 @@ def chart3_experienced_devs(TENURE_COLORS, df_all, go, mo): '' '' '' f'{_sel_html}' '
' @@ -820,8 +826,10 @@ def chart4_developer_changes(TENURE_COLORS, df_all, go, mo): '' '' '' f'{_sel_html}' '
' @@ -922,8 +930,10 @@ def chart5_newcomer_volatility(EC_LIGHT_BLUE, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -1080,8 +1090,10 @@ def chart6_btc_eth_share(df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -1205,8 +1217,10 @@ def chart_ecosystem_total_devs(EC_LIGHT_BLUE, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -1297,8 +1311,10 @@ def chart_ecosystem_tenure(TENURE_COLORS, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -1382,8 +1398,10 @@ def chart_ecosystem_activity(ACTIVITY_COLORS, df_all, go, mo, pd): '' '' '' f'{_sel_html}' '
' @@ -1499,8 +1517,10 @@ def chart_ecosystem_newcomers_by_year(df_all, go, mo): '' '' '' f'{_sel_html}' '
' @@ -1601,7 +1621,12 @@ def comparison_chart(df_all, mo, pd): _inner = ( '' '' - '' + '' '' f'{_sel_html}' '
' From 98327f91aa3e4a94c8585df05fd536e3131ac480 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:15:10 -0400 Subject: [PATCH 11/23] fix(css): update iframe style blocks with DDP design system in speedrun-ethereum --- notebooks/insights/speedrun-ethereum.py | 30 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/notebooks/insights/speedrun-ethereum.py b/notebooks/insights/speedrun-ethereum.py index 2af6f26..0c7a30d 100644 --- a/notebooks/insights/speedrun-ethereum.py +++ b/notebooks/insights/speedrun-ethereum.py @@ -530,8 +530,10 @@ def section_activity_by_ecosystem( '' '' '' f'{_sel_html}' '
' @@ -949,8 +951,10 @@ def section_experience_funnel( '' '' '' f'{_sel2_html}' '
' @@ -1185,8 +1189,10 @@ def section_experience_retention( '' '' '' f'{_sel3_html}' '
' @@ -1248,8 +1254,10 @@ def section_experienced_dev_activity( '' '' '' f'{_sel4_html}' '
' @@ -1304,8 +1312,10 @@ def section_cohort_year_retention( '' '' '' f'{_sel5_html}' '
' From 293e3629f8b24de629c15d08719c42f9281fc867 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:15:15 -0400 Subject: [PATCH 12/23] fix(css): update iframe style blocks with DDP design system in lifecycle and retention Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/developer-lifecycle.py | 12 ++++++++---- notebooks/insights/developer-retention.py | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/notebooks/insights/developer-lifecycle.py b/notebooks/insights/developer-lifecycle.py index 2290b53..3bb9613 100644 --- a/notebooks/insights/developer-lifecycle.py +++ b/notebooks/insights/developer-lifecycle.py @@ -199,8 +199,10 @@ def _categorize(label): '' '' '' f'{_sel_html}' '
' @@ -296,8 +298,10 @@ def ecosystem_comparison_tabs(ACTIVE_LABELS, CHURNED_LABELS, DORMANT_LABELS, FT_ '' '' '' f'{_sel_html}' '
' diff --git a/notebooks/insights/developer-retention.py b/notebooks/insights/developer-retention.py index aa4bb96..0c9c1a1 100644 --- a/notebooks/insights/developer-retention.py +++ b/notebooks/insights/developer-retention.py @@ -274,8 +274,10 @@ def _stat(value, label, caption=''): '' '' '' f'{_sel_html}' '
' From 6ae94038f41df7a51125f08d3263db68f889a184 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:39:48 -0400 Subject: [PATCH 13/23] chore: harmonize speedrun-ethereum header and accordion --- notebooks/insights/speedrun-ethereum.py | 97 ++++++++++--------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/notebooks/insights/speedrun-ethereum.py b/notebooks/insights/speedrun-ethereum.py index 0c7a30d..f0d91dc 100644 --- a/notebooks/insights/speedrun-ethereum.py +++ b/notebooks/insights/speedrun-ethereum.py @@ -6,63 +6,7 @@ @app.cell(hide_code=True) def header_title(mo): - mo.md(""" - # Case Study: Speedrun Ethereum - Owner: OSO Team · Last Updated: 2026-02-17 - - An in-depth case study on the role Speedrun Ethereum has played in onboarding and retaining new Ethereum developers. - """) - return - - -@app.cell(hide_code=True) -def header_accordion(mo): - mo.accordion({ - "Overview": mo.md(""" -- Speedrun Ethereum (SRE) is a self-paced challenge program that has onboarded 17,000+ developers into the Ethereum ecosystem -- This analysis examines whether SRE successfully converts newcomers into sustained Ethereum contributors — and the data suggests it does -- Key questions: What share of SRE graduates remain active in Ethereum after 1–2 years? How does prior experience affect outcomes? Where do graduates go after SRE? - """), - "Context": mo.md(""" -We conducted this analysis as part of a broader inquiry into the state of the Ethereum developer ecosystem in 2025, grounded in three working hypotheses: - -1. Developer retention is a leading indicator of ecosystem health and, over time, a meaningful predictor of long-term token price, value accrual, network GDP, etc. -2. Ethereum's early open-source culture is eroding as the crypto ecosystem matures, becomes more competitive, and partners with tradfi/web2. -3. Other ecosystems (eg, AI) have emerged as powerful bottom-up attractors for ambitious, mission-driven developers. - -Using Speedrun Ethereum as a focused case study, the data suggests that bottom-up programs still work. Speedrun Ethereum is successfully counteracting these headwinds by onboarding, retaining, and anchoring net-new developers in the Ethereum ecosystem. - -**Working hypotheses:** -1. Developer retention is a leading indicator of ecosystem health -2. Ethereum's early open-source culture is under pressure from competition and crypto maturation -3. AI and other ecosystems are attracting ambitious developers who might otherwise go to Ethereum - -**Key definitions:** -- **Users**: Developers with GitHub profiles saved in the SRE registry (not all 17K+ total users) -- **Cohort Month**: Profile `createdAt` date rounded to the nearest month -- **Batch ID**: Some though not all developers were assigned a learning batch (group) when they went through the program -- **Challenges Completed**: The number of SRE challenges the user successfully completed (according to their profile) -- **Location**: Where available, the country code of the user -- **Forked `scaffold-eth`**: Whether the user has one or more of the scaffold-eth repos forked to their personal GitHub -- **Experience Categories**: *Newb* (minimal prior GitHub activity), *Learning* (<1 year prior), *Experienced* (>12 months prior), *Delayed Start* (became active several months after SRE start) -- **Active Month**: ≥1 qualifying Push or PullRequest event on a public GitHub repo -- **Ecosystem Classification**: Repos classified as *Ethereum*, *Other EVM Chain*, *Personal*, or *Other* via Electric Capital mappings -- **Retention**: Share of a cohort active at month *t*, normalized at month 0 -- **Full-time month**: >10 days of qualifying activity -- **Velocity**: Sum over active days of (1 + ln(events per day)) -- **Change Categories**: Average monthly activity changes after SRE compared to before - -**Metric Definitions** -- Activity — Monthly Active Developer (MAD) methodology -- Retention — Cohort-based retention methodology - """), - "Data Sources": mo.md(""" -- **SRE GitHub users** — `int_sre_github_users`: user registry, cohorts, batches, challenges completed -- **GitHub events** — `int_sre_github_events_by_user`: public GitHub events joined to SRE users, from [GitHub Archive](https://gharchive.org) -- **Ecosystem mappings** — `stg_opendevdata__*`: Electric Capital's repo → ecosystem mappings, via [Open Dev Data](https://opendevdata.org/) -- **Further reading**: [Speedrun Ethereum](https://speedrunethereum.com/) · [Pyoso docs](https://docs.opensource.observer/docs/get-started/python) · [Marimo docs](https://docs.marimo.io/) - """), - }) + mo.Html('

Speedrun Ethereum

Measuring the impact of Speedrun Ethereum on developer onboarding and ecosystem growth.

Created: 2026-03-16
') return @@ -1700,6 +1644,45 @@ def section_where_now( return +@app.cell(hide_code=True) +def header_accordion(mo): + mo.accordion({ + "Metrics & Definitions": mo.md(""" +**Key definitions:** +- **Users**: Developers with GitHub profiles saved in the SRE registry (not all 17K+ total users) +- **Cohort Month**: Profile `createdAt` date rounded to the nearest month +- **Batch ID**: Some though not all developers were assigned a learning batch (group) when they went through the program +- **Challenges Completed**: The number of SRE challenges the user successfully completed (according to their profile) +- **Location**: Where available, the country code of the user +- **Forked `scaffold-eth`**: Whether the user has one or more of the scaffold-eth repos forked to their personal GitHub +- **Experience Categories**: *Newb* (minimal prior GitHub activity), *Learning* (<1 year prior), *Experienced* (>12 months prior), *Delayed Start* (became active several months after SRE start) +- **Active Month**: ≥1 qualifying Push or PullRequest event on a public GitHub repo +- **Ecosystem Classification**: Repos classified as *Ethereum*, *Other EVM Chain*, *Personal*, or *Other* via Electric Capital mappings +- **Retention**: Share of a cohort active at month *t*, normalized at month 0 +- **Full-time month**: >10 days of qualifying activity +- **Velocity**: Sum over active days of (1 + ln(events per day)) +- **Change Categories**: Average monthly activity changes after SRE compared to before + +**Metric Definitions:** +- **Activity** — Monthly Active Developer (MAD) methodology +- **Retention** — Cohort-based retention methodology + """), + "Assumptions & Limitations": mo.md(""" +- **SRE data completeness**: The SRE registry only covers developers who created a GitHub profile through the program — the full 17K+ user count includes developers not represented in this dataset +- **GitHub-only activity tracking**: All activity metrics are derived from public GitHub events; off-chain contributions, forum participation, and private repo activity are not captured +- **Attribution methodology**: Correlations between SRE participation and ecosystem activity do not imply causation — developers who complete SRE may have become active Ethereum contributors regardless +- **Time period scope**: Analysis is bounded by GitHub Archive availability and the SRE registry snapshot date; recent cohorts have shorter observation windows and lower apparent retention by construction + """), + "Data Sources": mo.md(""" +- **SRE GitHub users** — `int_sre_github_users`: user registry, cohorts, batches, challenges completed +- **GitHub events** — `int_sre_github_events_by_user`: public GitHub events joined to SRE users, from [GitHub Archive](https://gharchive.org) +- **Ecosystem mappings** — `stg_opendevdata__*`: Electric Capital's repo → ecosystem mappings, via [Open Dev Data](https://opendevdata.org/) +- **Further reading**: [Speedrun Ethereum](https://speedrunethereum.com/) · [Pyoso docs](https://docs.opensource.observer/docs/get-started/python) · [Marimo docs](https://docs.marimo.io/) + """), + }) + return + + @app.cell(hide_code=True) def _(): # Code snippets From 48130bf53f277a33cb4b6c697745b56a874ae528 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:40:10 -0400 Subject: [PATCH 14/23] chore: harmonize lifecycle and retention headers and accordions Co-Authored-By: Claude Opus 4.6 (1M context) --- notebooks/insights/developer-lifecycle.py | 100 +++++++++++----------- notebooks/insights/developer-retention.py | 42 +++------ 2 files changed, 61 insertions(+), 81 deletions(-) diff --git a/notebooks/insights/developer-lifecycle.py b/notebooks/insights/developer-lifecycle.py index 3bb9613..58d493d 100644 --- a/notebooks/insights/developer-lifecycle.py +++ b/notebooks/insights/developer-lifecycle.py @@ -6,61 +6,10 @@ @app.cell(hide_code=True) def header_title(mo): - mo.md(""" - # Lifecycle Analysis - Owner: OSO Team · Last Updated: 2026-02-17 - - Visualize the full lifecycle of a developer joining, contributing, and leaving an ecosystem. - """) + mo.Html('

Lifecycle Analysis

Tracking how developers move through lifecycle states across crypto ecosystems.

Created: 2026-03-16
') return -@app.cell(hide_code=True) -def header_accordion(mo): - mo.accordion({ - "Overview": mo.md(""" -- This notebook tracks developer lifecycle states — the month-by-month progression of developers joining, contributing, and eventually churning from an ecosystem -- It reveals how the balance between newcomers, established contributors, and churned developers shifts over time and across ecosystems -- Key metrics: monthly active developers by lifecycle state, churn ratio, dormant developer count - """), - "Context": mo.md(""" -**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: - -| Category | Label | Description | -|:---------|:------|:------------| -| **First Time** | `first time` | First-ever contribution to the ecosystem | -| **Full Time** | `full time` | 10+ active days, continuing from prior month | -| | `new full time` | First month reaching 10+ active days | -| | `part time to full time` | Transitioned from part-time level | -| | `dormant to full time` | Returned from dormancy at full-time level | -| **Part Time** | `part time` | 1-9 active days, continuing from prior month | -| | `new part time` | First month at part-time level | -| | `full time to part time` | Stepped down from full-time level | -| | `dormant to part time` | Returned from dormancy at part-time level | -| **Churned / Dormant** | `dormant` | No activity this month (previously active) | -| | `first time to dormant` | Dormant after first contribution | -| | `part time to dormant` | Dormant after part-time activity | -| | `full time to dormant` | Dormant after full-time activity | -| | `churned (after first time)` | Extended inactivity after first contribution | -| | `churned (after reaching part time)` | Extended inactivity after reaching part time | -| | `churned (after reaching full time)` | Extended inactivity after reaching full time | - -**Active** = First Time + Full Time + Part Time (all 9 labels above the Churned/Dormant group) - -**Churn Ratio** = sum(churned + dormant) / sum(active) over the trailing window (12mo or all-time) - -Data is bucketed monthly; private repos excluded; contributions include commits, issues, pull requests, and code reviews. - -**Metric Definitions** -- Lifecycle — Developer stage definitions -- Activity — Monthly Active Developer (MAD) methodology - """), - "Data Sources": mo.md(""" -- **Open Dev Data (Electric Capital)** — Ecosystem and developer taxonomy, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems) -- **Key Models** — `oso.int_crypto_ecosystems_developer_lifecycle_monthly_aggregated` - """), - }) - return @app.cell(hide_code=True) @@ -418,6 +367,53 @@ def apply_ec_style(fig, title=None, subtitle=None, y_title=None, show_legend=Tru return (apply_ec_style,) +@app.cell(hide_code=True) +def header_accordion(mo): + mo.accordion({ + "Metrics & Definitions": mo.md(""" +**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: + +| Category | Label | Description | +|:---------|:------|:------------| +| **First Time** | `first time` | First-ever contribution to the ecosystem | +| **Full Time** | `full time` | 10+ active days, continuing from prior month | +| | `new full time` | First month reaching 10+ active days | +| | `part time to full time` | Transitioned from part-time level | +| | `dormant to full time` | Returned from dormancy at full-time level | +| **Part Time** | `part time` | 1-9 active days, continuing from prior month | +| | `new part time` | First month at part-time level | +| | `full time to part time` | Stepped down from full-time level | +| | `dormant to part time` | Returned from dormancy at part-time level | +| **Churned / Dormant** | `dormant` | No activity this month (previously active) | +| | `first time to dormant` | Dormant after first contribution | +| | `part time to dormant` | Dormant after part-time activity | +| | `full time to dormant` | Dormant after full-time activity | +| | `churned (after first time)` | Extended inactivity after first contribution | +| | `churned (after reaching part time)` | Extended inactivity after reaching part time | +| | `churned (after reaching full time)` | Extended inactivity after reaching full time | + +**Active** = First Time + Full Time + Part Time (all 9 labels above the Churned/Dormant group) + +**Churn Ratio** = sum(churned + dormant) / sum(active) over the trailing window (12mo or all-time) + +**Metric Definitions** +- Lifecycle — Developer stage definitions +- Activity — Monthly Active Developer (MAD) methodology + """), + "Assumptions & Limitations": mo.md(""" +- **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 +- **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 +- **Identity resolution**: Developer identities are resolved by Electric Capital's fingerprinting; the same person using different accounts may be counted multiple times +- **Public GitHub only**: Only public GitHub commits and activity are tracked; private repos and non-GitHub platforms are excluded + """), + "Data Sources": mo.md(""" +- **Open Dev Data (Electric Capital)** — Ecosystem and developer taxonomy, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems) +- **Key Models** — `oso.int_crypto_ecosystems_developer_lifecycle_monthly_aggregated` + """), + }) + return + + @app.cell(hide_code=True) def test_connection(mo, pyoso_db_conn): _test_df = mo.sql("""SELECT 1 AS test""", engine=pyoso_db_conn, output=False) diff --git a/notebooks/insights/developer-retention.py b/notebooks/insights/developer-retention.py index 0c9c1a1..f9c9288 100644 --- a/notebooks/insights/developer-retention.py +++ b/notebooks/insights/developer-retention.py @@ -6,50 +6,34 @@ @app.cell(hide_code=True) def header_title(mo): - mo.md(""" - # Retention Analysis - Owner: OSO Team · Last Updated: 2026-02-17 - - Analyze developer retention by ecosystem and cohort year — what percentage of developers who joined in Year X are still active after N years? - """) + mo.Html('

Retention Analysis

Measuring cohort-based developer retention across crypto ecosystems.

Created: 2026-03-16
') return + + @app.cell(hide_code=True) def header_accordion(mo): mo.accordion({ - "Overview": mo.md(""" -- This notebook analyzes developer retention by cohort year: what share of developers who joined ecosystem X in year Y are still active after N years? -- Retention is measured as the percentage of the original cohort active in subsequent years (Year 0 = always 100%) -- Industry benchmarks for context: - -| Timeframe | Open Source | Strong OSS Ecosystem | -|:-----------|:------------|:---------------------| -| 1 year | ~15% | 25-35% | -| 2 years | ~8% | 15-20% | - """), - "Context": mo.md(""" + "Metrics & Definitions": mo.md(""" **Definitions** -- **Cohort**: Developers grouped by the year (or month) of their first contribution to the ecosystem +- **Cohort**: Developers grouped by the year of their first contribution to the ecosystem - **Retention Rate**: Percentage of the original cohort that remains active in subsequent periods - **Years Since Join**: Time elapsed since first contribution (Year 0 = joined year, always 100%) **Methodology** 1. **Cohort Assignment**: Each developer is assigned to a cohort based on their first contribution date -2. **Activity Tracking**: We track whether the developer had any activity in subsequent time periods +2. **Activity Tracking**: We track whether the developer had any activity in subsequent years 3. **Retention Rate**: Percentage of the original cohort that remains active - -**Limitations** - -- Multi-ecosystem developers may churn from one ecosystem but remain active in another -- Identity resolution is based on Electric Capital's developer fingerprinting -- Newer cohorts have less retention history to analyze - -**Metric Definitions** -- Retention — Cohort-based retention methodology -- Activity — Monthly Active Developer (MAD) methodology + """), + "Assumptions & Limitations": mo.md(""" +- **Multi-ecosystem developers**: Developers active in multiple ecosystems are counted separately per ecosystem — a developer who churns from one ecosystem may still be active in another +- **Identity resolution**: Developer identities are resolved by Electric Capital's fingerprinting; the same person using different accounts may be counted multiple times +- **Newer cohorts**: More recent cohorts have shorter observation windows and therefore fewer data points for retention analysis +- **Public commits only**: Only public GitHub activity is tracked; private repos and non-GitHub platforms are excluded +- **Activity windows**: Activity is measured using 28-day rolling windows via Open Dev Data's `repo_developer_28d_activities` model """), "Data Sources": mo.md(""" - **Open Dev Data (Electric Capital)** — Developer activity data, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems) From 60bbca48bb7d26d7c8ddb6bf6668f14c2ca367b5 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:40:14 -0400 Subject: [PATCH 15/23] chore: harmonize developer-report-2025 header and accordion Co-Authored-By: Claude Sonnet 4.6 --- notebooks/insights/developer-report-2025.py | 44 ++++++--------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/notebooks/insights/developer-report-2025.py b/notebooks/insights/developer-report-2025.py index 24cf415..5b0513e 100644 --- a/notebooks/insights/developer-report-2025.py +++ b/notebooks/insights/developer-report-2025.py @@ -1,49 +1,29 @@ import marimo -__generated_with = "0.18.4" +__generated_with = "unknown" app = marimo.App(width="full", css_file="../styles/insights.css") @app.cell(hide_code=True) def header_title(mo): - mo.md(""" - # 2025 Developer Trends - Owner: OSO Team · Last Updated: 2026-02-17 - - Explore an interactive reproduction of the [Electric Capital Developer Report](https://www.developerreport.com), updated with 2025 data. - """) + mo.Html('

2025 Developer Trends

Exploring monthly active developer trends across crypto ecosystems using Electric Capital data.

Created: 2026-03-16
') return @app.cell(hide_code=True) def header_accordion(mo): mo.accordion({ - "Overview": mo.md(""" - - As of December 2025, the total number of monthly active developers (MADs) across all crypto ecosystems reached its highest recorded level, driven by growth in newer chains and Layer 2s - - Ethereum remains the largest single ecosystem by MAD count, though its share of total crypto developers continued to decline as multi-chain activity increases - - Newcomer developers (those active for less than 1 year) represented a significant portion of 2025 MADs, indicating continued onboarding despite broader market fluctuations - - Full-time developers (active 10+ months of the year) showed resilience, with retention rates improving year-over-year compared to the 2022-2023 downturn - """), - "Context": mo.md(""" - - This analysis covers monthly active developers across all crypto ecosystems - - Data source: Open Dev Data (Electric Capital) via OSO data warehouse - - Time period: January 2015 to December 2025 (full historical data) - - Developers are original code authors (merge/PR integrators are not counted unless they authored commits) - - Monthly active developers are measured using a 28-day rolling activity window - - Uses curated Open Dev Data repository set (not comprehensive GitHub coverage) - - Developer identity resolution may miss some connections across accounts or pseudonyms - - Data freshness depends on Open Dev Data and OSO pipeline update cadence - - **Metric Definitions** - - Activity — Monthly Active Developer (MAD) methodology - - **Methodology** - - **Developers**: Original code authors only (merge/PR integrators excluded unless they authored commits) - - **Monthly Active Developers**: 28-day rolling activity window - - **Tenure Categories**: Newcomers (< 1 year), Emerging (1–2 years), Established (2+ years) + "Metrics & Definitions": mo.md(""" + - **Time period**: January 2015 to December 2025 (full historical data) + - **Monthly Active Developer (MAD)**: A developer who authored at least 1 commit in a given month (measured using a 28-day rolling activity window) + - **Tenure Categories**: Newcomers (< 1 year active), Emerging (1–2 years), Established (2+ years) - **Activity Levels**: Full-time (sustained activity over 84-day window), Part-time (intermittent), One-time (sporadic) - - > This analysis is inspired by the [Electric Capital Developer Report](https://www.developerreport.com). Data sourced from Open Dev Data via the OSO data warehouse. + """), + "Assumptions & Limitations": mo.md(""" + - **Commit-only activity measure**: Developer activity is based on commits only — pull requests, code reviews, and issue comments are not counted + - **Public repos only**: Private repositories are excluded from the dataset + - **Identity resolution**: Developer identities are resolved across forges using Electric Capital's methodology, but some cross-account connections may be missed + - **Ecosystem classification**: Ecosystem assignments follow the Electric Capital taxonomy; projects may belong to multiple ecosystems """), "Data Sources": mo.md(""" - **Open Dev Data** — Electric Capital's developer activity dataset, [github.com/electric-capital/crypto-ecosystems](https://github.com/electric-capital/crypto-ecosystems) From 69e0d825b3070708ceeed2143f9f5b481ac7e697 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:41:24 -0400 Subject: [PATCH 16/23] chore: harmonize ethereum-repo-rank accordion --- notebooks/insights/ethereum-repo-rank.py | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/notebooks/insights/ethereum-repo-rank.py b/notebooks/insights/ethereum-repo-rank.py index 4930e42..d4b7c96 100644 --- a/notebooks/insights/ethereum-repo-rank.py +++ b/notebooks/insights/ethereum-repo-rank.py @@ -670,17 +670,29 @@ def _(df_trending, df_engagement_daily, mo): @app.cell(hide_code=True) def _(mo): mo.accordion({ - "Methodology & Data Sources": mo.md(""" - **Developer panel**: Qualified builders with ≥12 months commit activity on Ethereum panel repos and verified GitHub logins. ~920 infrastructure, ~415 DeFi. + "Metrics & Definitions": mo.md(""" + **Ethereum builder panel**: Qualified builders with ≥12 months commit activity on Ethereum panel repos and verified GitHub logins. ~920 infrastructure, ~415 DeFi. - **Repo selection**: Top ~150 repos by aggregate panel builder attention (stars + forks). Stargazer/forker usernames scraped directly from the GitHub API (30-day rolling window). + **Engagement metrics**: Stars + forks collected over 30-day and 7-day rolling windows. Stargazer/forker usernames are scraped directly from the GitHub API and deduplicated within each window. - **Signal strength**: For each repo, we compute the % of engagers (stargazers + forkers, deduplicated) who are Ethereum panel builders — revealing disproportionate interest vs mainstream audience. + **Signal strength (eth_dev_pct)**: For each repo, the share of engagers who are Ethereum panel builders. A high percentage means the repo is drawing disproportionate attention from active Ethereum developers relative to the mainstream GitHub audience. - **Caveats**: Starring ≠ using. The repos were selected *because* they attracted Ethereum builder attention — this is not a random sample. The panel captures the most active slice of Ethereum builders, not all of them. Attention patterns shift; a repo trending today may be forgotten next month. + **Momentum**: 7-day daily engagement rate divided by 30-day daily engagement rate. A ratio above 1.0 means the repo is accelerating; below 1.0 means interest is cooling. + """), + "Assumptions & Limitations": mo.md(""" + **Starring ≠ using.** A star is a lightweight signal of interest, not adoption or production use. - **Data sources**: [OSO](https://www.oso.xyz) data warehouse (`ethereum.local_rank_models`, `ethereum.dev_engagement_models`) · GitHub API (GraphQL + REST) - """) + **Non-random sample.** The repos were selected *because* they attracted Ethereum builder attention — this is not a representative cross-section of all open source software. + + **Panel is a ceiling, not a floor.** The Ethereum builder panel captures the most active slice of Ethereum developers. Many builders fall below the activity threshold and are not counted. + + **Attention is ephemeral.** Engagement patterns shift quickly. A repo trending today may drop out of the rankings next month as the community's focus moves on. + """), + "Data Sources": mo.md(""" + **OSO data warehouse** — `ethereum.local_rank_models` (repo rankings and signal strength), `ethereum.dev_engagement_models` (per-repo engagement counts and panel overlap) + + **GitHub API** — GraphQL + REST endpoints used to collect stargazer and forker usernames for each tracked repo over rolling windows. + """), }) return From 6ffebdb8f8a77f3400a7c76196d9290b92736058 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:42:36 -0400 Subject: [PATCH 17/23] chore: harmonize defi-builder-journeys header and accordion --- notebooks/insights/defi-builder-journeys.py | 95 ++++++++++----------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/notebooks/insights/defi-builder-journeys.py b/notebooks/insights/defi-builder-journeys.py index b4378da..82e8a5b 100644 --- a/notebooks/insights/defi-builder-journeys.py +++ b/notebooks/insights/defi-builder-journeys.py @@ -6,10 +6,7 @@ @app.cell(hide_code=True) def header_title(mo): - mo.md(r""" - # DeFi Builder Journeys - Owner: OSO Team · Last Updated: 2026-03-16 - """) + mo.Html('

DeFi Builder Journeys

Analyzing builder activity across 40 top DeFi protocols on Ethereum and other major L1s.

Created: 2026-03-16
') return @@ -1330,6 +1327,51 @@ def transform_pipeline_composition( return (pipeline_by_eco,) +@app.cell(hide_code=True) +def section_conclusion(mo): + mo.accordion({ + "Metrics & Definitions": mo.md(""" + - Ecosystem Classification: + - Ethereum: Projects with >80% of TVL on Ethereum (L1 + L2s like Arbitrum, Polygon, Base, Optimism, etc.) + - Solana: Projects whose top chain by TVL is Solana + - Other: Remaining non-Ethereum, non-Solana projects (including Hyperliquid, Bitcoin, etc.) + - Project Classifications: + - Home Project: The DeFi project a builder is primarily associated with, based on their most active repository contributions. + - Feeder Projects: Projects where a builder contributed or starred repositories before onboarding to their home DeFi project. + - Builder Classifications: + - Qualifying Builder: A builder with 12+ months of sustained contribution to a top DeFi project's home repositories. + - Monthly Active Builders (MABs): Count of unique qualified builders with at least one contribution event in a given month. + - Newcomer: A qualified builder with fewer than 6 months of observable open-source activity before onboarding to their home project (came in cold). + - Cohort Retention: The percentage of builders from a given onboarding cohort (year) who remain active on their home project at each subsequent time interval. + - Lifecycle: + - Onboarding: A builder's first month of activity on their home project. + - Offboarding: A builder is considered offboarded if they have been inactive on their home project for 6+ consecutive months. + - Still Active: A builder who has contributed to their home project within the most recent 6 months of data. + - Flows: + - Net Flow: The year-over-year difference between builders entering and exiting an ecosystem. Positive means net talent gain. + - Cross-Ecosystem Flow (Ethereum <-> Other Crypto): The horizontal bar in the Net Flows section counts unique builders who moved between Ethereum and other crypto ecosystems over the full time period. This uses a different method from the annual inflow/outflow bars, which count year-over-year flows by source/destination category; the two methods do not sum to each other. + - Year-over-Year Flows: The annual bar chart breaks down builder inflows (entering) and outflows (leaving) by partner category (Other Crypto, Non-Crypto OSS, Inactive). The net flow line shows the cumulative balance; these year-over-year counts sum to the total net flow shown in the stat cards. + """), + "Assumptions & Limitations": mo.md(""" + - TVL as inclusion criterion: We use TVL to identify economically meaningful protocols, which excludes low-stakes forks and testnet-only experiments; this analysis focuses on capital-securing DeFi. + - Excluded protocols: Some high-TVL protocols have minimal observable OSS activity, so they may appear in tables but contribute little to flow analysis. + - Private repositories: Activity in private repos is not visible to GitHub Archive, so teams that develop behind closed doors (or move to private repos after open-source phases) may appear to go inactive; this is one of the most consequential limitations. + - Post-hire behavior: Some builders are hired by crypto firms and stop public contributions, so the inactive count may be inflated; we cannot distinguish left crypto from went private. + - Bot and noise filtering: We exclude known bots, but some automated contributions may still slip through. + - Identity resolution: OpenDevData maps multiple accounts to canonical IDs, but imperfect mapping may double-count some builders. + - Scope: This is a structured analysis of visible OSS builder flows across economically significant DeFi protocols, not a census of all crypto builders or proprietary development. + """), + "Data Sources": mo.md(""" + - [DefiLlama](https://defillama.com/) --- Top DeFi protocols by TVL (40 selected) + - [OSS Directory](https://github.com/opensource-observer/oss-directory) --- Protocol to GitHub mapping + - [OpenDevData (Electric Capital)](https://github.com/electric-capital/crypto-ecosystems) --- Ecosystem classifications + - [GitHub Archive](https://www.gharchive.org/) --- Builder activity events + - [OSO API](https://docs.oso.xyz/) --- Data pipeline and metrics + """), + }) + return + + @app.cell(hide_code=True) def settings_color_palette(): # Color palette — semantic colors reused across all charts @@ -2429,51 +2471,6 @@ def _grid_group(df, col_offset, border_color, label): return (comparison_tab_content,) -@app.cell(hide_code=True) -def section_conclusion(mo): - mo.accordion({ - "Metrics & Definitions": mo.md(""" - - Ecosystem Classification: - - Ethereum: Projects with >80% of TVL on Ethereum (L1 + L2s like Arbitrum, Polygon, Base, Optimism, etc.) - - Solana: Projects whose top chain by TVL is Solana - - Other: Remaining non-Ethereum, non-Solana projects (including Hyperliquid, Bitcoin, etc.) - - Project Classifications: - - Home Project: The DeFi project a builder is primarily associated with, based on their most active repository contributions. - - Feeder Projects: Projects where a builder contributed or starred repositories before onboarding to their home DeFi project. - - Builder Classifications: - - Qualifying Builder: A builder with 12+ months of sustained contribution to a top DeFi project's home repositories. - - Monthly Active Builders (MABs): Count of unique qualified builders with at least one contribution event in a given month. - - Newcomer: A qualified builder with fewer than 6 months of observable open-source activity before onboarding to their home project (came in cold). - - Cohort Retention: The percentage of builders from a given onboarding cohort (year) who remain active on their home project at each subsequent time interval. - - Lifecycle: - - Onboarding: A builder's first month of activity on their home project. - - Offboarding: A builder is considered offboarded if they have been inactive on their home project for 6+ consecutive months. - - Still Active: A builder who has contributed to their home project within the most recent 6 months of data. - - Flows: - - Net Flow: The year-over-year difference between builders entering and exiting an ecosystem. Positive means net talent gain. - - Cross-Ecosystem Flow (Ethereum <-> Other Crypto): The horizontal bar in the Net Flows section counts unique builders who moved between Ethereum and other crypto ecosystems over the full time period. This uses a different method from the annual inflow/outflow bars, which count year-over-year flows by source/destination category; the two methods do not sum to each other. - - Year-over-Year Flows: The annual bar chart breaks down builder inflows (entering) and outflows (leaving) by partner category (Other Crypto, Non-Crypto OSS, Inactive). The net flow line shows the cumulative balance; these year-over-year counts sum to the total net flow shown in the stat cards. - """), - "Assumptions & Limitations": mo.md(""" - - TVL as inclusion criterion: We use TVL to identify economically meaningful protocols, which excludes low-stakes forks and testnet-only experiments; this analysis focuses on capital-securing DeFi. - - Excluded protocols: Some high-TVL protocols have minimal observable OSS activity, so they may appear in tables but contribute little to flow analysis. - - Private repositories: Activity in private repos is not visible to GitHub Archive, so teams that develop behind closed doors (or move to private repos after open-source phases) may appear to go inactive; this is one of the most consequential limitations. - - Post-hire behavior: Some builders are hired by crypto firms and stop public contributions, so the inactive count may be inflated; we cannot distinguish left crypto from went private. - - Bot and noise filtering: We exclude known bots, but some automated contributions may still slip through. - - Identity resolution: OpenDevData maps multiple accounts to canonical IDs, but imperfect mapping may double-count some builders. - - Scope: This is a structured analysis of visible OSS builder flows across economically significant DeFi protocols, not a census of all crypto builders or proprietary development. - """), - "Data Sources": mo.md(""" - - [DefiLlama](https://defillama.com/) --- Top DeFi protocols by TVL (40 selected) - - [OSS Directory](https://github.com/opensource-observer/oss-directory) --- Protocol to GitHub mapping - - [OpenDevData (Electric Capital)](https://github.com/electric-capital/crypto-ecosystems) --- Ecosystem classifications - - [GitHub Archive](https://www.gharchive.org/) --- Builder activity events - - [OSO API](https://docs.oso.xyz/) --- Data pipeline and metrics - """), - }) - return - - @app.cell(hide_code=True) def main_layout( comparison_tab_content, From 368a8349f0448b01d267caa03a6ab10f9cbc8b29 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:45:59 -0400 Subject: [PATCH 18/23] fix: harmonize repo-rank header and shorten stat label to prevent overflow --- notebooks/insights/ethereum-repo-rank.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebooks/insights/ethereum-repo-rank.py b/notebooks/insights/ethereum-repo-rank.py index d4b7c96..23def31 100644 --- a/notebooks/insights/ethereum-repo-rank.py +++ b/notebooks/insights/ethereum-repo-rank.py @@ -11,7 +11,7 @@ @app.cell(hide_code=True) def _(mo): - _header_html = '

Repo Rank

Tracking which repos are trending in the Ethereum builder community.

Live dataUpdated 2026-03-16·Kariba Labs / OSO
' + _header_html = '

Repo Rank

Tracking which repos are trending in the Ethereum builder community.

Live dataCreated: 2026-03-16
' mo.Html(_header_html) return @@ -37,7 +37,7 @@ def _(df_trending, eth_dev_set, df_engagement_raw, mo): mo.hstack( [ mo.stat(value=f"{_panel_size:,}", label="Ethereum Builders Tracked", bordered=True, caption="≥12 months commit activity"), - mo.stat(value=f"{_active_eth}", label="Eth Builders Active on Trending Repos", bordered=True, caption=f"{_active_eth/_panel_size*100:.1f}% of panel"), + mo.stat(value=f"{_active_eth}", label="Active on Trending Repos", bordered=True, caption=f"{_active_eth/_panel_size*100:.1f}% of panel"), mo.stat(value=_top_eth_repo, label="#1 by Eth Builder Attention", bordered=True, caption=f"{_top_eth_devs} distinct eth builders"), mo.stat(value=_top_all_repo, label="#1 by All Builder Attention", bordered=True, caption=f"{_top_all_devs:,} distinct builders"), ], From 1d5d9828188ea3aebe2648513496eff83b5f0d53 Mon Sep 17 00:00:00 2001 From: ccerv1 Date: Tue, 17 Mar 2026 12:47:56 -0400 Subject: [PATCH 19/23] fix: default leaderboard sort to Eth Builder Attention --- notebooks/insights/ethereum-repo-rank.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/insights/ethereum-repo-rank.py b/notebooks/insights/ethereum-repo-rank.py index 23def31..ed86d43 100644 --- a/notebooks/insights/ethereum-repo-rank.py +++ b/notebooks/insights/ethereum-repo-rank.py @@ -103,8 +103,8 @@ def _(df_trending, mo): '
' '' '' '