diff --git a/readme.md b/readme.md
index 5f3c1254..d1e56775 100644
--- a/readme.md
+++ b/readme.md
@@ -431,6 +431,14 @@ You can use the `&layout=donut` option to change the card design.
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats)
```
+### Pie Chart Language Card Layout
+
+You can use the `&layout=pie` option to change the card design.
+
+```md
+[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/anuraghazra/github-readme-stats)
+```
+
### Hide Progress Bars
You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`).
@@ -451,6 +459,10 @@ You can use the `&hide_progress=true` option to hide the percentages and the pro
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats)
+- Pie Chart layout
+
+[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/anuraghazra/github-readme-stats)
+
- Hidden progress bars
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats)
diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js
index ed51f1c9..e03e8bcb 100644
--- a/src/cards/top-languages-card.js
+++ b/src/cards/top-languages-card.js
@@ -114,6 +114,16 @@ const calculateDonutLayoutHeight = (totalLangs) => {
return 215 + Math.max(totalLangs - 5, 0) * 32;
};
+/**
+ * Calculates height for the pie layout.
+ *
+ * @param {number} totalLangs Total number of languages.
+ * @returns {number} Card height.
+ */
+const calculatePieLayoutHeight = (totalLangs) => {
+ return 300 + Math.round(totalLangs / 2) * 25;
+};
+
/**
* Calculates the center translation needed to keep the donut chart centred.
* @param {number} totalLangs Total number of languages.
@@ -361,6 +371,101 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
`;
};
+/**
+ * Renders pie layout to display user's most frequently used programming languages.
+ *
+ * @param {Lang[]} langs Array of programming languages.
+ * @param {number} totalLanguageSize Total size of all languages.
+ * @returns {string} Compact layout card SVG object.
+ */
+const renderPieLayout = (langs, totalLanguageSize) => {
+ // Pie chart radius and center coordinates
+ const radius = 90;
+ const centerX = 150;
+ const centerY = 100;
+
+ // Start angle for the pie chart parts
+ let startAngle = 0;
+
+ // Start delay coefficient for the pie chart parts
+ let startDelayCoefficient = 1;
+
+ // SVG paths
+ const paths = [];
+
+ // Generate each pie chart part
+ for (const lang of langs) {
+ if (langs.length === 1) {
+ paths.push(`
+
+ `);
+ break;
+ }
+
+ const langSizePart = lang.size / totalLanguageSize;
+ const percentage = langSizePart * 100;
+ // Calculate the angle for the current part
+ const angle = langSizePart * 360;
+
+ // Calculate the end angle
+ const endAngle = startAngle + angle;
+
+ // Calculate the coordinates of the start and end points of the arc
+ const startPoint = polarToCartesian(centerX, centerY, radius, startAngle);
+ const endPoint = polarToCartesian(centerX, centerY, radius, endAngle);
+
+ // Determine the large arc flag based on the angle
+ const largeArcFlag = angle > 180 ? 1 : 0;
+
+ // Calculate delay
+ const delay = startDelayCoefficient * 100;
+
+ // SVG arc markup
+ paths.push(`
+
+
+
+ `);
+
+ // Update the start angle for the next part
+ startAngle = endAngle;
+ // Update the start delay coefficient for the next part
+ startDelayCoefficient += 1;
+ }
+
+ return `
+
+ `;
+};
+
/**
* Creates the SVG paths for the language donut chart.
*
@@ -505,7 +610,10 @@ const renderTopLanguages = (topLangs, options = {}) => {
let height = calculateNormalLayoutHeight(langs.length);
let finalLayout = "";
- if (layout === "compact" || hide_progress == true) {
+ if (layout === "pie") {
+ height = calculatePieLayoutHeight(langs.length);
+ finalLayout = renderPieLayout(langs, totalLanguageSize);
+ } else if (layout === "compact" || hide_progress == true) {
height =
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
@@ -580,6 +688,10 @@ const renderTopLanguages = (topLangs, options = {}) => {
`,
);
+ if (layout === "pie") {
+ return card.render(finalLayout);
+ }
+
return card.render(`
${finalLayout}
@@ -596,6 +708,7 @@ export {
calculateCompactLayoutHeight,
calculateNormalLayoutHeight,
calculateDonutLayoutHeight,
+ calculatePieLayoutHeight,
donutCenterTranslation,
trimTopLanguages,
renderTopLanguages,
diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts
index fea5aa95..7945118c 100644
--- a/src/cards/types.d.ts
+++ b/src/cards/types.d.ts
@@ -39,7 +39,7 @@ export type TopLangOptions = CommonOptions & {
hide_border: boolean;
card_width: number;
hide: string[];
- layout: "compact" | "normal" | "donut";
+ layout: "compact" | "normal" | "donut" | "pie";
custom_title: string;
langs_count: number;
disable_animations: boolean;
diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js
index e4f47c39..e4bc56de 100644
--- a/tests/renderTopLanguages.test.js
+++ b/tests/renderTopLanguages.test.js
@@ -9,6 +9,7 @@ import {
calculateCompactLayoutHeight,
calculateNormalLayoutHeight,
calculateDonutLayoutHeight,
+ calculatePieLayoutHeight,
donutCenterTranslation,
trimTopLanguages,
renderTopLanguages,
@@ -40,12 +41,13 @@ const langs = {
/**
* Retrieve the language percentage from the donut chart SVG.
+ *
* @param {string} d The SVG path element.
* @param {number} centerX The center X coordinate of the donut chart.
* @param {number} centerY The center Y coordinate of the donut chart.
* @returns {number} The percentage of the language.
*/
-const langPercentFromSvg = (d, centerX, centerY) => {
+const langPercentFromDonutLayoutSvg = (d, centerX, centerY) => {
const dTmp = d
.split(" ")
.filter((x) => !isNaN(x))
@@ -58,6 +60,34 @@ const langPercentFromSvg = (d, centerX, centerY) => {
return (endAngle - startAngle) / 3.6;
};
+/**
+ * Retrieve the language percentage from the pie chart SVG.
+ *
+ * @param {string} d The SVG path element.
+ * @param {number} centerX The center X coordinate of the pie chart.
+ * @param {number} centerY The center Y coordinate of the pie chart.
+ * @returns {number} The percentage of the language.
+ */
+const langPercentFromPieLayoutSvg = (d, centerX, centerY) => {
+ const dTmp = d
+ .split(" ")
+ .filter((x) => !isNaN(x))
+ .map((x) => parseFloat(x));
+ const startAngle = cartesianToPolar(
+ centerX,
+ centerY,
+ dTmp[2],
+ dTmp[3],
+ ).angleInDegrees;
+ let endAngle = cartesianToPolar(
+ centerX,
+ centerY,
+ dTmp[9],
+ dTmp[10],
+ ).angleInDegrees;
+ return ((endAngle - startAngle) / 360) * 100;
+};
+
describe("Test renderTopLanguages helper functions", () => {
it("getLongestLang", () => {
const langArray = Object.values(langs);
@@ -193,6 +223,20 @@ describe("Test renderTopLanguages helper functions", () => {
expect(calculateDonutLayoutHeight(10)).toBe(375);
});
+ it("calculatePieLayoutHeight", () => {
+ expect(calculatePieLayoutHeight(0)).toBe(300);
+ expect(calculatePieLayoutHeight(1)).toBe(325);
+ expect(calculatePieLayoutHeight(2)).toBe(325);
+ expect(calculatePieLayoutHeight(3)).toBe(350);
+ expect(calculatePieLayoutHeight(4)).toBe(350);
+ expect(calculatePieLayoutHeight(5)).toBe(375);
+ expect(calculatePieLayoutHeight(6)).toBe(375);
+ expect(calculatePieLayoutHeight(7)).toBe(400);
+ expect(calculatePieLayoutHeight(8)).toBe(400);
+ expect(calculatePieLayoutHeight(9)).toBe(425);
+ expect(calculatePieLayoutHeight(10)).toBe(425);
+ });
+
it("donutCenterTranslation", () => {
expect(donutCenterTranslation(0)).toBe(-45);
expect(donutCenterTranslation(1)).toBe(-45);
@@ -466,7 +510,7 @@ describe("Test renderTopLanguages", () => {
.filter((x) => !isNaN(x))
.map((x) => parseFloat(x));
const center = { x: d[7], y: d[7] };
- const HTMLLangPercent = langPercentFromSvg(
+ const HTMLLangPercent = langPercentFromDonutLayoutSvg(
queryAllByTestId(document.body, "lang-donut")[0].getAttribute("d"),
center.x,
center.y,
@@ -480,7 +524,7 @@ describe("Test renderTopLanguages", () => {
"size",
"40",
);
- const javascriptLangPercent = langPercentFromSvg(
+ const javascriptLangPercent = langPercentFromDonutLayoutSvg(
queryAllByTestId(document.body, "lang-donut")[1].getAttribute("d"),
center.x,
center.y,
@@ -494,7 +538,7 @@ describe("Test renderTopLanguages", () => {
"size",
"20",
);
- const cssLangPercent = langPercentFromSvg(
+ const cssLangPercent = langPercentFromDonutLayoutSvg(
queryAllByTestId(document.body, "lang-donut")[2].getAttribute("d"),
center.x,
center.y,
@@ -520,6 +564,81 @@ describe("Test renderTopLanguages", () => {
"circle",
);
});
+ it("should render with layout pie", () => {
+ document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" });
+
+ expect(queryByTestId(document.body, "header")).toHaveTextContent(
+ "Most Used Languages",
+ );
+
+ expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
+ "HTML 40.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute(
+ "size",
+ "40",
+ );
+
+ const d = queryAllByTestId(document.body, "lang-pie")[0]
+ .getAttribute("d")
+ .split(" ")
+ .filter((x) => !isNaN(x))
+ .map((x) => parseFloat(x));
+ const center = { x: d[0], y: d[1] };
+ const HTMLLangPercent = langPercentFromPieLayoutSvg(
+ queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"),
+ center.x,
+ center.y,
+ );
+ expect(HTMLLangPercent).toBeCloseTo(40);
+
+ expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
+ "javascript 40.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-pie")[1]).toHaveAttribute(
+ "size",
+ "40",
+ );
+ const javascriptLangPercent = langPercentFromPieLayoutSvg(
+ queryAllByTestId(document.body, "lang-pie")[1].getAttribute("d"),
+ center.x,
+ center.y,
+ );
+ expect(javascriptLangPercent).toBeCloseTo(40);
+
+ expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
+ "css 20.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-pie")[2]).toHaveAttribute(
+ "size",
+ "20",
+ );
+ const cssLangPercent = langPercentFromPieLayoutSvg(
+ queryAllByTestId(document.body, "lang-pie")[2].getAttribute("d"),
+ center.x,
+ center.y,
+ );
+ expect(cssLangPercent).toBeCloseTo(20);
+
+ expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);
+
+ // Should render full pie (circle) if one language is 100%.
+ document.body.innerHTML = renderTopLanguages(
+ { HTML: langs.HTML },
+ { layout: "pie" },
+ );
+ expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
+ "HTML 100.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute(
+ "size",
+ "100",
+ );
+ expect(queryAllByTestId(document.body, "lang-pie")).toHaveLength(1);
+ expect(queryAllByTestId(document.body, "lang-pie")[0].tagName).toBe(
+ "circle",
+ );
+ });
it("should render a translated title", () => {
document.body.innerHTML = renderTopLanguages(langs, { locale: "cn" });