Skip to content

Commit bd92937

Browse files
committed
initial commit
0 parents  commit bd92937

File tree

17 files changed

+670
-0
lines changed

17 files changed

+670
-0
lines changed

.github/workflows/stats.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Update SVG and Deploy to Pages
2+
3+
on:
4+
schedule:
5+
- cron: '0 */2 * * *'
6+
workflow_dispatch:
7+
push:
8+
branches: [ main ]
9+
10+
jobs:
11+
generate-and-deploy:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
token: ${{ secrets.GITHUB_TOKEN }}
19+
fetch-depth: 0
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.12'
25+
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
30+
31+
- name: Generate SVG file
32+
env:
33+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34+
USERNAME: "NF-coder"
35+
run: |
36+
python src/main.py
37+
38+
- name: Verify SVG file
39+
run: |
40+
if [ ! -f out.svg ]; then
41+
echo "Error: out.svg was not generated!"
42+
exit 1
43+
fi
44+
45+
mkdir -p deploy-files
46+
cp out.svg deploy-files/
47+
48+
- name: Deploy to GitHub Pages
49+
uses: peaceiris/actions-gh-pages@v3
50+
with:
51+
github_token: ${{ secrets.GITHUB_TOKEN }}
52+
publish_dir: ./deploy-files
53+
keep_files: false
54+
allow_empty_commit: false

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.vscode/
2+
.venv/
3+
__pycache__/
4+
5+
out.svg
6+
out.html

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Jinja2==3.1.6
2+
requests==2.32.4

src/calc.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from renderer.renderer import IRenderer
2+
from math import sin, cos, pi, degrees
3+
4+
class Math:
5+
@staticmethod
6+
def math_rounding(value: float) -> int:
7+
"""Округление по правилам математики"""
8+
return int(value)+1 if value%1>0.5 else int(value)
9+
10+
class Calc:
11+
def __init__(
12+
self,
13+
outer_radius: int,
14+
thickness: int,
15+
percent_array: list[float],
16+
sections_colors_array: list[str],
17+
names_array: list[str],
18+
renderer: IRenderer,
19+
margin_x: int = 0,
20+
margin_y: int = 0,
21+
) -> None:
22+
self._thickness = thickness
23+
self._outer_radius = outer_radius
24+
self._renderer = renderer
25+
self._margin_x = margin_x
26+
self._margin_y = margin_y
27+
self._percent_array = percent_array
28+
self._sections_colors_array = sections_colors_array
29+
self._inner_radius = outer_radius - thickness
30+
self._names_array = names_array
31+
32+
def __call__(self) -> str:
33+
angle_accumulator = 0 # sum of all previous angles
34+
35+
next_outer_curve_start_x = self._margin_x + self._outer_radius # x-axis position of end of previous outer curve / start of new outer curve
36+
next_outer_curve_start_y = self._margin_y # y-axis position of end of previous outer curve / start of new outer curve
37+
38+
next_inner_curve_start_x = self._margin_x + self._outer_radius # x-axis position of end of previous inner curve / start of new inner curve
39+
next_inner_curve_start_y = self._margin_y + self._thickness # y-axis position of end of previous inner curve / start of new inner curve
40+
41+
for percent,color,name in zip(self._percent_array, self._sections_colors_array, self._names_array):
42+
current_angle = 2*pi*percent + angle_accumulator
43+
44+
outer_x = self._margin_x + self._outer_radius*(1+sin(current_angle))
45+
outer_y = self._margin_y + self._outer_radius*(1-cos(current_angle))
46+
inner_x = self._margin_x + self._thickness + self._inner_radius*(1+sin(current_angle))
47+
inner_y = self._margin_y + self._thickness + self._inner_radius*(1-cos(current_angle))
48+
49+
self._renderer.add_section(
50+
(Math.math_rounding(next_outer_curve_start_x), Math.math_rounding(next_outer_curve_start_y)),
51+
(Math.math_rounding(outer_x), Math.math_rounding(outer_y)),
52+
(Math.math_rounding(inner_x), Math.math_rounding(inner_y)),
53+
(Math.math_rounding(next_inner_curve_start_x), Math.math_rounding(next_inner_curve_start_y)),
54+
section_start_angle=degrees(angle_accumulator),
55+
section_finish_angle=degrees(current_angle),
56+
color=color,
57+
name=name
58+
)
59+
60+
# Updating all values
61+
angle_accumulator += 2*pi*percent
62+
next_outer_curve_start_x = outer_x
63+
next_outer_curve_start_y = outer_y
64+
next_inner_curve_start_x = inner_x
65+
next_inner_curve_start_y = inner_y
66+
67+
return self._renderer.render()
68+

src/fetcher/fetcher.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import requests
2+
3+
from .fetcherSettings import FetcherSettings
4+
from .utils.bodyFabric import BodyFabric
5+
6+
from .langElem import LangElem
7+
8+
class FetchLangStats:
9+
def __init__(self, token: str) -> None:
10+
self._token = token
11+
12+
def fetch_user(self, username: str) -> list[LangElem]:
13+
headers = {
14+
'Authorization': f'token {self._token}',
15+
'Content-Type': 'application/json'
16+
}
17+
18+
body_fabric = BodyFabric(
19+
username=username,
20+
first_k_repos_fetch=FetcherSettings.FIRST_K_REPOS_FETCH,
21+
top_k_languages_in_repo=FetcherSettings.TOP_K_LANGUAGES_IN_REPO
22+
)
23+
24+
response = requests.post(
25+
url=FetcherSettings.GITHUB_APT_URL,
26+
headers=headers,
27+
json=body_fabric.create()
28+
)
29+
30+
if response.status_code == 200:
31+
response = response.json()
32+
33+
result = []
34+
35+
for repo in response["data"]["user"]["repositories"]["nodes"]:
36+
for languages in repo["languages"]["edges"]:
37+
result.append(
38+
LangElem(
39+
languages["size"],
40+
languages["node"]["color"],
41+
languages["node"]["name"]
42+
)
43+
)
44+
return result
45+
46+
raise RuntimeError("Github returnrd non-200 responce")

src/fetcher/fetcherSettings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from dataclasses import dataclass
2+
3+
@dataclass
4+
class FetcherSettings:
5+
GITHUB_APT_URL: str = "https://api.github.com/graphql"
6+
7+
FIRST_K_REPOS_FETCH: int = 100
8+
TOP_K_LANGUAGES_IN_REPO: int = 10
9+

src/fetcher/langElem.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from dataclasses import dataclass
2+
3+
@dataclass
4+
class LangElem:
5+
size: int
6+
github_color: str
7+
name: str

src/fetcher/utils/bodyFabric.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class BodyFabric:
2+
def __init__(
3+
self,
4+
username: str,
5+
first_k_repos_fetch: int,
6+
top_k_languages_in_repo: int
7+
) -> None:
8+
self._username = username
9+
self._first_k_repos_fetch = first_k_repos_fetch
10+
self._top_k_languages_in_repo = top_k_languages_in_repo
11+
12+
def create(self) -> dict[str, str]:
13+
return {
14+
"query":
15+
"""query {user(login: \"""" + self._username + """\") {
16+
repositories(ownerAffiliations: OWNER, isFork: false, first: """ + str(self._first_k_repos_fetch) + """) {
17+
nodes {
18+
name languages(
19+
first: """ + str(self._top_k_languages_in_repo) +""",
20+
orderBy: {
21+
field: SIZE,
22+
direction: DESC
23+
}
24+
) {
25+
edges {
26+
size
27+
node {
28+
color
29+
name
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}
37+
""".replace("\n","")
38+
}

src/main.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import os
2+
3+
from renderer.renderer import RenderBuilder
4+
from oklchUtils.OKLCHUtils import OKLCHUtils
5+
from calc import Calc
6+
7+
from fetcher.fetcher import FetchLangStats
8+
9+
# Fetch info
10+
USERNAME = os.environ['USERNAME']
11+
TOKEN = os.environ['GITHUB_TOKEN']
12+
13+
# Plane settings
14+
HEIGHT: int = 140
15+
WIDTH: int = 250
16+
17+
# Captions settings
18+
LEGEND_MARGIN_X: int = 140
19+
LEGEND_MARGIN_Y: int = 30
20+
SPACE_BETWEEN_CAPTIONS: int = 22
21+
FONT_COLOR: str = "#c1c1c1"
22+
23+
# Color settings
24+
OKLCH_CHROMA: float = 0.099
25+
OKLCH_LIGHTNESS: float = 0.636
26+
OTHER_COLOR_CHROMA: float = 0.000
27+
28+
# Round sizes settings
29+
OUTER_RADIUS: int = 55
30+
THICKNESS: int = 12
31+
32+
# Margins settings
33+
MARGIN_X: int = 20
34+
MARGIN_Y: int = 15
35+
36+
# Arrays settings
37+
TOP_K: int = 4
38+
39+
# Output filepath
40+
OUTPUT_FILE: str = "./out.svg"
41+
42+
def get_langs_data(token: str):
43+
info = {}
44+
45+
total_size = 0
46+
47+
for elem in FetchLangStats(token).fetch_user(USERNAME):
48+
total_size += elem.size
49+
if elem.name not in info: info[elem.name] = elem.size
50+
else: info[elem.name] += elem.size
51+
52+
return info.keys(), map(lambda x: x/total_size, info.values())
53+
54+
def truncate(langs_arr: list[tuple[float, str]], k: int):
55+
if len(langs_arr) <= k: return langs_arr
56+
return langs_arr[:k-1] + [(sum(map(lambda x: x[0], langs_arr[k-1:])), "Other")]
57+
58+
def main():
59+
NAMES_ARR, PERCENT_ARR = get_langs_data(TOKEN)
60+
61+
sorted_percents = sorted([(percent, name) for percent,name in zip(PERCENT_ARR, NAMES_ARR)], key=lambda x: x[0], reverse=True)
62+
63+
sorted_percents = truncate(sorted_percents, TOP_K)
64+
65+
_ = Calc(
66+
outer_radius=OUTER_RADIUS,
67+
thickness=THICKNESS,
68+
percent_array=[elem[0] for elem in sorted_percents],
69+
sections_colors_array=OKLCHUtils.create_colors_array(
70+
length=len(sorted_percents),
71+
chroma=OKLCH_CHROMA,
72+
lightness=OKLCH_LIGHTNESS
73+
),
74+
renderer=RenderBuilder(
75+
height=HEIGHT,
76+
width=WIDTH,
77+
outer_radius=OUTER_RADIUS,
78+
thickness=THICKNESS,
79+
margin_x=MARGIN_X,
80+
margin_y=MARGIN_Y,
81+
legend_margin_x=LEGEND_MARGIN_X,
82+
legend_margin_y=LEGEND_MARGIN_Y,
83+
space_between_captions=SPACE_BETWEEN_CAPTIONS,
84+
font_color=FONT_COLOR
85+
),
86+
margin_x=MARGIN_X,
87+
margin_y=MARGIN_Y,
88+
names_array=[elem[1] for elem in sorted_percents]
89+
)
90+
91+
with open(OUTPUT_FILE, "w+", encoding="utf-8-sig") as f:
92+
f.write(_())
93+
94+
if __name__ == '__main__':
95+
main()

src/oklchUtils/OKLCHUtils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .oklchConv.OKLCHConverter import OKLCHConverter
2+
3+
class OKLCHUtils:
4+
@staticmethod
5+
def create_colors_array(length: int, chroma: float, lightness: float):
6+
return [
7+
OKLCHConverter.oklch_to_rgb(lightness, chroma, hue)
8+
for hue in range(0, 360, 360//length)
9+
]

0 commit comments

Comments
 (0)