Skip to content

Commit 504ffec

Browse files
qwerty541devantler
authored andcommitted
Top languages card donut vertical layout (anuraghazra#2701)
* Top languages card donut layout * dev * dev * dev * dev
1 parent e7d8399 commit 504ffec

File tree

4 files changed

+250
-3
lines changed

4 files changed

+250
-3
lines changed

readme.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ You can provide multiple comma-separated values in the bg_color option to render
303303

304304
- `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`.
305305
- `hide_title` - _(boolean)_. Default: `false`.
306-
- `layout` - Switch between four available layouts `normal` & `compact` & `donut` & `pie`. Default: `normal`.
306+
- `layout` - Switch between four available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`.
307307
- `card_width` - Set the card's width manually _(number)_. Default `300`.
308308
- `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`.
309309
- `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.
431431
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats)
432432
```
433433

434+
### Donut Vertical Chart Language Card Layout
435+
436+
You can use the `&layout=donut-vertical` option to change the card design.
437+
438+
```md
439+
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/anuraghazra/github-readme-stats)
440+
```
441+
434442
### Pie Chart Language Card Layout
435443

436444
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
459467

460468
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats)
461469

470+
- Donut Vertical Chart layout
471+
472+
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/anuraghazra/github-readme-stats)
473+
462474
- Pie Chart layout
463475

464476
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/anuraghazra/github-readme-stats)

src/cards/top-languages-card.js

+96-1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ const cartesianToPolar = (centerX, centerY, x, y) => {
8484
return { radius, angleInDegrees };
8585
};
8686

87+
/**
88+
* Calculates length of circle.
89+
*
90+
* @param {number} radius Radius of the circle.
91+
* @returns {number} The length of the circle.
92+
*/
93+
const getCircleLength = (radius) => {
94+
return 2 * Math.PI * radius;
95+
};
96+
8797
/**
8898
* Calculates height for the compact layout.
8999
*
@@ -114,6 +124,16 @@ const calculateDonutLayoutHeight = (totalLangs) => {
114124
return 215 + Math.max(totalLangs - 5, 0) * 32;
115125
};
116126

127+
/**
128+
* Calculates height for the donut vertical layout.
129+
*
130+
* @param {number} totalLangs Total number of languages.
131+
* @returns {number} Card height.
132+
*/
133+
const calculateDonutVerticalLayoutHeight = (totalLangs) => {
134+
return 300 + Math.round(totalLangs / 2) * 25;
135+
};
136+
117137
/**
118138
* Calculates height for the pie layout.
119139
*
@@ -371,6 +391,76 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
371391
`;
372392
};
373393

394+
/**
395+
* Renders donut vertical layout to display user's most frequently used programming languages.
396+
*
397+
* @param {Lang[]} langs Array of programming languages.
398+
* @param {number} totalLanguageSize Total size of all languages.
399+
* @returns {string} Compact layout card SVG object.
400+
*/
401+
const renderDonutVerticalLayout = (langs, totalLanguageSize) => {
402+
// Donut vertical chart radius and total length
403+
const radius = 80;
404+
const totalCircleLength = getCircleLength(radius);
405+
406+
// SVG circles
407+
let circles = [];
408+
409+
// Start indent for donut vertical chart parts
410+
let indent = 0;
411+
412+
// Start delay coefficient for donut vertical chart parts
413+
let startDelayCoefficient = 1;
414+
415+
// Generate each donut vertical chart part
416+
for (const lang of langs) {
417+
const percentage = (lang.size / totalLanguageSize) * 100;
418+
const circleLength = totalCircleLength * (percentage / 100);
419+
const delay = startDelayCoefficient * 100;
420+
421+
circles.push(`
422+
<g class="stagger" style="animation-delay: ${delay}ms">
423+
<circle
424+
cx="150"
425+
cy="100"
426+
r="${radius}"
427+
fill="transparent"
428+
stroke="${lang.color}"
429+
stroke-width="25"
430+
stroke-dasharray="${totalCircleLength}"
431+
stroke-dashoffset="${indent}"
432+
size="${percentage}"
433+
data-testid="lang-donut"
434+
/>
435+
</g>
436+
`);
437+
438+
// Update the indent for the next part
439+
indent += circleLength;
440+
// Update the start delay coefficient for the next part
441+
startDelayCoefficient += 1;
442+
}
443+
444+
return `
445+
<svg data-testid="lang-items">
446+
<g transform="translate(0, 0)">
447+
<svg data-testid="donut">
448+
${circles.join("")}
449+
</svg>
450+
</g>
451+
<g transform="translate(0, 220)">
452+
<svg data-testid="lang-names" x="${CARD_PADDING}">
453+
${createLanguageTextNode({
454+
langs,
455+
totalSize: totalLanguageSize,
456+
hideProgress: false,
457+
})}
458+
</svg>
459+
</g>
460+
</svg>
461+
`;
462+
};
463+
374464
/**
375465
* Renders pie layout to display user's most frequently used programming languages.
376466
*
@@ -613,6 +703,9 @@ const renderTopLanguages = (topLangs, options = {}) => {
613703
if (layout === "pie") {
614704
height = calculatePieLayoutHeight(langs.length);
615705
finalLayout = renderPieLayout(langs, totalLanguageSize);
706+
} else if (layout === "donut-vertical") {
707+
height = calculateDonutVerticalLayoutHeight(langs.length);
708+
finalLayout = renderDonutVerticalLayout(langs, totalLanguageSize);
616709
} else if (layout === "compact" || hide_progress == true) {
617710
height =
618711
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
@@ -688,7 +781,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
688781
`,
689782
);
690783

691-
if (layout === "pie") {
784+
if (layout === "pie" || layout === "donut-vertical") {
692785
return card.render(finalLayout);
693786
}
694787

@@ -705,9 +798,11 @@ export {
705798
radiansToDegrees,
706799
polarToCartesian,
707800
cartesianToPolar,
801+
getCircleLength,
708802
calculateCompactLayoutHeight,
709803
calculateNormalLayoutHeight,
710804
calculateDonutLayoutHeight,
805+
calculateDonutVerticalLayoutHeight,
711806
calculatePieLayoutHeight,
712807
donutCenterTranslation,
713808
trimTopLanguages,

src/cards/types.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export type TopLangOptions = CommonOptions & {
3939
hide_border: boolean;
4040
card_width: number;
4141
hide: string[];
42-
layout: "compact" | "normal" | "donut" | "pie";
42+
layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie";
4343
custom_title: string;
4444
langs_count: number;
4545
disable_animations: boolean;

tests/renderTopLanguages.test.js

+140
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
radiansToDegrees,
77
polarToCartesian,
88
cartesianToPolar,
9+
getCircleLength,
910
calculateCompactLayoutHeight,
1011
calculateNormalLayoutHeight,
1112
calculateDonutLayoutHeight,
13+
calculateDonutVerticalLayoutHeight,
1214
calculatePieLayoutHeight,
1315
donutCenterTranslation,
1416
trimTopLanguages,
@@ -70,6 +72,20 @@ const langPercentFromDonutLayoutSvg = (d, centerX, centerY) => {
7072
return (endAngle - startAngle) / 3.6;
7173
};
7274

75+
/**
76+
* Calculate language percentage for donut vertical chart SVG.
77+
*
78+
* @param {number} partLength Length of current chart part..
79+
* @param {number} totalCircleLength Total length of circle.
80+
* @return {number} Chart part percentage.
81+
*/
82+
const langPercentFromDonutVerticalLayoutSvg = (
83+
partLength,
84+
totalCircleLength,
85+
) => {
86+
return (partLength / totalCircleLength) * 100;
87+
};
88+
7389
/**
7490
* Retrieve the language percentage from the pie chart SVG.
7591
*
@@ -230,6 +246,20 @@ describe("Test renderTopLanguages helper functions", () => {
230246
expect(calculateDonutLayoutHeight(10)).toBe(375);
231247
});
232248

249+
it("calculateDonutVerticalLayoutHeight", () => {
250+
expect(calculateDonutVerticalLayoutHeight(0)).toBe(300);
251+
expect(calculateDonutVerticalLayoutHeight(1)).toBe(325);
252+
expect(calculateDonutVerticalLayoutHeight(2)).toBe(325);
253+
expect(calculateDonutVerticalLayoutHeight(3)).toBe(350);
254+
expect(calculateDonutVerticalLayoutHeight(4)).toBe(350);
255+
expect(calculateDonutVerticalLayoutHeight(5)).toBe(375);
256+
expect(calculateDonutVerticalLayoutHeight(6)).toBe(375);
257+
expect(calculateDonutVerticalLayoutHeight(7)).toBe(400);
258+
expect(calculateDonutVerticalLayoutHeight(8)).toBe(400);
259+
expect(calculateDonutVerticalLayoutHeight(9)).toBe(425);
260+
expect(calculateDonutVerticalLayoutHeight(10)).toBe(425);
261+
});
262+
233263
it("calculatePieLayoutHeight", () => {
234264
expect(calculatePieLayoutHeight(0)).toBe(300);
235265
expect(calculatePieLayoutHeight(1)).toBe(325);
@@ -258,6 +288,18 @@ describe("Test renderTopLanguages helper functions", () => {
258288
expect(donutCenterTranslation(10)).toBe(35);
259289
});
260290

291+
it("getCircleLength", () => {
292+
expect(getCircleLength(20)).toBeCloseTo(125.663);
293+
expect(getCircleLength(30)).toBeCloseTo(188.495);
294+
expect(getCircleLength(40)).toBeCloseTo(251.327);
295+
expect(getCircleLength(50)).toBeCloseTo(314.159);
296+
expect(getCircleLength(60)).toBeCloseTo(376.991);
297+
expect(getCircleLength(70)).toBeCloseTo(439.822);
298+
expect(getCircleLength(80)).toBeCloseTo(502.654);
299+
expect(getCircleLength(90)).toBeCloseTo(565.486);
300+
expect(getCircleLength(100)).toBeCloseTo(628.318);
301+
});
302+
261303
it("trimTopLanguages", () => {
262304
expect(trimTopLanguages([])).toStrictEqual({
263305
langs: [],
@@ -569,6 +611,104 @@ describe("Test renderTopLanguages", () => {
569611
"circle",
570612
);
571613
});
614+
615+
it("should render with layout donut vertical", () => {
616+
document.body.innerHTML = renderTopLanguages(langs, {
617+
layout: "donut-vertical",
618+
});
619+
620+
expect(queryByTestId(document.body, "header")).toHaveTextContent(
621+
"Most Used Languages",
622+
);
623+
624+
expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
625+
"HTML 40.00%",
626+
);
627+
expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
628+
"size",
629+
"40",
630+
);
631+
632+
const totalCircleLength = queryAllByTestId(
633+
document.body,
634+
"lang-donut",
635+
)[0].getAttribute("stroke-dasharray");
636+
637+
const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
638+
queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
639+
"stroke-dashoffset",
640+
) -
641+
queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
642+
"stroke-dashoffset",
643+
),
644+
totalCircleLength,
645+
);
646+
expect(HTMLLangPercent).toBeCloseTo(40);
647+
648+
expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
649+
"javascript 40.00%",
650+
);
651+
expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute(
652+
"size",
653+
"40",
654+
);
655+
const javascriptLangPercent = langPercentFromDonutVerticalLayoutSvg(
656+
queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
657+
"stroke-dashoffset",
658+
) -
659+
queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
660+
"stroke-dashoffset",
661+
),
662+
totalCircleLength,
663+
);
664+
expect(javascriptLangPercent).toBeCloseTo(40);
665+
666+
expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
667+
"css 20.00%",
668+
);
669+
expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute(
670+
"size",
671+
"20",
672+
);
673+
const cssLangPercent = langPercentFromDonutVerticalLayoutSvg(
674+
totalCircleLength -
675+
queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
676+
"stroke-dashoffset",
677+
),
678+
totalCircleLength,
679+
);
680+
expect(cssLangPercent).toBeCloseTo(20);
681+
682+
expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);
683+
});
684+
685+
it("should render with layout donut vertical full donut circle of one language is 100%", () => {
686+
document.body.innerHTML = renderTopLanguages(
687+
{ HTML: langs.HTML },
688+
{ layout: "donut-vertical" },
689+
);
690+
expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
691+
"HTML 100.00%",
692+
);
693+
expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
694+
"size",
695+
"100",
696+
);
697+
const totalCircleLength = queryAllByTestId(
698+
document.body,
699+
"lang-donut",
700+
)[0].getAttribute("stroke-dasharray");
701+
702+
const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
703+
totalCircleLength -
704+
queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
705+
"stroke-dashoffset",
706+
),
707+
totalCircleLength,
708+
);
709+
expect(HTMLLangPercent).toBeCloseTo(100);
710+
});
711+
572712
it("should render with layout pie", () => {
573713
document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" });
574714

0 commit comments

Comments
 (0)