├── .nvmrc ├── .gitignore ├── src ├── helpers │ ├── hf.png │ ├── log.js │ ├── linguist.js │ ├── number.js │ ├── date.js │ ├── color.js │ └── chart.js ├── stats │ ├── index.js │ ├── readme.js │ ├── Repos.js │ ├── PRs.js │ └── Users.js └── index.js ├── generated ├── prs_by_day_bar.png ├── prs_by_state_doughnut.png ├── prs_by_state_stacked.png ├── repos_by_license_bar.png ├── users_by_prs_column.png ├── prs_by_language_spline.png ├── repos_reported_doughnut.png ├── users_by_state_doughnut.png ├── users_by_state_stacked.png ├── prs_accepted_by_merged_bar.png ├── prs_by_language_doughnut.png ├── repos_by_language_doughnut.png ├── prs_accepted_by_approval_bar.png ├── users_by_prs_extended_column.png ├── users_completions_top_countries_bar.png ├── users_engaged_linked_providers_bar.png ├── users_completions_experience_level_bar.png ├── users_completions_linked_providers_bar.png ├── users_registrations_ai_ml_interest_bar.png ├── users_registrations_student_status_bar.png ├── users_registrations_top_countries_bar.png ├── users_completions_contribution_type_bar.png ├── users_completions_top_countries_bar_excl.png ├── users_registrations_contribution_type_bar.png ├── users_registrations_experience_level_bar.png ├── users_registrations_linked_providers_bar.png ├── users_registrations_top_countries_bar_excl.png └── stats.txt ├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── package.json ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── README.dot.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | 4 | # Data not ready for public yet 5 | data/ 6 | -------------------------------------------------------------------------------- /src/helpers/hf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/src/helpers/hf.png -------------------------------------------------------------------------------- /generated/prs_by_day_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_by_day_bar.png -------------------------------------------------------------------------------- /generated/prs_by_state_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_by_state_doughnut.png -------------------------------------------------------------------------------- /generated/prs_by_state_stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_by_state_stacked.png -------------------------------------------------------------------------------- /generated/repos_by_license_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/repos_by_license_bar.png -------------------------------------------------------------------------------- /generated/users_by_prs_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_by_prs_column.png -------------------------------------------------------------------------------- /generated/prs_by_language_spline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_by_language_spline.png -------------------------------------------------------------------------------- /generated/repos_reported_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/repos_reported_doughnut.png -------------------------------------------------------------------------------- /generated/users_by_state_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_by_state_doughnut.png -------------------------------------------------------------------------------- /generated/users_by_state_stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_by_state_stacked.png -------------------------------------------------------------------------------- /generated/prs_accepted_by_merged_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_accepted_by_merged_bar.png -------------------------------------------------------------------------------- /generated/prs_by_language_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_by_language_doughnut.png -------------------------------------------------------------------------------- /generated/repos_by_language_doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/repos_by_language_doughnut.png -------------------------------------------------------------------------------- /generated/prs_accepted_by_approval_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/prs_accepted_by_approval_bar.png -------------------------------------------------------------------------------- /generated/users_by_prs_extended_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_by_prs_extended_column.png -------------------------------------------------------------------------------- /generated/users_completions_top_countries_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_completions_top_countries_bar.png -------------------------------------------------------------------------------- /generated/users_engaged_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_engaged_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_completions_experience_level_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_completions_experience_level_bar.png -------------------------------------------------------------------------------- /generated/users_completions_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_completions_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_ai_ml_interest_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_ai_ml_interest_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_student_status_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_student_status_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_top_countries_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_top_countries_bar.png -------------------------------------------------------------------------------- /generated/users_completions_contribution_type_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_completions_contribution_type_bar.png -------------------------------------------------------------------------------- /generated/users_completions_top_countries_bar_excl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_completions_top_countries_bar_excl.png -------------------------------------------------------------------------------- /generated/users_registrations_contribution_type_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_contribution_type_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_experience_level_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_experience_level_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_linked_providers_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_linked_providers_bar.png -------------------------------------------------------------------------------- /generated/users_registrations_top_countries_bar_excl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MattIPv4/hacktoberfest-data/HEAD/generated/users_registrations_top_countries_bar_excl.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
64 |
65 |
66 |
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 |
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 |
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 | 
225 |
226 | ## Diving in: Pull/Merge Requests
227 |
228 |
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 |
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 | 
333 |
334 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
79 |
80 |
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 |
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 |
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 | 
268 |
269 | ## Diving in: Pull/Merge Requests
270 |
271 |
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 |
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 | 
384 |
385 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------