Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New top language algorithm implementation #1732

Merged
4 changes: 4 additions & 0 deletions api/top-langs.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default async (req, res) => {
layout,
langs_count,
exclude_repo,
size_weight,
count_weight,
custom_title,
locale,
border_radius,
Expand All @@ -46,6 +48,8 @@ export default async (req, res) => {
const topLangs = await fetchTopLanguages(
username,
parseArray(exclude_repo),
size_weight,
count_weight,
);

const cacheSeconds = clampValue(
Expand Down
42 changes: 31 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-sp
##### Use GitHub's new media feature

You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.

```html
<picture>
<source
Expand Down Expand Up @@ -309,6 +309,8 @@ You can provide multiple comma-separated values in the bg_color option to render
- `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`.
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
- `hide_progress` - It uses the compact layout option, hides percentages, and removes the bars. Default: `false`.
- `size_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 1.
- `count_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 0.

> **Warning**
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
Expand Down Expand Up @@ -358,7 +360,25 @@ Use [show_owner](#customization) variable to include the repo's owner username
The top languages card shows a GitHub user's most frequently used top language.

> **Note**
> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.
> Top Languages does not indicate the user's skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.

### Language stats algorithm

We use the following algorithm to calculate the languages percentages on the language card:

```js
ranking_index = (byte_count ^ size_weight) * (repo_count ^ count_weight)
```

By default, only the byte count is used for determining the languages percentages shown on the language card (i.e. `size_weight=1` and `count_weight=0`). You can, however, use the `&size_weight=` and `&count_weight=` options to weight the language usage calculation. The values must be positive real numbers. [More details about the algorithm can be found here](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305).

- `&size_weight=1&count_weight=0` - _(default)_ Orders by byte count.
- `&size_weight=0.5&count_weight=0.5` - _(recommended)_ Uses both byte and repo count for ranking
- `&size_weight=0&count_weight=1` - Orders by repo count

```md
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&size_weight=0.5&count_weight=0.5)](https://github.com/anuraghazra/github-readme-stats)
```

### Usage

Expand Down Expand Up @@ -418,7 +438,7 @@ 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=compact)](https://github.com/anuraghazra/github-readme-stats)

- Hidden progress bars
- Hidden progress bars

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

Expand Down Expand Up @@ -559,14 +579,14 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
<details>
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>

1. Fork or clone this repo as per your needs
2. Add `express` to the dependencies section of `package.json`
https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61
3. Run `npm i` if needed (initial setup)
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11
5. You're done 🎉
</details>
1. Fork or clone this repo as per your needs
2. Add `express` to the dependencies section of `package.json`
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61>
3. Run `npm i` if needed (initial setup)
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11>
5. You're done 🎉
</details>

### Keep your fork up to date

Expand Down
24 changes: 22 additions & 2 deletions src/fetchers/top-languages-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ const fetcher = (variables, token) => {
* @param {string[]} exclude_repo List of repositories to exclude.
* @returns {Promise<import("./types").TopLangData>} Top languages data.
*/
const fetchTopLanguages = async (username, exclude_repo = []) => {
const fetchTopLanguages = async (
username,
exclude_repo = [],
size_weight = 1,
count_weight = 0,
) => {
if (!username) throw new MissingParamError(["username"]);

const res = await retryer(fetcher, { login: username });
Expand Down Expand Up @@ -101,6 +106,8 @@ const fetchTopLanguages = async (username, exclude_repo = []) => {
.sort((a, b) => b.size - a.size)
.filter((name) => !repoToHide[name.name]);

let repoCount = 0;

repoNodes = repoNodes
.filter((node) => node.languages.edges.length > 0)
// flatten the list of language nodes
Expand All @@ -111,20 +118,33 @@ const fetchTopLanguages = async (username, exclude_repo = []) => {

// if we already have the language in the accumulator
// & the current language name is same as previous name
// add the size to the language size.
// add the size to the language size and increase repoCount.
if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
langSize = prev.size + acc[prev.node.name].size;
repoCount += 1;
} else {
// reset repoCount to 1
// language must exist in at least one repo to be detected
repoCount = 1;
}
return {
...acc,
[prev.node.name]: {
name: prev.node.name,
color: prev.node.color,
size: langSize,
count: repoCount,
},
};
}, {});

Object.keys(repoNodes).forEach((name) => {
// comparison index calculation
repoNodes[name].size =
Math.pow(repoNodes[name].size, size_weight) *
Math.pow(repoNodes[name].count, count_weight);
});

const topLangs = Object.keys(repoNodes)
.sort((a, b) => repoNodes[b].size - repoNodes[a].size)
.reduce((result, key) => {
Expand Down
52 changes: 48 additions & 4 deletions tests/fetchTopLanguages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,22 @@ const error = {
};

describe("FetchTopLanguages", () => {
it("should fetch correct language data", async () => {
it("should fetch correct language data while using the new calculation", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);

let repo = await fetchTopLanguages("anuraghazra");
let repo = await fetchTopLanguages("anuraghazra", [], 0.5, 0.5);
expect(repo).toStrictEqual({
HTML: {
color: "#0f0",
count: 2,
name: "HTML",
size: 200,
size: 20.000000000000004,
},
javascript: {
color: "#0ff",
count: 2,
name: "javascript",
size: 200,
size: 20.000000000000004,
},
});
});
Expand All @@ -85,17 +87,59 @@ describe("FetchTopLanguages", () => {
expect(repo).toStrictEqual({
HTML: {
color: "#0f0",
count: 1,
name: "HTML",
size: 100,
},
javascript: {
color: "#0ff",
count: 2,
name: "javascript",
size: 200,
},
});
});

it("should fetch correct language data while using the old calculation", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);

let repo = await fetchTopLanguages("anuraghazra", [], 1, 0);
expect(repo).toStrictEqual({
HTML: {
color: "#0f0",
count: 2,
name: "HTML",
size: 200,
},
javascript: {
color: "#0ff",
count: 2,
name: "javascript",
size: 200,
},
});
});

it("should rank languages by the number of repositories they appear in", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);

let repo = await fetchTopLanguages("anuraghazra", [], 0, 1);
expect(repo).toStrictEqual({
HTML: {
color: "#0f0",
count: 2,
name: "HTML",
size: 2,
},
javascript: {
color: "#0ff",
count: 2,
name: "javascript",
size: 2,
},
});
});

it("should throw error", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, error);

Expand Down