diff --git a/public/content/developers/docs/nodes-and-clients/client-diversity/index.md b/public/content/developers/docs/nodes-and-clients/client-diversity/index.md index b5d6daa0e10..0a6c84a7534 100644 --- a/public/content/developers/docs/nodes-and-clients/client-diversity/index.md +++ b/public/content/developers/docs/nodes-and-clients/client-diversity/index.md @@ -41,12 +41,37 @@ 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. +### Execution Clients {#execution-clients-breakdown} + + + +### Consensus Clients {#consensus-clients-breakdown} + + + +This diagram may be outdated — go to [ethernodes.org](https://ethernodes.org) and [clientdiversity.org](https://clientdiversity.org) for up-to-date information. + +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 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. Up to date client diversity data for the consensus layer is now available at [clientdiversity.org](https://clientdiversity.org/). diff --git a/src/components/MdComponents/index.tsx b/src/components/MdComponents/index.tsx index f59fd908f34..bfb7dbf4586 100644 --- a/src/components/MdComponents/index.tsx +++ b/src/components/MdComponents/index.tsx @@ -17,6 +17,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" @@ -177,6 +178,7 @@ export const reactComponents = { FeaturedText, GlossaryTooltip, Page, + PieChart, QuizWidget: StandaloneQuizWidget, IssuesList, Tag, diff --git a/src/components/PieChart/index.tsx b/src/components/PieChart/index.tsx new file mode 100644 index 00000000000..7b993bddd6c --- /dev/null +++ b/src/components/PieChart/index.tsx @@ -0,0 +1,255 @@ +"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 ( + + {(title || description) && ( + + {title && {title}} + {description && {description}} + + )} + +

No data available

+
+
+ ) + } + + // 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 ( + + {displayName} {showPercentage && `(${percentage}%)`} + + ) + } + + // Custom tooltip content + const customTooltipContent = ({ + active, + payload, + }: TooltipProps) => { + 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 ( +
+

{data.name}

+

+ {showPercentage ? `${percentage}%` : data.value} +

+
+ ) + } + + return ( + + + {title && {title}} + {description && {description}} + + + + + + + + + + + + {processedData.map((_, i) => ( + + ))} + + + + + + + {(footerText || footerSubText) && ( + +
+
+ {footerText && ( +
+ {footerText} +
+ )} + {footerSubText && ( +
+ {footerSubText} +
+ )} +
+
+
+ )} +
+ ) +}