Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,703 changes: 3,999 additions & 3,704 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,35 @@ There is also a human cost to having majority clients. It puts excess strain and

## Current client diversity {#current-client-diversity}

![Pie chart showing client diversity](./client-diversity.png)
_Diagram data from [ethernodes.org](https://ethernodes.org) and [clientdiversity.org](https://clientdiversity.org/)_

The two pie charts above show snapshots of the current client diversity for the execution and consensus layers (at time of writing in January 2022). The execution layer is overwhelmingly dominated by [Geth](https://geth.ethereum.org/), with [Open Ethereum](https://openethereum.github.io/) a distant second, [Erigon](https://github.com/ledgerwatch/erigon) third and [Nethermind](https://nethermind.io/) fourth, with other clients comprising less than 1 % of the network. The most commonly used client on the consensus layer - [Prysm](https://prysmaticlabs.com/#projects) - is not as dominant as Geth but still represents over 60% of the network. [Lighthouse](https://lighthouse.sigmaprime.io/) and [Teku](https://consensys.net/knowledge-base/ethereum-2/teku/) make up ~20% and ~14% respectively, and other clients are rarely used.

The execution layer data were obtained from [Ethernodes](https://ethernodes.org) on 23-Jan-2022. Data for consensus clients was obtained from [Michael Sproul](https://github.com/sigp/blockprint). Consensus client data is more difficult to obtain because the consensus layer clients do not always have unambiguous traces that can be used to identify them. The data was generated using a classification algorithm that sometimes confuses some of the minority clients (see [here](https://twitter.com/sproulM_/status/1440512518242197516) for more details). In the diagram above, these ambiguous classifications are treated with an either/or label (e.g., Nimbus/Teku). Nevertheless, it is clear that the majority of the network is running Prysm. The data is a snapshot over a fixed set of blocks (in this case Beacon blocks in slots 2048001 to 2164916) and Prysm's dominance has sometimes been higher, exceeding 68%. Despite only being snapshots, the values in the diagram provide a good general sense of the current state of client diversity.
<ClientDiversityChart>
<PieChart
data={[
{ name: "Geth", value: 41 },
{ name: "Nethermind", value: 38 },
{ name: "Besu", value: 16 },
{ name: "Erigon", value: 3 },
{ name: "Reth", value: 2 }
]}
title="Execution Clients"
/>

<PieChart
data={[
{ name: "Lighthouse", value: 42.71 },
{ name: "Prysm", value: 30.91},
{ name: "Teku", value: 13.86},
{ name: "Nimbus", value: 8.74},
{ name: "Lodestar", value: 2.67 },
{ name: "Grandine", value: 1.04 },
{ name: "Other", value: 0.07 }
]}
title="Consensus Clients"
/>
</ClientDiversityChart>
Copy link
Member

@wackerow wackerow Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Getting challenging overflow issues with these charts when trying to lay them side-by-side. I think for simplicity and robustness, we should probably just keep them stacked.

Benefits:

  • No x-overflow when they're side-by-side
  • The ClientDiversityChart wrapper becomes completely unnecessary... can just render the two PieChart components we need
  • Can also just place the sub-text directly here as markdown, avoiding the need for altering page-developers-docs.json
Suggested change
<ClientDiversityChart>
<PieChart
data={[
{ name: "Geth", value: 41 },
{ name: "Nethermind", value: 38 },
{ name: "Besu", value: 16 },
{ name: "Erigon", value: 3 },
{ name: "Reth", value: 2 }
]}
title="Execution Clients"
/>
<PieChart
data={[
{ name: "Lighthouse", value: 42.71 },
{ name: "Prysm", value: 30.91},
{ name: "Teku", value: 13.86},
{ name: "Nimbus", value: 8.74},
{ name: "Lodestar", value: 2.67 },
{ name: "Grandine", value: 1.04 },
{ name: "Other", value: 0.07 }
]}
title="Consensus Clients"
/>
</ClientDiversityChart>
<PieChart
data={[
{ name: "Geth", value: 41 },
{ name: "Nethermind", value: 38 },
{ name: "Besu", value: 16 },
{ name: "Erigon", value: 3 },
{ name: "Reth", value: 2 }
]}
title="Execution Clients"
/>
<PieChart
data={[
{ name: "Lighthouse", value: 42.71 },
{ name: "Prysm", value: 30.91},
{ name: "Teku", value: 13.86},
{ name: "Nimbus", value: 8.74},
{ name: "Lodestar", value: 2.67 },
{ name: "Grandine", value: 1.04 },
{ name: "Other", value: 0.07 }
]}
title="Consensus Clients"
/>
> This diagram may be outdated—go to [ethernodes.org](https://ethernodes.org) and [clientdiversity.org](https://clientdiversity.org) for up-to-date information.
image


The two pie charts above show snapshots of the current client diversity for the execution and consensus layers (at time of writing in October 2025). Client diversity has improved over the years, and the execution layer has seen a reduction in the domination by [Geth](https://geth.ethereum.org/), with [Nethermind](https://www.nethermind.io/nethermind-client) a close second, [Besu](https://besu.hyperledger.org/) third and [Erigon](https://github.com/ledgerwatch/erigon) fourth, with other clients comprising less than 3 % of the network. The most commonly used client on the consensus layer - [Lighthouse](https://lighthouse.sigmaprime.io/) - is quite close with the second most used.[Prysm](https://prysmaticlabs.com/#projects) and [Teku](https://consensys.net/knowledge-base/ethereum-2/teku/) make up ~31% and ~14% respectively, and other clients are rarely used.

The execution layer data were obtained from [supermajority.info](https://supermajority.info/) on 22-Oct-2025. Data for consensus clients was obtained from [Michael Sproul](https://github.com/sigp/blockprint). Consensus client data is more difficult to obtain because the consensus layer clients do not always have unambiguous traces that can be used to identify them. The data was generated using a classification algorithm that sometimes confuses some of the minority clients (see [here](https://twitter.com/sproulM_/status/1440512518242197516) for more details). In the diagram above, these ambiguous classifications are treated with an either/or label (e.g. Nimbus/Teku). Nevertheless, it is clear that the majority of the network is running Prysm. Despite only being snapshots, the values in the diagram provide a good general sense of the current state of client diversity.

Up to date client diversity data for the consensus layer is now available at [clientdiversity.org](https://clientdiversity.org/).

Expand Down
27 changes: 27 additions & 0 deletions src/components/ClientDiversityChart/index.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the index.md file, would just ditch this wrapper entirely, along with it's import in MdComponents/index.tsx, and the additions to page-developers-docs.json... that string can go straight into the markdown file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have effected the changes. Testing before sending out.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client"

import React from "react"
import type { ReactNode } from "react"

import Translation from "../Translation"

interface ClientDiversityChartProps {
children: ReactNode
}

const ClientDiversityChart = ({ children }: ClientDiversityChartProps) => {
return (
<div className="grid w-full grid-cols-1 gap-4 lg:flex lg:flex-col">
<div className="grid grid-cols-1 xl:flex">{children}</div>

<div className="text-center text-sm italic text-gray-600 dark:text-white">
<Translation
id="client-diversity-chart-disclaimer"
ns="page-developers-docs"
/>
</div>
</div>
)
}

export default ClientDiversityChart
4 changes: 4 additions & 0 deletions src/components/MdComponents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ChildOnlyProp } from "@/lib/types"

import ContributorsQuizBanner from "@/components/Banners/ContributorsQuizBanner"
import Card from "@/components/Card"
import ClientDiversityChart from "@/components/ClientDiversityChart"
import BrowseApps from "@/components/Content/what-are-apps/BrowseApps"
import WhatAreAppsStories from "@/components/Content/what-are-apps/WhatAreAppsStories"
import Contributors from "@/components/Contributors"
Expand All @@ -17,6 +18,7 @@ import MarkdownImage from "@/components/Image/MarkdownImage"
import IssuesList from "@/components/IssuesList"
import LocaleDateTime from "@/components/LocaleDateTime"
import MainArticle from "@/components/MainArticle"
import { PieChart } from "@/components/PieChart"
import { StandaloneQuizWidget } from "@/components/Quiz/QuizWidget"
import TooltipLink from "@/components/TooltipLink"
import { ButtonLink } from "@/components/ui/buttons/Button"
Expand Down Expand Up @@ -165,6 +167,7 @@ export const ContentContainer = (props: ComponentProps<"article">) => {
export const reactComponents = {
...AlertComponents,
BrowseApps,
ClientDiversityChart,
ButtonLink,
Card,
ContentContainer,
Expand All @@ -177,6 +180,7 @@ export const reactComponents = {
FeaturedText,
GlossaryTooltip,
Page,
PieChart,
QuizWidget: StandaloneQuizWidget,
IssuesList,
Tag,
Expand Down
253 changes: 253 additions & 0 deletions src/components/PieChart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"use client"

import { TrendingUp } from "lucide-react"
import {
Cell,
Legend,
Pie,
PieChart as RechartsPieChart,
ResponsiveContainer,
type TooltipProps,
} from "recharts"
import type { Formatter } from "recharts/types/component/DefaultLegendContent"

import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart"

type PieChartDataPoint = { name: string; value: number }

/**
* PieChartProps defines the properties for the PieChart component.
*
* @property {PieChartDataPoint[]} data - The data to be displayed in the chart. Each object should have a `name` and `value` property.
* @property {string} [title] - The title of the chart.
* @property {string} [description] - The description of the chart.
* @property {string} [footerText] - The footer text of the chart.
* @property {string} [footerSubText] - The footer subtext of the chart.
* @property {boolean} [showPercentage=true] - Whether to show percentage values in legend and tooltips.
* @property {number} [minSlicePercentage=1] - Minimum percentage to show individual slices (smaller values grouped as "Other").
*/
type PieChartProps = {
data: PieChartDataPoint[]
title?: string
description?: string
footerText?: string
footerSubText?: string
showPercentage?: boolean
minSlicePercentage?: number
}

const defaultChartConfig = {
value: {
label: "Value",
color: "hsl(var(--accent-a))",
},
} satisfies ChartConfig

const COLORS = [
"hsla(var(--accent-a))",
"hsla(var(--accent-b))",
"hsla(var(--accent-c))",
"hsla(var(--accent-a-hover))",
"hsla(var(--accent-b-hover))",
"hsla(var(--accent-c-hover))",
]

const generateColor = (index: number): string => {
if (index < COLORS.length) {
return COLORS[index]
}
const hue = (index * 137.508) % 360
const saturation = 70 + (index % 2) * 15
const lightness = 50 + (index % 3) * 8
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
}

// Utility function to validate and process data
const processData = (
data: PieChartDataPoint[],
minSlicePercentage: number = 1
): PieChartDataPoint[] => {
const nonZeroData = data.filter((item) => item.value > 0)

const total = nonZeroData.reduce((sum, item) => sum + item.value, 0)

if (total === 0) return []

const mainItems = nonZeroData.filter(
(item) => (item.value / total) * 100 >= minSlicePercentage
)
const smallItems = nonZeroData.filter(
(item) => (item.value / total) * 100 < minSlicePercentage
)

// Group small items into "Other" if there are any
const processedData = [...mainItems]
if (smallItems.length > 0) {
const otherValue = smallItems.reduce((sum, item) => sum + item.value, 0)
processedData.push({ name: "Other", value: otherValue })
}

return processedData
}

export function PieChart({
data,
title,
description,
footerText,
footerSubText,
showPercentage = true,
minSlicePercentage = 0,
}: PieChartProps) {
const processedData = processData(data, minSlicePercentage)

if (processedData.length === 0) {
return (
<Card className="w-full">
<CardHeader className="!pt-0">
{title && <CardTitle>{title}</CardTitle>}
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent className="flex h-64 items-center justify-center">
<p className="text-muted-foreground">No data available</p>
</CardContent>
</Card>
)
}

// Calculate total for percentage display
const total = processedData.reduce((sum, item) => sum + item.value, 0)

// Function to calculate optimal chart dimensions based on data size and screen
const getChartDimensions = () => {
const dataCount = processedData.length
const baseHeight =
dataCount <= 4 ? 320 : Math.min(380, 280 + dataCount * 15)

return {
height: baseHeight,
outerRadius: Math.max(50, Math.min(80, 400 / Math.max(6, dataCount))),
cx: dataCount <= 3 ? "40%" : dataCount <= 5 ? "35%" : "30%",
}
}

const dimensions = getChartDimensions()

const legendFormatter: Formatter = (label: string, { payload }) => {
const numeric = typeof payload?.value === "number" ? payload.value : 0
const percentage = ((numeric / total) * 100).toFixed(1)

const isSmallScreen =
typeof window !== "undefined" ? window.innerWidth < 640 : false
const maxLength = isSmallScreen ? 10 : 15
const displayName =
label.length > maxLength ? `${label.substring(0, maxLength)}...` : label

return (
<span className="text-xs sm:text-sm" title={label}>
{displayName} {showPercentage && `(${percentage}%)`}
</span>
)
}

// Custom tooltip content
const customTooltipContent = ({
active,
payload,
}: TooltipProps<number, string>) => {
if (!active || !payload || !payload.length) return null

const [data] = payload

if (typeof data.value !== "number") return null

const percentage = ((data.value / total) * 100).toFixed(1)

return (
<div className="rounded-lg border bg-background p-2 shadow-lg">
<p className="font-medium">{data.name}</p>
<p className="text-muted-foreground text-sm">
{showPercentage ? `${percentage}%` : data.value}
</p>
</div>
)
}

return (
<Card
className="w-full"
role="img"
aria-label={title ? `${title} pie chart` : "Pie chart"}
>
<CardHeader className="!pt-0">
{title && <CardTitle>{title}</CardTitle>}
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>

<CardContent>
<ChartContainer config={defaultChartConfig}>
<ResponsiveContainer width="100%" height={dimensions.height}>
<RechartsPieChart className="-me-12 ms-4">
<ChartTooltip cursor={false} content={customTooltipContent} />

<Legend
layout="vertical"
verticalAlign="middle"
align="right"
className="max-w-1/2 break-all text-sm/snug"
formatter={legendFormatter}
/>

<Pie
data={processedData}
dataKey="value"
nameKey="name"
cx={dimensions.cx}
cy="50%"
outerRadius={dimensions.outerRadius}
label={false}
stroke="#ffffff"
strokeWidth={1}
>
{processedData.map((_, i) => (
<Cell key={`cell-${i}`} fill={generateColor(i)} />
))}
</Pie>
</RechartsPieChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>

{(footerText || footerSubText) && (
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
{footerText && (
<div className="flex items-center gap-2 font-medium leading-none">
{footerText} <TrendingUp className="h-4 w-4" />
</div>
)}
{footerSubText && (
<div className="text-muted-foreground flex items-center gap-2 leading-none">
{footerSubText}
</div>
)}
</div>
</div>
</CardFooter>
)}
</Card>
)
}
3 changes: 2 additions & 1 deletion src/intl/en/page-developers-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,6 @@
"back-to-top": "Back to top",
"banner-page-incomplete": "This page is incomplete and we'd love your help. Edit this page and add anything that you think might be useful to others.",
"next": "Next",
"previous": "Previous"
"previous": "Previous",
"client-diversity-chart-disclaimer": "This diagram may be outdated — go to <a href=\"https://ethernodes.org\">ethernodes.org</a> and <a href=\"https://clientdiversity.org/\">clientdiversity.org</a> for up-to-date information."
}