feat: implement number precision parameter for stats card (#4514)

* Update short number format 

Update short number format by adding 2 decimal places instead of 1 for numbers between 1,000 and 9,999

* review

* Update src/common/utils.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* dev

* dev

---------

Co-authored-by: Alexandr <qwerty541zxc@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lucas
2025-10-13 20:21:28 +03:00
committed by GitHub
co-authored by Copilot Alexandr
parent 7caaa21e73
commit 4143c6dec3
7 changed files with 85 additions and 11 deletions
+2
View File
@@ -43,6 +43,7 @@ export default async (req, res) => {
disable_animations,
border_radius,
number_format,
number_precision,
border_color,
rank_icon,
show,
@@ -124,6 +125,7 @@ export default async (req, res) => {
border_radius,
border_color,
number_format,
number_precision: parseInt(number_precision, 10),
locale: locale ? locale.toLowerCase() : null,
disable_animations: parseBoolean(disable_animations),
rank_icon,
+1
View File
@@ -385,6 +385,7 @@ If we don't support your language, please consider contributing! You can find mo
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
| `ring_color` | Color of the rank circle. | string (hex color) | `2f80ed` |
| `number_format` | Switches between two available formats for displaying the card values `short` (i.e. `6.6k`) and `long` (i.e. `6626`). | enum | `short` |
| `number_precision` | Enforce the number of digits after the decimal point for `short` number format. Must be an integer between 0 and 2. Will be ignored for `long` number format. | integer (0, 1 or 2) | `null` |
| `show` | Shows [additional items](#showing-additional-individual-stats) on stats card (i.e. `reviews`, `discussions_started`, `discussions_answered`, `prs_merged` or `prs_merged_percentage`). | string (comma-separated values) | `null` |
| `commits_year` | Filters and counts only commits made in the specified year. | integer _(YYYY)_ | `<current year> (one year to date)` |
+19 -5
View File
@@ -33,7 +33,8 @@ const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
* @param {boolean} params.showIcons Whether to show icons.
* @param {number} params.shiftValuePos Number of pixels the value has to be shifted to the right.
* @param {boolean} params.bold Whether to bold the label.
* @param {string} params.number_format The format of numbers on card.
* @param {string} params.numberFormat The format of numbers on card.
* @param {number=} params.numberPrecision The precision of numbers on card.
* @returns {string} The stats card text item SVG object.
*/
const createTextNode = ({
@@ -46,10 +47,17 @@ const createTextNode = ({
showIcons,
shiftValuePos,
bold,
number_format,
numberFormat,
numberPrecision,
}) => {
const precision =
typeof numberPrecision === "number" && !isNaN(numberPrecision)
? clampValue(numberPrecision, 0, 2)
: undefined;
const kValue =
number_format.toLowerCase() === "long" ? value : kFormatter(value);
numberFormat.toLowerCase() === "long" || id === "prs_merged_percentage"
? value
: kFormatter(value, precision);
const staggerDelay = (index + 3) * 150;
const labelOffset = showIcons ? `x="25"` : "";
@@ -251,6 +259,7 @@ const renderStatsCard = (stats, options = {}) => {
border_radius,
border_color,
number_format = "short",
number_precision,
locale,
disable_animations = false,
rank_icon = "default",
@@ -319,7 +328,11 @@ const renderStatsCard = (stats, options = {}) => {
STATS.prs_merged_percentage = {
icon: icons.prs_merged_percentage,
label: i18n.t("statcard.prs-merged-percentage"),
value: mergedPRsPercentage.toFixed(2),
value: mergedPRsPercentage.toFixed(
typeof number_precision === "number" && !isNaN(number_precision)
? clampValue(number_precision, 0, 2)
: 2,
),
id: "prs_merged_percentage",
unitSymbol: "%",
};
@@ -408,7 +421,8 @@ const renderStatsCard = (stats, options = {}) => {
showIcons: show_icons,
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
bold: text_bold,
number_format,
numberFormat: number_format,
numberPrecision: number_precision,
});
});
+1
View File
@@ -25,6 +25,7 @@ export type StatCardOptions = CommonOptions & {
custom_title: string;
disable_animations: boolean;
number_format: string;
number_precision: number;
ring_color: string;
text_bold: boolean;
rank_icon: RankIcon;
+15 -5
View File
@@ -77,15 +77,25 @@ const iconWithLabel = (icon, label, testid, iconSize) => {
};
/**
* Retrieves num with suffix k(thousands) precise to 1 decimal if greater than 999.
* Retrieves num with suffix k(thousands) precise to given decimal places.
*
* @param {number} num The number to format.
* @param {number=} precision The number of decimal places to include.
* @returns {string|number} The formatted number.
*/
const kFormatter = (num) => {
return Math.abs(num) > 999
? Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(1)) + "k"
: Math.sign(num) * Math.abs(num);
const kFormatter = (num, precision) => {
const abs = Math.abs(num);
const sign = Math.sign(num);
if (typeof precision === "number" && !isNaN(precision)) {
return (sign * (abs / 1000)).toFixed(precision) + "k";
}
if (abs < 1000) {
return sign * abs;
}
return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
};
/**
+7
View File
@@ -416,6 +416,13 @@ describe("Test renderStatsCard", () => {
expect(getByTestId(document.body, "commits").textContent).toBe("2k");
document.body.innerHTML = renderStatsCard(stats, { number_format: "long" });
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
document.body.innerHTML = renderStatsCard(stats, { number_precision: 2 });
expect(getByTestId(document.body, "commits").textContent).toBe("2.00k");
document.body.innerHTML = renderStatsCard(stats, {
number_format: "long",
number_precision: 2,
});
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
});
it("should render default rank icon with level A+", () => {
+40 -1
View File
@@ -12,16 +12,55 @@ import {
import { getCardColors } from "../src/common/color.js";
describe("Test utils.js", () => {
it("should test kFormatter", () => {
it("should test kFormatter default behavior", () => {
expect(kFormatter(1)).toBe(1);
expect(kFormatter(-1)).toBe(-1);
expect(kFormatter(500)).toBe(500);
expect(kFormatter(1000)).toBe("1k");
expect(kFormatter(1200)).toBe("1.2k");
expect(kFormatter(10000)).toBe("10k");
expect(kFormatter(12345)).toBe("12.3k");
expect(kFormatter(99900)).toBe("99.9k");
expect(kFormatter(9900000)).toBe("9900k");
});
it("should test kFormatter with 0 decimal precision", () => {
expect(kFormatter(1, 0)).toBe("0k");
expect(kFormatter(-1, 0)).toBe("-0k");
expect(kFormatter(500, 0)).toBe("1k");
expect(kFormatter(1000, 0)).toBe("1k");
expect(kFormatter(1200, 0)).toBe("1k");
expect(kFormatter(10000, 0)).toBe("10k");
expect(kFormatter(12345, 0)).toBe("12k");
expect(kFormatter(99000, 0)).toBe("99k");
expect(kFormatter(99900, 0)).toBe("100k");
expect(kFormatter(9900000, 0)).toBe("9900k");
});
it("should test kFormatter with 1 decimal precision", () => {
expect(kFormatter(1, 1)).toBe("0.0k");
expect(kFormatter(-1, 1)).toBe("-0.0k");
expect(kFormatter(500, 1)).toBe("0.5k");
expect(kFormatter(1000, 1)).toBe("1.0k");
expect(kFormatter(1200, 1)).toBe("1.2k");
expect(kFormatter(10000, 1)).toBe("10.0k");
expect(kFormatter(12345, 1)).toBe("12.3k");
expect(kFormatter(99900, 1)).toBe("99.9k");
expect(kFormatter(9900000, 1)).toBe("9900.0k");
});
it("should test kFormatter with 2 decimal precision", () => {
expect(kFormatter(1, 2)).toBe("0.00k");
expect(kFormatter(-1, 2)).toBe("-0.00k");
expect(kFormatter(500, 2)).toBe("0.50k");
expect(kFormatter(1000, 2)).toBe("1.00k");
expect(kFormatter(1200, 2)).toBe("1.20k");
expect(kFormatter(10000, 2)).toBe("10.00k");
expect(kFormatter(12345, 2)).toBe("12.35k");
expect(kFormatter(99900, 2)).toBe("99.90k");
expect(kFormatter(9900000, 2)).toBe("9900.00k");
});
it("should test parseBoolean", () => {
expect(parseBoolean(true)).toBe(true);
expect(parseBoolean(false)).toBe(false);