diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index dd48c13bb21..00000000000
--- a/.prettierrc
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "endOfLine": "lf",
- "semi": false,
- "singleQuote": false,
- "tabWidth": 2,
- "trailingComma": "es5",
- "plugins": [
- "prettier-plugin-tailwindcss"
- ]
-}
\ No newline at end of file
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
new file mode 100644
index 00000000000..63cd39590e2
Binary files /dev/null and b/.yarn/install-state.gz differ
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 00000000000..3186f3f0795
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1 @@
+nodeLinker: node-modules
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..bdc76aee8e8 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
@@ -39,20 +39,42 @@ Although these are unlikely scenarios, the Ethereum eco-system can mitigate thei
There is also a human cost to having majority clients. It puts excess strain and responsibility on a small development team. The lesser the client diversity, the greater the burden of responsibility for the developers maintaining the majority client. Spreading this responsibility across multiple teams is good for both the health of Ethereum's network of nodes and its network of people.
-## Current client diversity {#current-client-diversity}
-
-
-_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.
+### Current client diversity {#current-client-diversity}
+
+
( export const HR = () => (
-) +); // All base html element components export const htmlElements = { @@ -132,7 +133,7 @@ export const htmlElements = { time: LocaleDateTime, ul: UnorderedList, ...mdxTableComponents, -} +}; /** * Custom React components @@ -144,27 +145,28 @@ export const Page = ({-) +); export const Title = (props: ChildOnlyProp) => ( -) +); export const ContentContainer = (props: ComponentProps<"article">) => { return ( - ) -} + ); +}; // All custom React components export const reactComponents = { BrowseApps, ButtonLink, Card, + ClientDiversityChart, ContentContainer, Contributors, ContributorsQuizBanner, @@ -176,13 +178,14 @@ export const reactComponents = { GlossaryTooltip, InfoBanner, Page, + PieChart, QuizWidget: StandaloneQuizWidget, IssuesList, Tag, Title, WhatAreAppsStories, YouTube, -} +}; /** * All base markdown components as default export @@ -190,6 +193,6 @@ export const reactComponents = { const MdComponents = { ...htmlElements, ...reactComponents, -} +}; -export default MdComponents +export default MdComponents; diff --git a/src/components/PieChart/PieChart.tsx b/src/components/PieChart/PieChart.tsx new file mode 100644 index 00000000000..43dbcc0a526 --- /dev/null +++ b/src/components/PieChart/PieChart.tsx @@ -0,0 +1,253 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; +import { FaArrowTrendUp } from "react-icons/fa6"; +import { + Cell, + Legend, + Pie, + PieChart as RechartsPieChart, + ResponsiveContainer, +} from "recharts"; + +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 ( + + + ); + } + + // 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 = (value: string, legendEntry: any) => { + const payload = legendEntry.payload as unknown as PieChartDataPoint; + const percentage = ((payload.value / total) * 100).toFixed(1); + + // Dynamic label truncation based on screen size + const maxLength = window.innerWidth < 640 ? 10 : 15; + const displayName = + value.length > maxLength ? `${value.substring(0, maxLength)}...` : value; + + return ( + + {displayName} {showPercentage && `(${percentage}%)`} + + ); + }; + + // Custom tooltip content + const customTooltipContent = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const data = payload[0]; + const percentage = ((data.value / total) * 100).toFixed(1); + return ( ++ {title && +{title} } + {description &&{description} } ++ +No data available
+++ ); + } + return null; + }; + + return ( +{data.name}
++ {showPercentage ? `${percentage}%` : data.value} +
++ + ); +}+ {title && + +{title} } + {description &&{description} } ++ + + {(footerText || footerSubText) && ( ++ ++ ++ ++ + + + 1 ? 2 : 0} + label={false} + stroke="#fff" + strokeWidth={1} + > + {processedData.map((_, i) => ( + ++ ))} + | + + )} ++++ {footerText && ( +++ {footerText}+ )} + {footerSubText && ( ++ + {footerSubText} ++ )} +