├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml ├── preview │ └── user-statistician.png └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── generate-international-samples.yml │ ├── generate-samples.yml │ ├── major-release-num.yml │ ├── manual-theme-sample.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── octicons ├── README.md ├── archive-16.svg ├── comment-discussion-16.svg ├── eye-16.svg ├── git-commit-16.svg ├── git-pull-request-16.svg ├── heart-16.svg ├── issue-opened-16.svg ├── lock-16.svg ├── mark-github-16.svg ├── people-16.svg ├── person-add-16.svg ├── repo-16.svg ├── repo-forked-16.svg ├── repo-push-16.svg ├── repo-template-16.svg ├── ruby-16.svg └── star-16.svg ├── quickstart ├── README.md ├── all-defaults.yml ├── contributions.yml ├── dark-dimmed.yml ├── dark.yml ├── languages.yml ├── multiple-stats-cards.yml └── repositories.yml ├── src ├── ColorUtil.py ├── Colors.py ├── PieChart.py ├── StatConfig.py ├── Statistician.py ├── StatsImageGenerator.py ├── TextLength.py ├── UserStatistician.py ├── locales │ ├── bn.json │ ├── cs.json │ ├── de.json │ ├── el.json │ ├── en.json │ ├── es.json │ ├── fa.json │ ├── fi.json │ ├── fr.json │ ├── hi.json │ ├── hu.json │ ├── hy.json │ ├── id.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── lt.json │ ├── ml.json │ ├── nl.json │ ├── no.json │ ├── or.json │ ├── pl.json │ ├── pt.json │ ├── ro.json │ ├── ru.json │ ├── sat.json │ ├── sr.json │ ├── sv.json │ ├── th.json │ ├── tl.json │ ├── tr.json │ └── uk.json └── queries │ ├── basicstats.graphql │ ├── reposContributedTo.graphql │ ├── repostats.graphql │ └── singleYearQueryFragment.graphql ├── tests └── tests.py └── util ├── CharacterWidths.py ├── default-widths.json ├── default-widths.py └── refactor-locales-to-json.py /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Dockerfile 3 | !src 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.graphql linguist-detectable 2 | src/locales/*.json linguist-detectable 3 | quickstart/*.yml linguist-detectable 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | target-branch: "main" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "main" 16 | schedule: 17 | interval: "daily" 18 | 19 | -------------------------------------------------------------------------------- /.github/preview/user-statistician.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cicirello/user-statistician/4c6b9b1317921fe8bf278b45639755748f77d58d/.github/preview/user-statistician.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: [ '**.svg', '**.md' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Setup Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.12' 26 | 27 | - name: Run Python unit tests 28 | run: | 29 | python3 -u -m unittest tests/tests.py 30 | env: 31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | 33 | - name: Verify that the Docker image for the action builds 34 | run: docker build . --file Dockerfile 35 | 36 | - name: Integration test 37 | id: integration 38 | uses: ./ 39 | with: 40 | colors: dark 41 | commit-and-push: false 42 | featured-repository: Chips-n-Salsa 43 | animated-language-chart: true 44 | locale: en 45 | #fail-on-error: false 46 | #category-order: general, repositories, languages, contributions 47 | env: 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | 50 | - name: Output the outputs of the integration test of the action 51 | run: | 52 | echo "exit-code = ${{ steps.integration.outputs.exit-code }}" 53 | 54 | - name: Upload generated SVG as a workflow artifact for inspection if necessary 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: generated-image 58 | path: images/userstats.svg 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '34 20 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/generate-international-samples.yml: -------------------------------------------------------------------------------- 1 | name: international samples 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | samples: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | ref: samples 15 | 16 | - name: Sample German 17 | uses: cicirello/user-statistician@v1 18 | with: 19 | colors: light 20 | locale: de 21 | image-file: images/de.svg 22 | env: 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | 25 | - name: Sample Italian 26 | uses: cicirello/user-statistician@v1 27 | with: 28 | colors: dark 29 | locale: it 30 | image-file: images/it.svg 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/generate-samples.yml: -------------------------------------------------------------------------------- 1 | name: samples 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * 6' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | samples: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: samples 17 | 18 | - name: Sample light 19 | uses: cicirello/user-statistician@v1 20 | with: 21 | colors: light 22 | image-file: images/light.svg 23 | # Using custom-title is not necessary here in general. 24 | # I'm just using it so that the sample has a more generic name 25 | # of user rather than my name. The format this uses is identical 26 | # to if this input was not used, but with my name replaced by a 27 | # generic name. 28 | custom-title: Firstname Lastname's GitHub Activity 29 | env: 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | 32 | - name: Sample dark 33 | uses: cicirello/user-statistician@v1 34 | with: 35 | colors: dark 36 | image-file: images/dark.svg 37 | include-title: false 38 | env: 39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | 41 | - name: Sample dark-dimmed 42 | uses: cicirello/user-statistician@v1 43 | with: 44 | colors: dark-dimmed 45 | image-file: images/dark-dimmed.svg 46 | custom-title: My GitHub Statistics 47 | hide-keys: joined, mostStarred, mostForked, followers, following, private 48 | max-languages: 100 49 | animated-language-chart: true 50 | env: 51 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 52 | -------------------------------------------------------------------------------- /.github/workflows/major-release-num.yml: -------------------------------------------------------------------------------- 1 | name: Move Major Release Tag 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | movetag: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Get major version num and update tag 15 | run: | 16 | VERSION=${GITHUB_REF#refs/tags/} 17 | MAJOR=${VERSION%%.*} 18 | git config --global user.name 'Vincent A Cicirello' 19 | git config --global user.email 'cicirello@users.noreply.github.com' 20 | git tag -fa ${MAJOR} -m "Update major version tag" 21 | git push origin ${MAJOR} --force 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/manual-theme-sample.yml: -------------------------------------------------------------------------------- 1 | name: Generate Theme Sample 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | THEME: 7 | description: 'Theme Key' 8 | required: true 9 | 10 | jobs: 11 | theme-sample: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | ref: samples 19 | 20 | - name: Theme sample 21 | uses: cicirello/user-statistician@v1 22 | with: 23 | colors: ${{ github.event.inputs.THEME }} 24 | image-file: images/${{ github.event.inputs.THEME }}.svg 25 | # Using custom-title is not necessary here in general. 26 | # I'm just using it so that the sample has a more generic name 27 | # of user rather than my name. The format this uses is identical 28 | # to if this input was not used, but with my name replaced by a 29 | # generic name. 30 | custom-title: Firstname Lastname's GitHub Activity 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale PRs' 2 | on: 3 | schedule: 4 | - cron: '45 1 * * *' 5 | 6 | permissions: 7 | pull-requests: write 8 | 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | stale-pr-message: 'This PR has been automarked as stale for 30 days of inactivity, and will autoclose if still inactive in 5 days.' 16 | close-pr-message: 'Autoclosing this stale PR.' 17 | days-before-stale: 30 18 | days-before-close: 5 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | tests/__pycache__/ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] - 2025-05-19 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Deprecated 14 | 15 | ### Removed 16 | 17 | ### Fixed 18 | 19 | ### Dependencies 20 | * Bump cicirello/pyaction from 4.32.0 to 4.33.0 21 | 22 | ### CI/CD 23 | 24 | ### Other 25 | 26 | 27 | ## [1.24.0] - 2024-10-14 28 | 29 | ### Added 30 | * Translation to Czech (`locale: cs`) in #259 (@Sarthak027) 31 | * Translation to Greek (`locale: el`) in #260 (@Debasmita54) 32 | 33 | ### Dependencies 34 | * Bump cicirello/pyaction from 4.29.0 to 4.32.0 35 | 36 | 37 | ## [1.23.0] - 2024-05-13 38 | 39 | ### Added 40 | * An action input, `commit-message`, to enable customizing the commit message 41 | 42 | ### Dependencies 43 | * Bump cicirello/pyaction from 4.27.0 to 4.29.0 44 | 45 | 46 | ## [1.22.1] - 2023-12-08 47 | 48 | ### Fixed 49 | * Eliminated adjustment for watching own repositories from the "Watched By" stat for consistency with other stats that don't make such an adjustment such as the star count. 50 | 51 | ### Dependencies 52 | * Bump cicirello/pyaction from 4.25.0 to 4.27.0 53 | 54 | 55 | ## [1.22.0] - 2023-10-18 56 | 57 | ### Added 58 | * Translation to Armenian (`locale: hy`) in #240 (@JairTorres1003). 59 | 60 | ### Dependencies 61 | * Bump cicirello/pyaction from 4.24.0 to 4.25.0 (which includes bumping Python to 3.12). 62 | 63 | ### CI/CD 64 | * Bump Python to 3.12 in CI/CD workflows when running unit tests (@cicirello). 65 | 66 | 67 | ## [1.21.0] - 2023-10-04 68 | 69 | ### Added 70 | * Translation to Tagalog (`locale: tl`) in #227 (@digracesion). 71 | * Translation to Swedish (`locale: sv`) in #230 (@Viveksati5143). 72 | * Translation to Persian (`locale: fa`) in #232 (@AshkanArabim). 73 | * Translation to Malayalam (`locale: ml`) in #235 (@Sarthak027). 74 | * Translation to Finnish (`locale: fi`) in #236 (@Sadeedpv). 75 | 76 | ### Dependencies 77 | * Bump cicirello/pyaction from 4.22.0 to 4.24.0 78 | 79 | ### Other 80 | * Updated the quickstart / sample workflows to the latest version of actions/checkout. 81 | 82 | 83 | ## [1.20.5] - 2023-09-07 84 | 85 | ### Fixed 86 | * Resolved issue with failing to commit and push, a bug introduced in v1.20.3. 87 | 88 | 89 | ## [1.20.4] - 2023-09-07 90 | 91 | ### Fixed 92 | * Refactored everything locale related to extract definitions of locales from Python dictionaries into JSON files to make it easier to contribute additional language translations. 93 | 94 | 95 | ## [1.20.3] - 2023-09-06 96 | 97 | ### Fixed 98 | * Get repository owner (user for stats image) from GitHub Actions environment variables #210 (fixes issue related to update to GitHub CLI #209 determining owner of repository). 99 | 100 | ### Dependencies 101 | * Bump cicirello/pyaction from 4.14.0 to 4.22.0 102 | 103 | 104 | ## [1.20.2] - 2022-12-30 105 | 106 | ### Fixed 107 | * Better match background for GitHub-inspired themes, using GitHub's canvas.default instead of canvas.inset. 108 | 109 | 110 | ## [1.20.1] - 2022-12-30 111 | 112 | ### Fixed 113 | * Improved Russian Translation in #203, contributed by @mrtnvgr. 114 | 115 | ### Dependencies 116 | * Bump cicirello/pyaction from 4.12.0 to 4.14.0 117 | 118 | 119 | ## [1.20.0] - 2022-10-25 120 | 121 | ### Added 122 | * Translation to Odia (`locale: or`) in #186, contributed by @Prasanta-Hembram. 123 | 124 | ### Fixed 125 | * Some users may be using the action on a self-hosted runner not yet updated to a version supporting the 126 | new GitHub Actions `GITHUB_OUTPUT` env file. This patch adds backwards compatibility for that case by 127 | falling back to the deprecated `set-output` if `GITHUB_OUTPUT` doesn't exist. #190 (@cicirello). 128 | 129 | ### Dependencies 130 | * Bump cicirello/pyaction from 4.11.0 to 4.12.0, including upgrading Python within the Docker container to 3.11. 131 | 132 | ### CI/CD 133 | * Bump Python to 3.11 in CI/CD workflows. 134 | 135 | 136 | ## [1.19.0] - 2022-10-20 137 | 138 | ### Added 139 | * Translation to Santali (`locale: sat`) in #178, contributed by @Prasanta-Hembram. 140 | * Translation to Serbian (`locale: sr`) in #182, contributed by @keen003. 141 | 142 | ### Fixed 143 | * Replaced use of GitHub Action's deprecated `set-output` with the new `$GITHUB_OUTPUT` env file, 144 | in #184 (@cicirello). 145 | 146 | ### Dependencies 147 | * Bump cicirello/pyaction from 4.10.0 to 4.11.0 148 | 149 | 150 | ## [1.18.0] - 2022-10-12 151 | 152 | ### Added 153 | * Translation to Hungarian (`locale: hu`) in #172, contributed by @jpacsai. 154 | 155 | ### Dependencies 156 | * Bump cicirello/pyaction from 4.9.0 to 4.10.0 157 | 158 | 159 | ## [1.17.0] - 2022-10-05 160 | 161 | ### Added 162 | * Increased internationalization with the addition of new locales: 163 | * Dutch (`locale: nl`) in #166, contributed by @lovelacecoding. 164 | * Norwegian (`locale: no`) in #167, contributed by @rubjo. 165 | * Romanian (`locale: ro`) in #164, contributed by @donheshanthaka. 166 | * Thai (`locale: th`) in #165, contributed by @Slowlife01 and updated by @thititongumpun. 167 | 168 | ### Dependencies 169 | * Bump cicirello/pyaction from 4.8.1 to 4.9.0. 170 | 171 | 172 | ## [1.16.1] - 2022-09-09 173 | 174 | ### Fixed 175 | * Corrected minor error in language chart radius calculation that was causing too small margin around chart for users with long names. 176 | 177 | 178 | ## [1.16.0] - 2022-09-08 179 | 180 | ### Added 181 | * New themes, including 182 | * halloween - A dark theme for use around Halloween 183 | * halloween-light - A light theme for use around Halloween 184 | * batty - A light theme for use around Halloween 185 | * Additional icon options for the icon in top corners, including: 186 | * pumpkin 187 | * bat 188 | 189 | ### Dependencies 190 | * Bump cicirello/pyaction from 4.7.1 to 4.8.1, including upgrading Python within the Docker container to 3.10.7 191 | 192 | 193 | ## [1.15.1] - 2022-08-24 194 | 195 | ### Fixed 196 | * Decreased the size of icon in top corners for better visual appearance. 197 | 198 | 199 | ## [1.15.0] - 2022-08-19 200 | 201 | ### Added 202 | * Icons in upper corners surrounding the title, with the following features: 203 | * Theme-defined icons, initially the GitHub Octocat from Octicons for current built-in themes. 204 | * Input `top-icon` to enable overriding, such as disabling the icons, or setting a different one. 205 | * For now, `top-icon` is limited to the GitHub Octocat or nothing (additional options planned). 206 | 207 | ### Dependencies 208 | * Bump cicirello/pyaction from 4.4.0 to 4.7.1 209 | 210 | 211 | ## [1.14.0] - 2022-06-08 212 | 213 | ### Changed 214 | * Centered title. 215 | * Bumped base docker image cicirello/pyaction from 4.3.1 to 4.4.0. 216 | 217 | ### Fixed 218 | * Minor label edit. 219 | 220 | 221 | ## [1.13.0] - 2022-05-02 222 | 223 | ### Added 224 | * New themes added to correspond to all of GitHub's themes, including: 225 | * dark-high-contrast 226 | * light-high-contrast 227 | * dark-colorblind 228 | * light-colorblind 229 | * dark-tritanopia 230 | * light-tritanopia 231 | 232 | ### Changed 233 | * Bumped base Docker image cicirello/pyaction from 4.2.0 to 4.3.1. 234 | 235 | ### Fixed 236 | * Fixed margin calculation when most starred, most forked, or featured repo has long name. 237 | * Adjusted existing themes (dark, light, dark-dimmed) based on newer versions of corresponding GitHub themes. 238 | 239 | 240 | ## [1.12.3] - 2022-02-22 241 | 242 | ### Changed 243 | * Switched to specific release of base Docker image to avoid accidental breaking changes 244 | in base Docker image. 245 | 246 | ### Fixed 247 | * Total count of repositories (other than own) contributed to will now show as 248 | a blank spot on the SVG. Previously reported values were highly inaccurate, and 249 | cannot be computed accurately at the present time due to unavailability of 250 | necessary data from the GitHub GraphQL API. 251 | 252 | 253 | ## [1.12.2] - 2022-02-18 254 | 255 | ### Fixed 256 | * Suppressed Python's pycache on imports (fixes Issue #107). 257 | 258 | 259 | ## [1.12.1] - 2022-02-17 260 | 261 | ### Changed 262 | * Refactored text length calculation. 263 | 264 | 265 | ## [1.12.0] - 2021-11-04 266 | 267 | ### Added 268 | * Increased internationalization support with the addition of new locales: 269 | * Ukrainian (`locale: uk`) via [PR#102](https://github.com/cicirello/user-statistician/pull/102). 270 | 271 | ### Fixed 272 | * Added missing `lang` and `xml:lang` attributes to the opening svg tag to report the 273 | language of the content of the SVG to provide better support for visually impaired 274 | users who use a screen reader. 275 | 276 | 277 | ## [1.11.0] - 2021-10-13 278 | 279 | ### Added 280 | * Increased internationalization support with the addition of new locales: 281 | * Lithuanian (`locale: lt`) via [PR#98](https://github.com/cicirello/user-statistician/pull/98). 282 | * Japanese (`locale: ja`) via [PR#89](https://github.com/cicirello/user-statistician/pull/89). 283 | * Turkish (`locale: tr`) via [PR#90](https://github.com/cicirello/user-statistician/pull/90). 284 | 285 | 286 | ## [1.10.0] - 2021-10-06 287 | 288 | ### Added 289 | * Increased internationalization support with the addition of new locales: 290 | * Korean (`locale: ko`) via [PR#93](https://github.com/cicirello/user-statistician/pull/93). 291 | 292 | ### Fixed 293 | * The total column for the number of repositories (owned by someone else) that the user has 294 | contributed to, at the present time, cannot be computed exactly due to limitations in the 295 | GitHub API. The relevant queries seem to exclude older contribTo data. To account for this, 296 | that value is now listed as a lower bound (e.g., instead of a number like 7, it is listed 297 | as ≥7). This is the only stat affected by this. 298 | 299 | 300 | ## [1.9.0] - 2021-10-04 301 | 302 | ### Added 303 | * Increased internationalization support with the addition of new locales: 304 | * Portuguese (`locale: pt`) via [PR#69](https://github.com/cicirello/user-statistician/pull/69). 305 | * Bahasa Indonesia (`locale: id`) via [PR#71](https://github.com/cicirello/user-statistician/pull/71). 306 | * French (`locale: fr`) via [PR#77](https://github.com/cicirello/user-statistician/pull/77). 307 | * Spanish (`locale: es`) via [PR#79](https://github.com/cicirello/user-statistician/pull/79). 308 | * Russian (`locale: ru`) via [PR#80](https://github.com/cicirello/user-statistician/pull/80). 309 | * Hindi (`locale: hi`) via [PR#81](https://github.com/cicirello/user-statistician/pull/81). 310 | * Polish (`locale: pl`) via [PR#78](https://github.com/cicirello/user-statistician/pull/78). 311 | * Bengali (`locale: bn`) via [PR#92](https://github.com/cicirello/user-statistician/pull/92). 312 | 313 | ### Changed 314 | * Minor refactoring to improve code maintainability 315 | 316 | 317 | ## [1.8.1] - 2021-09-02 318 | 319 | ### Fixed 320 | * Improved visual consistency of fonts across browsers 321 | 322 | 323 | ## [1.8.0] - 2021-08-31 324 | 325 | ### Added 326 | * German locale: German translations of title template, headings, labels, 327 | etc for locale code `de`. 328 | 329 | ### Changed 330 | * Improved precision of fonts if the SVG is scaled. 331 | * Minor adjustment to margins. 332 | 333 | 334 | ## [1.7.1] - 2021-08-30 335 | 336 | ### Fixed 337 | * The width of the SVG is now set based on the content, including 338 | factoring in the effects of different locales where headings, and 339 | labels may be longer. Note that the `image-width` input can still 340 | be used to set a larger width. The action will now use the larger 341 | of the user-defined value of `image-width`, or the width necessary 342 | to accommodate the content. 343 | 344 | 345 | ## [1.7.0] - 2021-08-28 346 | 347 | ### Added 348 | * Italian locale: Italian translations of title template, headings, labels, 349 | etc for locale code `it`. 350 | 351 | ### Fixed 352 | * Added missing UTF-8 encoding when writing the SVG to fix issue with 353 | characters needed for some language translations. 354 | * Fixed exception in case when user stores the SVG at root of repo. 355 | 356 | 357 | ## [1.6.0] - 2021-08-09 358 | 359 | ### Added 360 | * User adjustable width, via a new action input `image-width`. 361 | 362 | ### Changed 363 | * Revised SVG generation to eliminate unnecessary SVG tags surrounding 364 | icon paths and language chart. This is a non-functional change. The SVG 365 | tags referred to here are not incorrect, but they are not needed. By changing 366 | SVG generation to not insert them, DOM size is decreased (possibly decreasing 367 | rendering time), and file size is decreased, possibly speeding up download time. 368 | 369 | 370 | ## [1.5.0] - 2021-08-06 371 | 372 | ### Added 373 | * A new action input, `featured-repository`, that enables the user of the action 374 | to (optionally) specify a repository to feature in the General Stats and Info 375 | section of the SVG. For example, perhaps they have a repository that they feel 376 | is a better representative of their work than their most starred and most forked 377 | repositories. 378 | * An option to animate the language distribution chart, a continuous rotation of the 379 | pie chart. This feature is disabled by default. It is controlled by a pair of new inputs: 380 | `animated-language-chart` and `language-animation-speed`. 381 | 382 | ### Fixed 383 | * Corrected bug in edge case when user only owns forks, which had been causing the 384 | action to fail with an exception. 385 | 386 | 387 | ## [1.4.0] - 2021-08-04 388 | 389 | ### Added 390 | * Most starred repo added to General Stats and Info section of SVG. 391 | * Most forked repo added to General Stats and Info section of SVG. 392 | 393 | ### Changed 394 | * "General User Stats" section renamed to "General Stats and Info" to better reflect 395 | the addition of Most Starred and Most Forked. 396 | 397 | 398 | ## [1.3.0] - 2021-07-29 399 | 400 | ### Added 401 | * The ability to exclude specific repositories from the language 402 | distribution chart, controlled by a new action input `language-repository-exclusions`, 403 | which is a list of repositories to exclude from the language stats. 404 | 405 | ### Changed 406 | * Revised the Quickstart workflows to include pushing the workflow file to 407 | the events that runs the workflow to make it even easier for a user to get started. 408 | 409 | 410 | ## [1.2.0] - 2021-07-23 411 | 412 | ### Added 413 | * The year user joined GitHub is now in General User Stats section of card. 414 | * New action input, `category-order`, which allows user to customize the order 415 | of the categories of stats. 416 | 417 | ### Changed 418 | * Minified SVG during generation (removed unnecessary characters like new lines, 419 | and a couple empty text tags). This doesn't change the contents or appearance 420 | of the SVG. 421 | 422 | 423 | ## [1.1.1] - 2021-07-22 424 | 425 | ### Fixed 426 | * Fixed minor bug in handling of failOnError input. 427 | 428 | 429 | ## [1.1.0] - 2021-07-20 430 | 431 | ### Added 432 | * Language Distribution section added to the card: 433 | * Languages section of the stats card that summarizes the distribution 434 | of languages for the public repositories owned by the user. This is intended 435 | to be the equivalent of the languages graph that GitHub generates for each 436 | individual repository, except for the combination of all of the user's 437 | repositories. The distribution is visualized, however, with a pie chart, rather 438 | than the simple line chart. 439 | * The language distribution calculation features a user customizable number 440 | of languages to display. Any extra languages beyond what the user specifies 441 | are summarized into a single "Other" item (much like the "Other" that appears 442 | in GitHub's language graphs in a repository for low percentage languages). 443 | * By default, the language distribution auto-calibrates the number of languages 444 | based on the percentages. Specifically, all languages that individually account for 445 | less than one percent are combined into an "Other" item. 446 | 447 | ### Changed 448 | * Text and title colors in built-in themes (light, dark, and dark-dimmed) 449 | changed slightly for accessibility (changed to ensure text and background 450 | have contrast ratio of at least 4.5, and title and background have contrast 451 | ratio of at least 4.5). Test cases will enforce this criteria on any themes 452 | that may be contributed in the future (but not on a user's own custom colors). 453 | * Increased width of image slightly for better visual appearance of data portion 454 | with label portion (e.g., right half with data is same width as left half with 455 | labels). 456 | * Changed default title template to better reflect content of the stats card. 457 | 458 | 459 | ## [1.0.2] - 2021-07-15 460 | 461 | ### Fixed 462 | * Corrected all-time count of repositories contributed to that are owned by others. 463 | 464 | 465 | ## [1.0.1] - 2021-07-14 466 | 467 | ### Fixed 468 | * Changed the author of commits to the github-actions bot 469 | to avoid artificially inflating the user of the action's 470 | commit count. 471 | 472 | 473 | ## [1.0.0] - 2021-07-13 474 | 475 | ### Added 476 | * This is the initial release. The user-statistician is a GitHub 477 | Action that generates a detailed visual summary of your activity 478 | on GitHub in the form of an SVG, suitable to display on your GitHub 479 | Profile README. It runs entirely on GitHub, and is designed to run on 480 | a schedule, pushing an updated user stats SVG to your profile repo. 481 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2025 Vincent A. Cicirello 2 | # https://www.cicirello.org/ 3 | # Licensed under the MIT License 4 | 5 | # The base image is pyaction, which is python slim, plus the GitHub CLI (gh). 6 | FROM ghcr.io/cicirello/pyaction:4.33.0 7 | 8 | # Copy the GraphQl queries and python source into the container. 9 | COPY src / 10 | 11 | # Set the entrypoint. 12 | ENTRYPOINT ["/UserStatistician.py"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vincent A. Cicirello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # user-statistician: Github action for generating a user stats card 2 | # 3 | # Copyright (c) 2021-2024 Vincent A Cicirello 4 | # https://www.cicirello.org/ 5 | # 6 | # MIT License 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | name: 'user-statistician' 27 | description: "Generate a GitHub stats SVG for your GitHub Profile README in GitHub Actions" 28 | branding: 29 | icon: 'book-open' 30 | color: 'green' 31 | inputs: 32 | image-file: 33 | description: 'Name and path of the image file to generate, relative to root of repository' 34 | required: false 35 | default: images/userstats.svg 36 | include-title: 37 | description: 'Include a title in the image' 38 | required: false 39 | default: true 40 | custom-title: 41 | description: 'Define a custom title for the image' 42 | required: false 43 | default: '' 44 | colors: 45 | description: 'The color theme or list of custom colors' 46 | required: false 47 | default: light 48 | hide-keys: 49 | description: 'A list of statistics to hide specified by their key' 50 | required: false 51 | default: '' 52 | fail-on-error: 53 | description: 'Choose whether to fail the workflow if there is an error' 54 | required: false 55 | default: true 56 | commit-and-push: 57 | description: 'Commits and pushes the generated image' 58 | required: false 59 | default: true 60 | locale: 61 | description: 'One of the supported ISO 639-1 (two character) or ISO 639-2 (three character) language codes' 62 | required: false 63 | default: en 64 | border-radius: 65 | description: 'The radius of the border' 66 | required: false 67 | default: 6 68 | show-border: 69 | description: 'Controls whether the stats SVG has a border' 70 | required: false 71 | default: true 72 | small-title: 73 | description: 'Controls size of title' 74 | required: false 75 | default: false 76 | max-languages: 77 | description: 'Controls maximum number of languages to list separately' 78 | required: false 79 | default: auto 80 | category-order: 81 | description: 'List of keys for the categories in order of appearance.' 82 | required: false 83 | default: general, repositories, contributions, languages 84 | language-repository-exclusions: 85 | description: 'List of repositories to exclude from language stats.' 86 | required: false 87 | default: '' 88 | featured-repository: 89 | description: 'Name of a repository to feature in the General Stats and Info section' 90 | required: false 91 | default: '' 92 | animated-language-chart: 93 | description: 'Boolean controlling whether the language chart is animated' 94 | required: false 95 | default: false 96 | language-animation-speed: 97 | description: 'The time for one full rotation in seconds' 98 | required: false 99 | default: 10 100 | image-width: 101 | description: 'The minimum width of the SVG in pixels' 102 | required: false 103 | default: 0 104 | top-icon: 105 | description: 'Icon displayed at top of SVG to left and right of title' 106 | required: false 107 | default: default 108 | commit-message: 109 | description: 'The commit message' 110 | required: false 111 | default: 'Automated change by https://github.com/cicirello/user-statistician' 112 | outputs: 113 | exit-code: 114 | description: '0 if successful or non-zero if unsuccessful' 115 | runs: 116 | using: 'docker' 117 | image: 'Dockerfile' 118 | args: 119 | - ${{ inputs.image-file }} 120 | - ${{ inputs.include-title }} 121 | - ${{ inputs.custom-title }} 122 | - ${{ inputs.colors }} 123 | - ${{ inputs.hide-keys }} 124 | - ${{ inputs.fail-on-error }} 125 | - ${{ inputs.commit-and-push }} 126 | - ${{ inputs.locale }} 127 | - ${{ inputs.border-radius }} 128 | - ${{ inputs.show-border }} 129 | - ${{ inputs.small-title }} 130 | - ${{ inputs.max-languages }} 131 | - ${{ inputs.category-order }} 132 | - ${{ inputs.language-repository-exclusions }} 133 | - ${{ inputs.featured-repository }} 134 | - ${{ inputs.animated-language-chart }} 135 | - ${{ inputs.language-animation-speed }} 136 | - ${{ inputs.image-width }} 137 | - ${{ inputs.top-icon }} 138 | - ${{ inputs.commit-message }} 139 | -------------------------------------------------------------------------------- /octicons/README.md: -------------------------------------------------------------------------------- 1 | # Icons Used in the Images 2 | 3 | The icons contained in this directory are used in the images 4 | generated by this action. They are 5 | from [GitHub's Octicons](https://github.com/primer/octicons), 6 | and are copyright (c) GitHub, Inc, and licensed by GitHub under 7 | the MIT license. 8 | 9 | This directory contains the unaltered Octicons in use by this 10 | action, as a reference point. Some of these may or may not have 11 | been altered as used by the action. 12 | 13 | -------------------------------------------------------------------------------- /octicons/archive-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/comment-discussion-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/eye-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/git-commit-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/git-pull-request-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/heart-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/issue-opened-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/lock-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/mark-github-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/people-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/person-add-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/repo-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/repo-forked-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/repo-push-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/repo-template-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/ruby-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /octicons/star-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | This directory contains several ready-to-use workflows for a few 4 | of the more common anticipated settings. The idea is to make it 5 | as easy as possible for you to try out the action. You can start 6 | with one of these and then customize to your liking. 7 | 8 | ## How to Use 9 | 10 | To use one of these workflows, do the following: 11 | 1. In your GitHub profile repository (repository with 12 | same name as your username), create a directory `.github/workflows` 13 | if you don't already have this. 14 | 2. Pick one of the provided workflows (see the [list](#workflow-list) below). 15 | 3. Download your chosen workflow and commit it to your `.github/workflows` 16 | directory within your profile repository. 17 | 4. If you didn't change the name of the file, then it will run when you push 18 | it to your repository since it has been configured to run on a push to 19 | that filename (assuming your branch is either named `main` or `master`). If 20 | you changed the name of the file, then edit the `paths` 21 | attribute on the `push` event with the new name of the workflow file. 22 | 5. The workflow is configured to run on a schedule daily. You can change the schedule to 23 | your liking. 24 | 6. You can also choose to run it manually because it is also configured on the 25 | `workflow_dispatch` event. To do this, 26 | navigate to the `Actions` tab for your profile repository. Select the 27 | workflow from the list of workflows on the left. You'll notice that 28 | it indicates: "This workflow has a workflow_dispatch event trigger." 29 | To the right of that click the "Run workflow" button to run the workflow 30 | manually. 31 | 7. You'll find the SVG in the images directory (which the action creates 32 | if it doesn't already exist). 33 | 8. Add a link to it in the `README.md` in your profile repository. If you 34 | used one of these workflows as is, without using the inputs to change 35 | the file name of the image, then you can add the image to your profile 36 | with the following Markdown: 37 | 38 | ```markdown 39 | ![My user statistics](images/userstats.svg) 40 | ``` 41 | 42 | Although not required, it is appreciated if you instead link the image to this repository 43 | so that others know how you generated it, with the following markdown: 44 | 45 | ```markdown 46 | [![My user statistics](images/userstats.svg)](https://github.com/cicirello/user-statistician) 47 | ``` 48 | 49 | ## Workflow List 50 | 51 | The ready-to-use workflows are as follows: 52 | * [all-defaults.yml](all-defaults.yml): This runs the action 53 | on a daily schedule using all of the default settings, which is a 54 | light color theme. 55 | * [dark.yml](dark.yml): This runs the action 56 | on a daily schedule with a dark color theme, but otherwise uses 57 | all of the default settings. 58 | * [dark-dimmed.yml](dark-dimmed.yml): This runs the action 59 | on a daily schedule with a dark-dimmed color theme, but otherwise uses 60 | all of the default settings. 61 | * [contributions.yml](contributions.yml): This runs the action 62 | on a daily schedule, only generating the contribution stats (hiding 63 | the other sections), with a dark-dimmed theme. 64 | * [repositories.yml](repositories.yml): This runs the action 65 | on a daily schedule, only generating the repositories stats (hiding 66 | the other sections), with a dark-dimmed theme. 67 | * [languages.yml](languages.yml): This runs the action 68 | on a daily schedule, only generating the languages distribution chart 69 | (hiding the other sections), with a dark theme. 70 | * [multiple-stats-cards.yml](multiple-stats-cards.yml): This runs the 71 | action on a daily schedule, generating three separate SVGs, one for 72 | the contribution stats, one for the repositories stats, and one for 73 | the language distribution chart. It uses the dark theme for all three. 74 | Note that if you use this one, you'll have three images to insert into 75 | your profile readme. 76 | -------------------------------------------------------------------------------- /quickstart/all-defaults.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician with all of 2 | # the defaults, which is a light theme, nothing hidden, etc. 3 | # It is configured on a daily schedule at 3am. 4 | 5 | name: user-statistician 6 | 7 | on: 8 | schedule: 9 | - cron: '0 3 * * *' 10 | push: 11 | branches: [ main, master ] 12 | paths: [ '.github/workflows/all-defaults.yml' ] 13 | workflow_dispatch: 14 | 15 | jobs: 16 | stats: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Generate the user stats image 23 | uses: cicirello/user-statistician@v1 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | -------------------------------------------------------------------------------- /quickstart/contributions.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician to 2 | # generate only the contributions stats, using 3 | # the dark-dimmed color theme. It is configured on a daily 4 | # schedule at 3am. 5 | 6 | name: user-statistician 7 | 8 | on: 9 | schedule: 10 | - cron: '0 3 * * *' 11 | push: 12 | branches: [ main, master ] 13 | paths: [ '.github/workflows/contributions.yml' ] 14 | workflow_dispatch: 15 | 16 | jobs: 17 | stats: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Generate the user stats image 24 | uses: cicirello/user-statistician@v1 25 | with: 26 | colors: dark-dimmed 27 | hide-keys: general, languages, repositories 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /quickstart/dark-dimmed.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician with the 2 | # dark-dimmed color theme, but otherwise uses all of 3 | # the defaults. It is configured on a daily schedule at 3am. 4 | 5 | name: user-statistician 6 | 7 | on: 8 | schedule: 9 | - cron: '0 3 * * *' 10 | push: 11 | branches: [ main, master ] 12 | paths: [ '.github/workflows/dark-dimmed.yml' ] 13 | workflow_dispatch: 14 | 15 | jobs: 16 | stats: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Generate the user stats image 23 | uses: cicirello/user-statistician@v1 24 | with: 25 | colors: dark-dimmed 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | -------------------------------------------------------------------------------- /quickstart/dark.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician with the 2 | # dark color theme, but otherwise uses all of 3 | # the defaults. It is configured on a daily schedule at 3am. 4 | 5 | name: user-statistician 6 | 7 | on: 8 | schedule: 9 | - cron: '0 3 * * *' 10 | push: 11 | branches: [ main, master ] 12 | paths: [ '.github/workflows/dark.yml' ] 13 | workflow_dispatch: 14 | 15 | jobs: 16 | stats: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Generate the user stats image 23 | uses: cicirello/user-statistician@v1 24 | with: 25 | colors: dark 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | -------------------------------------------------------------------------------- /quickstart/languages.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician to 2 | # generate only the language distribution chart, using 3 | # the dark color theme. It is configured on a daily 4 | # schedule at 3am. 5 | 6 | name: user-statistician 7 | 8 | on: 9 | schedule: 10 | - cron: '0 3 * * *' 11 | push: 12 | branches: [ main, master ] 13 | paths: [ '.github/workflows/languages.yml' ] 14 | workflow_dispatch: 15 | 16 | jobs: 17 | stats: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Generate the user stats image 24 | uses: cicirello/user-statistician@v1 25 | with: 26 | colors: dark 27 | hide-keys: general, contributions, repositories 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /quickstart/multiple-stats-cards.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician to 2 | # generate three SVGs, one each for contribution stats, 3 | # repositories stats, and the language distribution chart, using 4 | # the dark color theme. It uses the image-file input to assign each 5 | # SVG a unique filename. It is configured on a daily 6 | # schedule at 3am. 7 | 8 | name: user-statistician 9 | 10 | on: 11 | schedule: 12 | - cron: '0 3 * * *' 13 | push: 14 | branches: [ main, master ] 15 | paths: [ '.github/workflows/multiple-stats-cards.yml' ] 16 | workflow_dispatch: 17 | 18 | jobs: 19 | stats: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Generate the languages distribution 26 | uses: cicirello/user-statistician@v1 27 | with: 28 | image-file: images/languages.svg 29 | colors: dark 30 | hide-keys: general, contributions, repositories 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | 34 | - name: Generate the contributions stats 35 | uses: cicirello/user-statistician@v1 36 | with: 37 | image-file: images/contribs.svg 38 | colors: dark 39 | hide-keys: general, languages, repositories 40 | env: 41 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 42 | 43 | - name: Generate the repositories stats 44 | uses: cicirello/user-statistician@v1 45 | with: 46 | image-file: images/repos.svg 47 | colors: dark 48 | hide-keys: general, contributions, languages 49 | env: 50 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 51 | -------------------------------------------------------------------------------- /quickstart/repositories.yml: -------------------------------------------------------------------------------- 1 | # This workflow configures the user-statistician to 2 | # generate only the repositories stats, using 3 | # the dark-dimmed color theme. It is configured on a daily 4 | # schedule at 3am. 5 | 6 | name: user-statistician 7 | 8 | on: 9 | schedule: 10 | - cron: '0 3 * * *' 11 | push: 12 | branches: [ main, master ] 13 | paths: [ '.github/workflows/repositories.yml' ] 14 | workflow_dispatch: 15 | 16 | jobs: 17 | stats: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Generate the user stats image 24 | uses: cicirello/user-statistician@v1 25 | with: 26 | colors: dark-dimmed 27 | hide-keys: general, languages, contributions 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /src/ColorUtil.py: -------------------------------------------------------------------------------- 1 | # user-statistician: Github action for generating a user stats card 2 | # 3 | # Copyright (c) 2021-2023 Vincent A Cicirello 4 | # https://www.cicirello.org/ 5 | # 6 | # MIT License 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | 27 | def isValidColor(color): 28 | """Checks if a color is a valid color. 29 | 30 | Keyword arguments: 31 | color - The color to check, either in hex or as 32 | a named color or as an rgba(). 33 | """ 34 | validHexDigits = set("0123456789abcdefABCDEF") 35 | color = color.strip() 36 | if color.startswith("#"): 37 | if len(color) != 4 and len(color) != 7: 38 | return False 39 | return all(c in validHexDigits for c in color[1:]) 40 | elif color.startswith("rgba("): 41 | return strToRGBA(color) != None 42 | else: 43 | return color in _namedColors 44 | 45 | def strToRGBA(color): 46 | """Converts a str specifying rgba color to 47 | r, g, b, and a channels. Returns (r, g, b, a) is valid 48 | and otherwise returns None. 49 | 50 | Keyword arguments: 51 | color - a str of the form: rgba(r,g,b,a). 52 | """ 53 | if color.startswith("rgba("): 54 | last = color.find(")") 55 | if last > 5: 56 | rgba = color[5:last].split(",") 57 | if len(rgba) == 4: 58 | try: 59 | r = int(rgba[0]) 60 | g = int(rgba[1]) 61 | b = int(rgba[2]) 62 | a = float(rgba[3]) 63 | except ValueError: 64 | return None 65 | r = min(r, 255) 66 | g = min(g, 255) 67 | b = min(b, 255) 68 | r = max(r, 0) 69 | g = max(g, 0) 70 | b = max(b, 0) 71 | a = max(a, 0.0) 72 | a = min(a, 1.0) 73 | return r, g, b, a 74 | return None 75 | 76 | def highContrastingColor(color): 77 | """Computes a highly contrasting color. 78 | Specifically, maximizes the contrast ratio. Contrast ratio is 79 | (L1 + 0.05) / (L2 + 0.05), where L1 and L2 are the luminances 80 | of the colors and L1 is the larger luminance. Returns None 81 | if color is not a valid hex color or named color. 82 | 83 | Keyword arguments: 84 | color - The color to contrast with, in hex or as a named color. 85 | """ 86 | L = luminance(color) 87 | if L == None: 88 | return None 89 | if (L + 0.05) / 0.05 >= 1.05 / (L + 0.05): 90 | return "#000000" # black 91 | else: 92 | return "#ffffff" # white 93 | 94 | def contrastRatio(c1, c2): 95 | """Computes contrast ratio of a pair of colors. 96 | Returns the contrast ratio provided both colors are valid, 97 | and otherwise returns None. 98 | 99 | Keyword arguments: 100 | c1 - Color 1, in hex or as a named color. 101 | c2 - Color 2, in hex or as a named color. 102 | """ 103 | L1 = luminance(c1) 104 | L2 = luminance(c2) 105 | if L1 == None or L2 == None: 106 | return None 107 | if L1 < L2: 108 | L1, L2 = L2, L1 109 | return (L1 + 0.05) / (L2 + 0.05) 110 | 111 | def luminance(color): 112 | """Calculates the luminance of a color. Returns None 113 | if color is not a valid hex color or named color. 114 | 115 | Keyword arguments: 116 | color - The color, either in hex or as a named color. 117 | """ 118 | color = color.strip() 119 | if color.startswith("rgba("): 120 | rgba = strToRGBA(color) 121 | if rgba != None: 122 | r, g, b, a = rgba 123 | else: 124 | return None 125 | else: 126 | if not color.startswith("#"): 127 | if color not in _namedColors: 128 | return None 129 | color = _namedColors[color] 130 | if len(color) == 4: 131 | r = color[1] + color[1] 132 | g = color[2] + color[2] 133 | b = color[3] + color[3] 134 | elif len(color) == 7: 135 | r = color[1:3] 136 | g = color[3:5] 137 | b = color[5:7] 138 | else: 139 | return None 140 | r = int(r, base=16) 141 | g = int(g, base=16) 142 | b = int(b, base=16) 143 | r = _sRGBtoLin(r / 255) 144 | g = _sRGBtoLin(g / 255) 145 | b = _sRGBtoLin(b / 255) 146 | return 0.2126 * r + 0.7152 * g + 0.0722 * b 147 | 148 | def _sRGBtoLin(c): 149 | """Transformation from 150 | https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance 151 | 152 | Keyword arguments: 153 | c - A color channel (i.e., r, g, or b) 154 | """ 155 | if c <= 0.04045: 156 | return c / 12.92 157 | else : 158 | return ((c + 0.055)/1.055) ** 2.4 159 | 160 | # The SVG, CSS, etc named colors with their hex. 161 | # See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value. 162 | # We need the hex that corresponds to the names in the event that the 163 | # user specifies a color by name so that we can compute a high contrast 164 | # color relative to background for the "Other" language category, or 165 | # for languages without colors defined by Linguist. 166 | _namedColors = { 167 | "aliceblue" : "#f0f8ff", 168 | "antiquewhite" : "#faebd7", 169 | "aqua" : "#00ffff", 170 | "aquamarine" : "#7fffd4", 171 | "azure" : "#f0ffff", 172 | "beige" : "#f5f5dc", 173 | "bisque" : "#ffe4c4", 174 | "black" : "#000000", 175 | "blanchedalmond" : "#ffebcd", 176 | "blue" : "#0000ff", 177 | "blueviolet" : "#8a2be2", 178 | "brown" : "#a52a2a", 179 | "burlywood" : "#deb887", 180 | "cadetblue" : "#5f9ea0", 181 | "chartreuse" : "#7fff00", 182 | "chocolate" : "#d2691e", 183 | "coral" : "#ff7f50", 184 | "cornflowerblue" : "#6495ed", 185 | "cornsilk" : "#fff8dc", 186 | "crimson" : "#dc143c", 187 | "cyan" : "#00ffff", 188 | "darkblue" : "#00008b", 189 | "darkcyan" : "#008b8b", 190 | "darkgoldenrod" : "#b8860b", 191 | "darkgray" : "#a9a9a9", 192 | "darkgreen" : "#006400", 193 | "darkgrey" : "#a9a9a9", 194 | "darkkhaki" : "#bdb76b", 195 | "darkmagenta" : "#8b008b", 196 | "darkolivegreen" : "#556b2f", 197 | "darkorange" : "#ff8c00", 198 | "darkorchid" : "#9932cc", 199 | "darkred" : "#8b0000", 200 | "darksalmon" : "#e9967a", 201 | "darkseagreen" : "#8fbc8f", 202 | "darkslateblue" : "#483d8b", 203 | "darkslategray" : "#2f4f4f", 204 | "darkslategrey" : "#2f4f4f", 205 | "darkturquoise" : "#00ced1", 206 | "darkviolet" : "#9400d3", 207 | "deeppink" : "#ff1493", 208 | "deepskyblue" : "#00bfff", 209 | "dimgray" : "#696969", 210 | "dimgrey" : "#696969", 211 | "dodgerblue" : "#1e90ff", 212 | "firebrick" : "#b22222", 213 | "floralwhite" : "#fffaf0", 214 | "forestgreen" : "#228b22", 215 | "fuchsia" : "#ff00ff", 216 | "gainsboro" : "#dcdcdc", 217 | "ghostwhite" : "#f8f8ff", 218 | "goldenrod" : "#daa520", 219 | "gold" : "#ffd700", 220 | "gray" : "#808080", 221 | "green" : "#008000", 222 | "greenyellow" : "#adff2f", 223 | "grey" : "#808080", 224 | "honeydew" : "#f0fff0", 225 | "hotpink" : "#ff69b4", 226 | "indianred" : "#cd5c5c", 227 | "indigo" : "#4b0082", 228 | "ivory" : "#fffff0", 229 | "khaki" : "#f0e68c", 230 | "lavenderblush" : "#fff0f5", 231 | "lavender" : "#e6e6fa", 232 | "lawngreen" : "#7cfc00", 233 | "lemonchiffon" : "#fffacd", 234 | "lightblue" : "#add8e6", 235 | "lightcoral" : "#f08080", 236 | "lightcyan" : "#e0ffff", 237 | "lightgoldenrodyellow" : "#fafad2", 238 | "lightgray" : "#d3d3d3", 239 | "lightgreen" : "#90ee90", 240 | "lightgrey" : "#d3d3d3", 241 | "lightpink" : "#ffb6c1", 242 | "lightsalmon" : "#ffa07a", 243 | "lightseagreen" : "#20b2aa", 244 | "lightskyblue" : "#87cefa", 245 | "lightslategray" : "#778899", 246 | "lightslategrey" : "#778899", 247 | "lightsteelblue" : "#b0c4de", 248 | "lightyellow" : "#ffffe0", 249 | "lime" : "#00ff00", 250 | "limegreen" : "#32cd32", 251 | "linen" : "#faf0e6", 252 | "magenta" : "#ff00ff", 253 | "maroon" : "#800000", 254 | "mediumaquamarine" : "#66cdaa", 255 | "mediumblue" : "#0000cd", 256 | "mediumorchid" : "#ba55d3", 257 | "mediumpurple" : "#9370db", 258 | "mediumseagreen" : "#3cb371", 259 | "mediumslateblue" : "#7b68ee", 260 | "mediumspringgreen" : "#00fa9a", 261 | "mediumturquoise" : "#48d1cc", 262 | "mediumvioletred" : "#c71585", 263 | "midnightblue" : "#191970", 264 | "mintcream" : "#f5fffa", 265 | "mistyrose" : "#ffe4e1", 266 | "moccasin" : "#ffe4b5", 267 | "navajowhite" : "#ffdead", 268 | "navy" : "#000080", 269 | "oldlace" : "#fdf5e6", 270 | "olive" : "#808000", 271 | "olivedrab" : "#6b8e23", 272 | "orange" : "#ffa500", 273 | "orangered" : "#ff4500", 274 | "orchid" : "#da70d6", 275 | "palegoldenrod" : "#eee8aa", 276 | "palegreen" : "#98fb98", 277 | "paleturquoise" : "#afeeee", 278 | "palevioletred" : "#db7093", 279 | "papayawhip" : "#ffefd5", 280 | "peachpuff" : "#ffdab9", 281 | "peru" : "#cd853f", 282 | "pink" : "#ffc0cb", 283 | "plum" : "#dda0dd", 284 | "powderblue" : "#b0e0e6", 285 | "purple" : "#800080", 286 | "rebeccapurple" : "#663399", 287 | "red" : "#ff0000", 288 | "rosybrown" : "#bc8f8f", 289 | "royalblue" : "#4169e1", 290 | "saddlebrown" : "#8b4513", 291 | "salmon" : "#fa8072", 292 | "sandybrown" : "#f4a460", 293 | "seagreen" : "#2e8b57", 294 | "seashell" : "#fff5ee", 295 | "sienna" : "#a0522d", 296 | "silver" : "#c0c0c0", 297 | "skyblue" : "#87ceeb", 298 | "slateblue" : "#6a5acd", 299 | "slategray" : "#708090", 300 | "slategrey" : "#708090", 301 | "snow" : "#fffafa", 302 | "springgreen" : "#00ff7f", 303 | "steelblue" : "#4682b4", 304 | "tan" : "#d2b48c", 305 | "teal" : "#008080", 306 | "thistle" : "#d8bfd8", 307 | "tomato" : "#ff6347", 308 | "turquoise" : "#40e0d0", 309 | "violet" : "#ee82ee", 310 | "wheat" : "#f5deb3", 311 | "white" : "#ffffff", 312 | "whitesmoke" : "#f5f5f5", 313 | "yellow" : "#ffff00", 314 | "yellowgreen" : "#9acd32" 315 | } 316 | -------------------------------------------------------------------------------- /src/Colors.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2022 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | 29 | 30 | # Notes on the included themes: 31 | # 32 | # The light, dark, and dark-dimmed themes are based on 33 | # GitHub's themes, and color-palette (see 34 | # https://primer.style/primitives/colors). 35 | # 36 | # Specifically, from the link above we use: 37 | # * background color (bg): canvas.default 38 | # * border color: accent.muted 39 | # * icons: accent.emphasis 40 | # * text: fg.default 41 | # * title: accent.fg 42 | # 43 | # Notes to Potential Contributors: 44 | # 45 | # (1) For those who want to contribute a theme, 46 | # please check the combination of your background 47 | # color with text color, and background with title 48 | # color for accessibility at this site, 49 | # https://colorable.jxnblk.com/, and make sure the 50 | # combination has a rating of at least AA. You can also 51 | # simply run the test cases, which will automatically 52 | # verify that the text color and the background color have 53 | # a contrast ratio of at least 4.5:1, which is AA. 54 | # The contrast ratio between the background and title 55 | # colors should also be at least 4.5:1 (also enforced by test cases). 56 | # 57 | # (2) Before contributing a new color theme, ask yourself 58 | # whether it will likely have broad appeal or a narrow 59 | # audience. For example, if it is just the color palette 60 | # of your personal website or blog, then a theme may not 61 | # be necessary. You can simply use the colors input for 62 | # your usage. 63 | # 64 | # (3) Is it similar to one of the existing themes? Or does it 65 | # provide users with something truly new to choose from? 66 | # 67 | # (4) Please add the new theme alphabetized by theme name. 68 | # 69 | # (5) Include a comment with your GitHub userid indicating you 70 | # are the contributor of the theme (see the existing themes). 71 | # 72 | # (6) You can use either 3-digit hex, 6-digit hex, or named colors. 73 | # 74 | # (7) The existing test cases will automatically test that your 75 | # colors are valid hex, or valid named colors. 76 | # See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value 77 | # for list of named colors. 78 | 79 | colorMapping = { 80 | # Contributor: cicirello (Halloween related themes) 81 | "batty" : { 82 | "bg" : "#F6FAFD", 83 | "border" : "#C0C3C6", 84 | "icons" : "#151515", 85 | "text" : "#535353", 86 | "title" : "#151515", 87 | "title-icon" : "bat" 88 | }, 89 | 90 | # Contributor: cicirello (part of initial theme set) 91 | "dark" : { 92 | "bg" : "#0d1117", 93 | "border" : "rgba(56,139,253,0.4)", 94 | "icons" : "#1f6feb", 95 | "text" : "#c9d1d9", 96 | "title" : "#58a6ff", 97 | "title-icon" : "github" 98 | }, 99 | 100 | # Contributor: cicirello (updated theme set) 101 | "dark-colorblind" : { 102 | "bg" : "#0d1117", 103 | "border" : "rgba(56,139,253,0.4)", 104 | "icons" : "#1f6feb", 105 | "text" : "#c9d1d9", 106 | "title" : "#58a6ff", 107 | "title-icon" : "github" 108 | }, 109 | 110 | # Contributor: cicirello (part of initial theme set) 111 | "dark-dimmed" : { 112 | "bg" : "#22272e", 113 | "border" : "rgba(65,132,228,0.4)", 114 | "icons" : "#316dca", 115 | "text" : "#adbac7", 116 | "title" : "#539bf5", 117 | "title-icon" : "github" 118 | }, 119 | 120 | # Contributor: cicirello (updated theme set) 121 | "dark-high-contrast" : { 122 | "bg" : "#0a0c10", 123 | "border" : "#409eff", 124 | "icons" : "#409eff", 125 | "text" : "#f0f3f6", 126 | "title" : "#71b7ff", 127 | "title-icon" : "github" 128 | }, 129 | 130 | # Contributor: cicirello (updated theme set) 131 | "dark-tritanopia" : { 132 | "bg" : "#0d1117", 133 | "border" : "rgba(56,139,253,0.4)", 134 | "icons" : "#1f6feb", 135 | "text" : "#c9d1d9", 136 | "title" : "#58a6ff", 137 | "title-icon" : "github" 138 | }, 139 | 140 | # Contributor: cicirello (Halloween related themes) 141 | "halloween" : { 142 | "bg" : "#090B06", 143 | "border" : "#F5D913", 144 | "icons" : "#F46D0E", 145 | "text" : "#EB912D", 146 | "title" : "#F46D0E", 147 | "title-icon" : "pumpkin" 148 | }, 149 | 150 | # Contributor: cicirello (Halloween related themes) 151 | "halloween-light" : { 152 | "bg" : "#FFFDE9", 153 | "border" : "#E1DF81", 154 | "icons" : "#BA440B", 155 | "text" : "#50391F", 156 | "title" : "#BA440B", 157 | "title-icon" : "pumpkin" 158 | }, 159 | 160 | # Contributor: cicirello (part of initial theme set) 161 | "light" : { 162 | "bg" : "#ffffff", 163 | "border" : "rgba(84,174,255,0.4)", 164 | "icons" : "#0969da", 165 | "text" : "#24292f", 166 | "title" : "#0969da", 167 | "title-icon" : "github" 168 | }, 169 | 170 | # Contributor: cicirello (updated theme set) 171 | "light-colorblind" : { 172 | "bg" : "#ffffff", 173 | "border" : "rgba(84,174,255,0.4)", 174 | "icons" : "#0969da", 175 | "text" : "#24292f", 176 | "title" : "#0969da", 177 | "title-icon" : "github" 178 | }, 179 | 180 | # Contributor: cicirello (updated theme set) 181 | "light-high-contrast" : { 182 | "bg" : "#ffffff", 183 | "border" : "#368cf9", 184 | "icons" : "#0349b4", 185 | "text" : "#0E1116", 186 | "title" : "#0349b4", 187 | "title-icon" : "github" 188 | }, 189 | 190 | # Contributor: cicirello (updated theme set) 191 | "light-tritanopia" : { 192 | "bg" : "#ffffff", 193 | "border" : "rgba(84,174,255,0.4)", 194 | "icons" : "#0969da", 195 | "text" : "#24292f", 196 | "title" : "#0969da", 197 | "title-icon" : "github" 198 | }, 199 | } 200 | 201 | # These are template strings for the icons available for the title line. 202 | # Each color theme has an associated icon. User can also override the default 203 | # for the theme by name. 204 | # 205 | # The template strings each have up to 4 inputs, {0}, {1}, {2}, and {3}. 206 | # {0} is the width/height, i.e., it is square. 207 | # {1} is the x position in pixels. 208 | # {2} is the y position in pixels. 209 | # {3}, if present, is the fill color, which will be populated with the high 210 | # contrasting color relative to the background (e.g., for github, it will 211 | # either be white or black depending upon background, which is consistent 212 | # with GitHub's logo usage guidelines. 213 | iconTemplates = { 214 | "github" : """""", 215 | "pumpkin" : """""", 216 | "bat" : """""", 217 | } 218 | -------------------------------------------------------------------------------- /src/PieChart.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import math 29 | 30 | _headerTemplate = '' 31 | _pathTemplate = '' 32 | _circleTemplate = '' 33 | _animationTemplate = '' 34 | 35 | def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False): 36 | """Generates an SVG of a pie chart. The intention is to include 37 | as part of a larger SVG (e.g., it does not insert xmlns into the 38 | opening svg tag). If wedges list is empty, it retrurns None. 39 | 40 | Keyword argument: 41 | wedges - A list of Python dictionaries, with each dictionary 42 | containing fields color and percentage. 43 | radius - the radius, in pixels for the pie chart. 44 | animate - Pass True to animate the pie chart. 45 | speed - If animate is True, then this input is the number of 46 | seconds for one full rotation. 47 | """ 48 | if includeSVGHeader: 49 | components = [_headerTemplate.format(str(2*radius))] 50 | else: 51 | components = [] 52 | 53 | if len(wedges) == 0: 54 | return None 55 | elif len(wedges) == 1: 56 | components.append( 57 | _circleTemplate.format(wedges[0]["color"], str(radius))) 58 | else: 59 | startPercentage = 0 60 | for w in wedges: 61 | endPercentage = startPercentage + w["percentage"] 62 | w["start"] = startPercentage * 2 * math.pi 63 | w["end"] = endPercentage * 2 * math.pi 64 | startPercentage = endPercentage 65 | # Adjustment for any possible rounding error that 66 | # may have occurred when initial percentages were computed 67 | # (i.e., last edge should complete a full circle). 68 | wedges[-1]["end"] = 2 * math.pi 69 | 70 | if animate: 71 | components.append("") 72 | 73 | for w in wedges: 74 | components.append( 75 | _pathTemplate.format( 76 | w["color"], 77 | radius + radius * math.cos(w["start"]+math.pi), 78 | radius + radius * math.sin(w["start"]+math.pi), 79 | radius, 80 | 1 if w["percentage"] >= 0.5 else 0, # large arc flag 81 | 1, # clockwise=1 82 | radius + radius * math.cos(w["end"]+math.pi), 83 | radius + radius * math.sin(w["end"]+math.pi) 84 | ) 85 | ) 86 | 87 | if animate: 88 | components.append( 89 | _animationTemplate.format( 90 | radius, 91 | speed 92 | ) 93 | ) 94 | components.append("") 95 | 96 | if includeSVGHeader: 97 | components.append("") 98 | return "".join(components) 99 | -------------------------------------------------------------------------------- /src/StatConfig.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import json 29 | 30 | # The locale keys are ISO 639-1 two-character language codes 31 | # (see: https://www.loc.gov/standards/iso639-2/php/English_list.php). 32 | # If you are contributing a new locale, please add code in alphabetical 33 | # order below. 34 | supportedLocales = { 35 | "bn", 36 | "cs", 37 | "de", 38 | "el", 39 | "en", 40 | "es", 41 | "fa", 42 | "fi", 43 | "fr", 44 | "hi", 45 | "hu", 46 | "hy", 47 | "id", 48 | "it", 49 | "ja", 50 | "ko", 51 | "lt", 52 | "ml", 53 | "nl", 54 | "no", 55 | "or", 56 | "pl", 57 | "pt", 58 | "ro", 59 | "ru", 60 | "sat", 61 | "sr", 62 | "sv", 63 | "th", 64 | "tl", 65 | "tr", 66 | "uk", 67 | } 68 | 69 | # The default order for the categories of stats on the SVG 70 | categoryOrder = ["general", "repositories", "contributions", "languages"] 71 | 72 | # Mapping from category key to list of stats keys in the 73 | # order they should appear. 74 | statsByCategory = { 75 | "general" : [ 76 | "joined", 77 | "featured", 78 | "mostStarred", 79 | "mostForked", 80 | "followers", 81 | "sponsors", 82 | "following", 83 | "sponsoring" 84 | ], 85 | "repositories" : [ 86 | "public", 87 | "starredBy", 88 | "forkedBy", 89 | "watchedBy", 90 | "templates", 91 | "archived" 92 | ], 93 | "contributions" : [ 94 | "commits", 95 | "issues", 96 | "prs", 97 | "reviews", 98 | "contribTo", 99 | "private" 100 | ], 101 | "languages" : [] 102 | } 103 | 104 | _locale_directory = "/locales/" 105 | 106 | def loadLocale(locale) : 107 | """Loads the specified locale. 108 | 109 | Keyword arguments: 110 | locale - The locale code to load. 111 | """ 112 | with open(_locale_directory + locale + ".json", "r", encoding="utf8") as f: 113 | return json.load(f) 114 | 115 | # ADDITIONAL LICENSE NOTES 116 | # 117 | # GitHub's Octicons: 118 | # The paths defining the icons used in the action, as specified in the 119 | # Python dictionary icons below, are derived from GitHub's Octicons 120 | # (https://github.com/primer/octicons), and are copyright (c) GitHub, Inc, 121 | # and licensed by GitHub under the MIT license. 122 | 123 | # Dictionary of icon paths for the supported statistics. 124 | icons = { 125 | "joined" : '', 126 | "featured" : '', 127 | "mostStarred" : '', 128 | "mostForked" : '', 129 | "followers" : '', 130 | "following" : '', 131 | "sponsors" : '', 132 | "sponsoring" : '', 133 | "public" : '', 134 | "starredBy" : '', 135 | "forkedBy" : '', 136 | "watchedBy" : '', 137 | "templates" : '', 138 | "archived" : '', 139 | "commits" : '', 140 | "issues" : '', 141 | "prs" : '', 142 | "reviews" : '', 143 | "contribTo" : '', 144 | "private" : '', 145 | } 146 | -------------------------------------------------------------------------------- /src/Statistician.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import json 29 | import subprocess 30 | import os 31 | 32 | def set_outputs(names_values): 33 | """Sets the GitHub Action outputs. 34 | 35 | Keyword arguments: 36 | names_values - Dictionary of output names with values 37 | """ 38 | if "GITHUB_OUTPUT" in os.environ: 39 | with open(os.environ["GITHUB_OUTPUT"], "a") as f: 40 | for name, value in names_values.items(): 41 | print("{0}={1}".format(name, value), file=f) 42 | else: # Fall-back to deprecated set-output for self-hosted runners 43 | for name, value in names_values.items(): 44 | print("::set-output name={0}::{1}".format(name, value)) 45 | 46 | class Statistician: 47 | """The Statistician class executes GitHub GraphQl queries, 48 | and parses the query results. 49 | """ 50 | 51 | __slots__ = [ 52 | '_contributionYears', 53 | '_user', 54 | '_contrib', 55 | '_repo', 56 | '_login', 57 | '_name', 58 | '_languages', 59 | '_autoLanguages', 60 | '_maxLanguages', 61 | '_languageRepoExclusions', 62 | '_featuredRepo' 63 | ] 64 | 65 | def __init__( 66 | self, 67 | fail, 68 | autoLanguages, 69 | maxLanguages, 70 | languageRepoExclusions, 71 | featuredRepo): 72 | """The initializer executes the queries and parses the results. 73 | Upon completion of the intitializer, the user statistics will 74 | be available. 75 | 76 | Keyword arguments: 77 | fail - If True, the workflow will fail if there are errors. 78 | autoLanguages - If True, the number of displayed languages is chosen based on data, 79 | regardless of value of maxLanguages. 80 | maxLanguages - The maximum number of languages to display. Must be at least 1. If less than 81 | 1, it treats it as if it was 1. 82 | languageRepoExclusions - A set of repositories to exclude from language stats 83 | """ 84 | self._autoLanguages = autoLanguages 85 | self._maxLanguages = maxLanguages if maxLanguages >= 1 else 1 86 | self._languageRepoExclusions = languageRepoExclusions 87 | self._featuredRepo = featuredRepo 88 | self.ghDisableInteractivePrompts() 89 | basicStatsQuery = self.loadQuery("/queries/basicstats.graphql", 90 | fail) 91 | additionalRepoStatsQuery = self.loadQuery("/queries/repostats.graphql", 92 | fail) 93 | oneYearContribTemplate = self.loadQuery("/queries/singleYearQueryFragment.graphql", 94 | fail) 95 | reposContributedTo = self.loadQuery("/queries/reposContributedTo.graphql", 96 | fail) 97 | 98 | self.parseStats( 99 | self.executeQuery(basicStatsQuery, 100 | failOnError=fail), 101 | self.executeQuery(additionalRepoStatsQuery, 102 | needsPagination=True, 103 | failOnError=fail), 104 | self.executeQuery(reposContributedTo, 105 | needsPagination=True, 106 | failOnError=fail) 107 | ) 108 | self.parsePriorYearStats( 109 | self.executeQuery( 110 | self.createPriorYearStatsQuery(self._contributionYears, oneYearContribTemplate), 111 | failOnError=fail 112 | ) 113 | ) 114 | 115 | def getStatsByKey(self, key): 116 | """Gets a category of stats by key. 117 | 118 | Keyword arguments: 119 | key - A category key. 120 | """ 121 | if key == "general": 122 | return self._user 123 | elif key == "repositories": 124 | return self._repo 125 | elif key == "contributions": 126 | return self._contrib 127 | elif key == "languages": 128 | return self._languages 129 | else: 130 | return None # passed an invalid key 131 | 132 | def loadQuery(self, queryFilepath, failOnError=True): 133 | """Loads a graphql query. 134 | 135 | Keyword arguments: 136 | queryFilepath - The file with path of the query. 137 | failOnError - If True, the workflow will fail if there is an error loading the 138 | query; and if False, this action will quietly exit with no error code. In 139 | either case, an error message will be logged to the console. 140 | """ 141 | try: 142 | with open(queryFilepath, 'r') as file: 143 | return file.read() 144 | except IOError: 145 | print("Error (1): Failed to open query file:", queryFilePath) 146 | set_outputs({"exit-code" : 1}) 147 | exit(1 if failOnError else 0) 148 | 149 | def parseStats(self, basicStats, repoStats, reposContributedToStats): 150 | """Parses the user statistics. 151 | 152 | Keyword arguments: 153 | basicStats - The results of the basic stats query. 154 | repoStats - The results of the repo stats query. 155 | """ 156 | # Extract username (i.e., login) and fullname. 157 | # Name needed for title of statistics card, and username 158 | # needed if we support committing stats card. 159 | self._login = basicStats["data"]["user"]["login"] 160 | self._name = basicStats["data"]["user"]["name"] 161 | 162 | # The name field is nullable, so use the login id if 163 | # user's public name is null. 164 | if self._name == None: 165 | self._name = self._login 166 | 167 | # Extract most recent year data from query results 168 | pastYearData = basicStats["data"]["user"]["contributionsCollection"] 169 | 170 | # Extract repositories contributes to (owned by others) in past year 171 | pastYearData[ 172 | "repositoriesContributedTo"] = basicStats[ 173 | "data"]["user"]["repositoriesContributedTo"]["totalCount"] 174 | 175 | # Extract list of contribution years 176 | self._contributionYears = pastYearData["contributionYears"] 177 | # Just reorganizing data for clarity 178 | del pastYearData["contributionYears"] 179 | 180 | # Extract followed and following counts 181 | self._user = {} 182 | self._user["followers"] = [ 183 | basicStats["data"]["user"]["followers"]["totalCount"] ] 184 | self._user["following"] = [ 185 | basicStats["data"]["user"]["following"]["totalCount"] ] 186 | self._user["joined"] = [ min(self._contributionYears) ] 187 | 188 | # Extract sponsors and sponsoring counts 189 | self._user["sponsors"] = [ 190 | basicStats["data"]["user"]["sponsorshipsAsMaintainer"]["totalCount"] ] 191 | self._user["sponsoring"] = [ 192 | basicStats["data"]["user"]["sponsorshipsAsSponsor"]["totalCount"] ] 193 | 194 | # 195 | if self._featuredRepo != None: 196 | self._user["featured"] = [ self._featuredRepo ] 197 | 198 | # Extract all time counts of issues and pull requests 199 | issues = basicStats["data"]["user"]["issues"]["totalCount"] 200 | pullRequests = basicStats["data"]["user"]["pullRequests"]["totalCount"] 201 | 202 | # Reorganize for simplicity 203 | repoStats = list(map(lambda x : x["data"]["user"]["repositories"], repoStats)) 204 | reposContributedToStats = list( 205 | map(lambda x : x["data"]["user"]["topRepositories"], reposContributedToStats)) 206 | 207 | # This is the count of owned repos, including all public, 208 | # but may or may not include all private depending upon token used to authenticate. 209 | ownedRepositories = repoStats[0]["totalCount"] 210 | 211 | # Count num repos owned by someone else that the user has contributed to 212 | # NOTE: It doesn't appear that it is currently possible through any query 213 | # or combination of queries to actually compute this other than for the most recent 214 | # year's data. Keeping the query in, but changing to leave that stat blank in 215 | # the SVG. 216 | repositoriesContributedTo = sum( 217 | 1 for page in reposContributedToStats if page[ 218 | "nodes"] != None for repo in page[ 219 | "nodes"] if repo["owner"]["login"] != self._login) 220 | 221 | self._contrib = { 222 | "commits" : [pastYearData["totalCommitContributions"], 0], 223 | "issues" : [pastYearData["totalIssueContributions"], issues], 224 | "prs" : [pastYearData["totalPullRequestContributions"], pullRequests], 225 | "reviews" : [pastYearData["totalPullRequestReviewContributions"], 0], 226 | # See comment above for reason for this change. 227 | #"contribTo" : [pastYearData["repositoriesContributedTo"], repositoriesContributedTo], 228 | "contribTo" : [pastYearData["repositoriesContributedTo"]], 229 | "private" : [pastYearData["restrictedContributionsCount"], 0] 230 | } 231 | 232 | # The "nodes" field is nullable so make sure the user owns at least 1 repo. 233 | if repoStats[0]["totalCount"] > 0: 234 | # Note that the explicit checks of, if page["nodes"] != None, are 235 | # precautionary since the above check of totalCount should be sufficient 236 | # to protect against a null list of repos. 237 | 238 | # Count stargazers, forks of my repos, and watchers 239 | stargazers = sum( 240 | repo["stargazerCount"] for page in repoStats if page[ 241 | "nodes"] != None for repo in page[ 242 | "nodes"] if not repo["isPrivate"] and not repo["isFork"]) 243 | forksOfMyRepos = sum( 244 | repo["forkCount"] for page in repoStats if page[ 245 | "nodes"] != None for repo in page[ 246 | "nodes"] if not repo["isPrivate"] and not repo["isFork"]) 247 | stargazersAll = sum( 248 | repo["stargazerCount"] for page in repoStats if page[ 249 | "nodes"] != None for repo in page[ 250 | "nodes"] if not repo["isPrivate"]) 251 | forksOfMyReposAll = sum( 252 | repo["forkCount"] for page in repoStats if page[ 253 | "nodes"] != None for repo in page[ 254 | "nodes"] if not repo["isPrivate"]) 255 | 256 | # Find repos with most stars and most forks 257 | try: 258 | mostStars = max( 259 | (repo for page in repoStats if page[ 260 | "nodes"] != None for repo in page[ 261 | "nodes"] if not repo["isPrivate"] and not repo["isFork"]), 262 | key=lambda x : x["stargazerCount"])["name"] 263 | self._user["mostStarred"] = [ mostStars ] 264 | except ValueError: 265 | pass 266 | 267 | try: 268 | mostForks = max( 269 | (repo for page in repoStats if page[ 270 | "nodes"] != None for repo in page["nodes"] if not repo[ 271 | "isPrivate"] and not repo["isFork"]), 272 | key=lambda x : x["forkCount"])["name"] 273 | self._user["mostForked"] = [ mostForks ] 274 | except ValueError: 275 | pass 276 | 277 | # Compute number of watchers 278 | watchers = sum( 279 | repo["watchers"]["totalCount"] for page in repoStats if page[ 280 | "nodes"] != None for repo in page["nodes"] if not repo["isPrivate"]) 281 | 282 | watchersNonForks = sum( 283 | repo["watchers"]["totalCount"] for page in repoStats if page[ 284 | "nodes"] != None for repo in page["nodes"] if not repo[ 285 | "isPrivate"] and not repo["isFork"]) 286 | 287 | # Count of private repos (not accurate since depends on token used to authenticate query, 288 | # however, all those here are included in count of owned repos. 289 | privateCount = sum( 290 | 1 for page in repoStats if page[ 291 | "nodes"] != None for repo in page["nodes"] if repo["isPrivate"]) 292 | 293 | publicAll = ownedRepositories - privateCount 294 | 295 | # Counts of archived repos 296 | publicNonForksArchivedCount = sum( 297 | 1 for page in repoStats if page[ 298 | "nodes"] != None for repo in page["nodes"] if repo[ 299 | "isArchived"] and not repo["isPrivate"] and not repo["isFork"]) 300 | publicArchivedCount = sum( 301 | 1 for page in repoStats if page[ 302 | "nodes"] != None for repo in page["nodes"] if repo[ 303 | "isArchived"] and not repo["isPrivate"]) 304 | 305 | # Counts of template repos 306 | publicNonForksTemplatesCount = sum( 307 | 1 for page in repoStats if page[ 308 | "nodes"] != None for repo in page["nodes"] if repo[ 309 | "isTemplate"] and not repo["isPrivate"] and not repo["isFork"]) 310 | publicTemplatesCount = sum( 311 | 1 for page in repoStats if page[ 312 | "nodes"] != None for repo in page["nodes"] if repo[ 313 | "isTemplate"] and not repo["isPrivate"]) 314 | 315 | # Count of public non forks owned by user 316 | publicNonForksCount = ownedRepositories - sum( 317 | 1 for page in repoStats if page["nodes"] != None for repo in page[ 318 | "nodes"] if repo["isPrivate"] or repo["isFork"]) 319 | 320 | # Compute language distribution 321 | totalSize, languageData = self.summarizeLanguageStats(repoStats) 322 | else: 323 | # if no owned repos then set all repo related stats to 0 324 | stargazers = 0 325 | forksOfMyRepos = 0 326 | stargazersAll = 0 327 | forksOfMyReposAll = 0 328 | watchers = 0 329 | watchersNonForks = 0 330 | privateCount = 0 331 | publicAll = 0 332 | publicNonForksArchivedCount = 0 333 | publicArchivedCount = 0 334 | publicNonForksCount = 0 335 | publicNonForksTemplatesCount = 0 336 | publicTemplatesCount = 0 337 | totalSize, languageData = 0, {} 338 | 339 | self._repo = { 340 | "public" : [publicNonForksCount, publicAll], 341 | "starredBy" : [stargazers, stargazersAll], 342 | "forkedBy" : [forksOfMyRepos, forksOfMyReposAll], 343 | "watchedBy" : [watchersNonForks, watchers], 344 | "archived" : [publicNonForksArchivedCount, publicArchivedCount], 345 | "templates" : [publicNonForksTemplatesCount, publicTemplatesCount] 346 | } 347 | 348 | self._languages = self.organizeLanguageStats(totalSize, languageData) 349 | 350 | def organizeLanguageStats(self, totalSize, languageData): 351 | """Computes a list of languages and percentages in decreasing order 352 | by percentage. 353 | 354 | Keyword arguments: 355 | totalSize - total size of all code with language detection data 356 | languageData - the summarized language totals, colors, etc 357 | """ 358 | if totalSize == 0: 359 | return { "totalSize" : 0, "languages" : [] } 360 | else: 361 | languages = [ (name, data) for name, data in languageData.items() ] 362 | languages.sort(key = lambda L : L[1]["size"], reverse=True) 363 | if self._autoLanguages : 364 | for i, L in enumerate(languages): 365 | if L[1]["percentage"] < 0.01: 366 | self._maxLanguages = i 367 | break 368 | if len(languages) > self._maxLanguages: 369 | self.combineLanguages(languages, self._maxLanguages, totalSize) 370 | self.checkColors(languages) 371 | return { "totalSize" : totalSize, "languages" : languages } 372 | 373 | def combineLanguages(self, languages, maxLanguages, totalSize): 374 | """Combines lowest percentage languages into an Other. 375 | 376 | Keyword arguments: 377 | languages - Sorted list of languages (sorted by size). 378 | maxLanguages - The maximum number of languages to keep as is. 379 | """ 380 | if len(languages) > self._maxLanguages: 381 | combinedSize = sum(L[1]["size"] for L in languages[maxLanguages:]) 382 | languages[maxLanguages] = ( 383 | "Other", 384 | { "color" : None, 385 | "size" : combinedSize, 386 | "percentage" : combinedSize / totalSize 387 | } 388 | ) 389 | del languages[maxLanguages+1:] 390 | 391 | def checkColors(self, languages): 392 | """Make sure all languages have colors, and assign shades of gray to 393 | those that don't. 394 | 395 | Keyword arguments: 396 | languages - Sorted list of languages (sorted by size). 397 | """ 398 | # Not all languages have colors assigned by GitHub's Linguist. 399 | # In such cases, we alternate between these two shades of gray. 400 | colorsForLanguagesWithoutColors = [ "#959da5", "#d1d5da" ] 401 | index = 0 402 | for L in languages: 403 | if L[1]["color"] == None: 404 | L[1]["color"] = colorsForLanguagesWithoutColors[index] 405 | index = (index + 1) % 2 406 | 407 | def summarizeLanguageStats(self, repoStats): 408 | """Summarizes the language distibution of the user's owned repositories. 409 | 410 | Keyword arguments: 411 | repoStats - The results of the repo stats query. 412 | """ 413 | totalSize = 0 414 | languageData = {} 415 | for page in repoStats: 416 | if page["nodes"] != None: 417 | for repo in page["nodes"]: 418 | if (not repo["isPrivate"] and not repo["isFork"] and 419 | (repo["name"].lower() not in self._languageRepoExclusions)): 420 | totalSize += repo["languages"]["totalSize"] 421 | if repo["languages"]["edges"] != None : 422 | for L in repo["languages"]["edges"]: 423 | name = L["node"]["name"] 424 | if name in languageData: 425 | languageData[name]["size"] += L["size"] 426 | else: 427 | languageData[name] = { 428 | "color" : L["node"]["color"], 429 | "size" : L["size"] 430 | } 431 | for L in languageData: 432 | languageData[L]["percentage"] = languageData[L]["size"] / totalSize 433 | return totalSize, languageData 434 | 435 | def createPriorYearStatsQuery(self, yearList, oneYearContribTemplate): 436 | """Generates the query for prior year stats. 437 | 438 | Keyword arguments: 439 | yearList - a list of the years when the user had contributions, 440 | obtained by one of the other queries. 441 | oneYearContribTemplate - a string template of the part of a query 442 | for one year. 443 | """ 444 | query = "query($owner: String!) {\n user(login: $owner) {\n" 445 | for y in yearList: 446 | query += oneYearContribTemplate.format(y) 447 | query += " }\n}\n" 448 | return query 449 | 450 | def parsePriorYearStats(self, queryResults): 451 | """Parses one year of commits, PR reviews, and restricted contributions. 452 | 453 | Keyword arguments: 454 | queryResults - The results of the query. 455 | """ 456 | queryResults = queryResults["data"]["user"] 457 | self._contrib["commits"][1] = sum( 458 | stats["totalCommitContributions"] for k, stats in queryResults.items()) 459 | self._contrib["reviews"][1] = sum( 460 | stats["totalPullRequestReviewContributions"] for k, stats in queryResults.items()) 461 | self._contrib["private"][1] = sum( 462 | stats["restrictedContributionsCount"] for k, stats in queryResults.items()) 463 | 464 | def executeQuery(self, query, needsPagination=False, failOnError=True): 465 | """Executes a GitHub GraphQl query using the GitHub CLI (gh). 466 | 467 | Keyword arguments: 468 | query - The query as a string. 469 | needsPagination - Pass True to enable pagination of query results. 470 | failOnError - If True, the workflow will fail if there is an error executing the 471 | query; and if False, this action will quietly exit with no error code. In 472 | either case, an error message will be logged to the console. 473 | """ 474 | if "GITHUB_REPOSITORY_OWNER" in os.environ: 475 | owner = os.environ["GITHUB_REPOSITORY_OWNER"] 476 | else: 477 | print("Error (7): Could not determine the repository owner.") 478 | set_outputs({"exit-code" : 7}) 479 | exit(7 if failOnError else 0) 480 | arguments = [ 481 | 'gh', 'api', 'graphql', 482 | '-F', 'owner=' + owner, 483 | '--cache', '1h', 484 | '-f', 'query=' + query 485 | ] 486 | if needsPagination: 487 | arguments.insert(5, '--paginate') 488 | result = subprocess.run( 489 | arguments, 490 | stdout=subprocess.PIPE, 491 | universal_newlines=True 492 | ).stdout.strip() 493 | numPages = result.count('"data"') 494 | if numPages == 0: 495 | # Check if any error details 496 | result = json.loads(result) if len(result) > 0 else "" 497 | if "errors" in result: 498 | print("Error (2): GitHub api Query failed with error:") 499 | print(result["errors"]) 500 | code = 2 501 | else: 502 | print("Error (3): Something unexpected occurred during GitHub API query.") 503 | code = 3 504 | set_outputs({"exit-code" : code}) 505 | exit(code if failOnError else 0) 506 | elif needsPagination: 507 | if (numPages > 1): 508 | result = result.replace('}{"data"', '},{"data"') 509 | result = "[" + result + "]" 510 | result = json.loads(result) 511 | failed = False 512 | errorMessage = None 513 | if ((not needsPagination) and 514 | (("data" not in result) or result["data"] == None)): 515 | failed = True 516 | if "errors" in result: 517 | errorMessage = result["errors"] 518 | elif needsPagination and ("data" not in result[0] or result[0]["data"] == None): 519 | failed = True 520 | if "errors" in result[0] : 521 | errorMessage = result[0]["errors"] 522 | if failed: 523 | print("Error (6): No data returned.") 524 | if errorMessage != None: 525 | print(errorMessage) 526 | set_outputs({"exit-code" : 6}) 527 | exit(6 if failOnError else 0) 528 | return result 529 | 530 | def ghDisableInteractivePrompts(self): 531 | """Disable gh's interactive prompts. This is probably unnecessary, 532 | as all of our testing so far, the queries run fine and don't produce any 533 | prompts. Disabling as a precaution in case some unexpected condition occurs 534 | that generates a prompt, so we don't accidentally leave a workflow waiting for 535 | user itneraction. 536 | """ 537 | result = subprocess.run( 538 | ["gh", "config", "set", "prompt", "disabled"], 539 | stdout=subprocess.PIPE, 540 | universal_newlines=True 541 | ).stdout.strip() 542 | 543 | -------------------------------------------------------------------------------- /src/StatsImageGenerator.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | from StatConfig import statsByCategory, loadLocale, icons 29 | from PieChart import svgPieChart 30 | from Colors import iconTemplates 31 | from ColorUtil import highContrastingColor 32 | from TextLength import calculateTextLength, calculateTextLength110Weighted 33 | import math 34 | 35 | class StatsImageGenerator: 36 | """Generates an svg image from the collected stats.""" 37 | 38 | headerTemplate = '' 39 | backgroundTemplate = '' 40 | fontGroup = '' 41 | titleTemplate = '{0}' 42 | groupHeaderTemplate = '' 43 | tableEntryTemplate = """ 44 | {1} 45 | 46 | {4} 47 | {6} 48 | {10} 49 | """ 50 | tableEntryTemplateOneColumn = """ 51 | {1} 52 | 53 | {4} 54 | {6} 55 | """ 56 | tableHeaderTemplate = """ 57 | 58 | {2} 59 | {4} 60 | {7} 61 | """ 62 | tableHeaderTemplateOneColumn = """ 63 | 64 | {2} 65 | {4} 66 | """ 67 | tableHeaderTemplateNoColumns = """ 68 | 69 | {2} 70 | """ 71 | languageEntryTemplate = """ 72 | 73 | {3} 74 | """ 75 | languageEntryTemplateTwoLangs = """ 76 | 77 | {3} 78 | 79 | {10} 80 | """ 81 | languageStringTemplate = "{0} {1:.2f}%" 82 | pieTransform = """{0}""" 83 | pieContrast = """""" 84 | 85 | __slots__ = [ 86 | '_stats', 87 | '_colors', 88 | '_height', 89 | '_width', 90 | '_rows', 91 | '_lineHeight', 92 | '_margin', 93 | '_locale', 94 | '_labels', 95 | '_radius', 96 | '_titleSize', 97 | '_pieRadius', 98 | '_highContrast', 99 | '_categoryOrder', 100 | '_animateLanguageChart', 101 | '_animationSpeed', 102 | '_firstColX', 103 | '_secondColX', 104 | '_title', 105 | '_includeTitle', 106 | '_exclude', 107 | '_topIconSize' 108 | ] 109 | 110 | def __init__(self, 111 | stats, 112 | colors, 113 | locale, 114 | radius, 115 | titleSize, 116 | categories, 117 | animateLanguageChart, 118 | animationSpeed, 119 | width, 120 | customTitle, 121 | includeTitle, 122 | exclude): 123 | """Initializes the StatsImageGenerator. 124 | 125 | Keyword arguments: 126 | stats - An object of the Statistician class. 127 | colors - A dictionary containing the color theme. 128 | locale - The 2-character locale code. 129 | radius - The border radius. 130 | titleSize - The font size for the title. 131 | categories - List of category keys in order they should 132 | appear on card. 133 | animateLanguageChart - Boolean controlling whether to animate 134 | the language pie chart. 135 | animationSpeed - An integer duration for one full rotation 136 | of language pie chart. 137 | width - The minimum width of the SVG, but will autosize larger 138 | as needed. 139 | customTitle - If not None, this is used as the title, otherwise 140 | title is formed from user's name. 141 | includeTitle - If True inserts a title. 142 | exclude - A set of keys to exclude. 143 | """ 144 | self._stats = stats 145 | self._colors = colors 146 | self._highContrast = highContrastingColor(self._colors["bg"]) 147 | self._locale = locale 148 | self._labels = loadLocale(self._locale) 149 | self._radius = radius 150 | self._titleSize = titleSize 151 | if customTitle != None: 152 | self._title = customTitle 153 | else: 154 | self._title = self._labels["titleTemplate"].format(self._stats._name) 155 | self._includeTitle = includeTitle 156 | self._topIconSize = 25 157 | self._categoryOrder = categories 158 | self._exclude = exclude 159 | self._animateLanguageChart = animateLanguageChart 160 | self._animationSpeed = animationSpeed 161 | self._margin = 15 # CAUTION: Some templates currently have margin hardcoded to 15 (refactor before changing here) 162 | self._height = 0 163 | self._width = max( 164 | width, 165 | self.calculateMinimumFeasibleWidth() 166 | ) 167 | self._firstColX = (self._width // 2) 168 | self._secondColX = self._firstColX + (self._width // 4) 169 | self._lineHeight = 21 170 | self._pieRadius = ( 171 | ((self._width // 2 - 2*self._margin) // self._lineHeight * self._lineHeight) - ( 172 | self._lineHeight - 16)) // 2 173 | self._rows = [ 174 | StatsImageGenerator.headerTemplate, 175 | StatsImageGenerator.backgroundTemplate, 176 | StatsImageGenerator.fontGroup 177 | ] 178 | 179 | def calculateMinimumFeasibleWidth(self): 180 | """Calculates the minimum feasible width for the 181 | SVG based on the lengths of the labels of the 182 | stats that are to be included, the category headings, 183 | and the title (if any), factoring in the chosen locale. 184 | """ 185 | length = 0 186 | if self._includeTitle: 187 | length = calculateTextLength( 188 | self._title, self._titleSize, True, 600) + 2 * self._margin 189 | if "title-icon" in self._colors: 190 | length += 2 * (self._topIconSize + self._margin) 191 | for category in self._categoryOrder: 192 | if category not in self._exclude: 193 | if category == "languages": 194 | languageData = self._stats.getStatsByKey(category) 195 | if languageData["totalSize"] > 0: 196 | headingRowLength = calculateTextLength( 197 | self._labels["categoryLabels"][category]["heading"], 198 | 14, 199 | True, 200 | 600) 201 | headingRowLength += 2 * self._margin 202 | length = max(length, headingRowLength) 203 | for lang in languageData["languages"]: 204 | langStr = StatsImageGenerator.languageStringTemplate.format( 205 | lang[0], 206 | 100 * lang[1]["percentage"] 207 | ) 208 | langRowLength = calculateTextLength( 209 | langStr, 210 | 14, 211 | True, 212 | 600 213 | ) 214 | length = max( 215 | length, 216 | (langRowLength + 25 + (2 * self._margin)) * 2 217 | ) 218 | else: 219 | keys = self.filterKeys( 220 | self._stats.getStatsByKey(category), 221 | statsByCategory[category] 222 | ) 223 | if len(keys) > 0: 224 | headerRow = self._labels["categoryLabels"][category] 225 | headingRowLength = calculateTextLength( 226 | headerRow["heading"], 227 | 14, 228 | True, 229 | 600) 230 | headingRowLength += 2 * self._margin 231 | if headerRow["column-one"] != None: 232 | headingRowLength *= 2 233 | length = max(length, headingRowLength) 234 | if headerRow["column-one"] != None: 235 | length = max( 236 | length, 237 | 4*(self._margin + calculateTextLength( 238 | headerRow["column-one"], 239 | 14, 240 | True, 241 | 600)) 242 | ) 243 | if headerRow["column-two"] != None: 244 | length = max( 245 | length, 246 | 4*(self._margin + calculateTextLength( 247 | headerRow["column-two"], 248 | 14, 249 | True, 250 | 600)) 251 | ) 252 | data = self._stats.getStatsByKey(category) 253 | for k in keys: 254 | labelLength = calculateTextLength( 255 | self._labels["statLabels"][k], 256 | 14, 257 | True, 258 | 600) 259 | length = max( 260 | length, 261 | (labelLength + 25 + (2 * self._margin)) * 2 262 | ) 263 | if len(data[k]) == 1 and not self.isInt(data[k][0]): 264 | dataLength = calculateTextLength( 265 | data[k][0], 266 | 14, 267 | True, 268 | 600) 269 | length = max( 270 | length, 271 | 2*(dataLength + (2 * self._margin)) 272 | ) 273 | return math.ceil(length) 274 | 275 | def generateImage(self): 276 | """Generates and returns the image.""" 277 | self.insertTitle() 278 | for category in self._categoryOrder: 279 | if category not in self._exclude: 280 | if category == "languages": 281 | self.insertLanguagesChart( 282 | self._stats.getStatsByKey(category), 283 | self._labels["categoryLabels"][category]["heading"] 284 | ) 285 | else: 286 | self.insertGroup( 287 | self._stats.getStatsByKey(category), 288 | self._labels["categoryLabels"][category], 289 | self.filterKeys( 290 | self._stats.getStatsByKey(category), 291 | statsByCategory[category] 292 | ) 293 | ) 294 | self.finalizeImageData() 295 | return "".join(self._rows).replace("\n", "") 296 | 297 | def filterKeys(self, data, keys): 298 | """Returns a list of the keys that have non-zero data and which are not excluded. 299 | 300 | Keyword arguments: 301 | data - The data (either contrib or repo data) 302 | keys - The list of keys relevant for the table. 303 | """ 304 | return [ 305 | k for k in keys if ( 306 | (k not in self._exclude) and (k in data) and ( 307 | (not self.isInt(data[k][0]) 308 | ) or data[k][0] > 0 or (len(data[k]) > 1 and data[k][1] > 0))) ] 309 | 310 | def isInt(self, value): 311 | """Checks if a value is an int. 312 | 313 | Keyword arguments: 314 | value - The value to check. 315 | """ 316 | try: 317 | int(value) 318 | except: 319 | return False 320 | return True 321 | 322 | def insertTitle(self): 323 | """Generates, formats, and inserts title.""" 324 | if self._includeTitle: 325 | scale = round(0.75 * self._titleSize / 110, 3) 326 | titleTextLength = round(calculateTextLength110Weighted(self._title, 600)) 327 | self._rows.append( 328 | StatsImageGenerator.titleTemplate.format( 329 | self._title, 330 | self._colors["title"], 331 | "{0:.3f}".format(scale), 332 | round(self._firstColX/scale - titleTextLength/2), #str(round(self._margin/scale)), 333 | str(round(37/scale)), 334 | titleTextLength 335 | ) 336 | ) 337 | if "title-icon" in self._colors: 338 | icon = iconTemplates[self._colors["title-icon"]] 339 | self._rows.append( 340 | icon.format( 341 | self._topIconSize, 342 | self._margin, 343 | self._margin, 344 | self._highContrast 345 | ) 346 | ) 347 | self._rows.append( 348 | icon.format( 349 | self._topIconSize, 350 | self._width - self._margin - self._topIconSize, 351 | self._margin, 352 | self._highContrast 353 | ) 354 | ) 355 | self._height += 39 356 | 357 | def insertGroup(self, data, headerRow, keys): 358 | """Generates the portion of the image for a group 359 | (i.e., the repositories section or the contributions section). 360 | If there are no keys with data, then this does nothing (excludes 361 | all in group). 362 | 363 | Keyword arguments: 364 | data - A dictionary with the data. 365 | headerRow - A dictionary with the header row text. Pass None for 366 | no table header. 367 | keys - A list of keys in the order they should appear. 368 | """ 369 | if len(keys) > 0: 370 | scale = round(0.75 * 14 / 110, 3) 371 | self._height += self._lineHeight 372 | self._rows.append( 373 | StatsImageGenerator.groupHeaderTemplate.format( 374 | self._height, 375 | self._colors["text"])) 376 | if headerRow != None: 377 | if headerRow["column-one"] == None: 378 | template = StatsImageGenerator.tableHeaderTemplateNoColumns 379 | elif headerRow["column-two"] == None: 380 | template = StatsImageGenerator.tableHeaderTemplateOneColumn 381 | else: 382 | template = StatsImageGenerator.tableHeaderTemplate 383 | self._rows.append(template.format( 384 | "{0:.3f}".format(scale), 385 | str(round(12.5/scale)), 386 | headerRow["heading"], 387 | round(calculateTextLength110Weighted(headerRow["heading"], 600)), 388 | headerRow["column-one"], 389 | str(round(self._firstColX/scale)), 390 | round(calculateTextLength110Weighted(headerRow["column-one"], 600)), 391 | headerRow["column-two"], 392 | str(round(self._secondColX/scale)), 393 | round(calculateTextLength110Weighted(headerRow["column-two"], 600)) 394 | )) 395 | offset = self._lineHeight 396 | else: 397 | offset = 0 398 | for k in keys: 399 | template = StatsImageGenerator.tableEntryTemplate if ( 400 | len(data[k]) > 1) else StatsImageGenerator.tableEntryTemplateOneColumn 401 | label = self._labels["statLabels"][k] 402 | data1 = str(self.formatCount(data[k][0])) 403 | data2 = str(self.formatCount(data[k][1])) if len(data[k]) > 1 else "" 404 | self._rows.append(template.format( 405 | str(offset), 406 | icons[k].format(self._colors["icons"]), 407 | "{0:.3f}".format(scale), 408 | str(round(12.5/scale)), 409 | label, 410 | str(round(25/scale)), 411 | data1, 412 | str(round(self._firstColX/scale)), 413 | round(calculateTextLength110Weighted(label, 600)), 414 | round(calculateTextLength110Weighted(data1, 600)), 415 | data2, 416 | str(round(self._secondColX/scale)), 417 | round(calculateTextLength110Weighted(data2, 600)) 418 | )) 419 | offset += self._lineHeight 420 | self._rows.append("") 421 | self._height += offset 422 | 423 | def insertLanguagesChart(self, languageData, categoryHeading): 424 | """Generates and returns the SVG section for the language 425 | distribution summary and pie chart. 426 | 427 | Keyword arguments: 428 | languageData - The language stats data 429 | categoryHeading - The heading for the section 430 | """ 431 | if languageData["totalSize"] > 0: 432 | scale = round(0.75 * 14 / 110, 3) 433 | self._height += self._lineHeight 434 | self._rows.append( 435 | StatsImageGenerator.groupHeaderTemplate.format( 436 | self._height, 437 | self._colors["text"] 438 | ) 439 | ) 440 | self._rows.append( 441 | StatsImageGenerator.tableHeaderTemplateNoColumns.format( 442 | "{0:.3f}".format(scale), 443 | str(round(12.5/scale)), 444 | categoryHeading, 445 | round(calculateTextLength110Weighted(categoryHeading, 600)) 446 | ) 447 | ) 448 | offset = self._lineHeight 449 | self._rows.append( 450 | StatsImageGenerator.pieContrast.format( 451 | self._pieRadius, 452 | str(offset), 453 | self._highContrast, 454 | self._firstColX + self._margin 455 | ) 456 | ) 457 | self._rows.append( 458 | StatsImageGenerator.pieTransform.format( 459 | svgPieChart( 460 | [L[1] for L in languageData["languages"]], 461 | self._pieRadius - 1, 462 | self._animateLanguageChart, 463 | self._animationSpeed 464 | ), 465 | str(offset+1), 466 | self._firstColX + self._margin + 1 467 | ) 468 | ) 469 | diameter = self._pieRadius * 2 470 | numRowsToLeft = round(diameter / self._lineHeight) 471 | for i, L in enumerate(languageData["languages"]): 472 | if i < numRowsToLeft: 473 | lang = StatsImageGenerator.languageStringTemplate.format( 474 | L[0], 475 | 100 * L[1]["percentage"] 476 | ) 477 | self._rows.append( 478 | StatsImageGenerator.languageEntryTemplate.format( 479 | str(offset), 480 | L[1]["color"], 481 | self._highContrast, 482 | lang, 483 | "{0:.3f}".format(scale), 484 | str(round(25/scale)), 485 | str(round(12.5/scale)), 486 | round(calculateTextLength110Weighted(lang, 600)) 487 | ) 488 | ) 489 | offset += self._lineHeight 490 | else: 491 | break 492 | for j in range(numRowsToLeft, len(languageData["languages"]), 2): 493 | L = languageData["languages"][j] 494 | lang = StatsImageGenerator.languageStringTemplate.format( 495 | L[0], 496 | 100 * L[1]["percentage"] 497 | ) 498 | if j+1 < len(languageData["languages"]): 499 | L2 = languageData["languages"][j+1] 500 | lang2 = StatsImageGenerator.languageStringTemplate.format( 501 | L2[0], 502 | 100 * L2[1]["percentage"] 503 | ) 504 | self._rows.append( 505 | StatsImageGenerator.languageEntryTemplateTwoLangs.format( 506 | str(offset), 507 | L[1]["color"], 508 | self._highContrast, 509 | lang, 510 | "{0:.3f}".format(scale), 511 | str(round(25/scale)), 512 | str(round(12.5/scale)), 513 | round(calculateTextLength110Weighted(lang, 600)), 514 | L2[1]["color"], 515 | self._firstColX + 0.5, 516 | lang2, 517 | str(round((self._firstColX + 25)/scale)), 518 | round(calculateTextLength110Weighted(lang2, 600)) 519 | ) 520 | ) 521 | offset += self._lineHeight 522 | else: 523 | self._rows.append( 524 | StatsImageGenerator.languageEntryTemplate.format( 525 | str(offset), 526 | L[1]["color"], 527 | self._highContrast, 528 | lang, 529 | "{0:.3f}".format(scale), 530 | str(round(25/scale)), 531 | str(round(12.5/scale)), 532 | round(calculateTextLength110Weighted(lang, 600)) 533 | ) 534 | ) 535 | offset += self._lineHeight 536 | self._rows.append("") 537 | if diameter + self._lineHeight + self._lineHeight - self._margin - 1 <= offset: 538 | self._height += offset 539 | else: 540 | self._height += diameter + self._lineHeight + self._lineHeight - self._margin - 1 541 | 542 | def formatCount(self, count): 543 | """Formats the count. 544 | 545 | Keyword arguments: 546 | count - The count to format. 547 | """ 548 | if (not self.isInt(count)) or count < 100000: 549 | return count 550 | elif count < 1000000: 551 | return "{0:.1f}K".format(count // 100 * 100 / 1000) 552 | else: 553 | # can such a real user exist? 554 | return "{0:.1f}M".format(count // 100000 * 100000 / 1000000) 555 | 556 | def finalizeImageData(self): 557 | """Inserts the height into the svg opening tag and the rect for the background. 558 | Also inserts the border and background colors into the rect for the background. 559 | Must be called after generating the rest of the image since we won't know 560 | height until the end. Also inserts closing tags. 561 | """ 562 | self._height += self._lineHeight 563 | self._rows[0] = self._rows[0].format(str(self._height), str(self._width), self._locale) 564 | self._rows[1] = self._rows[1].format( 565 | str(self._height - 4), 566 | self._colors["border"], 567 | self._colors["bg"], 568 | str(self._width - 4), 569 | self._radius 570 | ) 571 | self._rows.append("\n\n") 572 | 573 | -------------------------------------------------------------------------------- /src/UserStatistician.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -B 2 | # 3 | # user-statistician: Github action for generating a user stats card 4 | # 5 | # Copyright (c) 2021-2024 Vincent A Cicirello 6 | # https://www.cicirello.org/ 7 | # 8 | # MIT License 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all 18 | # copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | # 28 | 29 | from Statistician import Statistician, set_outputs 30 | from Colors import colorMapping, iconTemplates 31 | from StatsImageGenerator import StatsImageGenerator 32 | from StatConfig import supportedLocales, categoryOrder 33 | import sys 34 | import os 35 | import subprocess 36 | 37 | def writeImageToFile(filename, image, failOnError): 38 | """Writes the image to a file, creating any 39 | missing directories from the path. 40 | 41 | Keyword arguments: 42 | filename - The filename for the image, with complete path. 43 | image - A string containing the image. 44 | failOnError - If True, the workflow will fail if there is an error 45 | writing the image to a file; and if False, this action will quietly 46 | exit with no error code. In either case, an error message will be 47 | logged to the console. 48 | """ 49 | # Since we're running in a docker container, everything runs 50 | # as root. We need this umask call so we'll have write permissions 51 | # once the action finished and we're outside the container again. 52 | os.umask(0) 53 | # Create the directory if it doesn't exist. 54 | directoryName = os.path.dirname(filename) 55 | if len(directoryName) > 0: 56 | os.makedirs(directoryName, exist_ok=True, mode=0o777) 57 | try: 58 | # Write the image to a file 59 | with open(filename, "wb") as file: 60 | image = image.encode(encoding="UTF-8") 61 | file.write(image) 62 | except IOError: 63 | print("Error (4): An error occurred while writing the image to a file.") 64 | set_outputs({"exit-code" : 4}) 65 | exit(4 if failOnError else 0) 66 | 67 | def executeCommand(arguments): 68 | """Execute a subprocess and return result and exit code. 69 | 70 | Keyword arguments: 71 | arguments - The arguments for the command. 72 | """ 73 | result = subprocess.run( 74 | arguments, 75 | stdout=subprocess.PIPE, 76 | universal_newlines=True 77 | ) 78 | return result.stdout.strip(), result.returncode 79 | 80 | def commitAndPush(filename, name, login, failOnError, commit_message): 81 | """Commits and pushes the image. 82 | 83 | Keyword arguments: 84 | filename - The path to the image. 85 | name - The user's name. 86 | login - The user's login id. 87 | failOnError - Boolean controlling whether or not to fail the run if an error occurs. 88 | commit_message - Message for the commit 89 | """ 90 | # Resolve issue related to user in Docker container vs owner of repository 91 | executeCommand( 92 | ["git", "config", "--global", "--add", "safe.directory", "/github/workspace"]) 93 | # Make sure this isn't being run during a pull-request. 94 | result = executeCommand( 95 | ["git", "symbolic-ref", "-q", "HEAD"]) 96 | if result[1] == 0: 97 | # Check if the image changed 98 | result = executeCommand( 99 | ["git", "status", "--porcelain", filename]) 100 | if len(result[0]) > 0: 101 | # Commit and push 102 | executeCommand( 103 | ["git", "config", "--global", "user.name", name]) 104 | executeCommand( 105 | ["git", "config", "--global", "user.email", 106 | login + '@users.noreply.github.com']) 107 | executeCommand(["git", "add", filename]) 108 | executeCommand(["git", "commit", "-m", 109 | commit_message, 110 | filename]) 111 | r = executeCommand(["git", "push"]) 112 | if r[1] != 0: 113 | print("Error (5): push failed.") 114 | set_outputs({"exit-code" : 5}) 115 | exit(5 if failOnError else 0) 116 | 117 | 118 | if __name__ == "__main__": 119 | 120 | imageFilenameWithPath = sys.argv[1].strip() 121 | 122 | includeTitle = sys.argv[2].strip().lower() == "true" 123 | 124 | customTitle = sys.argv[3].strip() 125 | if len(customTitle) == 0 or not includeTitle: 126 | customTitle = None 127 | 128 | colors = sys.argv[4].strip().replace(",", " ").split() 129 | if len(colors) == 1 and colors[0] in colorMapping: 130 | # get theme colors 131 | colors = colorMapping[colors[0]] 132 | elif len(colors) < 4: 133 | # default to light theme if invalid number of colors passed 134 | colors = colorMapping["light"] 135 | else: 136 | colors = { 137 | "title-icon" : "github", 138 | "bg" : colors[0], 139 | "border" : colors[1], 140 | "icons" : colors[2], 141 | "title" : colors[3], 142 | "text" : colors[4] if len(colors) > 4 else colors[3] 143 | } 144 | 145 | exclude = set(sys.argv[5].strip().replace(",", " ").split()) 146 | 147 | failOnError = sys.argv[6].strip().lower() == "true" 148 | 149 | commit = sys.argv[7].strip().lower() == "true" 150 | 151 | locale = sys.argv[8].strip().lower() 152 | if locale not in supportedLocales: 153 | locale = "en" 154 | 155 | radius = int(sys.argv[9]) 156 | 157 | showBorder = sys.argv[10].strip().lower() == "true" 158 | if not showBorder : 159 | radius = 0 160 | colors["border"] = colors["bg"] 161 | 162 | smallTitle = sys.argv[11].strip().lower() == "true" 163 | if smallTitle : 164 | titleSize = 16 165 | else : 166 | titleSize = 18 167 | 168 | maxLanguages = sys.argv[12].strip().lower() 169 | autoLanguages = maxLanguages == "auto" 170 | if not autoLanguages: 171 | maxLanguages = int(maxLanguages) 172 | else: 173 | maxLanguages = 1000 # doesn't really matter, but should be an int 174 | 175 | categories = sys.argv[13].strip().replace(",", " ").lower().split() 176 | validCategoryKeys = set(categoryOrder) 177 | categories = [ c for c in categories if c in validCategoryKeys] 178 | if len(categories) == 0 : 179 | categories = categoryOrder 180 | 181 | languageRepoExclusions = set( 182 | sys.argv[14].strip().replace(",", " ").lower().split()) 183 | 184 | featuredRepo = sys.argv[15].strip() 185 | if len(featuredRepo) == 0: 186 | featuredRepo = None 187 | 188 | animateLanguageChart = sys.argv[16].strip().lower() == "true" 189 | animationSpeed = int(sys.argv[17].strip()) 190 | 191 | width = int(sys.argv[18].strip()) 192 | 193 | topIcon = sys.argv[19].strip().lower() 194 | if topIcon == "none": 195 | colors.pop("title-icon", None) 196 | elif topIcon != "default" and topIcon in iconTemplates: 197 | colors["title-icon"] = topIcon 198 | 199 | commit_message = sys.argv[20].strip() 200 | 201 | stats = Statistician( 202 | failOnError, 203 | autoLanguages, 204 | maxLanguages, 205 | languageRepoExclusions, 206 | featuredRepo 207 | ) 208 | generator = StatsImageGenerator( 209 | stats, 210 | colors, 211 | locale, 212 | radius, 213 | titleSize, 214 | categories, 215 | animateLanguageChart, 216 | animationSpeed, 217 | width, 218 | customTitle, 219 | includeTitle, 220 | exclude 221 | ) 222 | image = generator.generateImage() 223 | writeImageToFile(imageFilenameWithPath, image, failOnError) 224 | 225 | if commit: 226 | commitAndPush( 227 | imageFilenameWithPath, 228 | "github-actions", 229 | "41898282+github-actions[bot]", 230 | failOnError, 231 | commit_message) 232 | 233 | set_outputs({"exit-code" : 0}) 234 | 235 | -------------------------------------------------------------------------------- /src/locales/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} এর গিটহাব কার্যকলাপ", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "সাধারণ পরিসংখ্যান এবং তথ্য", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "সংগ্রহস্থল", 11 | "column-one": "অ-কাঁটা", 12 | "column-two": "সব" 13 | }, 14 | "contributions": { 15 | "heading": "অবদানসমূহ", 16 | "column-one": "বিগত বছর", 17 | "column-two": "মোট" 18 | }, 19 | "languages": { 20 | "heading": "প্রকাশ্য ভান্ডারে ভাষা বিতরণ", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "যোগদানের বছর", 27 | "featured": "বৈশিষ্ট্যযুক্ত রেপো", 28 | "mostStarred": "সর্বাধিক তারকা প্রাপ্ত রেপো", 29 | "mostForked": "সর্বাধিক ফর্কড রেপো", 30 | "followers": "অনুসারী", 31 | "following": "অনুসরণ করছে", 32 | "sponsors": "পৃষ্ঠপোষক", 33 | "sponsoring": "পৃষ্ঠপোষকতা", 34 | "public": "ভাণ্ডার মালিকানাধীন", 35 | "starredBy": "তারকা প্রদান করেছে", 36 | "forkedBy": "ফোর্ক করেছে", 37 | "watchedBy": "দেখেছেন", 38 | "templates": "টেমপ্লেট সমুহ", 39 | "archived": "সংরক্ষণাগারভুক্ত", 40 | "commits": "কমিট করে", 41 | "issues": "ইস্যু", 42 | "prs": "অনুরোধগুলি টানুন", 43 | "reviews": "অনুরোধ টানার পর্যালোচনাগুলি", 44 | "contribTo": "অবদান", 45 | "private": "ব্যক্তিগত অবদান" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}ova GitHub aktivita", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Obecné statistiky a informace", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repozitáře", 11 | "column-one": "Neodvozené", 12 | "column-two": "Vše" 13 | }, 14 | "contributions": { 15 | "heading": "Příspěvky", 16 | "column-one": "Minulý rok", 17 | "column-two": "Celkem" 18 | }, 19 | "languages": { 20 | "heading": "Distribuce jazyků ve veřejných repozitářích", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Rok připojení", 27 | "featured": "Vyznačený repozitář", 28 | "mostStarred": "Nejvíce hvězdičkový repozitář", 29 | "mostForked": "Nejvíce odvozený repozitář", 30 | "followers": "Následovníci", 31 | "following": "Sleduji", 32 | "sponsors": "Sponzoři", 33 | "sponsoring": "Sponzorování", 34 | "public": "Mé repozitáře", 35 | "starredBy": "Hvězdičkované od", 36 | "forkedBy": "Odvozené od", 37 | "watchedBy": "Sledované od", 38 | "templates": "Šablony", 39 | "archived": "Archivované", 40 | "commits": "Závazky", 41 | "issues": "Problémy", 42 | "prs": "Žádosti o sloučení", 43 | "reviews": "Recenze žádostí o sloučení", 44 | "contribTo": "Přispěno do", 45 | "private": "Soukromé příspěvky" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}s GitHub Aktivität", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Allgemeine Statistiken und Informationen", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositories", 11 | "column-one": "Non-Forks", 12 | "column-two": "Alle" 13 | }, 14 | "contributions": { 15 | "heading": "Beiträge", 16 | "column-one": "Letztes Jahr", 17 | "column-two": "Gesamt" 18 | }, 19 | "languages": { 20 | "heading": "Verteilung der Sprachen in Öffentlichen Repositories", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Beitrittsdatum", 27 | "featured": "Vorgestelltes Repo", 28 | "mostStarred": "Meistmarkiertes Repo", 29 | "mostForked": "Meistgeforktes Repo", 30 | "followers": "Follower", 31 | "following": "Folgt", 32 | "sponsors": "Sponsoren", 33 | "sponsoring": "Sponsoring", 34 | "public": "Eigene Repositories", 35 | "starredBy": "Markiert Von", 36 | "forkedBy": "Geforkt Von", 37 | "watchedBy": "Verfolgt Von", 38 | "templates": "Vorlagen", 39 | "archived": "Archiviert", 40 | "commits": "Commits", 41 | "issues": "Issues", 42 | "prs": "Pull Requests", 43 | "reviews": "Überprüfungen von Pull Requests", 44 | "contribTo": "Beigetragen Zu", 45 | "private": "Private Beiträge" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Δραστηριότητα GitHub του {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Γενικά Στατιστικά και Πληροφορίες", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Αποθετήρια", 11 | "column-one": "Μη Forks", 12 | "column-two": "Όλα" 13 | }, 14 | "contributions": { 15 | "heading": "Συνεισφορές", 16 | "column-one": "Τελευταίος Χρόνος", 17 | "column-two": "Σύνολο" 18 | }, 19 | "languages": { 20 | "heading": "Κατανομή Γλωσσών στα Δημόσια Αποθετήρια", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Έτος Εγγραφής", 27 | "featured": "Προτεινόμενο Αποθετήριο", 28 | "mostStarred": "Αποθετήριο με τα Περισσότερα Αστέρια", 29 | "mostForked": "Αποθετήριο με τα Περισσότερα Forks", 30 | "followers": "Ακόλουθοι", 31 | "following": "Ακολουθεί", 32 | "sponsors": "Χορηγοί", 33 | "sponsoring": "Χορηγεί", 34 | "public": "Τα Αποθετήρια Μου", 35 | "starredBy": "Προτιμήθηκε Από", 36 | "forkedBy": "Fork Από", 37 | "watchedBy": "Παρακολουθήθηκε Από", 38 | "templates": "Πρότυπα", 39 | "archived": "Αρχειοθετημένα", 40 | "commits": "Commits", 41 | "issues": "Θέματα", 42 | "prs": "Αιτήματα Έλξης", 43 | "reviews": "Αξιολογήσεις Αιτημάτων Έλξης", 44 | "contribTo": "Συνεισφορά Σε", 45 | "private": "Ιδιωτικές Συνεισφορές" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}'s GitHub Activity", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "General Stats and Info", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositories", 11 | "column-one": "Non-Forks", 12 | "column-two": "All" 13 | }, 14 | "contributions": { 15 | "heading": "Contributions", 16 | "column-one": "Past Year", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Language Distribution in Public Repositories", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Year Joined", 27 | "featured": "Featured Repo", 28 | "mostStarred": "Most Starred Repo", 29 | "mostForked": "Most Forked Repo", 30 | "followers": "Followers", 31 | "following": "Following", 32 | "sponsors": "Sponsors", 33 | "sponsoring": "Sponsoring", 34 | "public": "My Repositories", 35 | "starredBy": "Starred By", 36 | "forkedBy": "Forked By", 37 | "watchedBy": "Watched By", 38 | "templates": "Templates", 39 | "archived": "Archived", 40 | "commits": "Commits", 41 | "issues": "Issues", 42 | "prs": "Pull Requests", 43 | "reviews": "Pull Request Reviews", 44 | "contribTo": "Contributed To", 45 | "private": "Private Contributions" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Actividad en GitHub de {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Estadísticas generales e información", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositorios", 11 | "column-one": "No bifurcados", 12 | "column-two": "Todos" 13 | }, 14 | "contributions": { 15 | "heading": "Contribuciones", 16 | "column-one": "Año pasado", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Distribución de lenguajes en repositorios públicos", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Año de ingreso", 27 | "featured": "Repositorio destacado", 28 | "mostStarred": "Repositorio con más estrellas", 29 | "mostForked": "Repositorio más bifurcado", 30 | "followers": "Seguidores", 31 | "following": "Siguiendo", 32 | "sponsors": "Patrocinadores", 33 | "sponsoring": "Patrocinando", 34 | "public": "Repositorios propios", 35 | "starredBy": "Con estrella por", 36 | "forkedBy": "Bifurcado por", 37 | "watchedBy": "Visto por", 38 | "templates": "Plantillas", 39 | "archived": "Archivado", 40 | "commits": "Commits", 41 | "issues": "Problemas", 42 | "prs": "Pull Requests", 43 | "reviews": "Revisiones de Pull Requests", 44 | "contribTo": "Contribuido a", 45 | "private": "Contribuciones privadas" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "عملکرد گیت‌هاب {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "آمار و اطلاعات کلی", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "ریپازیتوری ها", 11 | "column-one": "فورک نشده", 12 | "column-two": "همه" 13 | }, 14 | "contributions": { 15 | "heading": "همکاری ها", 16 | "column-one": "سال گذشته", 17 | "column-two": "مجموع" 18 | }, 19 | "languages": { 20 | "heading": "پراکندگی زبان ها در ریپو های عمومی", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "سال عضویت", 27 | "featured": "ریپازیتوری برجسته", 28 | "mostStarred": "ریپو با بیشترین ستاره", 29 | "mostForked": "ریپو با بیشترین فورک", 30 | "followers": "دنبال کنندگان", 31 | "following": "دنبال می‌کند", 32 | "sponsors": "اسپانسر ها", 33 | "sponsoring": "اسپانسر می‌کند", 34 | "public": "ریپازیتوری ها", 35 | "starredBy": "ستاره کنندگان", 36 | "forkedBy": "فورک کنندگان", 37 | "watchedBy": "بینندگان", 38 | "templates": "قالب ها", 39 | "archived": "آرشیو ها", 40 | "commits": "کامیت ها", 41 | "issues": "موضوعات", 42 | "prs": "درخواست های کشش", 43 | "reviews": "مرور درخواست های کشش", 44 | "contribTo": "همکاری کرده با", 45 | "private": "همکاری های خصوصی" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Käyttäjän {0} GitHub-toiminto", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Yleiset tilastot ja tiedot", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Tietovarastot", 11 | "column-one": "Ei-haarukat", 12 | "column-two": "Kaikki" 13 | }, 14 | "contributions": { 15 | "heading": "Avustukset", 16 | "column-one": "Viime vuosi", 17 | "column-two": "Kaikki yhteensä" 18 | }, 19 | "languages": { 20 | "heading": "Kielten jakelu julkisissa arkistoissa", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Vuosi liittyi", 27 | "featured": "Suositeltu Repo", 28 | "mostStarred": "Tähdellä merkityin Repo", 29 | "mostForked": "Useimmat Forked Repo", 30 | "followers": "Seuraajat", 31 | "following": "Seurata", 32 | "sponsors": "Sponsorit", 33 | "sponsoring": "Sponsorointi", 34 | "public": "Omat arkistot", 35 | "starredBy": "tähdellä", 36 | "forkedBy": "Haaroittunut", 37 | "watchedBy": "Katsonut", 38 | "templates": "Mallit", 39 | "archived": "Arkistoitu", 40 | "commits": "Sitoutuu", 41 | "issues": "ongelmia", 42 | "prs": "Vedä pyyntöjä", 43 | "reviews": "Pyydä arvosteluja", 44 | "contribTo": "Osallistunut", 45 | "private": "Yksityiset lahjoitukset" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Activité GitHub de {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Statistiques Générales et Info", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Dépôts", 11 | "column-one": "Non clonés", 12 | "column-two": "Tout" 13 | }, 14 | "contributions": { 15 | "heading": "Contributions", 16 | "column-one": "Dernière année", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Répartition des langages dans les dépôts publiques", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Année d'adhésion", 27 | "featured": "Dépôt en vedette", 28 | "mostStarred": "Dépôt le plus étoilé", 29 | "mostForked": "Dépôt le plus cloné", 30 | "followers": "Abonnés", 31 | "following": "Abonnements", 32 | "sponsors": "Sponsors", 33 | "sponsoring": "Sponsorise", 34 | "public": "Dépôts possédés", 35 | "starredBy": "Étoilé par", 36 | "forkedBy": "Cloné par", 37 | "watchedBy": "Regardé par", 38 | "templates": "Modèles", 39 | "archived": "Archivé", 40 | "commits": "Commits", 41 | "issues": "Issues", 42 | "prs": "Pull Requests", 43 | "reviews": "Révision de Pull Request", 44 | "contribTo": "Contribué à", 45 | "private": "Contributions privées" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} की गिटहब गतिविधि", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "साधारण सांख्यिकी और सूचना", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "भंडार", 11 | "column-one": "गैर-फोर्क", 12 | "column-two": "सभी" 13 | }, 14 | "contributions": { 15 | "heading": "योगदान", 16 | "column-one": "पिछला वर्ष", 17 | "column-two": "कुल" 18 | }, 19 | "languages": { 20 | "heading": "सार्वजनिक भंडारों में भाषा वितरण", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "युक्त होने का वर्ष", 27 | "featured": "विशेष रुप से प्रदर्शित भंडार", 28 | "mostStarred": "सर्वाधिक तारांकित भंडार", 29 | "mostForked": "सर्वाधिक फोर्क भंडार", 30 | "followers": "समर्थक", 31 | "following": "अनुगामी", 32 | "sponsors": "प्रायोजक", 33 | "sponsoring": "प्रायोजन", 34 | "public": "अपना भंडार", 35 | "starredBy": "किसके द्वारा तारांकित", 36 | "forkedBy": "किसके द्वारा फोर्क किया गया", 37 | "watchedBy": "किसके द्वारा देखा गया", 38 | "templates": "आकार पट्ट", 39 | "archived": "संग्रहीत", 40 | "commits": "प्रतिबद्ध", 41 | "issues": "मुद्दे", 42 | "prs": "अनुरोध", 43 | "reviews": "अनुरोध समीक्षा", 44 | "contribTo": "योगदान", 45 | "private": "गुप्त योगदान" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} GitHub aktivitása", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Általános statisztika és információ", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repository-k", 11 | "column-one": "Non-Fork-ok", 12 | "column-two": "Mind" 13 | }, 14 | "contributions": { 15 | "heading": "Kontribúciók", 16 | "column-one": "Elmúlt év", 17 | "column-two": "Összesen" 18 | }, 19 | "languages": { 20 | "heading": "Nyelvek eloszlása nyilvános repository-kban", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Csatlakozás éve", 27 | "featured": "Kiemelt repo", 28 | "mostStarred": "Legtöbbet csillagozott repo", 29 | "mostForked": "Legtöbbet fork-olt repo", 30 | "followers": "Követői", 31 | "following": "Követi", 32 | "sponsors": "Szponzorok", 33 | "sponsoring": "Szponzorál", 34 | "public": "Saját repository-k", 35 | "starredBy": "Csillagozta", 36 | "forkedBy": "Forkolta", 37 | "watchedBy": "Figyeli", 38 | "templates": "Sablonok", 39 | "archived": "Archiválva", 40 | "commits": "Commitok", 41 | "issues": "Issue-k", 42 | "prs": "Pull request-ek", 43 | "reviews": "Pull request review-k", 44 | "contribTo": "Kontribútolt", 45 | "private": "Privát kontribúciók" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/hy.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}-ի GitHub գործունեությունը", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Ընդհանուր Տեղեկություն և Տվյալներ", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Ռեպոզիտորիաներ", 11 | "column-one": "Ոչ պատառաքաղներ", 12 | "column-two": "Բոլորը" 13 | }, 14 | "contributions": { 15 | "heading": "Ներդրումներ", 16 | "column-one": "Վերջին տարի", 17 | "column-two": "Ընդամենը" 18 | }, 19 | "languages": { 20 | "heading": "Լեզուների բաշխում հանրային շտեմարաններում", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Գրանցվել է Տարին", 27 | "featured": "Առաջարկվող ռեպո", 28 | "mostStarred": "Ամենաաստղային ռեպո", 29 | "mostForked": "Առավել ճեղքված պահոց", 30 | "followers": "Հետևորդներ", 31 | "following": "Հետևելով", 32 | "sponsors": "Հովանավորներ", 33 | "sponsoring": "Հովանավորություն", 34 | "public": "Իմ Ռեպոզիտորիաներ", 35 | "starredBy": "Աստղանշված է", 36 | "forkedBy": "Արտադրված է կողմից", 37 | "watchedBy": "Դիտել է", 38 | "templates": "Կաղապարներ", 39 | "archived": "Արխիվացված", 40 | "commits": "Պարտավորվում է", 41 | "issues": "Հարցեր", 42 | "prs": "Քաշեք հարցումներ", 43 | "reviews": "Քաշեք հարցումների վերանայումները", 44 | "contribTo": "Նպաստել է.", 45 | "private": "Մասնավոր ներդրումներ." 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Aktivitas Github {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Info dan Status Umum", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositori", 11 | "column-one": "Non Fork", 12 | "column-two": "Semua" 13 | }, 14 | "contributions": { 15 | "heading": "Kontribusi", 16 | "column-one": "Tahun Lalu", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Distribusi Bahasa dalam Repositori Publik", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Tahun Bergabung", 27 | "featured": "Repositori Unggulan", 28 | "mostStarred": "Repositori dengan Bintang Terbanyak", 29 | "mostForked": "Repositori dengan Fork Terbanyak", 30 | "followers": "Pengikut", 31 | "following": "Mengikuti", 32 | "sponsors": "Sponsor", 33 | "sponsoring": "Mensponsori", 34 | "public": "Repositori yang Dimiliki", 35 | "starredBy": "Diberikan bintang oleh", 36 | "forkedBy": "Di-fork oleh", 37 | "watchedBy": "Dilihat oleh", 38 | "templates": "Template", 39 | "archived": "Diarsipkan", 40 | "commits": "Commits", 41 | "issues": "Isu", 42 | "prs": "Pull Requests", 43 | "reviews": "Ulasan Pull Request", 44 | "contribTo": "Berkontribusi Ke", 45 | "private": "Kontribusi Pribadi" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Attività GitHub di {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Statistiche Generali e Informazioni", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repository", 11 | "column-one": "Non-Fork", 12 | "column-two": "Tutti" 13 | }, 14 | "contributions": { 15 | "heading": "Contributi", 16 | "column-one": "Anno Scorso", 17 | "column-two": "Totale" 18 | }, 19 | "languages": { 20 | "heading": "Distribuzione del Linguaggio nei Repository Pubblici", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Anno di Iscrizione", 27 | "featured": "Repo in Primo Piano", 28 | "mostStarred": "Repo con più Stelle", 29 | "mostForked": "Repo con più Fork", 30 | "followers": "Seguaci", 31 | "following": "Seguendo", 32 | "sponsors": "Sponsors", 33 | "sponsoring": "Sponsorizza", 34 | "public": "Repository di Proprietà", 35 | "starredBy": "Stellato Da", 36 | "forkedBy": "Forkato Da", 37 | "watchedBy": "Seguito Da", 38 | "templates": "Modelli", 39 | "archived": "Archiviato", 40 | "commits": "Commits", 41 | "issues": "Problemi", 42 | "prs": "Richieste di Pull", 43 | "reviews": "Revisioni di Richieste di Pull", 44 | "contribTo": "Contribuito A", 45 | "private": "Contributi Privati" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}のgithubアクティビティ", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "一般的な統計と情報", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "リポジトリ", 11 | "column-one": "非フォーク", 12 | "column-two": "全て" 13 | }, 14 | "contributions": { 15 | "heading": "貢献", 16 | "column-one": "昨年", 17 | "column-two": "合計" 18 | }, 19 | "languages": { 20 | "heading": "公開リポジトリでの言語配布", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "入社年", 27 | "featured": "注目のリポジトリ", 28 | "mostStarred": "最もスター付きのリポジトリ", 29 | "mostForked": "最もフォークされたリポジトリ", 30 | "followers": "フォロワー", 31 | "following": "続く", 32 | "sponsors": "スポンサー", 33 | "sponsoring": "主催", 34 | "public": "所有リポジトリ", 35 | "starredBy": "主演", 36 | "forkedBy": "によるフォーク", 37 | "watchedBy": "によって見られた", 38 | "templates": "レンプレート", 39 | "archived": "記録", 40 | "commits": "専念", 41 | "issues": "問題", 42 | "prs": "プルリクエスト", 43 | "reviews": "プルリクエストレビュー", 44 | "contribTo": "に貢献しました", 45 | "private": "個人的な貢献" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}의 GitHub 활동", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "통계 및 정보", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "저장소", 11 | "column-one": "직접 만든(Non-Forks)", 12 | "column-two": "모두" 13 | }, 14 | "contributions": { 15 | "heading": "기여", 16 | "column-one": "지난해", 17 | "column-two": "총" 18 | }, 19 | "languages": { 20 | "heading": "공개 저장소 사용 언어 분포", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "가입 년도", 27 | "featured": "추천 저장소", 28 | "mostStarred": "Star를 가장 많이 받은 저장소", 29 | "mostForked": "Fork가 가장 많이된 저장소", 30 | "followers": "팔로워", 31 | "following": "팔로잉", 32 | "sponsors": "후원받은", 33 | "sponsoring": "후원하는", 34 | "public": "보유한 저장소", 35 | "starredBy": "받은 Star", 36 | "forkedBy": "Fork된 횟수", 37 | "watchedBy": "Watch된 횟수", 38 | "templates": "템플릿", 39 | "archived": "보관 처리된(Archived) 저장소", 40 | "commits": "커밋", 41 | "issues": "이슈", 42 | "prs": "풀 리퀘스트", 43 | "reviews": "리뷰", 44 | "contribTo": "기여 횟수", 45 | "private": "비공개" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/lt.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} aktyvumas GitHub", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Bendra statistika ir informacija", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repozitorijos", 11 | "column-one": "Neklonuotos", 12 | "column-two": "Visos" 13 | }, 14 | "contributions": { 15 | "heading": "Įnašai", 16 | "column-one": "Praeitais metais", 17 | "column-two": "Viso" 18 | }, 19 | "languages": { 20 | "heading": "Kalbu pasiskirstymas viešosiose repozitorijose", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Prisijungimo metai", 27 | "featured": "Siūloma repozitorija", 28 | "mostStarred": "Labiausiai pažymėta repozitorija", 29 | "mostForked": "Labiausiai klonuota repozitorija", 30 | "followers": "Sekėjai", 31 | "following": "Sekama", 32 | "sponsors": "Remėjai", 33 | "sponsoring": "Remiama", 34 | "public": "Priklausančios repozitorijos", 35 | "starredBy": "Pažymėta", 36 | "forkedBy": "Klonuota", 37 | "watchedBy": "Stebima", 38 | "templates": "Šablonai", 39 | "archived": "Archyvuota", 40 | "commits": "Commits", 41 | "issues": "Problemos", 42 | "prs": "Pull Prašymai", 43 | "reviews": "Pull prašymų peržiūros", 44 | "contribTo": "Prisidėjo prie", 45 | "private": "Privatūs įnašai" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/ml.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}-ന്റെ GitHub പ്രവർത്തനം", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "പൊതുവായ സ്ഥിതിവിവരക്കണക്കുകളും വിവരങ്ങളും", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "ശേഖരങ്ങൾ", 11 | "column-one": "നോൺ ഫോർക്കുകൾ", 12 | "column-two": "എല്ലാം" 13 | }, 14 | "contributions": { 15 | "heading": "സംഭാവനകൾ", 16 | "column-one": "കഴിഞ്ഞ വർഷം", 17 | "column-two": "ആകെ" 18 | }, 19 | "languages": { 20 | "heading": "പൊതു സംഭരണികളിലെ ഭാഷാ വിതരണം", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "ചേർന്ന വർഷം", 27 | "featured": "ഫീച്ചർ ചെയ്ത റിപ്പോ", 28 | "mostStarred": "ഏറ്റവും കൂടുതൽ നക്ഷത്രമിട്ട റിപ്പോ", 29 | "mostForked": "മോസ്റ്റ് ഫോർക്ക്ഡ് റിപ്പോ", 30 | "followers": "അനുയായികൾ", 31 | "following": "പിന്തുടരുന്നു", 32 | "sponsors": "സ്പോൺസർമാർ", 33 | "sponsoring": "സ്പോൺസർ ചെയ്യുന്നു", 34 | "public": "എന്റെ ശേഖരങ്ങൾ", 35 | "starredBy": "അഭിനയിച്ചത്", 36 | "forkedBy": "ഫോർക്ക്ഡ് ബൈ", 37 | "watchedBy": "വീക്ഷിച്ചത്", 38 | "templates": "ടെംപ്ലേറ്റുകൾ", 39 | "archived": "ആർക്കൈവ് ചെയ്തു", 40 | "commits": "കമ്മിറ്റ് ചെയ്യുന്നു", 41 | "issues": "പ്രശ്നങ്ങൾ", 42 | "prs": "അഭ്യർത്ഥനകൾ വലിക്കുക", 43 | "reviews": "റിക്വസ്റ്റ് റിവ്യൂകൾ വലിക്കുക", 44 | "contribTo": "സമർപ്പിച്ചിരിക്കുന്നത്", 45 | "private": "സ്വകാര്യ സംഭാവനകൾ" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}'s GitHub activiteiten", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Algemene statistieken en info", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositories", 11 | "column-one": "Non-Forks", 12 | "column-two": "Alles" 13 | }, 14 | "contributions": { 15 | "heading": "Bijdragen", 16 | "column-one": "Dit jaar", 17 | "column-two": "Totaal" 18 | }, 19 | "languages": { 20 | "heading": "Talen distributies in Publieke Repositories", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Jaar van aanmelding", 27 | "featured": "Uitgelichte repository", 28 | "mostStarred": "Repository met meeste sterren", 29 | "mostForked": "Repository met meeste forks", 30 | "followers": "Volgers", 31 | "following": "Volgend", 32 | "sponsors": "Sponsoren", 33 | "sponsoring": "Gesponsord", 34 | "public": "Mijn Repositories", 35 | "starredBy": "Ster gegeven door", 36 | "forkedBy": "Geforkt door", 37 | "watchedBy": "Gevolgd door", 38 | "templates": "Sjablonen", 39 | "archived": "Gearchiveerd", 40 | "commits": "Commits", 41 | "issues": "Problemen", 42 | "prs": "Pull Requests", 43 | "reviews": "Pull Request Recensies", 44 | "contribTo": "Bijgedragen aan", 45 | "private": "Prive Bijdragen" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}s GitHub-aktivitet", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Generell statistikk og info", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Kodebaser", 11 | "column-one": "Ikke-forgreninger", 12 | "column-two": "Alle" 13 | }, 14 | "contributions": { 15 | "heading": "Bidrag", 16 | "column-one": "Forrige år", 17 | "column-two": "Totalt" 18 | }, 19 | "languages": { 20 | "heading": "Språkdistribusjon i offentlige kodebaser", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Ble med i år", 27 | "featured": "Framhevet kodebase", 28 | "mostStarred": "Kodebase med flest stjerner", 29 | "mostForked": "Kodebase med flest forgreninger", 30 | "followers": "Følgere", 31 | "following": "Følger", 32 | "sponsors": "Sponsorer", 33 | "sponsoring": "Sponser", 34 | "public": "Mine kodebaser", 35 | "starredBy": "Stjernemerket av", 36 | "forkedBy": "Forgrenet av", 37 | "watchedBy": "Overvåket av", 38 | "templates": "Maler", 39 | "archived": "Arkivert", 40 | "commits": "Commits", 41 | "issues": "Saker", 42 | "prs": "Pull Requests", 43 | "reviews": "Pull Request-vurderinger", 44 | "contribTo": "Bidro til", 45 | "private": "Private bidrag" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/or.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}ର GitHub କାର୍ଯ୍ୟକଳାପ", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "ସାଧାରଣ ପରିସଂଖ୍ୟାନ ଏବଂ ସୂଚନା", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "ସଂଗ୍ରହାଳୟ", 11 | "column-one": "ଅଣ-ଫର୍କସ୍", 12 | "column-two": "ସମସ୍ତ" 13 | }, 14 | "contributions": { 15 | "heading": "ଅବଦାନ", 16 | "column-one": "ବିଗତ ବର୍ଷ", 17 | "column-two": "ମୋଟ" 18 | }, 19 | "languages": { 20 | "heading": "ସର୍ବସାଧାରଣ ସଂଗ୍ରହାଳୟରେ ଭାଷା ବଣ୍ଟନ", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "ବର୍ଷ ଯୋଗଦାନ", 27 | "featured": "ବୈଶିଷ୍ଟ୍ୟ ରେପୋ", 28 | "mostStarred": "ସର୍ବାଧିକ ତାରକା ରେପୋ", 29 | "mostForked": "ଅଧିକାଂଶ ଫୋର୍କଡ୍ ରେପୋ", 30 | "followers": "ଅନୁସରଣକାରୀ", 31 | "following": "ନିମ୍ନଲିଖିତ", 32 | "sponsors": "ପ୍ରଯୋଜକ", 33 | "sponsoring": "ପ୍ରାୟୋଜକ", 34 | "public": "ମୋର ସଂଗ୍ରହାଳୟ", 35 | "starredBy": "ଷ୍ଟାର୍ ହୋଇଥିବା", 36 | "forkedBy": "ଦ୍ୱାରା କଣ୍ଟା ହୋଇଛି", 37 | "watchedBy": "ଦେଖିଲା", 38 | "templates": "ଟେମ୍ପଲେଟ୍", 39 | "archived": "ସଂଗୃହିତ", 40 | "commits": "ପ୍ରତିବଦ୍ଧତା", 41 | "issues": "ସମସ୍ୟାଗୁଡିକ", 42 | "prs": "ଅନୁରୋଧ ଟାଣନ୍ତୁ", 43 | "reviews": "ଅନୁରୋଧ ସମୀକ୍ଷାଗୁଡିକ ଟାଣନ୍ତୁ", 44 | "contribTo": "ଯୋଗଦାନ", 45 | "private": "ବ୍ୟକ୍ତିଗତ ଅବଦାନ" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Aktywność {0} na GitHubie", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Ogólne statystyki i informacje", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repozytoria", 11 | "column-one": "Non-Forks", 12 | "column-two": "Wszystkie" 13 | }, 14 | "contributions": { 15 | "heading": "Kontrybucje", 16 | "column-one": "Ostatni rok", 17 | "column-two": "Wszystkie" 18 | }, 19 | "languages": { 20 | "heading": "Rozkład języków w Repozytoriach Publicznych", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Rok Dołączenia", 27 | "featured": "Polecane repozytorium", 28 | "mostStarred": "Repozytoria z największą ilością gwiazdek", 29 | "mostForked": "Najczęściej Forkowane Repozytoria", 30 | "followers": "Obserwujący", 31 | "following": "Obserwowani", 32 | "sponsors": "Sponsorzy", 33 | "sponsoring": "Sponsoring", 34 | "public": "Posiadane Repozytoria", 35 | "starredBy": "Polubione przez", 36 | "forkedBy": "Sforkowane przez", 37 | "watchedBy": "Obserwowane przez", 38 | "templates": "Szablony", 39 | "archived": "Zarchiwizowane", 40 | "commits": "Commity", 41 | "issues": "Problemy", 42 | "prs": "Pull Requesty", 43 | "reviews": "Recenzje Pull Requestów", 44 | "contribTo": "Kontrybuował Do", 45 | "private": "Prywatne Kontrybucje" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Atividade de {0} no GitHub", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Estatísticas Gerais e Informações", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositórios", 11 | "column-one": "Sem Forks", 12 | "column-two": "Todos" 13 | }, 14 | "contributions": { 15 | "heading": "Contribuições", 16 | "column-one": "Último ano", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Distribuição de Linguagens em Repositórios Públicos", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Ano de Inscrição", 27 | "featured": "Repositório em Primeiro Plano", 28 | "mostStarred": "Repositório com mais estrelas", 29 | "mostForked": "Repositório mais bifurcado", 30 | "followers": "Seguidores", 31 | "following": "A seguir", 32 | "sponsors": "Patrocinado", 33 | "sponsoring": "A patrocinar", 34 | "public": "Repositórios Possuídos", 35 | "starredBy": "Com Estrela De", 36 | "forkedBy": "Bifurcado Por", 37 | "watchedBy": "Visto Por", 38 | "templates": "Modelos", 39 | "archived": "Arquivados", 40 | "commits": "Commits", 41 | "issues": "Problemas", 42 | "prs": "Pull Requests", 43 | "reviews": "Avaliação de Pull Requests", 44 | "contribTo": "Contribuiu Para", 45 | "private": "Contribuições Privadas" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Activitatea GitHub a lui {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Statistici generale și informații", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Depozitele", 11 | "column-one": "Non-bifurcatii", 12 | "column-two": "Toate" 13 | }, 14 | "contributions": { 15 | "heading": "Contribuții", 16 | "column-one": "Anul trecut", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Distribuția limbii în arhivele publice", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "An alăturat", 27 | "featured": "Repo recomandate", 28 | "mostStarred": "Cel mai marcat Repo", 29 | "mostForked": "Repo cel mai bifurcat", 30 | "followers": "Urmaritori", 31 | "following": "Ca urmare a", 32 | "sponsors": "Sponsori", 33 | "sponsoring": "Sponsorizare", 34 | "public": "Arhivele mele", 35 | "starredBy": "Înscris de", 36 | "forkedBy": "Bifurcat de", 37 | "watchedBy": "Vizionat de", 38 | "templates": "Șabloane", 39 | "archived": "Arhivat", 40 | "commits": "Commits", 41 | "issues": "Probleme", 42 | "prs": "Solicitări de tragere", 43 | "reviews": "Recenzii Pull Request", 44 | "contribTo": "Contribuit la", 45 | "private": "Contribuții private" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Активность пользователя {0} на гитхабе", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Общая статистика и информация", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Статистика репозиториев", 11 | "column-one": "Без форков", 12 | "column-two": "Все" 13 | }, 14 | "contributions": { 15 | "heading": "Работа в репозиториях", 16 | "column-one": "За последний год", 17 | "column-two": "За все время" 18 | }, 19 | "languages": { 20 | "heading": "Использование языков в общедоступных репозиториях", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Год регистрации на гитхабе", 27 | "featured": "Избранное репо", 28 | "mostStarred": "Самое популярное репо", 29 | "mostForked": "Самое клонированное репо", 30 | "followers": "Подписчики", 31 | "following": "Подписан", 32 | "sponsors": "Спонсоры", 33 | "sponsoring": "Спонсирует", 34 | "public": "Собственные репозитории", 35 | "starredBy": "Добавили в избранное", 36 | "forkedBy": "Клонирован", 37 | "watchedBy": "Наблюдатели", 38 | "templates": "Шаблоны", 39 | "archived": "Заархивировано", 40 | "commits": "Коммиты", 41 | "issues": "Проблемы", 42 | "prs": "Пулл реквесты", 43 | "reviews": "Ревью пулл реквестов", 44 | "contribTo": "Участие в", 45 | "private": "Приватные изменения" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/sat.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}ᱟᱜ ᱜᱤᱴᱦᱚᱵᱽ ᱠᱟᱹᱢᱤᱦᱚᱨᱟᱠᱚ", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "ᱥᱟᱫᱷᱟᱨᱚᱬ ᱵᱟᱛᱟᱣ ᱟᱨ ᱵᱤᱵᱨᱚᱬ", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "ᱜᱩᱫᱟᱢ", 11 | "column-one": "ᱵᱤᱱ ᱯᱷᱚᱨᱠ ᱠᱚ", 12 | "column-two": "ᱢᱩᱴ" 13 | }, 14 | "contributions": { 15 | "heading": "ᱮᱱᱮᱢᱤᱭᱟᱹᱠᱚ", 16 | "column-one": "ᱪᱟᱞᱟᱣᱮᱱ ᱥᱮᱨᱢᱟᱸ", 17 | "column-two": "ᱢᱩᱴ" 18 | }, 19 | "languages": { 20 | "heading": "ᱥᱟᱱᱟᱢ ᱜᱩᱫᱟᱢ ᱨᱮ ᱯᱟᱹᱨᱥᱤ ᱠᱚᱣᱟᱜ ᱯᱟᱥᱱᱟᱣ", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "ᱥᱮᱞᱮᱫ ᱥᱮᱨᱢᱟᱸ", 27 | "featured": "ᱵᱤᱥᱮᱥ ᱜᱩᱫᱟᱢ", 28 | "mostStarred": "ᱡᱷᱚᱛᱚ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱪᱤᱱᱦᱟᱹ ᱦᱟᱜ ᱜᱩᱫᱟᱹᱢ", 29 | "mostForked": "ᱡᱟᱹᱥᱛᱤ ᱱᱚᱠᱚᱞ ᱠᱟᱱ ᱜᱚᱫᱟᱢ", 30 | "followers": "ᱯᱟᱧᱡᱟ ᱠᱩᱜ", 31 | "following": "ᱯᱟᱧᱡᱟ ᱮᱫᱟᱢ", 32 | "sponsors": "ᱨᱚᱠᱚᱢᱤᱭᱟᱹ", 33 | "sponsoring": "ᱨᱚᱠᱚᱢᱚᱜ ᱠᱟᱱᱟ", 34 | "public": "ᱤᱧᱟᱜ ᱜᱩᱫᱟᱢ ᱠᱚ", 35 | "starredBy": "ᱪᱤᱱᱦᱟᱹᱤᱭᱟᱹ", 36 | "forkedBy": "ᱱᱚᱠᱚᱞᱤᱭᱟᱹ", 37 | "watchedBy": "ᱛᱤᱱᱹᱜ ᱠᱚ ᱧᱮᱞ ᱠᱟᱫᱟ", 38 | "templates": "ᱪᱷᱟᱸᱪᱠᱚ", 39 | "archived": "ᱜᱟᱵᱟᱱᱮᱱᱟ", 40 | "commits": "ᱰᱟᱞᱟᱣᱠᱚ", 41 | "issues": "ᱯᱚᱞᱚᱡᱽᱠᱚ", 42 | "prs": "ᱚᱨ ᱱᱮᱦᱚᱨᱠᱚ", 43 | "reviews": "ᱚᱨ ᱱᱮᱦᱚᱨ ᱧᱮᱞᱯᱚᱨᱚᱠᱷ ᱠᱚ", 44 | "contribTo": "ᱮᱱᱮᱢ", 45 | "private": "ᱱᱤᱡᱚᱨᱟᱜ ᱩᱠᱩ ᱮᱱᱮᱢᱠᱚ" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} - Aktivnost na Githabu", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Opšta statistika i informacije", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repozitoriji", 11 | "column-one": "Ne-forkovani", 12 | "column-two": "Svi" 13 | }, 14 | "contributions": { 15 | "heading": "Doprinosi", 16 | "column-one": "Prošla godina", 17 | "column-two": "Ukupno" 18 | }, 19 | "languages": { 20 | "heading": "Zastupljenost jezika u javnim repozitorijima", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Godina pristupa", 27 | "featured": "Izabrani repozitorij", 28 | "mostStarred": "Najviše zvezdica na repou", 29 | "mostForked": "Najviše forkovan repo", 30 | "followers": "Pratilaca", 31 | "following": "Prati", 32 | "sponsors": "Sponzori", 33 | "sponsoring": "Sponzoriše", 34 | "public": "Lični repozitoriji", 35 | "starredBy": "Dodeljenih zvezdica", 36 | "forkedBy": "Broj forkovanja", 37 | "watchedBy": "Pregledi", 38 | "templates": "Šabloni", 39 | "archived": "Arhive", 40 | "commits": "Komiti", 41 | "issues": "Problemi", 42 | "prs": "Pul zahtevi", 43 | "reviews": "Revizije pul zahteva", 44 | "contribTo": "Doprinosi", 45 | "private": "Privatni doprinosi" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}'s GitHub -aktivitet", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Allmän statistik och information", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Förråd", 11 | "column-one": "Icke-gafflar", 12 | "column-two": "Allt" 13 | }, 14 | "contributions": { 15 | "heading": "Bidrag", 16 | "column-one": "Förra året", 17 | "column-two": "totala" 18 | }, 19 | "languages": { 20 | "heading": "Språkdistribution i offentliga arkiv", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "År ansluten", 27 | "featured": "Utvalda Repo", 28 | "mostStarred": "Mest stjärnklippta Repo", 29 | "mostForked": "Mest gaffelförsedda lagringsplatsen", 30 | "followers": "Anhängare", 31 | "following": "Följande", 32 | "sponsors": "Sponsorer", 33 | "sponsoring": "Sponsring", 34 | "public": "Förråd ägs", 35 | "starredBy": "Medverkat av", 36 | "forkedBy": "Gafflade av", 37 | "watchedBy": "Bevakad av", 38 | "templates": "Mallar", 39 | "archived": "Arkiverad", 40 | "commits": "Begår", 41 | "issues": "Frågor", 42 | "prs": "Pull-begäranden", 43 | "reviews": "Granskningar av pull-begäran", 44 | "contribTo": "Bidrog till", 45 | "private": "Privata bidrag" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "กิจกรรมของ {0} บน GitHub", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "สถิติและข้อมูลทั่วไป", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Repositories", 11 | "column-one": "ที่ไม่ใช่ Fork", 12 | "column-two": "รวมทั้งหมด" 13 | }, 14 | "contributions": { 15 | "heading": "Contributions", 16 | "column-one": "ปีที่แล้ว", 17 | "column-two": "รวมทั้งหมด" 18 | }, 19 | "languages": { 20 | "heading": "ภาษาที่ใช้ใน Repo สาธารณะ", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "ปีที่เข้าร่วม", 27 | "featured": "Repo ที่โดดเด่น", 28 | "mostStarred": "Repo ที่ติดดาวมากที่สุด", 29 | "mostForked": "Repo ที่มีการ Fork มากที่สุด", 30 | "followers": "ผู้ติดตาม", 31 | "following": "กำลังติดตาม", 32 | "sponsors": "ผู้สนับสนุน", 33 | "sponsoring": "กำลังสนับสนุน", 34 | "public": "Repo ทั้งหมดของฉัน", 35 | "starredBy": "ติดดาวทั้งหมด", 36 | "forkedBy": "มีการ Fork ทั้งหมด", 37 | "watchedBy": "ผู้ติดตาม", 38 | "templates": "เทมเพลตแม่แบบ", 39 | "archived": "เก็บถาวร", 40 | "commits": "คอมมิท", 41 | "issues": "ปัญหา", 42 | "prs": "Pull Requests", 43 | "reviews": "รีวิว Pull Request", 44 | "contribTo": "มีการช่วยไปแล้ว", 45 | "private": "Contributions ส่วนตัว" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/tl.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "Aktibidad sa GitHub ni {0}", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Pangkalahatang Statistika at Impormasyon", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Mga Repositoryo", 11 | "column-one": "Mga Non-Fork", 12 | "column-two": "Lahat" 13 | }, 14 | "contributions": { 15 | "heading": "Mga Kontribusyon", 16 | "column-one": "Nakaraang Taon", 17 | "column-two": "Kabuuan" 18 | }, 19 | "languages": { 20 | "heading": "Pamamahagi ng Wika sa Pangkabuuang Repositoryo", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Taon ng Pagsali", 27 | "featured": "Mga Naitampok na Repositoryo", 28 | "mostStarred": "Repositoryong may Pinakamaraming Bituin", 29 | "mostForked": "Repositoryong may Pinakamaraming Fork", 30 | "followers": "Mga Taga-subaybay", 31 | "following": "Mga Sinusubaybayan", 32 | "sponsors": "Mga Taga-suporta", 33 | "sponsoring": "Mga Sinusuportahan", 34 | "public": "Aking mga Repositoryo", 35 | "starredBy": "Binigyan ng Bituin Ni", 36 | "forkedBy": "Ni-Fork Ni", 37 | "watchedBy": "Inaabangan Ni", 38 | "templates": "Mga Template", 39 | "archived": "Tinabi", 40 | "commits": "Mga Commit", 41 | "issues": "Mga Isyu", 42 | "prs": "Mga Pull Request", 43 | "reviews": "Mga Naisuring Pull Request", 44 | "contribTo": "Nag-ambag Sa", 45 | "private": "Mga Pribadong Ambag" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0}'in GitHub Etkinliği", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Genel İstatistikler ve Bilgiler", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Depolar", 11 | "column-one": "Çatalsız", 12 | "column-two": "Tüm" 13 | }, 14 | "contributions": { 15 | "heading": "Katkılar", 16 | "column-one": "Geçen sene", 17 | "column-two": "Total" 18 | }, 19 | "languages": { 20 | "heading": "Genel Depolarda Dil Dağılımı", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Katıldığı Yıl", 27 | "featured": "Öne Çıkan Repo", 28 | "mostStarred": "En Çok Yıldızlı Repo", 29 | "mostForked": "En Çatallı Repo", 30 | "followers": "Takipçiler", 31 | "following": "Takip etmek", 32 | "sponsors": "Sponsorlar", 33 | "sponsoring": "Sponsorluk", 34 | "public": "Sahip Olunan Depolar", 35 | "starredBy": "Tarafından yıldız", 36 | "forkedBy": "Tarafından çatallandı", 37 | "watchedBy": "İzleyen", 38 | "templates": "Sablonlar", 39 | "archived": "Arşivlenmiş", 40 | "commits": "Taahhütler", 41 | "issues": "Sorunlar", 42 | "prs": "Çekme İstekleri", 43 | "reviews": "İstek İncelemelerini Çekin", 44 | "contribTo": "Katkıda Bulunanlar", 45 | "private": "Özel Katkılar" 46 | } 47 | } -------------------------------------------------------------------------------- /src/locales/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleTemplate": "{0} активностей на GitHub", 3 | "categoryLabels": { 4 | "general": { 5 | "heading": "Загальна статистика та інформація", 6 | "column-one": null, 7 | "column-two": null 8 | }, 9 | "repositories": { 10 | "heading": "Репозиторіїв", 11 | "column-one": "Без форків", 12 | "column-two": "Всі" 13 | }, 14 | "contributions": { 15 | "heading": "Внески", 16 | "column-one": "За останній рік", 17 | "column-two": "Всього" 18 | }, 19 | "languages": { 20 | "heading": "Використання мов у загальнодоступних репозиторіях", 21 | "column-one": null, 22 | "column-two": null 23 | } 24 | }, 25 | "statLabels": { 26 | "joined": "Рік приєднання", 27 | "featured": "Вибрані ререпозиторії", 28 | "mostStarred": "Найпопулярніший репозиторій", 29 | "mostForked": "Найбільш клонований репозиторій", 30 | "followers": "Підписники", 31 | "following": "Підписки", 32 | "sponsors": "Спонсори", 33 | "sponsoring": "Спонсорство", 34 | "public": "Власні репозиторії", 35 | "starredBy": "Відмітили", 36 | "forkedBy": "Клонували", 37 | "watchedBy": "Підписники", 38 | "templates": "Шаблони", 39 | "archived": "Заархівовано", 40 | "commits": "Комміти", 41 | "issues": "Проблеми", 42 | "prs": "Пулл реквести", 43 | "reviews": "Огляди пулл реквестів", 44 | "contribTo": "Участь в", 45 | "private": "Приватна участь" 46 | } 47 | } -------------------------------------------------------------------------------- /src/queries/basicstats.graphql: -------------------------------------------------------------------------------- 1 | query($owner: String!) { 2 | user(login: $owner) { 3 | contributionsCollection { 4 | totalCommitContributions 5 | totalIssueContributions 6 | totalPullRequestContributions 7 | totalPullRequestReviewContributions 8 | totalRepositoryContributions 9 | restrictedContributionsCount 10 | contributionYears 11 | } 12 | followers { 13 | totalCount 14 | } 15 | following { 16 | totalCount 17 | } 18 | issues { 19 | totalCount 20 | } 21 | login 22 | name 23 | pullRequests { 24 | totalCount 25 | } 26 | repositoriesContributedTo { 27 | totalCount 28 | } 29 | sponsorshipsAsMaintainer { 30 | totalCount 31 | } 32 | sponsorshipsAsSponsor { 33 | totalCount 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/queries/reposContributedTo.graphql: -------------------------------------------------------------------------------- 1 | query($owner: String!, $endCursor: String) { 2 | user(login: $owner) { 3 | topRepositories(first: 100, after: $endCursor, orderBy: {direction: DESC, field: UPDATED_AT}) { 4 | totalCount 5 | nodes { 6 | owner { 7 | login 8 | } 9 | } 10 | pageInfo { 11 | hasNextPage 12 | endCursor 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/queries/repostats.graphql: -------------------------------------------------------------------------------- 1 | query($owner: String!, $endCursor: String) { 2 | user(login: $owner) { 3 | repositories(first: 100, after: $endCursor, ownerAffiliations: OWNER) { 4 | totalCount 5 | nodes { 6 | stargazerCount 7 | forkCount 8 | isArchived 9 | isFork 10 | isPrivate 11 | isTemplate 12 | name 13 | watchers { 14 | totalCount 15 | } 16 | languages(first: 100, orderBy: {direction: DESC, field: SIZE}) { 17 | totalCount 18 | totalSize 19 | edges { 20 | size 21 | node { 22 | color 23 | name 24 | } 25 | } 26 | } 27 | } 28 | pageInfo { 29 | hasNextPage 30 | endCursor 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/queries/singleYearQueryFragment.graphql: -------------------------------------------------------------------------------- 1 | year{0}: contributionsCollection(from: "{0}-01-01T00:00:00.001Z") {{ 2 | totalCommitContributions 3 | totalPullRequestReviewContributions 4 | restrictedContributionsCount 5 | }} 6 | -------------------------------------------------------------------------------- /util/CharacterWidths.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2022 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import json 29 | import pprint 30 | 31 | if __name__ == "__main__" : 32 | with open("default-widths.json", "r") as f : 33 | defaultWidths = json.load(f) 34 | with open("default-widths.py", "wb") as f : 35 | heading = """######################################## 36 | # The dict that follows is derived from 37 | # default-widths.json from 38 | # https://github.com/google/pybadges, 39 | # which is licensed under Apache-2.0. 40 | ######################################## 41 | """ 42 | formatted = pprint.pformat(defaultWidths, indent=0, compact=True) 43 | formatted = formatted.replace(" " * 21, "") 44 | formatted = formatted.replace(" " * 17, "") 45 | s = heading + "\ndefaultWidths = " + formatted 46 | f.write(s.encode(encoding="UTF-8")) 47 | -------------------------------------------------------------------------------- /util/refactor-locales-to-json.py: -------------------------------------------------------------------------------- 1 | # 2 | # user-statistician: Github action for generating a user stats card 3 | # 4 | # Copyright (c) 2021-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | from StatConfig import * 29 | import json 30 | 31 | if __name__ == "__main__": 32 | for locale in supportedLocales: 33 | combined = { 34 | "titleTemplate" : titleTemplates[locale], 35 | "categoryLabels" : categoryLabels[locale], 36 | "statLabels" : { key : value["label"][locale] for key, value in statLabels.items() } 37 | } 38 | print("Processing", locale) 39 | with open("locales/" + locale + ".json", "w", encoding='utf8') as f : 40 | json.dump(combined, f, ensure_ascii=False, indent=2) 41 | 42 | --------------------------------------------------------------------------------