Repository: anuraghazra/github-readme-stats
Pull request #2709 (2023-05-09 05:46:40)

Top languages card pie layout
![image](https://github-readme-stats-lhjkv63gq-qwerty541.vercel.app/api/top-langs?username=qwerty541&layout=pie)

Preview deployed there: https://github-readme-stats-lhjkv63gq-qwerty541.vercel.app

Option: `https://github-readme-stats-lhjkv63gq-qwerty541.vercel.app/api/top-langs?username=qwerty541&layout=pie`

TODOS:

- [x] rebase master
- [x] docs
- [x] tests
- [x] animations

vercel[bot]: @qwerty541 is attempting to deploy a commit to the **github readme stats** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=github%20readme%20stats&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22dd7973d552ac0d4228e4cc58c2a49407f32519fc%22%7D%2C%22id%22%3A%22QmUdS14rnPZDxkSy33TepCFna1Pv5oCQ1TUvTt1daPhwUu%22%2C%22org%22%3A%22anuraghazra%22%2C%22prId%22%3A2709%2C%22repo%22%3A%22github-readme-stats%22%7D).


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/2709?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) Report
Patch coverage: **`26.74`**% and project coverage change: **`-1.39`** :warning:
> Comparison is base [(`8ff5a3b`)](https://app.codecov.io/gh/anuraghazra/github-readme-stats/commit/8ff5a3ba45cb493f61444d2963e969b2b54d91e4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) 97.35% compared to head [(`d9d9a84`)](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/2709?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) 95.97%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2709      +/-   ##
==========================================
- Coverage   97.35%   95.97%   -1.39%     
==========================================
  Files          24       24              
  Lines        4311     4396      +85     
  Branches      393      395       +2     
==========================================
+ Hits         4197     4219      +22     
- Misses        112      175      +63     
  Partials        2        2              
```


| [Impacted Files](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/2709?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) | Coverage Δ | |
|---|---|---|
| [src/cards/top-languages-card.js](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/2709?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra#diff-c3JjL2NhcmRzL3RvcC1sYW5ndWFnZXMtY2FyZC5qcw==) | `86.92% <26.74%> (-13.08%)` | :arrow_down: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/2709?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra).


Patch:
diff --git a/readme.md b/readme.md
index 5f3c125..d1e5677 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 5523c93..e03e8bc 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.
@@ -268,6 +278,7 @@ const createDonutLanguagesNode = ({ langs, totalSize }) => {
       return createCompactLangNode({
         lang,
         totalSize,
+        hideProgress: false,
         index,
       });
     }),
@@ -360,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(`
+        <circle
+          cx="${centerX}"
+          cy="${centerY}"
+          r="${radius}"
+          stroke="none"
+          fill="${lang.color}"
+          data-testid="lang-pie"
+          size="100"
+        />
+      `);
+      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(`
+      <g class="stagger" style="animation-delay: ${delay}ms">
+        <path
+          data-testid="lang-pie"
+          size="${percentage}"
+          d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
+          fill="${lang.color}"
+        />
+      </g>
+    `);
+
+    // Update the start angle for the next part
+    startAngle = endAngle;
+    // Update the start delay coefficient for the next part
+    startDelayCoefficient += 1;
+  }
+
+  return `
+    <svg data-testid="lang-items">
+      <g transform="translate(0, 0)">
+        <svg data-testid="pie">
+          ${paths.join("")}
+        </svg>
+      </g>
+      <g transform="translate(0, 220)">
+        <svg data-testid="lang-names" x="${CARD_PADDING}">
+          ${createLanguageTextNode({
+            langs,
+            totalSize: totalLanguageSize,
+            hideProgress: false,
+          })}
+        </svg>
+      </g>
+    </svg>
+  `;
+};
+
 /**
  * Creates the SVG paths for the language donut chart.
  *
@@ -504,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);
 
@@ -579,6 +688,10 @@ const renderTopLanguages = (topLangs, options = {}) => {
     `,
   );
 
+  if (layout === "pie") {
+    return card.render(finalLayout);
+  }
+
   return card.render(`
     <svg data-testid="lang-items" x="${CARD_PADDING}">
       ${finalLayout}
@@ -595,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 fea5aa9..7945118 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;


Test patch:
diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js
index e4f47c3..e4bc56d 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" });