From 18444a800ae0219ce4834ea08b8cf71786caee1c Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sun, 7 May 2023 10:12:03 +0300 Subject: [PATCH 1/5] Top languages card donut layout --- src/cards/top-languages-card.js | 75 ++++++++++++++++++++++++++++++++- src/cards/types.d.ts | 2 +- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index ce8e12a839c77..c1b695b163ca5 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -98,7 +98,7 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => { /** * Creates compact layout of text only language nodes. * - * @param {object[]} props Function properties. + * @param {object} props Function properties. * @param {Lang[]} props.langs Array of programming languages. * @param {number} props.totalSize Total size of all languages. * @param {boolean} props.hideProgress Whether to hide percentage. @@ -217,6 +217,70 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { `; }; +/** + * Renders donut 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 renderDonutLayout = (langs, totalLanguageSize) => { + const radius = 80; + const circleLength = 2 * Math.PI * radius; + + let circles = []; + let indent = 0; + + for (const lang of langs) { + const currentTotalLanguageSizePart = (lang.size / totalLanguageSize) * 100; + const currentCircleLengthPart = + circleLength * (currentTotalLanguageSizePart / 100); + circles.push(` + + `); + indent += currentCircleLengthPart; + } + + const donutSvg = ` + + + ${circles.join("")} + + `; + const languagesSvg = ` + + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + hideProgress: false, + })} + + + `; + + return `${donutSvg}${languagesSvg}`; +}; + +/** + * Calculates height for the donut layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculateDonutLayoutHeight = (totalLangs) => { + return 300 + Math.round(totalLangs / 2) * 25; +}; + /** * Calculates height for the compact layout. * @@ -316,7 +380,10 @@ const renderTopLanguages = (topLangs, options = {}) => { let height = calculateNormalLayoutHeight(langs.length); let finalLayout = ""; - if (layout === "compact" || hide_progress == true) { + if (layout === "donut") { + height = calculateDonutLayoutHeight(langs.length); + finalLayout = renderDonutLayout(langs, totalLanguageSize); + } else if (layout === "compact" || hide_progress == true) { height = calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0); @@ -387,6 +454,10 @@ const renderTopLanguages = (topLangs, options = {}) => { `, ); + if (layout === "donut") { + return card.render(finalLayout); + } + return card.render(` ${finalLayout} diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 02a41b5769387..fea5aa954222c 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"; + layout: "compact" | "normal" | "donut"; custom_title: string; langs_count: number; disable_animations: boolean; From e086493512515a4ef9f581c7c65a34873571b7d6 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Wed, 17 May 2023 09:43:47 +0300 Subject: [PATCH 2/5] dev --- src/cards/top-languages-card.js | 75 ++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index 8afe391662464..37759cc78856e 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 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. * @@ -384,55 +394,50 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { let circles = []; let indent = 0; + let startDelayCoefficient = 1; for (const lang of langs) { const currentTotalLanguageSizePart = (lang.size / totalLanguageSize) * 100; const currentCircleLengthPart = circleLength * (currentTotalLanguageSizePart / 100); + const delay = startDelayCoefficient * 100; circles.push(` - + + + `); indent += currentCircleLengthPart; + startDelayCoefficient += 1; } - const donutSvg = ` - - - ${circles.join("")} - - `; - const languagesSvg = ` - + return ` + + + + + ${circles.join("")} + + - ${createLanguageTextNode({ - langs, - totalSize: totalLanguageSize, - hideProgress: false, - })} + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + hideProgress: false, + })} + `; - - return `${donutSvg}${languagesSvg}`; -}; - -/** - * 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; }; /** From d44323f180827a8b9bb6d100fef1f273ce9cecdd Mon Sep 17 00:00:00 2001 From: Alexandr Date: Wed, 17 May 2023 09:58:49 +0300 Subject: [PATCH 3/5] dev --- readme.md | 14 +++++++++++++- src/cards/top-languages-card.js | 5 +++++ tests/renderTopLanguages.test.js | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) 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 37759cc78856e..d04702178bfbf 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -401,6 +401,7 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { const currentCircleLengthPart = circleLength * (currentTotalLanguageSizePart / 100); const delay = startDelayCoefficient * 100; + circles.push(` { /> `); + + // Update the indent for the next part indent += currentCircleLengthPart; + // Update the start delay coefficient for the next part startDelayCoefficient += 1; } @@ -780,6 +784,7 @@ export { calculateCompactLayoutHeight, calculateNormalLayoutHeight, calculateDonutLayoutHeight, + calculateDonutVerticalLayoutHeight, calculatePieLayoutHeight, donutCenterTranslation, trimTopLanguages, diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js index a8bc873a79ef1..eb7cae0e04953 100644 --- a/tests/renderTopLanguages.test.js +++ b/tests/renderTopLanguages.test.js @@ -9,6 +9,7 @@ import { calculateCompactLayoutHeight, calculateNormalLayoutHeight, calculateDonutLayoutHeight, + calculateDonutVerticalLayoutHeight, calculatePieLayoutHeight, donutCenterTranslation, trimTopLanguages, @@ -230,6 +231,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); From d14d0cd239c07b8fdd58b612671ba3a1b225df47 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Wed, 17 May 2023 10:15:24 +0300 Subject: [PATCH 4/5] dev --- src/cards/top-languages-card.js | 23 ++++++---- tests/renderTopLanguages.test.js | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index d04702178bfbf..7669c97e2e28b 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -389,17 +389,23 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { * @returns {string} Compact layout card SVG object. */ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { + // Donut vertical chart radius and circle length const radius = 80; - const circleLength = 2 * Math.PI * radius; + const totalCircleLength = 2 * Math.PI * 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 currentTotalLanguageSizePart = (lang.size / totalLanguageSize) * 100; - const currentCircleLengthPart = - circleLength * (currentTotalLanguageSizePart / 100); + const percentage = (lang.size / totalLanguageSize) * 100; + const circleLength = totalCircleLength * (percentage / 100); const delay = startDelayCoefficient * 100; circles.push(` @@ -411,14 +417,16 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { fill="transparent" stroke="${lang.color}" stroke-width="25" - stroke-dasharray="${circleLength}" + stroke-dasharray="${totalCircleLength}" stroke-dashoffset="${indent}" + size="${percentage}" + data-testid="lang-donut" /> `); // Update the indent for the next part - indent += currentCircleLengthPart; + indent += circleLength; // Update the start delay coefficient for the next part startDelayCoefficient += 1; } @@ -426,8 +434,7 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { return ` - - + ${circles.join("")} diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js index eb7cae0e04953..ff747f0e2f45b 100644 --- a/tests/renderTopLanguages.test.js +++ b/tests/renderTopLanguages.test.js @@ -584,6 +584,81 @@ 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 d = getNumbersFromSvgPathDefinitionAttribute( + // queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"), + // ); + // 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-donut")[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-donut")[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 with layout pie", () => { document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" }); From 7cde69ea949c125b1ab40f8e4054128bc7d6b12b Mon Sep 17 00:00:00 2001 From: Alexandr Date: Thu, 18 May 2023 03:30:37 +0300 Subject: [PATCH 5/5] dev --- src/cards/top-languages-card.js | 15 +++- tests/renderTopLanguages.test.js | 118 ++++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 36 deletions(-) diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index 7669c97e2e28b..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. * @@ -389,9 +399,9 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { * @returns {string} Compact layout card SVG object. */ const renderDonutVerticalLayout = (langs, totalLanguageSize) => { - // Donut vertical chart radius and circle length + // Donut vertical chart radius and total length const radius = 80; - const totalCircleLength = 2 * Math.PI * radius; + const totalCircleLength = getCircleLength(radius); // SVG circles let circles = []; @@ -788,6 +798,7 @@ export { radiansToDegrees, polarToCartesian, cartesianToPolar, + getCircleLength, calculateCompactLayoutHeight, calculateNormalLayoutHeight, calculateDonutLayoutHeight, diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js index ff747f0e2f45b..ed3bd3d76973c 100644 --- a/tests/renderTopLanguages.test.js +++ b/tests/renderTopLanguages.test.js @@ -6,6 +6,7 @@ import { radiansToDegrees, polarToCartesian, cartesianToPolar, + getCircleLength, calculateCompactLayoutHeight, calculateNormalLayoutHeight, calculateDonutLayoutHeight, @@ -71,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. * @@ -273,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: [], @@ -586,7 +613,9 @@ describe("Test renderTopLanguages", () => { }); it("should render with layout donut vertical", () => { - document.body.innerHTML = renderTopLanguages(langs, { layout: "donut-vertical" }); + document.body.innerHTML = renderTopLanguages(langs, { + layout: "donut-vertical", + }); expect(queryByTestId(document.body, "header")).toHaveTextContent( "Most Used Languages", @@ -600,16 +629,21 @@ describe("Test renderTopLanguages", () => { "40", ); - // const d = getNumbersFromSvgPathDefinitionAttribute( - // queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"), - // ); - // 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); + 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%", @@ -618,12 +652,16 @@ describe("Test renderTopLanguages", () => { "size", "40", ); - // const javascriptLangPercent = langPercentFromPieLayoutSvg( - // queryAllByTestId(document.body, "lang-pie")[1].getAttribute("d"), - // center.x, - // center.y, - // ); - // expect(javascriptLangPercent).toBeCloseTo(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%", @@ -632,31 +670,43 @@ describe("Test renderTopLanguages", () => { "size", "20", ); - // const cssLangPercent = langPercentFromPieLayoutSvg( - // queryAllByTestId(document.body, "lang-pie")[2].getAttribute("d"), - // center.x, - // center.y, - // ); - // expect(cssLangPercent).toBeCloseTo(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); + expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100); + }); - // Should render full pie (circle) if one language is 100%. + it("should render with layout donut vertical full donut circle of one language is 100%", () => { document.body.innerHTML = renderTopLanguages( { HTML: langs.HTML }, - { layout: "pie" }, + { layout: "donut-vertical" }, ); 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", - // ); + 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", () => {