forked from mirrored/github-readme-stats
feat: add bytes stats format option for top-languages card (#3708)
* feat: add `display_bytes` option to top-languages. * docs: add description about display bytes in top-languages * feat: add `stats_format` option instead of `display_bytes` * docs: add description about stats format in top-languages * refactor: rewrite with function to determine display value * docs: add `stats_format`to table of parameter * fix: remove unnecessary decimal part from format of bytes * tests: add tests of stats_format in top-langs card * Update tests/renderTopLanguagesCard.test.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update readme.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * prettier * jsdoc --------- Co-authored-by: Alexandr <qwerty541zxc@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
Alexandr
parent
485c2247f8
commit
078040d26e
+95
-21
@@ -9,6 +9,7 @@ import {
|
||||
getCardColors,
|
||||
lowercaseTrim,
|
||||
measureText,
|
||||
formatBytes,
|
||||
} from "../common/utils.js";
|
||||
import { langCardLocales } from "../translations.js";
|
||||
|
||||
@@ -196,6 +197,18 @@ const trimTopLanguages = (topLangs, langs_count, hide) => {
|
||||
return { langs, totalLanguageSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get display value corresponding to the format.
|
||||
*
|
||||
* @param {number} size Bytes size.
|
||||
* @param {number} percentages Percentage value.
|
||||
* @param {string} format Format of the stats.
|
||||
* @returns {string} Display value.
|
||||
*/
|
||||
const getDisplayValue = (size, percentages, format) => {
|
||||
return format === "bytes" ? formatBytes(size) : `${percentages.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create progress bar text item for a programming language.
|
||||
*
|
||||
@@ -203,20 +216,33 @@ const trimTopLanguages = (topLangs, langs_count, hide) => {
|
||||
* @param {number} props.width The card width
|
||||
* @param {string} props.color Color of the programming language.
|
||||
* @param {string} props.name Name of the programming language.
|
||||
* @param {number} props.progress Usage of the programming language in percentage.
|
||||
* @param {number} props.size Size of the programming language.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {string} props.statsFormat Stats format.
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Programming language SVG node.
|
||||
*/
|
||||
const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
||||
const createProgressTextNode = ({
|
||||
width,
|
||||
color,
|
||||
name,
|
||||
size,
|
||||
totalSize,
|
||||
statsFormat,
|
||||
index,
|
||||
}) => {
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
|
||||
const progress = (size / totalSize) * 100;
|
||||
const displayValue = getDisplayValue(size, progress, statsFormat);
|
||||
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${displayValue}</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
@@ -237,11 +263,20 @@ const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
||||
* @param {Lang} props.lang Programming language object.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean=} props.hideProgress Whether to hide percentage.
|
||||
* @param {string=} props.statsFormat Stats format
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Compact layout programming language SVG node.
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const createCompactLangNode = ({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat = "percentages",
|
||||
index,
|
||||
}) => {
|
||||
const percentages = (lang.size / totalSize) * 100;
|
||||
const displayValue = getDisplayValue(lang.size, percentages, statsFormat);
|
||||
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
@@ -249,7 +284,7 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${hideProgress ? "" : percentage + "%"}
|
||||
${lang.name} ${hideProgress ? "" : displayValue}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
@@ -262,9 +297,15 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
* @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.
|
||||
* @param {string=} props.statsFormat Stats format
|
||||
* @returns {string} Programming languages SVG node.
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
||||
const createLanguageTextNode = ({
|
||||
langs,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
}) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
@@ -274,6 +315,7 @@ const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
index,
|
||||
}),
|
||||
);
|
||||
@@ -299,15 +341,17 @@ const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
||||
* @param {object} props Function properties.
|
||||
* @param {Lang[]} props.langs Array of programming languages.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {string} props.statsFormat Stats format
|
||||
* @returns {string} Donut layout programming language SVG node.
|
||||
*/
|
||||
const createDonutLanguagesNode = ({ langs, totalSize }) => {
|
||||
const createDonutLanguagesNode = ({ langs, totalSize, statsFormat }) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang, index) => {
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
@@ -322,18 +366,19 @@ const createDonutLanguagesNode = ({ langs, totalSize }) => {
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Normal layout card SVG object.
|
||||
*/
|
||||
const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
const renderNormalLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang, index) => {
|
||||
return createProgressTextNode({
|
||||
width,
|
||||
name: lang.name,
|
||||
color: lang.color || DEFAULT_LANG_COLOR,
|
||||
progress: parseFloat(
|
||||
((lang.size / totalLanguageSize) * 100).toFixed(2),
|
||||
),
|
||||
size: lang.size,
|
||||
totalSize: totalLanguageSize,
|
||||
statsFormat,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
@@ -349,9 +394,16 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {boolean=} hideProgress Whether to hide progress bar.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
||||
const renderCompactLayout = (
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hideProgress,
|
||||
statsFormat = "percentages",
|
||||
) => {
|
||||
const paddingRight = 50;
|
||||
const offsetWidth = width - paddingRight;
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
@@ -397,6 +449,7 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
@@ -407,9 +460,10 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderDonutVerticalLayout = (langs, totalLanguageSize) => {
|
||||
const renderDonutVerticalLayout = (langs, totalLanguageSize, statsFormat) => {
|
||||
// Donut vertical chart radius and total length
|
||||
const radius = 80;
|
||||
const totalCircleLength = getCircleLength(radius);
|
||||
@@ -465,6 +519,7 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => {
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
})}
|
||||
</svg>
|
||||
</g>
|
||||
@@ -477,9 +532,10 @@ const renderDonutVerticalLayout = (langs, totalLanguageSize) => {
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderPieLayout = (langs, totalLanguageSize) => {
|
||||
const renderPieLayout = (langs, totalLanguageSize, statsFormat) => {
|
||||
// Pie chart radius and center coordinates
|
||||
const radius = 90;
|
||||
const centerX = 150;
|
||||
@@ -560,6 +616,7 @@ const renderPieLayout = (langs, totalLanguageSize) => {
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
})}
|
||||
</svg>
|
||||
</g>
|
||||
@@ -610,9 +667,10 @@ const createDonutPaths = (cx, cy, radius, percentages) => {
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Donut layout card SVG object.
|
||||
*/
|
||||
const renderDonutLayout = (langs, width, totalLanguageSize) => {
|
||||
const renderDonutLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
||||
const centerX = width / 3;
|
||||
const centerY = width / 3;
|
||||
const radius = centerX - 60;
|
||||
@@ -655,7 +713,7 @@ const renderDonutLayout = (langs, width, totalLanguageSize) => {
|
||||
return `
|
||||
<g transform="translate(0, 0)">
|
||||
<g transform="translate(0, 0)">
|
||||
${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize })}
|
||||
${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, statsFormat })}
|
||||
</g>
|
||||
|
||||
<g transform="translate(125, ${donutCenterTranslation(langs.length)})">
|
||||
@@ -738,6 +796,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
stats_format = "percentages",
|
||||
} = options;
|
||||
|
||||
const i18n = new I18n({
|
||||
@@ -779,10 +838,14 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
});
|
||||
} else if (layout === "pie") {
|
||||
height = calculatePieLayoutHeight(langs.length);
|
||||
finalLayout = renderPieLayout(langs, totalLanguageSize);
|
||||
finalLayout = renderPieLayout(langs, totalLanguageSize, stats_format);
|
||||
} else if (layout === "donut-vertical") {
|
||||
height = calculateDonutVerticalLayoutHeight(langs.length);
|
||||
finalLayout = renderDonutVerticalLayout(langs, totalLanguageSize);
|
||||
finalLayout = renderDonutVerticalLayout(
|
||||
langs,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
} else if (layout === "compact" || hide_progress == true) {
|
||||
height =
|
||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
||||
@@ -792,13 +855,24 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hide_progress,
|
||||
stats_format,
|
||||
);
|
||||
} else if (layout === "donut") {
|
||||
height = calculateDonutLayoutHeight(langs.length);
|
||||
width = width + 50; // padding
|
||||
finalLayout = renderDonutLayout(langs, width, totalLanguageSize);
|
||||
finalLayout = renderDonutLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
} else {
|
||||
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
||||
finalLayout = renderNormalLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
}
|
||||
|
||||
const card = new Card({
|
||||
|
||||
Vendored
+1
@@ -44,6 +44,7 @@ export type TopLangOptions = CommonOptions & {
|
||||
langs_count: number;
|
||||
disable_animations: boolean;
|
||||
hide_progress: boolean;
|
||||
stats_format: "percentages" | "bytes";
|
||||
};
|
||||
|
||||
export type WakaTimeOptions = CommonOptions & {
|
||||
|
||||
@@ -598,6 +598,33 @@ const dateDiff = (d1, d2) => {
|
||||
return Math.round(diff / (1000 * 60));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert bytes to a human-readable string representation.
|
||||
*
|
||||
* @param {number} bytes The number of bytes to convert.
|
||||
* @returns {string} The human-readable representation of bytes.
|
||||
* @throws {Error} If bytes is negative or too large.
|
||||
*/
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes < 0) {
|
||||
throw new Error("Bytes must be a non-negative number");
|
||||
}
|
||||
|
||||
if (bytes === 0) {
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
const base = 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(base));
|
||||
|
||||
if (i >= sizes.length) {
|
||||
throw new Error("Bytes is too large to convert to a human-readable string");
|
||||
}
|
||||
|
||||
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export {
|
||||
ERROR_CARD_LENGTH,
|
||||
renderError,
|
||||
@@ -624,4 +651,5 @@ export {
|
||||
chunkArray,
|
||||
parseEmojis,
|
||||
dateDiff,
|
||||
formatBytes,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user