├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .nvmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.dot.md ├── README.md ├── generated ├── prs_accepted_by_approval_bar.png ├── prs_accepted_by_merged_bar.png ├── prs_by_day_bar.png ├── prs_by_language_doughnut.png ├── prs_by_language_spline.png ├── prs_by_state_doughnut.png ├── prs_by_state_stacked.png ├── repos_by_language_doughnut.png ├── repos_by_license_bar.png ├── repos_reported_doughnut.png ├── stats.txt ├── users_by_prs_column.png ├── users_by_prs_extended_column.png ├── users_by_state_doughnut.png ├── users_by_state_stacked.png ├── users_completions_contribution_type_bar.png ├── users_completions_experience_level_bar.png ├── users_completions_linked_providers_bar.png ├── users_completions_top_countries_bar.png ├── users_completions_top_countries_bar_excl.png ├── users_engaged_linked_providers_bar.png ├── users_registrations_ai_ml_interest_bar.png ├── users_registrations_contribution_type_bar.png ├── users_registrations_experience_level_bar.png ├── users_registrations_linked_providers_bar.png ├── users_registrations_student_status_bar.png ├── users_registrations_top_countries_bar.png └── users_registrations_top_countries_bar_excl.png ├── package-lock.json ├── package.json └── src ├── helpers ├── chart.js ├── color.js ├── date.js ├── hf.png ├── linguist.js ├── log.js └── number.js ├── index.js └── stats ├── PRs.js ├── Repos.js ├── Users.js ├── index.js └── readme.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended' 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2022, 11 | }, 12 | rules: { 13 | 'linebreak-style': ['error', 'unix'], 14 | semi: ['error', 'always'], 15 | quotes: ['error', 'single'], 16 | 'comma-dangle': ['error', 'always-multiline'], 17 | 'no-prototype-builtins': 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Install & Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout commit 11 | uses: actions/checkout@v3 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version-file: .nvmrc 17 | cache: npm 18 | 19 | # https://www.npmjs.com/package/canvas#compiling 20 | - name: Install OS dependencies 21 | run: sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Run tests 27 | run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | 4 | # Data not ready for public yet 5 | data/ 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Hacktoberfest Stats 2 | 3 | ## Getting some data 4 | 5 | Unfortunately, the raw data from Hacktoberfest is not publicly available. 6 | However, we are able to share the schema for the JSON data input that is used for this script: 7 | 8 | ```yaml 9 | schema: 10 | type: object 11 | properties: 12 | generation: 13 | type: object 14 | properties: 15 | started: 16 | type: string 17 | format: date-time 18 | ended: 19 | type: string 20 | format: date-time 21 | data: 22 | type: object 23 | properties: 24 | users: 25 | type: object 26 | properties: 27 | states: 28 | type: object 29 | properties: 30 | daily: 31 | type: object 32 | additionalProperties: 33 | type: object 34 | properties: 35 | states: 36 | type: object 37 | additionalProperties: 38 | type: integer 39 | count: 40 | type: integer 41 | all: 42 | type: object 43 | properties: 44 | states: 45 | type: object 46 | additionalProperties: 47 | type: integer 48 | count: 49 | type: integer 50 | providers: 51 | type: object 52 | additionalProperties: 53 | type: object 54 | properties: 55 | states: 56 | type: object 57 | additionalProperties: 58 | type: integer 59 | count: 60 | type: integer 61 | metadata: 62 | type: object 63 | additionalProperties: 64 | type: object 65 | properties: 66 | values: 67 | type: object 68 | additionalProperties: 69 | type: object 70 | properties: 71 | states: 72 | type: object 73 | additionalProperties: 74 | type: integer 75 | count: 76 | type: integer 77 | pull_requests: 78 | type: object 79 | additionalProperties: 80 | type: object 81 | properties: 82 | states: 83 | type: object 84 | additionalProperties: 85 | type: object 86 | properties: 87 | counts: 88 | type: object 89 | additionalProperties: 90 | type: integer 91 | average: 92 | type: integer 93 | all: 94 | type: object 95 | properties: 96 | counts: 97 | type: object 98 | additionalProperties: 99 | type: integer 100 | average: 101 | type: integer 102 | pull_requests: 103 | type: object 104 | properties: 105 | states: 106 | type: object 107 | properties: 108 | daily: 109 | type: object 110 | additionalProperties: 111 | type: object 112 | properties: 113 | states: 114 | type: object 115 | additionalProperties: 116 | type: integer 117 | count: 118 | type: integer 119 | all: 120 | type: object 121 | properties: 122 | states: 123 | type: object 124 | additionalProperties: 125 | type: integer 126 | count: 127 | type: integer 128 | providers: 129 | type: object 130 | properties: 131 | daily: 132 | type: object 133 | additionalProperties: 134 | type: object 135 | properties: 136 | providers: 137 | type: object 138 | additionalProperties: 139 | type: object 140 | properties: 141 | states: 142 | type: object 143 | additionalProperties: 144 | type: integer 145 | count: 146 | type: integer 147 | all: 148 | type: object 149 | properties: 150 | providers: 151 | type: object 152 | additionalProperties: 153 | type: object 154 | properties: 155 | states: 156 | type: object 157 | additionalProperties: 158 | type: integer 159 | count: 160 | type: integer 161 | languages: 162 | type: object 163 | properties: 164 | daily: 165 | type: object 166 | additionalProperties: 167 | type: object 168 | properties: 169 | languages: 170 | type: object 171 | additionalProperties: 172 | type: object 173 | properties: 174 | states: 175 | type: object 176 | additionalProperties: 177 | type: integer 178 | count: 179 | type: integer 180 | all: 181 | type: object 182 | properties: 183 | languages: 184 | type: object 185 | additionalProperties: 186 | type: object 187 | properties: 188 | states: 189 | type: object 190 | additionalProperties: 191 | type: integer 192 | count: 193 | type: integer 194 | merged: 195 | type: object 196 | additionalProperties: 197 | type: object 198 | properties: 199 | states: 200 | type: object 201 | additionalProperties: 202 | type: integer 203 | count: 204 | type: integer 205 | approved: 206 | type: object 207 | additionalProperties: 208 | type: object 209 | properties: 210 | states: 211 | type: object 212 | additionalProperties: 213 | type: integer 214 | count: 215 | type: integer 216 | additions: 217 | type: object 218 | properties: 219 | states: 220 | type: object 221 | additionalProperties: 222 | type: integer 223 | count: 224 | type: integer 225 | deletions: 226 | type: object 227 | properties: 228 | states: 229 | type: object 230 | additionalProperties: 231 | type: integer 232 | count: 233 | type: integer 234 | files: 235 | type: object 236 | properties: 237 | states: 238 | type: object 239 | additionalProperties: 240 | type: integer 241 | count: 242 | type: integer 243 | commits: 244 | type: object 245 | properties: 246 | states: 247 | type: object 248 | additionalProperties: 249 | type: integer 250 | count: 251 | type: integer 252 | repositories: 253 | type: object 254 | properties: 255 | pull_requests: 256 | type: object 257 | additionalProperties: 258 | type: object 259 | properties: 260 | counts: 261 | type: object 262 | additionalProperties: 263 | type: integer 264 | count: 265 | type: integer 266 | average: 267 | type: number 268 | languages: 269 | type: object 270 | properties: 271 | languages: 272 | type: object 273 | additionalProperties: 274 | type: integer 275 | unique: 276 | type: integer 277 | licenses: 278 | type: object 279 | properties: 280 | licenses: 281 | type: object 282 | additionalProperties: 283 | type: integer 284 | unique: 285 | type: integer 286 | excluded_repositories: 287 | type: object 288 | properties: 289 | active: 290 | type: object 291 | additionalProperties: 292 | type: object 293 | properties: 294 | has_note: 295 | type: object 296 | additionalProperties: 297 | type: object 298 | properties: 299 | reports: 300 | type: object 301 | additionalProperties: 302 | type: integer 303 | count: 304 | type: integer 305 | count: 306 | type: integer 307 | count: 308 | type: integer 309 | ``` 310 | 311 | This schema is part of a larger OpenAPI 3.0 schema that is used to describe the whole API behind 312 | Hacktoberfest. When generating data, keep in mind that many parts of this schema rely on 313 | `additionalProperties` rather than explicit properties. In many cases this may simply be 314 | `true`/`false` for flags, or assorted state names (`spam`, `waiting`, `accepted`, etc.). Make sure 315 | you have all the state names that the script expects from Hacktoberfest in your data (you may need 316 | to do a bit of trial and error to get everything). 317 | 318 | Once you have data in a JSON file that conforms to this schema, update the 319 | [`src/index.js`](src/index.js) file to load it in. 320 | 321 | ## Generating the stats 322 | 323 | ### Install the project's dependencies 324 | 325 | Ensure that you are running the correct version of Node.js as specified in [`.nvmrc`](.nvmrc). 326 | 327 | ``` 328 | npm ci 329 | ``` 330 | 331 | (Note that `canvas` may require some OS dependencies if a binary is not available for your OS: 332 | ) 333 | 334 | ### Run the script 335 | 336 | ``` 337 | npm start 338 | ``` 339 | 340 | ### Output 341 | 342 | All the text-based stats will be logged to console and saved to `generated/stats.txt`. 343 | All the generated charts/graphs will be saved to the `generated` directory. 344 | 345 | ## Linting 346 | 347 | This project uses eslint to enforce code-style standards. 348 | 349 | ``` 350 | npm test 351 | ``` 352 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Matt (IPv4) Cowley 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.dot.md: -------------------------------------------------------------------------------- 1 | # Hacktoberfest {{= data.readme.year }} Stats 2 | 3 | Hi there, 👋 4 | 5 | I'm [Matt Cowley](https://mattcowley.co.uk/), Senior Software Engineer II at 6 | [DigitalOcean](https://digitalocean.com/). 7 | 8 | I work on a bunch of things at DigitalOcean, with a great mix of engineering, developer relations 9 | and community management. And, part of what I get to work on is helping out with Hacktoberfest, 10 | including being the lead engineer for the backend that powers the event. 11 | 12 | Welcome to my deeper dive on the data and stats for Hacktoberfest {{= data.readme.year }}, expanding 13 | on what we already shared in our [recap blog post](https://{{= data.readme.blog }}). 14 | 15 | ## At a glance 16 | 17 | What did we accomplish together in October {{= data.readme.year }}? 18 | These are the highlights from Hacktoberfest #{{= data.readme.year - 2013 }}: 19 | 20 | - Registered users: **{{= c(data.readme.registeredUsers) }}** 21 | - Engaged users (1-3 accepted PR/MRs): **{{= c(data.readme.engagedUsers) }}** 22 | - Completed users (4+ accepted PR/MRs): **{{= c(data.readme.completedUsers) }}** 23 | - Accepted PR/MRs: **{{= c(data.readme.acceptedPRs) }}** 24 | - Active repositories (1+ accepted PR/MRs): **{{= c(data.readme.activeRepos) }}** 25 | - Countries represented by registered users: **{{= c(data.readme.countriesRegistered) }}** 26 | - Countries represented by completed users: **{{= c(data.readme.countriesCompleted) }}** 27 | 28 | > Take a read of our overall recap blog post for Hacktoberfest {{= data.readme.year }} here: 29 | > [{{= data.readme.blog }}](https://{{= data.readme.blog }}) 30 | 31 | ## Application states 32 | 33 | Before jumping in and looking at the data in detail, we should first cover some important 34 | information about how we categorise users and pull/merge requests in the Hacktoberfest application 35 | and in the data used here. 36 | 37 | For users, there are four key states that you'll see: 38 | 39 | - **Completed**: A user that submitted four or more accepted PR/MRs during Hacktoberfest. 40 | - **Engaged**: A user that submitted between one and three accepted PR/MRs during Hacktoberfest. 41 | - **Registered**: A user that completed the registration flow for Hacktoberfest, but did not submit 42 | any PR/MRs that were accepted. 43 | - **Disqualified**: A user that was disqualified for submitting 2 or more spammy PR/MRs, 44 | irrespective of how many accepted PR/MRs they may have also had. 45 | 46 | For pull/merge requests, there are six states used to process them that you'll see: 47 | 48 | - **Accepted**: A PR/MR that was accepted by a project maintainer, either by being merged or 49 | approved in a participating repository, or by being given the `hacktoberfest-accepted` label. 50 | - **Not accepted**: Any PR/MR that was submitted to a participating repository (having the 51 | `hacktoberfest` topic), but that was not actively accepted by a maintainer. 52 | - **Not participating**: Any PR/MR that was submitted by a participant to a repository that was not 53 | participating in Hacktoberfest (i.e. having the `hacktoberfest` topic, or adding the 54 | `hacktoberfest-accepted` label to specific PRs). 55 | - **Invalid**: Any PR/MR that was given a label containing the word `invalid` by a maintainer. Any 56 | PR/MR with a matching label was not counted towards a participant's total. 57 | - **Spam**: Any PR/MR that was given a label by a maintainer containing the 'spam', or PR/MRs that 58 | our abuse logic detected as spam. These are not counted toward winning, and also count toward a 59 | user being disqualified. 60 | - **Excluded**: Any PR/MR that was submitted to a repository that has been excluded from 61 | Hacktoberfest for not following our values. These do not count toward winning, nor do they count 62 | toward a user being disqualified. 63 | 64 | ## Diving in: Users 65 | 66 | This year, Hacktoberfest had **{{= c(data.Users.totalUsers) }}** folks who went through our 67 | registration flow for the event. Spam has been a huge focus for us throughout the event, as with 68 | previous years, and so during this flow folks were reminded about our rules and values for the event 69 | with clear and simple language, as well as agreeing to a rule that folks with two or more PR/MRs 70 | identified as spam by maintainers would be disqualified. More on this later. 71 | 72 | During the registration flow, folks can also choose to tell us which country they are from--this 73 | helps us better understand, and cater to, the global audience for the event--and 74 | **{{= p((data.Users.totalUsers - data.Users.totalUsersNoCountry) / data.Users.totalUsers) }}** of 75 | them did so. 76 | 77 |

78 | Bar chart of the top countries for completed users 79 | Bar chart of the top countries for all registered users 80 |

81 | 82 | The top country, by far, was once again {{= data.Users.totalUsersByCountry[0][0] }} with 83 | **{{= c(data.Users.totalUsersByCountry[0][1]) }} 84 | ({{= p(data.Users.totalUsersByCountry[0][1] / data.Users.totalUsers) }})** registrants 85 | self-identifying as being from there, showing how much of a reach open-source, and tech in general, 86 | has there. 87 | 88 | We can see the true global reach of Hacktoberfest and open-source by looking at more of the top 89 | countries based on registrations: 90 | 91 | {{~ data.Users.totalUsersByCountry.slice(0, 10) :item:i }} 92 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} ({{= p(item[1] / data.Users.totalUsers) }}) 93 | {{~ }} 94 | 95 | In total, **{{= c(data.Users.totalUsersByCountry.length) }} countries** were represented by folks 96 | that registered for the {{= data.readme.year }} event. 97 | 98 | We can also look at just the users that completed Hacktoberfest, and see how the countries are 99 | distributed for those users: 100 | 101 | {{~ data.Users.totalUsersCompletedByCountry.slice(0, 10) :item:i }} 102 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} ({{= p(item[1] / data.Users.totalUsersCompleted) }}) 103 | {{~ }} 104 | 105 | Doughnut diagram of users by application state 106 | 107 | Of course, there's more to Hacktoberfest than just registering for the event, folks actually submit 108 | PR/MRs to open-source projects! This year, we had 109 | **{{= c(data.Users.totalUsersCompleted + data.Users.totalUsersEngaged) }} 110 | users 111 | ({{= p((data.Users.totalUsersCompleted + data.Users.totalUsersEngaged) / data.Users.totalUsers) }} 112 | of total registrations)** that submitted one or more PR/MRs that were accepted by maintainers. 113 | Of those, **{{= c(data.Users.totalUsersCompleted) }} 114 | ({{= p(data.Users.totalUsersCompleted / (data.Users.totalUsersCompleted + data.Users.totalUsersEngaged)) }}) 115 | ({{= p(data.Users.totalUsersCompleted / data.Users.totalUsers) }} of total registrations)** went on 116 | to submit at least four accepted PR/MRs to successfully complete Hacktoberfest. 117 | 118 | Impressively, we saw that **{{= c(data.Users.totalUsersByAcceptedPRsCapped[4][1]) }} users 119 | ({{= p(data.Users.totalUsersByAcceptedPRsCapped[4][1] / data.Users.totalUsersCompleted) }} of total 120 | completed)** submitted more than 4 accepted PR/MRs, going above and beyond to contribute to 121 | open-source outside the goal set for completing Hacktoberfest. 122 | 123 | This year, Hacktoberfest removed the free t-shirt as a reward for completing Hacktoberfest, instead 124 | replacing it with a digital reward kit unlocked once you had four accepted PR/MRs, and a digital 125 | badge from Holopin that levelled up with each PR/MR accepted on your journey from registration to 126 | completion. While we still saw many folks register and engage with Hacktoberfest, the numbers are 127 | much lower than previous years, likely due to this change in reward, and while disappointing this 128 | was expected. 129 | 130 | Sadly, {{= c(data.Users.totalUsersDisqualified) }} users were disqualified this year 131 | ({{= p(data.Users.totalUsersDisqualified / data.Users.totalUsers) }} of total registrations), with 132 | an additional {{= c(data.Users.totalUsersWarned) }} 133 | ({{= p(data.Users.totalUsersWarned / data.Users.totalUsers) }} of total registrations) warned. 134 | Disqualification of users happen automatically if two or more of their PR/MRs are actively 135 | identified as spam by project maintainers, with users being sent a warning email (and shown a notice 136 | on their profile) when they have one PR/MR that is identified as spam. We were very happy to see how 137 | low this number was though, indicating to us that our efforts to educate and remind contributors of 138 | the quality standards expected of them during Hacktoberfest are working. _(Of course, we can only 139 | report on what we see in our data here, and do acknowledge that folks may have received spam that 140 | wasn't flagged so won't be represented in our reporting)._ 141 | 142 | 143 |

144 | 145 | Hacktoberfest supported multiple providers this year, GitHub & GitLab. Registrants could choose to 146 | link just one provider to their account, or multiple if they desired, with contributions from each 147 | provider combined into a single record for the user. 148 | 149 | Based on this we can take a look at the most popular providers for open-source based on some 150 | Hacktoberfest-specific metrics. First, we can see that based on registrations, **the most popular 151 | provider was {{= data.Users.totalUsersByProvider[0][0] }} with 152 | {{= c(data.Users.totalUsersByProvider[0][1]) }} registrants 153 | ({{= p(data.Users.totalUsersByProvider[0][1] / data.Users.totalUsers) }}).** 154 | 155 | {{~ data.Users.totalUsersByProvider :item:i }} 156 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} 157 | ({{= p(item[1] / data.Users.totalUsers) }} of registered users) 158 | {{~ }} 159 | 160 | _Users were able to link one or more providers to their account, so the counts here may sum to more 161 | than the total number of users registered._ 162 | 163 | We can also look at a breakdown of users that were engaged (1-3 accepted PR/MRs) and users that 164 | completed Hacktoberfest (4+ PR/MRs) by provider. 165 | 166 | Engaged users by provider: 167 | 168 | {{~ data.Users.totalUsersEngagedByProvider :item:i }} 169 | - {{= item[0] }}: {{= c(item[1]) }} 170 | ({{= p(item[1] / data.Users.totalUsersEngaged) }} of engaged users) 171 | {{~ }} 172 | 173 | Completed users by provider: 174 | 175 | {{~ data.Users.totalUsersCompletedByProvider :item:i }} 176 | - {{= item[0] }}: {{= c(item[1]) }} 177 | ({{= p(item[1] / data.Users.totalUsersCompleted) }} of completed users) 178 | {{~ }} 179 | 180 | Bar chart of users by experience level 181 | 182 | When registering for Hacktoberfest, we also asked users for some optional self-identification around 183 | their experience with contributing to open-source, and how they intended to contribute. First, we 184 | can take a look at the experience level users self-identified as having when registering: 185 | 186 | {{~ data.Users.totalUsersByExperience :item:i }} 187 | - {{= item[0] }}: {{= c(item[1]) }} 188 | ({{= p(item[1] / data.Users.totalUsers) }} of registered users) 189 | {{~ }} 190 | 191 | _{{= c(data.Users.totalUsersNoExperience) }} users did not self-identify their experience level._ 192 | 193 | We can compare this to the breakdown of users that completed Hacktoberfest by experience level: 194 | 195 | {{~ data.Users.totalUsersCompletedByExperience :item:i }} 196 | - {{= item[0] }}: {{= c(item[1]) }} 197 | ({{= p(item[1] / data.Users.totalUsersCompleted) }} of completed users) 198 | {{~ }} 199 | 200 | _{{= c(data.Users.totalUsersCompletedNoExperience) }} users who completed Hacktoberfest did not 201 | self-identify their experience when registering._ 202 | 203 | 204 |

205 | 206 | Not everyone is comfortable writing code, and so Hacktoberfest focused on encouraging more 207 | contributors to get involved with open-source this year through non-code contributors. We can look 208 | at what contribution types users indicated they intended to make during Hacktoberfest when 209 | registering (they could pick multiple, or none): 210 | 211 | {{~ data.Users.totalUsersByContribution :item:i }} 212 | - {{= item[0] }}: {{= c(item[1]) }} 213 | ({{= p(item[1] / data.Users.totalUsers) }} of registered users) 214 | {{~ }} 215 | 216 | _Of course, this is only what users indicated they intended to do, and doesn't necessarily reflect 217 | their actual contributions they ended up making to open-source (determining what is and what isn't 218 | a "non-code" PR/MR would be a difficult task)._ 219 | 220 | This year, we also asked users during registration whether they were students or not, to give us a 221 | better sense of the audience that is participating in Hacktoberfest. We can see that 222 | **{{= c(data.Users.totalUsersStudents) }} users 223 | ({{= p(data.Users.totalUsersStudents / data.Users.totalUsers) }} of registered users)** indicated 224 | that they were students when registering. 225 | 226 | Bar chart of users by AI/ML interest 227 | 228 | Folks were also asked if they'd be interested in contributing to AI/ML projects specifically during 229 | Hacktoberfest, as this area of open-source is growing rapidly and we wanted to see if there was 230 | interest in it. 231 | 232 | We can see that **{{= c(data.Users.totalUsersAIML) }} users 233 | ({{= p(data.Users.totalUsersAIML / data.Users.totalUsers) }} of registered users)** indicated they 234 | were actively interested in AI/ML projects, while **{{= c(data.Users.totalUsersNotAIML) }} users 235 | ({{= p(data.Users.totalUsersNotAIML / data.Users.totalUsers) }} of registered users)** indicated 236 | they were not interested in AI/ML projects (this was an optional question, with 237 | {{= c(data.Users.totalUsersMissingAIML) }} users not providing a preference). 238 | 239 | While we obviously can't know for sure the preference of those that did not interact with this 240 | question, there was a much larger portion of folks registering that did not engage with this 241 | question than other questions ({{= p(data.Users.totalUsersMissingAIML / data.Users.totalUsers) }}, 242 | compared to just {{= p(data.Users.totalUsersNoExperience / data.Users.totalUsers) }} that did not 243 | indicate their experience level), which is potentially an indicator that folks were unfamiliar with 244 | or disinterested in AI/ML. 245 | 246 | 247 |

248 | 249 | As with previous years of Hacktoberfest, users had to submit PR/MRs to participating projects during 250 | October that then had to be accepted by maintainers during October. If a user submitted four or 251 | more PR/MRs, then they completed Hacktoberfest. However, not everyone hit the 4 PR/MR target, with 252 | some falling short, and many going beyond the target to contribute further. 253 | 254 | We can see how many accepted PR/MRs each user had and bucket them: 255 | 256 | {{~ data.Users.totalUsersByAcceptedPRs :item:i }} 257 | - {{= item[0] }}{{= (i === data.Users.totalUsersByAcceptedPRs.length - 1 ? '+' : '') }} 258 | PR{{= (item[0] === 1 ? '': 's') }}/MR{{= (item[0] === 1 ? '': 's') }}: {{= c(item[1]) }} 259 | ({{= p(item[1] / data.Users.totalUsersCompleted) }}) 260 | {{~ }} 261 | 262 | Looking at this, we can see that quite a few users only managed to get 1 accepted PR/MR, but 263 | after that it quickly trailed off for 2 and 3 PR/MRs. It seems like the target of 4 PR/MRs 264 | encouraged many users to push through to getting all 4 PR/MRs created/accepted if they got that 265 | first one completed. 266 | 267 | ![Bar chart of users by accepted PR/MRs](generated/users_by_prs_extended_column.png) 268 | 269 | ## Diving in: Pull/Merge Requests 270 | 271 | Doughnut diagram of PR/MRs by application state 272 | 273 | Now on to what you've been waiting for, and the core of Hacktoberfest itself, the pull/merge 274 | requests. This year Hacktoberfest tracked **{{= c(data.PRs.totalPRs) }}** PR/MRs that were within 275 | the bounds of the Hacktoberfest event, and **{{= c(data.PRs.totalAcceptedPRs) }} 276 | ({{= p(data.PRs.totalAcceptedPRs / data.PRs.totalPRs) }})** of those went on to be accepted! 277 | 278 | Unfortunately, not every pull/merge request can be accepted though, for one reason or another, and 279 | this year we saw that there were **{{= c(data.PRs.totalNotAcceptedPRs )}} 280 | ({{= p(data.PRs.totalNotAcceptedPRs / data.PRs.totalPRs) }})** PR/MRs that were submitted to 281 | participating repositories but that were not accepted by maintainers, as well as 282 | **{{= c(data.PRs.totalNotParticipatingPRs )}} 283 | ({{= p(data.PRs.totalNotParticipatingPRs / data.PRs.totalPRs) }})** PR/MRs submitted by 284 | Hacktoberfest participants to repositories that were not participating in Hacktoberfest. As a 285 | reminder to folks, repositories opt-in to participating in Hacktoberfest by adding the 286 | `hacktoberfest` topic to their repository (or individual PR/MRs can be opted-in with the 287 | `hacktoberfest-accepted` label). 288 | 289 | Spam is also a big issue that we focus on reducing during Hacktoberfest, and we tracked the number 290 | of PR/MRs that were identified by maintainers as spam, as well as those that were caught by 291 | automation we'd written to stop spammy users. We'll talk more about all-things-spam later on. 292 | 293 | 294 |

295 | 296 | This year, Hacktoberfest supported multiple providers that contributors could use to submit 297 | contributions to open-source projects. Let's take a look at the breakdown of PR/MRs per provider: 298 | 299 | {{~ data.PRs.totalPRsByProvider :item:i }} 300 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} 301 | ({{= p(item[1] / data.PRs.totalPRs) }} of total PR/MRs) 302 | {{~ }} 303 | 304 | PRs and MRs that are accepted by maintainers for Hacktoberfest aren't necessarily merged -- 305 | Hacktoberfest supports multiple different ways for a maintainer to indicate that a PR/MR is 306 | legitimate and should be counted. PR/MRs can be merged, or they can be given the 307 | `hacktoberfest-accepted` label, or maintainers can leave an overall approving review. 308 | 309 | Of the accepted PR/MRs, **{{= c(data.PRs.totalAcceptedPRsMerged) }} 310 | ({{= p(data.PRs.totalAcceptedPRsMerged / data.PRs.totalAcceptedPRs) }})** were merged into the 311 | repository, and **{{= c(data.PRs.totalAcceptedPRsApproved) }} 312 | ({{= p(data.PRs.totalAcceptedPRsApproved / data.PRs.totalAcceptedPRs) }})** were approved by a 313 | maintainer. Note that there may be overlap here, as a PR/MR may have been approved and then merged. 314 | Unfortunately, we don't have direct aggregated data for the `hacktoberfest-accepted` label. 315 | 316 | With this many accepted PRs, we can also take a look at some interesting averages determined from 317 | the accepted PR/MRs. The average accepted PR/MR... 318 | 319 | - ...contained **{{= c(data.PRs.averageAcceptedPRCommits) }} commits** 320 | - ...added/edited/removed **{{= c(data.PRs.averageAcceptedPRFiles) }} files** 321 | - ...made a total of **{{= c(data.PRs.averageAcceptedPRAdditions) }} additions** _(lines)_ 322 | - ...included **{{= c(data.PRs.averageAcceptedPRDeletions) }} deletions** _(lines)_ 323 | 324 | _Note that lines containing edits will be counted as both an addition and a deletion._ 325 | 326 | We can also take a look at all the different languages that we observed during Hacktoberfest. These 327 | are based on the primary language reported for the repository, and the number of accepted 328 | Hacktoberfest PRs that were submitted to that repository. Unfortunately, GitLab does not expose 329 | language information via their API, so this only considers GitHub PRs. 330 | 331 | {{~ data.PRs.totalAcceptedPRsByLanguage.slice(0, 10) :item:i }} 332 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} 333 | ({{= p(item[1] / data.PRs.totalAcceptedPRs) }} of all accepted PRs) 334 | {{~ }} 335 | 336 | Bar chart of accepted PR/MRs by most popular days 337 | 338 | Hacktoberfest happens throughout the month of October, with participants allowed to submit 339 | pull/merge requests at any point from October 1 - 31 in any timezone. However, there tends to be 340 | large spikes in submitted PR/MRs towards the start and end of the month as folks are reminded to 341 | get them in to count! Let's take a look at the most popular days during Hacktoberfest by accepted 342 | PR/MR creation this year: 343 | 344 | {{~ data.PRs.totalPRsByDay :item:i }} 345 | {{= i + 1 }}. {{= item[0] }}: {{= c(item[1]) }} 346 | ({{= p(item[1] / data.PRs.totalAcceptedPRs) }} of all accepted PRs) 347 | {{~ }} 348 | 349 | 350 |

351 | 352 | ## Diving in: Spam 353 | 354 | After the issues Hacktoberfest faced at the start of the 2020 event, spam was top of mind for our 355 | whole team this year as we planned and launched Hacktoberfest {{= data.readme.year }}. We kept the 356 | rules the same as we'd landed on last year, with Hacktoberfest being an opt-in event for 357 | repositories, and our revised standards on quality contributions to make it easier for participants 358 | to understand what is expected of them when contributing to open source as part of Hacktoberfest. 359 | 360 | **Our efforts to reduce spam can be seen in our data, with only {{= c(data.PRs.totalSpamPRs) }} 361 | ({{= p(data.PRs.totalSpamPRs / data.PRs.totalPRs) }}) pull/merge requests being flagged as spam by 362 | maintainers (or identified as spam by our automated logic).** _(Of course, we can only report on 363 | what we see in our data here, and do acknowledge that folks may have received spam that wasn't 364 | flagged so won't be represented in our reporting)._ 365 | 366 | We also took a stronger stance on excluding repositories reported by the community that did not 367 | align with our values, mostly repositories encouraging low effort contributions to allow folks to 368 | quickly win Hacktoberfest. Pull/merge requests to a repository that had been excluded from 369 | Hacktoberfest, based on community reports, would not be counted for winning Hacktoberfest (but also 370 | would not count against individual users in terms of disqualification). 371 | 372 | **Excluded repositories accounted for a much larger swathe of pull/merge requests during 373 | Hacktoberfest, with {{= c(data.PRs.totalExcludedPRs) }} 374 | ({{= p(data.PRs.totalExcludedPRs / data.PRs.totalPRs) }}) being discounted due to being submitted 375 | to an excluded repository.** 376 | 377 | If we plot all pull/merge requests during Hacktoberfest by day, broken down by state, the impact 378 | that excluded repositories had can be seen clearly, and also shows that there are significant spikes 379 | at the start and end of Hacktoberfest as folks trying to cheat the system tend to do so as 380 | Hacktoberfest launches and its on their mind, or when they get our reminder email that Hacktoberfest 381 | is ending soon: 382 | 383 | ![Stacked area plot of PR/MRs by created at day and state](generated/prs_by_state_stacked.png) 384 | 385 | Doughnut diagram of reported repositories by review state 386 | 387 | For transparency, we can also take a look at the excluded repositories we processed for 388 | Hacktoberfest {{= data.readme.year }}. A large part of this list was prior excluded repositories 389 | from previous Hacktoberfest years which were persisted across to this year. However, a form was 390 | available on the site for members of our community to report repositories that they felt did not 391 | follow our values, with automation in place to process these reports and exclude repositories that 392 | were repeatedly reported, as well as reports being reviewed by our team. 393 | 394 | In total, Hacktoberfest {{= data.readme.year }} had {{= c(data.Repos.totalReposExcluded) }} 395 | repositories that were actively excluded, 396 | {{= p(data.Repos.totalReposExcluded / data.Repos.totalReposReported) }} of the total repositories 397 | reported. Only {{= c(data.Repos.totalReposPermitted) }} repositories were permitted after having 398 | been reported and subsequently reviewed by our team. Unfortunately, 399 | {{= c(data.Repos.totalReposUnreviewed) }} 400 | ({{= p(data.Repos.totalReposUnreviewed / data.Repos.totalReposReported) }}) of the repositories that 401 | were reported by the community were never reviewed by our team, and did not meet a threshold that 402 | triggered any automation for exclusion. 403 | 404 | 405 |

406 | 407 | ## Wrapping up 408 | 409 | Well, that's all the stats I've generated from the Hacktoberfest {{= data.readme.year }} raw data -- 410 | you can find the raw output of the stats generation script in the 411 | [`generated/stats.txt`](generated/stats.txt) file, as well as all the graphics which are housed in 412 | [`generated`](generated) directory. 413 | 414 | If there is anything more you'd like to see/know, please feel free to reach out and ask, I'll be 415 | more than happy to generate it if possible. 416 | 417 | All the scripts used to generate these stats & graphics are contained in this repository, in the 418 | [`src`](src) directory. I have some more information about this in the 419 | [CONTRIBUTING.md](CONTRIBUTING.md) file, including a schema for the input data, however, the 420 | Hacktoberfest {{= data.readme.year }} raw data, much like previous years' data, isn't public. 421 | 422 | Author: [Matt Cowley](https://mattcowley.co.uk/) - If you notice any errors within this document 423 | please let me know, and I will endeavour to correct them. 💙 424 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hacktoberfest 2023 Stats 2 | 3 | Hi there, 👋 4 | 5 | I'm [Matt Cowley](https://mattcowley.co.uk/), Senior Software Engineer II at [DigitalOcean](https://digitalocean.com/). 6 | 7 | I work on a bunch of things at DigitalOcean, with a great mix of engineering, developer relations and community management. And, part of what I get to work on is helping out with Hacktoberfest, including being the lead engineer for the backend that powers the event. 8 | 9 | Welcome to my deeper dive on the data and stats for Hacktoberfest 2023, expanding on what we already shared in our [recap blog post](https://www.digitalocean.com/blog/10th-anniversary-hacktoberfest-recap). 10 | 11 | ## At a glance 12 | 13 | What did we accomplish together in October 2023? These are the highlights from Hacktoberfest #10: 14 | 15 | - Registered users: **98,855** 16 | - Engaged users (1-3 accepted PR/MRs): **8,325** 17 | - Completed users (4+ accepted PR/MRs): **15,523** 18 | - Accepted PR/MRs: **118,469** 19 | - Active repositories (1+ accepted PR/MRs): **31,711** 20 | - Countries represented by registered users: **184** 21 | - Countries represented by completed users: **121** 22 | 23 | > Take a read of our overall recap blog post for Hacktoberfest 2023 here: 24 | > [www.digitalocean.com/blog/10th-anniversary-hacktoberfest-recap](https://www.digitalocean.com/blog/10th-anniversary-hacktoberfest-recap) 25 | 26 | ## Application states 27 | 28 | Before jumping in and looking at the data in detail, we should first cover some important information about how we categorise users and pull/merge requests in the Hacktoberfest application and in the data used here. 29 | 30 | For users, there are four key states that you'll see: 31 | 32 | - **Completed**: A user that submitted four or more accepted PR/MRs during Hacktoberfest. 33 | - **Engaged**: A user that submitted between one and three accepted PR/MRs during Hacktoberfest. 34 | - **Registered**: A user that completed the registration flow for Hacktoberfest, but did not submit 35 | any PR/MRs that were accepted. 36 | - **Disqualified**: A user that was disqualified for submitting 2 or more spammy PR/MRs, 37 | irrespective of how many accepted PR/MRs they may have also had. 38 | 39 | For pull/merge requests, there are six states used to process them that you'll see: 40 | 41 | - **Accepted**: A PR/MR that was accepted by a project maintainer, either by being merged or 42 | approved in a participating repository, or by being given the `hacktoberfest-accepted` label. 43 | - **Not accepted**: Any PR/MR that was submitted to a participating repository (having the 44 | `hacktoberfest` topic), but that was not actively accepted by a maintainer. 45 | - **Not participating**: Any PR/MR that was submitted by a participant to a repository that was not 46 | participating in Hacktoberfest (i.e. having the `hacktoberfest` topic, or adding the 47 | `hacktoberfest-accepted` label to specific PRs). 48 | - **Invalid**: Any PR/MR that was given a label containing the word `invalid` by a maintainer. Any 49 | PR/MR with a matching label was not counted towards a participant's total. 50 | - **Spam**: Any PR/MR that was given a label by a maintainer containing the 'spam', or PR/MRs that 51 | our abuse logic detected as spam. These are not counted toward winning, and also count toward a 52 | user being disqualified. 53 | - **Excluded**: Any PR/MR that was submitted to a repository that has been excluded from 54 | Hacktoberfest for not following our values. These do not count toward winning, nor do they count 55 | toward a user being disqualified. 56 | 57 | ## Diving in: Users 58 | 59 | This year, Hacktoberfest had **98,855** folks who went through our registration flow for the event. Spam has been a huge focus for us throughout the event, as with previous years, and so during this flow folks were reminded about our rules and values for the event with clear and simple language, as well as agreeing to a rule that folks with two or more PR/MRs identified as spam by maintainers would be disqualified. More on this later. 60 | 61 | During the registration flow, folks can also choose to tell us which country they are from--this helps us better understand, and cater to, the global audience for the event--and **99.84%** of them did so. 62 | 63 |

64 | Bar chart of the top countries for completed users 65 | Bar chart of the top countries for all registered users 66 |

67 | 68 | The top country, by far, was once again India with **54,883 (55.52%)** registrants self-identifying as being from there, showing how much of a reach open-source, and tech in general, has there. 69 | 70 | We can see the true global reach of Hacktoberfest and open-source by looking at more of the top countries based on registrations: 71 | 72 | 1. India: 54,883 (55.52%) 73 | 2. United States: 4,815 (4.87%) 74 | 3. Brazil: 2,640 (2.67%) 75 | 4. Indonesia: 1,742 (1.76%) 76 | 5. Pakistan: 1,692 (1.71%) 77 | 6. Nigeria: 1,664 (1.68%) 78 | 7. Germany: 1,560 (1.58%) 79 | 8. Sri Lanka: 1,418 (1.43%) 80 | 9. United Kingdom: 1,078 (1.09%) 81 | 10. Canada: 1,067 (1.08%) 82 | 83 | In total, **184 countries** were represented by folks that registered for the 2023 event. 84 | 85 | We can also look at just the users that completed Hacktoberfest, and see how the countries are distributed for those users: 86 | 87 | 1. India: 7,905 (50.92%) 88 | 2. United States: 823 (5.30%) 89 | 3. Germany: 418 (2.69%) 90 | 4. Brazil: 363 (2.34%) 91 | 5. Sri Lanka: 338 (2.18%) 92 | 6. Indonesia: 222 (1.43%) 93 | 7. United Kingdom: 218 (1.40%) 94 | 8. Pakistan: 208 (1.34%) 95 | 9. France: 188 (1.21%) 96 | 10. Canada: 173 (1.11%) 97 | 98 | Doughnut diagram of users by application state 99 | 100 | Of course, there's more to Hacktoberfest than just registering for the event, folks actually submit PR/MRs to open-source projects! This year, we had **23,848 users (24.12% of total registrations)** that submitted one or more PR/MRs that were accepted by maintainers. Of those, **15,523 (65.09%) (15.70% of total registrations)** went on to submit at least four accepted PR/MRs to successfully complete Hacktoberfest. 101 | 102 | Impressively, we saw that **8,424 users (54.27% of total completed)** submitted more than 4 accepted PR/MRs, going above and beyond to contribute to open-source outside the goal set for completing Hacktoberfest. 103 | 104 | This year, Hacktoberfest removed the free t-shirt as a reward for completing Hacktoberfest, instead replacing it with a digital reward kit unlocked once you had four accepted PR/MRs, and a digital badge from Holopin that levelled up with each PR/MR accepted on your journey from registration to completion. While we still saw many folks register and engage with Hacktoberfest, the numbers are much lower than previous years, likely due to this change in reward, and while disappointing this was expected. 105 | 106 | Sadly, 82 users were disqualified this year (0.08% of total registrations), with an additional 219 (0.22% of total registrations) warned. Disqualification of users happen automatically if two or more of their PR/MRs are actively identified as spam by project maintainers, with users being sent a warning email (and shown a notice on their profile) when they have one PR/MR that is identified as spam. We were very happy to see how low this number was though, indicating to us that our efforts to educate and remind contributors of the quality standards expected of them during Hacktoberfest are working. _(Of course, we can only report on what we see in our data here, and do acknowledge that folks may have received spam that wasn't flagged so won't be represented in our reporting)._ 107 | 108 | 109 |

110 | 111 | Hacktoberfest supported multiple providers this year, GitHub & GitLab. Registrants could choose to link just one provider to their account, or multiple if they desired, with contributions from each provider combined into a single record for the user. 112 | 113 | Based on this we can take a look at the most popular providers for open-source based on some Hacktoberfest-specific metrics. First, we can see that based on registrations, **the most popular provider was GitHub with 98,515 registrants (99.66%).** 114 | 115 | 1. GitHub: 98,515 116 | (99.66% of registered users) 117 | 2. GitLab: 1,306 118 | (1.32% of registered users) 119 | 120 | _Users were able to link one or more providers to their account, so the counts here may sum to more than the total number of users registered._ 121 | 122 | We can also look at a breakdown of users that were engaged (1-3 accepted PR/MRs) and users that completed Hacktoberfest (4+ PR/MRs) by provider. 123 | 124 | Engaged users by provider: 125 | 126 | - GitHub: 8,319 127 | (99.93% of engaged users) 128 | - GitLab: 135 129 | (1.62% of engaged users) 130 | 131 | Completed users by provider: 132 | 133 | - GitHub: 15,500 134 | (99.85% of completed users) 135 | - GitLab: 343 136 | (2.21% of completed users) 137 | 138 | Bar chart of users by experience level 139 | 140 | When registering for Hacktoberfest, we also asked users for some optional self-identification around their experience with contributing to open-source, and how they intended to contribute. First, we can take a look at the experience level users self-identified as having when registering: 141 | 142 | - Newbie: 56,566 143 | (57.22% of registered users) 144 | - Familiar: 26,969 145 | (27.28% of registered users) 146 | - Experienced: 10,971 147 | (11.10% of registered users) 148 | 149 | _4,349 users did not self-identify their experience level._ 150 | 151 | We can compare this to the breakdown of users that completed Hacktoberfest by experience level: 152 | 153 | - Newbie: 6,337 154 | (40.82% of completed users) 155 | - Familiar: 4,963 156 | (31.97% of completed users) 157 | - Experienced: 3,409 158 | (21.96% of completed users) 159 | 160 | _814 users who completed Hacktoberfest did not self-identify their experience when registering._ 161 | 162 | 163 |

164 | 165 | Not everyone is comfortable writing code, and so Hacktoberfest focused on encouraging more contributors to get involved with open-source this year through non-code contributors. We can look at what contribution types users indicated they intended to make during Hacktoberfest when registering (they could pick multiple, or none): 166 | 167 | - Code: 89,786 168 | (90.83% of registered users) 169 | - Non-code: 46,257 170 | (46.79% of registered users) 171 | 172 | _Of course, this is only what users indicated they intended to do, and doesn't necessarily reflect their actual contributions they ended up making to open-source (determining what is and what isn't a "non-code" PR/MR would be a difficult task)._ 173 | 174 | This year, we also asked users during registration whether they were students or not, to give us a better sense of the audience that is participating in Hacktoberfest. We can see that **60,826 users (61.53% of registered users)** indicated that they were students when registering. 175 | 176 | Bar chart of users by AI/ML interest 177 | 178 | Folks were also asked if they'd be interested in contributing to AI/ML projects specifically during Hacktoberfest, as this area of open-source is growing rapidly and we wanted to see if there was interest in it. 179 | 180 | We can see that **50,550 users (51.14% of registered users)** indicated they were actively interested in AI/ML projects, while **30,953 users (31.31% of registered users)** indicated they were not interested in AI/ML projects (this was an optional question, with 17,352 users not providing a preference). 181 | 182 | While we obviously can't know for sure the preference of those that did not interact with this question, there was a much larger portion of folks registering that did not engage with this question than other questions (17.55%, compared to just 4.40% that did not indicate their experience level), which is potentially an indicator that folks were unfamiliar with or disinterested in AI/ML. 183 | 184 | 185 |

186 | 187 | As with previous years of Hacktoberfest, users had to submit PR/MRs to participating projects during October that then had to be accepted by maintainers during October. If a user submitted four or more PR/MRs, then they completed Hacktoberfest. However, not everyone hit the 4 PR/MR target, with some falling short, and many going beyond the target to contribute further. 188 | 189 | We can see how many accepted PR/MRs each user had and bucket them: 190 | 191 | - 1 192 | PR/MR: 5,057 193 | (32.58%) 194 | - 2 195 | PRs/MRs: 2,114 196 | (13.62%) 197 | - 3 198 | PRs/MRs: 1,163 199 | (7.49%) 200 | - 4 201 | PRs/MRs: 7,120 202 | (45.87%) 203 | - 5 204 | PRs/MRs: 3,079 205 | (19.84%) 206 | - 6 207 | PRs/MRs: 1,629 208 | (10.49%) 209 | - 7 210 | PRs/MRs: 916 211 | (5.90%) 212 | - 8 213 | PRs/MRs: 588 214 | (3.79%) 215 | - 9 216 | PRs/MRs: 443 217 | (2.85%) 218 | - 10+ 219 | PRs/MRs: 1,769 220 | (11.40%) 221 | 222 | Looking at this, we can see that quite a few users only managed to get 1 accepted PR/MR, but after that it quickly trailed off for 2 and 3 PR/MRs. It seems like the target of 4 PR/MRs encouraged many users to push through to getting all 4 PR/MRs created/accepted if they got that first one completed. 223 | 224 | ![Bar chart of users by accepted PR/MRs](generated/users_by_prs_extended_column.png) 225 | 226 | ## Diving in: Pull/Merge Requests 227 | 228 | Doughnut diagram of PR/MRs by application state 229 | 230 | Now on to what you've been waiting for, and the core of Hacktoberfest itself, the pull/merge requests. This year Hacktoberfest tracked **267,408** PR/MRs that were within the bounds of the Hacktoberfest event, and **118,469 (44.30%)** of those went on to be accepted! 231 | 232 | Unfortunately, not every pull/merge request can be accepted though, for one reason or another, and this year we saw that there were **25,112 (9.39%)** PR/MRs that were submitted to participating repositories but that were not accepted by maintainers, as well as **82,319 (30.78%)** PR/MRs submitted by Hacktoberfest participants to repositories that were not participating in Hacktoberfest. As a reminder to folks, repositories opt-in to participating in Hacktoberfest by adding the `hacktoberfest` topic to their repository (or individual PR/MRs can be opted-in with the `hacktoberfest-accepted` label). 233 | 234 | Spam is also a big issue that we focus on reducing during Hacktoberfest, and we tracked the number of PR/MRs that were identified by maintainers as spam, as well as those that were caught by automation we'd written to stop spammy users. We'll talk more about all-things-spam later on. 235 | 236 | 237 |

238 | 239 | This year, Hacktoberfest supported multiple providers that contributors could use to submit contributions to open-source projects. Let's take a look at the breakdown of PR/MRs per provider: 240 | 241 | 1. GitHub: 266,560 242 | (99.68% of total PR/MRs) 243 | 2. GitLab: 848 244 | (0.32% of total PR/MRs) 245 | 246 | PRs and MRs that are accepted by maintainers for Hacktoberfest aren't necessarily merged -- Hacktoberfest supports multiple different ways for a maintainer to indicate that a PR/MR is legitimate and should be counted. PR/MRs can be merged, or they can be given the `hacktoberfest-accepted` label, or maintainers can leave an overall approving review. 247 | 248 | Of the accepted PR/MRs, **116,233 (98.11%)** were merged into the repository, and **32,676 (27.58%)** were approved by a maintainer. Note that there may be overlap here, as a PR/MR may have been approved and then merged. Unfortunately, we don't have direct aggregated data for the `hacktoberfest-accepted` label. 249 | 250 | With this many accepted PRs, we can also take a look at some interesting averages determined from the accepted PR/MRs. The average accepted PR/MR... 251 | 252 | - ...contained **2.91 commits** 253 | - ...added/edited/removed **11.2 files** 254 | - ...made a total of **1,117.42 additions** _(lines)_ 255 | - ...included **400.76 deletions** _(lines)_ 256 | 257 | _Note that lines containing edits will be counted as both an addition and a deletion._ 258 | 259 | We can also take a look at all the different languages that we observed during Hacktoberfest. These are based on the primary language reported for the repository, and the number of accepted Hacktoberfest PRs that were submitted to that repository. Unfortunately, GitLab does not expose language information via their API, so this only considers GitHub PRs. 260 | 261 | 1. JavaScript: 16,697 262 | (14.09% of all accepted PRs) 263 | 2. Python: 14,851 264 | (12.54% of all accepted PRs) 265 | 3. TypeScript: 12,879 266 | (10.87% of all accepted PRs) 267 | 4. HTML: 11,175 268 | (9.43% of all accepted PRs) 269 | 5. C++: 10,679 270 | (9.01% of all accepted PRs) 271 | 6. Java: 7,759 272 | (6.55% of all accepted PRs) 273 | 7. Jupyter Notebook: 5,028 274 | (4.24% of all accepted PRs) 275 | 8. Ruby: 4,389 276 | (3.70% of all accepted PRs) 277 | 9. CSS: 3,362 278 | (2.84% of all accepted PRs) 279 | 10. Go: 3,338 280 | (2.82% of all accepted PRs) 281 | 282 | Bar chart of accepted PR/MRs by most popular days 283 | 284 | Hacktoberfest happens throughout the month of October, with participants allowed to submit pull/merge requests at any point from October 1 - 31 in any timezone. However, there tends to be large spikes in submitted PR/MRs towards the start and end of the month as folks are reminded to get them in to count! Let's take a look at the most popular days during Hacktoberfest by accepted PR/MR creation this year: 285 | 286 | 1. 2023-10-02: 5,672 287 | (4.79% of all accepted PRs) 288 | 2. 2023-10-01: 5,418 289 | (4.57% of all accepted PRs) 290 | 3. 2023-10-03: 4,615 291 | (3.90% of all accepted PRs) 292 | 4. 2023-10-24: 4,589 293 | (3.87% of all accepted PRs) 294 | 5. 2023-10-04: 4,320 295 | (3.65% of all accepted PRs) 296 | 6. 2023-10-11: 4,241 297 | (3.58% of all accepted PRs) 298 | 7. 2023-10-07: 4,077 299 | (3.44% of all accepted PRs) 300 | 8. 2023-10-09: 4,064 301 | (3.43% of all accepted PRs) 302 | 9. 2023-10-14: 4,060 303 | (3.43% of all accepted PRs) 304 | 10. 2023-10-05: 3,989 305 | (3.37% of all accepted PRs) 306 | 11. 2023-10-15: 3,946 307 | (3.33% of all accepted PRs) 308 | 12. 2023-10-10: 3,943 309 | (3.33% of all accepted PRs) 310 | 13. 2023-10-08: 3,939 311 | (3.32% of all accepted PRs) 312 | 14. 2023-10-31: 3,907 313 | (3.30% of all accepted PRs) 314 | 15. 2023-10-16: 3,851 315 | (3.25% of all accepted PRs) 316 | 317 | 318 |

319 | 320 | ## Diving in: Spam 321 | 322 | After the issues Hacktoberfest faced at the start of the 2020 event, spam was top of mind for our whole team this year as we planned and launched Hacktoberfest 2023. We kept the rules the same as we'd landed on last year, with Hacktoberfest being an opt-in event for repositories, and our revised standards on quality contributions to make it easier for participants to understand what is expected of them when contributing to open source as part of Hacktoberfest. 323 | 324 | **Our efforts to reduce spam can be seen in our data, with only 332 (0.12%) pull/merge requests being flagged as spam by maintainers (or identified as spam by our automated logic).** _(Of course, we can only report on what we see in our data here, and do acknowledge that folks may have received spam that wasn't flagged so won't be represented in our reporting)._ 325 | 326 | We also took a stronger stance on excluding repositories reported by the community that did not align with our values, mostly repositories encouraging low effort contributions to allow folks to quickly win Hacktoberfest. Pull/merge requests to a repository that had been excluded from Hacktoberfest, based on community reports, would not be counted for winning Hacktoberfest (but also would not count against individual users in terms of disqualification). 327 | 328 | **Excluded repositories accounted for a much larger swathe of pull/merge requests during Hacktoberfest, with 39,859 (14.91%) being discounted due to being submitted to an excluded repository.** 329 | 330 | If we plot all pull/merge requests during Hacktoberfest by day, broken down by state, the impact that excluded repositories had can be seen clearly, and also shows that there are significant spikes at the start and end of Hacktoberfest as folks trying to cheat the system tend to do so as Hacktoberfest launches and its on their mind, or when they get our reminder email that Hacktoberfest is ending soon: 331 | 332 | ![Stacked area plot of PR/MRs by created at day and state](generated/prs_by_state_stacked.png) 333 | 334 | Doughnut diagram of reported repositories by review state 335 | 336 | For transparency, we can also take a look at the excluded repositories we processed for Hacktoberfest 2023. A large part of this list was prior excluded repositories from previous Hacktoberfest years which were persisted across to this year. However, a form was available on the site for members of our community to report repositories that they felt did not follow our values, with automation in place to process these reports and exclude repositories that were repeatedly reported, as well as reports being reviewed by our team. 337 | 338 | In total, Hacktoberfest 2023 had 8,329 repositories that were actively excluded, 339 | 70.78% of the total repositories reported. Only 80 repositories were permitted after having been reported and subsequently reviewed by our team. Unfortunately, 3,359 (28.54%) of the repositories that were reported by the community were never reviewed by our team, and did not meet a threshold that triggered any automation for exclusion. 340 | 341 | 342 |

343 | 344 | ## Wrapping up 345 | 346 | Well, that's all the stats I've generated from the Hacktoberfest 2023 raw data -- you can find the raw output of the stats generation script in the [`generated/stats.txt`](generated/stats.txt) file, as well as all the graphics which are housed in [`generated`](generated) directory. 347 | 348 | If there is anything more you'd like to see/know, please feel free to reach out and ask, I'll be more than happy to generate it if possible. 349 | 350 | All the scripts used to generate these stats & graphics are contained in this repository, in the [`src`](src) directory. I have some more information about this in the [CONTRIBUTING.md](CONTRIBUTING.md) file, including a schema for the input data, however, the Hacktoberfest 2023 raw data, much like previous years' data, isn't public. 351 | 352 | Author: [Matt Cowley](https://mattcowley.co.uk/) - If you notice any errors within this document please let me know, and I will endeavour to correct them. 💙 353 | -------------------------------------------------------------------------------- /generated/prs_accepted_by_approval_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_accepted_by_approval_bar.png -------------------------------------------------------------------------------- /generated/prs_accepted_by_merged_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_accepted_by_merged_bar.png -------------------------------------------------------------------------------- /generated/prs_by_day_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_by_day_bar.png -------------------------------------------------------------------------------- /generated/prs_by_language_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_by_language_doughnut.png -------------------------------------------------------------------------------- /generated/prs_by_language_spline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_by_language_spline.png -------------------------------------------------------------------------------- /generated/prs_by_state_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_by_state_doughnut.png -------------------------------------------------------------------------------- /generated/prs_by_state_stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/prs_by_state_stacked.png -------------------------------------------------------------------------------- /generated/repos_by_language_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/repos_by_language_doughnut.png -------------------------------------------------------------------------------- /generated/repos_by_license_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/repos_by_license_bar.png -------------------------------------------------------------------------------- /generated/repos_reported_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/repos_reported_doughnut.png -------------------------------------------------------------------------------- /generated/stats.txt: -------------------------------------------------------------------------------- 1 | Started 11/22/2023, 5:31:57 PM 2 | 3 | 4 | ---- 5 | Readme Stats 6 | ---- 7 | 8 | Registered users: 98,855 9 | Completed users: 15,523 10 | Accepted PR/MRs: 118,469 11 | Active repositories (1+ accepted PR/MRs): 31,711 12 | Countries represented by registered users: 184 13 | Countries represented by completed users: 121 14 | Day with most accepted PR/MRs submitted: 2023-10-02 (4.79%) 15 | Most common repository language in accepted PR/MRs: JavaScript (14.09%) 16 | 17 | Region-specific: 18 | Registered users in the US: 4,815 19 | Completed users in the US: 823 20 | Registered users in India: 54,883 21 | Completed users in India: 7,905 22 | 23 | 24 | ---- 25 | PR Stats 26 | ---- 27 | 28 | Total PR/MRs: 267,408 29 | Accepted PR/MRs: 118,469 (44.30%) 30 | Unaccepted PR/MRs: 107,431 (40.17%) 31 | of which were not accepted by a maintainer: 25,112 (23.38%) 32 | of which were not in a participating repo: 82,319 (76.62%) 33 | Invalid PR/MRs: 41,432 (15.49%) 34 | of which were in an excluded repo: 39,859 (96.20%) 35 | of which were labeled as invalid: 1,241 (3.00%) 36 | of which were identified as spam: 332 (0.80%) 37 | PR/MRs that could not be processed: 76 (0.03%) 38 | (PR/MR data failed to load from the provider, such as if the user were suspended from the provider's platform) 39 | 40 | Total PR/MRs by provider: 41 | GitHub: 266,560 (99.68%) 42 | GitLab: 848 (0.32%) 43 | 44 | Accepted PR/MRs by merge status: 45 | Merged: 116,233 (98.11%) 46 | Not: 2,236 (1.89%) 47 | 48 | Accepted PR/MRs by approving review: 49 | Approved: 32,676 (27.58%) 50 | Not: 85,793 (72.42%) 51 | 52 | Accepted PR/MRs by language: 341 languages 53 | JavaScript: 16,697 (14.09%) 54 | Python: 14,851 (12.54%) 55 | TypeScript: 12,879 (10.87%) 56 | HTML: 11,175 (9.43%) 57 | C++: 10,679 (9.01%) 58 | Java: 7,759 (6.55%) 59 | Jupyter Notebook: 5,028 (4.24%) 60 | Ruby: 4,389 (3.70%) 61 | CSS: 3,362 (2.84%) 62 | Go: 3,338 (2.82%) 63 | PHP: 2,945 (2.49%) 64 | Rust: 2,119 (1.79%) 65 | C: 1,870 (1.58%) 66 | C#: 1,584 (1.34%) 67 | Dart: 1,460 (1.23%) 68 | Shell: 1,403 (1.18%) 69 | Kotlin: 1,240 (1.05%) 70 | Nix: 873 (0.74%) 71 | Vue: 697 (0.59%) 72 | MDX: 629 (0.53%) 73 | Markdown: 494 (0.42%) 74 | Swift: 470 (0.40%) 75 | DM: 445 (0.38%) 76 | Svelte: 344 (0.29%) 77 | Astro: 294 (0.25%) 78 | Dockerfile: 257 (0.22%) 79 | SCSS: 253 (0.21%) 80 | Lua: 208 (0.18%) 81 | HCL: 208 (0.18%) 82 | Jinja: 170 (0.14%) 83 | EJS: 162 (0.14%) 84 | Julia: 155 (0.13%) 85 | PowerShell: 151 (0.13%) 86 | Makefile: 133 (0.11%) 87 | Elixir: 128 (0.11%) 88 | Perl: 108 (0.09%) 89 | Assembly: 103 (0.09%) 90 | Groovy: 91 (0.08%) 91 | R: 91 (0.08%) 92 | YAML: 90 (0.08%) 93 | BASIC: 88 (0.07%) 94 | Vala: 88 (0.07%) 95 | Scala: 72 (0.06%) 96 | TeX: 65 (0.05%) 97 | GDScript: 63 (0.05%) 98 | Haskell: 59 (0.05%) 99 | Puppet: 51 (0.04%) 100 | Nim: 43 (0.04%) 101 | Ballerina: 41 (0.03%) 102 | ABAP: 39 (0.03%) 103 | 104 | Top days by accepted PR/MRs: 105 | October 2: 5,672 (4.79%) 106 | October 1: 5,418 (4.57%) 107 | October 3: 4,615 (3.90%) 108 | October 24: 4,589 (3.87%) 109 | October 4: 4,320 (3.65%) 110 | October 11: 4,241 (3.58%) 111 | October 7: 4,077 (3.44%) 112 | October 9: 4,064 (3.43%) 113 | October 14: 4,060 (3.43%) 114 | October 5: 3,989 (3.37%) 115 | October 15: 3,946 (3.33%) 116 | October 10: 3,943 (3.33%) 117 | October 8: 3,939 (3.32%) 118 | October 31: 3,907 (3.30%) 119 | October 16: 3,851 (3.25%) 120 | 121 | On average, an accepted PR/MR had: 122 | 3 commits 123 | 11 modified files 124 | 1,117 additions 125 | 401 deletions 126 | 127 | 128 | ---- 129 | User Stats 130 | ---- 131 | 132 | Total Users: 98,855 133 | Users that submitted no accepted PR/MRs: 75,007 (75.88%) 134 | Users that submitted 1-3 accepted PR/MRs: 8,325 (8.42%) 135 | Users that submitted 4+ accepted PR/MRs: 15,523 (15.70%) 136 | Users that were warned (1 spammy PR/MR): 219 (0.22%) 137 | Users that were disqualified (2+ spammy PR/MRs): 82 (0.08%) 138 | 139 | Users by number of accepted PRs/MR submitted: 140 | 1 PR/MR: 5,057 (5.12%) 141 | 2 PRs/MRs: 2,114 (2.14%) 142 | 3 PRs/MRs: 1,163 (1.18%) 143 | 4 PRs/MRs: 7,120 (7.20%) 144 | 5 PRs/MRs: 3,079 (3.11%) 145 | 6 PRs/MRs: 1,629 (1.65%) 146 | 7 PRs/MRs: 916 (0.93%) 147 | 8 PRs/MRs: 588 (0.59%) 148 | 9 PRs/MRs: 443 (0.45%) 149 | 10+ PRs/MRs: 1,769 (1.79%) 150 | 151 | Top countries by registrations: 184 countries 152 | 1. India: 54,883 (55.52%) 153 | 2. United States: 4,815 (4.87%) 154 | 3. Brazil: 2,640 (2.67%) 155 | 4. Indonesia: 1,742 (1.76%) 156 | 5. Pakistan: 1,692 (1.71%) 157 | 6. Nigeria: 1,664 (1.68%) 158 | 7. Germany: 1,560 (1.58%) 159 | 8. Sri Lanka: 1,418 (1.43%) 160 | 9. United Kingdom: 1,078 (1.09%) 161 | 10. Canada: 1,067 (1.08%) 162 | 11. Nepal: 842 (0.85%) 163 | 12. France: 691 (0.70%) 164 | 13. Bangladesh: 684 (0.69%) 165 | 14. Spain: 586 (0.59%) 166 | 15. Italy: 433 (0.44%) 167 | 16. Netherlands: 429 (0.43%) 168 | 17. Poland: 427 (0.43%) 169 | 18. Kenya: 423 (0.43%) 170 | 19. Mexico: 374 (0.38%) 171 | 20. Australia: 343 (0.35%) 172 | 21. Thailand: 324 (0.33%) 173 | 22. Philippines: 272 (0.28%) 174 | 23. Korea, Republic of: 261 (0.26%) 175 | 24. Japan: 221 (0.22%) 176 | 25. Sweden: 220 (0.22%) 177 | + 159 more... 178 | 160 (0.16%) users did not specify their country 179 | 180 | Top countries by completions: 121 countries 181 | 1. India: 7,905 (50.92%) 182 | 2. United States: 823 (5.30%) 183 | 3. Germany: 418 (2.69%) 184 | 4. Brazil: 363 (2.34%) 185 | 5. Sri Lanka: 338 (2.18%) 186 | 6. Indonesia: 222 (1.43%) 187 | 7. United Kingdom: 218 (1.40%) 188 | 8. Pakistan: 208 (1.34%) 189 | 9. France: 188 (1.21%) 190 | 10. Canada: 173 (1.11%) 191 | 11. Nepal: 157 (1.01%) 192 | 12. Nigeria: 131 (0.84%) 193 | 13. Poland: 122 (0.79%) 194 | 14. Netherlands: 122 (0.79%) 195 | 15. Spain: 114 (0.73%) 196 | 16. Bangladesh: 106 (0.68%) 197 | 17. Italy: 94 (0.61%) 198 | 18. Japan: 73 (0.47%) 199 | 19. Australia: 73 (0.47%) 200 | 20. Thailand: 62 (0.40%) 201 | 21. Switzerland: 59 (0.38%) 202 | 22. Korea, Republic of: 57 (0.37%) 203 | 23. Sweden: 54 (0.35%) 204 | 24. Viet Nam: 46 (0.30%) 205 | 25. Austria: 43 (0.28%) 206 | + 96 more... 207 | 25 (0.16%) users did not specify their country 208 | 209 | Registered users by provider: 210 | (Users were able to link one, or both, of the supported providers to their Hacktoberfest account) 211 | GitHub: 98,515 (99.66%) 212 | GitLab: 1,306 (1.32%) 213 | 214 | Engaged (1-3 PR/MRs) users by provider: 215 | (Users were able to link one, or both, of the supported providers to their Hacktoberfest account) 216 | GitHub: 8,319 (99.93%) 217 | GitLab: 135 (1.62%) 218 | 219 | Completed (4+ PR/MRs) users by provider: 220 | (Users were able to link one, or both, of the supported providers to their Hacktoberfest account) 221 | GitHub: 15,500 (99.85%) 222 | GitLab: 343 (2.21%) 223 | 224 | Registered users by experience: 225 | (Users were able to optionally self-identify their experience level when registering) 226 | Newbie: 56,566 (57.22%) 227 | Familiar: 26,969 (27.28%) 228 | Experienced: 10,971 (11.10%) 229 | 4,349 (4.40%) users did not specify their experience level 230 | 231 | Completed (4+ PR/MRs) users by experience: 232 | (Users were able to optionally self-identify their experience level when registering) 233 | Newbie: 6,337 (40.82%) 234 | Familiar: 4,963 (31.97%) 235 | Experienced: 3,409 (21.96%) 236 | 814 (5.24%) users did not specify their experience level 237 | 238 | Registered users by intended contribution type: 239 | (Users were able to optionally self-identify what type of contribution(s) they intended to make when registering) 240 | (Users were able to select multiple options) 241 | Code: 89,786 (90.83%) 242 | Non-code: 46,257 (46.79%) 243 | 244 | Completed (4+ PR/MRs) users by intended contribution type: 245 | (Users were able to optionally self-identify what type of contribution(s) they intended to make when registering) 246 | (Users were able to select multiple options) 247 | Code: 14,150 (91.16%) 248 | Non-code: 7,373 (47.50%) 249 | 250 | Registered users by student status: 251 | (Users were able to optionally self-identify if they're a student when registering) 252 | Yes (enrolled student): 60,826 (61.53%) 253 | No (not a student): 28,469 (28.80%) 254 | 9,560 (9.67%) users did not specify their enrolment status 255 | 256 | Registered users by AI/ML interest: 257 | (Users were able to optionally self-identify if they're interested in AI/ML when registering) 258 | Interested: 50,550 (51.14%) 259 | Not Interested: 30,953 (31.31%) 260 | 17,352 (17.55%) users did not specify their interest in AI/ML 261 | 262 | 263 | ---- 264 | Repo Stats 265 | ---- 266 | 267 | Total active repos: 31,711 268 | (A repository was considered active if it received a PR/MR from a Hacktoberfest participant that was considered accepted) 269 | 270 | Total tracked repos: 362,732 271 | (A repository was considered tracked if it received a PR/MR from a Hacktoberfest participant, whether the repository was participating in Hacktoberfest or not) 272 | 273 | Reported repositories: 11,768 274 | (The Hacktoberfest community was able to report repositories that they did not feel followed our values) 275 | Excluded repositories: 8,329 (70.78%) 276 | Permitted repositories: 80 (0.68%) 277 | Unreviewed repositories: 3,359 (28.54%) 278 | 279 | Tracked repos by language: 338 languages 280 | JavaScript: 65,777 (18.13%) 281 | Python: 45,875 (12.65%) 282 | HTML: 35,283 (9.73%) 283 | TypeScript: 24,597 (6.78%) 284 | Java: 21,732 (5.99%) 285 | C++: 21,535 (5.94%) 286 | PHP: 13,263 (3.66%) 287 | CSS: 11,474 (3.16%) 288 | Go: 10,310 (2.84%) 289 | Jupyter Notebook: 10,228 (2.82%) 290 | C: 7,908 (2.18%) 291 | Ruby: 6,985 (1.93%) 292 | C#: 6,397 (1.76%) 293 | Shell: 6,257 (1.72%) 294 | Dart: 4,968 (1.37%) 295 | Rust: 4,443 (1.22%) 296 | Kotlin: 3,988 (1.10%) 297 | Vue: 2,743 (0.76%) 298 | Swift: 1,898 (0.52%) 299 | Dockerfile: 1,659 (0.46%) 300 | SCSS: 1,594 (0.44%) 301 | Lua: 930 (0.26%) 302 | HCL: 919 (0.25%) 303 | Makefile: 782 (0.22%) 304 | Elixir: 765 (0.21%) 305 | Svelte: 680 (0.19%) 306 | Perl: 667 (0.18%) 307 | EJS: 652 (0.18%) 308 | Scala: 630 (0.17%) 309 | PowerShell: 606 (0.17%) 310 | Julia: 572 (0.16%) 311 | Haskell: 531 (0.15%) 312 | Objective-C: 500 (0.14%) 313 | R: 491 (0.14%) 314 | TeX: 476 (0.13%) 315 | Astro: 371 (0.10%) 316 | Jinja: 315 (0.09%) 317 | Solidity: 308 (0.08%) 318 | MDX: 297 (0.08%) 319 | Clojure: 251 (0.07%) 320 | Nix: 251 (0.07%) 321 | Vim script: 250 (0.07%) 322 | Groovy: 240 (0.07%) 323 | Assembly: 216 (0.06%) 324 | Blade: 204 (0.06%) 325 | CoffeeScript: 202 (0.06%) 326 | Emacs Lisp: 196 (0.05%) 327 | Vim Script: 181 (0.05%) 328 | Mustache: 172 (0.05%) 329 | Smarty: 172 (0.05%) 330 | Tracked repos without a detectable language: 35,253 (9.72%) 331 | 332 | Tracked repos by license: 41 licenses 333 | MIT: 91,673 (25.27%) 334 | Apache-2.0: 21,459 (5.92%) 335 | GPL-3.0: 20,043 (5.53%) 336 | NOASSERTION: 16,512 (4.55%) 337 | BSD-3-Clause: 4,282 (1.18%) 338 | AGPL-3.0: 3,732 (1.03%) 339 | GPL-2.0: 2,809 (0.77%) 340 | CC0-1.0: 2,211 (0.61%) 341 | MPL-2.0: 1,489 (0.41%) 342 | Unlicense: 1,208 (0.33%) 343 | BSD-2-Clause: 964 (0.27%) 344 | CC-BY-4.0: 873 (0.24%) 345 | LGPL-3.0: 870 (0.24%) 346 | ISC: 628 (0.17%) 347 | LGPL-2.1: 612 (0.17%) 348 | CC-BY-SA-4.0: 324 (0.09%) 349 | 0BSD: 199 (0.05%) 350 | WTFPL: 196 (0.05%) 351 | EPL-2.0: 167 (0.05%) 352 | BSL-1.0: 93 (0.03%) 353 | EPL-1.0: 85 (0.02%) 354 | EUPL-1.2: 63 (0.02%) 355 | OSL-3.0: 63 (0.02%) 356 | Artistic-2.0: 61 (0.02%) 357 | Zlib: 61 (0.02%) 358 | AFL-3.0: 55 (0.02%) 359 | MIT-0: 45 (0.01%) 360 | OFL-1.1: 24 (0.01%) 361 | BSD-3-Clause-Clear: 20 (0.01%) 362 | ECL-2.0: 19 (0.01%) 363 | MS-PL: 19 (0.01%) 364 | BSD-4-Clause: 17 (0.00%) 365 | UPL-1.0: 11 (0.00%) 366 | LPPL-1.3c: 10 (0.00%) 367 | PostgreSQL: 7 (0.00%) 368 | ODbL-1.0: 7 (0.00%) 369 | EUPL-1.1: 7 (0.00%) 370 | MS-RL: 2 (0.00%) 371 | Vim: 2 (0.00%) 372 | GFDL-1.3: 1 (0.00%) 373 | NCSA: 1 (0.00%) 374 | Tracked repos without a detectable license: 191,808 (52.88%) 375 | 376 | Finished 11/22/2023, 5:32:06 PM 377 | -------------------------------------------------------------------------------- /generated/users_by_prs_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_by_prs_column.png -------------------------------------------------------------------------------- /generated/users_by_prs_extended_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_by_prs_extended_column.png -------------------------------------------------------------------------------- /generated/users_by_state_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_by_state_doughnut.png -------------------------------------------------------------------------------- /generated/users_by_state_stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_by_state_stacked.png -------------------------------------------------------------------------------- /generated/users_completions_contribution_type_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_completions_contribution_type_bar.png -------------------------------------------------------------------------------- /generated/users_completions_experience_level_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_completions_experience_level_bar.png -------------------------------------------------------------------------------- /generated/users_completions_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_completions_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_completions_top_countries_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_completions_top_countries_bar.png -------------------------------------------------------------------------------- /generated/users_completions_top_countries_bar_excl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_completions_top_countries_bar_excl.png -------------------------------------------------------------------------------- /generated/users_engaged_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_engaged_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_ai_ml_interest_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_ai_ml_interest_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_contribution_type_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_contribution_type_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_experience_level_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_experience_level_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_student_status_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_student_status_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_top_countries_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_top_countries_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_top_countries_bar_excl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/generated/users_registrations_top_countries_bar_excl.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hacktoberfest-data", 3 | "version": "1.0.0", 4 | "description": "Generating stats from the raw Hacktoberfest application data.", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "test": "eslint src/{**/*,*}.js", 10 | "test:fix": "npm run test -- --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/MattIPv4/hacktoberfest-data.git" 15 | }, 16 | "author": "Matt (IPv4) Cowley", 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/MattIPv4/hacktoberfest-data/issues" 20 | }, 21 | "homepage": "https://github.com/MattIPv4/hacktoberfest-data#readme", 22 | "devDependencies": { 23 | "eslint": "^8.53.0" 24 | }, 25 | "dependencies": { 26 | "@fontsource/jetbrains-mono": "^5.0.17", 27 | "canvas": "^2.11.2", 28 | "country-list": "^2.3.0", 29 | "dot": "^2.0.0-beta.1", 30 | "jimp": "^0.22.10", 31 | "js-yaml": "^4.1.0", 32 | "jsdom": "^22.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/chart.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { registerFont } = require('canvas'); 4 | registerFont(path.join(path.dirname(require.resolve('@fontsource/jetbrains-mono')), 'files/jetbrains-mono-latin-400-normal.woff'), { family: 'JetBrains Mono', weight: 400 }); 5 | registerFont(path.join(path.dirname(require.resolve('@fontsource/jetbrains-mono')), 'files/jetbrains-mono-latin-700-normal.woff'), { family: 'JetBrains Mono', weight: 700 }); 6 | 7 | const jsdom = require('jsdom'); 8 | const { JSDOM } = jsdom; 9 | const Jimp = require('jimp'); 10 | 11 | const colors = { 12 | background: '#0F0913', // void 13 | backgroundBox: '#655F67', // manga 400 14 | line: '#655F67', // manga 400 15 | text: '#EFEDEF', // manga 200 16 | textBox: '#EFEDEF', // manga 200 17 | highlightPositive: '#33B6D8', // blue 200 18 | highlightNeutral: '#FFFBA4', // gold 100 19 | highlightNeutralAlt: '#D2B863', // gold 200 20 | highlightNegative: '#EC4237', // red 200 21 | }; 22 | 23 | const config = (width, height, data, opts) => { 24 | opts = opts || {}; 25 | opts.size = { 26 | width, 27 | height, 28 | }; 29 | opts.padding = opts.padding || {}; 30 | opts.padding.top = opts.padding.top || 0; 31 | opts.padding.right = opts.padding.right || 0; 32 | opts.padding.bottom = opts.padding.bottom || 0; 33 | opts.padding.left = opts.padding.left || 0; 34 | 35 | for (const dataSeries of data) { 36 | dataSeries.indexLabelFontColor = dataSeries.indexLabelFontColor || colors.text; 37 | dataSeries.indexLabelFontWeight = dataSeries.indexLabelFontWeight || 'regular'; 38 | dataSeries.indexLabelFontFamily = dataSeries.indexLabelFontFamily || '\'JetBrains Mono\''; 39 | } 40 | 41 | const axis = { 42 | gridColor: colors.line, 43 | lineColor: colors.line, 44 | tickColor: colors.line, 45 | labelFontColor: colors.text, 46 | labelFontWeight: 'regular', 47 | labelFontFamily: '\'JetBrains Mono\'', 48 | titleFontColor: colors.text, 49 | titleFontWeight: 'bold', 50 | titleFontFamily: '\'JetBrains Mono\'', 51 | }; 52 | return { 53 | width: width - opts.padding.left - opts.padding.right, 54 | height: height - opts.padding.top - opts.padding.bottom, 55 | backgroundColor: colors.background, 56 | axisX: axis, 57 | axisY: axis, 58 | legend: { 59 | fontColor: colors.text, 60 | fontWeight: 'regular', 61 | fontFamily: '\'JetBrains Mono\'', 62 | horizontalAlign: 'center', 63 | verticalAlign: 'bottom', 64 | maxWidth: (width - opts.padding.left - opts.padding.right) * .9, 65 | }, 66 | title: { 67 | fontColor: colors.text, 68 | fontWeight: 'bold', 69 | fontFamily: '\'JetBrains Mono\'', 70 | horizontalAlign: 'center', 71 | verticalAlign: 'top', 72 | maxWidth: (width - opts.padding.left - opts.padding.right), 73 | }, 74 | data, 75 | renderOpts: opts, 76 | }; 77 | }; 78 | 79 | const render = async config => { 80 | return new Promise(resolve => { 81 | const makeCanvas = window => { 82 | window.chart = new window.CanvasJS.Chart('chartContainer', config); 83 | window.chart.render(); 84 | window.getCanvas(window); 85 | }; 86 | const getCanvas = window => { 87 | const canvas = window.document.body.querySelector('#chartContainer canvas'); 88 | 89 | // Apply padding 90 | if (config.renderOpts.size.width !== config.width || config.renderOpts.size.height !== config.height) { 91 | const ctx = canvas.getContext('2d'); 92 | const temp = ctx.getImageData(0, 0, canvas.width, canvas.height); 93 | ctx.canvas.width = config.renderOpts.size.width; 94 | ctx.canvas.height = config.renderOpts.size.height; 95 | ctx.fillStyle = config.backgroundColor; 96 | ctx.fillRect(0, 0, config.renderOpts.size.width, config.renderOpts.size.height); 97 | ctx.putImageData(temp, config.renderOpts.padding.left, config.renderOpts.padding.top); 98 | } 99 | 100 | resolve(canvas.toDataURL('image/png')); 101 | }; 102 | const virtualConsole = new jsdom.VirtualConsole(); 103 | virtualConsole.sendTo(console, { omitJSDOMErrors: true }); 104 | new JSDOM(`
`, { 105 | resources: 'usable', 106 | runScripts: 'dangerously', 107 | virtualConsole, 108 | beforeParse(window) { 109 | window.makeCanvas = makeCanvas; 110 | window.getCanvas = getCanvas; 111 | }, 112 | }); 113 | }); 114 | }; 115 | 116 | const save = async (file, data, watermark_opts) => { 117 | const base64Data = data.replace(/^data:image\/png;base64,/, ''); 118 | 119 | const chart = await Jimp.read(Buffer.from(base64Data, 'base64')); 120 | const hf = await Jimp.read(path.join(__dirname, 'hf.png')); 121 | hf.resize(watermark_opts.width || Jimp.AUTO, watermark_opts.height || Jimp.AUTO); 122 | chart.blit(hf, watermark_opts.x - (hf.bitmap.width / 2), watermark_opts.y - (hf.bitmap.height / 2)); 123 | 124 | await chart.writeAsync(file); 125 | }; 126 | 127 | module.exports = { colors, config, render, save }; 128 | -------------------------------------------------------------------------------- /src/helpers/color.js: -------------------------------------------------------------------------------- 1 | // Thanks https://stackoverflow.com/a/39077686/5577674 2 | const hexToRgb = hex => hex 3 | .replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => '#' + r + r + g + g + b + b) 4 | .substring(1).match(/.{2}/g).map(x => parseInt(x, 16)); 5 | 6 | // Thanks https://stackoverflow.com/a/39077686/5577674 7 | const rgbToHex = ([r, g, b]) => '#' + [r, g, b].map(x => { 8 | const hex = x.toString(16); 9 | return hex.length === 1 ? '0' + hex : hex; 10 | }).join(''); 11 | 12 | // Thanks https://stackoverflow.com/a/11868159 13 | const brightness = hex => { 14 | const [r, g, b] = hexToRgb(hex); 15 | return Math.round(((parseInt(r) * 299) + 16 | (parseInt(g) * 587) + 17 | (parseInt(b) * 114)) / 1000); 18 | }; 19 | 20 | const isBright = hex => brightness(hex) > 125; 21 | 22 | const darken = (hex, percentage) => rgbToHex(hexToRgb(hex).map(x => Math.round(x * (100 - percentage) / 100))); 23 | 24 | const lighten = (hex, percentage) => darken(hex, -percentage); 25 | 26 | const mix = (hex1, hex2, percentage) => rgbToHex(hexToRgb(hex1).map((x, i) => Math.round(x * (percentage / 100) + hexToRgb(hex2)[i] * (100 - percentage) / 100))); 27 | 28 | module.exports = { hexToRgb, rgbToHex, brightness, isBright, darken, lighten, mix }; 29 | -------------------------------------------------------------------------------- /src/helpers/date.js: -------------------------------------------------------------------------------- 1 | const getDateArray = (start, end) => { 2 | // Thanks https://stackoverflow.com/a/4413721 3 | const arr = []; 4 | const dt = new Date(start); 5 | while (dt <= end) { 6 | arr.push(new Date(dt)); 7 | dt.setDate(dt.getDate() + 1); 8 | } 9 | return arr; 10 | }; 11 | 12 | const dateFromDay = (year, day) => { 13 | // Thanks https://stackoverflow.com/a/4049020 14 | const date = new Date(year, 0); 15 | return new Date(date.setDate(day)); 16 | }; 17 | 18 | const formatDate = (date, short) => { 19 | // Thanks https://stackoverflow.com/a/3552493 20 | let monthNames = [ 21 | 'January', 'February', 'March', 22 | 'April', 'May', 'June', 'July', 23 | 'August', 'September', 'October', 24 | 'November', 'December', 25 | ]; 26 | if (short) { 27 | monthNames = [ 28 | 'Jan', 'Feb', 'Mar', 29 | 'Apr', 'May', 'Jun', 'Jul', 30 | 'Aug', 'Sept', 'Oct', 31 | 'Nov', 'Dec', 32 | ]; 33 | } 34 | 35 | const day = date.getDate(); 36 | const monthIndex = date.getMonth(); 37 | 38 | return `${monthNames[monthIndex]} ${day}`; 39 | }; 40 | 41 | module.exports = { getDateArray, dateFromDay, formatDate }; 42 | -------------------------------------------------------------------------------- /src/helpers/hf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/11d29b1c826b4448a8ea3ea8db67a7f179037c9a/src/helpers/hf.png -------------------------------------------------------------------------------- /src/helpers/linguist.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const colors = {}; 3 | 4 | const clean = lang => lang.toString().toLowerCase().trim(); 5 | 6 | const load = async () => { 7 | const res = await fetch('https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml'); 8 | const text = await res.text(); 9 | const data = yaml.load(text); 10 | Object.entries(data).forEach(lang => { 11 | if (lang[1].color) colors[clean(lang[0])] = lang[1].color; 12 | }); 13 | }; 14 | 15 | const get = lang => colors[clean(lang)]; 16 | 17 | module.exports = { load, get }; 18 | -------------------------------------------------------------------------------- /src/helpers/log.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | let output; 3 | 4 | const log = message => { 5 | output += `${message}\n`; 6 | console.log(message); 7 | }; 8 | 9 | const reset = () => { 10 | output = ''; 11 | }; 12 | 13 | const save = file => { 14 | fs.writeFileSync(file, output); 15 | }; 16 | 17 | module.exports = { log, reset, save }; 18 | -------------------------------------------------------------------------------- /src/helpers/number.js: -------------------------------------------------------------------------------- 1 | const commas = num => { 2 | return num.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }); 3 | }; 4 | 5 | const integer = num => { 6 | if (num < 1) return `Less than 1 (${commas(num)})`; 7 | return Math.round(num).toLocaleString(); 8 | }; 9 | 10 | const percentage = num => { 11 | return `${(num * 100).toFixed(2)}%`; 12 | }; 13 | 14 | const human = num => { 15 | if (num >= 1000000000) return `${commas(num / 1000000000)}B`; 16 | if (num >= 1000000) return `${commas(num / 1000000)}M`; 17 | if (num >= 1000) return `${commas(num / 1000)}K`; 18 | return commas(num); 19 | }; 20 | 21 | module.exports = { commas, integer, percentage, human }; 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const dot = require('dot'); 4 | const log = require('./helpers/log'); 5 | const stats = require('./stats'); 6 | const number = require('./helpers/number'); 7 | 8 | const year = 2023; 9 | 10 | const main = async () => { 11 | log.reset(); 12 | log.log(`Started ${new Date().toLocaleString()}`); 13 | 14 | const { data } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', year.toString(), 'stats.json'), 'utf8')); 15 | data.year = year; 16 | const results = await stats(data, log.log); 17 | 18 | log.log(''); 19 | log.log(`Finished ${new Date().toLocaleString()}`); 20 | log.save(path.join(__dirname, '../generated/stats.txt')); 21 | 22 | const template = fs.readFileSync(path.join(__dirname, '..', 'README.dot.md'), 'utf8'); 23 | const result = dot.template(template, { argName: 'data, c, p', strip: false })(results, number.commas, number.percentage) 24 | // Fix double line breaks in lists 25 | .replace(/( *(?:\d+\.|-).+(?:\n.+)*)\n\n( *(?:\d+\.|-))/g, '$1\n$2') 26 | // Fix line breaks in text (this would break code blocks, but we don't have any) 27 | .replace(/([^\\\n])\n(?!\s*(?:[-<>]|\d+\.))([^\s])/g, '$1 $2') 28 | // Fix triple (or more) line breaks 29 | .replace(/\n{3,}/g, '\n\n'); 30 | fs.writeFileSync(path.join(__dirname, '..', 'README.md'), result); 31 | }; 32 | 33 | main().catch(err => { 34 | console.error(err); 35 | process.exit(1); 36 | }); 37 | -------------------------------------------------------------------------------- /src/stats/PRs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const number = require('../helpers/number'); 3 | const chart = require('../helpers/chart'); 4 | const linguist = require('../helpers/linguist'); 5 | const color = require('../helpers/color'); 6 | const { getDateArray, formatDate } = require('../helpers/date'); 7 | 8 | module.exports = async (data, log) => { 9 | /*************** 10 | * PR Stats 11 | ***************/ 12 | log('\n\n----\nPR Stats\n----'); 13 | const results = {}; 14 | await linguist.load(); 15 | 16 | // PRs by state 17 | results.totalPRs = data.pull_requests.states.all.count - data.pull_requests.states.all.states['out-of-bounds']; 18 | results.totalAcceptedPRs = data.pull_requests.states.all.states['accepted']; 19 | results.totalNotAcceptedPRs = data.pull_requests.states.all.states['not-accepted']; 20 | results.totalNotParticipatingPRs = data.pull_requests.states.all.states['not-participating']; 21 | results.totalInvalidPRs = data.pull_requests.states.all.states['invalid']; 22 | results.totalSpamPRs = data.pull_requests.states.all.states['spam']; 23 | results.totalExcludedPRs = data.pull_requests.states.all.states['excluded']; 24 | 25 | const totalInvalidPRs = results.totalInvalidPRs + results.totalSpamPRs + results.totalExcludedPRs; 26 | const totalUnacceptedPRs = results.totalNotAcceptedPRs + results.totalNotParticipatingPRs; 27 | const totalPRs = results.totalAcceptedPRs + totalInvalidPRs + totalUnacceptedPRs; 28 | const totalBuggedPRs = results.totalPRs - totalPRs; 29 | log(''); 30 | log(`Total PR/MRs: ${number.commas(results.totalPRs)}`); 31 | log(` Accepted PR/MRs: ${number.commas(results.totalAcceptedPRs)} (${number.percentage(results.totalAcceptedPRs / results.totalPRs)})`); 32 | log(` Unaccepted PR/MRs: ${number.commas(totalUnacceptedPRs)} (${number.percentage(totalUnacceptedPRs / results.totalPRs)})`); 33 | log(` of which were not accepted by a maintainer: ${number.commas(results.totalNotAcceptedPRs)} (${number.percentage(results.totalNotAcceptedPRs / totalUnacceptedPRs)})`); 34 | log(` of which were not in a participating repo: ${number.commas(results.totalNotParticipatingPRs)} (${number.percentage(results.totalNotParticipatingPRs / totalUnacceptedPRs)})`); 35 | log(` Invalid PR/MRs: ${number.commas(totalInvalidPRs)} (${number.percentage(totalInvalidPRs / results.totalPRs)})`); 36 | log(` of which were in an excluded repo: ${number.commas(results.totalExcludedPRs)} (${number.percentage(results.totalExcludedPRs / totalInvalidPRs)})`); 37 | log(` of which were labeled as invalid: ${number.commas(results.totalInvalidPRs)} (${number.percentage(results.totalInvalidPRs / totalInvalidPRs)})`); 38 | log(` of which were identified as spam: ${number.commas(results.totalSpamPRs)} (${number.percentage(results.totalSpamPRs / totalInvalidPRs)})`); 39 | log(` PR/MRs that could not be processed: ${number.commas(totalBuggedPRs)} (${number.percentage(totalBuggedPRs / results.totalPRs)})`); 40 | log(' (PR/MR data failed to load from the provider, such as if the user were suspended from the provider\'s platform)'); 41 | 42 | const totalPRsByStateConfig = chart.config(1000, 1000, [{ 43 | type: 'doughnut', 44 | startAngle: 150, 45 | indexLabelPlacement: 'outside', 46 | indexLabelFontSize: 22, 47 | showInLegend: true, 48 | dataPoints: [ 49 | { 50 | y: results.totalAcceptedPRs, 51 | indexLabel: 'Accepted', 52 | legendText: `Accepted: ${number.commas(results.totalAcceptedPRs)} (${number.percentage(results.totalAcceptedPRs / results.totalPRs)})`, 53 | color: chart.colors.highlightPositive, 54 | indexLabelFontSize: 32, 55 | }, 56 | { 57 | y: results.totalNotAcceptedPRs, 58 | indexLabel: 'Not accepted', 59 | legendText: `Not accepted by maintainer: ${number.commas(results.totalNotAcceptedPRs)} (${number.percentage(results.totalNotAcceptedPRs / results.totalPRs)})`, 60 | color: chart.colors.highlightNeutral, 61 | }, 62 | { 63 | y: results.totalNotParticipatingPRs, 64 | indexLabel: 'Not participating', 65 | legendText: `Repo not participating: ${number.commas(results.totalNotParticipatingPRs)} (${number.percentage(results.totalNotParticipatingPRs / results.totalPRs)})`, 66 | color: chart.colors.highlightNeutral, 67 | }, 68 | { 69 | y: results.totalInvalidPRs, 70 | indexLabel: 'Invalid', 71 | legendText: `Labeled as invalid: ${number.commas(results.totalInvalidPRs)} (${number.percentage(results.totalInvalidPRs / results.totalPRs)})`, 72 | color: chart.colors.highlightNeutralAlt, 73 | }, 74 | { 75 | y: results.totalExcludedPRs, 76 | indexLabel: 'Excluded', 77 | legendText: `Excluded repository: ${number.commas(results.totalExcludedPRs)} (${number.percentage(results.totalExcludedPRs / results.totalPRs)})`, 78 | color: chart.colors.highlightNegative, 79 | }, 80 | { 81 | y: results.totalSpamPRs, 82 | indexLabel: 'Spam', 83 | legendText: `Identified as spam: ${number.commas(results.totalSpamPRs)} (${number.percentage(results.totalSpamPRs / results.totalPRs)})`, 84 | color: chart.colors.highlightNegative, 85 | }, 86 | ].map(x => [x, { 87 | y: results.totalPRs * 0.007, 88 | color: 'transparent', 89 | showInLegend: false, 90 | }]).flat(1), 91 | }], { padding: { top: 10, left: 5, right: 5, bottom: 5 }}); 92 | totalPRsByStateConfig.title = { 93 | ...totalPRsByStateConfig.title, 94 | text: 'All PR/MRs: Breakdown by State', 95 | fontSize: 48, 96 | padding: 5, 97 | margin: 15, 98 | }; 99 | totalPRsByStateConfig.legend = { 100 | ...totalPRsByStateConfig.legend, 101 | fontSize: 32, 102 | markerMargin: 32, 103 | }; 104 | totalPRsByStateConfig.subtitles = [ 105 | { 106 | text: '_', 107 | fontColor: chart.colors.background, 108 | fontSize: 16, 109 | verticalAlign: 'bottom', 110 | horizontalAlign: 'center', 111 | }, 112 | ]; 113 | await chart.save( 114 | path.join(__dirname, '../../generated/prs_by_state_doughnut.png'), 115 | await chart.render(totalPRsByStateConfig), 116 | { width: 250, x: 500, y: 430 }, 117 | ); 118 | 119 | // PRs by provider 120 | const providerMap = { 121 | github: 'GitHub', 122 | gitlab: 'GitLab', 123 | }; 124 | 125 | // PRs by provider 126 | results.totalPRsByProvider = Object.entries(data.pull_requests.providers.all.providers) 127 | .map(([ provider, { count, states } ]) => ([ 128 | providerMap[provider] || provider, 129 | count - states['out-of-bounds'], 130 | ])) 131 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 132 | log(''); 133 | log('Total PR/MRs by provider:'); 134 | for (const [ provider, count ] of results.totalPRsByProvider) { 135 | log(` ${provider}: ${number.commas(count)} (${number.percentage(count / results.totalPRs)})`); 136 | } 137 | 138 | // Accepted PRs by merge status 139 | results.totalAcceptedPRsMerged = data.pull_requests.merged.true.states.accepted; 140 | results.totalAcceptedPRsNotMerged = data.pull_requests.merged.false.states.accepted; 141 | 142 | log(''); 143 | log('Accepted PR/MRs by merge status:'); 144 | log(` Merged: ${number.commas(results.totalAcceptedPRsMerged)} (${number.percentage(results.totalAcceptedPRsMerged / results.totalAcceptedPRs)})`); 145 | log(` Not: ${number.commas(results.totalAcceptedPRsNotMerged)} (${number.percentage(results.totalAcceptedPRsNotMerged / results.totalAcceptedPRs)})`); 146 | 147 | const totalAcceptedPRsMergedConfig = chart.config(1000, 1000, [{ 148 | type: 'bar', 149 | indexLabelFontSize: 32, 150 | dataPoints: [ 151 | { 152 | y: results.totalAcceptedPRsMerged, 153 | label: 'Yes', 154 | color: chart.colors.highlightPositive, 155 | indexLabelPlacement: results.totalAcceptedPRsMerged / results.totalAcceptedPRs > 0.2 ? 'inside' : 'outside', 156 | indexLabel: number.percentage(results.totalAcceptedPRsMerged / results.totalAcceptedPRs), 157 | indexLabelFontColor: (results.totalAcceptedPRsMerged / results.totalAcceptedPRs > 0.2 158 | && color.isBright(chart.colors.highlightPositive)) ? chart.colors.background : chart.colors.text, 159 | }, 160 | { 161 | y: results.totalAcceptedPRsNotMerged, 162 | label: 'No', 163 | color: chart.colors.highlightNeutral, 164 | indexLabelPlacement: results.totalAcceptedPRsNotMerged / results.totalAcceptedPRs > 0.2 ? 'inside' : 'outside', 165 | indexLabel: number.percentage(results.totalAcceptedPRsNotMerged / results.totalAcceptedPRs), 166 | indexLabelFontColor: (results.totalAcceptedPRsNotMerged / results.totalAcceptedPRs > 0.2 167 | && color.isBright(chart.colors.highlightNeutral)) ? chart.colors.background : chart.colors.text, 168 | }, 169 | ].sort((a, b) => a.y > b.y ? 1 : -1), 170 | }]); 171 | totalAcceptedPRsMergedConfig.axisX = { 172 | ...totalAcceptedPRsMergedConfig.axisX, 173 | labelFontSize: 36, 174 | }; 175 | totalAcceptedPRsMergedConfig.axisY = { 176 | ...totalAcceptedPRsMergedConfig.axisY, 177 | labelFontSize: 24, 178 | labelFormatter: e => number.human(e.value), 179 | interval: 25000, 180 | }; 181 | totalAcceptedPRsMergedConfig.title = { 182 | ...totalAcceptedPRsMergedConfig.title, 183 | text: 'Accepted PR/MRs:\nChanges Merged', 184 | fontSize: 48, 185 | padding: 5, 186 | margin: 100, 187 | }; 188 | await chart.save( 189 | path.join(__dirname, '../../generated/prs_accepted_by_merged_bar.png'), 190 | await chart.render(totalAcceptedPRsMergedConfig), 191 | { width: 200, x: 880, y: 820 }, 192 | ); 193 | 194 | // Accepted PRs by approval 195 | results.totalAcceptedPRsApproved = data.pull_requests.approved.true.states.accepted; 196 | results.totalAcceptedPRsNotApproved = data.pull_requests.approved.false.states.accepted; 197 | 198 | log(''); 199 | log('Accepted PR/MRs by approving review:'); 200 | log(` Approved: ${number.commas(results.totalAcceptedPRsApproved)} (${number.percentage(results.totalAcceptedPRsApproved / results.totalAcceptedPRs)})`); 201 | log(` Not: ${number.commas(results.totalAcceptedPRsNotApproved)} (${number.percentage(results.totalAcceptedPRsNotApproved / results.totalAcceptedPRs)})`); 202 | 203 | const totalAcceptedPRsApprovedConfig = chart.config(1000, 1000, [{ 204 | type: 'bar', 205 | indexLabelFontSize: 32, 206 | dataPoints: [ 207 | { 208 | y: results.totalAcceptedPRsApproved, 209 | label: 'Yes', 210 | color: chart.colors.highlightPositive, 211 | indexLabelPlacement: results.totalAcceptedPRsApproved / results.totalAcceptedPRs > 0.2 ? 'inside' : 'outside', 212 | indexLabel: number.percentage(results.totalAcceptedPRsApproved / results.totalAcceptedPRs), 213 | indexLabelFontColor: (results.totalAcceptedPRsApproved / results.totalAcceptedPRs > 0.2 214 | && color.isBright(chart.colors.highlightPositive)) ? chart.colors.background : chart.colors.text, 215 | }, 216 | { 217 | y: results.totalAcceptedPRsNotApproved, 218 | label: 'No', 219 | color: chart.colors.highlightNeutral, 220 | indexLabelPlacement: results.totalAcceptedPRsNotApproved / results.totalAcceptedPRs > 0.2 ? 'inside' : 'outside', 221 | indexLabel: number.percentage(results.totalAcceptedPRsNotApproved / results.totalAcceptedPRs), 222 | indexLabelFontColor: (results.totalAcceptedPRsNotApproved / results.totalAcceptedPRs > 0.2 223 | && color.isBright(chart.colors.highlightNeutral)) ? chart.colors.background : chart.colors.text, 224 | }, 225 | ].sort((a, b) => a.y > b.y ? 1 : -1), 226 | }]); 227 | totalAcceptedPRsApprovedConfig.axisX = { 228 | ...totalAcceptedPRsApprovedConfig.axisX, 229 | labelFontSize: 36, 230 | }; 231 | totalAcceptedPRsApprovedConfig.axisY = { 232 | ...totalAcceptedPRsApprovedConfig.axisY, 233 | labelFontSize: 24, 234 | labelFormatter: e => number.human(e.value), 235 | interval: 25000, 236 | }; 237 | totalAcceptedPRsApprovedConfig.title = { 238 | ...totalAcceptedPRsApprovedConfig.title, 239 | text: 'Accepted PR/MRs:\nApproving Review', 240 | fontSize: 48, 241 | padding: 5, 242 | margin: 100, 243 | }; 244 | await chart.save( 245 | path.join(__dirname, '../../generated/prs_accepted_by_approval_bar.png'), 246 | await chart.render(totalAcceptedPRsApprovedConfig), 247 | { width: 200, x: 880, y: 820 }, 248 | ); 249 | 250 | // Breaking down accepted PRs by language, other tags 251 | results.totalAcceptedPRsByLanguage = Object.entries(data.pull_requests.languages.all.languages) 252 | .filter(([ lang ]) => lang && lang !== 'null') 253 | .map(([ lang, langData ]) => [ lang, langData.states.accepted || 0 ]) 254 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 255 | 256 | log(''); 257 | log(`Accepted PR/MRs by language: ${number.commas(results.totalAcceptedPRsByLanguage.length)} languages`); 258 | for (const [ lang, count ] of results.totalAcceptedPRsByLanguage.slice(0, 50)) { 259 | log(` ${lang}: ${number.commas(count)} (${number.percentage(count / results.totalAcceptedPRs)})`); 260 | } 261 | 262 | let doughnutTotal = 0; 263 | const totalPRsByLanguageConfig = chart.config(1000, 1000, [{ 264 | type: 'doughnut', 265 | startAngle: 180, 266 | indexLabelPlacement: 'outside', 267 | dataPoints: results.totalAcceptedPRsByLanguage.slice(0, 10).map(([ lang, count ]) => { 268 | const dataColor = linguist.get(lang) || chart.colors.highlightNeutral; 269 | const percent = (count || 0) / results.totalAcceptedPRs; 270 | doughnutTotal += (count || 0); 271 | return { 272 | y: count || 0, 273 | indexLabel: `${lang.split(' ')[0]}: ${number.percentage(percent)}`, 274 | color: dataColor, 275 | indexLabelFontSize: percent > 0.1 ? 24 : percent > 0.05 ? 22 : 20, 276 | indexLabelMaxWidth: 500, 277 | }; 278 | }), 279 | }], { padding: { top: 5, left: 10, right: 10, bottom: 30 }}); 280 | if (results.totalAcceptedPRs > doughnutTotal) { 281 | totalPRsByLanguageConfig.data[0].dataPoints.push({ 282 | y: results.totalAcceptedPRs - doughnutTotal, 283 | indexLabel: `Others: ${number.percentage((results.totalAcceptedPRs - doughnutTotal) / results.totalAcceptedPRs)}`, 284 | color: chart.colors.highlightNeutral, 285 | indexLabelFontSize: 24, 286 | }); 287 | } 288 | totalPRsByLanguageConfig.data[0].dataPoints = totalPRsByLanguageConfig.data[0].dataPoints.map(x => [x, { 289 | y: results.totalAcceptedPRs * 0.005, 290 | color: 'transparent', 291 | showInLegend: false, 292 | }]).flat(1); 293 | totalPRsByLanguageConfig.title = { 294 | ...totalPRsByLanguageConfig.title, 295 | text: 'Accepted PR/MRs: Top 10 Languages', 296 | fontSize: 48, 297 | padding: 5, 298 | margin: 15, 299 | }; 300 | totalPRsByLanguageConfig.subtitles = [{ 301 | ...totalPRsByLanguageConfig.title, 302 | text: `Hacktoberfest saw ${number.commas(results.totalAcceptedPRsByLanguage.length)} different programming languages represented across the ${number.commas(results.totalAcceptedPRs)} accepted PR/MRs submitted by participants.`, 303 | fontSize: 32, 304 | padding: 20, 305 | cornerRadius: 5, 306 | verticalAlign: 'bottom', 307 | horizontalAlign: 'center', 308 | maxWidth: 850, 309 | backgroundColor: chart.colors.backgroundBox, 310 | fontColor: chart.colors.textBox, 311 | }]; 312 | await chart.save( 313 | path.join(__dirname, '../../generated/prs_by_language_doughnut.png'), 314 | await chart.render(totalPRsByLanguageConfig), 315 | { width: 200, x: 500, y: 430 }, 316 | ); 317 | 318 | // Breaking down PRs by day 319 | results.totalPRsByDay = Object.entries(data.pull_requests.states.daily) 320 | .map(([ day, dayData ]) => [ day, dayData.states.accepted || 0 ]) 321 | .sort((a, b) => a[1] < b[1] ? 1 : -1) 322 | .slice(0, 15); 323 | 324 | log(''); 325 | log('Top days by accepted PR/MRs:'); 326 | for (const [ day, count ] of results.totalPRsByDay) { 327 | log(` ${formatDate(new Date(day))}: ${number.commas(count)} (${number.percentage(count / results.totalAcceptedPRs)})`); 328 | } 329 | 330 | const totalPRsByDayConfig = chart.config(1000, 1000, [{ 331 | type: 'bar', 332 | indexLabelPlacement: 'inside', 333 | indexLabelFontSize: 24, 334 | dataPoints: results.totalPRsByDay.slice(0, 10).map(([ day, count ], i) => { 335 | const colors = [ 336 | chart.colors.highlightPositive, 337 | chart.colors.highlightNeutral, 338 | chart.colors.highlightNeutralAlt, 339 | chart.colors.highlightNegative, 340 | ]; 341 | const dataColor = colors[i % colors.length]; 342 | return { 343 | y: count, 344 | label: formatDate(new Date(day), true), 345 | color: dataColor, 346 | indexLabel: number.percentage(count / results.totalAcceptedPRs), 347 | indexLabelFontColor: color.isBright(dataColor) ? chart.colors.background : chart.colors.text, 348 | }; 349 | }).reverse(), 350 | }]); 351 | totalPRsByDayConfig.axisX = { 352 | ...totalPRsByDayConfig.axisX, 353 | labelFontSize: 34, 354 | }; 355 | totalPRsByDayConfig.axisY = { 356 | ...totalPRsByDayConfig.axisY, 357 | labelFontSize: 24, 358 | labelFormatter: (e) => number.human(e.value), 359 | interval: 1000, 360 | }; 361 | totalPRsByDayConfig.title = { 362 | ...totalPRsByDayConfig.title, 363 | text: 'Accepted PR/MRs: Most Popular Days', 364 | fontSize: 48, 365 | padding: 5, 366 | margin: 15, 367 | }; 368 | await chart.save( 369 | path.join(__dirname, '../../generated/prs_by_day_bar.png'), 370 | await chart.render(totalPRsByDayConfig), 371 | { width: 200, x: 880, y: 820 }, 372 | ); 373 | 374 | // Breaking down PRs by day and by language 375 | results.totalAcceptedPRsByLanguageByDay = results.totalAcceptedPRsByLanguage.slice(0, 10).map(([ language ]) => ({ 376 | language, 377 | // One day before Hacktoberfest, two days after 378 | daily: getDateArray(new Date(`${data.year}-09-29`), new Date(`${data.year}-11-03`)) 379 | .map(date => ({ 380 | date, 381 | count: data.pull_requests.languages.daily?.[date.toISOString().split('T')[0]]?.languages?.[language]?.states?.accepted || 0, 382 | })), 383 | })); 384 | const totalPRsByLanguageByDayConfig = chart.config(2500, 1000, results.totalAcceptedPRsByLanguageByDay 385 | .map(({ language, daily }) => ({ 386 | type: 'spline', 387 | name: language, 388 | showInLegend: true, 389 | dataPoints: daily.map(({ date, count }) => ({ 390 | x: date, 391 | y: count, 392 | })), 393 | lineThickness: 3, 394 | color: linguist.get(language) || chart.colors.highlightNeutral, 395 | }))); 396 | totalPRsByLanguageByDayConfig.axisX = { 397 | ...totalPRsByLanguageByDayConfig.axisX, 398 | labelFontSize: 34, 399 | interval: 1, 400 | intervalType: 'week', 401 | title: 'PR/MR Created At', 402 | titleFontSize: 24, 403 | titleFontWeight: 400, 404 | }; 405 | totalPRsByLanguageByDayConfig.axisY = { 406 | ...totalPRsByLanguageByDayConfig.axisY, 407 | labelFontSize: 34, 408 | interval: 100, 409 | }; 410 | totalPRsByLanguageByDayConfig.title = { 411 | ...totalPRsByLanguageByDayConfig.title, 412 | text: 'Accepted PR/MRs: Top 10 Languages', 413 | fontSize: 48, 414 | padding: 5, 415 | margin: 15, 416 | }; 417 | totalPRsByLanguageByDayConfig.subtitles = [ 418 | { 419 | text: '_', 420 | fontColor: chart.colors.text, 421 | fontSize: 16, 422 | verticalAlign: 'bottom', 423 | horizontalAlign: 'center', 424 | }, 425 | ]; 426 | 427 | await chart.save( 428 | path.join(__dirname, '../../generated/prs_by_language_spline.png'), 429 | await chart.render(totalPRsByLanguageByDayConfig), 430 | { width: 200, x: 1250, y: 200 }, 431 | ); 432 | 433 | // Breaking down PRs by day and by state 434 | results.totalPRsByStateByDay = Object.keys(data.pull_requests.states.all.states) 435 | .map(state => ({ 436 | state, 437 | // One day before Hacktoberfest, two days after 438 | daily: getDateArray(new Date(`${data.year}-09-29`), new Date(`${data.year}-11-03`)) 439 | .map(date => ({ 440 | date, 441 | count: data.pull_requests.states.daily?.[date.toISOString().split('T')[0]]?.states?.[state] || 0, 442 | })), 443 | })); 444 | 445 | const totalPRsByStateByDayOrder = ['accepted', 'not-accepted', 'not-participating', 'invalid', 'excluded', 'spam']; 446 | const totalPRsByStateByDayColors = { 447 | spam: color.mix(chart.colors.highlightNegative, chart.colors.highlightNeutralAlt, 75), 448 | excluded: chart.colors.highlightNegative, 449 | invalid: chart.colors.highlightNeutralAlt, 450 | 'not-participating': color.mix(chart.colors.highlightNeutral, chart.colors.highlightNeutralAlt, 50), 451 | 'not-accepted': chart.colors.highlightNeutral, 452 | accepted: chart.colors.highlightPositive, 453 | }; 454 | 455 | const totalPRsByStateByDayConfig = chart.config(2500, 1000, results.totalPRsByStateByDay 456 | .filter(({ state }) => totalPRsByStateByDayOrder.includes(state)) 457 | .sort((a, b) => totalPRsByStateByDayOrder.indexOf(b.state) - totalPRsByStateByDayOrder.indexOf(a.state)) 458 | .map(({ state, daily }) => ({ 459 | type: 'stackedArea', 460 | name: state, 461 | showInLegend: true, 462 | dataPoints: daily.map(({ date, count }) => ({ 463 | x: date, 464 | y: count, 465 | })), 466 | lineThickness: 3, 467 | color: totalPRsByStateByDayColors[state] || chart.colors.highlightNeutral, 468 | }))); 469 | totalPRsByStateByDayConfig.axisX = { 470 | ...totalPRsByStateByDayConfig.axisX, 471 | labelFontSize: 34, 472 | interval: 1, 473 | intervalType: 'week', 474 | title: 'PR Created At', 475 | titleFontSize: 24, 476 | titleFontWeight: 400, 477 | }; 478 | totalPRsByStateByDayConfig.axisY = { 479 | ...totalPRsByStateByDayConfig.axisY, 480 | labelFontSize: 34, 481 | interval: 1000, 482 | }; 483 | totalPRsByStateByDayConfig.title = { 484 | ...totalPRsByStateByDayConfig.title, 485 | text: 'All PR/MRs: Breakdown by State', 486 | fontSize: 48, 487 | padding: 5, 488 | margin: 15, 489 | }; 490 | totalPRsByStateByDayConfig.subtitles = [ 491 | { 492 | text: '_', 493 | fontColor: chart.colors.text, 494 | fontSize: 16, 495 | verticalAlign: 'bottom', 496 | horizontalAlign: 'center', 497 | }, 498 | ]; 499 | 500 | await chart.save( 501 | path.join(__dirname, '../../generated/prs_by_state_stacked.png'), 502 | await chart.render(totalPRsByStateByDayConfig), 503 | { width: 200, x: 1250, y: 200 }, 504 | ); 505 | 506 | // Averages of certain metrics 507 | results.averageAcceptedPRCommits = data.pull_requests.commits.states.accepted / results.totalAcceptedPRs; 508 | results.averageAcceptedPRFiles = data.pull_requests.files.states.accepted / results.totalAcceptedPRs; 509 | results.averageAcceptedPRAdditions = data.pull_requests.additions.states.accepted / results.totalAcceptedPRs; 510 | results.averageAcceptedPRDeletions = data.pull_requests.deletions.states.accepted / results.totalAcceptedPRs; 511 | 512 | log(''); 513 | log('On average, an accepted PR/MR had:'); 514 | log(` ${number.integer(results.averageAcceptedPRCommits)} commits`); 515 | log(` ${number.integer(results.averageAcceptedPRFiles)} modified files`); 516 | log(` ${number.integer(results.averageAcceptedPRAdditions)} additions`); 517 | log(` ${number.integer(results.averageAcceptedPRDeletions)} deletions`); 518 | 519 | return results; 520 | }; 521 | -------------------------------------------------------------------------------- /src/stats/Repos.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const number = require('../helpers/number'); 3 | const chart = require('../helpers/chart'); 4 | const linguist = require('../helpers/linguist'); 5 | const color = require('../helpers/color'); 6 | 7 | module.exports = async (data, log) => { 8 | /*************** 9 | * Repo Stats 10 | ***************/ 11 | log('\n\n----\nRepo Stats\n----'); 12 | const results = {}; 13 | await linguist.load(); 14 | 15 | // Total: Repos 16 | results.totalReposActive = data.repositories.pull_requests.accepted.count; 17 | results.totalReposTracked = Object.values(data.repositories.languages.languages).reduce((acc, count) => acc + count, 0); 18 | results.totalReposReported = data.excluded_repositories.count; 19 | results.totalReposExcluded = data.excluded_repositories.active.true.count; 20 | results.totalReposPermitted = data.excluded_repositories.active.false.has_note.true.count; 21 | results.totalReposUnreviewed = data.excluded_repositories.active.false.has_note.false.count; 22 | log(''); 23 | log(`Total active repos: ${number.commas(results.totalReposActive)}`); 24 | log('(A repository was considered active if it received a PR/MR from a Hacktoberfest participant that was considered accepted)'); 25 | log(''); 26 | log(`Total tracked repos: ${number.commas(results.totalReposTracked)}`); 27 | log('(A repository was considered tracked if it received a PR/MR from a Hacktoberfest participant, whether the repository was participating in Hacktoberfest or not)'); 28 | log(''); 29 | log(`Reported repositories: ${number.commas(results.totalReposReported)}`); 30 | log('(The Hacktoberfest community was able to report repositories that they did not feel followed our values)'); 31 | log(` Excluded repositories: ${number.commas(results.totalReposExcluded)} (${number.percentage(results.totalReposExcluded / results.totalReposReported)})`); 32 | log(` Permitted repositories: ${number.commas(results.totalReposPermitted)} (${number.percentage(results.totalReposPermitted / results.totalReposReported)})`); 33 | log(` Unreviewed repositories: ${number.commas(results.totalReposUnreviewed)} (${number.percentage(results.totalReposUnreviewed / results.totalReposReported)})`); 34 | 35 | const totalReposReportedConfig = chart.config(1000, 1000, [{ 36 | type: 'doughnut', 37 | startAngle: 90, 38 | indexLabelPlacement: 'outside', 39 | indexLabelFontSize: 32, 40 | showInLegend: true, 41 | dataPoints: [ 42 | { 43 | y: results.totalReposExcluded, 44 | indexLabel: 'Excluded', 45 | legendText: `Excluded: ${number.commas(results.totalReposExcluded)} (${number.percentage(results.totalReposExcluded / results.totalReposReported)})`, 46 | color: chart.colors.highlightNegative, 47 | }, 48 | { 49 | y: results.totalReposPermitted, 50 | indexLabel: 'Permitted', 51 | legendText: `Permitted: ${number.commas(results.totalReposPermitted)} (${number.percentage(results.totalReposPermitted / results.totalReposReported)})`, 52 | color: chart.colors.highlightPositive, 53 | }, 54 | { 55 | y: results.totalReposUnreviewed, 56 | indexLabel: 'Unreviewed', 57 | legendText: `Unreviewed: ${number.commas(results.totalReposUnreviewed)} (${number.percentage(results.totalReposUnreviewed / results.totalReposReported)})`, 58 | color: chart.colors.highlightNeutral, 59 | }, 60 | ].map(x => [x, { 61 | y: results.totalReposReported * 0.007, 62 | color: 'transparent', 63 | showInLegend: false, 64 | }]).flat(1), 65 | }], { padding: { top: 10, left: 5, right: 5, bottom: 5 }}); 66 | totalReposReportedConfig.title = { 67 | ...totalReposReportedConfig.title, 68 | text: 'Reported Repositories', 69 | fontSize: 48, 70 | padding: 5, 71 | margin: 25, 72 | }; 73 | totalReposReportedConfig.legend = { 74 | ...totalReposReportedConfig.legend, 75 | fontSize: 36, 76 | markerMargin: 32, 77 | maxWidth: 750, 78 | margin: 25, 79 | }; 80 | totalReposReportedConfig.subtitles = [ 81 | { 82 | text: '_', 83 | fontColor: chart.colors.background, 84 | fontSize: 16, 85 | verticalAlign: 'bottom', 86 | horizontalAlign: 'center', 87 | }, 88 | ]; 89 | await chart.save( 90 | path.join(__dirname, '../../generated/repos_reported_doughnut.png'), 91 | await chart.render(totalReposReportedConfig), 92 | { width: 250, x: 500, y: 475 }, 93 | ); 94 | 95 | // Breaking down repos by language 96 | results.totalReposByLanguage = Object.entries(data.repositories.languages.languages) 97 | .filter(([ lang ]) => lang && lang !== 'null') 98 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 99 | results.totalReposNoLanguage = data.repositories.languages.languages.null; 100 | log(''); 101 | log(`Tracked repos by language: ${results.totalReposByLanguage.length} languages`); 102 | for (const [ lang, count ] of results.totalReposByLanguage.slice(0, 50)) { 103 | const name = lang || 'Unknown'; 104 | log(` ${name}: ${number.commas(count)} (${number.percentage(count / results.totalReposTracked)})`); 105 | } 106 | log(`Tracked repos without a detectable language: ${number.commas(results.totalReposNoLanguage)} (${number.percentage(results.totalReposNoLanguage / results.totalReposTracked)})`); 107 | 108 | let doughnutTotal = 0; 109 | const totalReposByLanguageConfig = chart.config(1000, 1000, [{ 110 | type: 'doughnut', 111 | startAngle: 180, 112 | indexLabelPlacement: 'outside', 113 | dataPoints: results.totalReposByLanguage.slice(0, 10).map(([ lang, count ]) => { 114 | const dataColor = linguist.get(lang) || chart.colors.highlightNeutral; 115 | const percent = (count || 0) / results.totalReposTracked; 116 | doughnutTotal += (count || 0); 117 | return { 118 | y: count || 0, 119 | indexLabel: `${lang.split(' ')[0]}: ${number.percentage(percent)}`, 120 | color: dataColor, 121 | indexLabelFontSize: percent > 0.1 ? 24 : percent > 0.05 ? 22 : 20, 122 | indexLabelMaxWidth: 500, 123 | }; 124 | }), 125 | }], { padding: { top: 5, left: 10, right: 10, bottom: 5 }}); 126 | if (results.totalReposTracked > doughnutTotal) { 127 | totalReposByLanguageConfig.data[0].dataPoints.push({ 128 | y: results.totalReposTracked - doughnutTotal, 129 | indexLabel: `Others: ${number.percentage((results.totalReposTracked - doughnutTotal) / results.totalReposTracked)}`, 130 | color: chart.colors.highlightNeutral, 131 | indexLabelFontSize: 24, 132 | }); 133 | } 134 | totalReposByLanguageConfig.data[0].dataPoints = totalReposByLanguageConfig.data[0].dataPoints.map(x => [x, { 135 | y: results.totalReposTracked * 0.005, 136 | color: 'transparent', 137 | showInLegend: false, 138 | }]).flat(1); 139 | totalReposByLanguageConfig.title = { 140 | ...totalReposByLanguageConfig.title, 141 | text: 'Tracked Repos: Top 10 Languages', 142 | fontSize: 48, 143 | padding: 5, 144 | margin: 15, 145 | }; 146 | totalReposByLanguageConfig.subtitles = [{ 147 | ...totalReposByLanguageConfig.title, 148 | text: 'A repository was considered tracked if it received a PR/MR from a Hacktoberfest participant, whether the repository was participating in Hacktoberfest or not', 149 | fontSize: 16, 150 | padding: 10, 151 | margin: 5, 152 | cornerRadius: 5, 153 | verticalAlign: 'bottom', 154 | horizontalAlign: 'center', 155 | maxWidth: 900, 156 | backgroundColor: chart.colors.backgroundBox, 157 | fontColor: chart.colors.textBox, 158 | }, { 159 | ...totalReposByLanguageConfig.title, 160 | text: `Hacktoberfest saw ${number.commas(results.totalReposByLanguage.length)} different programming languages represented across the ${number.commas(results.totalReposTracked)} tracked repositories.`, 161 | fontSize: 32, 162 | padding: 20, 163 | margin: 0, 164 | cornerRadius: 5, 165 | verticalAlign: 'bottom', 166 | horizontalAlign: 'center', 167 | maxWidth: 850, 168 | backgroundColor: chart.colors.backgroundBox, 169 | fontColor: chart.colors.textBox, 170 | }]; 171 | await chart.save( 172 | path.join(__dirname, '../../generated/repos_by_language_doughnut.png'), 173 | await chart.render(totalReposByLanguageConfig), 174 | { width: 200, x: 500, y: 435 }, 175 | ); 176 | 177 | // Breakdown by license 178 | results.totalReposByLicenses = Object.entries(data.repositories.licenses.licenses) 179 | .filter(([ license ]) => license && license !== 'null') 180 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 181 | results.totalReposNoLicense = data.repositories.licenses.licenses.null; 182 | log(''); 183 | log(`Tracked repos by license: ${results.totalReposByLicenses.length} licenses`); 184 | for (const [ license, count ] of results.totalReposByLicenses.slice(0, 50)) { 185 | log(` ${license}: ${number.commas(count)} (${number.percentage(count / results.totalReposTracked)})`); 186 | } 187 | log(`Tracked repos without a detectable license: ${number.commas(results.totalReposNoLicense)} (${number.percentage(results.totalReposNoLicense / results.totalReposTracked)})`); 188 | 189 | let topRepoLicensesTotal = results.totalReposNoLicense; 190 | const topRepoLicensesConfig = chart.config(1000, 1000, [{ 191 | type: 'bar', 192 | indexLabelFontSize: 24, 193 | dataPoints: results.totalReposByLicenses.slice(0, 10).map(([ license, count ], i) => { 194 | const colors = [ 195 | chart.colors.highlightPositive, 196 | chart.colors.highlightNeutral, 197 | chart.colors.highlightNeutralAlt, 198 | chart.colors.highlightNegative, 199 | ]; 200 | const dataColor = colors[i % colors.length]; 201 | const percentWidth = count / results.totalReposByLicenses[0][1]; 202 | topRepoLicensesTotal += count; 203 | return { 204 | y: count, 205 | indexLabelPlacement: percentWidth > 0.4 ? 'inside' : 'outside', 206 | indexLabel: `${license}: ${number.commas(count)} (${number.percentage(count / results.totalReposTracked)})`, 207 | color: dataColor, 208 | indexLabelFontColor: (percentWidth > 0.4 && color.isBright(chart.colors.highlightPositive)) 209 | ? chart.colors.background : chart.colors.text, 210 | }; 211 | }), 212 | }]); 213 | topRepoLicensesConfig.data[0].dataPoints.push({ 214 | y: results.totalReposTracked - topRepoLicensesTotal, 215 | indexLabelPlacement: 'outside', 216 | indexLabel: `Others: ${number.commas(results.totalReposTracked - topRepoLicensesTotal)} (${number.percentage((results.totalReposTracked - topRepoLicensesTotal) / results.totalReposTracked)})`, 217 | color: chart.colors.highlightNeutral, 218 | indexLabelFontColor: chart.colors.text, 219 | }); 220 | topRepoLicensesConfig.axisY = { 221 | ...topRepoLicensesConfig.axisY, 222 | tickThickness: 0, 223 | labelFormatter: () => '', 224 | }; 225 | topRepoLicensesConfig.axisX = { 226 | ...topRepoLicensesConfig.axisX, 227 | tickThickness: 0, 228 | labelFormatter: () => '', 229 | }; 230 | topRepoLicensesConfig.title = { 231 | ...topRepoLicensesConfig.title, 232 | text: 'Tracked Repos: Top 10 Licenses', 233 | fontSize: 48, 234 | padding: 10, 235 | margin: 10, 236 | }; 237 | topRepoLicensesConfig.subtitles = [{ 238 | ...totalReposByLanguageConfig.title, 239 | text: 'A repository was considered tracked if it received a PR/MR from a Hacktoberfest participant, whether the repository was participating in Hacktoberfest or not', 240 | fontSize: 16, 241 | padding: 10, 242 | margin: 0, 243 | cornerRadius: 5, 244 | verticalAlign: 'bottom', 245 | horizontalAlign: 'center', 246 | maxWidth: 900, 247 | backgroundColor: chart.colors.backgroundBox, 248 | fontColor: chart.colors.textBox, 249 | }, { 250 | ...topRepoLicensesConfig.title, 251 | text: `${number.percentage(results.totalReposNoLicense / results.totalReposTracked)} repositories use no license that can be detected`, 252 | fontSize: 28, 253 | padding: 20, 254 | margin: 0, 255 | cornerRadius: 5, 256 | verticalAlign: 'top', 257 | horizontalAlign: 'right', 258 | dockInsidePlotArea: true, 259 | maxWidth: 500, 260 | backgroundColor: chart.colors.backgroundBox, 261 | fontColor: chart.colors.textBox, 262 | }]; 263 | await chart.save( 264 | path.join(__dirname, '../../generated/repos_by_license_bar.png'), 265 | await chart.render(topRepoLicensesConfig), 266 | { width: 200, x: 880, y: 325 }, 267 | ); 268 | 269 | return results; 270 | }; 271 | -------------------------------------------------------------------------------- /src/stats/Users.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const number = require('../helpers/number'); 3 | const chart = require('../helpers/chart'); 4 | const { getDateArray } = require('../helpers/date'); 5 | const color = require('../helpers/color'); 6 | const { getName, overwrite } = require('country-list'); 7 | 8 | overwrite([ 9 | { 10 | code: 'US', 11 | name: 'United States', // United States of America 12 | }, 13 | { 14 | code: 'GB', 15 | name: 'United Kingdom', // United Kingdom of Great Britain and Northern Ireland 16 | }, 17 | { 18 | code: 'TW', 19 | name: 'Taiwan', // Taiwan, Province of China 20 | }, 21 | ]); 22 | 23 | const usersTopChart = async (userData, totalUsers, title, file, interval, mainSubtitle = null, smallSubtitle = null) => { 24 | const max = Math.max(...userData.map(([, count]) => count)); 25 | const config = chart.config(1000, 1000, [{ 26 | type: 'bar', 27 | indexLabelFontSize: 24, 28 | dataPoints: userData.slice(0, 10).map(([ title, count ], i) => { 29 | const colors = [ 30 | chart.colors.highlightPositive, 31 | chart.colors.highlightNeutral, 32 | chart.colors.highlightNeutralAlt, 33 | chart.colors.highlightNegative, 34 | ]; 35 | const dataColor = colors[i % colors.length]; 36 | const percentWidth = count / max; 37 | return { 38 | y: count, 39 | color: dataColor, 40 | indexLabelPlacement: percentWidth > 0.5 ? 'inside' : 'outside', 41 | indexLabel: `${title || 'Not Given'} (${number.percentage(count / totalUsers)})`, 42 | indexLabelFontColor: (percentWidth > 0.5 && color.isBright(chart.colors.highlightPositive)) 43 | ? chart.colors.background : chart.colors.text, 44 | }; 45 | }).reverse(), 46 | }]); 47 | config.axisY = { 48 | ...config.axisY, 49 | labelFontSize: 24, 50 | labelFormatter: e => number.human(e.value), 51 | interval, 52 | }; 53 | config.axisX = { 54 | ...config.axisX, 55 | tickThickness: 0, 56 | labelFormatter: () => '', 57 | }; 58 | config.title = { 59 | ...config.title, 60 | text: title, 61 | fontSize: 42, 62 | padding: 10, 63 | margin: 10, 64 | }; 65 | config.subtitles = []; 66 | if (mainSubtitle) { 67 | config.subtitles.unshift({ 68 | ...config.title, 69 | text: mainSubtitle, 70 | fontColor: chart.colors.textBox, 71 | fontSize: 28, 72 | padding: 20, 73 | margin: 0, 74 | cornerRadius: 5, 75 | verticalAlign: 'bottom', 76 | horizontalAlign: 'center', 77 | backgroundColor: chart.colors.backgroundBox, 78 | }); 79 | } 80 | if (smallSubtitle) { 81 | config.subtitles.unshift({ 82 | ...config.title, 83 | text: smallSubtitle, 84 | fontColor: chart.colors.textBox, 85 | fontSize: 22, 86 | padding: 15, 87 | margin: 10, 88 | cornerRadius: 5, 89 | verticalAlign: 'bottom', 90 | horizontalAlign: 'right', 91 | dockInsidePlotArea: true, 92 | maxWidth: 400, 93 | backgroundColor: chart.colors.backgroundBox, 94 | }); 95 | } 96 | await chart.save( 97 | path.join(__dirname, `../../generated/${file}.png`), 98 | await chart.render(config), 99 | mainSubtitle 100 | ? smallSubtitle 101 | ? { width: 200, x: 880, y: 620 } 102 | : { width: 200, x: 880, y: 740 } 103 | : smallSubtitle 104 | ? { width: 200, x: 880, y: 720 } 105 | : { width: 200, x: 880, y: 820 }, 106 | ); 107 | }; 108 | 109 | const cappedAcceptedUserPRs = (data, max) => Object.entries(data.users.pull_requests.accepted.all.counts) 110 | .reduce((arr, [ prs, users ]) => { 111 | arr[Math.min(Number(prs), max) - 1][1] += users; 112 | return arr; 113 | }, Array(max).fill(null).map((_, i) => [ i + 1, 0 ])); 114 | 115 | module.exports = async (data, log) => { 116 | /*************** 117 | * User Stats 118 | ***************/ 119 | log('\n\n----\nUser Stats\n----'); 120 | const results = {}; 121 | 122 | // Total users 123 | results.totalUsers = data.users.states.all.count; 124 | results.totalUsersNotEngaged = data.users.states.all.count - data.users.states.all.states['first-accepted']; 125 | results.totalUsersEngaged = data.users.states.all.states['first-accepted'] - data.users.states.all.states.contributor; 126 | results.totalUsersCompleted = data.users.states.all.states.contributor; 127 | results.totalUsersWarned = data.users.states.all.states.warning; 128 | results.totalUsersDisqualified = data.users.states.all.states.disqualified; 129 | 130 | log(''); 131 | log(`Total Users: ${number.commas(results.totalUsers)}`); 132 | log(` Users that submitted no accepted PR/MRs: ${number.commas(results.totalUsersNotEngaged)} (${number.percentage(results.totalUsersNotEngaged / results.totalUsers)})`); 133 | log(` Users that submitted 1-3 accepted PR/MRs: ${number.commas(results.totalUsersEngaged)} (${number.percentage(results.totalUsersEngaged / results.totalUsers)})`); 134 | log(` Users that submitted 4+ accepted PR/MRs: ${number.commas(results.totalUsersCompleted)} (${number.percentage(results.totalUsersCompleted / results.totalUsers)})`); 135 | log(` Users that were warned (1 spammy PR/MR): ${number.commas(results.totalUsersWarned)} (${number.percentage(results.totalUsersWarned / results.totalUsers)})`); 136 | log(` Users that were disqualified (2+ spammy PR/MRs): ${number.commas(results.totalUsersDisqualified)} (${number.percentage(results.totalUsersDisqualified / results.totalUsers)})`); 137 | 138 | const totalUsersByStateConfig = chart.config(1000, 1000, [{ 139 | type: 'doughnut', 140 | startAngle: 160, 141 | indexLabelPlacement: 'outside', 142 | indexLabelFontSize: 22, 143 | showInLegend: true, 144 | dataPoints: [ 145 | { 146 | y: results.totalUsersCompleted, 147 | indexLabel: 'Completed', 148 | legendText: `Completed: 4+ accepted PR/MRs: ${number.commas(results.totalUsersCompleted)} (${number.percentage(results.totalUsersCompleted / results.totalUsers)})`, 149 | color: chart.colors.highlightPositive, 150 | indexLabelFontSize: 32, 151 | }, 152 | { 153 | y: results.totalUsersEngaged, 154 | indexLabel: 'Engaged', 155 | legendText: `Engaged: 1-3 accepted PR/MRs: ${number.commas(results.totalUsersEngaged)} (${number.percentage(results.totalUsersEngaged / results.totalUsers)})`, 156 | color: chart.colors.highlightNeutral, 157 | indexLabelFontSize: 26, 158 | }, 159 | { 160 | y: results.totalUsersNotEngaged, 161 | indexLabel: 'Registered', 162 | legendText: `Registered: No accepted PR/MRs: ${number.commas(results.totalUsersNotEngaged)} (${number.percentage(results.totalUsersNotEngaged / results.totalUsers)})`, 163 | color: chart.colors.highlightNeutralAlt, 164 | indexLabelFontSize: 26, 165 | }, 166 | { 167 | y: results.totalUsersDisqualified, 168 | indexLabel: 'Disqualified', 169 | legendText: `Disqualified: Spammy behaviour: ${number.commas(results.totalUsersDisqualified)} (${number.percentage(results.totalUsersDisqualified / results.totalUsers)})`, 170 | color: chart.colors.highlightNegative, 171 | indexLabelFontSize: 26, 172 | }, 173 | ].map(x => [x, { 174 | y: results.totalUsers * 0.007, 175 | color: 'transparent', 176 | showInLegend: false, 177 | }]).flat(1), 178 | }], { padding: { top: 10, left: 5, right: 5, bottom: 5 }}); 179 | totalUsersByStateConfig.title = { 180 | ...totalUsersByStateConfig.title, 181 | text: 'All Users: Breakdown by State', 182 | fontSize: 48, 183 | padding: 5, 184 | margin: 15, 185 | }; 186 | totalUsersByStateConfig.legend = { 187 | ...totalUsersByStateConfig.legend, 188 | fontSize: 28, 189 | markerMargin: 32, 190 | }; 191 | totalUsersByStateConfig.subtitles = [ 192 | { 193 | text: '_', 194 | fontColor: chart.colors.background, 195 | fontSize: 16, 196 | verticalAlign: 'bottom', 197 | horizontalAlign: 'center', 198 | }, 199 | ]; 200 | await chart.save( 201 | path.join(__dirname, '../../generated/users_by_state_doughnut.png'), 202 | await chart.render(totalUsersByStateConfig), 203 | { width: 250, x: 500, y: 470 }, 204 | ); 205 | 206 | // Users by accepted PRs 207 | results.totalUsersByAcceptedPRs = cappedAcceptedUserPRs(data, 10); 208 | 209 | log(''); 210 | log('Users by number of accepted PRs/MR submitted:'); 211 | for (const [ prs, users ] of results.totalUsersByAcceptedPRs) { 212 | log(` ${prs}${prs === 10 ? '+' : ''} PR${prs === 1 ? '' : 's'}/MR${prs === 1 ? '' : 's'}: ${number.commas(users)} (${number.percentage(users / results.totalUsers)})`); 213 | } 214 | 215 | const totalUsersByPRsExtConfig = chart.config(2500, 1000, [{ 216 | type: 'column', 217 | dataPoints: results.totalUsersByAcceptedPRs.map(([ prs, users ]) => ({ 218 | y: users, 219 | color: Number.parseInt(prs) > 4 220 | ? chart.colors.highlightNeutral 221 | : Number.parseInt(prs) === 4 222 | ? chart.colors.highlightPositive 223 | : chart.colors.highlightNegative, 224 | label: `${prs}${prs === 10 ? '+' : ''} PR/MR${prs === 1 ? '' : 's'}`, 225 | })), 226 | }]); 227 | totalUsersByPRsExtConfig.axisX = { 228 | ...totalUsersByPRsExtConfig.axisX, 229 | labelFontSize: 24, 230 | }; 231 | totalUsersByPRsExtConfig.axisY = { 232 | ...totalUsersByPRsExtConfig.axisY, 233 | labelFontSize: 24, 234 | }; 235 | totalUsersByPRsExtConfig.title = { 236 | ...totalUsersByPRsExtConfig.title, 237 | text: 'Users: Accepted Pull/Merge Requests', 238 | fontSize: 48, 239 | padding: 5, 240 | margin: 40, 241 | }; 242 | totalUsersByPRsExtConfig.subtitles = [{ 243 | ...totalUsersByPRsExtConfig.title, 244 | text: `Over the month, ${number.commas(results.totalUsersCompleted)} participants (${number.percentage(results.totalUsersCompleted / results.totalUsers)}) submitted 4 or more accepted PR/MRs, completing Hacktoberfest.`, 245 | fontColor: chart.colors.textBox, 246 | fontSize: 32, 247 | padding: 15, 248 | margin: 0, 249 | cornerRadius: 5, 250 | verticalAlign: 'top', 251 | horizontalAlign: 'right', 252 | dockInsidePlotArea: true, 253 | maxWidth: 800, 254 | backgroundColor: chart.colors.backgroundBox, 255 | }, { 256 | ...totalUsersByPRsExtConfig.title, 257 | text: `Graphic does not include participants that submitted no accepted PR/MRs (${number.commas(results.totalUsersNotEngaged)} (${number.percentage(results.totalUsersNotEngaged / results.totalUsers)})).`, 258 | fontColor: chart.colors.textBox, 259 | fontSize: 16, 260 | padding: 15, 261 | margin: 0, 262 | cornerRadius: 5, 263 | verticalAlign: 'bottom', 264 | horizontalAlign: 'center', 265 | backgroundColor: chart.colors.backgroundBox, 266 | }]; 267 | await chart.save( 268 | path.join(__dirname, '../../generated/users_by_prs_extended_column.png'), 269 | await chart.render(totalUsersByPRsExtConfig), 270 | { width: 200, x: 1250, y: 220 }, 271 | ); 272 | 273 | results.totalUsersByAcceptedPRsCapped = cappedAcceptedUserPRs(data, 5); 274 | 275 | const totalUsersByPRsConfig = chart.config(1000, 1000, [{ 276 | type: 'column', 277 | dataPoints: results.totalUsersByAcceptedPRsCapped.map(([ prs, users ]) => ({ 278 | y: users, 279 | color: Number.parseInt(prs) > 4 280 | ? chart.colors.highlightNeutral 281 | : Number.parseInt(prs) === 4 282 | ? chart.colors.highlightPositive 283 | : chart.colors.highlightNegative, 284 | label: `${prs}${prs === 5 ? '+' : ''} PR/MR${prs === 1 ? '' : 's'}`, 285 | })), 286 | }]); 287 | totalUsersByPRsConfig.axisX = { 288 | ...totalUsersByPRsConfig.axisX, 289 | labelFontSize: 24, 290 | }; 291 | totalUsersByPRsConfig.axisY = { 292 | ...totalUsersByPRsConfig.axisY, 293 | labelFontSize: 24, 294 | }; 295 | totalUsersByPRsConfig.title = { 296 | ...totalUsersByPRsConfig.title, 297 | text: 'Users: Accepted Pull/Merge Requests', 298 | fontSize: 42, 299 | padding: 5, 300 | margin: 40, 301 | }; 302 | totalUsersByPRsConfig.subtitles = [{ 303 | ...totalUsersByPRsConfig.title, 304 | text: `Graphic does not include participants that submitted no accepted PR/MRs (${number.commas(results.totalUsersNotEngaged)} (${number.percentage(results.totalUsersNotEngaged / results.totalUsers)})).`, 305 | fontColor: chart.colors.textBox, 306 | fontSize: 16, 307 | padding: 15, 308 | margin: 0, 309 | cornerRadius: 5, 310 | verticalAlign: 'bottom', 311 | horizontalAlign: 'center', 312 | backgroundColor: chart.colors.backgroundBox, 313 | }]; 314 | await chart.save( 315 | path.join(__dirname, '../../generated/users_by_prs_column.png'), 316 | await chart.render(totalUsersByPRsConfig), 317 | { width: 200, x: 500, y: 180 }, 318 | ); 319 | 320 | // Registrations by country 321 | results.totalUsersByCountry = Object.entries(data.users.metadata['demographic-country'].values) 322 | .filter(([ country ]) => country !== '') 323 | .map(([ country, { count } ]) => [ getName(country) || country, count ]) 324 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 325 | results.totalUsersNoCountry = data.users.metadata['demographic-country'].values['']?.count || 0; 326 | 327 | log(''); 328 | log(`Top countries by registrations: ${number.commas(results.totalUsersByCountry.length)} countries`); 329 | results.totalUsersByCountry.slice(0, 25).forEach(([ country, count ], i) => { 330 | log(`${i + 1}. ${country}: ${number.commas(count)} (${number.percentage(count / results.totalUsers)})`); 331 | }); 332 | if (results.totalUsersByCountry.length > 25) 333 | log(`+ ${number.commas(results.totalUsersByCountry.length - 25)} more...`); 334 | log(`${number.commas(results.totalUsersNoCountry)} (${number.percentage(results.totalUsersNoCountry / results.totalUsers)}) users did not specify their country`); 335 | 336 | const registrationsCaption = `In total, at least ${number.commas(results.totalUsersByCountry.filter(([ country ]) => country !== '').length)} countries were represented by users who registered to participate in Hacktoberfest.`; 337 | await usersTopChart( 338 | results.totalUsersByCountry, 339 | results.totalUsers, 340 | 'Registered Users: Top Countries', 341 | 'users_registrations_top_countries_bar', 342 | 5000, 343 | registrationsCaption, 344 | `Graphic does not include users that did not specify their country, ${number.commas(results.totalUsersNoCountry)} (${number.percentage(results.totalUsersNoCountry / results.totalUsers)}).`, 345 | ); 346 | await usersTopChart( 347 | results.totalUsersByCountry.filter(([ country ]) => !['United States', 'India'].includes(country)), 348 | results.totalUsers, 349 | 'Registered Users: Top Countries', 350 | 'users_registrations_top_countries_bar_excl', 351 | 200, 352 | registrationsCaption, 353 | `Graphic does not include India (${number.percentage(results.totalUsersByCountry.find(([ country ]) => country === 'India')[1] / results.totalUsers)}), the United States (${number.percentage(results.totalUsersByCountry.find(([ country ]) => country === 'United States')[1] / results.totalUsers)}), and users that did not specify their country (${number.percentage(results.totalUsersNoCountry / results.totalUsers)}).`, 354 | ); 355 | 356 | // Completions by country 357 | results.totalUsersCompletedByCountry = Object.entries(data.users.metadata['demographic-country'].values) 358 | .filter(([ country, { states } ]) => country !== '' && states.contributor) 359 | .map(([ country, { states } ]) => [ getName(country) || country, states.contributor ]) 360 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 361 | results.totalUsersCompletedNoCountry = data.users.metadata['demographic-country'].values['']?.states?.contributor || 0; 362 | 363 | log(''); 364 | log(`Top countries by completions: ${number.commas(results.totalUsersCompletedByCountry.length)} countries`); 365 | results.totalUsersCompletedByCountry.slice(0, 25).forEach(([ country, count ], i) => { 366 | log(`${i + 1}. ${country}: ${number.commas(count)} (${number.percentage(count / results.totalUsersCompleted)})`); 367 | }); 368 | if (results.totalUsersCompletedByCountry.length > 25) 369 | log(`+ ${number.commas(results.totalUsersCompletedByCountry.length - 25)} more...`); 370 | log(`${number.commas(results.totalUsersCompletedNoCountry)} (${number.percentage(results.totalUsersCompletedNoCountry / results.totalUsersCompleted)}) users did not specify their country`); 371 | 372 | 373 | const completionsCaption = `In total, at least ${number.commas(results.totalUsersCompletedByCountry.length)} countries were represented by users who completed and won Hacktoberfest.`; 374 | await usersTopChart( 375 | results.totalUsersCompletedByCountry, 376 | results.totalUsersCompleted, 377 | 'Completed Users: Top Countries', 378 | 'users_completions_top_countries_bar', 379 | 1000, 380 | completionsCaption, 381 | `Graphic does not include users that did not specify their country, ${number.commas(results.totalUsersCompletedNoCountry)} (${number.percentage(results.totalUsersCompletedNoCountry / results.totalUsersCompleted)}).`, 382 | ); 383 | await usersTopChart( 384 | results.totalUsersCompletedByCountry.filter(([ country ]) => !['United States', 'India'].includes(country)), 385 | results.totalUsersCompleted, 386 | 'Completed Users: Top Countries', 387 | 'users_completions_top_countries_bar_excl', 388 | 50, 389 | completionsCaption, 390 | `Graphic does not include India (${number.percentage(results.totalUsersCompletedByCountry.find(([ country ]) => country === 'India')[1] / results.totalUsersCompleted)}), the United States (${number.percentage(results.totalUsersCompletedByCountry.find(([ country ]) => country === 'United States')[1] / results.totalUsersCompleted)}), and users that did not specify their country (${number.percentage(results.totalUsersCompletedNoCountry / results.totalUsersCompleted)}).`, 391 | ); 392 | 393 | // Breaking down users by day and by state 394 | results.totalUsersByStateByDay = Object.keys(data.users.states.all.states) 395 | .reduce((states, state) => ({ 396 | ...states, 397 | [state]: getDateArray(new Date(`${data.year}-09-26`), new Date(`${data.year}-11-01`)) 398 | .reduce((obj, date) => { 399 | const day = date.toISOString().split('T')[0]; 400 | return { 401 | ...obj, 402 | [day]: { 403 | date, 404 | count: data.users.states.daily?.[day]?.states?.[state] || 0, 405 | }, 406 | }; 407 | }, {}), 408 | }), {}); 409 | 410 | const totalUsersByStateByDayOrder = ['contributor', 'first-accepted', 'registered', 'disqualified']; 411 | const totalUsersByStateByDayColors = { 412 | disqualified: chart.colors.highlightNegative, 413 | registered: chart.colors.highlightNeutralAlt, 414 | 'first-accepted': chart.colors.highlightNeutral, 415 | contributor: chart.colors.highlightPositive, 416 | }; 417 | 418 | const totalUsersByStateByDayConfig = chart.config(2500, 1000, Object.entries(results.totalUsersByStateByDay) 419 | .filter(([ state ]) => totalUsersByStateByDayOrder.includes(state)) 420 | .sort(([ a ], [ b ]) => totalUsersByStateByDayOrder.indexOf(b) - totalUsersByStateByDayOrder.indexOf(a)) 421 | .map(([ state, daily ]) => ({ 422 | type: 'stackedArea', 423 | name: state, 424 | showInLegend: true, 425 | dataPoints: Object.entries(daily).map(([ day, { date, count } ]) => ({ 426 | x: date, 427 | y: state === 'registered' 428 | ? count - (results.totalUsersByStateByDay['first-accepted']?.[day]?.count || 0) 429 | : (state === 'first-accepted' 430 | ? count - (results.totalUsersByStateByDay.contributor?.[day]?.count || 0) 431 | : count), 432 | })), 433 | lineThickness: 3, 434 | color: totalUsersByStateByDayColors[state] || chart.colors.highlightNeutral, 435 | }))); 436 | totalUsersByStateByDayConfig.axisX = { 437 | ...totalUsersByStateByDayConfig.axisX, 438 | labelFontSize: 34, 439 | interval: 1, 440 | intervalType: 'week', 441 | title: 'User Registered At', 442 | titleFontSize: 24, 443 | titleFontWeight: 400, 444 | }; 445 | totalUsersByStateByDayConfig.axisY = { 446 | ...totalUsersByStateByDayConfig.axisY, 447 | labelFontSize: 34, 448 | interval: 1000, 449 | }; 450 | totalUsersByStateByDayConfig.title = { 451 | ...totalUsersByStateByDayConfig.title, 452 | text: 'All Users: Breakdown by State', 453 | fontSize: 48, 454 | padding: 5, 455 | margin: 15, 456 | }; 457 | totalUsersByStateByDayConfig.subtitles = [ 458 | { 459 | text: '_', 460 | fontColor: chart.colors.text, 461 | fontSize: 16, 462 | verticalAlign: 'bottom', 463 | horizontalAlign: 'center', 464 | }, 465 | ]; 466 | 467 | await chart.save( 468 | path.join(__dirname, '../../generated/users_by_state_stacked.png'), 469 | await chart.render(totalUsersByStateByDayConfig), 470 | { width: 200, x: 1250, y: 220 }, 471 | ); 472 | 473 | const providerMap = { 474 | github: 'GitHub', 475 | gitlab: 'GitLab', 476 | }; 477 | 478 | // Provider accounts registered 479 | results.totalUsersByProvider = Object.entries(data.users.providers) 480 | .map(([ provider, { count } ]) => ([ 481 | providerMap[provider] || provider, 482 | count, 483 | ])) 484 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 485 | log(''); 486 | log('Registered users by provider:'); 487 | log('(Users were able to link one, or both, of the supported providers to their Hacktoberfest account)'); 488 | for (const [ provider, count ] of results.totalUsersByProvider) { 489 | log(` ${provider}: ${number.commas(count)} (${number.percentage(count / results.totalUsers)})`); 490 | } 491 | 492 | await usersTopChart( 493 | results.totalUsersByProvider, 494 | results.totalUsers, 495 | 'Registered Users: Linked Providers', 496 | 'users_registrations_linked_providers_bar', 497 | 10000, 498 | 'Users were able to link one, or both, of the supported providers to their Hacktoberfest account.', 499 | ); 500 | 501 | // Provider accounts engaged 502 | results.totalUsersEngagedByProvider = Object.entries(data.users.providers) 503 | .map(([ provider, { states } ]) => ([ 504 | providerMap[provider] || provider, 505 | (states['first-accepted'] || 0) - (states.contributor || 0), 506 | ])) 507 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 508 | log(''); 509 | log('Engaged (1-3 PR/MRs) users by provider:'); 510 | log('(Users were able to link one, or both, of the supported providers to their Hacktoberfest account)'); 511 | for (const [ provider, count ] of results.totalUsersEngagedByProvider) { 512 | log(` ${provider}: ${number.commas(count)} (${number.percentage(count / results.totalUsersEngaged)})`); 513 | } 514 | 515 | await usersTopChart( 516 | results.totalUsersEngagedByProvider, 517 | results.totalUsersEngaged, 518 | 'Engaged Users: Linked Providers', 519 | 'users_engaged_linked_providers_bar', 520 | 1000, 521 | 'Users were able to link one, or both, of the supported providers to their Hacktoberfest account.', 522 | ); 523 | 524 | // Provider accounts completed 525 | results.totalUsersCompletedByProvider = Object.entries(data.users.providers) 526 | .map(([ provider, { states } ]) => ([ 527 | providerMap[provider] || provider, 528 | states.contributor || 0, 529 | ])) 530 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 531 | log(''); 532 | log('Completed (4+ PR/MRs) users by provider:'); 533 | log('(Users were able to link one, or both, of the supported providers to their Hacktoberfest account)'); 534 | for (const [ provider, count ] of results.totalUsersCompletedByProvider) { 535 | log(` ${provider}: ${number.commas(count)} (${number.percentage(count / results.totalUsersCompleted)})`); 536 | } 537 | 538 | await usersTopChart( 539 | results.totalUsersCompletedByProvider, 540 | results.totalUsersCompleted, 541 | 'Completed Users: Linked Providers', 542 | 'users_completions_linked_providers_bar', 543 | 2500, 544 | 'Users were able to link one, or both, of the supported providers to their Hacktoberfest account.', 545 | ); 546 | 547 | const experienceMap = { 548 | 'stage-newbie': 'Newbie', 549 | 'stage-familiar': 'Familiar', 550 | 'stage-experienced': 'Experienced', 551 | }; 552 | 553 | // Experience level 554 | results.totalUsersByExperience = Object.entries(data.users.metadata) 555 | .filter(([ level ]) => level.startsWith('stage-')) 556 | .map(([ level, { values } ]) => ([ 557 | experienceMap[level] || level, 558 | values.true?.count || 0, 559 | ])) 560 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 561 | results.totalUsersNoExperience = results.totalUsers - results.totalUsersByExperience.reduce((sum, [ , count ]) => sum + count, 0); 562 | 563 | log(''); 564 | log('Registered users by experience:'); 565 | log('(Users were able to optionally self-identify their experience level when registering)'); 566 | for (const [ level, count ] of results.totalUsersByExperience) { 567 | log(` ${level}: ${number.commas(count)} (${number.percentage(count / results.totalUsers)})`); 568 | } 569 | log(`${number.commas(results.totalUsersNoExperience)} (${number.percentage(results.totalUsersNoExperience / results.totalUsers)}) users did not specify their experience level`); 570 | 571 | await usersTopChart( 572 | results.totalUsersByExperience, 573 | results.totalUsers, 574 | 'Registered Users: Experience Level', 575 | 'users_registrations_experience_level_bar', 576 | 10000, 577 | null, 578 | `Graphic does not include users that did not specify their experience level, ${number.commas(results.totalUsersNoExperience)} (${number.percentage(results.totalUsersNoExperience / results.totalUsers)}).`, 579 | ); 580 | 581 | // Experience level completed 582 | results.totalUsersCompletedByExperience = Object.entries(data.users.metadata) 583 | .filter(([ level ]) => level.startsWith('stage-')) 584 | .map(([ level, { values } ]) => ([ 585 | experienceMap[level] || level, 586 | values.true?.states?.contributor || 0, 587 | ])) 588 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 589 | results.totalUsersCompletedNoExperience = results.totalUsersCompleted - results.totalUsersCompletedByExperience.reduce((sum, [ , count ]) => sum + count, 0); 590 | 591 | log(''); 592 | log('Completed (4+ PR/MRs) users by experience:'); 593 | log('(Users were able to optionally self-identify their experience level when registering)'); 594 | for (const [ level, count ] of results.totalUsersCompletedByExperience) { 595 | log(` ${level}: ${number.commas(count)} (${number.percentage(count / results.totalUsersCompleted)})`); 596 | } 597 | log(`${number.commas(results.totalUsersCompletedNoExperience)} (${number.percentage(results.totalUsersCompletedNoExperience / results.totalUsersCompleted)}) users did not specify their experience level`); 598 | 599 | await usersTopChart( 600 | results.totalUsersCompletedByExperience, 601 | results.totalUsersCompleted, 602 | 'Completed Users: Experience Level', 603 | 'users_completions_experience_level_bar', 604 | 1000, 605 | null, 606 | `Graphic does not include users that did not specify their experience level, ${number.commas(results.totalUsersCompletedNoExperience)} (${number.percentage(results.totalUsersCompletedNoExperience / results.totalUsersCompleted)}).`, 607 | ); 608 | 609 | const typeMap = { 610 | 'type-code': 'Code', 611 | 'type-non-code': 'Non-code', 612 | }; 613 | 614 | // Contribution type 615 | results.totalUsersByContribution = Object.entries(data.users.metadata) 616 | .filter(([ type ]) => type.startsWith('type-')) 617 | .map(([ type, { values } ]) => ([ 618 | typeMap[type] || type, 619 | values.true?.count || 0, 620 | ])) 621 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 622 | 623 | log(''); 624 | log('Registered users by intended contribution type:'); 625 | log('(Users were able to optionally self-identify what type of contribution(s) they intended to make when registering)'); 626 | log('(Users were able to select multiple options)'); 627 | for (const [ type, count ] of results.totalUsersByContribution) { 628 | log(` ${type}: ${number.commas(count)} (${number.percentage(count / results.totalUsers)})`); 629 | } 630 | 631 | await usersTopChart( 632 | results.totalUsersByContribution, 633 | results.totalUsers, 634 | 'Registered Users: Contribution Type', 635 | 'users_registrations_contribution_type_bar', 636 | 10000, 637 | 'Users were able to optionally select one, or more, intended contribution types when registering.', 638 | ); 639 | 640 | // Contribution type completed 641 | results.totalUsersCompletedByContribution = Object.entries(data.users.metadata) 642 | .filter(([ type ]) => type.startsWith('type-')) 643 | .map(([ type, { values } ]) => ([ 644 | typeMap[type] || type, 645 | values.true?.states?.contributor || 0, 646 | ])) 647 | .sort((a, b) => a[1] < b[1] ? 1 : -1); 648 | 649 | log(''); 650 | log('Completed (4+ PR/MRs) users by intended contribution type:'); 651 | log('(Users were able to optionally self-identify what type of contribution(s) they intended to make when registering)'); 652 | log('(Users were able to select multiple options)'); 653 | for (const [ type, count ] of results.totalUsersCompletedByContribution) { 654 | log(` ${type}: ${number.commas(count)} (${number.percentage(count / results.totalUsersCompleted)})`); 655 | } 656 | 657 | await usersTopChart( 658 | results.totalUsersCompletedByContribution, 659 | results.totalUsersCompleted, 660 | 'Completed Users: Contribution Type', 661 | 'users_completions_contribution_type_bar', 662 | 1000, 663 | 'Users were able to optionally select one, or more, intended contribution types when registering.', 664 | ); 665 | 666 | // Students 667 | results.totalUsersStudents = data.users.metadata['demographic-student'].values.true?.count || 0; 668 | results.totalUsersNotStudents = data.users.metadata['demographic-student'].values.false?.count || 0; 669 | results.totalUsersMissingStudents = results.totalUsers - results.totalUsersStudents - results.totalUsersNotStudents; 670 | 671 | log(''); 672 | log('Registered users by student status:'); 673 | log('(Users were able to optionally self-identify if they\'re a student when registering)'); 674 | log(` Yes (enrolled student): ${number.commas(results.totalUsersStudents)} (${number.percentage(results.totalUsersStudents / results.totalUsers)})`); 675 | log(` No (not a student): ${number.commas(results.totalUsersNotStudents)} (${number.percentage(results.totalUsersNotStudents / results.totalUsers)})`); 676 | log(`${number.commas(results.totalUsersMissingStudents)} (${number.percentage(results.totalUsersMissingStudents / results.totalUsers)}) users did not specify their enrolment status`); 677 | 678 | await usersTopChart( 679 | [ 680 | ['Enrolled Student', results.totalUsersStudents], 681 | ['Not a Student', results.totalUsersNotStudents], 682 | ['Not Given', results.totalUsersMissingStudents], 683 | ], 684 | results.totalUsers, 685 | 'Registered Users: Student Status', 686 | 'users_registrations_student_status_bar', 687 | 10000, 688 | 'Users were able to optionally indicate if they were an enrolled student, or not, when registering.', 689 | ); 690 | 691 | // AI/ML 692 | results.totalUsersAIML = data.users.metadata['demographic-ai-ml'].values.true?.count || 0; 693 | results.totalUsersNotAIML = data.users.metadata['demographic-ai-ml'].values.false?.count || 0; 694 | results.totalUsersMissingAIML = results.totalUsers - results.totalUsersAIML - results.totalUsersNotAIML; 695 | 696 | log(''); 697 | log('Registered users by AI/ML interest:'); 698 | log('(Users were able to optionally self-identify if they\'re interested in AI/ML when registering)'); 699 | log(` Interested: ${number.commas(results.totalUsersAIML)} (${number.percentage(results.totalUsersAIML / results.totalUsers)})`); 700 | log(` Not Interested: ${number.commas(results.totalUsersNotAIML)} (${number.percentage(results.totalUsersNotAIML / results.totalUsers)})`); 701 | log(`${number.commas(results.totalUsersMissingAIML)} (${number.percentage(results.totalUsersMissingAIML / results.totalUsers)}) users did not specify their interest in AI/ML`); 702 | 703 | await usersTopChart( 704 | [ 705 | ['Interested', results.totalUsersAIML], 706 | ['Not Interested', results.totalUsersNotAIML], 707 | ['Not Given', results.totalUsersMissingAIML], 708 | ], 709 | results.totalUsers, 710 | 'Registered Users: AI/ML Interest', 711 | 'users_registrations_ai_ml_interest_bar', 712 | 10000, 713 | 'Users were able to optionally indicate if they were interested in AI/ML projects, or not, when registering.', 714 | ); 715 | 716 | return results; 717 | }; 718 | -------------------------------------------------------------------------------- /src/stats/index.js: -------------------------------------------------------------------------------- 1 | const statsGenerators = [ 2 | 'readme', 3 | 'PRs', 4 | 'Users', 5 | 'Repos', 6 | ]; 7 | 8 | module.exports = async (data, log) => { 9 | const results = {}; 10 | for (const generator of statsGenerators) { 11 | results[generator] = await require(`./${generator}`)(data, log); 12 | } 13 | return results; 14 | }; 15 | -------------------------------------------------------------------------------- /src/stats/readme.js: -------------------------------------------------------------------------------- 1 | const number = require('../helpers/number'); 2 | 3 | module.exports = async (data, log) => { 4 | /*************** 5 | * Readme Stats 6 | ***************/ 7 | log('\n\n----\nReadme Stats\n----'); 8 | const results = {}; 9 | 10 | results.year = data.year; 11 | results.blog = 'www.digitalocean.com/blog/10th-anniversary-hacktoberfest-recap'; 12 | 13 | results.registeredUsers = data.users.states.all.count; 14 | results.engagedUsers = data.users.states.all.states['first-accepted'] - data.users.states.all.states.contributor; 15 | results.completedUsers = data.users.states.all.states.contributor; 16 | results.acceptedPRs = data.pull_requests.states.all.states.accepted; 17 | results.activeRepos = data.repositories.pull_requests.accepted.count; 18 | results.countriesRegistered = Object.keys(data.users.metadata['demographic-country'].values).filter(country => country !== '').length; 19 | results.countriesCompleted = Object.entries(data.users.metadata['demographic-country'].values).filter(([ country, { states } ]) => country !== '' && states.contributor).length; 20 | 21 | const dailyPRStates = data.pull_requests.states.daily; 22 | results.mostPRsDay = Object.keys(dailyPRStates).sort((a, b) => (dailyPRStates[b].states.accepted || 0) - (dailyPRStates[a].states.accepted || 0))[0]; 23 | results.mostPRsDayPercentage = dailyPRStates[results.mostPRsDay].states.accepted / results.acceptedPRs; 24 | 25 | const allPRLanguages = data.pull_requests.languages.all.languages; 26 | results.mostCommonLanguageInPRs = Object.keys(allPRLanguages).sort((a, b) => (allPRLanguages[b].states.accepted || 0) - (allPRLanguages[a].states.accepted || 0))[0]; 27 | results.mostCommonLanguageInPRsPercentage = allPRLanguages[results.mostCommonLanguageInPRs].states.accepted / results.acceptedPRs; 28 | 29 | log(''); 30 | log(`Registered users: ${number.commas(results.registeredUsers)}`); 31 | log(`Completed users: ${number.commas(results.completedUsers)}`); 32 | log(`Accepted PR/MRs: ${number.commas(results.acceptedPRs)}`); 33 | log(`Active repositories (1+ accepted PR/MRs): ${number.commas(results.activeRepos)}`); 34 | log(`Countries represented by registered users: ${number.commas(results.countriesRegistered)}`); 35 | log(`Countries represented by completed users: ${number.commas(results.countriesCompleted)}`); 36 | log(`Day with most accepted PR/MRs submitted: ${results.mostPRsDay} (${number.percentage(results.mostPRsDayPercentage)})`); 37 | log(`Most common repository language in accepted PR/MRs: ${results.mostCommonLanguageInPRs} (${number.percentage(results.mostCommonLanguageInPRsPercentage)})`); 38 | 39 | results.americaRegisteredUsers = data.users.metadata['demographic-country'].values['us']?.count || 0; 40 | results.americaCompletedUsers = data.users.metadata['demographic-country'].values['us']?.states?.contributor || 0; 41 | results.indiaRegisteredUsers = data.users.metadata['demographic-country'].values['in']?.count || 0; 42 | results.indiaCompletedUsers = data.users.metadata['demographic-country'].values['in']?.states?.contributor || 0; 43 | 44 | log(''); 45 | log('Region-specific:'); 46 | log(` Registered users in the US: ${number.commas(results.americaRegisteredUsers)}`); 47 | log(` Completed users in the US: ${number.commas(results.americaCompletedUsers)}`); 48 | log(` Registered users in India: ${number.commas(results.indiaRegisteredUsers)}`); 49 | log(` Completed users in India: ${number.commas(results.indiaCompletedUsers)}`); 50 | 51 | return results; 52 | }; 53 | --------------------------------------------------------------------------------