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,
p,
q,
custom_title,
locale,
border_radius,
Expand All @@ -46,6 +48,8 @@ export default async (req, res) => {
const topLangs = await fetchTopLanguages(
username,
parseArray(exclude_repo),
p,
q,
);

const cacheSeconds = clampValue(
Expand Down
22 changes: 22 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
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`.
- `p` - Configures ranking algorithm, defaults to 1, recommended is 0.5 _(number)_
- `q` - Configures ranking algorithm, defaults to 0, recommended is 0.5 _(number)_

> **Warning**
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
Expand Down Expand Up @@ -345,6 +347,26 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
```

### Ranking configuration

You can use the `&p=` and `&q=` options to change the method used to rank the languages used. The values must be positive real numbers.

The algorithm used is

```javascript
ranking_index = (byte_count ^ p) * (repo_count ^ q)
```

[Details here.](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305)

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

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

### Demo

[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
Expand Down
20 changes: 18 additions & 2 deletions src/fetchers/top-languages-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ 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 = [], p = 1, q = 0) => {
if (!username) throw new MissingParamError(["username"]);

const res = await retryer(fetcher, { login: username });
Expand Down Expand Up @@ -101,6 +101,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 +113,34 @@ 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 = repoCount + 1;
}
else {
// reset repoCount to 1
// language must exist in atleast 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, p) * Math.pow(repoNodes[name].count, q);
})

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", p = 0.5, q = 0.5);
expect(repo).toStrictEqual({
HTML: {
color: "#0f0",
count: 2,
name: "HTML",
size: 200,
size: 20,
},
javascript: {
color: "#0ff",
count: 2,
name: "javascript",
size: 200,
size: 20,
},
});
});
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", p = 1, q = 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", exclude_repo = [], p = 0, q = 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