diff --git a/readme.md b/readme.md index f59342f3ab9f45..0de7caa4756d5c 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 e03e8bcb00a35e..c6cedb1fb077c1 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 ` + + + + ${circles.join("")} + + + + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + hideProgress: false, + })} + + + + `; +}; + /** * 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 7945118cbe3848..d6a1de05d176f6 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 a8bc873a79ef1a..ed3bd3d76973ca 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" });