Skip to content

Commit f1c67a5

Browse files
committed
feat: improve pie chart positioning
1 parent 2ffb07f commit f1c67a5

File tree

2 files changed

+85
-18
lines changed

2 files changed

+85
-18
lines changed

src/cards/top-languages-card.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,25 @@ const calculateNormalLayoutHeight = (totalLangs) => {
104104
return 45 + (totalLangs + 1) * 40;
105105
};
106106

107+
/**
108+
* Calculates height for the pie layout.
109+
*
110+
* @param {number} totalLangs Total number of languages.
111+
* @returns {number} Card height.
112+
*/
113+
const calculatePieLayoutHeight = (totalLangs) => {
114+
return 215 + Math.max(totalLangs - 5, 0) * 32;
115+
};
116+
117+
/**
118+
* Calculates the center translation needed to keep the doughnut chart centred.
119+
* @param {number} totalLangs Total number of languages.
120+
* @returns {number} Doughnut center translation.
121+
*/
122+
const doughnutCenterTranslation = (totalLangs) => {
123+
return -45 + Math.max(totalLangs - 5, 0) * 16;
124+
};
125+
107126
/**
108127
* Trim top languages to lang_count while also hiding certain languages.
109128
*
@@ -334,7 +353,6 @@ const createDoughnutPaths = (cx, cy, radius, percentages) => {
334353
const paths = [];
335354
let startAngle = 0;
336355
let endAngle = 0;
337-
let doughnutPercent = 0;
338356

339357
const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0);
340358
for (let i = 0; i < percentages.length; i++) {
@@ -343,7 +361,6 @@ const createDoughnutPaths = (cx, cy, radius, percentages) => {
343361
let percent = parseFloat(
344362
((percentages[i] / totalPercent) * 100).toFixed(2),
345363
);
346-
doughnutPercent += percent;
347364

348365
endAngle = 3.6 * percent + startAngle;
349366
const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate doughnut 90 degrees counter-clockwise.
@@ -386,9 +403,12 @@ const renderDoughnutLayout = (langs, width, totalLanguageSize) => {
386403
langsPercents,
387404
);
388405

389-
const doughnutPaths = langPaths
390-
.map((section, i) => {
391-
const output = `
406+
const doughnutPaths =
407+
langs.length === 1
408+
? `<circle cx="${centerX}" cy="${centerY}" r="${radius}" stroke="${colors[0]}" fill="none" stroke-width="${strokeWidth}" data-testid="lang-doughnut" size="100"/>`
409+
: langPaths
410+
.map((section, i) => {
411+
const output = `
392412
<g>
393413
<path
394414
data-testid="lang-doughnut"
@@ -401,9 +421,9 @@ const renderDoughnutLayout = (langs, width, totalLanguageSize) => {
401421
</g>
402422
`;
403423

404-
return output;
405-
})
406-
.join("");
424+
return output;
425+
})
426+
.join("");
407427

408428
const donut = `<svg width="${width}" height="${width}">${doughnutPaths}</svg>`;
409429

@@ -413,7 +433,7 @@ const renderDoughnutLayout = (langs, width, totalLanguageSize) => {
413433
${createDoughnutLanguagesNode({ langs, totalSize: totalLanguageSize })}
414434
</g>
415435
416-
<g transform="translate(125, -45)">
436+
<g transform="translate(125, ${doughnutCenterTranslation(langs.length)})">
417437
${donut}
418438
</g>
419439
</g>
@@ -469,7 +489,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
469489
height = calculateCompactLayoutHeight(langs.length);
470490
finalLayout = renderCompactLayout(langs, width, totalLanguageSize);
471491
} else if (layout?.toLowerCase() === "pie") {
472-
height = height - 60; // padding
492+
height = calculatePieLayoutHeight(langs.length);
473493
width = width + 50; // padding
474494
finalLayout = renderDoughnutLayout(langs, width, totalLanguageSize);
475495
} else {
@@ -516,6 +536,8 @@ export {
516536
cartesianToPolar,
517537
calculateCompactLayoutHeight,
518538
calculateNormalLayoutHeight,
539+
calculatePieLayoutHeight,
540+
doughnutCenterTranslation,
519541
trimTopLanguages,
520542
renderTopLanguages,
521543
MIN_CARD_WIDTH,

tests/renderTopLanguages.test.js

+53-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { queryAllByTestId, queryByTestId } from "@testing-library/dom";
22
import { cssToObject } from "@uppercod/css-to-object";
3-
import {
4-
MIN_CARD_WIDTH,
5-
radiansToDegrees,
6-
renderTopLanguages,
7-
calculateCompactLayoutHeight,
8-
calculateNormalLayoutHeight,
9-
trimTopLanguages,
10-
} from "../src/cards/top-languages-card.js";
113
import {
124
getLongestLang,
135
degreesToRadians,
6+
radiansToDegrees,
147
polarToCartesian,
158
cartesianToPolar,
9+
calculateCompactLayoutHeight,
10+
calculateNormalLayoutHeight,
11+
calculatePieLayoutHeight,
12+
doughnutCenterTranslation,
13+
trimTopLanguages,
14+
renderTopLanguages,
15+
MIN_CARD_WIDTH,
1616
} from "../src/cards/top-languages-card.js";
1717

1818
// adds special assertions like toHaveTextContent
@@ -179,6 +179,34 @@ describe("Test renderTopLanguages helper functions", () => {
179179
expect(calculateNormalLayoutHeight(10)).toBe(485);
180180
});
181181

182+
it("calculatePieLayoutHeight", () => {
183+
expect(calculatePieLayoutHeight(0)).toBe(215);
184+
expect(calculatePieLayoutHeight(1)).toBe(215);
185+
expect(calculatePieLayoutHeight(2)).toBe(215);
186+
expect(calculatePieLayoutHeight(3)).toBe(215);
187+
expect(calculatePieLayoutHeight(4)).toBe(215);
188+
expect(calculatePieLayoutHeight(5)).toBe(215);
189+
expect(calculatePieLayoutHeight(6)).toBe(247);
190+
expect(calculatePieLayoutHeight(7)).toBe(279);
191+
expect(calculatePieLayoutHeight(8)).toBe(311);
192+
expect(calculatePieLayoutHeight(9)).toBe(343);
193+
expect(calculatePieLayoutHeight(10)).toBe(375);
194+
});
195+
196+
it("doughnutCenterTranslation", () => {
197+
expect(doughnutCenterTranslation(0)).toBe(-45);
198+
expect(doughnutCenterTranslation(1)).toBe(-45);
199+
expect(doughnutCenterTranslation(2)).toBe(-45);
200+
expect(doughnutCenterTranslation(3)).toBe(-45);
201+
expect(doughnutCenterTranslation(4)).toBe(-45);
202+
expect(doughnutCenterTranslation(5)).toBe(-45);
203+
expect(doughnutCenterTranslation(6)).toBe(-29);
204+
expect(doughnutCenterTranslation(7)).toBe(-13);
205+
expect(doughnutCenterTranslation(8)).toBe(3);
206+
expect(doughnutCenterTranslation(9)).toBe(19);
207+
expect(doughnutCenterTranslation(10)).toBe(35);
208+
});
209+
182210
it("trimTopLanguages", () => {
183211
expect(trimTopLanguages([])).toStrictEqual({
184212
langs: [],
@@ -474,6 +502,23 @@ describe("Test renderTopLanguages", () => {
474502
expect(cssLangPercent).toBeCloseTo(20);
475503

476504
expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);
505+
506+
// Should render full doughnut (circle) if one language is 100%.
507+
document.body.innerHTML = renderTopLanguages(
508+
{ HTML: langs.HTML },
509+
{ layout: "pie" },
510+
);
511+
expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
512+
"HTML 100.00%",
513+
);
514+
expect(queryAllByTestId(document.body, "lang-doughnut")[0]).toHaveAttribute(
515+
"size",
516+
"100",
517+
);
518+
expect(queryAllByTestId(document.body, "lang-doughnut")).toHaveLength(1);
519+
expect(queryAllByTestId(document.body, "lang-doughnut")[0].tagName).toBe(
520+
"circle",
521+
);
477522
});
478523

479524
it("should render a translated title", () => {

0 commit comments

Comments
 (0)