Skip to content

Commit c53bb93

Browse files
authored
Merge pull request #16547 from LifeofDan-EL/feat-client-diversity
Feat: Implement dynamic Pie Chart for Client Diversity and enhance localization support.
2 parents 1b50150 + db55313 commit c53bb93

File tree

3 files changed

+288
-6
lines changed

3 files changed

+288
-6
lines changed

public/content/developers/docs/nodes-and-clients/client-diversity/index.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,37 @@ There is also a human cost to having majority clients. It puts excess strain and
4141

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

44-
![Pie chart showing client diversity](./client-diversity.png)
45-
_Diagram data from [ethernodes.org](https://ethernodes.org) and [clientdiversity.org](https://clientdiversity.org/)_
46-
47-
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.
48-
49-
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.
44+
### Execution Clients {#execution-clients-breakdown}
45+
46+
<PieChart
47+
data={[
48+
{ name: "Geth", value: 41 },
49+
{ name: "Nethermind", value: 38 },
50+
{ name: "Besu", value: 16 },
51+
{ name: "Erigon", value: 3 },
52+
{ name: "Reth", value: 2 }
53+
]}
54+
/>
55+
56+
### Consensus Clients {#consensus-clients-breakdown}
57+
58+
<PieChart
59+
data={[
60+
{ name: "Lighthouse", value: 42.71 },
61+
{ name: "Prysm", value: 30.91},
62+
{ name: "Teku", value: 13.86},
63+
{ name: "Nimbus", value: 8.74},
64+
{ name: "Lodestar", value: 2.67 },
65+
{ name: "Grandine", value: 1.04 },
66+
{ name: "Other", value: 0.07 }
67+
]}
68+
/>
69+
70+
This diagram may be outdated — go to [ethernodes.org](https://ethernodes.org) and [clientdiversity.org](https://clientdiversity.org) for up-to-date information.
71+
72+
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.
73+
74+
The execution layer data were obtained from [supermajority.info](https://supermajority.info/) on 26-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.
5075

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

src/components/MdComponents/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import MarkdownImage from "@/components/Image/MarkdownImage"
1717
import IssuesList from "@/components/IssuesList"
1818
import LocaleDateTime from "@/components/LocaleDateTime"
1919
import MainArticle from "@/components/MainArticle"
20+
import { PieChart } from "@/components/PieChart"
2021
import { StandaloneQuizWidget } from "@/components/Quiz/QuizWidget"
2122
import TooltipLink from "@/components/TooltipLink"
2223
import { ButtonLink } from "@/components/ui/buttons/Button"
@@ -177,6 +178,7 @@ export const reactComponents = {
177178
FeaturedText,
178179
GlossaryTooltip,
179180
Page,
181+
PieChart,
180182
QuizWidget: StandaloneQuizWidget,
181183
IssuesList,
182184
Tag,

src/components/PieChart/index.tsx

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"use client"
2+
3+
import { TrendingUp } from "lucide-react"
4+
import {
5+
Cell,
6+
Legend,
7+
Pie,
8+
PieChart as RechartsPieChart,
9+
ResponsiveContainer,
10+
type TooltipProps,
11+
} from "recharts"
12+
import type { Formatter } from "recharts/types/component/DefaultLegendContent"
13+
14+
import {
15+
Card,
16+
CardContent,
17+
CardDescription,
18+
CardFooter,
19+
CardHeader,
20+
CardTitle,
21+
} from "@/components/ui/card"
22+
import {
23+
ChartConfig,
24+
ChartContainer,
25+
ChartTooltip,
26+
} from "@/components/ui/chart"
27+
28+
type PieChartDataPoint = { name: string; value: number }
29+
30+
/**
31+
* PieChartProps defines the properties for the PieChart component.
32+
*
33+
* @property {PieChartDataPoint[]} data - The data to be displayed in the chart. Each object should have a `name` and `value` property.
34+
* @property {string} [title] - The title of the chart.
35+
* @property {string} [description] - The description of the chart.
36+
* @property {string} [footerText] - The footer text of the chart.
37+
* @property {string} [footerSubText] - The footer subtext of the chart.
38+
* @property {boolean} [showPercentage=true] - Whether to show percentage values in legend and tooltips.
39+
* @property {number} [minSlicePercentage=1] - Minimum percentage to show individual slices (smaller values grouped as "Other").
40+
*/
41+
type PieChartProps = {
42+
data: PieChartDataPoint[]
43+
title?: string
44+
description?: string
45+
footerText?: string
46+
footerSubText?: string
47+
showPercentage?: boolean
48+
minSlicePercentage?: number
49+
}
50+
51+
const defaultChartConfig = {
52+
value: {
53+
label: "Value",
54+
color: "hsl(var(--accent-a))",
55+
},
56+
} satisfies ChartConfig
57+
58+
const COLORS = [
59+
"hsla(var(--accent-a))",
60+
"hsla(var(--accent-b))",
61+
"hsla(var(--accent-c))",
62+
"hsla(var(--accent-a-hover))",
63+
"hsla(var(--accent-b-hover))",
64+
"hsla(var(--accent-c-hover))",
65+
]
66+
67+
const generateColor = (index: number): string => {
68+
if (index < COLORS.length) {
69+
return COLORS[index]
70+
}
71+
const hue = (index * 137.508) % 360
72+
const saturation = 70 + (index % 2) * 15
73+
const lightness = 50 + (index % 3) * 8
74+
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
75+
}
76+
77+
// Utility function to validate and process data
78+
const processData = (
79+
data: PieChartDataPoint[],
80+
minSlicePercentage: number = 1
81+
): PieChartDataPoint[] => {
82+
const nonZeroData = data.filter((item) => item.value > 0)
83+
84+
const total = nonZeroData.reduce((sum, item) => sum + item.value, 0)
85+
86+
if (total === 0) return []
87+
88+
const mainItems = nonZeroData.filter(
89+
(item) => (item.value / total) * 100 >= minSlicePercentage
90+
)
91+
const smallItems = nonZeroData.filter(
92+
(item) => (item.value / total) * 100 < minSlicePercentage
93+
)
94+
95+
// Group small items into "Other" if there are any
96+
const processedData = [...mainItems]
97+
if (smallItems.length > 0) {
98+
const otherValue = smallItems.reduce((sum, item) => sum + item.value, 0)
99+
processedData.push({ name: "Other", value: otherValue })
100+
}
101+
102+
return processedData
103+
}
104+
105+
export function PieChart({
106+
data,
107+
title,
108+
description,
109+
footerText,
110+
footerSubText,
111+
showPercentage = true,
112+
minSlicePercentage = 0,
113+
}: PieChartProps) {
114+
const processedData = processData(data, minSlicePercentage)
115+
116+
if (processedData.length === 0) {
117+
return (
118+
<Card className="w-full">
119+
{(title || description) && (
120+
<CardHeader className="!pt-0">
121+
{title && <CardTitle>{title}</CardTitle>}
122+
{description && <CardDescription>{description}</CardDescription>}
123+
</CardHeader>
124+
)}
125+
<CardContent className="flex h-64 items-center justify-center">
126+
<p className="text-muted-foreground">No data available</p>
127+
</CardContent>
128+
</Card>
129+
)
130+
}
131+
132+
// Calculate total for percentage display
133+
const total = processedData.reduce((sum, item) => sum + item.value, 0)
134+
135+
// Function to calculate optimal chart dimensions based on data size and screen
136+
const getChartDimensions = () => {
137+
const dataCount = processedData.length
138+
const baseHeight =
139+
dataCount <= 4 ? 320 : Math.min(380, 280 + dataCount * 15)
140+
141+
return {
142+
height: baseHeight,
143+
outerRadius: Math.max(50, Math.min(80, 400 / Math.max(6, dataCount))),
144+
cx: dataCount <= 3 ? "40%" : dataCount <= 5 ? "35%" : "30%",
145+
}
146+
}
147+
148+
const dimensions = getChartDimensions()
149+
150+
const legendFormatter: Formatter = (label: string, { payload }) => {
151+
const numeric = typeof payload?.value === "number" ? payload.value : 0
152+
const percentage = ((numeric / total) * 100).toFixed(1)
153+
154+
const isSmallScreen =
155+
typeof window !== "undefined" ? window.innerWidth < 640 : false
156+
const maxLength = isSmallScreen ? 10 : 15
157+
const displayName =
158+
label.length > maxLength ? `${label.substring(0, maxLength)}...` : label
159+
160+
return (
161+
<span className="text-xs sm:text-sm" title={label}>
162+
{displayName} {showPercentage && `(${percentage}%)`}
163+
</span>
164+
)
165+
}
166+
167+
// Custom tooltip content
168+
const customTooltipContent = ({
169+
active,
170+
payload,
171+
}: TooltipProps<number, string>) => {
172+
if (!active || !payload || !payload.length) return null
173+
174+
const [data] = payload
175+
176+
if (typeof data.value !== "number") return null
177+
178+
const percentage = ((data.value / total) * 100).toFixed(1)
179+
180+
return (
181+
<div className="rounded-lg border bg-background p-2 shadow-lg">
182+
<p className="font-medium">{data.name}</p>
183+
<p className="text-muted-foreground text-sm">
184+
{showPercentage ? `${percentage}%` : data.value}
185+
</p>
186+
</div>
187+
)
188+
}
189+
190+
return (
191+
<Card
192+
className="w-full"
193+
role="img"
194+
aria-label={title ? `${title} pie chart` : "Pie chart"}
195+
>
196+
<CardHeader className="!pt-0">
197+
{title && <CardTitle>{title}</CardTitle>}
198+
{description && <CardDescription>{description}</CardDescription>}
199+
</CardHeader>
200+
201+
<CardContent>
202+
<ChartContainer config={defaultChartConfig}>
203+
<ResponsiveContainer width="100%" height={dimensions.height}>
204+
<RechartsPieChart className="-me-12 ms-4">
205+
<ChartTooltip cursor={false} content={customTooltipContent} />
206+
207+
<Legend
208+
layout="vertical"
209+
verticalAlign="middle"
210+
align="right"
211+
className="max-w-1/2 break-all text-sm/snug"
212+
formatter={legendFormatter}
213+
/>
214+
215+
<Pie
216+
data={processedData}
217+
dataKey="value"
218+
nameKey="name"
219+
cx={dimensions.cx}
220+
cy="50%"
221+
outerRadius={dimensions.outerRadius}
222+
label={false}
223+
stroke="#ffffff"
224+
strokeWidth={1}
225+
>
226+
{processedData.map((_, i) => (
227+
<Cell key={`cell-${i}`} fill={generateColor(i)} />
228+
))}
229+
</Pie>
230+
</RechartsPieChart>
231+
</ResponsiveContainer>
232+
</ChartContainer>
233+
</CardContent>
234+
235+
{(footerText || footerSubText) && (
236+
<CardFooter>
237+
<div className="flex w-full items-start gap-2 text-sm">
238+
<div className="grid gap-2">
239+
{footerText && (
240+
<div className="flex items-center gap-2 font-medium leading-none">
241+
{footerText} <TrendingUp className="h-4 w-4" />
242+
</div>
243+
)}
244+
{footerSubText && (
245+
<div className="text-muted-foreground flex items-center gap-2 leading-none">
246+
{footerSubText}
247+
</div>
248+
)}
249+
</div>
250+
</div>
251+
</CardFooter>
252+
)}
253+
</Card>
254+
)
255+
}

0 commit comments

Comments
 (0)