diff --git a/readme.md b/readme.md
index f59342f3ab9f4..0de7caa4756d5 100644
--- a/readme.md
+++ b/readme.md
@@ -303,7 +303,7 @@ You can provide multiple comma-separated values in the bg_color option to render
- `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`.
- `hide_title` - _(boolean)_. Default: `false`.
-- `layout` - Switch between four available layouts `normal` & `compact` & `donut` & `pie`. Default: `normal`.
+- `layout` - Switch between four available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`.
- `card_width` - Set the card's width manually _(number)_. Default `300`.
- `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`.
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
@@ -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)
```
+### Donut Vertical Chart Language Card Layout
+
+You can use the `&layout=donut-vertical` option to change the card design.
+
+```md
+[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/anuraghazra/github-readme-stats)
+```
+
### Pie Chart Language Card Layout
You can use the `&layout=pie` option to change the card design.
@@ -459,6 +467,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)
+- Donut Vertical Chart layout
+
+[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](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)
diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js
index e03e8bcb00a35..c6cedb1fb077c 100644
--- a/src/cards/top-languages-card.js
+++ b/src/cards/top-languages-card.js
@@ -84,6 +84,16 @@ const cartesianToPolar = (centerX, centerY, x, y) => {
return { radius, angleInDegrees };
};
+/**
+ * Calculates length of circle.
+ *
+ * @param {number} radius Radius of the circle.
+ * @returns {number} The length of the circle.
+ */
+const getCircleLength = (radius) => {
+ return 2 * Math.PI * radius;
+};
+
/**
* Calculates height for the compact layout.
*
@@ -114,6 +124,16 @@ const calculateDonutLayoutHeight = (totalLangs) => {
return 215 + Math.max(totalLangs - 5, 0) * 32;
};
+/**
+ * Calculates height for the donut vertical layout.
+ *
+ * @param {number} totalLangs Total number of languages.
+ * @returns {number} Card height.
+ */
+const calculateDonutVerticalLayoutHeight = (totalLangs) => {
+ return 300 + Math.round(totalLangs / 2) * 25;
+};
+
/**
* Calculates height for the pie layout.
*
@@ -371,6 +391,76 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
`;
};
+/**
+ * Renders donut vertical 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 renderDonutVerticalLayout = (langs, totalLanguageSize) => {
+ // Donut vertical chart radius and total length
+ const radius = 80;
+ const totalCircleLength = getCircleLength(radius);
+
+ // SVG circles
+ let circles = [];
+
+ // Start indent for donut vertical chart parts
+ let indent = 0;
+
+ // Start delay coefficient for donut vertical chart parts
+ let startDelayCoefficient = 1;
+
+ // Generate each donut vertical chart part
+ for (const lang of langs) {
+ const percentage = (lang.size / totalLanguageSize) * 100;
+ const circleLength = totalCircleLength * (percentage / 100);
+ const delay = startDelayCoefficient * 100;
+
+ circles.push(`
+
+
+
+ `);
+
+ // Update the indent for the next part
+ indent += circleLength;
+ // Update the start delay coefficient for the next part
+ startDelayCoefficient += 1;
+ }
+
+ return `
+
+ `;
+};
+
/**
* Renders pie layout to display user's most frequently used programming languages.
*
@@ -613,6 +703,9 @@ const renderTopLanguages = (topLangs, options = {}) => {
if (layout === "pie") {
height = calculatePieLayoutHeight(langs.length);
finalLayout = renderPieLayout(langs, totalLanguageSize);
+ } else if (layout === "donut-vertical") {
+ height = calculateDonutVerticalLayoutHeight(langs.length);
+ finalLayout = renderDonutVerticalLayout(langs, totalLanguageSize);
} else if (layout === "compact" || hide_progress == true) {
height =
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
@@ -688,7 +781,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
`,
);
- if (layout === "pie") {
+ if (layout === "pie" || layout === "donut-vertical") {
return card.render(finalLayout);
}
@@ -705,9 +798,11 @@ export {
radiansToDegrees,
polarToCartesian,
cartesianToPolar,
+ getCircleLength,
calculateCompactLayoutHeight,
calculateNormalLayoutHeight,
calculateDonutLayoutHeight,
+ calculateDonutVerticalLayoutHeight,
calculatePieLayoutHeight,
donutCenterTranslation,
trimTopLanguages,
diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts
index 7945118cbe384..d6a1de05d176f 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" | "pie";
+ layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie";
custom_title: string;
langs_count: number;
disable_animations: boolean;
diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js
index a8bc873a79ef1..ed3bd3d76973c 100644
--- a/tests/renderTopLanguages.test.js
+++ b/tests/renderTopLanguages.test.js
@@ -6,9 +6,11 @@ import {
radiansToDegrees,
polarToCartesian,
cartesianToPolar,
+ getCircleLength,
calculateCompactLayoutHeight,
calculateNormalLayoutHeight,
calculateDonutLayoutHeight,
+ calculateDonutVerticalLayoutHeight,
calculatePieLayoutHeight,
donutCenterTranslation,
trimTopLanguages,
@@ -70,6 +72,20 @@ const langPercentFromDonutLayoutSvg = (d, centerX, centerY) => {
return (endAngle - startAngle) / 3.6;
};
+/**
+ * Calculate language percentage for donut vertical chart SVG.
+ *
+ * @param {number} partLength Length of current chart part..
+ * @param {number} totalCircleLength Total length of circle.
+ * @return {number} Chart part percentage.
+ */
+const langPercentFromDonutVerticalLayoutSvg = (
+ partLength,
+ totalCircleLength,
+) => {
+ return (partLength / totalCircleLength) * 100;
+};
+
/**
* Retrieve the language percentage from the pie chart SVG.
*
@@ -230,6 +246,20 @@ describe("Test renderTopLanguages helper functions", () => {
expect(calculateDonutLayoutHeight(10)).toBe(375);
});
+ it("calculateDonutVerticalLayoutHeight", () => {
+ expect(calculateDonutVerticalLayoutHeight(0)).toBe(300);
+ expect(calculateDonutVerticalLayoutHeight(1)).toBe(325);
+ expect(calculateDonutVerticalLayoutHeight(2)).toBe(325);
+ expect(calculateDonutVerticalLayoutHeight(3)).toBe(350);
+ expect(calculateDonutVerticalLayoutHeight(4)).toBe(350);
+ expect(calculateDonutVerticalLayoutHeight(5)).toBe(375);
+ expect(calculateDonutVerticalLayoutHeight(6)).toBe(375);
+ expect(calculateDonutVerticalLayoutHeight(7)).toBe(400);
+ expect(calculateDonutVerticalLayoutHeight(8)).toBe(400);
+ expect(calculateDonutVerticalLayoutHeight(9)).toBe(425);
+ expect(calculateDonutVerticalLayoutHeight(10)).toBe(425);
+ });
+
it("calculatePieLayoutHeight", () => {
expect(calculatePieLayoutHeight(0)).toBe(300);
expect(calculatePieLayoutHeight(1)).toBe(325);
@@ -258,6 +288,18 @@ describe("Test renderTopLanguages helper functions", () => {
expect(donutCenterTranslation(10)).toBe(35);
});
+ it("getCircleLength", () => {
+ expect(getCircleLength(20)).toBeCloseTo(125.663);
+ expect(getCircleLength(30)).toBeCloseTo(188.495);
+ expect(getCircleLength(40)).toBeCloseTo(251.327);
+ expect(getCircleLength(50)).toBeCloseTo(314.159);
+ expect(getCircleLength(60)).toBeCloseTo(376.991);
+ expect(getCircleLength(70)).toBeCloseTo(439.822);
+ expect(getCircleLength(80)).toBeCloseTo(502.654);
+ expect(getCircleLength(90)).toBeCloseTo(565.486);
+ expect(getCircleLength(100)).toBeCloseTo(628.318);
+ });
+
it("trimTopLanguages", () => {
expect(trimTopLanguages([])).toStrictEqual({
langs: [],
@@ -569,6 +611,104 @@ describe("Test renderTopLanguages", () => {
"circle",
);
});
+
+ it("should render with layout donut vertical", () => {
+ document.body.innerHTML = renderTopLanguages(langs, {
+ layout: "donut-vertical",
+ });
+
+ 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-donut")[0]).toHaveAttribute(
+ "size",
+ "40",
+ );
+
+ const totalCircleLength = queryAllByTestId(
+ document.body,
+ "lang-donut",
+ )[0].getAttribute("stroke-dasharray");
+
+ const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
+ queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
+ "stroke-dashoffset",
+ ) -
+ queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
+ "stroke-dashoffset",
+ ),
+ totalCircleLength,
+ );
+ expect(HTMLLangPercent).toBeCloseTo(40);
+
+ expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
+ "javascript 40.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute(
+ "size",
+ "40",
+ );
+ const javascriptLangPercent = langPercentFromDonutVerticalLayoutSvg(
+ queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
+ "stroke-dashoffset",
+ ) -
+ queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
+ "stroke-dashoffset",
+ ),
+ totalCircleLength,
+ );
+ expect(javascriptLangPercent).toBeCloseTo(40);
+
+ expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
+ "css 20.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute(
+ "size",
+ "20",
+ );
+ const cssLangPercent = langPercentFromDonutVerticalLayoutSvg(
+ totalCircleLength -
+ queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
+ "stroke-dashoffset",
+ ),
+ totalCircleLength,
+ );
+ expect(cssLangPercent).toBeCloseTo(20);
+
+ expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);
+ });
+
+ it("should render with layout donut vertical full donut circle of one language is 100%", () => {
+ document.body.innerHTML = renderTopLanguages(
+ { HTML: langs.HTML },
+ { layout: "donut-vertical" },
+ );
+ expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
+ "HTML 100.00%",
+ );
+ expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
+ "size",
+ "100",
+ );
+ const totalCircleLength = queryAllByTestId(
+ document.body,
+ "lang-donut",
+ )[0].getAttribute("stroke-dasharray");
+
+ const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
+ totalCircleLength -
+ queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
+ "stroke-dashoffset",
+ ),
+ totalCircleLength,
+ );
+ expect(HTMLLangPercent).toBeCloseTo(100);
+ });
+
it("should render with layout pie", () => {
document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" });